イナヅマTVログ

Canvas 2Dライブラリを作ろうとして感じたこととDisplayObject

| 0件のコメント

誰の役にも立たないゴミな記事です。
しかも長い。
ボンクラ脳でせっかくあれこれ考えたので書くけど、読まなくてイイよ。

Canvas API

Canvasはなかなかステキなヤツです。
Flashの代わりにはなれないけど「近い」ことはできそうです。

iOS 8でWebGLも使えるようになり大きな足かせが取れ、ますます可能性が広がってきたと思います。

でも、Canvas APIを使って何かを作ろうとしてもローレベルなのでラップされたライブラリがあったりすると便利だったりします。
Canvas始めました – Canvas APIで遊んで(遊ばれて)わかったことをだらだらと
Canvas APIで悪戦苦闘しているオレがいます。

おれメモ, Canvas系JavaScriptライブラリ一覧
この記事に書いた他にもたくさんのライブラリが公開されています。

UnityもVersion 5でCanvas書き出しを提供するとアナウンスしてるので楽しみです。
Vimeo: [Unite Japan 2014]Unity と Web デプロイメントの未来[J][日付・音声差し替え]

今時のCanvas系ライブラリに求められているのはWebGLでレンダリングし非対応端末用にCanvas 2Dレンダリングをフォールバックとして用意するでしょうか。
レンダリングの仕組みが全然違うので言うほど簡単なことではないと思いますが、CreateJSが対応しているのはスゴいことです。
WebGL Support in EaselJS

実案件でCanvasを使ったりと状況はずいぶん変わったなぁと感じてます。
以前 Canvas API をさわってから時間が空いてしまいましたが少し仕事に余裕ができたのでまた遊んでみました。
今回はライブラリのようなものを作ってみることにしました。

作っているうちに DisplayObject な機能があると便利だと強く思いました。DisplayObjectは多機能です。
Adobe® Flash® Platform 用 ActionScript® 3.0 リファレンスガイド: DisplayObject

Canvas用ライブラリを作るために必要なこと

で、やっと本題です。
Canvas用ライブラリを作るために必要なことを考えてみました。
あらためて言うまでも無いですが既に多くのライブラリが存在するのでそちらを使うのが賢明です。
ライブラリを使い限られた時間をクリエティブに向け作品のブラッシュアップをする方が遙かに良いと思います。

Game Engineを自作するような人にとっては「何を今さら…」なことでしょう。

Canvas 2Dの話になります。

DisplayObjectな機能

Flashな人には馴染みのあるDisplayObject。
HTMLではDOM Element(Node)と同等でしょうか。

ライブラリに描画の元になる機能(Object)があると助かります。
Canvas用ライブラリを作ることの大半は「DisplayObjectな機能」実装になるのではないかと思いました。

DOM操作をする時には意識しなくて良いDOMの機能もCanvas上では必要であれば自前(JavaScript)で用意しなくてはなりません。
「必要であれば」としましたが殆どの場合必要になるのではないかと思います。

ActionScript DisplayObjectやDOM Elementの機能を全て実装するのは大変ですが、少なくとも以下の機能があると便利だし最低限どうにかなりそうです。
– 座標(x, y, [z])
– 回転(radian, degree)
– スケール(拡大 / 縮小)
– 透明度(opacity)
– 表示 / 非表示(visible / invisible)
– 重なり(レイヤー z-index)
– 親子(parent, addChild, appendChild)
– ユーザー操作(click, mousedown…)

描画済みオブジェクトはCanvas上では選択できずプロパティを取得することは不可能なので、全てメモリ上に保持することが必要です。
できるだけ効率的に…

座標

基点をどうするか悩みました。
x, y, (z)座標系の基点です。
zが加わると脳回路はショートするので2D(x, y)で考えています。

一般的にTOP LEFTの左上基点です。
DOM Element, Flash DisplayObjectもそうなっています。

ですが、回転計算が容易に思えたため描画オブジェクト矩形(bounding box)中心を基点にしました。
慣れれば使えます。
個人的なライブラリだから許されることで一般公開用の汎用ライブラリだと問題になりそうです。

回転

