OrbitvuOrbitvu

APIを使用したVIEWERのカスタムUI - チュートリアル

Orbitvu VIEWER(SUN、Free、Infinity360の各バージョン)はViewerのUIをカスタマイズできるAPIをサポートしています。このチュートリアルでは、Viewerのボタンを以下のように再実装します。

Orbitvu VIEWER(SUN、Free、Infinity360の各バージョン)はViewerのUIをカスタマイズできるAPIをサポートしています。このチュートリアルでは、Viewerのボタンを以下のように再実装します。

このチュートリアルは、Web開発(HTML、JavaScript、CSS)についての基本的な知識をお持ちの方を対象としています。

index.html

<!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="robots" content="noindex, nofollow">
    <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">
    <!-- Load hammer.js -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>
    <!-- Load Screenfull library -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/screenfull.js/3.3.3/screenfull.min.js"></script>
    <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>
    <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_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');
                api_obj.addCallback(
                    'mode_changed_' + this.uid,
                    'mode_changed');

                // 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();
            };
            window['mode_changed_' + this.uid] = (mode) => {
                this.state.mode = mode;
                this.refresh_drag_rotate_btn();
            };
        }

        setup_button_handlers() {
            // button event handlers
            let btn_autorotate = document.getElementById('ui-autorotate-' + this.uid);
            let btn_zoom_in = document.getElementById('ui-zoom-in-' + this.uid);
            let btn_zoom_out = document.getElementById('ui-zoom-out-' + this.uid);
            let btn_drag_rotate = document.getElementById('ui-drag-rotate-' + this.uid);
            let btn_fullscreen = document.getElementById('ui-fullscreen-' + this.uid);
            let viewer_container = document.getElementById('viewer-container-' + 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'})
                }
            });

            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();
            });

            // 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'});
                    }
                }
            });

            // 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);
                }
            });
        }

        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>';
            }
        }

        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');
    document.addEventListener("DOMContentLoaded", (event) => {
        vui.setup_callbacks();
        vui.setup_button_handlers()
    });
    </script>
</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&style=4&doubletap_mode=zoom&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 -->

        <div id="custom-ui-WZarBpyJ7hHX77zaF49Rb9" class="custom-ui" style="display: none;">
            <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>

ページレイアウト

まず、ページと独自ボタンのレイアウトを定義するところから始めます。SUNサーバー上のデモプレゼンテーションを使用します。

<!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/share/WZarBpyJ7hHX77zaF49Rb9/20838/360/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>

上記のコードには、注目すべき点がいくつかあります。

10行目: ボタン用のアイコンを見栄えよくするため、Font Awesomeライブラリを読み込んでいます。

16〜23行目: Orbitvu SUNからコピーした埋め込みコードです。

15、25、27〜31行目: これらの要素のidは、SUNのプレゼンテーションのuid(この例ではWZarBpyJ7hHX77zaF49Rb9)を使って一意になるよう設定されています。同じページに複数のプレゼンテーションを配置する場合に役立ちます。

トップレベルに<div>要素が2つある理由を疑問に思うかもしれません。

<div class="viewer-wrapper">
    <div class="viewer-container" id="viewer-container-WZarBpyJ7hHX77zaF49Rb9">
        ...
    </div>
</div>

これは、一部の古いブラウザでFullscreenボタンを正しく動作させるために必要です。詳細は、Fullscreenボタンのサポートを追加する際に後述します。

Orbitvu SUNではなく自分のサーバーでホストしているプレゼンテーションを使用する場合は、こちらのドキュメントの説明に従ってプレゼンテーションを埋め込む必要があります。

ここで、目的のページレイアウトになるようスタイルを追加します。

<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>

これで、ページは以下のようになります。別ウィンドウで表示してソースを確認するには、こちらをクリックしてください。

index_1.html
<!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="robots" content="noindex, nofollow">
    <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">
    <!-- Load hammer.js -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>
    <!-- Load Screenfull library -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/screenfull.js/3.3.3/screenfull.min.js"></script>
    <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>
