Hatena::ブログ(Diary)

風と宇宙とプログラム このページをアンテナに追加 RSSフィード

2010-08-15

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)

os0xos0x 2010/08/15 23:52 > *1:getImageData()関数。Operaでは実装されていない。
あれ、OperaはcreateImageDataは未実装(new ImageDataで代替は可能)ですが、getImageDataは実装してますよね。
ちなみに、単純に拡大というとdrawImageのほうが良さそうですが、アンチエイリアスを効かせないためにピクセル操作で拡大したってことでOKでしょうか?

futomifutomi 2010/08/16 09:18 とても興味深く読ませて頂きました。2点、補足を。

rect()の引数w, hですが、実は、以前のCanvas仕様では負数を認めていなかったんです。そして、もし負数が指定されたらINDEX_SIZE_ERRを出すことになっていました。そう言う意味では、Operaは古い仕様にしっかりと準拠しているようですね。
http://www.w3.org/TR/2008/WD-html5-20080122/#rectx

Canvas仕様は、その後、左回りの必要性も考慮してか、wとhに負数を指定したらエラーとなる規定を削除しました。ということで、最新の仕様では、この記事でも紹介されているとおり、晴れて左回りの矩形のパスをrect()で作れるようになったんです。でも、まだ最新の仕様に追随したブラウザーはないようですね。

次に、globalCompositeOperationですが、使う状況によって挙動がブラウザーごとに違うため、本当に使いづらいですよね。この記事では、パスを使って描いた2つの図形をglobalCompositeOperationで重ねているようですが、drawImage()を使って<img>や別の<canvas>のイメージを取り込んで重ねた場合は、また状況が違ってきます。この場合、私が試した限りでは、Chrome、Safari、Operaの挙動が正しく、Firefoxはダメダメでした。

どのブラウザーでも、同じ挙動になって欲しいですよね。

shinichiro_hshinichiro_h 2010/08/18 01:31 globalCompositeOperation ですが、昔の WebKit はもうちょっとおかしかったのでちょっぴり修正したことがあります。その時この違いも気付いたんですけど、ちょっと聞いてみると WebKit の仕様であるべき、と考えてると言われたように記憶してます。具体的にはこのへんのバグを見ていただければと思います。

https://bugs.webkit.org/show_bug.cgi?id=39177

と言ってもまぁ使う側としては挙動違うのはなんとかしてくれって感じですよねえ…

さてここからは激しく余談というか知ったかぶりたいだけという感じなのですけど、このへんの挙動はバックエンドの描画エンジンとして使ってるライブラリに依存する感じだったりします。

で、例えば同じ WebKit でも WebKit/GTK+ は描画エンジンが Firefox と同じ cairo なので…と書きつつ確認してて気付いたんですが、昔は Firefox と一致してたはずですが、今は Chrome/Safari と一致するように変更したみたいです。でも例えば Qt WebKit はなんか激しくおかしいというか、 Firefox とも Safari/Chrome とも違う挙動をしたりしますし、あと見た目が同じでも getImageData で取ってきた pixel の値なんかは Safari と Chrome の間で大幅に違うはずです。後者の方は Chrome の描画エンジンである skia がやってる最適化のせいだと思います。

revulorevulo 2010/08/18 22:04 FlashCanvas ライブラリのデバッグをするのに、参考にさせていただきました。どうもありがとうございます。

サンプルが IE では動作しませんでしたので、IE & FlashCanvas ライブラリの組み合わせでも動くように、若干変更を加えてみました。

http://flashcanvas.net/examples/dl.dropbox.com/u/1865210/mindcat/

Canvas 要素を多数使ったり、getImageData() を使ったりした場合の動作が非常に遅いのですが、一応 IE でも動作しますので、よろしければご覧下さい。Shadow の動作がおかしいのは、近いうちに修正する予定です。

それと、記事の内容について、いくつか気付いた点をコメントしておきます。

globalCompositeOperation について。
copy operator は "Display the source image instead of the destination image." と定義されていますので、Opera の結果が正しく、Firefox の結果は間違っていると思います。

arc について。
これは Safari/Firefox の結果が正しく、Chrome の結果は間違っていると思います。左下のサンプルは、anticlockwise が true、startAngle-endAngle が -490 度になっています。「角度の差が2π以上」ではないのに、円周すべてを描画するのはおかしいです。

mindcatmindcat 2010/08/22 14:43 ここのところ平日は忙しくて、自分のブログも見られない状態です。

@os0Xさん、ご指摘ありがとうございます。追記しておいきました。拡大にdrawImageを使わなかったのは、ご想像の通りピクセル補間を避けたかったためです。

@futomiさん、html5.jpサイトを運営されている方ですよね?そのサイトで以前、おおいに勉強させて頂きました。詳しい情報ありがとうございます。イメージのglobalCompositeOperationについても後の機会に調査したいと思います。

@shinichiro_hさん、コメントありがとうございます。cairoとskiaは僕も使ったことありますが、ソフトならまだしも、HWアクセラレーションに対応したときどうなるか心配ですね。以前、2D GraphicsはAAG(http://www.antigrain.com/)に期待していたのですが、あまり進展もなく終わっているようです。

@revuloさん、微力ながらまたお役に立てたようで嬉しいです。copyについて多くの説明がOpera方式なのは認識しているのですが、イメージではなくベクトル図形とした場合、図形以外の部分はsourceの対象ではないと解釈もできないかな、と思っています。その方が応用も広くなります。arcについては、ご指摘のとおりですね。ただ、endAngleの位置にlineToで描いているのですが、その線がないのが気になります。

dynamisdynamis 2010/08/22 15:38 いつものことながら良くまとまってる記事で、大変参考になります。

ちなみに、arcTo については Opera だけでなく Firefox も 3.6 で改善されており、顔らしくはなっているのでご確認いただけるとうれしいです。

mindcatmindcat 2010/08/23 01:48 @dynamisさん、僕も普段はFirefoxを使っているので了解しております。archToが改善されていることはhttp://d.hatena.ne.jp/mindcat/20100131/1264958828 でも紹介させていただきました。

勉強会のお誘いの件、ありがとうございます。ただ、ここしばらく、本業の仕事の方の開発が非常に忙しい状況が続いていて、申し訳ありませんが、辞退させてください。今後もよろしく。

トレードメモ管理人トレードメモ管理人 2014/08/22 08:21 自分のサイトでcanvasを使っていろいろ作っているため、作る時にこのサイトを先に見ていればな〜といまさらながら思いました。
ディープな情報がまとまっていてとてもよいサイトですね。
これからもちょくちょく覗かせていただきます。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証