角度はradianを内部計算で使用しています。
degreeの方が自分は分かりやすいので変換関数を用意しました。

// degree to radian
var deg2rad = function ( degree ) {
    return degree * Math.PI / 180;
};
 
// radian to degree
var rad2deg = function ( radian ) {
    return radian * 180 / Math.PI;
};

Shape(context.fill, context.stroke) と Bitmap(context.drawImage) は実装を分けました。
*便宜的にShape, Bitmapと表記しています。
*context は CanvasRenderingContext2Dです。

var context = document.getElementById( "canvas").getContext( "2d" );

Shapeは moveTo, lineTo する描画ポイント(座標)の回転後の位置計算をし描画しました。
これなら context.save, context.restore が不要になるかと考えたからです。

Bitmap は context.translate, context.rotate し描画しました。
描画前に context.save, context.setTransform( 1, 0, 0, 1, 0, 0 ) を行い、描画後に context.restore しています。

分けなくても良かったのかもと今となっては考えたりします。

スケール(拡大 / 縮小)

Shapeは各描画ポイントのスケール後の位置を計算し描画しました。
Bitmapは drawImage の第6引数から第9引数の x, y, width, height のスケール後の位置とサイズを計算し描画しました。
msdn: drawImage method

CanvasRenderingContext2D.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);

透明度

Shapeは単色で考えました。
単色なら色指定に rgba が使えるので簡単に透明度(アルファ, opacity)設定が可能です。

Bitmapは context.globalAlpha を使います。
描画前に context.save を行い、描画後に context.restore しています。

表示 / 非表示

visibleプロパティをデフォルト true で設定しました。
俗に言う「フラッグ」で描画を する / しない を分岐しました。

スケール: 0, 透明度: 0 も描画の対象から外しました。

重なり

コード内部では描画順になります。
当たり前のことですが先に描画したものは後から描画したものの下になり一部あるいは全体が隠れてしまいます。

重なりを変えるということは描画順を変えることになります。
描画順を変えるためには描画オブジェクトを管理する必要が出てきます。

Scene という概念を取り入れました。
描画オブジェクトは全て Scene へ追加し描画させることとしました。
Scene は全ての描画オブジェクトを保持することになるため、描画順の管理も可能になります。

Scene を複数用意すれば画面遷移のようなこともできるようになります。

親子

これ難しかった。

canvas に CanvasRenderingContext2D は一つです。
CanvasRenderingContext2D でしか描画はできません。
描画時の座標はcanvasに対するグローバル座標になります。

親子関係(addChild, appendChild)時の子の座標は親に対する座標になります。
ローカル座標です。
しかし描画時には canvasに対するグローバル座標 にする必要があります。

また、子のグローバル座標は親の 座標、回転、スケール に影響を受けます。
この時、子のローカル座標は変化しません。

親 -> 子 -> 孫
と入れ子になるとどうなる?
とか考えてると軽いパニックです。

ActionScript DisplayObject には 2D系では globalToLocal, localToGlobal が用意されています。
DisplayObject.localToGlobal
記憶に間違いが無ければ、このlocalToGlobalメソッドは処理が重くボトルネックになりやすく使いどころが難しかったと思います。

子オブジェクトは常にローカル座標とグローバル座標を保持することにしました。
孫オブジェクトは子オブジェクトのグローバル座標から自身のグローバル座標を計算することになります。

既存のCanvas系ライブラリがどのように解決しているのか参考にしようとしましたがコードが入り組んでいて正直良くわかりませんでした。

ユーザー操作

例えばユーザーの click(touchstart) に反応させたいとします。

描画オブジェクト.addEventListener( “click”, onClick );

のように書けると便利なのは良くわかっているのですが…
描画オブジェクトはcanvas上で一枚のbitmapデータになるので正攻法ではできません。

clickイベントはcanvasタグ上あるいはdocument.bodyで取得可能です。
イベント発生時の座標も分かります。
その座標からcanvasのオフセットを減算するとcanvas上のグローバル座標が計算できます。
計算後の座標を使い反応すべき描画オブジェクトがあるか否かの判定が可能になります。
【Canvas練習ノート】マウス(タッチ)操作 – ドラッグとか

