document

HTML5 & JavaScript side
twitter: @y_imaya

あとから線の編集が可能なお絵描きツールの描画高速化

はじめに

2010 年くらいからのんびり開発している趣味のお絵かきツール Diceros の実装について、ドキュメントを残しておいたほうがよさそうなのでこの文章を書きます。
まだ未完成ですが、お絵描きツールは以下の URL で開発バージョンを触ることができます。IE10+, Google Chrome 17+ あたりで動作すると思います。Wacom のペンタブレットプラグイン(MouseEvent)、Android Chrome(TouchEvent)、IE10(PointerEvents) あたりの筆圧に対応してます。 

しばらく触るとわかると思うのですが、このお絵描きツールは(VectorLayer/SVGLayerでは)一度書いた線をあとから調整することができるようになっています。

一度描いた線の再描画が多い

Diceros では描いた線を後から編集することができるため、それぞれの線を個別に保持しておかなくてはいけません。
個別の線をそれぞれ専用のオフスクリーンバッファ(DOMツリーから分離されているCanvas)に描画しても良いのですが、バウンディングボックスなどで極力小さくなるようにしても莫大な量のメモリを消費してしまいますし、余白部分が多いので描画自体もあまりはやくありません。
そこで、Diceros では描画命令を記録しておく方法を採用し、再描画処理を高速に行うように工夫しています。

描画命令を保存する

Diceros では高速化のため CanvasRenderingContext2D の一部メソッドと同じインターフェースを持った別のオブジェクトに保存しています。
再描画のたびに描画に(制御点の位置など)必要な情報を計算しなおすのはムダなため、描画命令自体をフックして覚えておこうという作戦です。

また、これは当初想定していなかったのですが、Canvas の他にも SVG で描画するのも中間形式から変換するだけなので簡単に実装することができました。

中間形式のデータはとても簡単になっていて、以下の様な Array で実装されています。

[[メソッドID], [メソッドの引数の数], [引数1], [引数2], ... ]

fillStyle のようなプロパティも Object.defineProperty で setter を設定して、この配列に入るようになっています。

線の編集

線の編集では、編集中の動作を重くしないために以下の三つのオフスクリーンバッファに描画したあと、結合しています。

  • 編集中の線より前の線
  • 編集中の線
  • 編集中の線より後ろの線

diceros_line_layer

ここで重要なのは、頻繁に再描画されるのは「編集中の線」だけで、それ以外は一度描画したらあとはそのまま再利用できる点です。

編集中の前後の線の再描画

編集前後の線は、(線の編集中は)めったに更新されないため "function 生成" というテクニックを使うことで高速化することができます。
なぜ高速になるのか、簡単な例で説明します。

たとえば、上記の中間形式を描画させようとしたら以下のようなコードになると思います。

function draw(ctx) {
    // 描画命令の中間形式配列
    var codeArray = [...];
    // メソッドID から メソッド名への変換テーブル
    var codeToName = [...];
    // CanvasRenderingContext2D のメソッド名
    var method;
    // 引数の個数
    var argnum;

    for (var i = 0, il = codeArray.length; i < il; ++i) {
        // メソッド ID からメソッド名への変更
        method = codeToName[codeArray[i++]];
        // 引数の個数の取得
        argnum = ctx[codeArray[i++]];
        // メソッドの実行
        ctx[method].apply(ctx, codeArray.slice(i, i += argnum));
    }
}

これを function 生成するコードにすると以下のようになります。

function createDrawFunction() {
    // 描画命令の中間形式配列
    var codeArray = [...];
    // メソッドID から メソッド名への変換テーブル
    var codeToName = [...];
    // CanvasRenderingContext2D のメソッド名
    var method;
    // 引数の個数
    var argnum;
    // 生成される function の中身
    var functionBody = [];

    for (var i = 0, il = codeArray.length; i < il; ++i) {
        // メソッド ID からメソッド名への変更
        method = codeToName[codeArray[i++]];
        // 引数の個数の取得
        argnum = ctx[codeArray[i++]];
        // メソッド呼び出しを作成
        functionBody.push(
            "ctx." + method + "(" + codeArray.slice(i, i += argnum).join(',') + ')'
        );
    }

    return new Function("ctx", functionBody.join('\n'));
}

