HTML5 Canvas のarcTo関数の実装が未だにorz

2年近く前にCanvas APIの実装状況を網羅的に調査したことがありますが、ブラウザごとに実装がいろいろ異なっていていました。その中で一番目立ったのがarcTo関数の実装の違いです。改めて調べてみました。

ブラウザのバージョンは以下です。

Chrome Safari Opera Firefox
4.0.249.43 4.0.4(531.21.10) 10.10 3.5.7

IEExplorerCanvas r3を見てみましたがarcToは実装されていませんでした。

テストケース

今回、新たに簡単なテストケースを作りました。左図のような絵を描画するものです。arcToだけを使って描画しています。ちょっとキモイ絵になってしまいした。



Chromeではこうなります

まあまあですが、描かれるべき線と左目の点がありません。

左目の点がないのは、arcToの問題もありますが、さらに長さゼロの線に対して両端(lineCap)がroundのときその部分が描画されるかどうかの違いもあります。ChromeOperaでは長さゼロの線分の場合には描かれません。



Safariではこうなります

1つの線を除き、正しい結果になっています。この1つの線の問題は後で詳しく説明します。



Operaではこうなります

はぁ?これは一体どうしちゃったのでしょう。笑うしかないです。



Firefoxではこうなります

もう、むちゃくちゃですね。トチ狂ってます。



arcToの仕様は?

さて、本題のarcToの仕様はどうなっているでしょうか。W3CのドラフトWHATWGのドラフトを見比べるとこの部分の違いはありません。

arcTo関数の仕様はCanvasの中では確かに複雑な方です。しかし、1つの問題を除き解釈の違いが出るような余地はありません。

ちょっと長いですが、全文引用しておきます。

The arcTo(x1, y1, x2,y2, radius) method must first ensure there is a subpath for (x1, y1). Then, the behavior depends on the arguments and the last point in the subpath, as described below.

Negative values for radius must cause the implementation to raise an INDEX_SIZE_ERR exception.

Let the point (x0, y0) be the last point in the subpath.

If the point (x0, y0) is equal to the point (x1, y1), or if the point (x1, y1) is equal to the point (x2, y2), or if the radius radius is zero, then the method must add the point (x1, y1) to the subpath, and connect that point to the previous point (x0, y0) by a straight line.

Otherwise, if the points (x0, y0), (x1, y1), and (x2, y2) all lie on a single straight line, then the method must add the point (x1, y1) to the subpath, and connect that point to the previous point (x0, y0) by a straight line.

Otherwise, let The Arc be the shortest arc given by circumference of the circle that has radius radius, and that has one point tangent to the half-infinite line that crosses the point (x0, y0) and ends at the point (x1, y1), and that has a different point tangent to the half-infinite line that ends at the point (x1, y1) and crosses the point (x2, y2). The points at which this circle touches these two lines are called the start and end tangent points respectively. The method must connect the point (x0, y0) to the start tangent point by a straight line, adding the start tangent point to the subpath, and then must connect the start tangent point to the end tangent point by The Arc, adding the end tangent point to the subpath.

要するにA:(x0, y0), B:(x1, y1), C:(x2, y2)の3点と半径Rの円弧について、

  1. A=BまたはB=CまたはR=0のときは、線分ABを描く
  2. A, B, Cが直線上にあるときは、線分ABを描く
  3. それ以外のときは、ABをA側に伸ばした半直線L1と、BCをC側に伸ばした半直線L2の両方に接するような半径Rの円弧に対し、L1に接する点をP1、L2に接する点をP2としたとき、A-P1の線分とP1-P2のBに近い方の円弧を描くということです。


この仕様で1つ問題と思われるものは、2.でA, B, CがA, C, BまたはC, A, Bの順に直線上にある場合です。これは直線が「く」の字に折り返されて一直線になるときですから、直線上から少しでもずれた場合、接する円は無限遠に遠ざかるため、AからBとは反対方向の無限遠に伸びる半直線になります。それが、A, B, Cが数学的に直線上になったとき、非連続的に線分ABになるというわけですから、仕様として不自然です。もし、2.をA,B,Cの順番に直線上にあるときと限定して解釈すると、C, A, Bの順に直線状にあるときの仕様が曖昧になります。3.の極限値としての解釈はできますが、そうすると逆に、2.も3.の極限値に含まれるので2.自体が不要になります。また、この場合でも、R=0になった瞬間に1.が適用されるので、非連続性が発生します。つまり、パラメータが連続的に変わっても、描かれる図形が非連続的に変化するというのは、グラフィックAPIとしては好ましくないと思います。

Safariの実装では線分ABではなく、AからBとは反対方向の無限遠に伸びる半直線として描かれています。一方、ChromeではA, B, Cが直線上にある場合は何も描かれません。これはこれで問題です。

また、FirefoxOperaでは円の半径Rがゼロのとき例外が発生します。

こんなことは当然議論になっているだろうと検索したら、下記の記事が見つかりました。
[whatwg] Canvas arcTo all points on a line
これを読むと、Safari方式を前提としているようです。

ソースコード

上記の変な顔の図形と、線分ABとBCを時計の針のように回転して描画されるarcToアニメーションのファイルを下記に置きました。ブラウザで実際に確認できます。

http://dl.dropbox.com/u/1865210/mindcat/arcto.html

次回は気が向いたら、arcToをarcを使って実装することを考えてみます。