チュートリアル51:
JavaScript によるユーザインターフェイスの設計

jsui オブジェクトは、JavaScript を使った、Max 環境で使用するユーザインターフェイスオブジェクトの設計を可能にしてくれます。jsui オブジェクトのJavaScript の実装は、js オブジェクトと同様ですが、OpenGL コマンドによる 2D 及び 3D のベクタグラフィックス描画をサポートする API が追加されています。また、jsui オブジェクトウィンドウ内でのマウスによるインタラクションを取り扱うメソッドも含まれています。

JavaScript によって提供される利点に加え、jsui は UI の開発をフレキシブルに行うために、多くのビルトイン(組み込み)の機能を提供します。

  • jsui オブジェクトは、jsui オブジェクトボックスのサイズに比例してその形状を描画します。jsui オブジェクトのサイズ変更を行った場合、その中に描画される全ての要素は正しくサイズ変更されます。

  • jsui オブジェクトは、様々な種類に及ぶ単体のシェイプや描画プリミティブをサポートするベクタグラフィックス言語(OpenGL)によって動作します。加えて、多くの高レベルグラフィックス関数を利用できます。jsui オブジェクトはまた、画像のアンチエイリアスを実行することができるため、パフォーマンスの低下は伴いますが、可能な限り滑らかなオブジェクトを提供してくれます。

  • jsui オブジェクトは、オブジェクトボックスの境界を越えたシーンを描画することを可能にします。OpenGL 空間でカメラ位置を調整することによって、同じ UI オブジェクトの様々な「ビュー」(光景)を作り、管理することができます。

ユーザインターフェイスデザインでの jsui オブジェクトの使用に加え、単に Max パッチャー内でアルゴリズムによる描画操作を行うために組み込まれた OpenGL 描画エンジンとしてこのオブジェクトを使うこともできます。

このチュートリアルでは、あなたがすでにこのマニュアルの JavaScript チュートリアルに目を通していることを仮定しています。jsui オブジェクトは、そのグラフィック言語のほとんどが OpenGL 関数に基づいていますが、その詳細はこのチュートリアルの範囲を超えています。OpenGL ‘Redbook’はこれらの関数の標準的なリファレンスです。オンラインバージョンは次の URL で手に入れることができます。

http://www.opengl.org/documentation/red_book_1.0/

訳注:2007/3/27 現在、次の URL で入手できます。(上記 URL は無効になっています)
http://www.glprogramming.com/red/

また、次の URL で OpenGL 関係のドキュメント(英文)を見ることができます。
http://www.opengl.org/documentation/

日本語で読めるものとしては、新しい版(ver.2.0に対応)の訳が出版されています。

「OpenGL プログラミングガイド」ピアソンエデュケーション; 原著第5版
(2006/12/19)ISBN-10: 4894717239

jsui でサポートされる OpenGL API は jsui sketch というオブジェクトに含まれています。このオブジェクトはほとんどの OpenGL コマンド、及び記号定数を理解します。OpenGLコード(例えば、’Redbook’にある C言語のサンプルコード)と、 jsui JavaScript コードの sketch メソッドやプロパティとの間の変換は、次のようなガイドラインを守ることによって、ほとんど直接行うことができます。

  • すべての OpenGL コマンドは、jsui sketch オブジェクトでは小文字で表します。例えば、glColor() は、sketch.glcolor() になります。

  • OpenGL の記号定数は、小文字で表すことに加え、’GL’というプリフィックスを取り除きます。そのため、例えば、GL_CLIP_PLANE1 は clip_plane1() になります。

多くの高レベル描画、及びシェイプコマンドが利用できます。これによって、ユーザインターフェイス開発がスピードアップするでしょう。これらのコマンドのリストを含む jsui sketch のリファレンスは、Javascript in Max マニュアル(及び、jsui オブジェクトのヘルプパッチ)にあります。

jsui のアクション

・チュートリアルパッチ 51.Javascript UI.pat を開いて下さい。グリーン色の背景に明るい赤の円が格子状に並んでいる jsui オブジェクトがあることがわかると思います。オブジェクト内の円をクリックすると、円の色が暗めの赤に変わります。同じ円をもう一度クリックすると、色は明るい赤に戻ります。

