HTML5 Canvasのブラウザによって異なる微妙な振る舞いについてまとめてみた。

はじめに

CanvasHTML5とは切り離された独立した仕様(HTML Canvas 2D Context)になっているようですが、現状のブラウザ上でのCanvasのについて、普段はあまり気にしない微妙な振る舞いについて調べた結果をまとめてみました。

調べたブラウザの各バージョンは以下の通りです。

Firefox Chrome Safari Opera
3.6.8 6.0.490.1 dev 5.0.1 10.61

線を描く (lineTo)

ただの直線を描くだけのlineToですが、その単純なものにも、恐らく、多くの人が普段は気にしないような問題があります。それは座標値とアンチエリアスです。詳しく見る前に、実際の結果を示しましょう。下記のイメージ中に描かれている線は、いずれも線幅(lineWidth)が1の線です。



(左から、Firefox, Chrome, Safari, Opera)

どのブラウザでも同じように描画されているようですが、よく見ると、青の45度の斜め線がChromeとそれ以外では異なります。小さくてわかりにくいので、10倍に拡大して見てみましょう。この拡大もcanvas(イメージのキャプチャ*1 )を使っています。下図で、左がChrome以外で、右がChromeです。青の斜め線はChrome以外ではアンチエリアスされていますが、Chromeではされていません。




さららに面白いことに、これらの線はいずれも線幅が1であるにも関わらず、色が薄くて太く見えるということです。特に、上端の水平線の赤と左端の垂直線の緑はどちらも(2, 2)の座標から右方向と下方向に伸ばしている直線で座標値は整数値です。どうして2ピクセル分の幅になっているかというと、Canvs2Dの座標値はピクセルをベースにしているのではなく、あくまで数学的な意味でのものだからです。

つまり、原点(0, 0)はcanvasの左上ですが、左上のピクセル(の中央)が原点ではなく、左上のピクセルの左上の隅が原点になります。なので、整数で表される座標値は、ピクセルピクセルのちょうど境界になるため、幅1であっても2ピクセル分に跨って描画されることになります。±0.5シフトしてピクセルの中央になるように座標を指定すれば水平・垂直の線は1ピクセル分だけの幅になります。

上の図で、4本の赤の垂直線のY座標は左からそれぞれ15.0, 18.3, 20.5, 22.7 になっていますが、20.5の場合ちょうどピクセル幅が1になっていることがわかります。

細い線を描く (lineWidth)

線の幅はGraphicsContext2DオブジェクトのlineWidthプロパティ指定して、その型はfloat(JavaScriptではNumberにマッピング)なので、幅が2.5の線や1より細い線などが表現可能ということになります。

先ほどの描画した線の幅を0.5に設定したときの結果を以下に示します。






Firefox(左)とChrome(右)

垂直の赤の線に注目してください。Y座標値がそれぞれ15.0, 18.3, 20.5, 22.7の垂直線であり、線幅が1のときは2ピクセルに跨るため太くなる結果になりましたが、線幅が0.5のときは15.0の線以外は1ピクセル内に収まる幅です。確かに、Chrome以外は1ピクセル幅の線として描画されています。Chromeの場合は、幅が1のときと同じまま全体的にアルファがかかったようになっています。そのため、1より細い線がChromeでは寝ぼけた感じになることがあります。

さて、1より細い線はアルファ値を小さくして透明度を上げて描画されていますが、これは実装上の動作であり、canvasの仕様ではそのようなことは規定されていません。事実上、アルファ値は0〜255の1バイトで実装されるので、線幅とアルファ値が線形関係になっていると仮定すると、線幅が1/255より細い線はアルファ値が0以下になり完全透明なので描画されないということになります。


実際に確認してみましょう。といっても、薄い細い線を1本描いてそれを目で見ても分かりません。イメージをキャプチャしてデータを調べれば分かりますが、ここでは別の方法をとります。つまり、線幅の間隔で平行な線でびっしりと埋めてどうなるかを見てみます。左図は幅1の線を赤と青を交互に1づつずらしながら描いたものです。この線の幅を小さくしてどうなるかを確認しました。線幅が小さくなると赤と青が混じり合って紫になりますが、さらに小さくすると突然描画されなくなる場合があります。


結果を以下に示します。

Firefox Chrome Safari Opera
1/256 1/128 1/255

Firefoxは1/256を境界として描画されなくなります。Chromeは1/128のようです。Safariは全体的にアルファが下がって明確な境界は見つけられませんでした。Oparaは1/51200しても描画されました。


左図は線幅が1/64のときのChromeの結果です。格子状のパタンが表示されていますが、これはcanvasの背景にセットしたイメージです。Chromeの場合、細い線のアルファのかけ方が他と異なり、上書き(copy)方式のようなものになっているようです。

これまでの動作や以降の項目で登場するいくつかは下記のリンクから実際の動作を確認できます。

矩形とその塗りつぶし (rect, fill)

矩形を簡単に描画するrect()というAPIがあります。

The rect(x, y, w, h) method must create a new subpath containing just the four points (x, y), (x+w, y), (x+w, y+h), (x, y+h), with those four points connected by straight lines, and must then mark the subpath as closed. It must then create a new subpath with the point (x, y) as the only point in the subpath.