これを実行すると、例えば以下のような function が生成されます。

function(ctx) {
    ctx.beginPath();
    ctx.moveTo(0,0);
    ctx.lineTo(50,50);
    ctx.lineTo(100,100);
    ctx.closePath();
    ctx.fill();
}

このように、中間形式を使わず純粋に描画するためだけの function が作成されます。
この生成された function は配列やループ、変数の参照など、描画命令以外の余計な処理が一切入らないのでかなり高速に動作します。

編集中の線の再描画

編集中の線の再描画は、頻繁に更新されるため、小細工を加える余地がすくないです。
中間形式を用いてそのまま描画しています。
上記の function 生成を利用しないのは、編集中の線は「変更=再描画」となるので function を生成するメリットがないからです。
(むしろ、function を生成するコスト分だけ損になります。)

その他

紀平さんが Chrome+HTML5 Developers Live Japan #5 で function 生成についてライブコーディングしながら説明しているので、より丁寧な説明が必要な方は参考になると思います。

Web Audio API で Sound Font を使った標準 MIDI ファイルの再生

はじめに

Google Chrome では Web Audio API という API を使って音を鳴らすことができます。
今回、これをつかって Sound Font を使った標準 MIDI ファイル(以下 SMF と表記)のプレイヤーを作ってみました。
なお、仕様の具体的な話しなどはほとんどしません。
音楽的な知識などもほとんどないため、何かおかしなことをしていたらご指摘いただけるとありがたいです。

また、今回の実装はあくまでも実験・検証用のものなので実用にはまだ手を加えなくてはいけないところが多いため、もし利用としようと思う方がいたらそこは注意してください。
動作環境は PC 版の Google Chrome のみです。
現在開催中の Google I/O で Chrome for Android でのサポートも明言されたそうですので、そのうち Android でも利用可能になるかも知れません。

もう少し完成度たかめてから公開しようかとも思っていたのですが、旬の話題みたいなのでビッグウェーブに乗ろうと思います。

デモ

以下のページで公開しています。
サウンドフォント(約10MB)のダウンロードがあるのでロードが終わるまでは気長に待ってください。

構成

まず、大きく分けて SMF を扱う部分と Sound Font を扱う部分に分かれます。
そして、この二つの部分を WebMidiLink というものでつないています。

  • SMF
    • SMF の Parser
      • MFi(着メロで使われたファイルフォーマット。拡張子は mld ) から SMF への変換モジュールもあります
    • MIDI メッセージのスケジューリングを行い、WebMidiLink へメッセージを流すプレイヤー
  • Sound Font
    • Sound Font version 2 の Parser
    • WebMidiLink のメッセージを受け取り実際に音を鳴らすシンセサイザー

WebMidiLink とは g200kg さんの提唱している window の message イベントを利用した MIDI メッセージをやり取りする仕様です。
http://www.g200kg.com/en/docs/webmidilink/

Sound Font

Sound Font では音色毎にいくつかの音域に分けてサンプルが格納されています。
このサンプルにはキーが設定されているため、その音域はその指定されているキーをもとにして平均律で算出し、再生速度を調整することで鳴らす事が出来ます。
また、各サンプルには Loop Start, Loop End が設定されていて、Note On されている時はこのループをぐるぐる再生し続けます。
(この再生方法のため、Safari では loop の実装が整っていないためおかしく聞こえます)

Sound Font の再生では他に以下の機能に対応しています。(しているつもりです…。)

  • Attack, Decay, Sustain, Release
  • Pitch Bend 変更
  • ピッチの補正
    • coarseTune
    • fineTune
    • Pitch Correction
    • modEnvToPitch
  • Web Audio API で扱えないサンプリングレートの補正(22,050 未満を引き延ばす)

また、今回つくったものでは A320U という GPLv2 ライセンスで公開されている Sound Font を標準で利用するようにしています。