</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>

Viewer APIへのアクセス

ページレイアウトが定義できたので、次はViewer APIへの接続です。そのためにはまず、Viewer APIが初期化されるとすぐにAPIオブジェクトを受け取るコールバックを定義する必要があります。これはviewer_api_initパラメータで行えます。まずはコールバック関数を書いてみましょう。

<!-- Put this before the SUN embed code -->
<script type="text/javascript">
    window['api_init'] = (api_obj) => {
        console.log('api initialized');
    };
</script>

最初は単に「api initialized」というテキストを出力するだけです。

次に、このコールバックをVIEWERに接続します。Orbitvu SUNの埋め込みコードを使用しているので、クエリ文字列にviewer_api_initを追加するだけです。例:&viewer_api_init=api_init(以下のコードのハイライト部分を参照)

<!-- Embed code from Orbitvu SUN starts here -->
<script type="text/javascript"
        async
        src="//orbitvu.co/share/WZarBpyJ7hHX77zaF49Rb9/20838/360/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 -->

これで、「api initialized」というテキストがコンソールに出力されるはずです。動作しない場合は、コンソールにエラーが出ていないか、SUNを使用していない場合はご使用のVIEWERがAPIをサポートしているかを確認してください。

コードの再利用性を高める

先ほど定義したコールバック関数には、小さな問題が1つあります。それは、api_initというグローバル関数として定義されているため、ページに別の360°プレゼンテーションを追加する場合、正しいViewerインスタンスに接続できるよう、コールバック名を一意にする必要があるという点です。

そこで、プレゼンテーションのuidであるWZarBpyJ7hHX77zaF49Rb9をコールバック名の一部として使用します。また、コードを簡単に再利用できるよう、クラスに組み込みます。

これにより、コールバックの識別子は「api_init_WZarBpyJ7hHX77zaF49Rb9」となります。そしてVIEWERの埋め込みコードは以下のようになります。

<!-- 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 -->

再利用可能なJavaScriptクラスは、次のように定義します。

<!-- Put this before the SUN embed code -->
<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>

ここではES6のクラスとアロー関数を使用しています。古いブラウザをサポートする必要がある場合は、Babelなどのコンパイラを使って後方互換性を持たせてください。

VIEWER APIの使用

これで、APIオブジェクトを使って何か役立つことができるようになりました。お気づきかもしれませんが(特にネットワークが遅い場合や、開発者ツールでネットワーク速度を制限している場合)、ページが読み込まれるとカスタムボタンはすぐに表示される一方で、VIEWERの初期化は少し遅れて行われます。

VIEWERが正しく初期化される前にボタンを使うことはできないため、これは望ましくありません。そこで、ボタンは最初は非表示にしておき、VIEWERの初期化後に表示したいと考えます。これは、partial_loadが有効な場合にVIEWERが少なくとも4フレームを読み込んだ時点で発火するpartially_initializedイベントを使用することで簡単に実現できます。

すべてのフレームが読み込まれたときに呼び出されるviewer_initializedコールバックもあります。

まず、ボタンのコンテナにdisplay: noneを追加します。

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

次に、VIEWERの初期化時にボタンを表示するイベントハンドラーを追加します。

<!-- Put this before the SUN embed code -->
<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>

14行目: partially_initializedイベントにコールバックをバインドしています。

19行目: コールバックの定義です。

20行目: APIオブジェクトを変数に格納しています。

24行目: display スタイルをflexに変更してボタンパネルを表示しています。

結果として、ページは以下のようになります。新しいウィンドウで開いてソースコードを確認するには、こちらをクリックしてください。

index_2.html
<!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="robots" content="noindex, nofollow">
    <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">
    <!-- Load hammer.js -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>
    <!-- Load Screenfull library -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/screenfull.js/3.3.3/screenfull.min.js"></script>
    <style type="text/css">
        body {
          margin: 0;
        }

        @media only screen and (min-width: 500px) {
          .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;
          /* necessary to properly position custom-ui inside this element */
          position: relative;
          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;
          z-index: 99;
        }

        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 0px 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>
    <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');
    document.addEventListener("DOMContentLoaded", (event) => {
        vui.setup_callbacks();
    });
    </script>
