yoppa.org


jQueryで拡がるWebデザインの可能性

今日の内容

前回の授業では、canvas要素とJavascriptを連携して、図形やアニメーションを作成する方法について学びました。簡単とはいえ、実際にやってみると、なかなか難しい部分もあったと思います。

今回の授業は、より手軽な方法で、Webページにちょっとしたアニメーションを追加したり、タブナビゲーションやオートスクロールなど気の効いたインタラクションを追加する方法として、jQueryというJavaScriptライブラリを紹介したいと思います。

jQueryの特徴

jQueryは、JavaScriptとHTMLの相互作用を強化する軽量なJavaScriptライブラリです。フリーなオープンソースプロジェクトとして公開されているので、誰でも自由にライブラリを利用することが可能となっています。現在では、マイクロソフトやノキアなど大手デベロッパーにも採用され、今後ますます注目が集りそうなライブラリです。

jQueryは以下のような特徴が挙げられます。

  • ブラウザに依存しない
  • ページ内のドキュメント要素(DOM)走査と変更
  • CSS操作
  • エフェクトとアニメーション
  • Ajaxを利用した非同期な通信
  • プラグインによる機能拡張

jQueryの習得は、素のJavaScriptを一から習得するのに比べると格段に容易です。しかしながら、その利用によって拡がる表現の可能性はとても大きなものがあります。jQueryの機能はとても多岐にわたるため、その全てを1回の授業で紹介するのは無理ですが、その魅力の一端に触れることができればと思います。

jQuweyの環境設定

jQueryを利用するためには、jQueryのライブラリをWebページに読みこむ必要があります。自分で作成しているHTML書類に、jQueryライブラリを読みこむ方法は、大きくわけて2つあります。ひとつは、jQeuryのサイトから最新のjQueryライブラリをダウンロードして、そのファイルをHTMLのhead要素内でlink要素を使用して関連づける方法。もうひとつは、jQueryのライブラリをホスティングしている外部のWebサービスを利用する方法です。

今回の授業では、ライブラリをダウンロードする方法ではなく、より簡易に利用可能な外部のjQueryのホスティングを利用する方法を使用してみましょう。jQueryなどの主要なJavaScriptライブラリは、Google Libraries APIから簡単に利用することが可能です。Googleにホスティングされている現時点で最新のjQuery ver 1.4.4を利用するには、HTMLファイルのhead要素に以下のような記述を追加するだけです。

<script src="http://www.google.com/jsapi"></script>
<script>google.load("jquery", "1.4");</script>

jQueryのライブラリを読みこんだら、これを利用したJavaScriptのプログラムを追加することで、様々な効果を使用することが可能となります。今回は、外部ファイルとしてJavaScriptを作成して、それをHTMファイル側からよびだして関連づけてみましょう。今回作成するJavaScriptを「original.js」というファイル名にして、HTMLファイルと同じフォルダに配置するものとします。このときHTMLファイルの骨組は以下のようなものになるでしょう。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>jQueryのテスト</title>
    <script src="http://www.google.com/jsapi"></script>
    <script>google.load("jquery", "1.4.4");</script>
    <script src="original.js"></script>
  </head>
  <body>
    <h1>jQueryのテスト</h1>
  </body>
</html>

jQuery基本:クリックすると文字がフェードアウト

まずは、簡単なサンプルからみていきましょう。下記のHTMLとJavaScriptファイル「original.js」を同じフォルダ内に作成して、下記のHTMLとJavaScirptのコードを記入してみましょう。