ループ

一部のゲームなどでは SMF 内で独自にループ再生の目印を付けていることがあります。
Ys2 Eternal で使われているメタイベントの Marker を用いたループや、PC 版 RPG ツクールシリーズなどで使われている CC#111 のループが有名です。
今回作成したプレイヤーでは上記の2つのループ方式にも対応しています。

音を鳴らす

SMF では 16 のチャンネルがあり、それぞれに Volume, Panpot, Pitch Bend などが設定できます。
今回作成したものでは、各ノートを以下のように接続して鳴らしています。

[BufferSource] ---> [Panner] ---> [Gain(Volume, ADSR)] ---+
[BufferSource] ---> [Panner] ---> [Gain(Volume, ADSR)] ---+---> [Gain(Master)] ---> [Destination]
[BufferSource] ---> [Panner] ---> [Gain(Volume, ADSR)] ---+

BufferSource は各ノートで、PannerGain(Volume, ADSR) でチャンネル毎の設定を適用しています。
そして最後はマスター・ボリュームである Gain(Master) に繋いでそれを出力するという感じになっています。

現在は割と大雑把に上記のような構成の AudioNode を NoteOn ごとに作って使い捨てているのですが、今後はこのあたりをもっと効率的に行う予定です。

謝辞

g200kg さんの Web Audio API の解説 にはかなりお世話になりました。
また、音がずれてるなどの指摘をくださった @miyazaqui さん、音楽関係の基礎知識などについて丁寧に説明してくださった @yoya さんのおかげでなんとか聞こえるレベルまでこぎ着ける事が出来ました。ありがとうございます。

その他

今回は minify したファイルのみの公開とします。
もったいぶってるわけではなくて、近いうちに大規模な書き直しを行おうと思っているからです。
minify されててもいいってひとは適当にデモからソースを読んでください。

また A320U を使用しているとき Cello の音がズレているという指摘をもらっています。
これはサウンドフォントの modEnvToPitch という設定を無視するようにすれば正しい音程になるようですが、これを無効にすると他の音でおかしくなるので現在調査中です。

音程がずれる問題の原因 (2013/06/03 16:15 追記)

@g200kg さんからご指摘をいただきまして、Modulation Envelope の適用を行ったら改善したようです。 modEnvToPitch では Attack でピッチ・ローパスフィルタの設定が最も変わるというだけで、この記事を公開した段階ではその実装のみを行っていたのですが、Envelope なので Attack と Sustain の差によってピッチの変化がずれ、その結果音程がずれていたものだと思います。

追記:2013/06/03 16:15

以下の場所でソースコードの公開を行いました。

zlib.js 0.1.6 をリリースしました

はじめに

本日 zlib.js 0.1.6 をリリースしました。ここでは告知とともに 0.1.6 の更新内容などを簡単に説明したいと思います。

なお、今回から Change Log を添付したあるので概要はそちらでも確認できます。
今回のバージョンでは主にビルド環境の整理やテスト・デバッグ効率の向上を行っています。

https://github.com/imaya/zlib.js

ビルド環境の更新、最適化

zlib.js は Closure Compiler でビルドしているのですが、今までは minify したコードに余分なコードが含まれている事がありました。
今回の更新では Closure Compiler の更新とその辺りの設定を見直す事で minify したファイルのサイズを縮小しました。
例えば、Inflate だけならば約 7KB とさらにコンパクトになっています。

Raw 形式、CRC-32 の独立ビルドの追加

今までは Deflate アルゴリズム単体でのビルドはなく、ZLIB や GZIP, PKZIP などのコンテナ形式への対応だけでしたが、今回からは Deflate のみの利用が可能になりました。

以下のようにして使います。

var plain = new Uint8Array(1024);

// compression
var compressed = new Zlib.RawDeflate(plain).compress();

// decompression
var decompressed = new Zlib.RawInflate(compressed).decompress();

また、CRC-32 アルゴリズムも単体で利用する事ができるようになりました。

var plain = new Uint8Array(1024);

