2013年2月27日水曜日

canvas要素にwebフォントを確実に描画する方法

webページをデザインする上で,必要に応じてフォントをダウンロードすることが出来るwebフォント機構は非常に魅力的で利便性の高い仕組みですが,使い方によっては色々と面倒な作業が必要なようです.今回解説するcanvas要素との連携はこの最たるもので,場合によっては正しい処理結果が得られません.

webフォントとは


今日のwebブラウザの大部分ではcssにおける@font-face規則を記述することで,webサーバー上のフォントファイルを必要に応じてダウンロードすることが可能となっています.この仕組みを利用すると,従来難しかったデザイン性の高いフォントをユーザー環境に依存せずに表示することが可能となります.なお文字の種類が多い日本語環境よりも,少ない文字種で文章の記述がまかなえる欧文環境でよく使われています.

webフォントの特徴


このように魅力的なwebフォントなのですが,webフォントであるがゆえに特徴的な動作を採ります. webフォントはサーバーからフォントファイルをダウンロードするので,実際にブラウザに表示されるまでに次のような処理が行われます.
  1. @font-face規則の記述を元にwebフォントを提供するサーバーにリクエストを発行する.
  2. サーバーからwebフォントデータが送信される.
  3. webフォントデータの内容を解析し,webページに文字を反映させる.
これらの処理は通常メインのwebページがロードされてから行われるのですが,メインページのレンダリング処理とは非同期に(並行して)行われます.というのも,いずれも時間の掛かる処理であり,いちいちフォントのダウンロードを待っていてはwebページの表示に時間がかかりすぎるからです.

また,webフォントを読み込むことが出来なくとも,webページのレンダリング処理は可能であることから,ブラウザはまずwebページを表示させることを優先します.そのあとでwebフォントの利用が可能となってから文字列の描画内容を切り替えるのです.

このような動作からブラウザによってはwebフォントのロード完了前後で文字の見た目が変化する場合があります.

canvas要素と組み合わせた場合の問題点


一般的なwebページの表示においてこの動作は上手く行きます.ですが,canvas要素と組み合わせた場合に非常に厄介な問題に突き当たります.

通常スクリプト処理は様々なタイミングで実行されますが,先ほどの処理1〜3の間にwebフォントを参照するコードが実行された場合どうなるでしょう?おそらく,文字形のデータが得られずに代替フォントの内容で文字列が描画される事となるでしょう.つまり,canvas要素にwebフォントを正しく描画するには,webフォントの利用が可能となってからスクリプトを開始する必要があるのです

前置きが長くなりましたが,ここからが本記事の本題です.ではどうやって「webフォントが利用可能となったタイミング」 を検知するのでしょうか?domにおいてwebフォントのロードに関わるapiを筆者は見つけることができませんでした.また一般的に認知されているwindowオブジェクトのloadイベントで処理を開始する方法も,筆者の環境では正しく動作しませんでした.

※webkit系のブラウザではリロード時に不具合を発生します.webページのリロード時に限りwebフォントの解析がloadイベント時に完了していないケースがあるようです.
これが無ければ結構すんなりとしたコードになったんだけれど.

webフォントの準備完了を判定する方法


この問題はcanvas要素のapiを利用することで解決します.CanvasRenderingContext2DのmeasureTextメソッドを利用するのです.例を示します.

<!DOCTYPE html>
<html>
 <head>
 <meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
 <style type="text/css">
  @font-face {
   font-family: 'Chela One';
   src: url(http://themes.googleusercontent.com/static/fonts/chelaone/v1/DHUBEAsCcSRMyWTJ6sisfj8E0i7KZn-EPnyo3HZu7kw.woff) format('woff');
  }
 </style>
 <script type="text/javascript">
  window.onload = function(){
   //描画を試行する
   //リトライ回数
   var cnt = 0;
   function tryDraw(){
    if(!loaded() && cnt<30){
     setTimeout(tryDraw, 100);
     cnt++;
    }else{
     draw();
    }
   }

   //webフォントのロード状況を確認する
   var c1 = document.createElement("canvas");
   var c2 = c1.cloneNode(false);
   var ctx1 = c1.getContext("2d");
   var ctx2 = c2.getContext("2d");
   //webフォントと代替フォントとを指定.
   //NOTE:monoscopeだとwebkitでリロード時に失敗する
   ctx1.font = "normal 30px 'Chela One', serif";
   ctx2.font = "normal 30px serif";
   var text = "this is test text.";  
   function loaded(){
    //テキスト幅を比較する
    //webフォントが利用可能となると,フォント幅が一致する.
    var tm1 = ctx1.measureText(text);
    var tm2 = ctx2.measureText(text);
    return tm1.width != tm2.width;
   }

   //実際の描画処理を行う
   function draw(){
    var canvas = document.createElement("canvas");
    var ctx = canvas.getContext("2d");
    ctx.fillStyle = "blue";
    ctx.font = "normal 30px 'Chela One', serif";
    ctx.fillText("It's web fonts.", 0, 30);
    document.body.appendChild(canvas);
   }

   //処理開始
   tryDraw();
  }
 </script>
 </head>
 <body>
 </body>
</html>
ポイントを示します.
  • (loadedメソッド)webフォントのロード状況を確認するためにcanvas要素を二つ準備します.
    コンテキストオブジェクトを取得し,fontスタイルを設定します.この時,
    • 片方には「webフォント+代替フォント」を指定します.
    • もう片方には「代替フォントのみ」を指定します.
    ここでサンプル文字列を二つのmeasureTextメソッドでその幅を測って比較します.
    すると,webフォントのロード状況により次のような結果となります.

    • 未完了時…計測結果はどちらも代替フォントによる幅となるため,同じ値となります.
    • 完了時…片方はwebフォントによる幅,もう片方は代替フォントによる幅となるため値が異なります.
    従って,webフォントのロード状況の判定が行えることになります.
    ※なお,webkitでは代替フォントとしてmonospaceを使うと上手く行きませんでした.
  • tryDrawメソッド)上記のloadedメソッドを使ってwebフォントのロードが完了したことを確認し,完了していればdrawメソッドを実行します.そうでなければ100ミリ秒待って再実行します.
    なお,その際,フォントのロードに失敗したケースを想定し,確認する上限回数を定義しておくとよいでしょう.

このロジックで問題となりそうな点は1点あって,万が一webフォントと代替フォントとの文字幅の構成が全く同じだった場合に正しいチェックが行われないことになりますが,試行回数の上限を設定して有るため,描画処理は確実に実行されます.

firefox,chrome,opera,ie10,safari5.1での動作(ロード時・リロード時) を確認しました.

補足


svgを利用する方法もあります.同様のロジックをsvgのtext要素で記述しておき,スクリプトでbboxメソッドを使って文字列の幅を計測・比較するのです.

webフォントも描画リソースとしてラスタ画像と同様に扱いたいところですが,現状このような方法しか思いつきません.もっとスマートな方法があればご教示願いたいところです.

0 件のコメント:

コメントを投稿