矩形を描いてその後、新しいパスを生成して位置を(x,y)にセットしろ、とありますが、この解釈の違いがブラウザで現れています。wとhは正数でなければいけないとは書かれていないので、負数でもよいと考えられます。そのとき(x,y)は矩形の左上とは異なる他の点になりますが、ChromeSafariでは(x,y)が必ず左上になるように調整していると思われる結果になっています。

さらに、仕様からは矩形を描く線分の方向も読み取れますが、wとhを正または負に設定するすることで、時計回りや反時計回りの矩形を描画することもできます。しかし、ChromeSafariでは頂点の値が調整されてしまうためか、常に時計回りで描画されるようです。

一方、Operaではw, hに負の数を指定するとINDEX_SIZE_ERR例外が投げられます。

以下の結果にその違いがわかります。







Chrome,Safari(左)とFirefox(中)およびOpera(右)

実際の動作は以下で確認できます。


なお、矩形領域の塗りつぶしは、線幅がゼロの線で囲まれた内側を塗りつぶすことですから、座標値がちょうど整数のときにアンチエリアスされることなく各ピクセルがピッタリ描画されます。左図は、5x5の大きさの矩形を左上座標を変えて描画したものです。アンチエリアスされているのは、座標が0.5の端数であるものです。

また、fill()による塗りつぶしのwinding ruleはnon-zeroのみがサポートされています。

The fill() method must fill all the subpaths of the current path, using fillStyle, and using the non-zero winding number rule.

影を描く (shadow)

Canvas2D APIは2Dなのに影を描画するという機能があります。利用シーンが多いため特別に用意された機能のように思われますが、ちょっと違和感があります。

影にはぼかし効果(blur)を指定できますが、図形やイメージ自体にblurが適用できないのは、ちょっと残念です。

この影の描画はChromeがちょと変な実装になっています。Chromeでは以下のような問題がります。

  • 図形をrotate()で回転すると影の位置自体も回転してしまう。
  • 図形にアルファを付けて薄くしても、影が薄くならない
  • 図形にグラデーションをつけると、影にもグラデーションがついてしまう
  • イメージに影がつかない

一方、Safariでは図形にグラデーションを付けると、影がつかないという問題があります。







Firefox/Opera(左)とChrome(中央)、およびSafari(右)

実際の動作は下記で確認できます。

クリップ境界のアンチエリアス (clip)

Chromeでは、clipでくり抜いた図形の境界がアンチエリアスされないという症状があります。






Chrome以外(左)とChrome(右)

直線に接する弧を描く (arcTo)

arcToの妙については下記の過去のエントリーで取り上げました。

Operaに変化があったようですが、相変わらず激しく変です。以前のOperaは左のような描画をしていましたが、今は右のようなものになっています。少しは顔らしくなったのでしょうか?




Composite

globalCompositeOperationはPorter-Duffの方法で合成されますが、ブラウザによって微妙に結果が異なります。なお、canvas仕様ではlighterというプロパティは定義されていても、darkerというプロパティは定義されていませんが、SafariChromeでは実装されているようです。

Firefoxの結果を以下に示します。

下はOperaの結果です。copyとdarker以外はFirefoxと同じです。

下はSafariChromeの結果です。両者は同じです。Firefox/Operaと比べて大きな違いがあるのがわかります。

Firefox系とChrome系のどちらが正しいかは、Porter-Duffがどう定義されているかを調べればよいわけですがWikipediaの図入り説明が分かりやすいでしょう。

これを見ると、どうやらFirefox/Operaが正しく、Chrome/Safariは間違っているようです。

PorterとDuffによるオリジナルの論文は下記から読むことができます。

ちなみに、下記のページが暫く前に、はてブがたくさん付けられたようですが、ブラウザで実行した結果を表示しているため、ブラウザによりに結果が異なることに注意が必要です。

実際の動作は以下から確認してみてください。

360度以上のarc

arcはstartAngleからendAngleまでの円周上の弧を描画するものですが、その角度が360度以上のときどうなるかは、仕様では以下のように明記されています。

If the anticlockwise argument is false and endAngle-startAngle is equal to or greater than 2π, or, if the anticlockwise argument is true and startAngle-endAngle is equal to or greater than 2π, then the arc is the whole circumference of this circle.

つまり、360度以上のarcは一周全部を描画する必要があります。Chrome以外では正しくないようです。fill()したときの結果も異なります。







Chrome(左)、Safari/Firefox(中央)、Opera(右)

実際の動作は以下より確認できます。

グラデーション

しばらく前までは、ChromeのRadialGradientが酷い状態でしたが、現在は問題は無くなっています。

テキスト (fillText, strokeText)

textBaselineプロパティで垂直位置のベースラインを指定できますが、Firefoxはhangingが正しくありません。alphabeticを指定したときと同じなので、実装されていないのかもしれません。

また、fillText()でSafariOperaはアンチエリアスされますが、FirefoxChromeはされません。strokeText()では全部のブラウザがアンチエリアスされていました。






Firefox/Chrome(左)、Safari/Opera(右)

おわりに

ブラウザ毎に異なるcanvasの細かい動作についてまとめてみました。canvas仕様はまだドラフトなので今の段階で100%準拠は期待できないとしても、ブラウザ間での動作の違いは解消して欲しいものです。

動作を確認するソースコードの説明は一切省略しましたが、殴り書きコードですが興味ある方は中身を覗いてみてください。

*1:getImageData()関数とcreateImageData()関数で実装Operaでは後者の関数は実装されていない。(追記8/22)