当たり判定と言うかcontain(含まれる)判定が必要です。
苦手な数学・三角関数を嫌と言うほど使わないといけないことを理解しました。

余談ですが、数学、三角関数の情報はネット上に満ち溢れています。
しかも情報の鮮度は劣化せず不変です。

プログラム, HTML, CSS, アプリ関連の情報は日付を確認しないと現在では使えない、もっとも酷い場合は悪影響を与えることもありますがそのようなことがありません。

Google先生に聞けばほぼ解決可能です。
後はスクリプト実装するだけです。
これがオレスキルだとかなり大変だけど。

描画オブジェクト.addEventListener( “click”, onClick ) の準備はできたのですがスクリプトが複雑になりそうなため実装は諦めました。
その代わり座標にヒットする描画オブジェクト(InteractiveObject)を重なりが上位のものから順に配列で返すことにしました。

繰返し処理

一回表示して終わりで無い限り描画ループの機能が必要になります。
一回表示して終わりだったら画像を使えば良いので canvas で描画する限り「繰返し処理」は必須とも言えます。

requestAnimationFrame
requestAnimationFrame, cancelAnimationFrame を使うことになります。

requestAnimationFrameは一カ所だけで使うのが良さそうです。
描画やユーザー操作判定などをここで行います。

ライブラリ側に機能を取り込むか使用側に委ねるかはどちらでも良いのかなと思います。
ただブラウザ毎に若干違う実装は吸収しておくと良いかと思います。

と思ったら three.js はr68で “Removed shims for console and requestAnimationFrame” らしく互換コードの記述を削除していました。
https://github.com/mrdoob/three.js/releases/tag/r68
がんがん古いブラウザを切り捨ててるなぁ。

削除前、three.jsではrequestAnimationFrameに関するコードは以下のようになっていました。

var lastTime = 0;
var vendors = [ 'ms', 'moz', 'webkit', 'o' ];
 
for ( var x = 0; x < vendors.length && !self.requestAnimationFrame; ++ x ) {
 
    self.requestAnimationFrame = self[ vendors[ x ] + 'RequestAnimationFrame' ];
    self.cancelAnimationFrame = self[ vendors[ x ] + 'CancelAnimationFrame' ] || self[ vendors[ x ] + 'CancelRequestAnimationFrame' ];
}
 
if ( self.requestAnimationFrame === undefined && self.setTimeout !== undefined ) {
 
    self.requestAnimationFrame = function ( callback ) {
 
        var currTime = Date.now(), timeToCall = Math.max( 0, 16 - ( currTime - lastTime ) );
        var id = self.setTimeout( function() { callback( currTime + timeToCall ); }, timeToCall );
        lastTime = currTime + timeToCall;
        return id;
    };
 
}
 
if( self.cancelAnimationFrame === undefined && self.clearTimeout !== undefined ) {
 
    self.cancelAnimationFrame = function ( id ) { self.clearTimeout( id ); };
}

Canvas用ライブラリを使う

既存のCanvas用ライブラリは何らかの方法でCanvas APIだけでは足りないことを解決してると思います。
便利になればなるほどコードは増えていくと予想されます。
極論するとCanvas系ライブラリはFlashやブラウザの機能をJavaScriptでエミュレートさせていると思えます。

ライブラリがどこまで面倒を見るのか、回転や親子機能をどのように実装するかはライブラリ開発者の設計思想に依存しそうです。
ライブラリが使いにくいと感じる時は自身の開発スタイルがあっていないので、スタイルを変えるか他のライブラリに乗り換えを考えたほうが良いのかも。

ひょとっすると将来はどれかがデファクトになるのかもしれませんし、そうならないのかもしれません。
殆どのライブラリがマイナーバージョンの現在が一番面白いのかもです。

Canvas再生環境が整い始めた今、現場にいられることはとても幸せです。

と、一月ほどCanvas APIでゴニョゴニョした感想文でした。
長文をお読みいただき、ありがとうございます。

【おまけ】
さて作っていたライブラリですが…
ブラウザのエミュレートまでは無理でしたが自分が欲しい機能までは実装できたように思います。
でも使うかなぁ、どうだろう。

コメントを残す

必須欄は * がついています