// CRC-32
var crc32 = Zlib.CRC32.calc(plain);

Pretty Print ビルド、Source Maps のサポート

今回から開発・デバッグ用に Pretty Print されたバージョンのビルドと、Source Maps のサポートを追加しました。
Source Maps の使い方はドキュメントに書いてありますが、とりあえず使えるようにしただけなので使いにくいかも知れません。
もっと良い構成などありましたら教えていただけると助かります。

Travis CI のサポート

今回のバージョンから Travis CI をつかって Firefox, Chrome で自動テストを行うようにしました。
…とはいっても、時々テストに使っている BusterJS の実行がささってしまうのでイマイチな時もあります。

ストリーム展開のビルドを修正

zlib.js では inflate_stream.min.js というファイルで実験的にストリーム展開に対応しています。
前回のバージョンでは手違いでストリームに対応していないままになっていたのを修正を行いました。

JavaScript でペンと筆圧を扱う

はじめに

JavaScript でアプリケーションを作っていると、筆圧を取得したくなることがよくあると思います。
ここでは JavaScript でタッチやペンによる筆圧の取得の仕方について簡単にまとめます。

Wacom ペンタブレットによる筆圧の取得

Wacom のペンタブレットではブラウザにプラグインをインストールすることにより筆圧の取得が可能になります。
最近はタブレットの最新版ドライバをインストールすると一緒にプラグインもインストールされるようです。

タブレットプラグインのバージョンによる API 変更

Wacom のタブレットプラグインでは元々ペンを扱う単体のものでしたが、バージョン 2 からはタッチと統合されたため API が変更されました。
基本的には統合であるため、プラグインのオブジェクトに .penAPI を付けるか付けないかの違いしかありません。

筆圧の取得

まず、以下のように Wacom タブレットプラグインをドキュメント上に設置します。

<object type="application/x-wacomtabletplugin"></object>

タブレットプラグインでは、後述する Touch Events や Pointer Events の様にイベントに筆圧がついてくるわけではないので、イベントのハンドリング自体は Mouse Events で行い、そこで筆圧を取得するのが良いと思います。

// プラグインの object 要素を取得
var plugin = document.querySelector('object[type="application/x-wacomtabletplugin"]');
var pressure;

// Pen API からペンの筆圧を取得する (pointerType:1 = ペン, 2 = マウス, 3 = 消しゴム)
if (plugin && plugin.penAPI && plugin.penAPI.isWacom && plugin.penAPI.pointerType === 1) {
  pressure =  plugin.penAPI.pressure;
}

筆圧以外の情報やタッチについても取得したい場合は Wacom の WebPluguinReleaseNote を参照。

Touch イベントによる筆圧の取得

モバイル環境 (Android) ではスタイラスと筆圧に対応した端末もいくつかあります。
Chrome や Firefox では Touch オブジェクトに筆圧のプロパティがあります。
現在 Chrome ではプレフィックス付きの webkitForce, Firefox では単に force となっています。

touchstart, touchend, touchmove などのイベントをハンドリングし、その中で TouchEvent オブジェクトの上記プロパティを参照すると良いでしょう。

このプロパティのサポート状況は以下のようになっています。

webkitForceforce
Android Chrome 17+O-
Android Browser(4.1)--
Firefox 6+-O
iOS Safari(6.0)--

Pointer Events (IE10) による筆圧の取得

IE10 では Pointer Events というTouch Events とは別のイベントでタッチやペンの入力を扱う事が出来ます。
ブラウザが Pointer Events に対応しているかどうかは navigator.msPointerEnable によって判断することができます。
MSPointerDown, MSPointerUp, MSPointerMove などのイベントをハンドリングし、その中でイベントの pressure プロパティを参照することで筆圧を取得します。
また、入力がマウスなのか、ペンなのか、それともタッチなのか判別することもできます。
なお、筆圧が取得可能なのはペンのみで、タッチでは pressure は 0 に固定されるようです。

var pressure;

