HOME > 電算 > asymptote メモ
asymptote メモ
Asymptote(http://asymptote.sourceforge.net/)はベクタ・グラフィックスのための言語。GPLだ。字で命令を書いて、ポストスクリプトで出力できたりする Metapost っちいもの。UNIX, Linux, MacOS, Microsoft Windows などで動く。LaTeX に依存せず(ラベルを LaTeX 形式で書くことはできるが)、融通が利いてコードも(自分の書いたやつなら)スッキリ見える。すぐ使えるカンタンな文法を持つ一方、代入時に起こる暗黙の型キャストのふりをして、キュービックスプライン補間をしてしまうなど、ユーザが気づかぬうちに大胆?なことを行おうとする傾向もある。日本語は通らない(後日 http://oku.edu.mie-u.ac.jp/~okumura/blog/node/2337 に日本語が通ると書いてあるのを発見。まだ試していない)。
[リンク]何ができるのか手っ取り早くイメージしたい人は、本家 http://asymptote.sourceforge.net/ の Document から公式の Tutorial がたどれる(し、マニュアルもそこにある)。http://www.artofproblemsolving.com/Wiki/index.php/Asymptote_%28Vector_Graphics_Language%29 にもチュートリアルがある。また、http://piprim.tuxfamily.org/asymptote/generale/index.html に使用例がある。
ウソ書いてたらごめん。とってもウソ書いてそうな気がする……
インストールなど
本家は http://asymptote.sourceforge.net/ で、ここからダウンロード。rpm とかもある。わしの Linux は古くさいので glibc が古くて、最新の asymptote はインストールできなんだ。しかたないから、バージョン 1.18 をインストール。
標準的なディレクトリ構成
/usr/loca/bin/ 本体 /usr/local/share/asymptote/ モジュール /usr/local/share/doc/ ドキュメント /usr/local/info/ info用 /usr/local/man/man1/ man用 /usr/share/texmf/tex/latex/asymptote/ LaTeX 用スタイルファイル ~/.asy/config.asy 設定ファイル
起動は
$ asy
で、対話モード。カレント・ディレクトリに out.eps というファイルが出力され、それを(標準では) gv で表示する。
$ asy unko.asy
のごとくすると、ファイルから命令を読み取る。この場合、カレント・ディレクトリに unko.esp ができるだけ。-V オプションをつけると、ファイルに書き込むのと同時に gv で表示してくれる。対話モードから asymptote を終了させるには、
> quit
でよろしい。
ベクトル表現と演算
pair というデータ型
データ型 には、void, bool, int, real, string などいろいろあるが pair というやつがよく使われる。見てのとおり、実数のペア。平面座標をあらわすのに使われる。ほかに、triple があり、3次元用。
> pair a; // 宣言(&暗黙の初期化) > a=(1,2); // 代入
で、a が定義できる。宣言をすると同時に (0,0) に初期化される。a に何が入っているのか見たいならば、
> write(a); (1,2)
のような具合。変数を評価しても自然に出力されるわけではないから write( ) を使う。
pair 型から、一つひとつの実数を取り出すには 「. 」というオペレータを使う。
> pair b = (1,2); // 宣言&代入 > write(b.x); 1 > write(b.y); 2
+ - の各オペレータを pair に対して使うと、
> pair c = (3,4); > pair d = (5,6); > write(c+d); // 8=3+5 10=4+6 (8,10) > write(c-d); // 3-5=-2 4-6=-2 (-2,-2)
これは、わかりやすい。次に * / のオペレータを pair に対して使ってみる。
> write(c*2); // これは予想通り > (6,4) > write(c/2); // これも予想通り > (1.5,2) > write(c*d); // これはどうなる? > (-9,38) > write(c/d); // これはどうなる? > (0.639344262295082,0.0327868852459016)
pair というのは、じつは複素数をあらわしていたのだということがわかる。そうだったのかぁ。でも、知らん顔をして pair はただのベクトルだと思い込んで使うことにする。
定数
組込み定数に pi というものがある。
> write(pi) 3.14159265358979
transform というもの
(a,b,c,d,e,f) は transform というもので、これを (x, y) という pair 型にに左から掛けてやると、アフィン変換ができる。つまり、
|a| |c d| |x| | | + | | * | | |b| |e f| |y|
になるということ。点 (2,3) を x 軸 y 軸方向に 10 だけ移動させるには
> write ((10,10,1,0,0,1) * (2,3)); (12,13)
のようにすればよい。6 つも数字を書くのは面倒だから、shift(10,10) * (2,3) と書いてもよろしいことになっている。これは、先に見たとおり (10,10)+(2,3) でもかまわないよね。同様に、x, y とも 2 倍にするための (0,0,2,0,0,2) は scale(2) と書いてよろしい。これも、先に見たとおり、2 * (2,3) でもかまわない。
unit, dir という関数
ベクトル (a,b) に対し、unit((a,b)) でその方向の単位ベクトルを求めることができる。
> pair a=(1,1); > write(unit(a)); > (0.707106781186548,0.707106781186548)
plain というベース・モジュール(とくに import の要なし)には、
N, up (0,1) NNE unit(N+NE) NE unit(N+E) ENE unit(E+NE) E, right (1,0) ESE unit(E+SE) SE unit(E+S) SSE unit(S+SE) S,down (0,-1) SSW unit(S+SW) SW unit(S+W) WSW unit(W+SW) W, left (-1,0)
という単位ベクトルが定められている。
角度 d に対し、dir(d) で、その方向の単位ベクトルを求めることができる。d の単位は degree である。ちなみに、sin(r) や cos(r) のとき、r は radian だ。
> real d=45; //degree > write(dir(d)); (0.707106781186548,0.707106781186547) > write((sin(d*pi/180),cos(d*pi/180))) (0.707106781186547,0.707106781186548)

キャンバス
picture というキャンバス
描画は picture と呼ばれるキャンバスに対してなされる。picture は
> picture mypic;
のようにして宣言する。
dot という関数は pair 型をとり、picture に描画する。では、先に用意した mypic に点を打ってみる。
> pair p1, p2; > p1 = (0,0); > p2 = (100,100); > dot(mypic,p1); > dot(mypic,p2);
だが、これだけでは実際に何も描かれない。図形が載った picture を Postscript 座標を持つ平面にはりつけて、出力しないといけない。shipout という関数は、picture を Postscript 座標にはりつけることにより実際に EPS 出力をする。
> shipout(mypic);

※ dot に 一つの pair を与えると、点を描画するが、二つの pair a, bを与えると、ベクトル a とベクトル b の内積を返す。
座標系
単位(というか座標系)については、いささか混み入ったことがある。picture にサイズを与えない場合は、picture 中の長さを表す数字は shipout 時に Postscript の座標系によって解釈される。つまり Postscript にいう big point(s) という単位がついているとみなされる(1 bp = 1/72 inch である)。したがって、前の例で打った 点 (0,0) と 点 (100,100) 間の距離は、sqrt(20000)/72 インチで出力される。( sqrt(real) はルート real を返す。)
picture には大きさを与えることができ、この場合、話がちょっと違ってくる。これが Metapost との大きな違いであると、asymptote の公式ホームページには書いてある。図形のプロポーションを歪めない範囲で、図形が picture の大きさいっぱいになるように座標系が定まるのだ。
> picture mypic; > size(pic=mypic, 2cm, 1cm); > pair p1, p2; > p1 = (0,0); > p2 = (100,100); > dot(mypic,p1); > dot(mypic,p2); > shipout(mypic);

赤字(太字)にした部分により、mypic の x 方向サイズが 2 cm に、y 方向のサイズが 1 cm に決まる。そして、ここが大事なところなのだが、このようにサイズを与えると、その picture の中にある図形が、指定されたサイズにおさまるように picture 内部の座標系が調節されるのである。したがって、上の例では、y 軸方向の 1 cm をいっぱいいっぱいに使い、x 軸方向には余裕を残したまま、1 cm x 1 cm の正方形のおおむね左下と右上に二つの点が出力される。(おおむね、と言ったのは、実際は少しのマージンが考慮される故なり)。
size を picture に与えるのは dot する前でもいいし、後でもいい。shipout する前ならばよろしい。size の引数で pic= を省略して、size(pic1,1cm,2cm) のようにしてもいい。size(pic1,1cm) とすると、size(pic1,1cm,1cm) と同じである。また、たんに size(1cm, 2cm) とすると、currentpicture(後述)のサイズが 1cm x 2cm に指定される。size(pic=pic1,1cm,0) とすると、x 軸方向に 1cm の制限がかけられるが、y 軸方向のサイズは無制限になる。また、size(pic=pic1,0,0) とすると、座標系が最初の状態に戻り、1 が 1 bp をあらわすようになり、出力の大きさにかんする制限が解除される。
[暮らしの安心メモ]サイズを与えた picture に一つだけ点を打つと、warning: x scaling in picture unbounded とか warning: y scaling in picture unbounded とかいう警告が出されるが、これは「大きさのない点をひとつ打って picture のサイズに拡大しろって? 点は大きさがないから拡大できないじゃないですか。無理無理」と言っているのである。
add という関数
ある picture の内容を別の picture に追加することができる。add(picture1, picture2 によって、picture2 に載っている図形が、picture1 に追加コピーされる。
> picture pic1; > size(pic=pic1, 5cm, 5cm); > picture pic2; > size(pic=pic2, 1cm, 1cm); > dot(pic1, (0,0)); > dot(pic1, (100,100)); > dot(pic2, (100,0)); > dot(pic2, (0,100)); > add(pic1, pic2); > shipout(pic1);

上の例では一辺 5 cm の正方形の四つの角に点が打たれる。座標系は picture に属するものであり、図形に属するものではないので、追加される図形がもつ長さは、単位なしのまま先方に輸出されるから、pic2 に対する 1cm x 1cm という指定は出力に影響していない。
currentpicture という picture
じつは、currentpicture という名前の picture が最初から用意されていて、さまざまな関数において、picture の指定を省略した場合デフォルトになっている。さらに、dot などの描画関数で currentpicture を対象とした場合、描画した瞬間、暗黙のうちに shipout される(一方、add の場合は、dot の場合と違って、相手が current picture であっても暗黙のうちに shipout がされることはないから、明示的にこれを行う必要がある)。
したがって、
> picture mypicture; > dot(pic=mypicture, (0,0)); > shipout(mypicture);
というのは、たんに
> dot((0,0));
とやるのと同じである。
[暮らしの便利メモ]複数のピクチャーを統合するときに実際的な方法は、各 picture をデフォルトの picture である currentpicture に add していくことである。これなら、add(pic1); add(pic2) とだけやればよいわけだ。
erase という関数
picture の内容を消すためには
> erase(mypicture);
というように行う。ただし、このとき座標系にかんする情報は消去されない。座標系にかんする情報も消去したいなら、
> erase(mypicture); > size(mypicture,0,0);
とする。なお、erase もデフォルトの picture は currentpicture である。
[暮らしの便利メモ]やっかいな図を対話的につくるときは、picture backup; とやって backup 用の picture を用意しておき、バックアップしておきたい時点で erase(backup); add(backup,currentpicture); とやっておけば、「やっちまったー」ってときに、erase(); add(backup); で簡単に前のバックアップまで currentpicture の内容を戻すことができる。
frame というキャンバス
asymptote には、picture のほかに frame という描画キャンバスがある。これは常に Postscript の座標を有し、picture のようにそれが変化したりしない。したがって、frame 中の図形を picture に add しても、それが伸縮して出力されたりはしない。そのため、キャプションの文字を入れておくなどの用途に用いられる。
あらたな frame の宣言は
> frame myframe;
のように行う。
次は、frame と picture の違いを見るための例。// 以後はコメント。
> picture mypic; > frame myframe; > // currentpicture はデフォルトなので picture currentpicture; は不要 > size(5cm, 5cm); // size(pic=currentpicture, 5cm, 5cm); と同じこと > dot(mypic,(0,0)); > dot(mypic,(10,10)); > dot(myframe,(0,10)); > dot(myframe,(10,0)); > add(mypic); // add(currentpicture, mypic); と同じこと > add(myframe); // add(currentpicture, myframe); と同じこと warning: y scaling in picture unbounded

mypic 中の 2 点は、輸出相手の currentpicture のサイズに合わせて広がれるだけ広がって出力された(picture の右上端、左下端へ二点)が、myframe の中の 2 点は、輸出されても最初に与えられた座標、つまり、(0 bp, 10 bp) と (10 bp, 0 bp) を律義に守って出力されている(左下付近の別の二点)ことがわかる。
[くらしの安心メモ]frame の内容を、大きさを与えた picture に add したとき、warning: x scaling in picture unbounded とか warning: y scaling in picture unbounded とかいう警告が出ることがある。frame に描画したものはサイズを持つ picture にはりつけても伸縮しないから、「図形があんたの考えてるサイズからはみ出したよ」と言っているのである。
曲線と直線
path というもの
path はノードと、コントロール・ポイントの列によって、ベジエ曲線(その特殊なケースとしての直線を含む)を表現するものである。(このメモではベジエ曲線といえば 3 次のベジエ曲線を指すことにする)
では、path について見む。まず、
> pair a=(0,0); > pair b=(0.5,1); > pair c=(1,0);
のような三点のノード候補に加え、
> pair p1=(-0.309017,0.412023); > pair p2=(-0.0150283,1); > pair p3=(1.01503,1); > pair p4=(1.30902,0.412023);
のようなコントロール・ポイント候補を作成しておく。
先に示しておくと、次の図のようなことがやりたいのである。

そのためには、まず、
> path mypath;
のように path を宣言する。つぎに、
> mypath=(a..controls p1 and p2..b..controls p3 and p4..c); > write(mypath); // 代入された内容を確認 (0,0).. controls (-0.309017,0.412023) and (-0.0150283,1) ..(0.5,1).. controls (1.01503,1) and (1.30902,0.412023) ..(1,0)
このように、ノードをあらわす pair の間にコントロール・ポイントをはさんだ形で path を記述し、代入してやればよい。
path を描画するには、
> size(5cm); > draw(mypath);
のようにする。

なお、draw も dot と同じで、デフォルトの picture は currentpicture である。そして、この場合、描画されると同時に暗黙の内に shipout される。
線分をあらわす path
線分をあらわす path は、一般にベジエ曲線をあらわすパスの特殊なケースに過ぎない。
> pair a=(0,0); > pair b=(1,1); > pair p1=(0.333333,0.333333); > pair p2=(0.666667,0.666667); > picture mypic; > size(mypic, 5cm, 5cm); > dot(a,red); > dot(b,red); > dot(p1,blue); > dot(p2,blue); > path mypath=(a..controls p1 and p2..b); > draw(mypic, mypath); > shipout(mypic);

(上の図で、点につけたラベルは、別のアプリケーションでつけ足したもの。)
コントロール・ポイントがない path を作れば線分が引けるのではないかと思うかもしれないが、それは許されない。path のノードは開放された端点では内側に一つ、それ以外では前後に一つのコントロール・ポイントを持たなくてはならない。不満に思う向きは、guide の項を見ると了解されるだろう。
閉じた path
path を閉じるには、最後に ..cycle というのをつければよい。path を閉じると、コントロール・ポイントが、path が閉じていないときより 2 つ余計に必要になる。
> pair a=(1,0); > pair b=(0,1); > pair c=(-1,0); > pair p1=(1,0.5); > pair p2=(0.5,1); > pair p3=(-0.5,1); > pair p4=(-1,0.5); > pair p5=(-1,-0.5); > pair p6=(1,-0.5); > mypath=(a..controls p1 and p2..b..controls p3 and p4..c..controls p5 and p6..cycle); > size(5cm); > draw(mypath);

補遺:path を食う関数
guide というもの
コントロール・ノードを自前で作成しないと線分一本引けないというのでは困る。しかし、guide というものがあるから心配ない。guide は、ノードたちの位置を持っている点では path と同じである。しかし、path と異なりコントロール・ポイントじたいを持たず、asymptote がそこからお任せでパスを作成してくれる。
guide は次のように宣言する。
> guide myguide;
ヒントをまったく与えないときの guide の書き方は、
> pair a=(0,0); > pair b=(0.5,1); > pair c=(1,0); > myguide = (a..b..c);
のようなものだ。
guide を path に変換するのは、暗黙のうちに行われる。たとえば、
> path mypath=myguide;
のように代入してやると、キャストされた結果、mypath にパスが入っている。
> write(mypath); (0,0).. controls (-0.309017,0.412023) and (-0.0150283,1) ..(0.5,1).. controls (1.01503,1) and (1.30902,0.412023) ..(1,0)
4 つのコントロール・ポイントが自動的に計算されているのがわかる。あとは、フツーに、
> draw(mypath);
とやると、描画できる。

(上の図はわかりやすいように、ノードとコントロール・ポイントを描き込んである。)
ちなみに(というか、実用上大事な点だが)guide から path への暗黙のキャストは、draw( ) 関数の引数として path のかわりに guide を食わせたときにも起こるから、先ほどの例でいうなら、はじめから
> draw((a..b..c));
とやっても、結果は同じじゃ。
ここまで勝手にやられると、一体どうやってコントロール・ポイントを計算しとるのかということが気になってくる。マニュアルによると、asymptote が guide から path を得るための方法は、Donald Knuth の 小論文 The MetaFontbook の Chapter 14 に書いてあるものだそうだ。どうやら、極力円弧に近づくという方針らしい。
ところで、円弧っぽいもの以外は guide を使って描けないのかというと、そうではない。ユーザはどのようにノードを補間するか(コントロール・ノードをどこに作成するか)にかんするヒントを guide に含めてやることができる。
guide に記すことのできる補間のためのヒントは3種類ある。tension,direction,curl だ。
tension は、スプライン曲線のテンションを定めるものだそうで、ノードを結ぶ曲線がどのくらい「ツッパル」かを示す大きさで、数字が大きいほど直線に近くなるという。(正直にいうと、私はこれがどういう仕組みなのか理解していないんだ)。0.75 以上の実数が指定でき、デフォルトは 1。tension は、一つのセグメント(一連の path の中でふたつの node にはさまれた部分)について二つ指定できる。
nodeA..tension 1 and 2..nodeB nodeA..tension 1 ..nodeB
という書き方ができる。tension が一つしか指定されていないときは、両方に同じ値が与えられたと見なされる。
> pair a=(0,0); > pair b=(0.5,1); > pair c=(1,0); > dot(a); dot(b); dot(c); > draw((a..tension 0.75 and 0.75 ..b..tension 0.75 and 0.75 ..c),blue); > draw((a..tension 1 and 1 ..b..tension 1 and 1 ..c)); > draw((a..tension 2 and 2 ..b..tension 2 and 2 ..c),red);

direction は、ノードとコントロールポイントを進行方向に結ぶベクトルと同じ方向の単位ベクトル。(0,1) でもいいし unit(1,1) とか dir(45) でもいい。
開放されたパスの端点以外は一つのノードにつき、二つの direction を書くことができ、開放されたパスの端点では一つの direction を書くことができる。また、そもそも direction は、全く書かなくてもかまわない。
具体的な書き方は、
{dir(45)}nodeA{dir(-45)}
{dir(45)}nodeA
nodeA{dir(-45)}
nodeA
の如し。
size(5cm);
pair a=(0,0);
pair b=(0.5,1);
pair c=(1,0);
pair b1=unit((1,1/2));
pair b2=unit((1,1/2));
path mypath=(a..{b1}b{b2}..c);
draw(mypath);
上の例では 点 a, b, c の位置と、点 b における二つのハンドルの方向を定めて描画する。
なお、direction として指定するベクトルの向きに注意。

(上の図はわかりやすいように、ノードとコントロール・ポイントを描き込んである。)
curl は、端点ノードの曲率を与える。 数値が大きいほど「曲がり」がはげしい。0 が「真っ直」で、1 が「円っぽい」である。デフォルトは 1 である。具体的な書き方は、
{curl 0.5}nodeA{curl 0.5}
{curl 0.5}nodeA
nodeA{curl 0.5}
nodeA
の如し。
size(5cm);
pair a=(0,0);
pair b=(0.5,1);
pair c=(1,0);
label("a",a,S);
label("b",b,N);
label("c",c,S);
draw ((a{curl 0}..b..{curl 0}c),blue);
draw ((a{curl 1}..b..{curl 1}c));
draw ((a{curl 10}..b..{curl 10}c),red);

二つのノードにはさまれたひとつのセグメントの両側に curl が指定された場合、curl の値に関係なくそのセグメントは真っ直になる。また、curl は direction と両立しない。
線分をあらわす guide
さいしょに、
> size(2cm); > pair a=(0,0); > pair b=(1,1); > pair c=(2,0);
direction を使う場合は、
> guide myguide=(a{unit(b-a)}..{unit(b-a)}b{unit(c-b)}..{unit(c-b)}c);
> draw(myguide);

curl を使う場合は、
> guide myguide=(a{curl 0}..{curl 0}b{curl 0}..{curl 0}c);
> draw(myguide);
一つのセグメントの両端に curl が設定されているなら、curl の値に関係なくそのセグメントは真っ直になるから、curl の値は何でもよい。

で、同じ折れ線を描くことができる。a-b, b-c 間にノードがはさまれないから、tension がいくつであっても(つまりデフォルトの 1 であっても)、真っ直な線になる。
折れ線をあらわす (a{curl 1}..{curl 1}b{curl 1}..{curl 1}c) という guide を (a--b--c) と書くこともでき、非常にしばしば用いられる。これは、たいがいのチュートリアルの最初に記されている表現である。
閉じた guide
閉じたパスに解決されるように guide を書くためには、path の場合と同様、--cycle というのを最後につけてやればよい。
基本的な図形
長方形・正方形
unitsquare
unitsquare という guide が用意されている。南西端を (0,0) にもつ一辺 1 の正方形である。しばしばこれを transform でアフィン変換して、任意の長方形を描くのに用いる。
> write(unitsquare);
(0,0)
{curl 1}..{curl 1}(1,0)
{curl 1}..{curl 1}(1,1)
{curl 1}..{curl 1}(0,1)
{curl 1}..{curl 1}cycle
のようにして見てみると、セグメントの両端に curl を指定することにより、真っ直な線を得ていることがわかる。
path に解決された後のノードとコントロール・ポイントを描いてみると、

のように確認できる。
円・円弧
円や円弧も一般の path によって表現され、特別なものではない。
unitcircle という、中心 (0,0) で半径 1 の円をあらわす guide が用意されているので、例としてこれを見てみる。
> write(unitcircle) (1,0) ..(0,1) ..(-1,0) ..(0,-1) ..cycle
よくある東西南北の 4 点をノードに持つベジエ曲線である。path に解決されたもので、ノードとコントロール・ポイントを描いてみると、

説明と飾り
(気が向いたら続く)