</head>

<body>
<div class="viewer-wrapper">
    <div class="viewer-container" id="viewer-container-WZarBpyJ7hHX77zaF49Rb9">

        <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>

        <div id="custom-ui-WZarBpyJ7hHX77zaF49Rb9" class="custom-ui" style="display: none;">
            <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>

コントロールパネルの再実装

ここからは、もう少し実用的な内容に入っていきます。VIEWERのボタンを再実装してみましょう。

Autorotateボタン

Autorotateボタンの実装に必要な要件は以下の通りです。

  • 自動回転を開始する(停止している場合)
  • 自動回転を停止する(動作している場合)
  • 自動回転が動作しているときにボタンの色を変える
  • 自動回転が停止しているときにボタンの色を変える

使用するイベントとAPI呼び出し:

  • autorotate - 自動回転を開始/停止するAPI呼び出し
  • autorotate_stop - 自動回転が停止したときに発火するイベント
  • autorotate_start - 自動回転が開始したときに発火するイベント

このコードでは、現在の自動回転の状態を「把握」する(状態は変数に保存されます)ため、また、ボタンの色を変えるために、autorotate_startおよびautorotate_stopイベントのコールバックを定義します。ボタンのクリックハンドラーは、現在の状態に応じて自動回転を開始/停止するautorotate呼び出しをトリガーします。

<!-- Put this before the SUN embed code -->
<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>
  • 7行目:現在の自動回転の状態を保存
  • 24〜29行目:VIEWERにコールバックをバインド
  • 36〜43行目:autorotate_startおよびautorotate_stopのコールバック - 現在の状態を更新し、ボタンを再描画
  • 51〜58行目:autorotateボタンのクリックハンドラー - VIEWERのAPIメソッドautorotateを呼び出す
  • 61〜69行目: 現在の自動回転の状態に応じてボタンの色を変える(activeのCSSクラスを使用)

実際の動作を見てみましょう。フルスクリーンでの例はこちらにあります。

index_3.html
<!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="robots" content="noindex, nofollow">
    <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">
    <!-- Load hammer.js -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>
    <!-- Load Screenfull library -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/screenfull.js/3.3.3/screenfull.min.js"></script>
    <style type="text/css">
        body {
          margin: 0;
        }

        @media only screen and (min-width: 500px) {
          .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;
          /* necessary to properly position custom-ui inside this element */
          position: relative;
          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;
          z-index: 99;
        }

        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 0px 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>
    <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);
        let viewer_container = document.getElementById('viewer-container-' + 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');
    document.addEventListener("DOMContentLoaded", (event) => {
        vui.setup_callbacks();
        vui.setup_button_handlers()
    });

    </script>
</head>

<body>
<div class="viewer-wrapper">
    <div class="viewer-container" id="viewer-container-WZarBpyJ7hHX77zaF49Rb9">

        <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>

        <div id="custom-ui-WZarBpyJ7hHX77zaF49Rb9" class="custom-ui" style="display: none;">
            <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>

ズームイン・ズームアウト

ズームイン・ズームアウトボタンでは、以下のAPIを使用します。

まず、単純な実装は以下のようになります(分かりやすくするため、以下のコードにはautorotateボタンの実装は含まれていません)。

<!-- Put this before the SUN embed code -->
<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>

動作は以下のようになります(フルページの例はこちらにあります)。

index_4.html
<!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="robots" content="noindex, nofollow">
    <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">
    <!-- Load hammer.js -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>
    <!-- Load Screenfull library -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/screenfull.js/3.3.3/screenfull.min.js"></script>
    <style type="text/css">
        body {
          margin: 0;
        }

        @media only screen and (min-width: 500px) {
          .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;
          /* necessary to properly position custom-ui inside this element */
          position: relative;
          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;
          z-index: 99;
        }

        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 0px 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>
    <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_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);
        let btn_zoom_in = document.getElementById('ui-zoom-in-' + this.uid);
        let btn_zoom_out = document.getElementById('ui-zoom-out-' + this.uid);
        let viewer_container = document.getElementById('viewer-container-' + 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'})
          }
        });

        // 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});
          }
        });
      }

      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');
    document.addEventListener("DOMContentLoaded", (event) => {
        vui.setup_callbacks();
        vui.setup_button_handlers()
    });
    </script>