・いくつかの円をクリックすると、その円はハイライト(濃い赤)します。パッチの右側の、router オブジェクトの上にある uslider オブジェクトを操作してみて下さい。どの円がクリックされている場合に、 router の下のどの uslider オブジェクトが上のスライダの値を反映しているか、その対応関係に注意して下さい。jsui オブジェクトに接続された clear と書かれているメッセージボックスをクリックして下さい。円はすべて薄い赤に変わり、router オブジェクトはもはや上の uslider オブジェクトからのメッセージを通さなくなります。0 0 11 1 1、などのようなリストを含んだメッセージボックスをクリックして下さい。jsui オブジェクトは更新され、斜めの列を表示します。router オブジェクトは上の1番目の uslider からのメッセージを下の1番目の uslider に渡すようになり、2番目以降に対しても同様に動作します。

この jsui オブジェクトは JavaScript コードを使って、Max の matrixctrl オブジェクトのいくつかの機能をエミュレートしています。横の列(columns) は router オブジェクトへの入力を表し、縦の行(rows)は出力を指定します。jsui は(入出力の状態を表すフォーマットの)リストを送ることによって router オブジェクトと通信しています。このリストは、router オブジェクトに対して、インレットで受け取ったメッセージを jsui オブジェクトのその時点の設定によって指定された適切なアウトレットへ送るよう命令するものです。

js オブジェクトと同様、jsui オブジェクトもJavaScript によって書かれたファイルからプログラムを読み込みます。このファイルは、サーチパス内のどこかに保存されているものです。jsui はグラフィカルオブジェクトなので、ファイル名をタイプするオブジェクトボックスはありません。その代わり、jsui オブジェクトのインスペクタを使って、JavaScript ソースファイルを指定します。

・チュートリアルパッチをアンロックして、jsui オブジェクトをハイライトさせて下さい。Object メニューから Get Info ... を選んで下さい。インスペクタが現れ、'JavaScript File' と書かれたテキストフィールドには jsui ソースファイルの名前 ('mymatrix.js') が表示されていると思います。

このインスペクタでは、オブジェクトの周りのボーダーのオン/オフだけでなく、オブジェクトのサイズをセットすることもできます。オブジェクトのボーダーを使用不可にし、Max パッチの背景色を jsui オブジェクトの背景色と合わせることによって、シームレスなユーザインターフェイスの設計を支援することができます。

描画コード

jsui オブジェクトはグラフィカルなため、オブジェクトをダブルクリックしても js オブジェクトのようにテキストエディタは開きません。その代わりに、open と書かれたメッセージボックスをクリックして下さい。JavaScript ファイル ('mymatrix.js') が書かれたテキストエディタが表示されます。このチュートリアルでは、JavaScript ファイルは、ディスク内のチュートリアルパッチと同じフォルダに保存されています。

js スクリプトと同様、この jsui オブジェクトのためのコードも、グローバルブロックから始まっています。このブロックでは、オブジェクトのインレットとアウトレットやコードで使用するグローバル変数を定義することができます。また、オブジェクトが初期化される際に実行したいコマンドを書いておくこともできます。

// インレットとアウトレット inlets = 1; outlets = 1; // グローバル変数 var ncols=4; // デフォルトの列数 var nrows=4; // デフォルトの行数 var vbrgb = [0.8,1.,0.8,0.5]; var vmrgb = [0.9,0.5,0.5,0.75]; var vfrgb = [1.,0.,0.2,1.]; // ステート配列の初期化 var state = new Array(8); for(i=0;i<8;i++) { state[i] = new Array(8); for(j=0;j<64;j++) { state[i][j] = 0; } } // jsui のデフォルトを 2d にセット sketch.default2d(); // グラフィックスの初期化 draw(); refresh();

この JavaScript コードでは、2つのグローバル変数(ncolsnrows) が定義され、すべての関数からアクセスできるようになっています。また、描画のための色の配列や、ユーザインターフェイスのどの円が「オン」でどの円が「オフ」かについての情報を保持するために使用する state 配列のために、いくつかのグローバルなArray オブジェクトが定義されています。