HTML

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>jQueryのテスト</title>
    <script src="http://www.google.com/jsapi"></script>
    <script>google.load("jquery", "1.4.4");</script>
    <script src="original.js"></script>
    <style>
      body {margin:2em 4em;}
      p {background-color: #39f; color:white; padding:1em;}
    </style>
  </head>
  <body>
    <p>クリックすると消えるよ!!</p>
  </body>
</html>

JavaScript (original.js)

$(function() { //jQueryの開始
    $('p').click(function() { //pタグをクリックしたら
        $(this).fadeOut('slow'); //自分自身を消す
    });
});

デモ

このサンプルがjQueryの基本的な使用法になります。

まず、プログラム全体が「$(function(){ … });」で囲まれていることに注目してください。この記述は、中括弧「{…}」内の記述を、このHTMLドキュメントの読み込みが完了した時点で実行するという意味をもっています。つまり、この簡単な記述でドキュメントの読み込み完了のイベント処理を行うことができているのです。このようにjQueryでは短い記述で実際には複雑な処理を、簡単に行うことができることが特徴です。

次に「$(‘p’).click(function() { … });」という部分に注目してみましょう。まず「$(‘p’)」の部分に注目します。この記述は、読み込んだHTMLドキュメントの全てのp要素に対して行う命令であることを示しています。このようにjQueryではドキュメント内の要素にアクセスする際に非常に簡素な記述で行うことが可能です。この記述のルールは、CSSにおけるセレクタの記述ルールとほぼ共通していますので、CSSを使用した経験があれば容易に理解できるでしょう。

その後に続く「.click(function(){ … });」で、要素をクリックした際に命令を実行するように指示しています。その内容として記述されている、「$(this).fadeOut(‘slow’);」は、自分自身(つまりクリックされた要素)をフェードアウトする、という意味です。つまりこれによってクリックすると自分自身をフェードアウトさせるという処理が行われているのです。

フェードイン/フェードアウトのタイミングを調整する

何も指定しないでフェードイン(fadeIn)やフェードアウト(fadeOut)を実行すると、その変化に要する時間は400ミリ秒に設定されています。このタイミングは変更可能です。「fadeIn(‘fast’)」 というように「fast」と指定すると、変化は200ミリ秒に短縮されます。逆に、「fadeOut(‘slow’)」というように「slow」という指定をすると、変化時間は600msになります。また、「fadeIn(1000)」というように、時間をミリ秒単位で数値で指定することも可能です。

実際に様々な指定を試してみましょう。

HTML

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>jQueryのテスト</title>
    <script src="http://www.google.com/jsapi"></script>
    <script>google.load("jquery", "1.4.4");</script>
    <script src="original.js"></script>
    <style>
      body {margin:2em 4em;}
      p {
      background-color: #39f;
      color:white;
      padding:1em;
      }
    </style>
  </head>
  <body>
    <p>この部分が、いろいろアニメーションするよ!!</p>
    <ul>
      <li><a id="fadeout_fast" href="#">素早くフェードアウト</a></li>
      <li><a id="fadein_fast" href="#">素早くフェードイン</a></li>
      <li><a id="fadeout_slow" href="#">ゆっくりフェードアウト</a></li>
      <li><a id="fadein_slow" href="#">ゆっくりフェードイン</a></li>
      <li><a id="fadeout_2000" href="#">2秒(2000ms)かけてフェードアウト</a></li>
      <li><a id="fadein_4000" href="#">4秒(4000ms)かけてフェードイン</a></li>
    </ul>
  </body>
</html>

JavaScript (original.js)

$(function() {
    //素早くフェードアウト
    $('#fadeout_fast').click(function() {
        $('p').fadeOut('fast');
    });
    //素早くフェードイン
    $('#fadein_fast').click(function() {
        $('p').fadeIn('fast');
    });
    //ゆっくりフェードアウト
    $('#fadeout_slow').click(function() {
        $('p').fadeOut('slow');
    });
    //ゆっくりフェードイン
    $('#fadein_slow').click(function() {
        $('p').fadeIn('slow');
    });
    //2秒でフェードアウト
    $('#fadeout_2000').click(function() {
        $('p').fadeOut(2000);
    });
    //4秒でフェードイン
    $('#fadein_4000').click(function() {
        $('p').fadeIn(4000);
    });
});

デモ

場所の移動

.animate関数を利用すると、場所の移動するアニメーションも自在に作成することが可能です。実例を見ながら理解していきましょう。

HTML

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>jQueryのテスト</title>
    <script src="http://www.google.com/jsapi"></script>
    <script>google.load("jquery", "1.4.4");</script>
    <script src="original.js"></script>
    <style>
      body {margin:2em 4em;}
      p {background-color: #39f; color:white; padding:1em;}
      #animField {width:640px; height:480px; padding:1em; border:1px solid #ccc;}
      #block {width:100px; position:relative;}
    </style>
  </head>
  <body>
    <div id="animField">
      <p id="block">動くよ!</p>
    </div>
    <ul>
      <li><a id="animation01" href="#">右にスライドして拡大</a></li>
      <li><a id="animation02" href="#">左にスライドして縮小、薄くなる</a></li>
      <li><a id="animation03" href="#">1000msで下に移動して、200msで戻ってくる</a></li>
      <li><a id="animation04" href="#">500msで右下に移動して、1000ms停止して、200msで戻ってくる</a></li>
    </ul>
  </body>
</html>

JavaScript (original.js)

$(function() {
    //アニメーション01
    $('#animation01').click(function() {
        $('#block').animate({
            left: '400px',
            width: '200px',
            height: '200px',
            opacity: 1.0
        });
    });
    //アニメーション02
    $('#animation02').click(function() {
        $('#block').animate({
            left: '0px',
            width: '100px',
            height: '1em',
            opacity: .5
        });
    });
    //アニメーション03
    $('#animation03').click(function() {
        $('#block').animate({
            top: '400px',
            opacity: 0.2
        }, 1000).animate({
            top: '0px',
            opacity: 1.0
        }, 200);
    });
    //アニメーション04
    $('#animation04').click(function() {
        $('#block').animate({
            top: '400px',
            left: '400px',
            opacity: 0.2
        }, 500).delay(1000).animate({
            top: '0px',
            left: '0px',
            opacity: 1.0
        }, 200);
    });
});

デモ

プラグインを利用する 1:タブメニュー

jQueryはプラグインを使用することで、様々な便利な機能を拡張していくことが可能です。プラグインは多岐にわたっていますが、まずサンプルとしてタブメニューを可能とするプラグインをみてみましょう。

タブプラグインを利用可能にするには、HTMLのhead要素に、jQuery UIのライブラリを新規に読み込む必要があります。jQueryのメインのライブラリに加えて、現時点でのjQuery UIの最新のバージョン1.8.6を読み込むには以下のように記述します。このサンプルではjQuery UIのライブラリのリンクとあわせて、UI用のスタイルシートの読み込みも行っています。

<script src="http://www.google.com/jsapi"></script>
<script>google.load("jquery", "1.4");</script>
<script>google.load("jqueryui", "1.8.6");</script>

タブメニューの実装はとても簡単です。サンプルを掲載します。

HTML

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>jQueryのテスト</title>
    <script src="http://www.google.com/jsapi"></script>
    <script>google.load("jquery", "1.4.4");</script>
    <script>google.load("jqueryui", "1.8.6");</script>
    <script src="original.js"></script>
    <link href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.6/themes/smoothness/jquery-ui.css" rel="stylesheet" type="text/css" />
  </head>
  <body>
    <h1>jQuery UI : タブメニューサンプル</h1>
    <div id="tabs">
      <ul>
	<li><a href="#tabs-1">Tab 1</a></li>
	<li><a href="#tabs-2">Tab 2</a></li>
	<li><a href="#tabs-3">Tab 3</a></li>
      </ul>
      <div id="tabs-1">
	<p>ここにタブ1の文章を入れていきます。</p>
      </div>
      <div id="tabs-2">
	<p>ここにタブ2の文章を入れていきます。</p>
      </div>
      <div id="tabs-3">
	<p>ここにタブ3の文章を入れていきます。</p>
      </div>
    </div>
  </body>
</html>

JavaScript (original.js)

$(function() {
    $("#tabs").tabs();
});

デモ

さらに高度なプラグインの利用:フォトギャラリー、ライトボックス

jQueryを利用したより高度なプラグインが数多く公開されています。これらのプラグインを活用することで、フォトギャラリーやライトボックスなど様々な高度な機能が簡単に利用可能です。

下記にjQueryのプラグインに関するリンクを紹介します。この他にも沢山の情報がありますので、検索を活用しながらいろいろ調べてみてください。


AS3でアニメーションを作成する 3 – たくさん物体を同時に動かす

たくさんの図形を動かすには

先週の授業では、ムービークリップの座標(x, y)の情報を、フレームが更新するたびに少しずつ変化させることで物体が動いてみえるという、アニメーションの基本原理について、ActionScriptでプログラムしました。

では、複数の図形を別々に同時に動かすにはどうすれば良いでしょうか。その方法について、考えていきたいと思います。

スクリプトのテンプレート

今回もプログラムを進めるにあたって利用すると便利なテンプレートを作成しました。下記のコードをコピーして使用してください。

package {
  import flash.display.*;
  import flash.events.*;

  public class Main extends Sprite {
  //ここにグローバルな変数を記入

    public function Main() {
    //初期設定の項目を記入
            
      //フレーム更新のイベントリスナーの登録
      this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
    }

    //イベントハンドラ
    function enterFrameHandler(event:Event):void {

    }
  }
}

ムービクリップの用意

まず、先週と同様にAS3形式のFLAファイルを用意します。その上でムービークリップシンボルとして「Ball」というクラス名でシンボルを用意します。ムービークリップには円の形を描いておきます。

次に、このFLAファイルのムービーのプロパティーで、クラスの設定を「Main」にします。そしてこのFLAファイルがあるフォルダと同じフォルダに「Main.as」というファイルをActionScriptファイルとして作成します。このMain.as内に先程のスクリプトのテンプレートをコピーしてペーストしてください。

これで準備は完了です。

アニメーションの基本形

今日は、まず下記のような上下にバウンドするボールのアニメーションを、基本的な動きとして使用します。単体のアニメーションを実現するスクリプトは下記のようになります。

package {
  import flash.display.Stage;
  import flash.display.*;
  import flash.events.*;

  public class Main extends Sprite {
    var ball:Ball = new Ball();
    var speedY:Number;

    public function Main() {
      this.addChild(ball);
      ball.x=stage.stageWidth/2;
      ball.y=stage.stageHeight/2;
      speedY=10;

      this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
    }

    function enterFrameHandler(event:Event):void {
      ball.y+=speedY;
      if (ball.y<0||ball.y>stage.stageHeight) {
        speedY*=-1;
      }
    }
  }
}

画面クリックでスタート

/wp-content/uploads/2010/11/Main011.swf, 400, 300

方法1:複数のムービクリップのインスタンスを作成する

複数の物体を動かす方法で一番簡単なのは、その数だけムービークリップのインスタンスを生成して、全てに対してアニメーションの設定をしていく方法です。ここでは、2つのBallを上下にバウンドさせています。

package {
  import flash.display.Stage;
  import flash.display.*;
  import flash.events.*;

  public class Main extends Sprite {
    var ball1:Ball = new Ball();
    var ball2:Ball = new Ball();
    var speedY1:Number;
    var speedY2:Number;

    public function Main() {
      this.addChild(ball1);
      ball1.x=stage.stageWidth/3;
      ball1.y=stage.stageHeight/2;
      speedY1=10;

      this.addChild(ball2);
      ball2.x=stage.stageWidth/3*2;
      ball2.y=stage.stageHeight/2;
      speedY2=12;

      this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
    }

    function enterFrameHandler(event:Event):void {
      ball1.y+=speedY1;
      if (ball1.y<0||ball1.y>stage.stageHeight) {
        speedY1*=-1;
      }

      ball2.y+=speedY2;
      if (ball2.y<0||ball2.y>stage.stageHeight) {
        speedY2*=-1;
      }
    }
  }
}

画面クリックでスタート

/wp-content/uploads/2010/11/Main021.swf, 400, 300

方法2:配列を使用する

複数のムービークリップのインスタンスを生成する方法では、すぐに数が増えた際に破綻してしまいます。物体の数が10から20個、100個と増えていったら、その数に比例してプログラムの分量が増えていってしまい、すぐに把握できなくなってしまうでしょう。

こうした大量のデータを管理するには、配列(Array)と呼ばれるデータ形式を利用すると便利です。配列とは関係をもつ一連のデータを格納できる仕組です。変数を箱とすると、配列は箱を沢山連結した「ロッカー」のようなものだとイメージするとわかりやすいかもしれません。同じ型の数値であれば、指定した数だけ一気に値を格納する領域を用意することが可能です。先程の円の中心座標を例にすると、4つの円の中心座標を格納するには、以下の宣言をします。

var 《配列の名前》:Array = new Array();

こうすることで、複数のデータを格納するロッカーのようなものが生成されます。このロッカーには0番から順番に番号が割り振られていて、「《配列の名前》[番号]」というように指定すると、特定の番号のロッカーに格納したデータを取りだすことが可能となります。

このロッカーに値を追加するには以下のように行います。

《配列の名前》.push(《格納するデータ》);

例えば、ロッカーに順番にアルファベットの文字を保存していき、取り出すプログラムは下記のようになります。

var alphabet:Array = new Array();

alphabet.push("a");
alphabet.push("b");
alphabet.push("c");
alphabet.push("d");

trace(alphabet[0]); // a 
trace(alphabet[1]); // b
trace(alphabet[2]); // c
trace(alphabet[3]); // d

この仕組みを利用して、複数のムービクリップとそのY軸方向へのスピードを、配列で管理してみましょう。

package {
  import flash.display.Stage;
  import flash.display.*;
  import flash.events.*;

  public class Main extends Sprite {
    var ballArray:Array = new Array();
    var speedArray:Array = new Array();
    var ballNum:int;

    public function Main() {
      ballNum=8;

      for (var i:int = 0; i < ballNum; i++) {
        var ball:Ball = new Ball();
        ball.x=stage.stageWidth/ballNum*i+30;
        ball.y=stage.stageHeight/2;
        ballArray.push(ball);
        this.addChild(ballArray[i]);

        var speedY:Number=i+10;
        speedArray.push(speedY);
      }

      this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
    }

    function enterFrameHandler(event:Event):void {
      for (var i:int = 0; i < ballNum; i++) {
        ballArray[i].y+=speedArray[i];
        if (ballArray[i].y<0||ballArray[i].y>stage.stageHeight) {
          speedArray[i]*=-1;
        }
      }
    }
  }
}

画面クリックでスタート

/wp-content/uploads/2010/11/Main031.swf, 400, 300

応用:ランダムな方向にアニメーションさせる

この複数の動きを配列で管理する仕組みを応用して、X軸方向とY軸方向にランダムなスピードで移動するアニメーションを作成してみましょう。

package {
  import flash.display.Stage;
  import flash.display.*;
  import flash.events.*;

  public class Main extends Sprite {
    var ballArray:Array = new Array();
    var speedXArray:Array = new Array();
    var speedYArray:Array = new Array();
    var ballNum:int;

    public function Main() {
      ballNum=50;

      for (var i:int = 0; i < ballNum; i++) {
        var ball:Ball = new Ball();
        ball.x=stage.stageWidth/2;
        ball.y=stage.stageHeight/2;
        ballArray.push(ball);
        this.addChild(ballArray[i]);

        var speedX:Number=Math.random()*20 - 10;
        var speedY:Number=Math.random()*20 - 10;
        speedXArray.push(speedX);
        speedYArray.push(speedY);
      }

      this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
    }

    function enterFrameHandler(event:Event):void {
      for (var i:int = 0; i < ballNum; i++) {
        ballArray[i].x+=speedXArray[i];
        ballArray[i].y+=speedYArray[i];
        if (ballArray[i].x<0||ballArray[i].x>stage.stageWidth) {
          speedXArray[i]*=-1;
        }
        if (ballArray[i].y<0||ballArray[i].y>stage.stageHeight) {
          speedYArray[i]*=-1;
        }
      }
    }
  }
}

画面クリックでスタート

/wp-content/uploads/2010/11/Main041.swf, 400, 300


Kinect Hack 現状まとめ

追記 : ofxKinectを使用したデモを、今週末の日曜日、11月28日に行われる「TMUG,TKSC,PICnome合同忘年会」でやらせていただくことになりました。記事を読んで興味をもった方、ぜひお越しください。

マイクロソフトから発売された「Xbox 360」用に開発された「Kinect」というゲーム用コントローラが、いまアツい視線を浴びています。Kinectの特徴は、デバイスを手にもったり体に装置を装着することなしに、ジェスチャーや音声認識をすることがでるという点で、それをゲームのコントローラーとして利用しようとしているわけです。

ジェスチャーの認識機能の基本的なしくみは、Kinect前部にとりつけられたレンズのひとつから、レーザーのパターンを広範囲に照射して、そのパターンの粗さや幾何学的な歪み具合から、対象物の3D構造を認識しているようです。暗視モードでKinectを使用している部屋を撮影すると無数の赤外線レーザのパターンが照射されているのがわかります。

と、ここまでは一般的なゲームコントローラーの話題だったのですが、なぜKinectがここまで注目されているのかというと、本来は XBox 360 専用だったKinectセンサーなのですが、一般的なUSB接続を使用しているため、PCやMacなどに普通に接続することが可能です。発売から数日で世界中のハッカー達が集中的に解析して、KinectセンサーとXBox本体との通信の仕組みを、PCやMacで使用できるように移植したオープンソースのドライバを開発してしまったのです。

このことが「Kinect Hack」という新たなムーブメントを生みだし、発売から数日で様々な環境で簡単に使用できるようになってしまいました。僕が現時点で把握している範囲では以下のものがあるようです。

このように、様々な開発プラットフォームに移植されています。その勢いはものすごいものがあります。まさに、世界中のハッカー大集結といった様相です。

最近はすっかりopenFrameworksべったりの僕としては、やはりofxKinectを使ってみようということで、ここ数日いろいろ試しています。まず先程紹介した、TheoさんのGitのリポジトリから最新のofxKinectをダウンロードして、openFrameworksのappsフォルダ内にコピーすると、すぐにサンプルが実行できると思います。

このサンプルはKinectで認識した距離情報つきの動画(左上)を、OpenCVで解析して、物体を認識しています。普通のカメラで認識する場合と違って、特定の距離の形のみを抜きだすことができるのが画期的です。うまく調整すれば、キャプチャー画像のように手の形のみをきれいに抜き出すことなども可能となります。

この情報を利用して、例えばジェスチャーでパペットを操作するというような作品も可能となります。Theo Watsonさんによる素晴しい実例があります。

Interactive Puppet Prototype with Xbox Kinect from Theo Watson on Vimeo.

Theoさん作のofxKinectは、さすが良くできていて、クラスのメソッドを通じてKinectで検出した様々な情報にすぐにアクセスできるようになっています。例えば、

  • float getDistanceAt(int x, int y); – 指定した座標(x, y)の距離を計測してfloat型で返す
  • ofColor getCalibratedColorAt(int x, int y); – 指定した座標(x, y)のカメラで撮影した色を取得する

とういうようなものがあり、この2つの情報をあわせると、すぐに、やくしまるえつこ「ヴィーナスとジーザス」のような表現ができてしまうというわけです。

早速、プログラムしてみました。アドオンやプロジェクトファイルなど一式パッケージングしたものはこちらからダウンロードできます。ofKinectStudy.zip (Zip, 115.4KB)

testApp.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "ofxOpenCv.h"
#include "ofxKinect.h"

class testApp : public ofBaseApp
{
    
public:
    
    void setup();
    void update();
    void draw();
    void exit();
    
    void keyPressed  (int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    
    ofxKinect kinect;
    float rotX, rotY;    
};

#endif

testApp.cpp

#include "testApp.h"

void testApp::setup()
{
    ofSetFrameRate(60);
    ofBackground(0,0,0);
    ofSetCircleResolution(12);
    
    rotX = 0;
    rotY = 0;
    
    kinect.init();
    kinect.setVerbose(true);
    kinect.open();
}

void testApp::update()
{
    kinect.update();
}

void testApp::draw()
{
    int step = 4;
    int calibrateX = 0;
    int calibrateY = 10;
    unsigned char * calibratedPixels = kinect.getCalibratedRGBPixels();
    ofTranslate(ofGetWidth()/2, ofGetHeight()/2);
    ofRotateX(rotX);
    ofRotateY(rotY);
    ofTranslate(kinect.width/2, kinect.height/2, 1000);
    for (int j = 0; j < kinect.height; j+=step) {
        for (int i = 0; i < kinect.width; i+=step) {
            float distance = kinect.getDistanceAt(i, j);          
            int colorPos = kinect.width * j * 3 + i * 3;
            ofSetColor(calibratedPixels[colorPos], calibratedPixels[colorPos+1], calibratedPixels[colorPos+2]);
            ofPushMatrix();
            ofTranslate(i - kinect.width, j - kinect.height, distance * -10.0);
            ofRotateX(-rotX);
            ofRotateY(-rotY);
            ofCircle(0, 0, step*0.4);
            ofPopMatrix();
        }
    }
}

void testApp::exit()
{
    kinect.close();
}

void testApp::keyPressed (int key){}
void testApp::mouseMoved(int x, int y){}

void testApp::mouseDragged(int x, int y, int button)
{
    rotX = float(y) / ofGetHeight() * 360 - 180;
    rotY = float(x) / ofGetWidth() * 360 - 180;
}

void testApp::mousePressed(int x, int y, int button){}
void testApp::mouseReleased(int x, int y, int button){}
void testApp::windowResized(int w, int h){}

実験の結果、こんな映像ができました。楽しいです。処理もすごくスムーズです。

ofxKinect study #05 from Atsushi Tadokoro on Vimeo.

今後も続々とKinect Hackの成果でてきそうです。最後に、いくつか参考になりそうなサイトをあげておきます。

というわけで、ぜひ皆さんもこのKinect Hackのビッグウェーブに乗りましょう!!


openFramewrorks – addonを使う 2:reacTIVision + ofxTUIO でタンジブルなインタフェイスを作る

reacTIVisionとは?

今日の授業では、reacTIVisionを使用してインタラクティブなアプリケーションを作成してみようと思います。

reacTIVisionとは、タンジブル(Tangible)なユーザインタフェイスを実現するための、オープンソースのツールキットです。タンジブルとは、形のない情報に直接触れることが出来るようなインターフェイスのことです。reacTIVisionは、このタンジブルな環境を実現するために、専用のマーカーを使用して複数のポイントの位置と角度を、素早く認識し解析することができます。

reacTIVisionを応用した有名な作品として、Reactableが挙げられます。Reactableは、テーブルの上に物理的にオブジェクトを配置していくことで、音響の生成や、音楽の演奏をすることができる、タンジブルな音楽創作環境です。

reacTIVisionを利用することで、このようなインタラクティブでタンジブルな作品を比較的容易に制作することが可能となります。

reacTIVisonを利用したシステムの概要

まず初めに、reacTIVisionを使用したシステムの全体像について理解しましょう。reacTIVisionは単体のアプリケーションとなっていて、コンピュータに接続したカメラで専用のマーカーの位置と角度を識別します。その解析結果は、TUIOという通信プロトコルを使用して送出されます。TUIOとはマルチタッチのインタフェイスやタンジブルなインタフェイスの情報の送受信に特化した、通信プロトコルです。TUIOはOSC(Open Sound Control)のプロトコルをベースにしてそれを拡張したものとなっています。

TUIOを情報を受信してどのようにインタフェイスを設計し表現するかという部分が、創作に関係するパートとなります。今回はこのTUIOを受信して表現する部分にopenFrameworksを用いることになります。(もちろんopenFrameworks以外のアプリケーションを使用することも可能です)。openFrameworksでTUIOを使用するにはofxTUIOというアドオンを用います。

reacTIVisonのセットアップ

まずは、マーカーを読みとる側のreacTIVisionを設定します。といっても、こちらはアプリケーションをダウンロードして実行するだけです。reacTIVisionのWebサイトのreactiから、最新のreacTIVison vision engineをダウンロードします。Zipファイルを解凍して、reacTIVisionというアプリケーションを実行します

reacTIVisonのマーカーはreacTIVisionのアプリケーションのフォルダ内のsymbolフォルダ内の「default.pdf」をプリントアウトして使用します。カメラの前でマーカーをかざすと、マーカーの位置と角度、マーカーのID番号が表示されます。正しく認識できたら、マーカーの読み取り側の環境は設定完了です。

openFrameworksのセットアップ

次にopenFrameworksの設定を行います。

openFrameworks側では、reacTIVision vision engineから送出されるTUIOプロトコルを受信して、その内容を解析します。その解析結果(マーカーの位置、角度、ID番号)を利用して、ビジュアルやサウンドをインタラクティブに操作できるようにしてみようと思います。

openFrameworksでTUIOを送受信できるようにするには、ofxTUIOというアドオンを利用します。またこのofxTUIOを使用するためには、併せてofxOSCというアドオンも必要となります。ofxOSCはFAT版のaddonsに最初から含まれていますので、ofxTUIOだけダウンロードすれば必要なシステムは整います。以下のリンクよりofxTUIOをダウンロードして、openFrameworksのaddonsフォルダ内に配置します。

このアドオンを使用するには、先週行ったofxBox2dと同様にプロジェクトにアドオンのソースとライブラリを追加する必要があります。新規にopenFrameworksのプロジェクトを作成し、ファイルリストにofxTUIOとofxOSCを追加します。追加が完了すると、ファイルリストは以下のような構成になるでしょう。

マーカーの位置を認識する

まず初めに認識したマーカーの位置と角度とIDを表示するだけの簡単なプログラムを作成してみましょう。このプログラムがofxTUIOを用いたアプリケーションのテンプレートとなります。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "ofxTuio.h"

//--------------------------------------------------------
class testApp : public ofSimpleApp{
    
public:
    
	void setup();
	void update();
	void draw();
    
	void keyPressed  (int key);
	void mouseMoved(int x, int y );
	void mouseDragged(int x, int y, int button);
	void mousePressed(int x, int y, int button);
	void mouseReleased();
    
    //TUIOの物体(マーカー)に関係するイベントを追加
    //物体の追加を検知
	void objectAdded(ofxTuioObject & tuioObject);
    //物体の削除を検知
	void objectRemoved(ofxTuioObject & tuioObject);
    //物体の状態の更新を検知
	void objectUpdated(ofxTuioObject & tuioObject);

    //TUIOのカーソル(タッチポイントなど)に関係するイベント
    //カーソルの追加を検知
	void tuioAdded(ofxTuioCursor & tuioCursor);
    //カーソルの削除を検知
	void tuioRemoved(ofxTuioCursor & tuioCursor);
    //カーソルの状態の更新を検知
	void tuioUpdated(ofxTuioCursor & tuioCursor);
    
private:
    //TUIOのクライアントのインスタンス化
	myTuioClient tuio;
    //ログの出力用
    string log;
};

#endif

testApp.cpp

#include "testApp.h"

void testApp::setup(){
    //TUIOに関連するイベントリスナーの追加
	ofAddListener(tuio.objectAdded,this,&testApp::objectAdded);
	ofAddListener(tuio.objectRemoved,this,&testApp::objectRemoved);
	ofAddListener(tuio.objectUpdated,this,&testApp::objectUpdated);
	ofAddListener(tuio.cursorAdded,this,&testApp::tuioAdded);
	ofAddListener(tuio.cursorRemoved,this,&testApp::tuioRemoved);
	ofAddListener(tuio.cursorUpdated,this,&testApp::tuioUpdated);
    
    //フレームレート設定
	ofSetFrameRate(60);
    //背景を黒に
    ofBackground(0,0,0);
    
    //ポート番号3333で、TUIOの通信開始
	tuio.start(3333);
    
    //ログのテキストを空に
    log="";
}

void testApp::update(){
    //TUIOのメッセージを受信
	tuio.getMessage();
}

void testApp::draw(){
    //カーソルの状態を表示
	tuio.drawCursors();
    //オブジェクトの状態を表示
	tuio.drawObjects();
    //ログを表示
    ofSetColor(0xffffff);
    ofDrawBitmapString(log, 20, 20);
}

void testApp::keyPressed  (int key){
}
void testApp::mouseMoved(int x, int y ){
}
void testApp::mouseDragged(int x, int y, int button){
}
void testApp::mousePressed(int x, int y, int button){
}
void testApp::mouseReleased(){
}

void testApp::objectAdded(ofxTuioObject & tuioObject){
    //マーカー追加
    log = " new object: " + ofToString(tuioObject.getFiducialId())+
    " X: "+ofToString(tuioObject.getX())+
    " Y: "+ofToString(tuioObject.getY())+
    " angle: "+ofToString(tuioObject.getAngleDegrees());
}
void testApp::objectRemoved(ofxTuioObject & tuioObject){
    //マーカー削除
	log = " object removed: " + ofToString(tuioObject.getFiducialId())+
    " X: "+ofToString(tuioObject.getX())+
    " Y: "+ofToString(tuioObject.getY())+
    " angle: "+ofToString(tuioObject.getAngleDegrees());
}
void testApp::objectUpdated(ofxTuioObject & tuioObject){
    //マーカーの状態更新
	log = " object updated: " + ofToString(tuioObject.getFiducialId())+
    " X: "+ofToString(tuioObject.getX())+
    " Y: "+ofToString(tuioObject.getY())+
    " angle: "+ofToString(tuioObject.getAngleDegrees());
}
void testApp::tuioAdded(ofxTuioCursor & tuioCursor){
    //カーソル追加
	log = " new cursor: " + ofToString(tuioCursor.getFingerId())+
    " X: "+ofToString(tuioCursor.getX())+
    " Y: "+ofToString(tuioCursor.getY());
}
void testApp::tuioRemoved(ofxTuioCursor & tuioCursor){
    //カーソル削除
	log =  " cursor removed: " + ofToString(tuioCursor.getFingerId())+
    " X: "+ofToString(tuioCursor.getX())+
    " Y: "+ofToString(tuioCursor.getY());
}
void testApp::tuioUpdated(ofxTuioCursor & tuioCursor){
    //カーソル状態更新
	log =  " cursor updated: " + ofToString(tuioCursor.getFingerId())+
    " X: "+ofToString(tuioCursor.getX())+
    " Y: "+ofToString(tuioCursor.getY());
}

ofxTUIOでは、TUIOの送受信をmyTuioClientというクラスを通して行います。myTuioClientは、TUIO通信の開始や終了、物体の状態の表示などを、複数のオブジェクトを一括した状態で情報の取得が可能となっています。myTuioClientの基本的なメソッドには次のようなものがあります。以下の例では、myTuioClientのインスタンスをtuioとした場合の例になります。

  • tuio.start(ポート番号) – 設定したポート番号でTUIOの通信を開始
  • tuio.stop() – 通信の有料
  • tuio.getMessage() – TUIOのメッセージを受信
  • tuio.drawCursors() – カーソルの状態を画面に表示
  • tuio.drawObjects() – 物体(マーカー)の状態を画面に表示

このプログラムでは、testAppのsetup()内で、myTuioClientのstart()で通信を開始し、update()内では、getMessage()を繰り返し呼びだすことで、常に最新のTUIOメッセージを取得しています。その状態をもとに、drawObjects()とdrawCursors()を使用して画面状に現在の状態を描画しています。

また、ofxTUIOは、個別の物体やカーソルの状態の更新をTUIOのイベントという形で通知します。これは、マウスやキーボードからの入力を通知するイベント(keyPressed、mouseReleasedなど)と似ています。TUIOから通知されるイベントには下記のようなものがあります。

  • objectAdded(ofxTuioObject & tuioObject) – 物体(tuioObject)が画面に追加された際に通知される
  • objectRemoved(ofxTuioObject & tuioObject) – 物体(tuioObject)が画面から削除された際に通知される
  • objectUpdated(ofxTuioObject & tuioObject) – 物体(tuioObject)の状態が更新された際に通知される
  • tuioAdded(ofxTuioCursor & tuioCursor) – カーソル(tuioCursor)が画面に追加された際に通知される
  • tuioRemoved(ofxTuioCursor & tuioCursor) – カーソル(tuioCursor)が画面から削除された際に通知される
  • tuioUpdated(ofxTuioCursor & tuioCursor) – カーソル(tuioCursor)の状態が更新された際に通知される

イベントは、検出した物体のインスタンス、またはカーソルのインスタンスと一緒に送られてきます。このインスタンスに定義されたアクセサ(メソッド)を通して、物体の情報を取り出すことが可能となっています。物体の状態を知るためのアクセサとして以下のものがあります。

  • tuioObject.getFiducialId() – 物体のマーカーID番号
  • tuioObject.getX() – 物体のX座標
  • tuioObject.getY() – 物体のY座標
  • tuioObject.getAngleDegrees() – 物体の配置角度

サンプルプログラムでは、これらのTUIOに関するイベント情報を画面の左上に常に最新の状態で表示するようにしています。

オリジナルなマーカーを描画する

最初の例では、tuio.drawObjects()を利用してマーカーの状態を描画しましたが、この表示はあくまで状態の確認用のものであり、実際に作品を制作する際には、マーカーの状態(ID、座標、角度)にアクセスして、オリジナルの表現方法で描画していく必要があります。

複数のマーカーの情報を取得する際にも、myTuioClientのメソッドを利用します。myTuioClientのインスタンスをtuioとした場合、tuio.getTuioObjects() メソッドを実行すると、myTuioClientを通して全てのオブジェクトのリストを取得できます。データの形式は、ofxTuioObjectのポインタ型を格納したlistという形式になっています。このlistは、vectorとよく似た動的な配列型です。しかしvectorと違って、listはその配列内の要素にアクセスする際には必ずイテレータ(iterator)を用いなければならないという制約があります。

このサンプルプログラムでもfor文の継続条件にイテレータを使用して、個々のオブジェクト(ofxTuioObject)のポインタを取り出し、そこから、X座標、Y座標、角度を取得しています。例えば、ofxTuioObjectのポインタ型を*blobとした場合下記のようにして個々の情報にアクセスすることができます。

  • blob->getFiducialId() – オブジェクトのID
  • blob->getX() – X座標
  • blob->getY() – Y座標

マーカーの状態をオリジナルな方法で描画するサンプルとして、まずは簡単な例として、画像ファイルを読み込んで、全てのマーカーの位置と角度の情報を取得して、その場所と角度を画像で再現するというプログラムを作成してみたいと思います。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "ofxTuio.h"

//--------------------------------------------------------
class testApp : public ofSimpleApp{
    
public:
    
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased();
    
    //TUIOの物体(マーカー)に関係するイベントを追加
    //物体の追加を検知
    void objectAdded(ofxTuioObject & tuioObject);
    //物体の削除を検知
    void objectRemoved(ofxTuioObject & tuioObject);
    //物体の状態の更新を検知
    void objectUpdated(ofxTuioObject & tuioObject);

    //TUIOのカーソル(タッチポイントなど)に関係するイベント
    //カーソルの追加を検知
    void tuioAdded(ofxTuioCursor & tuioCursor);
    //カーソルの削除を検知
    void tuioRemoved(ofxTuioCursor & tuioCursor);
    //カーソルの状態の更新を検知
    void tuioUpdated(ofxTuioCursor & tuioCursor);
    
private:
    //TUIOのクライアントのインスタンス化
    myTuioClient tuio;
    //ログの出力用
    string log;
    //表示する画像
    ofImage myImage;
};

#endif

testApp.cpp

#include "testApp.h"

void testApp::setup(){
    //TUIOに関連するイベントリスナーの追加
    ofAddListener(tuio.objectAdded,this,&testApp::objectAdded);
    ofAddListener(tuio.objectRemoved,this,&testApp::objectRemoved);
    ofAddListener(tuio.objectUpdated,this,&testApp::objectUpdated);
    ofAddListener(tuio.cursorAdded,this,&testApp::tuioAdded);
    ofAddListener(tuio.cursorRemoved,this,&testApp::tuioRemoved);
    ofAddListener(tuio.cursorUpdated,this,&testApp::tuioUpdated);
    
    //フレームレート設定
    ofSetFrameRate(60);
    //背景を黒に
    ofBackground(0,0,0);
    
    //ポート番号3333で、TUIOの通信開始
    tuio.start(3333);
    //ログのテキストを空に
    log="";
    //画像ファイルを読み込み
    myImage.loadImage("images/photo.png");
}

void testApp::update(){
    //TUIOのメッセージを受信
    tuio.getMessage();
}

void testApp::draw(){
    //オブジェクトのリストを取得
    list<ofxTuioObject*> objectList = tuio.getTuioObjects(); 
    //リスト操作のためのイテレータを準備
    list<ofxTuioObject*>::iterator tobj;
    //全てのオブジェクトをイテレータで操作
    for (tobj=objectList.begin(); tobj != objectList.end(); tobj++) {
        //物体を取得
        ofxTuioObject *blob = (*tobj);
        //座標を記録
        glPushMatrix();
        //座標を移動
        glTranslatef(blob->getX()*ofGetWidth(), blob->getY()*ofGetHeight(), 0.0);
        //ID番号を表示
        ofDrawBitmapString("id = " + ofToString(blob->getFiducialId(), 0), -64, 80);
        //回転
        glRotatef(blob->getAngleDegrees(), 0.0, 0.0, 1.0);
        ofSetColor(255, 255, 255);
        //画像を描画
        myImage.draw(-64, -64);
        //座標を復帰
        glPopMatrix();
    }
    
    //ログを表示
    ofSetColor(0xffffff);
    ofDrawBitmapString(log, 20, 20);
}

void testApp::keyPressed  (int key){
}
void testApp::mouseMoved(int x, int y ){
}
void testApp::mouseDragged(int x, int y, int button){
}
void testApp::mousePressed(int x, int y, int button){
}
void testApp::mouseReleased(){
}

void testApp::objectAdded(ofxTuioObject & tuioObject){
    //マーカー追加
    log = " new object: " + ofToString(tuioObject.getFiducialId())+
    " X: "+ofToString(tuioObject.getX())+
    " Y: "+ofToString(tuioObject.getY())+
    " angle: "+ofToString(tuioObject.getAngleDegrees());
}
void testApp::objectRemoved(ofxTuioObject & tuioObject){
    //マーカー削除
    log = " object removed: " + ofToString(tuioObject.getFiducialId())+
    " X: "+ofToString(tuioObject.getX())+
    " Y: "+ofToString(tuioObject.getY())+
    " angle: "+ofToString(tuioObject.getAngleDegrees());
}
void testApp::objectUpdated(ofxTuioObject & tuioObject){
    //マーカーの状態更新
    log = " object updated: " + ofToString(tuioObject.getFiducialId())+
    " X: "+ofToString(tuioObject.getX())+
    " Y: "+ofToString(tuioObject.getY())+
    " angle: "+ofToString(tuioObject.getAngleDegrees());
}
void testApp::tuioAdded(ofxTuioCursor & tuioCursor){
    //カーソル追加
    log = " new cursor: " + ofToString(tuioCursor.getFingerId())+
    " X: "+ofToString(tuioCursor.getX())+
    " Y: "+ofToString(tuioCursor.getY());
}
void testApp::tuioRemoved(ofxTuioCursor & tuioCursor){
    //カーソル削除
    log =  " cursor removed: " + ofToString(tuioCursor.getFingerId())+
    " X: "+ofToString(tuioCursor.getX())+
    " Y: "+ofToString(tuioCursor.getY());
}
void testApp::tuioUpdated(ofxTuioCursor & tuioCursor){
    //カーソル状態更新
    log =  " cursor updated: " + ofToString(tuioCursor.getFingerId())+
    " X: "+ofToString(tuioCursor.getX())+
    " Y: "+ofToString(tuioCursor.getY());
}

タンジブル・サウンドプレーヤー

応用例として、マーカーの位置や角度によって複数の音を同時再生するタンジブル・サウンドプレーヤーを作ってみましょう。

この例では、5つのofSoundPlayerを準備して、それぞれにサウンドファイルを読み込んでいます。そして、マーカーのX座標を音の左右の定位に、マーカーのY座標をオーディオの再生スピードに、マーカーの角度でサウンドのボリュームが変化するようにしています。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "ofxTuio.h"
//#include "CustomTuioClient.h"

#define NUM 5

//--------------------------------------------------------
class testApp : public ofSimpleApp{
    
public:
    
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased();
    
    void objectAdded(ofxTuioObject & tuioObject);
    void objectRemoved(ofxTuioObject & tuioObject);
    void objectUpdated(ofxTuioObject & tuioObject);
    
    void tuioAdded(ofxTuioCursor & tuioCursor);
    void tuioRemoved(ofxTuioCursor & tuioCursor);
    void tuioUpdated(ofxTuioCursor & tuioCursor);
    
private:
    myTuioClient tuio;
    string log;
    ofSoundPlayer mySounds[NUM];
};

#endif

testApp.cpp

#include "testApp.h"

void testApp::setup(){
    //イベントリスナーの追加
    ofAddListener(tuio.objectAdded,this,&testApp::objectAdded);
    ofAddListener(tuio.objectRemoved,this,&testApp::objectRemoved);
    ofAddListener(tuio.objectUpdated,this,&testApp::objectUpdated);
    ofAddListener(tuio.cursorAdded,this,&testApp::tuioAdded);
    ofAddListener(tuio.cursorRemoved,this,&testApp::tuioRemoved);
    ofAddListener(tuio.cursorUpdated,this,&testApp::tuioUpdated);
    
    //画面の基本設定
    ofBackground(0,0,0);
    ofSetFrameRate(60);
    ofSetCircleResolution(32);
    ofEnableAlphaBlending();
    ofSetVerticalSync(true);
    ofEnableSmoothing();
    
    //サウンドファイルの読み込み
    for (int i = 0; i < NUM; i++) {
        mySounds[i].loadSound("sounds/rainstick.aif");
        mySounds[i].setLoop(true);
    }    
    
    //TUIO通信開始、ポート3333
    tuio.start(3333);
}

void testApp::update(){
    //TUIOメッセージの受信
    tuio.getMessage();

    //オブジェクトのリストを取得
    list<ofxTuioObject*> objectList = tuio.getTuioObjects(); 
    //イテレータの準備
    list<ofxTuioObject*>::iterator tobj;
    //カウンター用変数
    int i = 0;
    //表示されている物体の数だけくりかえし
    for (tobj=objectList.begin(); tobj != objectList.end(); tobj++) {
        //個別の物体を取り出し
        ofxTuioObject *blob = (*tobj);
        //IDを抽出
        int id = blob->getFiducialId();
        //サウンドが設定されたIDの範囲なら
        if (id >= 0 && id < NUM) {
            //X座標を左右の定位に適用
            mySounds[id].setPan(blob->getX());
            //Y座標を再生スピードの適用
            mySounds[id].setSpeed((1.0f - blob->getY()) * 2.0);
            //角度をサウンドのボリュームに適用
            mySounds[id].setVolume(blob->getAngleDegrees()/360.0);
        }
    }
}

void testApp::draw(){
    //オリジナルの図形を描画
    //半径を20に
    float radius = 20;
    //オブジェクトのリストを取得
    list<ofxTuioObject*> objectList = tuio.getTuioObjects(); 
    //イテレータの準備
    list<ofxTuioObject*>::iterator tobj;
    //表示されている物体の数だけくりかえし
    for (tobj=objectList.begin(); tobj != objectList.end(); tobj++) {
        //個別の物体を取り出し
        ofxTuioObject *blob = (*tobj);
        //オリジナルの物体を描画
        glPushMatrix();
        glTranslatef(blob->getX()*ofGetWidth(), blob->getY()*ofGetHeight(), 0.0);
        glRotatef(blob->getAngleDegrees(), 0.0, 0.0, 1.0);
        ofFill();
        ofSetColor(0, 0, 255, 127);
        //サウンドのレベルによって半径を変化させて青い円を描く
        float* spectrum;
        spectrum = ofSoundGetSpectrum(1);
        ofCircle(0, 0, spectrum[0] * radius * 100.0);
        ofSetColor(255, 255, 255);
        ofNoFill();
        ofCircle(0, 0, radius);
        ofLine(0, -radius, 0, 0);
        glPopMatrix();
    }
    
    //ログを表示
    ofSetColor(0xffffff);
    ofDrawBitmapString(log, 20, 20);
}

void testApp::keyPressed  (int key){
}
void testApp::mouseMoved(int x, int y ){
}
void testApp::mouseDragged(int x, int y, int button){
}
void testApp::mousePressed(int x, int y, int button){
}
void testApp::mouseReleased(){
}

void testApp::objectAdded(ofxTuioObject & tuioObject){
    int id = tuioObject.getFiducialId();
    if (id >= 0 && id < NUM) {
        mySounds[tuioObject.getFiducialId()].play();
    }
    //マーカー追加
    log = " new object: " + ofToString(tuioObject.getFiducialId())+
    " X: "+ofToString(tuioObject.getX())+
    " Y: "+ofToString(tuioObject.getY())+
    " angle: "+ofToString(tuioObject.getAngleDegrees());
}
void testApp::objectRemoved(ofxTuioObject & tuioObject){
    int id = tuioObject.getFiducialId();
    if (id >= 0 && id < NUM) {
        mySounds[tuioObject.getFiducialId()].stop();
    }
    //マーカー削除
    log = " object removed: " + ofToString(tuioObject.getFiducialId())+
    " X: "+ofToString(tuioObject.getX())+
    " Y: "+ofToString(tuioObject.getY())+
    " angle: "+ofToString(tuioObject.getAngleDegrees());
}
void testApp::objectUpdated(ofxTuioObject & tuioObject){
    //マーカーの状態更新
    log = " object updated: " + ofToString(tuioObject.getFiducialId())+
    " X: "+ofToString(tuioObject.getX())+
    " Y: "+ofToString(tuioObject.getY())+
    " angle: "+ofToString(tuioObject.getAngleDegrees());
}
void testApp::tuioAdded(ofxTuioCursor & tuioCursor){
    //カーソル追加
    log = " new cursor: " + ofToString(tuioCursor.getFingerId())+
    " X: "+ofToString(tuioCursor.getX())+
    " Y: "+ofToString(tuioCursor.getY());
}
void testApp::tuioRemoved(ofxTuioCursor & tuioCursor){
    //カーソル削除
    log =  " cursor removed: " + ofToString(tuioCursor.getFingerId())+
    " X: "+ofToString(tuioCursor.getX())+
    " Y: "+ofToString(tuioCursor.getY());
}
void testApp::tuioUpdated(ofxTuioCursor & tuioCursor){
    //カーソル状態更新
    log =  " cursor updated: " + ofToString(tuioCursor.getFingerId())+
    " X: "+ofToString(tuioCursor.getX())+
    " Y: "+ofToString(tuioCursor.getY());
}

おまけ

いま、Microsoftのゲーム用センサーKinectをハックして作品に使用することが世界的なブームになっています。

ofxKinectというアドオンを使用すると、Kinectの入力をopenFrameworksで使用することが可能となります。Kinectを購入したので、実際にKinectで遊んでみましょう。

ofxKinect test from Atsushi Tadokoro on Vimeo.

ofxKinect Study #01 from Atsushi Tadokoro on Vimeo.

ofxKinect study #03 from Atsushi Tadokoro on Vimeo.

サンプルプログラムのダウンロード

今日とりあげたサンプルは下記からダウンロードしてください。


AS3でアニメーションを作成する 2 – 物体をなめらかに移動する

物体が移動するアニメーションを作る

先週の授業では、アニメーション作成の導入として、ムービークリップのrotationプロパティを操作して物体が回転するアニメーションを作成しました。今週はこの手法を応用して、物体がなめらかに移動するアニメーションを作成してみましょう。

物体を動いているように見せるには、物体の位置、つまり座標を変化させることで実現可能です。ムービークリップの二次元座標を変化させるには、x (横方向の位置), y (縦方向の位置) を操作します。

アニメーションのためのテンプレート

先週と同様に、ActionScript 3でアニメーションを実現するため、イベントリスナーを使用して画面の更新を行います。イベントリスナーの更新、また、そのイベントハンドラ(関数)を記入したテンプレートを用意しました。まずは、これをコピーして使用しましょう。

package {
  import flash.display.Stage;
  import flash.display.Sprite;
  import flash.events.Event;

  public class Main extends Sprite {
    //ここに変数を宣言する

    public function Main() {
      //初期化のための命令を記述

      //イベントリスナーの登録
      this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
    }

    function enterFrameHandler(event:Event):void {
      //更新する内容を記述
    }
  }
}

位置を移動する

では、さっそく位置を移動するプログラムを作成してみましょう。

いつものようにまずは、AS3形式でFLAファイルを作成し、ドキュメントクラスを「Main.as」に設定します。次に、FLAファイル側に新規にムービークリップシンボルを作成します。今回はムービークリップのクラス名は「Ball」と名付けています。

このムービークリップの座標(x, y)をそれぞれ、posX、posYという変数に格納するようにしています。また、x軸方向の移動スピードをspeedX、Y軸方向のスピードをspeedYという変数で変化させることにします。まず、Main() 関数で初期位置とスピードを設定した上で、ムービークリップをステージ上に表示しています。その後、定期的に呼びだされるイベントハンドラでは、以下の処理を行っています。

  • X方向の座標 posX を、1フレームで speedX 移動する – posX = posX + speedX
  • X方向の座標 posY を、1フレームで speedY 移動する – posX = posX + speedX
package {
  import flash.display.Sprite;
  import flash.events.Event;

  public class Main extends Sprite {

    //変数を宣言
    var posX:Number,posY:Number;//位置
    var speedX:Number,speedY:Number;//速度
    //ムービークリップをインスタンス化
    var ball:Ball = new Ball();

    public function Main() {
      //位置を初期化
      posX=0;
      posY=0;
      speedX = 4;
      speedY = 2;
      //ムービクリップを画面に表示
      this.addChild(ball);
      //イベントリスナーを登録する
      this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
    }

    function enterFrameHandler(event:Event):void {
      //座標を更新
      posX+=speedX;
      posY+=speedY;
      //更新した座標をボールのプロパティに適用
      ball.x=posX;
      ball.y=posY;
    }
  }
}
/wp-content/uploads/2010/11/Main1.swf, 400, 300

条件分岐:画面の端にきたら反対側の端へ

現状では画面の端からボールがはみ出ても、そのまま画面の外に向かって進みつづけ、ずっと見えない架空の画面外を進みつづけてしまいます。では、この状態を避けるために、画面の端にきたら反対側の端から出現するようにできないでしょうか。

そのために、常にENTER_FRAMEのイベントハンドラ内でボールの座標を監視して、もし画面の外に出たらその瞬間にボールを片方の端に移動するという処理を加えてみます。

AS3のプログラムで「もし〜ならば、…せよ」というような条件分岐を実現するには、if文という構文を使用します。if文は下記のような書式になっています。

if (《条件文》) {
    《真文 = 条件が正しかった際の処理》
} else {
    《偽文 = 条件が正しく無い際の処理》
}

画面からはみ出たら反対側から出現というのを、条件に分けて列挙すると次のようになるでしょう

  1. 左:(条件)もし x座標が0より小さくなったら → (処理) x座標を画面の幅の座標に
  2. 右:(条件)もし x座標が画面の幅の座標より大きくなったら → (処理) x座標を0に
  3. 上:(条件)もし y座標が0より小さくなったら → (処理) y座標を画面の高さの座標に
  4. 下:(条件)もし y座標が画面の高さの座標より大きくなったら → (処理) y座標を0に

AS3では、画面の幅と高さは次の式で取り出すことが可能です。

  • 画面の幅: stage.stageWidth
  • 画面の高さ: stage.stageHeight

以上のことを踏まえて、下記のようなコードを作成しましょう。

package {
  import flash.display.Stage;
  import flash.display.Sprite;
  import flash.events.Event;

  public class Main extends Sprite {

    //変数を宣言
    var posX:Number,posY:Number;//位置
    var speedX:Number,speedY:Number;//速度
    //ムービークリップをインスタンス化
    var ball:Ball = new Ball();

    public function Main() {
      //位置を初期化
      posX=0;
      posY=0;
      speedX=4;
      speedY=2;
      //ムービクリップを画面に表示
      this.addChild(ball);
      //イベントリスナーを登録する
      this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
    }

    function enterFrameHandler(event:Event):void {
      //座標を更新
      posX+=speedX;
      posY+=speedY;
      //更新した座標をボールのプロパティに適用
      ball.x=posX;
      ball.y=posY;
      //画面の端に来たら反対側から出現
      //1.左
      if (posX<0) {
        posX=stage.stageWidth;
      }
      //2.右
      if (posX>stage.stageWidth) {
        posX=0;
      }
      //3.上
      if (posY<0) {
        posX=stage.stageHeight;
      }
      //4.下
      if (posY>stage.stageHeight) {
        posY=0;
      }
    }
  }
}
/wp-content/uploads/2010/11/Main2.swf, 400, 300

画面の端でバウンドする

では、次に画面の端で反対側から出現するのではなく、バウンドするように改造してみましょう。

バウンドするという動作は、ボールのスピードの方向が反転するということを意味します。X軸方向のスピードが反転する際には、speedXが、-speedXに、Y軸方向のスピードが反転する際には、speedYが、-speedYになります。実際にプログラムで表現してみましょう。

package {
  import flash.display.Stage;
  import flash.display.Sprite;
  import flash.events.Event;

  public class Main extends Sprite {

    //変数を宣言
    var posX:Number,posY:Number;//位置
    var speedX:Number,speedY:Number;//速度
    //ムービークリップをインスタンス化
    var ball:Ball = new Ball();

    public function Main() {
      //位置を初期化
      posX=0;
      posY=0;
      speedX=4;
      speedY=2;
      //ムービクリップを画面に表示
      this.addChild(ball);
      //イベントリスナーを登録する
      this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
    }

    function enterFrameHandler(event:Event):void {
      //座標を更新
      posX+=speedX;
      posY+=speedY;
      //更新した座標をボールのプロパティに適用
      ball.x=posX;
      ball.y=posY;
      //画面の端に来たら反対側から出現
      //1.左か右
      if (posX<0 || posX > stage.stageWidth) {
        speedX *= -1;
      }
      //2.上か下
      if (posY<0 || posY > stage.stageHeight) {
        speedY *= -1;
      }
    }
  }
}           
/wp-content/uploads/2010/11/Main3.swf, 400, 300

CSS入門:情報の体裁を表現する

CSS (Cascading Style Sheet) 入門

今回は、Webページの情報の体裁(デザイン)を表現するための技術であるCSS (Cascading Style Sheet) の導入を行います。


openFramewrorks – addonを使う 1:ofxBox2Dで物理演算

アドオン(Addon)とは

今日の授業では、アドオンの利用について解説します。アドオン(addon)とはopenFrameworksに機能を拡張するためのライブラリーです。アドオンを追加することで、openFrameworks単体ではできなかった様々な機能を実現しています。また、アドオンはopenFrameworksの開発者以外でも独自に開発して追加することが可能となっており、様々な開発者がopenFrameworks向けのアドオンを公開しています。

OpenFramwroksのFAT版をインストールすると、既にいくつかのアドオンが付属してきます。ネットワーク通信から、画像解析、3次元モデルの読み込みなど、様々な領域をカバーしています。

  • ofxDirList ディレクトリの項目の一覧を生成
  • ofxXmlSettings アプリケーションの設定をXML形式で保存、読込み
  • ofxOsc Open Sound Control (アプリケーション間で主にサウンド情報をやりとりするプロトコル) をopenFrameworksで使用する
  • ofxOpenCv:画像処理・画像認識用のC言語ライブラリOpenCVを使用できるようにする
  • ofxNetwork:ネットワーク通信のプロトコル、TCPとUDPを使用可能にする、マルチキャストにも対応
  • ofxThred:クロスプラットフォームでスレッドの管理を実現
  • ofxVectorMath:ベクトルや行列の計算をする
  • ofxVectorGraphics:openFrameworksからPostscriptを生成し出力する
  • ofx3dModelLoader:3ds形式の3DモデルをopenFrameworksに読みこむ

また、FAT版には収録されていないアドオンも多数存在します。様々な分野のアドオンが活発に開発され、openFrameworksのフォーラムなどで発表されています。ネット上からは様々なサードパーティーのアドオンを入手可能です。

アドオンを使用してみる:ofxBox2dを使う

では実際にアドオンを使用してみましょう。この章では、ofxBox2dというアドオンを試してみます。ofxBox2dは、Box2DというライブラリをopenFrameworksで使用できるよう移植したものです。Box2Dは「物理エンジン」と呼ばれるライブラリの一つです。物理エンジンは、重力や衝突、摩擦といった物理計算を複雑な計算をすることなく利用できるようにしたライブラリです。Box2DはもともとはC++で書かれたライブラリですが、その便利さからActionScript3やJava、C#など様々な言語に移植されています。

Box2Dのソースコードは、Google Codeで管理されています。最新版をダウンロードするにはGoogle Codeのプロジェクトページから直接ダウンロードするのが良いでしょう。

このリストの中から、2010年11月現在最新のバージョンである、ofxBox2d_v2.1 (ofxBox2d_v2.1.zip) をダウンロードして、Zipファイルを解凍してください。解凍すると「ofxBox2d」というフォルダが生成されます。

ダウンロードしたアドオンを使用するには所定の場所にインストールする必要があります。先程ダウンロードした「ofxBox2d」を下記の場所にインストールしてください。

  • 《openFrameworksをインストールしたフォルダ》/addons/ofxBox2d

アドオンをopenFrameworksのプロジェクトに追加する

次にアドオンをプロジェクトに追加します。XCodeで「ファイル」→「新規プロジェクト」を選択し、プロジェクトタイプをopenFramework Applicationで新規のプロジェクトをプロジェクト名「ofxBox2dTest」で作成します。ここにofxBox2dを追加してみましょう。

まず、Xコード画面の左側のコラム、「グループとファイル」の欄の一番先頭にあるプロジェクト名「ofxBox2dTest」の表示されたアイコンをコントロール+クリック、もしくは右クリックします。表示されるメニューから「追加」→「新規グループ」を選択します。すると、「ofxBox2dTest」の下にフォルダが追加され、フォルダ名を入力するモードになります。ここでフォルダ名を「addons」に設定します。

次に作成したaddonsフォルダをコントロール+クリック、もしくは右クリックします。先程と同様にメニューが表示されます。今度は「追加」→「既存のファイル」を選択します。するとファイル選択のダイアログが表示されますので、先程addonsフォルダにインストールした「ofxBox2d」フォルダを選択してください。

設定は下記の画面のとおりにして、「追加」ボタンを押します。

最後に、グループとファイルの中のofxBox2dフォルダの中にある、「example」フォルダを選択し、デリートキーを押します。すると「参照を削除:この参照のみを削除しますか? オリジナルファイルもゴミ箱に入れますか?」という選択画面が表示されます。ここで「参照を削除」を選んでください。以上の作業を完了すると、グループとファイルの中身は、下記の画面のようになるはずです。これでプロジェクトにofxBox2dのアドオンが追加されました。

ofxBox2dを使用するには、もう1つofxVectorMathをアドオンとして追加する必要があります。ofxVectorMathは最初からopenFrameworksのFAT版に収録されているのでダウンロードする必要はありません。プロジェクトに追加すれば使用可能です。先程と同様の手順で、「addons」フォルダをコントロール+クリック、もしくは右クリックして、「追加」→「既存ファイルの追加」を選択します。ofxBox2dと同じ手順で、ofxVectorMathをフォルダごと選択してaddonsの中に追加します。ofxVectorMathフォルダの中にある「example」フォルダと「install.xml」ファイルもデリートキーで消去し「参照を削除」を選択します。最終的にグループとファイル内のaddonsフォルダの中身は下記のようになるでしょう。これで必要なアドオンの準備は完了です。

ofxBox2Dを利用したプログラム 1 – 落下する円

まずはじめに簡単なサンプルから始めましょう。画面をドラッグするとランダムに半径を設定した円が落下してくるというプログラムを作成しています。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "ofxVectorMath.h" //ofxVectorMathのライブラリを読込み
#include "ofxBox2d.h" //ofxBox2dのライブラリを読込み

class testApp : public ofBaseApp {
    
public:
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    
    ofxBox2d box2d; //Box2Dのインスタンス
    vector <ofxBox2dCircle *> circles; //円を格納するvector
};

#endif

testApp.cpp

#include "testApp.h"

void testApp::setup(){
    //画面設定
    ofSetVerticalSync(true);
    ofBackground(0, 0, 0);
    ofSetFrameRate(60);
    ofSetCircleResolution(16);
    
    //Box2Dの世界を初期化
    box2d.init();
    //重力を設定、下に5の力
    box2d.setGravity(0,1);
    //画面を壁で囲む
    box2d.createBounds(0, 0, ofGetWidth(), ofGetHeight());
    //box2Dの計算を10fpsで
    box2d.setFPS(10);
}

void testApp::update(){
    //Box2Dを更新
    box2d.update();
}

void testApp::draw(){
    //circlesに格納された全ての円を描画(イテレータ使用)
    for(vector <ofxBox2dCircle *>::iterator it = circles.begin(); it != circles.end(); ++it) {
        (*it)->draw();
    }
    
    //Box2Dで生成された図形を描画
    box2d.draw();
    
    //フレームレートと操作説明を表示
    ofSetColor(255, 255, 255);
    string info = "";
    info += "Press [c] : delete all circles";
    info += "\n\nFPS: "+ofToString(ofGetFrameRate());
    info += "\nNumber of circles: " + ofToString(circles.size(), 0);
    ofDrawBitmapString(info, 20, 30);
}

void testApp::keyPressed(int key){
    //cキーでcircle配列をクリア
    if (key == 'c') {
        //circles配列の全ての要素を消去する(イテレータ使用)
        for(vector <ofxBox2dCircle *>::iterator it = circles.begin(); it != circles.end();){
            (*it)->destroyShape();
            delete *it;
            it = circles.erase(it);
        }
    }
}

void testApp::keyReleased(int key){
}

void testApp::mouseMoved(int x, int y){
}

//マウスドラッグで、円を追加
void testApp::mouseDragged(int x, int y, int button){      
    //ofxBox2dCircle(円)クラスをインスタンス化
    ofxBox2dCircle* circle = new ofxBox2dCircle();
    //半径を設定
    float r = ofRandom(10, 30);
    //物理パラメータを設定(重さ、反発力、摩擦力)
    circle->setPhysics(1.0, 0.8, 0.5);
    //マウスの位置に円を設定
    circle->setup(box2d.getWorld(), mouseX, mouseY, r);
    //生成した円をcirclesに追加
    circles.push_back(circle);
}

void testApp::mousePressed(int x, int y, int button){
}

void testApp::mouseReleased(int x, int y, int button){
}

void testApp::windowResized(int w, int h){
}

testApp.hの冒頭で、#includeを使って、「ofxVectorMath.h」と「ofxBox2d.h」を読みこんでいるところに注目してください。このことで、ofxVectorMathとofxBox2dのアドオンの機能を利用できるようになります

#include "ofxVectorMath.h" //ofxVectorMathのライブラリを読込み
#include "ofxBox2d.h" //ofxBox2dのライブラリを読込み

パブリック変数の定義として、ofxBox2dをインスタンス化してbox2dを生成しています。このofxBox2dが様々な演算をする物理エンジン本体です。それに加えて、生成された円ofxBox2dCircleのポインタ型を格納するためのVecor「circles」を作成します。

ofxBox2d box2d; //Box2Dのインスタンス
vector <ofxBox2dCircle *> circles; //円を格納するvector

testApp.cppでは、setup()関数の中で、Box2Dの世界の様々な初期設定を行っています。「box2d.init()」でBox2Dの世界の初期化、「box2d.setGrabity(…)」で重力の設定(下向きに5の強さ)、「box2d.createBounds(…)」では、画面の上下左右を壁で囲んでいます。最後に、「box2d.setFPS(…)」で1秒間に描画するコマ数を設定しています。

//Box2Dの世界を初期化
box2d.init();
//重力を設定、下に5の力
box2d.setGravity(0,1);
//画面を壁で囲む
box2d.createBounds(0, 0, ofGetWidth(), ofGetHeight());
//box2Dの計算を10fpsで
box2d.setFPS(10);

次に、mouseDragged()関数の処理をみてみましょう。ここでは、マウスがクリックされる度に、新規の円の生成をしています。まず、半径をランダムに決定しています。次に「ofxBox2dCircle」クラスをインスタンス化して「circle」を生成しています。このofBox2dCircleは、Box2Dで定義された円形の物体です。この物体をBox2Dの世界に追加すると、重力や物体同士の衝突などが自動的に適用され計算をしてくれます。次にこの生成された円に基本的な物理パラメータを追加します。「circle.setPhysics(…)」で、重さ、反発力、摩擦力を設定しています。基本設定が完了したところで「circle.setup(…)」が円をBox2dの世界に追加しています。生成された円は、vectorであるcirclesにpush_back()して追加しています。

//マウスドラッグで、円を追加
void testApp::mouseDragged(int x, int y, int button){      
    //ofxBox2dCircle(円)クラスをインスタンス化
    ofxBox2dCircle* circle = new ofxBox2dCircle();
    //半径を設定
    float r = ofRandom(10, 30);
    //物理パラメータを設定(重さ、反発力、摩擦力)
    circle->setPhysics(1.0, 0.8, 0.5);
    //マウスの位置に円を設定
    circle->setup(box2d.getWorld(), mouseX, mouseY, r);
    //生成した円をcirclesに追加
    circles.push_back(circle);
}

update()関数とdraw()関数での処理はとてもシンプルです。update()関数内では、ofxBox2dクラスのインスタンスbox2dに対して、update()メソッドを呼びだすだけで全ての座標計算を自動的に行います。こうした面倒な計算を全て任せてしまうことが出来る部分が、物理エンジンを使う最大のメリットと言えるでしょう。

void testApp::update(){
    //Box2Dを更新
    box2d.update();
}

座標の計算が終了したら、draw()関数で作成した円を全て描画します。circlesに格納された全ての円をfor文を利用して描画しています。最後にofxBox2dのインスタンスbox2dに対して、draw()メソッドを呼びだしています。これは、box2d側で生成されたオブジェクト、このサンプルの場合は上下左右の壁や、マウスで円をドラッグした際に表示されるラインなどを描画しています。

void testApp::draw(){
    //circlesに格納された全ての円を描画(イテレータ使用)
    for(vector <ofxBox2dCircle *>::iterator it = circles.begin(); it != circles.end(); ++it) {
        (*it)->draw();
    }
    
    //Box2Dで生成された図形を描画
    box2d.draw();
}

ofxBox2Dを利用したプログラム 2 – 円や四角形を描画する

ofxBox2Dでは円以外にもいろいろな形態があらかじめ用意されています。あらかじめ利用可能な形態は下記のとおり沢山あります。

  • ofxBox2dCircle – 円
  • ofxBox2dRect – 四角形
  • ofxBox2dLine – 線
  • ofxBox2dJoint – 2つの物体を結ぶ線(ばね)
  • ofxBox2dPolygon – 多角形
  • ofxBox2dSoftBody – ばねを利用した、弾性をもった物体

次の例は、キーボードで選択することで、円と四角形を描くことができるというサンプルです。また、円や四角形位置や大きさは、マウスをドラッグすることで画面上に描くように設定することができるよう工夫しています。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "ofxVectorMath.h" //ofxVectorMathのライブラリを読込み
#include "ofxBox2d.h" //ofxBox2dのライブラリを読込み

class testApp : public ofBaseApp {
    
public:
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    
    ofxBox2d box2d; //Box2Dのインスタンス
    vector <ofxBox2dCircle *> circles; //円を格納するvector
    vector <ofxBox2dRect *> rects;
    bool drawing; //形を描き中かどうか
    ofPoint drawStart; //描き始めの場所
    int shape; //描く形 0:Circle、1:Rect
};
#endif

testApp.cpp

#include "testApp.h"

void testApp::setup(){
    //画面設定
    ofSetVerticalSync(true);
    ofSetCircleResolution(64);
    ofBackground(0, 0, 0);
    //描画は60fpsで
    ofSetFrameRate(60);

    //Box2Dの世界を初期化
    box2d.init();
    //重力を設定、下に5の力
    box2d.setGravity(0,5);
    //画面を壁で囲む
    box2d.createBounds(0, 0, ofGetWidth(), ofGetHeight());
    //Box2Dの計算を15fpsで
    box2d.setFPS(15);
    //描画中ではない
    drawing = false;
    //最初に描く形態は丸
    shape = 0;
}

void testApp::update(){
    //Box2Dを更新
    box2d.update();
}

void testApp::draw(){
    //描画中の形態を描く
    if (drawing) {
        ofSetColor(127, 127, 127);
        ofNoFill();
        if (shape == 0) {
            //描画中の円
            ofCircle(drawStart.x, drawStart.y, ofDist(drawStart.x, drawStart.y, mouseX, mouseY));
        } else {
            //描画中の四角形
            ofRect(drawStart.x, drawStart.y, mouseX - drawStart.x, mouseY - drawStart.y);
        }
    }
    
    //circlesに格納された全ての円を描画
    for(vector <ofxBox2dCircle *>::iterator it = circles.begin(); it != circles.end(); ++it) {
        (*it)->draw();
    }
    
    //rectsに格納された全ての四角形を描画
    for(vector <ofxBox2dRect *>::iterator it = rects.begin(); it != rects.end(); ++it) {
        (*it)->draw();
    }
    
    //Box2Dで生成された図形を描画
    box2d.draw();
    
    //ログと操作説明
    ofSetColor(255, 255, 255);
    string info = "";
    info += "Press [1] to draw circle";
    info += "\nPress [2] to draw rect";
    info += "\nPress [c] to delete all circles";
    info += "\nPress [r] to delete all rects";
    info += "\n\nFPS: "+ofToString(ofGetFrameRate());
    info += "\nNumber of circles: " + ofToString(circles.size(), 0);
    info += "\nNumber of rects: " + ofToString(rects.size(), 0);
    ofDrawBitmapString(info, 20, 30);
}

void testApp::keyPressed(int key){
    //1キーで丸を描くモードに
    if (key == '1') {
        shape = 0;
    }
    //2キーで四角形を描くモードに    
    if (key == '2') {
        shape = 1;
    }
    if (key == 'c') {
        //circles配列の全ての要素を消去する(イテレータ使用)
        for(vector <ofxBox2dCircle *>::iterator it = circles.begin(); it != circles.end();){
            (*it)->destroyShape();
            delete *it;
            it = circles.erase(it);
        }
    }
    if (key == 'r') {
        //rects配列の全ての要素を消去する(イテレータ使用)
        for(vector <ofxBox2dRect *>::iterator it = rects.begin(); it != rects.end();){
            (*it)->destroyShape();
            delete *it;
            it = rects.erase(it);
        }
    }
}

void testApp::keyReleased(int key){
}

void testApp::mouseMoved(int x, int y){
}

void testApp::mouseDragged(int x, int y, int button){
}

void testApp::mousePressed(int x, int y, int button){
    //物体を描き始めた地点を記録
    drawStart.x = x;
    drawStart.y = y;
    //描画中モードへ
    drawing = true;
}

void testApp::mouseReleased(int x, int y, int button){
    //マウスを離したら、物体を生成
    //形態のモードに応じた物体を生成
    if (shape == 0) {
        //円を生成
        //物体のサイズを計測
        float r = ofDist(drawStart.x, drawStart.y, x, y);
        //最低半径を10に
        r < 10? r = 10 : r;
        //Box2Dの円のインスタンスを生成
        ofxBox2dCircle *c = new ofxBox2dCircle();
        //物理特性の設定
        c->setPhysics(1.0, 0.8, 0.5);
        //世界に追加
        c->setup(box2d.getWorld(), drawStart.x, drawStart.y, r); 
        //ベクターに追加
        circles.push_back(c);
    } else {
        //四角形を生成
        //サイズを計測
        float w = abs(x - drawStart.x);
        float h = abs(y - drawStart.y);
        //最低の幅と高さを10に
        w < 10? w = 10 : w;
        h < 10? h = 10 : h;
        //Box2Dの四角形のインスタンスを生成
        ofxBox2dRect *r = new ofxBox2dRect();
        //物理特性の設定
        r->setPhysics(1.0, 0.8, 0.5);
        //世界に追加
        r->setup(box2d.getWorld(), drawStart.x, drawStart.y, w, h);
        //ベクターに追加
        rects.push_back(r);
    }
    drawing = false;
}

void testApp::windowResized(int w, int h){
}

カスタムの図形を生成する

ofxBox2D example from Atsushi Tadokoro on Vimeo.

ofxBox2dCircleは、ワイヤーフレームだけの円で少し味気ない感じもします。そこで、ofxBox2dCircleを継承したオリジナルのクラスを作成して、より表現に幅を持たせてみたいと思います。前回と前々回で解説した、オブジェクト指向のoFプログラミングでやった手順通りに新規にクラスを追加します。XCodeの「グループとファイル」内のsrcフォルダをコントロール+クリック、もしくは右クリックして、「追加」→「新規ファイル」を選択し、新規ファイルのテンプレート選択画面から「MacOSX」→「C and C++」→「C++ File」を選択し、ファイル名を「CustomCircle.cpp」に設定します。

testAppからは、ofxBox2dCircleを使用するのと全く同じやり方で、CustomCircleを使用することが可能です。これは、CustomCircleがofxBox2dCircleを継承したクラスなので、その性質をそのまま受け継いでいるからです。

CustomCircleクラスではまた、それぞれの物体の「寿命」を設定しています。これは、一定時間表示すると自動的に自らを消滅させるという機能です。そのために、CustomCircle自体には、生死を判定する「death」というBoolean型の変数を用意して、カウンターの値が一定値を越えると、「death = true」にしています。testAppクラスから、現在画面に表示しているCustomCircleのインスタンスのdeathの状態を常にupdate関数でチェックして、もし死んでいる物体があれば、オブジェクトを消去するという処理をしています。これによって、一定時間で消滅するという動きを実現しています。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "ofxVectorMath.h" //ofxVectorMathのライブラリを読込み
#include "ofxBox2d.h" //ofxBox2dのライブラリを読込み
#include "CustomCircle.h" //CustomCircleで作成したクラスを使用

class testApp : public ofBaseApp {
    
public:
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    
    ofxBox2d box2d; //Box2Dのインスタンス
    vector <CustomCircle *> circles; //CustomCircleを格納するvector
};

#endif

testApp.cpp

#include "testApp.h"

void testApp::setup(){
    //画面設定
    ofSetCircleResolution(16);
    ofBackground(0, 0, 0);
    //描画は60fpsで
    ofSetFrameRate(60);
    //画面の透明度を加算合成に
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);    
    //Box2Dの世界を初期化
    box2d.init();
    //重力を設定、下に1の力
    box2d.setGravity(0,0.5);
    //床を生成
    box2d.createFloor(ofGetWidth(), ofGetHeight());
    //Box2Dの計算を10fpsで
    box2d.setFPS(10);
}

void testApp::update(){
    for(vector <CustomCircle *>::iterator it = circles.begin(); it != circles.end();){
        //CustomCircleの状態を更新
        (*it)->update();
        //もし寿命が尽きていたら、CustomCircleを消去
        if ((*it)->dead) {
            //形態をBox2Dの世界から消去
            (*it)->destroyShape();
            //オブジェクトを解放
            delete *it;
            //動的配列から、オブジェクトを削除
            it = circles.erase(it);
        } else {
            ++it;
        }
    }
    box2d.update(); //Box2Dを更新
}

void testApp::draw(){
    //circlesに格納された全ての円を描画
    for(vector <CustomCircle *>::iterator it = circles.begin(); it != circles.end(); ++it) {
        (*it)->draw();
    }
    
    //Box2Dで生成された図形を描画
    box2d.draw();
    
    //ログと操作説明
    ofSetColor(255, 255, 255);
    string info = "";
    info += "Drag Mouse to draw circles";
    info += "\nPress [c] to delete all circles";
    info += "\n\nFPS: "+ofToString(ofGetFrameRate());
    info += "\nNumber of circles: " + ofToString(circles.size(), 0);
    ofDrawBitmapString(info, 20, 30);
}

void testApp::keyPressed(int key){
    //cキーでcircle配列をクリア
    if (key == 'c') {
        for(vector <CustomCircle *>::iterator it = circles.begin(); it != circles.end();){
            (*it)->destroyShape();
            delete *it;
            it = circles.erase(it);
        }
    }
}

void testApp::keyReleased(int key){
}

void testApp::mouseMoved(int x, int y){
}

void testApp::mouseDragged(int x, int y, int button){
    //半径をランダムに決める
    float r = ofRandom(5, 10);
    //Box2Dの円のインスタンスを生成
    CustomCircle *c = new CustomCircle();
    //物理特性の設定
    c->setPhysics(1.0, 0.8, 0.1);
    //世界に追加
    c->setup(box2d.getWorld(), x, y, r); 
    //力をランダムに加える
    float force = 150;
    c->addForce(ofPoint(ofRandom(-force, force), ofRandom(-force, 0)), 1);
    //ベクターに追加
    circles.push_back(c);
}

void testApp::mousePressed(int x, int y, int button){
}

void testApp::mouseReleased(int x, int y, int button){
}

void testApp::windowResized(int w, int h){
}

CustomCircle.h

#include "ofxVectorMath.h"
#include "ofxBox2d.h"

//ofxBox2dCircleを継承したクラスCustomCircleを定義
class CustomCircle : public ofxBox2dCircle {
public:
    CustomCircle(); //コンストラクタ
    void update(); //カウンタ更新
    void draw(); //円を描画する
    float counter; //カウンタ
    float phase; //初期位相
    int lifeTime; //表示される長さ
    bool dead;//生死の判定
    
};

CustomCircle.cpp

#include "CustomCircle.h"

CustomCircle::CustomCircle()
{
    //現在の年齢
    counter = 0;
    //初期位相
    phase = ofRandom(0, PI*2);
    //寿命を設定
    lifeTime = 300;
    //生死の判定
    dead = false;
}

void CustomCircle::update()
{
    
    //寿命が尽きたら死亡
    if (counter > lifeTime) {
        dead = true;
    }
    //画面からはみ出たら死亡
    if(getPosition().x < 0 || getPosition().x > ofGetWidth() || getPosition().y > ofGetHeight())
    {
        //死亡宣告
        dead = true;
    }
    //年齢を追加
    counter++;
}

void CustomCircle::draw()
{
    //半径を取得
    float radius = getRadius();
    float r = abs(sin(counter * 0.03 + phase)) * radius + radius * 0.25;
    //座標を変更
    glPushMatrix();
    //物体の位置に座標を移動
    glTranslatef(getPosition().x, getPosition().y, 0);
    ofFill(); //色を塗り潰す
    //パーティクルを描く
    ofSetColor(127, 255, 255, 24); 
    ofCircle(0, 0, radius*2.0);
    ofSetColor(31, 127, 255, 127); 
    ofCircle(0, 0, r);
    ofSetColor(255, 255, 255); 
    ofCircle(0, 0, r * 0.25);
    //座標を復元
    glPopMatrix();
}

衝突を検知する

物体の衝突を検知することも可能です。下記の例では、衝突を検知するクラス b2ContactListener を継承した MyContactListener というクラスを作成し、物体同士が衝突した際に音を鳴らすようにしています。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "ofxVectorMath.h" //ofxVectorMathのライブラリを読込み
#include "ofxBox2d.h" //ofxBox2dのライブラリを読込み
#include "CustomCircle.h" //CustomCircleで作成したクラスを使用

//衝突を検出するためのリスナーのクラス
class MyContactListener : public b2ContactListener {
    
public:
    MyContactListener();
    void Add(const b2ContactPoint* point);		
    void Remove(const b2ContactPoint* point);
    
    ofSoundPlayer mySound;
};

// ----------------- testApp ---------------------
class testApp : public ofBaseApp {
    
public:
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    
    ofxBox2d box2d; //Box2Dのインスタンス
    vector <CustomCircle *> circles; //CustomCircleを格納するvector
    MyContactListener contacts; //衝突感知のリスナー
};

#endif

testApp.cpp

#include "testApp.h"

//衝突を検知するリスナー MyContactListener の実装
extern testApp *myApp;

MyContactListener::MyContactListener()
{
    mySound.loadSound("Tink.aiff");
}
void MyContactListener::Add(const b2ContactPoint* point){
	//衝突を検知したら、ランダムなピッチで音を鳴らす
    mySound.setSpeed(ofRandom(0.5, 2.0));
    mySound.play();
}

void MyContactListener::Remove(const b2ContactPoint* point){
}

// ----------------- testApp ---------------------
void testApp::setup(){
    //画面設定
    ofSetCircleResolution(16);
    ofBackground(0, 0, 0);
    //描画は60fpsで
    ofSetFrameRate(60);
    //画面の透明度を加算合成に
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);    
    //Box2Dの世界を初期化
    box2d.init();
    //重力を設定、下に1の力
    box2d.setGravity(0,0.5);
    //床を生成
    box2d.createFloor(ofGetWidth(), ofGetHeight());
    //Box2Dの計算を10fpsで
    box2d.setFPS(10);
    //衝突感知のリスナーを追加
    box2d.getWorld() -> SetContactListener(&contacts);
}

void testApp::update(){
    for(vector <CustomCircle *>::iterator it = circles.begin(); it != circles.end();){
        //CustomCircleの状態を更新
        (*it)->update();
        //もし寿命が尽きていたら、CustomCircleを消去
        if ((*it)->dead) {
            //形態をBox2Dの世界から消去
            (*it)->destroyShape();
            //オブジェクトを解放
            delete *it;
            //動的配列から、オブジェクトを削除
            it = circles.erase(it);
        } else {
            ++it;
        }
    }
    box2d.update(); //Box2Dを更新
}

void testApp::draw(){
    //circlesに格納された全ての円を描画
    for(vector <CustomCircle *>::iterator it = circles.begin(); it != circles.end(); ++it) {
        (*it)->draw();
    }
    
    //Box2Dで生成された図形を描画
    box2d.draw();
    
    //ログと操作説明
    ofSetColor(255, 255, 255);
    string info = "";
    info += "Drag Mouse to draw circles";
    info += "\nPress [c] to delete all circles";
    info += "\n\nFPS: "+ofToString(ofGetFrameRate());
    info += "\nNumber of circles: " + ofToString(circles.size(), 0);
    ofDrawBitmapString(info, 20, 30);
}

void testApp::keyPressed(int key){
    //cキーでcircle配列をクリア
    if (key == 'c') {
        for(vector <CustomCircle *>::iterator it = circles.begin(); it != circles.end();){
            (*it)->destroyShape();
            delete *it;
            it = circles.erase(it);
        }
    }
}

void testApp::keyReleased(int key){
}

void testApp::mouseMoved(int x, int y){
}

void testApp::mouseDragged(int x, int y, int button){
    //半径をランダムに決める
    float r = ofRandom(5, 10);
    //Box2Dの円のインスタンスを生成
    CustomCircle *c = new CustomCircle();
    //物理特性の設定
    c->setPhysics(1.0, 0.8, 0.1);
    //世界に追加
    c->setup(box2d.getWorld(), x, y, r); 
    //力をランダムに加える
    float force = 150;
    c->addForce(ofPoint(ofRandom(-force, force), ofRandom(-force, 0)), 1);
    //ベクターに追加
    circles.push_back(c);
}

void testApp::mousePressed(int x, int y, int button){
}

void testApp::mouseReleased(int x, int y, int button){
    //半径をランダムに決める
    float r = ofRandom(5, 10);
    //Box2Dの円のインスタンスを生成
    CustomCircle *c = new CustomCircle();
    //物理特性の設定
    c->setPhysics(1.0, 0.8, 0.1);
    //世界に追加
    c->setup(box2d.getWorld(), x, y, r); 
    //力をランダムに加える
    //float force = 150;
    //c->addForce(ofPoint(ofRandom(-force, force), ofRandom(-force, 0)), 1);
    //ベクターに追加
    circles.push_back(c);
}

void testApp::windowResized(int w, int h){
}

サンプルプログラムのダウンロード

今日とりあげたサンプルは下記からダウンロードしてください。


自己紹介ページの講評会

今日の内容

前回は課題として「自己紹介」のWebページを作成するという出題をしました。今日の授業では、まず作成したページをWebサーバにアップロードします。その上で作成したページを講評していきます。

自己紹介のHTMLを準備する

作成した自己紹介のHTMLファイルを、準備します。もしページに画像などを貼りつけた場合は、画像ファイルも忘れずにファイル一式をまとめておくようにします。

作成したHTMLファイルは、例えば以下のようなものになるでしょう。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>自己紹介:田所淳</title>
  </head>
  <body>
    <h1 id="title">田所 淳</h1>
    <p>千葉商科大学 政策情報学部 3年<br />学籍番号:0123456789</p>
    <h2>略歴</h2>
    <p><img src="img/portrate.jpg" alt="田所淳 - 写真" /></p>
    <ul>
      <li>1972年4月29日千葉県松戸市に生まれる</li>
      <li>1996年3月慶應義塾大学政策メディア研究科修士課程修了</li>
      <li>1996年〜1999年 NTTラーニングシステムズ入社 (1997年からはICC事業部にて勤務)</li>
      <li>1999年3月退社、同年3月有限会社バスキュールデザイン入社 (現在は株式会社バスキュール)</li>
      <li>2001年、有限会社バスキュールデザイン退社、フリーランスとなり現在に至る</li>
      <li>2001年9月〜 多摩美術大学非常勤講師</li>
      <li>2002年4月〜 千葉商科大学非常勤講師</li>
    </ul>
    <p>大学ではコンピュータ音楽を専攻していました。当時はマシンパワーもまだ非力だったので、あらかじめプログラムして生成した音響をミックスしたものをDATに録音してテープ音楽として発表するという形式が主でした。その後、音響と映像の融合といったテーマに興味が移り、描いた図形を音響に変換するような簡易UPICのようなプログラムを開発したりしていました。現在のような、リアルタイムに映像と音響を同時にパーソナルなノートブックで扱える環境は、当時からすると夢のようです。</p>
    <p>紆余曲折あり、多摩美の非常勤講師と並行して、現在はFlashを中心としたWebデザイン、プログラミングで生計をたてています。</p>
    <h2>Bookmarks</h2>
    <ul>
      <li>yoppa:<a href="https://yoppa.org/">https://yoppa.org/</a></li>
      <li>mixi: <a href="http://mixi.jp/show_friend.pl?id=59319">http://mixi.jp/show_friend.pl?id=59319</a></li>
      <li>tumblr: <a href="http://yoppa.tumblr.com/">http://yoppa.tumblr.com/</a></li>
      <li>twitter: <a href="http://twitter.com/tadokoro">http://twitter.com/tadokoro</a></li>
      <li>picasa: <a href="http://picasaweb.google.com/tadokoro">http://picasaweb.google.com/tadokoro</a></li>
      <li>flickr: <a href="http://www.flickr.com/photos/70636282@N00/">http://www.flickr.com/photos/70636282@N00/</a></li>
      <li>del.icio.us: <a href="http://del.icio.us/tadokoro">http://del.icio.us/tadokoro</a></li>
      <li>SlideShare: <a href="http://www.slideshare.net/tado/slideshows">http://www.slideshare.net/tado/slideshows</a></li>
    </ul>
    <address>田所淳; 2010年11月10日更新; <a href="mailto:tadokoro@gmail.com">Atsushi Tadokoro</a></address>
  </body>
</html>

ページを公開する

作成したWebページを公開するには、Webサーバにファイルを転送する必要があります。ファイルの転送には、主にSFTPやFTPなどのプロトコル(通信の方式)を利用します。CUCの環境では、WinSCPというアプリケーションを用いてファイルを転送することが可能です。操作の詳細は「ICC Local Guide 2010 WWW version」のページで解説されています。この中の、「インターネット編 (PDFファイル)」の133ページにある「ファイルの保存、公開」を参照してください。

課題の講評会

作成したWebページを一人ずつみていきます。


AS3でアニメーションを作成する

AS3でアニメーションを作るには

今回の授業では、いよいよAS3を用いたアニメーションに挑戦します。タイムラインを用いたアニメーションではなく、プログラムだけでアニメーションを行うには、発想の方法自体を切り替えていく必要があります。

AS3でアニメーションを作成する場合は、いくつかのステップに整理して考えていくと良いでしょう。以下のようなステップを想定していきましょう。

  • 起動 – 初期設定:Flashを起動したら、最初に1度だけ実行される初期設定を行います。画面の基本状態や、アニメーションで更新しない要素などは初期設定で行います。
  • 更新するイベントを登録:アニメーションを生みだすための定期的に更新されるイベントを登録します。AS3ではこの仕組みをイベントリスナーと呼んでいます。
  • 状態を更新:登録されたイベントに1コマ単位で更新される情報を記述していきます。この状態の差分が動きを生みだします。状態の更新の命令は、指定した間隔でプログラムを停止するまで実行を繰り返します。

イベントとイベントリスナー

AS3では、様々な処理が「イベント」と呼ばれる機能から構成されます。

イベントとは、ユーザからのマウス操作や、キーボードからの入力などのインタラクションや、ネットワークを用いた外部データの読み込みなど様々な状態の変化を扱う処理を意味しています。

Flashの画面が更新された際にも、「onEnterFrame」というイベントが発生します。この画面更新を通知するイベントにあわせて状態を更新していくというのがAS3でのアニメーションの基本となります。

イベントを受けとるようにするには「イベントリスナー」という仕組みを用います。イベントリスナーとは、イベントを受けとった際に行う処理を登録する仕組みです。イメージとしては、ホテルなどの「受付」のようなものを想像してみてください。例えば、受付の担当者に「自分宛てに電話があれば、要件をメモして後から報告してください」と依頼したとします。この依頼と依頼する処理の内容がイベントリスナーに相当します。担当者は依頼された内容を記憶しており、実際に電話がかかってきた際には依頼された通りの手順でメモを残して後から報告します。

アニメーションを作成する際にも同様の考えかたをします。画面が更新された際に発生するイベント「onEnterFrame」を受けとった際におこなう処理を、あらかじめイベントリスナーとして登録します。プログラムが実行されると、このイベントリスナーに登録された処理が一定間隔の時間をおいて実行されることになります。これがアニメーションを生成するエンジンとなるのです。

イベントリスナーのイメージ

ドキュメントクラスを作る

まず最初に、前回と同様にFLAファイルと連動したドキュメントクラスを作成します。新規にAS3形式のFLAファイルを作成し、ドキュメントのプロパティからクラスを指定します(例: Main)。同じフォルダ内に、クラス名と同じファイル名(例:Main.as)で、ASファイルを生成し、以下のテンプレートとなる枠組みを記入します。

package {
  import flash.display.Sprite;
  import flash.events.Event;

  public class Main extends Sprite {
    public function Main() {

    }
  }
}

イベントリスナーを登録する

では早速、画面更新された際に発生するイベント「EnterFrame」を処理するためのイベントリスナーを登録してみましょう。イベントリスナーは、「this.addEventListener(イベントの種類, イベントを受けて実行する命令の名前)」という記述をします。

例えば、EnterFrameいべんとを受けとった際に、enterFrameHandler という名前の命令を実行するように設定する場合は以下のような記述となります。

this.addEventListner(Event.ENTER_FRAME, enterFrameHandler);

この処理をメインクラスに書き込んでみましょう。

package {
  import flash.display.Sprite;
  import flash.events.Event;

    public class Main extends Sprite {
        public function Main() {
            
            //イベントリスナーを登録する
            this.addEventListner(Event.ENTER_FRAME, enterFrameHandler);
        
        }
    }
}

次にこのイベントが発生した際に行う処理を記述する必要があります。AS3ではイベントを処理する命令の集合のことを、イベントハンドラと呼んでいます。EnterFrameを処理するイベントハンドラは下記のようになります。

function イベントハンドラ名(event:Event):void

先程設定したイベントハンドラ、enterFrameHandlerをメインクラスに追記してみましょう。

package {
    import flash.display.Sprite;
    public class Main extends Sprite {
        public function Main() {

            //イベントリスナーを登録する
            this.addEventListner(Event.ENTER_FRAME, enterFrameHandler);

        }
    }
}

function enterFrameHandler(event:Event):void {
    //イベントを受けとった際の処理を以下に記述する
}

ムービークリップを追加

まず最初に物体が回転する動きを作成してみましょう。Main.flaに、新規にムービークリップを作成します。作成したムービクリップは「Spinner」というクラス名でリンケージの設定を行います。

まず始めに、画面の中心にSpinnerを配置してみましょう。メインクラスに以下のように命令を追加してライブラリにあるムービクリップ「Spinner」を画面に追加しましょう。

package {
  import flash.display.Sprite;
  import flash.events.Event;
  
  public class Main extends Sprite {
    
    //Spinnerのインスタンス
    var spinner:Spinner;

    public function Main() {
      //Spinnerのインスタンスを生成
      spinner =  new Spinner();

      //画面の中心に移動
      spinner.x=200;
      spinner.y=150;

      //画面に追加
      this.addChild(spinner);

      //イベントリスナーを登録する
      stage.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
    }


    public function enterFrameHandler(e:Event):void {
      //イベントを受けとった際の処理を以下に記述する
    }
  }
}

回転角度を更新する

画面中央に追加したムービークリップを、フレームが更新するたびに少しずつ回転してみましょう。回転する命令は、enterFrameHandlerの中に記述していきます。すると、この角度の変化が回転するアニメーションになるのです。

package {
  import flash.display.Sprite;
  import flash.events.Event;
  
  public class Main extends Sprite {
    
    //Spinnerのインスタンス
    var spinner:Spinner;

    public function Main() {
      //Spinnerのインスタンスを生成
      spinner =  new Spinner();

      //画面の中心に移動
      spinner.x=200;
      spinner.y=150;

      //画面に追加
      this.addChild(spinner);

      //イベントリスナーを登録する
      stage.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
    }


    public function enterFrameHandler(e:Event):void {
      //イベントを受けとった際の処理を以下に記述する
      spinner.rotation+=10;
    }
  }
}
/wp-content/uploads/2010/11/Main01.swf, 400, 300

画面をクリックでスタート

回転スピードを徐々速くしていく

このプログラムを少し改造して、徐々に回転速度を上げていくムービーにしてみましょう。まず変数としてrotationSpeedというNumber型の変数を用意します。最初はこのスピードは0ですが、フレームが更新すると、徐々にスピードを上げていくようにしてみましょう。

package {
  import flash.display.Sprite;
  import flash.events.Event;
  
  public class Main extends Sprite {
    
    //Spinnerのインスタンス
    var spinner:Spinner;
    //回転スピード
    var rotationSpeed:Number;

    public function Main() {
      //Spinnerのインスタンスを生成
      spinner =  new Spinner();

      //画面の中心に移動
      spinner.x=200;
      spinner.y=150;

      //画面に追加
      this.addChild(spinner);
      
      //初期スピードを0に
      rotationSpeed = 0;

      //イベントリスナーを登録する
      stage.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
    }


    public function enterFrameHandler(e:Event):void {
      //イベントを受けとった際の処理を以下に記述する
      
      //指定スピードで回転
      spinner.rotation+=rotationSpeed;
      
      //スピードを増加させる
      rotationSpeed += 0.1;
    }
  }
}
/wp-content/uploads/2010/11/Main02.swf, 400, 300

画面をクリックでスタート

回転する物体の模様を工夫してみる

回転する物体にいろいろと模様を書き込むと、不思議な効果が生まれます。いろいろ工夫してみましょう。

/wp-content/uploads/2010/11/Main03.swf, 400, 300

画面をクリックでスタート

/wp-content/uploads/2010/11/Main04.swf, 400, 300

画面をクリックでスタート

サンプルファイルのダウンロード

今回の授業で取りあげたサンプルファイルは、下記からダウンロードしてください。