アシアルブログ

アシアルの中の人が技術と想いのたけをつづるブログです

Leap Motionで操作できるPhoneGap(Cordova)アプリ

こんにちは。今回は田中が書いた英語版アシアルブログの翻訳記事をお届けします。
原文はこちら

==============================

こんにちは。
今日は「Leap Motion」という最近手に入れた新しいガジェットを紹介したいと思います。
ガジェット好きな人は知っているかもしれませんが、Leap Motionは小さなスティック状のデバイスで、手のモーションをとらえてホストPCに送る入力装置です。
Kinectに似ていますが、よりシンプルなSDKが用意されていて安価です。

通常、Leap Motionを使う時はPCにつなぎ、Leap Motion対応のアプリケーションを操作します。誰でもLeap Motion対応のアプリケーションを開発することができます。Leap Motionには、WindowsMacアプリの開発者向けにC++SDKが、ウェブ開発者向けにjavascriptSDKが含まれています。
JavaScript、ということは、PhoneGapと組み合わせることで、使い慣れたウェブ技術を使ってLeamp Motionにアクセスするモバイルアプリを作ることができます。


■PhoneGapアプリはどのようにLeap Motionとつながるか
Leap Motionは USB経由でホストPCと接続されることになります。つまり、PhoneGapアプリからはPCにアクセスすることになります。PCにLeap Motionのドライバーをインストールすると、ウェブソケットプロトコルを待ち受けるデーモンプログラムが自動的にインストール、設定されます。そして、PhoneGapアプリはHTML5のウェブソケットクライアントを使って、Leap MotionJavaScript APIを通してPCに接続します。

ウェブソケットクライアントではサーバーからアクセスされる必要があり、Androidバイスではこの条件を満たすことができません。そこで今回は、サンプルアプリを動かすのにiPadを使っています。


Leap Motion SDK
Leap MotionJavaScript SDKオープンソースで公式ウェブサイトからダウンロードできます。
スタートガイドには、アプリとLeap Motionがどのように通信するのかという重要な概要が記載されていますので、そちらを一読することをおすすめします。
ウェブソケットの接続を通したnutshellでは、特定の頻度(デフォルトでは60fps)で、ハードウェアが計算した生の座標データやジェスチャーコマンドを含むユーザーの動作データが送信されます。
認識されるジェスチャーには、キータップ(キーボードをタップするような動作)、スクリーンタップ、スワイプ、そして円を描くような動きがあります。そして、それぞれの指とその動き(速度など)を検知することもできます。

ここまでの説明で、Leap Motion対応の新しいアプリケーションを作成する準備ができました。この記事では、開発環境にMonacaを使用しています。Monacaとは、PhoneGapアプリを作成するためのブラウザ上で使う開発環境で、クラウドサービスとして提供されています。無料で使うことができますので、まだ使ったことがないという方はぜひ登録して使ってみて下さい。


■Hello Leap Motion!
それでは、最初にシンプルなアプリを作りましょう。最小限のテンプレートから新しいプロジェクトを作成すると、IDEには以下のような画面が表示されます。


index.htmlの内容を、以下のように変更します。


<html>
  <head>
    <script src="plugins/plugin-loader.js"></script>
    <link rel="stylesheet" href="plugins/plugin-loader.css">
    <style>
      #motion {
        box-sizing: border-box;
        position: absolute;
        top: 30%; left: 20%; width: 60%;
        opacity: 0.9;
        background-color: #fff;
        padding: 30px;
        border: 1px solid #ccc;
        border-radius: 20px;
        text-align: center;
        font-size: 30px;
      }
    </style>
    <script src="http://js.leapmotion.com/0.2.0-beta1/leap.min.js"></script>
    <script>
    monaca.viewport({"width": "640"});
    Leap.loop({
      enableGestures: true,
      host: '10.0.5.130' // CHANGE HERE!!
    }, function(obj) {
      if (obj.gestures) {
        obj.gestures.forEach(function(gesture) {
          if (gesture.state == "start") {
            $("#motion").text(gesture.type).show();
            lastGestureType = gesture.type;
            addList(gesture);
          } else if (gesture.state == "stop") {
            $("#motion").fadeOut();
          }
        });
      }
    });
    function addList(json) {
      var $list = $("#gestures");
      $list.prepend("<li class='ui-li ui-li-static'><b>Starting " + json.type + "</b> <div style='font-size: 80%'>" + json + "</div></li>")
    }
    </script>
  </head>
  <body>
    <div data-role="page">
      <div data-role="header" data-position="fixed"><h1>Leap Motion Gestures</h1></div>
      <div class="ui-listview" id="gestures"></div>
    </div>
    <div id="motion" style="display: none"></div>
  </body>
</html>