</head>

<body>
<div class="viewer-wrapper">
    <div class="viewer-container" id="viewer-container-WZarBpyJ7hHX77zaF49Rb9">
        
        <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>

        <div id="custom-ui-WZarBpyJ7hHX77zaF49Rb9" class="custom-ui" style="display: none;">
            <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>

ズームイン・ズームアウトボタンは動作していますが、拡大・縮小するたびに何度もクリックする必要があります。ボタンを押している間、拡大・縮小し続け、ボタンを離すと止まるようにできれば(元の実装と一貫性が取れて)より良いでしょう。

そこで、クリックとタッチのイベントを標準化された方法で処理するためにHammer.jsライブラリを使用し、タッチ・クリックの開始時にsetIntervalを使ってタッチ・クリックが終了する(clearInterval)まで継続的にズームさせます。

<!-- Load hammer.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>

<!-- Put this before the SUN embed code -->
<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>

Hammer.jsライブラリの詳細についてはここでは扱いませんが、ここで使用しているのはtap、press、pressupの3つのイベントであると言えば十分でしょう。tap(素早く押して離す動作)では、単純に1回分のズーム操作をトリガーします。長押しを識別するpressイベントでは、(pressupまで)プレゼンテーションを継続的にズームさせるsetIntervalのコールバックを開始します。

  • 20〜25行目:intervalをクリアするヘルパー関数
  • 28〜30行目:Hammerによってコールバックが設定されていても、デフォルトのonClickの動作を防ぐ必要があります。
  • 33〜47行目:mouseout/pointeroutイベント用の追加ハンドラーです。ズーム中にユーザーがマウスの右ボタンをクリックすると、pressupイベントが発火しない場合があります。そのため、これらの追加イベントをリッスンして即座にズームを中断します。
  • 61、67、71行目:ズームインするためのVIEWER APIの呼び出し
  • 111、117、121行目:ズームアウトするためのVIEWER APIの呼び出し

結果はこちらで確認できます。

index_5.html
<!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="robots" content="noindex, nofollow">
    <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">
    <!-- Load hammer.js -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>
    <!-- Load Screenfull library -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/screenfull.js/3.3.3/screenfull.min.js"></script>
    <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>

    <script type="text/javascript">
    class CustomViewerUI {
        constructor(uid) {
            this.uid = uid;
            this.viewer_api_obj = null;
            this.state = {
                autorotate: 'no'
            };
            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) => {
                this.viewer_api_obj = api_obj;
                api_obj.addCallback(
                    'autorotate_start_' + this.uid,
                    'autorotate_start');
                api_obj.addCallback(
                    'autorotate_stop_' + this.uid,
                    'autorotate_stop');
                api_obj.addCallback(
                    'mode_changed_' + this.uid,
                    'mode_changed');

                // 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);
            let btn_zoom_in = document.getElementById('ui-zoom-in-' + this.uid);
            let btn_zoom_out = document.getElementById('ui-zoom-out-' + this.uid);
            let viewer_container = document.getElementById('viewer-container-' + 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'})
                }
            });

            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();
            });
        }

        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');
    document.addEventListener("DOMContentLoaded", (event) => {
        vui.setup_callbacks();
        vui.setup_button_handlers()
    });
    </script>
</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&style=4&doubletap_mode=zoom&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 -->

        <div id="custom-ui-WZarBpyJ7hHX77zaF49Rb9" class="custom-ui" style="display: none;">
            <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>

ドラッグ/回転ボタン

