Web フォントがレンダリングされるタイミングを得ようとすると、そのやんちゃな挙動を制御するため人類は今まで下記のような対策をしてきた。
無知な僕は同様の事象にハマり、一通り調べた後、下記の答えにたどり着いた。
canvas に WebFont を指定するとき、一回どこかの DOM で使う WebFont をレンダリングしておかないと死に至る現象を発見したので皆様もお気を付けください
— ダメレオン (@damele0n) April 10, 2014
要するに DOM もしくは canvas 上で、そのフォントが指定されていて一度そのフォントがレンダリングされてからでないと canvas 上はレンダリングされない、ということだ。
(ちなみに Chrome, Safari, Opera という Webkit 系のブラウザはこの現象が発生して、Firefox は window.onload の後であればレンダリングせずとも canvas に Web フォントが適用された。)
そもそも、それってどうなの…って話ではあるが、前述のとおり Web フォントの読み込まれるタイミングは以前から不審に思っていて信頼もできなかった。
前置きが長くなったが、隣のデスクのウーパールーパーな人の話や Web や Chromium のソースを追ってみたところ CSS Font Loading Module という仕様が策定されている段階で、Chrome のみ既に実装が済んでいる。(なんなら仕様にない実装が生えてる)
仕様はざっくり理解はしたが、人に説明できるレベルでもないので今回は簡単な紹介をする。
- 注意1:ここで説明する CSS Font Loading の仕様は This document is a First Public Working Draft. つまり、Working Draft 中の Working Draft である。
- 注意2: 前述の通り、現在 Chrome だけ実装されており、他ブラウザでは実装がないためエラーとなる。
サンプルコード
以降の説明では、下記のようなスタイルが指定されているとする。
@font-face {
font-family: "my_font_01";
src: url("path/to/font_file_01.woff") format("woff");
}
@font-face {
font-family: "my_font_02";
src: url("path/to/font_file_02.woff") format("woff");
}
@font-face {
font-family: "my_font_03";
src: url("path/to/font_file_03.woff") format("woff");
}
body {
font-family: "my_font_01", "my_font_02", sans-serif;
}
document.fonts の各イベントについて
CSS Font Loading の実装がある場合、document.fonts というオブジェクトが確認できる。
このオブジェクトは、3種類のイベントをもつ。(括弧内は、addEventListener で listen する場合のイベントタイプ)
onloading ( loading )
: フォントセットの読み込み中に発火する
onloadingdone ( loadingdone )
: フォントセットの読み込みが完了した時に発火する
onloadingerror ( loadingerror )
: フォントセットの読み込みが失敗した時に発火する
フォントセットの〜と書いたが、イベントが発火するのはフォントセットごとではなく、フォントセットを読み込むドキュメント単位でイベントが走る。
つまり、サンプルコードの場合であれば my_font_01, my_font_02 が body に指定されているので、my_font_01, my_font_02 を読み込むイベント、読み込み完了のイベント、(エラーの場合)読み込みエラーのイベントが走ることになる。
my_font_03 は?という疑問があると思うが、@font-face で指定されたものは 使用時に読み込まれる ため、サンプルコード内では使用していないので読み込まれることはない。
この場合、ドキュメントの読み込みが完了後に何らかのタイミングで
<div style="font-family: 'my_font_03'"></div>
のように、my_font_03 が指定された場合、その時点から my_font_03 の読み込み処理が開始し、上記のイベントが走る。
CSS @font-face で指定したフォントがレンダリングできるかどうかを判断する
document.fonts のイベントを使う
上でも説明したが、document.fonts オブジェクトに走るイベントで下記のようにフォントが読み込まれたかどうかを判断できる。
イメージ的には window.onload のような感じだ。
document.fonts.addEventListener('loadingend', function() {
// 正常に読み込まれた場合の処理
});
document.fonts.addEventListener('loadingerror', function() {
// 失敗した場合の処理
});
document.fonts.ready() を使う
document.fonts のイベントは、フォントの読み込み完了前に listen しておかなければならず使い勝手が悪いシーンもある。
fonts.ready() は、任意のタイミングでフォントが読み込まれているかどうかを判断するのに役立つ。
この API は珍しく Promise オブジェクトを返すが、昨今のウェッブエンジニア殿なら問題なく扱えると思う。
document.fonts.ready().then(function(fontFaceSet) { // FontFaceSet ( document.fonts ) が渡される
// 読み込まれた場合の処理
});
- ready() から返る Promise オブジェクトは、reject されることはないようである。
document.fonts.check() を使ってフォント名を指定して判断する
fonts.ready() では、 どのフォントが読み込まれたか というのが判断しづらいため、 fonts.check() と合わせて使用するといい。
document.fonts.ready().then(function(fontFaceSet) { // FontFaceSet ( document.fonts ) が渡される
if (fontFaceSet.check('10px "my_font_01"')) { // 引数はフォント名だけ渡すとダメなので、 CSS の font 指定のフォーマットで渡す
// my_font_01 が使える
}
});
document.fonts.load() を使ってフォントを読み込みながら判断する
my_font_03 のように、ドキュメント上のどこでも使用されていないものはフォントファイル自体が読み込まれていない。fonts.load() を使用することでフォントを読み込みつつ、完了と同時に処理をすることができる。
fonts.load() は、 fonts.ready() と同じく Promise オブジェクトを返すが、引数に font の指定、もしくは FontSet オブジェクトを渡すことで、指定されたフォントを読み込んでくれる。
これによって、指定したフォントが読み込まれていることが保証されるので厳密に処理ができる。
// 上記サンプルコードのように font-face にて FontFace が生成されている場合は、 font-family の名前を渡すことで判断ができる
var fonts = document.fonts;
// fonts.check() と同様に、CSS の font 指定のようなフォーマットで渡す
var p = fonts.load('10px "my_font_01"');
// 複数のフォントを指定した場合は、引数が増える
p.then(function(fontset[, fontset...]) {
// 読み込み成功
});
p.catch(function(err) {
// 読み込み失敗
});
// 動的に FontFace を生成して読み込むこともできる
var fontFace = new FontFace("my_new_font_01", "url(my_new_font_01.woff)", {/* font-face の各パラメータをオブジェクトで渡せる */});
fontFace.load().then(function (loadedFontFace) {
// 自動で document.fonts へ追加されないようなので、手で渡してあげる
document.fonts.add(loadedFontFace);
// body へフォントを反映
document.body.style.fontFamily = '"my_new_font_01", sans-serif';
});
前置きのように、特に Canvas で Web フォントを扱おうとすると地獄のような苦しみを味わってきたが、これで少しはマシになるのではないかなと。
仕様策定段階なので、全ブラウザですぐ使えるモノではないが覚えておくのに損はないだろう。
ちなみに、ブラウザ差異を埋めつつ Web フォントの細かい挙動を上手く扱いたければ typekit/webfontloader が一番良いと思う。
仕様を読み切っていないので、突っ込み大歓迎です。
追記
document.fontsのpolyfill既にあった気がする。
— きがうなしょま (@hail2u_) April 11, 2014
ありました。bramstein/fontloader
ありがとうございます!
追記 (2014/07/04)
Last Call Working Draft にあわせて onloadingend
を onloadingdone
へ修正