// PointerEvent からペンの筆圧を取得する
if (window.navigator.msPointerEnable && pointerEvent.pointerType === MSPointerEvent.MSPOINTER_TYPE_PEN) {
  pressure = pointerEvent.pressure;
}

まとめ

それぞれの環境で利用可能な筆圧の取得方法をまとめると以下のようになります。

環境Wacom Tablet PluginTouch EventsPointer Events
Windows(PC), MacOSO--
Windows Tablet + IE10--O
Android Chrome-O-
Android Firefox-O-

ただし、現在 WebKit で Pointer Events の実装が勧められているみたいなので、今後は Android でも Pointer Events で取得できるようになるかもしれません。

なお、筆圧はどの方法でも 0 から1 の範囲になります。

ブラウザのデコード機能を利用した Shift JIS などの読み込み

はじめに

JavaScript でバイナリから文字列を取り出したら Shift JIS だったなんてことよくありますよね。
そういう文字列もさっと表示したいことがあります。

読み込む方法はいくつかある

これらの文字列を読み込む方法はいくつかあって、自分が把握してるだけでも以下のものがあります。

  1. Shift JIS と UTF-16 の対応表をつくる
  2. Blob, File API を使って読み込む
  3. script, Data URL を使って変換

1, 2 の方法についてはそれぞれ解説や実装があるのですが、3 の方法については見当たらなかったので説明してみます。

準備

念のため 2 段階で文字コードの識別を試みます。

script 要素の charset 属性

script 要素には charset 属性というのがあって、この属性がセットされていた場合、指定された文字コードとして読み込みます。
仕様では Scripts in HTML documents(HTML4) や Scripts in HTML documents にかかれています。

Data URL

Data URL は The "data" URL scheme ( RFC2397 ) で定義されている、データを Web ページに埋め込むための URI Scheme です。
フォーマットはシンプルで

data:[<mediatype>][;base64],<data>

となっています。
このフォーマットの <mediatype> は以下のようなものになっています。

mediatype  := [ type "/" subtype ] *( ";" parameter )

ここから少し仕様を追っていくのが大変なので、簡単に説明すると mediatype には text/plain のような MIME Type だけではなく、text/plain; charset=Shift_JIS のように文字コードを指定する事ができます。
parameterattributevalue について詳しく知りたい方は RFC2045 を参照してください。
ちなみに mediatype が指定されていない場合のデフォルト値は text/plain; charset=us-ascii となります。

変換を行う

script 要素と Data URL について説明したので、ここからそれを使って以下のように文字コードを変換します。

  1. 変換前の文字列を文字列リテラルの文字列に変換する ( hoge という文字を 'hoge' という文字列に変換)
    • いくつかの文字は文字列リテラルとして正しくなるようにエスケープしてやる
      • バックスラッシュは \\
      • クオートは \x27
      • LF は \x0a
      • CR は \x0d
  2. 変換後の文字列を受ける callback function を作る
    • 例えば以下のような感じです
    • function Callback(str) {
      console.log(str);
      }
  3. 生成された文字列リテラルを引数にして callback function の呼び出しを行うスクリプト文字列を生成する
    • "Callback('hoge');"
  4. 生成されたスクリプト文字列を Data URL に変換して script 要素で呼び出す
    • もちろん、Data URL と script は charset 指定する

欠点

script 要素の生成と追加を行っているので非同期でしか使えません。
同期で変換したい場合はぽりごんさんのライブラリを使うと良いと思います。

おまけ

これらをまとめてやってくれる azoth.js というのを作りました。
自分でやるのめんどくさいという人はどうぞ。

ちなみに JavaScript の文字列 (UTF-16) から UTF-8 への変換と、UTF-8 からの変換は、escape/unescape, encodeURIComponent/decodeURIComponent で行う事ができるのでライブラリが必要なかったりします。

おわりに

一応簡単なテストなどもしていますが、もし考慮漏れなどありましたらお知らせください。

記事検索
最新コメント
最新トラックバック
カテゴリ別アーカイブ
タグクラウド
QRコード
QRコード
  • ライブドアブログ