注:JavaScript での多次元配列は、Array オブジェクトの各々の要素それ自身が Array オブジェクトであるような Arrayによってメモリ割り当てが行なわれます(2次元以上が必要な場合も同様です)。これは、多次元配列を直接宣言することができる他のプログラミング言語(例えば C 言語など)を経験した方には、何か不自然なもののように感じられるかも知れません。私たちのグローバルブロックにある for() ループ はこの割り当てを行い、配列 state の全ての要素を 0 に初期化するものです。一度、多次元配列が作られてしまえば、通常のブラケット(角括弧)による書法(例えば、state[4][2] 等のような書法)を使って参照することができます

変数と配列の宣言に続いて、js オブジェクトのグラフィック動作についての3つのコマンドがあることがわかると思います。最初の、sketch.defalt2d() は、jsui オブジェクトに対して、2次元画面のグラフィックコマンドを送ることを前提としていくつかのデフォルト動作を初期化するよう命じています。これは、OpenGLレンダリングコンテキスト上のデフォルトビューをセットし、単に私たちがウィンドウにグラフィック要素を描画し始めることを容易にするいくつかのユーティリティルーチンを実行します。draw() コマンド(どのような名前をつけることもできます)は、メイングラフィック関数を参照するものです。このメイングラフィック関数は、jsui オブジェクトのユーザ・インターフェイスを描くために必要なすべてのコマンドを含むように書きます。refresh() コマンドは、OpenGL のバックバッファ(フリッカー(ちらつき)を防ぐために最初に描画が行なわれる場所)を実際のスクリーンディスプレイにコピーします。refresh() コマンドをコメントアウトすると、jsui オブジェクトが何かを表示しようとするのを妨げてしまいます。

・グローバルブロックの下にある、draw() 関数を調べてみましょう。これは、jsui に対して、スクリーンインターフェイスを描画するために必要な全てのコマンドを提供する関数です。

// draw -- メイングラフィック関数 function draw() { with (sketch) { // ポリゴンを描画する方法をセット // clear 色のセット glclearcolor(vbrgb[0],vbrgb[1],vbrgb[2],vbrgb[3]); glclear(); // 背景の消去 colstep=2./ncols; // column あたりどのくらい移動するか rowstep=2./nrows; // row あたりどのくらい移動するか for(i=0;i<ncols;i++) // columns を通した繰り返し { for(j=0;j<nrows;j++) // rows を通した繰り返し { // 描画ポイントの移動 moveto((i*colstep + colstep/2)-1.0, 1.0 - (j*rowstep +rowstep/2), 0.); if(state[i][j]) // 'on' の色の設定 { glcolor(vfrgb[0],vfrgb[1],vfrgb[2],vfrgb[3]); } else // 'off' の色の設定(vbrgb vfrgb の中間の色) { glcolor(vmrgb[0],vmrgb[1],vmrgb[2],vmrgb[3]); } circle(0.7/Math.max(nrows,ncols)); // 円の描画 } } } }

グラフィックスコマンド(すべて 'gl' で始まりますが、circle() コマンドと同様です)はすべて sketch オブジェクトのメソッド、およびプロパティです。これは、Math オブジェクトがほとんどの一般的な数学関数をカプセル化しているのと同じように、ほとんどの OpenGL APIをカプセル化したものです。

注:JavaScript の with() 文は、あるオブジェクトに属するプロパティとメソッド(この例では、OpenGL の機能を提供する sketch オブジェクト)を、すべてのコマンドにsketch という参照を行なわずに使えるようにします。with() を使わない場合、gcolor() の代わりに、sketch.gcolor()circle() の代わりに sketch.circle() 等のように書かなければなりません。この有用なトリックは、他のオブジェクト(例えば、Task や Patcher)に強く依存する関数でも使用することができます。

この draw() 関数は、いくつかのデフォルトの描画動作を設定し、配列 vbrgb で定義した色でウィンドウをクリアします。その後、オブジェクトのために定義した列の数(ncols)と行の数(nrows)に基づいて、配列 state を通した反復処理を行ないます。state の特定の要素が 0 (off) ならば、vmrgb で定義した色で円を描き、要素が 1 (on) の場合は、vfrgb の色で円を描きます。円の位置は行と列の数によって決定され、両軸が-1.0 と 1.0 の間でセットされる OpenGL ワールドの境界に基づいています(すなわち、jsui ウィンドウの中央は、OpenGL 座標では 0,0 になります)。