ドラッグ/回転ボタンは、プレゼンテーション上でマウスをドラッグしたときの動作を切り替えます。プレゼンテーションの回転、または現在のフレームの移動のいずれかを引き起こします。VIEWERはデフォルトではrotateモードですが、ズームインすると(auto_drag_switchが有効な場合、デフォルトで有効)自動的にdragモードに切り替わります。

このボタンを再実装するために必要なのは、モードが変更されたとき(mode_changedイベント)に見た目を変更し、クリック時にmodeを切り替えることです。

<!-- Put this before the SUN embed code -->
<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>

9行目:現在のモードの値を保存しています。

26〜28行目:mode_changedイベントのコールバックを登録しています。

32〜35行目:コールバックが現在の状態を更新し、ボタンを再描画します。

45〜56行目: ボタンのクリックハンドラーで、VIEWERのAPIメソッドを呼び出します。

実際の動作はこちらです。

index_6.html
<!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="robots" content="noindex, nofollow">
    <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">
    <!-- Load hammer.js -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>
    <!-- Load Screenfull library -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/screenfull.js/3.3.3/screenfull.min.js"></script>
    <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>
</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&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 -->

        <div id="custom-ui-WZarBpyJ7hHX77zaF49Rb9" class="custom-ui" style="display: none;">
            <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>

<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) => {
                this.viewer_api_obj = api_obj;
                api_obj.addCallback(
                    'autorotate_start_' + this.uid,
                    'autorotate_start');
                api_obj.addCallback(
                    'autorotate_stop_' + this.uid,
                    'autorotate_stop');
                api_obj.addCallback(
                    'mode_changed_' + this.uid,
                    'mode_changed');

                // 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();
            };
            window['mode_changed_' + this.uid] = (mode) => {
                this.state.mode = mode;
                this.refresh_drag_rotate_btn();
            };
        }

        setup_button_handlers() {
            // button event handlers
            let btn_autorotate = document.getElementById('ui-autorotate-' + this.uid);
            let btn_zoom_in = document.getElementById('ui-zoom-in-' + this.uid);
            let btn_zoom_out = document.getElementById('ui-zoom-out-' + this.uid);
            let btn_drag_rotate = document.getElementById('ui-drag-rotate-' + this.uid);
            let viewer_container = document.getElementById('viewer-container-' + 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'})
                }
            });

            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();
            });
            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();
            });
            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();
            });

            // 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>';
            }
        }

        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');
    document.addEventListener("DOMContentLoaded", (event) => {
        vui.setup_callbacks();
        vui.setup_button_handlers()
    });
</script>
</body>
</html>

Fullscreenボタン

最後に実装するのはFullscreenボタンです。詳細に入る前に、いくつか重要な点を検討する必要があります。まず、Fullscreen APIはすべてのブラウザでサポートされているわけではないことに注意してください。つまり、一部のブラウザではフルスクリーンを一切使用しないか、独自の回避策(例えばページ上の一部の要素をリサイズするなど)を実装することになります。

このチュートリアルでは、Fullscreen APIをサポートするブラウザのみを対象とし、扱いやすくするためにScreenfullライブラリを使用します。

次に検討すべきなのは、フルスクリーンモードに切り替える要素です。Fullscreen APIは、選択した要素のスタイル(CSS)を変更してフルスクリーンにすることで動作します。つまり、Orbitvu VIEWER自体をフルスクリーンモードに切り替えることはできますが、VIEWERの外側に定義されたボタンはフルスクリーンから取り残され、見えなくなってしまいます。そこで、VIEWERと独自のボタンの両方を含む独自の要素を作成し、その要素に対してFullscreen APIを使用します。これがこのチュートリアルで行うことです。フルスクリーンに切り替える要素はviewer-container-WZarBpyJ7hHX77zaF49Rb9になります。

