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 に解決されたもので、ノードとコントロール・ポイントを描いてみると、

説明と飾り

(気が向いたら続く)

――目次――
HOME雑文写真壁紙馬鹿読書語学
│├英語
│└日本語電算地理
│└白地図ブログ