注:ワールドのサイズは-1.0 〜 1.0 の範囲の座標値に限定されるわけではありません。私たちのデフォルトビューポートは単に y の範囲が -1.0 〜 1.0 で、x の範囲がオブジェクトのアスペクト比に基づいてスケールされている画面の中央に私たちの視点をセットするだけのものです。チュートリアルのオブジェクトボックスはたまたま正方形なので、範囲は両軸で同じになります。ビューポートを変更すると(例えば、仮想の「カメラ」の位置を操作すると)、jsui オブジェクトボックスの中で見える座標が変更されます。

訳注:チュートリアルパッチをアンロックして、jsui の縦のサイズを変えてみると、高さ(y軸)によって座標が拡大、縮小されるのがわかります。幅(x軸)を変更した場合、座標の拡大、縮小は行なわれず、見える範囲が広がります。

パラメータの設定

・チュートリアルパッチで、ナンバーボックスを変更して行と列の数をセットして下さい。その値によって、オブジェクトが動的に円を 8 行 8 列まで追加する点に注意して下さい。JavaScript コードの rows()cols() 関数を見て下さい。関数が自分自身の変数を設定した後、bang() 関数を呼び出している点に注意して下さい。

// rows -- jsui の行の数を変更する function rows(val) { if(arguments.length) { nrows=arguments[0]; bang(); // 描画を行い、ディスプレイを更新する } } // cols -- jsui の列の数を変更する function cols(val) { if(arguments.length) { ncols=arguments[0]; bang(); // 描画を行い、ディスプレイを更新する } }

オブジェクトに対するMax からの変更(マウスイベントを含みます)が行なわれる毎に、その直後に呼び出される bang() 関数は、単に、グローバルブロックで行なったように draw()refresh() を呼び出しているだけです。これによって、jsui オブジェクトを更新し、そのウィンドウにグラフィカルな変更が反映されます。

// bang -- ディスプレイの描画とリフレッシュ function bang() { draw(); refresh(); }

必要に応じて描画を行なうことにより、オブジェクトが使用するプロセッサの時間の量を減らすことができます。

・チュートリアルパッチで、jsui オブジェクトに対する frgb および brgb メッセージのセットを行なう swatch オブジェクトを変更してみて下さい。JavaScript コードの中の、対応する関数(frgb() および brgb())を見て下さい。'off' を表す円の色(vmrgb)のための配列が、frgb、および brgb メッセージによってセットされる色のちょうど中間になっていることに注意して下さい。

// frgb -- 円の描画色(及びクリックされた時の描画色)の変更 function frgb(r,g,b) { vfrgb[0] = r/255.; vfrgb[1] = g/255.; vfrgb[2] = b/255.; vmrgb[0] = 0.5*(vfrgb[0]+vbrgb[0]); vmrgb[1] = 0.5*(vfrgb[1]+vbrgb[1]); vmrgb[2] = 0.5*(vfrgb[2]+vbrgb[2]); bang(); // 描画とディスプレイの更新 } // brgb -- 背景色の変更 function brgb(r,g,b) { vbrgb[0] = r/255.; vbrgb[1] = g/255.; vbrgb[2] = b/255.; vmrgb[0] = 0.5*(vfrgb[0]+vbrgb[0]); vmrgb[1] = 0.5*(vfrgb[1]+vbrgb[1]); vmrgb[2] = 0.5*(vfrgb[2]+vbrgb[2]); bang(); // 描画とディスプレイの更新 }

注:OpenGL における色は、0.0 〜 1.0 の範囲の4つの浮動小数点数の値として表現され、それぞれ赤、(red)、青(blue)、緑(green)、アルファ(alpha-透明度)の量に対応しています。これは、通常、整数 0 〜 255(アルファの値を持ちません)によって色を記述する多くのビデオシステムと対照的です。私たちの frgb() 及び brgb() 関数のほとんどの動作では、後者(swatch オブジェクトで使われるもの)を前者(jsuiオブジェクトが理解するもの)に変換しています。

マウスによるインタラクション

・チュートリアルパッチをアンロックし、jsui オブジェクトのサイズを変更して下さい(円のサイズが動的に変更されます)。パッチャーをロックして、マウスクリックによって、依然として正しい円の状態の変更が行なわれることに注目して下さい。再びパッチをロックして、JavaScript コードの onclick() 関数 を見て下さい。

