Closure Library で作る簡易ドローツール(Python Hack-a-thon #3 資料)
今週末の土曜日に開催される Python Hack-a-thon #3 にて、 JavaScript ハンズオンを担当させていただくことになりました。 Python Hack-a-thon でなぜ JavaScript という感じですが、そのあたりが日本の Python コミュニティーのおおらかさということでしょうか(笑)。
問題はその題材ですが、以前から気になっていた Closure Library を使って簡単なドローツールを作ることにしました。これなら見た目にも楽しいし、 Closure Library は日常業務でも役に立つでしょう。
そんなわけで、本日はそのテキスト作りも兼ねて Closure Library の使い方をご紹介します。
Closure Library とは
Closure Library は、 Google が開発しているオープンソース (Apache License 2.0) の JavaScript フレームワークです。 Closure Tools という一連の開発ツールキットの一部という位置付けで、他に以下のツールがあります。
- Closure Compiler (JavaScript ソースサイズ削減ツール)
- Closure Template (テンプレートライブラリ)
- Closure Inspector (デバッグ支援ツール)
最大の特徴は、その機能の豊富さです。リファレンスを見ていただければわかりますが、通常の DOM 操作やイベント処理はもちろん、各種 UI ウィジェット、国際化サポート、 AJAX、 JSON、プロトコルバッファ、テストフレームワークなど、ありとあらゆる機能が集約されています。ほとんどのアプリケーションはこれひとつでじゅうぶんに賄えるでしょう。
また、 Closure Library は実際に Gmail や Google Docs で使用されており、実績という面でも申し分ありません。機能の豊富さもそれらのニーズを満たすためのものと考えれば納得できますね。使い方をマスターすれば、日々のアプリケーション開発の強力な武器となってくれることは間違いありません。
本日の題材について
Closure Library には SVG, Canvas, VML を抽象化したグラフィックライブラリ goog.graphics が含まれています。本日はそれを使って簡単なドローツールを作ってみたいと思います。実現する機能は以下のとおりです。
- 描画できる図形は楕円のみ。
- マウスドラッグで楕円の描画、移動が行える。
- 線、塗り潰しの色を独立して変更できる。
- IE など、 SVG が使えないブラウザはサポート対象外(一部 SVG のみの機能を使用しているため)。
完成するとこうなります(IE では正常に動きません)。
完成したソースコードは以下の場所にあります。
実際に Closure Library を使った開発を体験していただき、今後本格的なアプリケーションを開発するための基礎知識の習得を目指します。最後まで進めていただければ、あとは必要な機能を自分で調べて使っていけるようになるでしょう。ぜひ順を追って作業をしてみてください。
開発環境の準備
最初に Closure Library を使ったアプリケーション開発のための環境構築をします。具体的には、 Closure Library 本体と Closure Compiler、それに JRE と Python のランタイムを(もしなければ)インストールします。
Closure Library 本体は、プロジェクトサイトからチェックアウトして入手します。以降では simple-draw というディレクトリにチェックアウトしたものと仮定します。
svn checkout http://closure-library.googlecode.com/svn/trunk/ simple-draw
これ以降はアプリケーションの完成後に JavaScript ソースを統合・圧縮してファイルサイズを削減するために必要なものです。 Closure Library を実験的に使ってみるだけであれば、なくてもかまいません。
Closure Compiler はダウンロードページで "compiler-latest.zip" をダウンロードし、展開します。ここでは上でチェックアウトした simple-draw ディレクトリ以下の closure/bin に展開したものとします。
cd simple-draw/closure/bin wget http://closure-compiler.googlecode.com/files/compiler-latest.zip unzip compiler-latest.zip
そして、必要なら JRE や Python のランタイムをインストールします。例えば Windows なら以下のページからインストーラがダウンロードできます。
- JRE : http://www.java.com/ja/download/
- Python : http://www.python.org/download/
以上で開発環境が整いました。
ファイルの雛形の作成
今回作成する簡易ドローツールは simple-draw.html と simple-draw.js の 2 つのファイルで構成されますので、これらの雛形を作成しましょう。いずれも先ほどチェックアウトした simple-draw ディレクトリの直下に配置します。
まずは simple-draw.html です。以下の内容で保存してください。
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <script src="closure/goog/base.js"></script> <script src="simple-draw.js"></script> </head> <body onload="initialize();"> </body> </html>
とくに説明の必要はないと思います。最初に読み込んでいる base.js は各モジュールの読み込みやその他の汎用ユーティリティーが定義されている JavaScript ファイルです。他の必要なモジュールは simple-draw.js 内で読み込みます。
次は simple-draw.js です。内容は以下のとおりです。
goog.require('goog.array'); goog.require('goog.dom'); goog.require('goog.events.EventType'); goog.require('goog.ui.SelectionModel'); goog.require('goog.ui.Toolbar'); goog.require('goog.ui.ToolbarToggleButton'); goog.require('goog.ui.ToolbarColorMenuButton'); goog.require('goog.fx.Dragger'); goog.require('goog.fx.Dragger.EventType'); goog.require('goog.graphics'); goog.require('goog.graphics.Font'); // 初期化 function initialize() { } goog.exportSymbol('initialize', initialize, window);
require は base.js で定義されているメソッドで、 Closure Library のモジュールをロードするためのものです。モジュール名を引数に指定してこのメソッドを呼び出せば、依存関係も含めて適切な JavaScript ファイルを読み込んでくれます。上記のコードでは、このサンプルで必要なすべてのモジュールを読み込んでいます。
最後に呼び出している exportSymbol メソッドは、 Closure Compiler によるシンボル名の短縮を避けて initialize 関数を外部から呼び出せるようにするものです。
図形を描画してみる
それでは、図形を描画してみましょう。そのためには、 HTML に描画領域となる div を挿入し、 JavaScript でそこへ描画します。 simple-draw.html に追加するコードは以下のとおりです。 body タグの内部に挿入してください。
<div style="float:left"> <div id="canvas" style="border: solid 1px black;"></div> </div>
<div id="canvas"> の内部に図形が表示されます(実際にはこの中に svg タグが挿入され、そこに図形が表示されます)。外側のラッパー div を float にしているのは、単にボーダーが横幅いっぱいに広がるのを防いでいるだけです。見た目の問題で、本来の動作とは関係ありません。
次は JavaScript で goog.graphics を初期化し、画面中央に円を表示します。
var graphics = null; var currentStroke = null; var currentFill = null; // 初期化 function initialize(){ // 描画領域の初期化 graphics = goog.graphics.createGraphics(512, 512); currentStroke = new goog.graphics.Stroke(2, '#000000'); currentFill = new goog.graphics.SolidFill('#ffff00'); // 円を表示(下の1行は次の段階に進む前に削除じてください) graphics.drawCircle(256, 256, 100, currentStroke, currentFill); var canvas = goog.dom.$('canvas') graphics.render(canvas); }
Closure Library には SVG (SvgGraphics), Canvas (CanvasGraphics), VML (VmlGraphics) のそれぞれの API を抽象化したクラス (AbstractGraphics) が用意されており、ブラウザがどの API をサポートしているかを(ほとんど)意識せずに描画を実行できます。最初に呼んでいる createGraphics メソッドは、それらのクラスから現在のブラウザがサポートしているものを選択してインスタンス化するものです。引数は描画領域のサイズです。
Stroke クラスと SolidFill クラスはそれぞれ線スタイルと塗り潰しスタイルを表現します。塗り潰しには SolidFill のほかに LinearGradient も指定できます。
図形の描画は、 AbstractGraphics に定義されている draw〜系のメソッドで行います。上記の例では、 drawCircle メソッドで円を描画しています。描画と言っても、このメソッドが行うのは図形インスタンスの作成と登録で、本来なら createCircle とでも呼ぶべきものです。実際このメソッドは EllipseElement のインスタンスを返し、それを利用して後から図形のパラメータを変更できます。
最後に描画領域の親になる DOM 要素を引数にして render メソッドを呼び出せば、ページ上に図形が描画されます。
マウスドラッグで楕円を描画する
図形が描画できたので、マウスを使って描画領域の自由な位置に楕円を描けるようにしましょう。マウスドラッグの追跡には Dragger クラスを使うのですが、このクラスは DOM 要素をドラッグするために設計されており、図形を直接ドラッグすることはできません。そこで、今回はダミーの DOM 要素をドラッグさせて、その座標だけを利用することにします。
ということで、まずは HTML にダミーのドラッグターゲットを記述します。 body タグ内の一番最後に、以下のように挿入してください。
<body onload="initialize();"> <!-- ... --> <div id="dummy-drag-target" style="display:none;"></div> </body>
HTML の変更はこれだけですので、 JavaScript の追加に移ります。必要なグローバル変数を定義し、描画領域にマウスダウンイベントのハンドラを登録します。
var dragger = null; var currentMode = 'ellipse'; var currentShape = null; // 初期化 function initialize(){ // ... goog.events.listen(canvas, goog.events.EventType.MOUSEDOWN, onMouseDownCanvas); }
グローバル変数のうち、 currentMode 後に使うものを先行して準備したもので、ここでは "ellipse" に固定です。
イベントハンドラの登録は events モジュールの listen メソッドで行います。このメソッドの書式は以下のとおりです。
goog.events.listen(target, type, listener, capture, scope)
引数 | 機能 |
---|---|
target | イベントハンドラを設定する DOM 要素。 |
type | イベントハンドラを識別する文字列。 |
listener | イベント発生時に実行する関数。 |
capture | ハンドラを設定するフェイズ。true ならキャプチャーフェイズ。 |
scope | イベントハンドラ実行時の this の値。 |
Closure Library では DOM イベント以外の任意のイベントも作成でき、そのハンドラの登録も同じ listen メソッドで行えます。イベント処理の詳細はこちらのドキュメントで解説されていますので、参考にしてください。
次は、描画領域内でマウスボタンが押された際のイベントハンドラを記述します。
// クライアント座標系から描画領域の座標系に変換 function clientToCanvas(x, y) { var scroll = goog.dom.getDocumentScroll(); var bounds = goog.dom.$('canvas').getBoundingClientRect(); return { x: x + scroll.x - bounds.left, y: y + scroll.y - bounds.top }; } // 描画領域内でマウスボタンが押された際の処理 function onMouseDownCanvas(e) { if(!dragger && currentMode == 'ellipse') { var pt = clientToCanvas(e.clientX, e.clientY); currentShape = graphics.drawCircle(0, 0, 1, currentStroke, currentFill); currentShape.setTransformation(pt.x, pt.y, 0, 0, 0); dragger = new goog.fx.Dragger(goog.dom.$('dummy-drag-target')); goog.events.listen(dragger, goog.fx.Dragger.EventType.DRAG, onDragCanvas); goog.events.listen(dragger, goog.fx.Dragger.EventType.END, onDragEnd); dragger.startDrag(e); } }
clientToCanvas 関数は、イベントオブジェクトが持っているクリック座標を描画領域の左上を原点とする座標系に変換する関数です。
onMouseDownCanvas がイベントハンドラ本体で、図形の作成とドラッグの準備を行っています。基本的には先ほどと同じように drawCircle メソッドで円を描画(というか作成)しているだけですが、描画位置の指定に setTransformation メソッドを使用しているのが異なる点です。これは各図形クラス共通の抽象基底クラスである Element のメソッドで、平行移動と回転を図形にします。こちらを使う方が、円(楕円)以外の図形のサポートが楽になるはずです。
次にドラッグの準備として、ドラッグする DOM 要素(ここでは単なるダミー)を引数にしてインスタンス化し、必要なイベントハンドラを登録しています。 DRAG はドラッグ中にマウスを動かしたとき、 END はドラッグが終了したときに発生するイベントです。そして最後に startDrag メソッドを呼べば、ドラッグが開始されます。
DRAG イベントの処理は以下のようになります。
// ドラッグによる図形描画の処理 function onDragCanvas(e) { if(dragger && currentShape) { var pt = clientToCanvas(e.clientX, e.clientY); var trans = currentShape.getTransform(); currentShape.setRadius( Math.max(1, Math.abs(pt.x - trans.getTranslateX())), Math.max(1, Math.abs(pt.y - trans.getTranslateY()))); } }
currentShape は onMouseDownCanvas 関数内で作成した EllipseElement インスタンスです。その setRadius メソッドを呼び出して、 X, Y 軸方向の半径を設定しています。 getTransform メソッドは座標変換の情報を格納した AffineTransform を取得するもので、その getTranslate[XY] メソッドで平行移動量(このサンプルに限っては楕円の中心座標に等しい)が得られます。
END イベントの処理は、単に dragger を破棄するだけです。
// ドラッグが終了した際の処理 function onDragEnd(e) { if(dragger) { dragger.dispose(); dragger = null; } }
以上で楕円描画の処理が完成しました。描画領域上でドラッグして、いろいろな楕円を描いてみてください。
ツールバーを表示する
次は描画した図形の移動を実装したいのですが、そのためには描画モードと移動モードを切り替える UI が必要です。描画領域の上にツールバーを追加して、そこにモード切り替えボタンを表示することにしましょう。
見栄えの良いツールバーを表示するためには、スタイル定義が必要です。ここでは、 Closure Library のデモで使われているスタイルをそのまま流用します。 head タグ内に以下の link タグを追加して、 CSS ファイルを読み込みます。
<link rel="stylesheet" type="text/css" href="closure/goog/css/toolbar.css">
ツールバー上のボタン類は JavaScript で追加することも可能ですが、静的なものなら HTML として用意する方が手軽です。描画領域の div の直前に、以下のようにして挿入してください。
<div id="toolbar" class="goog-toolbar"> <div id="toolbar-mode-move" class="goog-toolbar-toggle-button">移動</div> <div id="toolbar-mode-ellipse" class="goog-toolbar-toggle-button">◯</div> </div> <div id="canvas" style="border: solid 1px black;"></div>
ここでは、「移動」と「◯」の 2 つのボタンを持ったツールバーを定義しています。 goog-toolbar〜というクラスを持った div がツールバーのボタンとなり、クラスによってボタンの機能が変化します。 "goog-toolbar-toggle-button" は、 ON/OFF の状態を持つボタンです。ボタンの内部には、 div の内容がそのまま表示されます。上のコードでは単純な文字列ですが、任意のインライン要素が記述できるので、フォントを変えたり画像を含めたりできます。
必要なタグを用意したので、次は JavaScript でツールバーを初期化します。
var toolbar = null; // 初期化 function initialize(){ <!-- ... --> // ツールバーの初期化 toolbar = new goog.ui.Toolbar(); toolbar.decorate(goog.dom.$('toolbar')); // モード切り替えボタンの初期化 var selectionModel = new goog.ui.SelectionModel(); selectionModel.setSelectionHandler(onClickMode); goog.array.forEach(['move', 'ellipse'], function(id) { var button = toolbar.getChild('toolbar-mode-' + id); selectionModel.addItem(button); goog.events.listen(button, goog.ui.Component.EventType.ACTION, function(e) { selectionModel.setSelectedItem(e.target); }); }); selectionModel.setSelectedItem(toolbar.getChild('toolbar-mode-' + currentMode)); }
Toolbar クラスのインスタンスを作成し、先ほど定義したツールバーの div を引数にして decorate メソッドを呼べば、必要なタグが挿入されてツールバーとして使えるようになります。
ここでは「移動」ボタンと「◯」ボタンをラジオボタンのように排他的に制御したいので、 SelectionModel クラスを利用します。それぞれのボタンを addItem メソッドで追加して、ボタンが押された際に setSelectedItem メソッドを呼べば、ラジオボタンのような制御を自動で行ってくれます。ボタンの状態が変化した場合は、 setSelectionHandler メソッドで登録した関数が呼び出されます。ここでは、以下のような処理を行います。
// ツールバーのモード切り替えボタンが押された際の処理 function onClickMode(button, select) { if(button && /^toolbar-mode-(.+)/.exec(button.getId())) { if(select) currentMode = RegExp.$1; button.setChecked(select); } }
button には状態の変更が必要なボタンが、 select にはボタンの選択状態が true/false で渡されます。 currentMode を適切な値に変更し、選択状態を実際のボタンに反映させています。
これでツールバーとモード切り替えボタンの実装ができました。まだツールバーを付けただけなので機能は変わりませんが、「移動」ボタンがアクティブな間は描画領域内でドラッグしても楕円が描画されません。
図形をドラッグできるようにする
モード切り替えができるようになったので、図形をドラッグで移動する処理を実装しましょう。まずは、図形上でマウスボタンを押した際のイベントを登録します。
// 描画領域内でマウスボタンが押された際の処理 function onMouseDownCanvas(e) { if(!dragger && currentMode == 'ellipse') { // ... goog.events.listen( currentShape, goog.events.EventType.MOUSEDOWN, onMouseDownShape, false, currentShape); } }
これで、図形上でマウスボタンが押されると、 onMouseDownShape 関数が呼ばれるようになります。ただし、このようなイベント処理は SvgGraphics にしか実装されていないので、 IE などの SVG をサポートしないブラウザでは無視されてしまいます。これが今回のサンプルが IE で動かない所以です。逆に言うとこの部分さえ独自に実装すれば IE もサポートできるので、興味のある方は挑戦してみてください。
onMouseDownShape 関数の処理は以下のようになっています。
// 図形内でマウスボタンが押された際の処理 function onMouseDownShape(e) { if(currentMode == 'move') { if(!dragger && this) { currentShape = this; dragger = new goog.fx.Dragger(goog.dom.$('dummy-drag-target')); dragger.prevX = e.clientX; dragger.prevY = e.clientY; goog.events.listen(dragger, goog.fx.Dragger.EventType.DRAG, onDragShape); goog.events.listen(dragger, goog.fx.Dragger.EventType.END, onDragEnd); dragger.startDrag(e); } } }
this にはマウスボタンが押された図形が設定されていますので(イベントを登録する際の最後の引数に注目)、それをグローバル変数の currentShape に保存しておきます。そして図形描画と同じようにドラッグの開始処理を行っています。
また、 dragger.prev[XY] にマウス座標を格納していますが、これは正式な使い方ではありません。グローバル変数を用意するのが面倒だったので、 dragger に勝手にメンバを追加して保存しています。あまり行儀の良いやり方ではありませんが、サンプルということでご了承ください。
ドラッグ時のイベントのうち、 END (ドラッグ終了)イベントは図形描画と同じですが、 DRAG (ドラッグ中)イベントは別個のものとなっています。
// ドラッグによる図形移動の処理 function onDragShape(e) { if(dragger && currentShape) { var trans = currentShape.getTransform(); var newX = e.clientX - dragger.prevX + trans.getTranslateX(); var newY = e.clientY - dragger.prevY + trans.getTranslateY(); currentShape.setTransformation(newX, newY, 0, 0, 0); dragger.prevX = e.clientX; dragger.prevY = e.clientY; } }
基本的に、前回の呼び出しからの座標の変化を算出し、それを図形の平行移動量に足しているだけです。これでマウスによる図形の移動が実装できました。適当に図形を描画してから「移動」ボタンを押し、図形をドラッグしてみてください。
線色・背景色の変更
最後に、図形の色を変更する機能を追加します。 Closure Library には色選択のツールバーボタン (ColorMenuButton) が用意されているので、それを使えば簡単に実装可能です。
まずは色選択ボタン用の CSS ファイルの読み込みを HTML の head タグ内に追加します。
<link rel="stylesheet" type="text/css" href="closure/goog/css/colormenubutton.css">
ボタンの追加はツールバーの定義部分に div を追加するだけです。線と塗り潰しで別のボタンを用意しました。
<div id="toolbar" class="goog-toolbar"> <!-- ... --> <div id="toolbar-color-stroke" class="goog-toolbar-color-menu-button">線色</div> <div id="toolbar-color-fill" class="goog-toolbar-color-menu-button">背景色</div> </div>
そして、 JavaScript でこれらのボタンを初期化します。
// 初期化 function initialize(){ // ... // 色選択ボタンの初期化 var strokeButton = toolbar.getChild('toolbar-color-stroke'); strokeButton.setSelectedColor(currentStroke.getColor()); goog.events.listen(strokeButton, goog.ui.Component.EventType.ACTION, onChangeStrokeColor); var fillButton = toolbar.getChild('toolbar-color-fill'); fillButton.setSelectedColor(currentFill.getColor()); goog.events.listen(fillButton, goog.ui.Component.EventType.ACTION, onChangeFillColor); }
あまり説明は必要ないでしょうが、 setSelectedColor で色の初期値をボタンに設定し(ボタンの下部に自動的に表示されます)、色が変更された際のイベントハンドラを登録しています。イベントの処理は以下のようになります。
// 線色が変更された際の処理 function onChangeStrokeColor(e) { var color = e.target.getSelectedColor() || '#000000'; currentStroke = new goog.graphics.Stroke(2, color); if(currentMode == 'move' && currentShape) currentShape.setStroke(currentStroke); } // 背景色が変更された際の処理 function onChangeFillColor(e) { var color = e.target.getSelectedColor() || '#000000'; currentFill = new goog.graphics.SolidFill(color); if(currentMode == 'move' && currentShape) currentShape.setFill(currentFill); }
こちらも見ての通りです。選択された色を取得して currentStroke もしくは currentFill を作りなおし、もし移動モードでかつ currentShape が存在した場合は、その図形の色も変更しています。
以上で色変更は行えるようになりました。しかし、より親切な UI にするために、移動モードで図形をクリックした際に図形の色が色選択ボタン反映されるようにしましょう。
// 図形内でマウスボタンが押された際の処理 function onMouseDownShape(e) { if(currentMode == 'move') { if(!dragger && this) { // ... var strokeButton = toolbar.getChild('toolbar-color-stroke'); strokeButton.setSelectedColor(currentShape.getStroke().getColor()); var fillButton = toolbar.getChild('toolbar-color-fill'); fillButton.setSelectedColor(currentShape.getFill().getColor()); } } }
以上ですべて完成です。色を変更してから図形を描画したり、移動モードで図形をクリックしてから色を変更したりして、動作を確認してみてください。
JavaScript ファイルの圧縮
完成したページの読み込みの様子を Firebug などで見るとわかるのですが、現状では大量の JavaScript ファイルを読み込んでおり、非常に効率が悪くなっています。この状態でページを公開すると、レスポンスが悪くサーバーにも大きな負荷をかけることになります。
Closure Library のリポジトリに含まれている calcdeps.py を利用すると、この問題を解消できます。この Python スクリプトは require メソッドの呼び出しを追跡して、すべての必要な JavaScript ファイルを見つけ出して結合してくれます。その上で Closure Compiler を呼び出し、ソースを圧縮してサイズを削減することも可能です。
それを行うには、 simple-draw ディレクトリで以下のコマンドを実行するだけです。 simple-draw-min.js に圧縮後の JavaScript ファイルが出力されます。
cd simple-draw python closure/bin/calcdeps.py -i simple-draw.js -p . \ -o compiled -c closure/bin/compiler.jar \ -f "--compilation_level=ADVANCED_OPTIMIZATIONS" > simple-draw-min.js
それぞれのオプションの意味は以下のとおりです。 calcdeps.py の詳細は「Using the Dependency Calculation Script」を、 Closure Compiler については「Getting Started with the Closure Compiler Application」をご参照ください。
オプション | 機能 |
---|---|
-i | 圧縮する JavaScript ファイル |
-p | Closure Library のパス |
-o | 出力モード |
-c | Closure Compiler の指定 |
-f | Closure Compiler に渡すオプション |
あとは、圧縮後の JavaScript ファイルのみを読み込むように HTML を変更します。 head タグのみを抜き出すと以下のようになります。
<head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" type="text/css" href="closure/goog/css/toolbar.css"> <link rel="stylesheet" type="text/css" href="closure/goog/css/colormenubutton.css"> <script src="simple-draw-min.js"></script> </head>
Closure Compiler は未使用コードの削除なども行う強力なサイズ削減ツールで、規模の大きい Closure Library には必須です。アプリケーションのリリース時には必ずこの作業を行うようにしましょう。
参考資料
以上でごく簡単なドローツールを実装しましたが、あきらかにまだまだ機能不足ですね。もし時間が許せば、これをベースにしてさらに高度な機能を実装してみてください。以下に Closure Library の機能を調べる上で使えるページを挙げておきます。公式ドキュメント以外は Closure Library のリポジトリにも含まれています。
- 公式サイトのドキュメント
- API リファレンス
- 各種サンプルデモ(目次もあります。ただしすべてがリストアップされてはいないのでご注意を)
API リファレンスは非常に充実しているのですが、 HOW-TO 的なドキュメントが少ないので機能の概要を把握するまでが大変です。まずは自分が使いたい機能のサンプルを探して、そのコードを覗いてみるのが、一番の近道だと思います。
詳しくはこちらの記事をどうぞ!
この記事にコメントする