このアプローチにはもう1つ問題があります。VIEWERの外側にある要素をフルスクリーンに切り替えた場合、VIEWER自体はそのことを「認識」しません。VIEWERは、明示的な幅と高さの設定などによって強制されていたとしても、自身の寸法を変更する必要があるため、フルスクリーンへの切り替えを「認識」することが重要です(言い換えると、ページ上ではVIEWERのウィンドウを小さく(例えば200px × 350px)しておきたい一方で、フルスクリーンにする際は利用可能なスペースいっぱいに拡大したい、ということです)。そのため、fullscreenというVIEWERのAPI呼び出しを使って、VIEWERにフルスクリーンへの切り替えを知らせる必要があります。

コードを見てみましょう。

<!-- Load Screenfull library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/screenfull.js/3.3.3/screenfull.min.js"></script>

<!-- Put this before the SUN embed code -->
<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>

13行目:現在のフルスクリーンの状態を記憶します。

32行目:フルスクリーンが変更されたとき、フルスクリーンが有効になった場合はVIEWER APIの「fullscreen: enter」メソッドを呼び出します。

35行目:フルスクリーンが変更されたとき、フルスクリーンが無効になった場合はVIEWER APIの「fullscreen: cancel」メソッドを呼び出します。

46行目:フルスクリーンボタンがクリックされたとき、VIEWER APIの「fullscreen:pre」メソッドを呼び出します。

47行目:フルスクリーンを切り替えます。

ご覧のとおり、フルスクリーンの呼び出しには3つのAPIメソッドを使用しました。

  • pre - Viewer要素の現在の寸法を保存します(後で元に戻せるようにするため)
  • enter - 現在強制されている寸法(例えばwidth)を解除します(preの後に呼び出す必要があります)
  • cancel - preで保存された寸法を元に戻します(フルスクリーン終了時に呼び出す必要があります)

残りの作業は以下の2つです。

  • デフォルトのVIEWERボタンを非表示にする
  • VIEWERのデフォルトのダブルタップ/ダブルクリックの動作(フルスクリーンに入る動作)を無効にする(VIEWER単体だけをフルスクリーンにしたいわけではないため)

これらはどちらも、プレゼンテーションの埋め込みコードにパラメータを追加するだけで簡単に行えます。ボタンを削除するパラメータはstyle、ダブルタップの動作を変更するパラメータはdoubletap_modeです。

結果のコードは以下の通りです。

<!-- Embed code from Orbitvu SUN starts here -->
<script type="text/javascript"
        async
        src="//orbitvu.co/share/WZarBpyJ7hHX77zaF49Rb9/20838/360/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 -->

完成した例はこちらにあります。

index.html

<!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="robots" content="noindex, nofollow">
    <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">
    <!-- Load hammer.js -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>
    <!-- Load Screenfull library -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/screenfull.js/3.3.3/screenfull.min.js"></script>
    <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>
    <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_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');
                api_obj.addCallback(
                    'mode_changed_' + this.uid,
                    'mode_changed');

                // 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();
            };
            window['mode_changed_' + this.uid] = (mode) => {
                this.state.mode = mode;
                this.refresh_drag_rotate_btn();
            };
        }

        setup_button_handlers() {
            // button event handlers
            let btn_autorotate = document.getElementById('ui-autorotate-' + this.uid);
            let btn_zoom_in = document.getElementById('ui-zoom-in-' + this.uid);
            let btn_zoom_out = document.getElementById('ui-zoom-out-' + this.uid);
            let btn_drag_rotate = document.getElementById('ui-drag-rotate-' + this.uid);
            let btn_fullscreen = document.getElementById('ui-fullscreen-' + this.uid);
            let viewer_container = document.getElementById('viewer-container-' + 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'})
                }
            });

            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();
            });

            // 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'});
                    }
                }
            });

            // 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);
                }
            });
        }

        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>';
            }
        }

        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');
    document.addEventListener("DOMContentLoaded", (event) => {
        vui.setup_callbacks();
        vui.setup_button_handlers()
    });
    </script>
</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&style=4&doubletap_mode=zoom&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 -->

        <div id="custom-ui-WZarBpyJ7hHX77zaF49Rb9" class="custom-ui" style="display: none;">
            <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>

On this page