onclick()ondblclick()ondrag() 関数が定義されている場合、jsui オブジェクトに対して、ユーザがオブジェクト上でマウスのクリック、ダブルクリック、ドラッグを行なったときにどうすれば良いかが命じられます。この関数は、いくつかのフラグ(マウスボタンが押されていたかどうか、[shift] キーの状態など)、およびアクションが起きたオブジェクトウィンドウ内の場所をアーギュメントとして呼び出されます。このチュートリアルの onclick() 関数では、マウスクリックが行なわれた座標の x および y に対応する、最初の2つのアーギュメントだけを使っています。

// onclick -- マウスクリックイベントへの対応 function onclick(x,y) { worldx = sketch.screentoworld(x,y)[0]; worldy = sketch.screentoworld(x,y)[1]; colwidth = 2./ncols; // ワールド座標による列の幅 rowheight = 2./nrows; // ワールド座標による行の高さ x_click = Math.floor((worldx+1.)/colwidth); // どの列をクリックしたか y_click = Math.floor((1.-worldy)/rowheight); // どの行をクリックしたか // クリックされた場所のステートを反転 state[x_click][y_click] = !state[x_click][y_click]; // クリックされた点の座標位置と// ステートを出力 outlet(0, x_click, y_click, state[x_click][y_click]); bang(); // 描画とディスプレイの更新 s}

OpenGL グラフィックスワールドは浮動小数点数による座標で定義されています(このケースでは、-1.0と1.0)。jsui マウス関数は、マウスイベントが発生した位置を、ピクセル(オブジェクトボックスの左上隅から数えます)による座標で返します。格子状に並んでいる円に対してマウスイベントを正しく評価するためには、これらの2つのシステム(ワールド座標とスクリーン座標)の間で値の変換を行う必要があります。sketch のメソッド worldtoscreen() および screentoworld() はこのような変換を行ってくれます。

worldx = sketch.screentoworld(x,y)[0]; worldy = sketch.screentoworld(x,y)[1];

クリックした場所の幅と高さを知ることができれば、x及びy軸方向の円の数によってそれを分割し、行と列の幅を得ることができます。

訳注:この次の式は個々の行、及び列の幅を求めています。「クリックした場所」ではなく「全体の」と考えた方が意味がわかりやすいでしょう。

 

colwidth = 2./ncols; // ワールド座標による列の幅 rowheight = 2./nrows; // ワールド座標による行の高さ

そして、マウスクリックが行われた座標を適用することによって、クリックされた位置から一番近い円を求めることができます。

x_click = Math.floor((worldx+1.)/colwidth); // どの列をクリックしたか y_click = Math.floor((1.-worldy)/rowheight); // どの行をクリックしたか

クリックした円が求められたら、配列 stateのそれに対応した要素を反転させます。

state[x_click][y_click] = !state[x_click][y_click];

配列 state を正しくセットした後、変更が行われた箇所に対応するリストを、jsui オブジェクトのアウトレットから Max へ送信します。そして、jsui オブジェクト自身に対して bang を送り、変更を反映するようにグラフィックスを更新します。

outlet(0, x_click, y_click, state[x_click][y_click]); bang();

onclick() 関数をローカルに設定している点に注意して下さい。これによって、Max パッチからの onclick メッセージによってトリガできないようになっています。

・JavaScript コード、および、それがパッチャーの中での jsui オブジェクトの動作に関わる方法に慣れ親しんで下さい。パッチの右側にある metro オブジェクトをアクティブにするために、toggle オブジェクトをクリックして下さい。これは、uslider オブジェクトからの入力をシミュレートするものです。値がマウスクリックによって渡され、リストとして出力されるまでの流れを確認する手助けとするために、JavaScript コードに post()文を置いてみて下さい。

まとめ

jsui オブジェクトは、JavaScript をプログラミング言語として利用し、カスタマイズ可能なユーザ・インターフェイスの設計、および実装を可能にしてくれる、非常にパワフルなツールです。プログラムのキーポイントは、メイン描画関数(jsui オブジェクトのグラフィカルな表示を行うための一連のコマンドを定義します)および、マウスインタラクション関数である、onclick()ondblclick()ondrag() が必要であるということです。覚えておかなければならない重要な点は、jsui の sketch オブジェクトで使われる OpenGL API と、Max環境の間には、色の表現の違い(OpenGL では浮動小数点数、Max では整数)、空間座標表現の違い(OpenGLでは浮動小数点数によるワールド座標、Maxではピクセルによるデカルト座標)があるということです。