ホストアドレスの部分(ソースコードの中で、// CHANGE HERE!!と記載されている部分)は、お使いのPCのIPアドレスに変更します。Leap Motionのドライバーがインストールされていれば、サーバーは動いているはずです。

プログラムの内容は、コードを見ていただければ何をしているのかわかると思います。「Leap.loop」という関数が最も重要で、1秒間に最大60回コールバック関数を呼び出し、検出されたジェスチャーを画面に表示しています。

App Storeから「Monaca Debugger」をダウンロードすればこのプログラムを動かすことができるので、Monacaを使うのが初めての方はぜひ試してみて下さい。

プログラムを実行した結果のスクリーンショットが以下になります。モーションを検知しつつ、生データをダンプして検知したモーションを表示します。



■より複雑なプログラム
それでは、より複雑なプログラムを作りましょう。
index.htmlを以下のコードに置き換えます。



<html>
  <head>
    <title>DOM Visualizer - Leap</title>
    <script src="http://js.leapmotion.com/0.2.0-beta1/leap.min.js"></script>
    <script>
 
      function moveFinger(Finger, posX, posY, posZ, dirX, dirY, dirZ) {
        Finger.style.webkitTransform = "translateX("+posX+"px) translateY("+posY+"px) translateZ("+posZ+"px) rotateX("+dirX+"deg) rotateY(0deg) rotateZ("+dirZ+"deg)";
      }
 
      function moveSphere(Sphere, posX, posY, posZ, rotX, rotY, rotZ) {
        Sphere.style.webkitTransform = "translateX("+posX+"px) translateY("+posY+"px) translateZ("+posZ+"px) rotateX("+rotX+"deg) rotateY(0deg) rotateZ(0deg)";
      }
 
      var fingers = {};
      var spheres = {};
      Leap.loop({
        enableGestures: true,
        host: '10.0.5.130' // CHANGE HERE!!
      }, function(frame) {
        var fingerIds = {};
        var handIds = {};
        if (frame.hands === undefined ) {
          var handsLength = 0
        } else {
          var handsLength = frame.hands.length;
        }
 
        for (var handId = 0, handCount = handsLength; handId != handCount; handId++) {
          var hand = frame.hands[handId];
          var posX = (hand.palmPosition[0]*3);
          var posY = (hand.palmPosition[2]*3)-200;
          var posZ = (hand.palmPosition[1]*3)-400;
          var rotX = (hand._rotation[2]*90);
          var rotY = (hand._rotation[1]*90);
          var rotZ = (hand._rotation[0]*90);
          var sphere = spheres[hand.id];
          if (!sphere) {
            var sphereDiv = document.getElementById("sphere").cloneNode(true);
                sphereDiv.setAttribute('id',hand.id);
                sphereDiv.style.backgroundColor='#'+Math.floor(Math.random()*16777215).toString(16);
                document.getElementById('scene').appendChild(sphereDiv);
                spheres[hand.id] = hand.id;
          } else {
            var sphereDiv =  document.getElementById(hand.id);
            if (typeof(sphereDiv) != 'undefined'  & & sphereDiv != null) {
              moveSphere(sphereDiv, posX, posY, posZ, rotX, rotY, rotZ);
            }
          }
          handIds[hand.id] = true;
        }
        for (handId in spheres) {
          if (!handIds[handId]) {
            var sphereDiv =  document.getElementById(spheres[handId]);
            sphereDiv.parentNode.removeChild(sphereDiv);
            delete spheres[handId];
          }
        }
 
        for (var pointableId = 0, pointableCount = frame.pointables.length; pointableId != pointableCount; pointableId++) {
          var pointable = frame.pointables[pointableId];
          var posX = (pointable.tipPosition[0]*3);
          var posY = (pointable.tipPosition[2]*3)-200;
          var posZ = (pointable.tipPosition[1]*3)-400;
          var dirX = -(pointable.direction[1]*90);
          var dirY = -(pointable.direction[2]*90);
          var dirZ = (pointable.direction[0]*90);
          var finger = fingers[pointable.id];
          if (!finger) {
            var fingerDiv = document.getElementById("finger").cloneNode(true);
                fingerDiv.setAttribute('id',pointable.id);
                fingerDiv.style.backgroundColor='#'+Math.floor(Math.random()*16777215).toString(16);
                document.getElementById('scene').appendChild(fingerDiv);
                fingers[pointable.id] = pointable.id;
          } else {
            var fingerDiv =  document.getElementById(pointable.id);
            if (typeof(fingerDiv) != 'undefined'  & & fingerDiv != null) {
              moveFinger(fingerDiv, posX, posY, posZ, dirX, dirY, dirZ);
            }
          }
          fingerIds[pointable.id] = true;
        }
        for (fingerId in fingers) {
          if (!fingerIds[fingerId]) {
            var fingerDiv =  document.getElementById(fingers[fingerId]);
            fingerDiv.parentNode.removeChild(fingerDiv);
            delete fingers[fingerId];
          }
        }
        document.getElementById('showHands').addEventListener('mousedown', function() {
          document.getElementById('app').setAttribute('class','show-hands');
        }, false);
        document.getElementById('hideHands').addEventListener('mousedown', function() {
          document.getElementById('app').setAttribute('class','');
        }, false);
      });
 
    </script>
    <style>
      *,*:before,*:after {
        margin: 0;
        padding: 0;
        border: 0;
        -webkit-box-sizing: border-box;
      }
      button {
        padding: .5em;
      }
      #app {
        position: absolute;
        width: 100%;
        height: 100%;
        font-size: 200%;
        overflow: hidden;
        background-color: #101010;
        -webkit-perspective: 1000;
      }
      #scene,
      #scene:before {
        position: absolute;
        left: 50%;
        top: 50%;
        width: 40em;
        height: 40em;
        margin: -20em 0 0 -20em;
        border: 4px solid #A0A0A0;
        background-color: rgba(255,255,255,.1);
        background-image:
        -webkit-linear-gradient(rgba(255,255,255,.4) .1em, transparent .1em),
        -webkit-linear-gradient(0deg, rgba(255,255,255,.4) .1em, transparent .1em),
        -webkit-linear-gradient(rgba(255,255,255,.3) .05em, transparent .05em),
        -webkit-linear-gradient(0deg, rgba(255,255,255,.3) .05em, transparent .05em);
        background-size: 5em 5em, 5em 5em, 1em 1em, 1em 1em;
        background-position: -.1em -.1em, -.1em -.1em, -.05em -.05em, -.05em -.05em;
        -webkit-transform-style: preserve-3d;
        -webkit-transform: rotateX(75deg);
      }
      #scene {
        -webkit-transform: rotateX(75deg);
      }
      #scene:before {
        content: '';
        -webkit-transform: rotateX(90deg) translateZ(19.5em) translateY(20em);
      }
      .cube {
        background-color: red;
        -webkit-transform-style: preserve-3d;
        -webkit-transform: translateX(19.5em) translateY(19.5em) translateZ(0em);
      }
      .finger,
      .sphere {
        position: absolute;
        left: 50%;
        top: 50%;
        width: 1em;
        height: 1em;
        margin: -.5em 0 0 -.5em;
        -webkit-transform-style: preserve-3d;
        -webkit-transform: translateX(14.5em) translateY(14.5em) translateZ(0);
      }
 
      .finger {
        opacity: .8;
        height: 3em;
      }
 
      .sphere {
        opacity: .3;
        display: none;
        font-size: 100px;
      }
 
      .show-hands .sphere {
        display: block;
      }
 
      .face {
        position: absolute;
        width: 1em;
        height: 1em;
        background-color: inherit;
        -webkit-transform-style: preserve-3d;
        -webkit-transform-origin: 0 0;
        -webkit-box-shadow: inset 0 0 0 1px rgba(255,255,255,.9);
      }
      .cube .face.tp { -webkit-transform: translateZ(1em); }
      .cube .face.ft { -webkit-transform: rotateX(90deg) translateZ(-1em); }
      .cube .face.bk { -webkit-transform: rotateX(90deg); }
      .cube .face.lt { -webkit-transform: rotateY(90deg) translateX(-1em); }
      .cube .face.rt { -webkit-transform: rotateY(90deg) translateX(-1em) translateZ(1em); }
 
      .finger .face.tp { -webkit-transform: translateZ(1em); height: 3em; }
      .finger .face.ft { -webkit-transform: rotateX(90deg) translateZ(-3em); }
      .finger .face.bk { -webkit-transform: rotateX(90deg); }
      .finger .face.lt { -webkit-transform: rotateY(90deg) translateX(-1em); height: 3em;}
      .finger .face.rt { -webkit-transform: rotateY(90deg) translateX(-1em) translateZ(1em); height: 3em;}
 
    </style>
  </head>
  <body>
    <div id="app" class="show-hands">
      <button id="showHands">Show Hands</button>
      <button id="hideHands">hide Hands</button>
      <div id="scene">
        <div id="cube" class="cube">
          <div class="face tp"></div>
          <div class="face lt"></div>
          <div class="face rt"></div>
          <div class="face ft"></div>
          <div class="face bk"></div>
        </div>
        <div id="finger" class="cube finger">
          <div class="face tp"></div>
          <div class="face lt"></div>
          <div class="face rt"></div>
          <div class="face ft"></div>
          <div class="face bk"></div>
        </div>
        <div id="sphere" class="cube sphere">
          <div class="face tp"></div>
          <div class="face lt"></div>
          <div class="face rt"></div>
          <div class="face ft"></div>
          <div class="face bk"></div>
        </div>
      </div>
    </div>
  </body>
</html>


このサンプルプログラムを動かしている動画を掲載しました。この動画を見ると、Leap Motionが5本の指の動きを即座に検知していることがわかります。



このサンプルアプリケーションは、こちらのページのプログラムを少し変更したものです。


■まとめ
このおもしろい技術をどうモバイルアプリに組み込むか、あとはあなた次第です。落とし穴があるとすれば、PCをサーバーとして動かす必要がある点くらいです。次期バージョンのLeap Motionでは、独自のWiFi接続とウェブソケットサーバーがLeap Motion内に組み込まれることを期待しています。

実際、今回紹介した例よりすごいプログラムを作ることができますので、もしMonacaLeap MotionとPhoneGapを使ったアプリを作ったら、是非私たちに教えて下さい。そうしていただければ、このブログで紹介したいと思います。

それではまた!