コードリスト

// mymatrix.js // // matrixctrl のような、クリックで操作できる簡単な格子状スイッチのシミュレーション // // rld, 5.04 // // インレットとアウトレット inlets = 1; outlets = 1; // グローバル変数 var ncols=4; // デフォルトの列数 var nrows=4; // デフォルトの行数 var vbrgb = [0.8,1.,0.8,0.5]; var vmrgb = [0.9,0.5,0.5,0.75]; var vfrgb = [1.,0.,0.2,1.]; // ステート配列の初期化 var state = new Array(8); for(i=0;i<8;i++) { state[i] = new Array(8); for(j=0;j<64;j++) { state[i][j] = 0; } } // jsui のデフォルトを 2d にセット sketch.default2d(); // グラフィックスの初期化 draw(); refresh(); // draw -- メイングラフィック関数 function draw() { with (sketch) { // ポリゴンを描画する方法をセット // clear 色のセット glclearcolor(vbrgb[0],vbrgb[1],vbrgb[2],vbrgb[3]); glclear(); // 背景の消去 colstep=2./ncols; // column あたりどのくらい移動するか rowstep=2./nrows; // row あたりどのくらい移動するか for(i=0;i<ncols;i++) // columns を通した繰り返し { for(j=0;j<nrows;j++) // rows を通した繰り返し { // 描画ポイントの移動 moveto((i*colstep + colstep/2)-1.0, 1.0 - (j*rowstep +rowstep/2), 0.); if(state[i][j]) // 'on' の色の設定 { glcolor(vfrgb[0],vfrgb[1],vfrgb[2],vfrgb[3]); } else // 'off' の色の設定(vbrgb vfrgb の中間の色) { glcolor(vmrgb[0],vmrgb[1],vmrgb[2],vmrgb[3]); } circle(0.7/Math.max(nrows,ncols)); // 円の描画 } } } } // bang -- ディスプレイの描画とリフレッシュ function bang() { draw(); refresh(); } // rows -- jsui の行の数を変更する function rows(val) { if(arguments.length) { nrows=arguments[0]; bang(); // 描画を行い、ディスプレイを更新する } } // cols -- jsui の列の数を変更する function cols(val) { if(arguments.length) { ncols=arguments[0]; bang(); // 描画を行い、ディスプレイを更新する } } // list -- Max からの変更に応答して内部のステートを更新 function list(v) { if(arguments.length==3) // アーギュメントの数が正しくない場合は何もしない { // リストに基づいて内部のステートを更新 state[arguments[0]][arguments[1]]=arguments[2]; // アウトレットからリストをエコー出力 outlet(0, arguments[0], arguments[1], arguments[2]); } bang(); // ディスプレイの描画と更新 } // clear -- ステートを再初期化 function clear() { for(i=0;i<ncols;i++) { for(j=0;j<nrows;j++) { state[i][j]=0; // ステートを再初期化 } } outlet(0, “clear”); // router または matrix~ のダウンストリームをクリア bang(); // ディスプレイの描画と更新 }th = 2./ncols; // ワールド座標による列の幅 rowheight = 2./nrows; // ワールド座標による行の高さ x_click = Math.floor((worldx+1.)/colwidth); // どの列をクリックしたか y_click = Math.floor((1.-worldy)/rowheight); // どの行をクリックしたか // クリックされた場所のステートを反転 state[x_click][y_click] = !state[x_click][y_click]; // クリックされた点の座標位置と// ステートを出力 outlet(0, x_click, y_click, state[x_click][y_click]); bang(); // 描画とディスプレイの更新 } onclick.local = 1; // Max からのトリガを防ぐために関数をプライベートにする // ondblclick -- onclick()に処理を渡す function ondblclick(x,y) { onclick(x,y); } ondblclick.local = 1; // Max からのトリガを防ぐために関数をプライベートにする

参照

jsui JavaScript UI オブジェクト
js Max JavaScript オブジェクト
lcd パッチャーウィンドウ内でグラフィックスを描画
matrixctrl マトリックス・スイッチコントロール
router Max のメッセージルータ