02 幾何図形の描画
この節では,幾何図形の描画をします。幾何図形は作図ツールで描くことができますが,ツールにない扇形や,ツールにあってもそのままでは使いにくい矢印や矢じりを作ります。
弧
作図ツールには,3点を指定して弧を描くものがあります。しかし,3点をとらなくても弧が描けると便利です。
円の一部としての弧
円は,媒介変数表示で x=r cosθ,y=r sinθ として描けます。すると,θの範囲を制限することで弧が描けることになります。これは,次の1行でできます。
plot(pa+r*[cos(t),sin(t)],start->th1,stop->th2,steps->200,size->sz,color->col);
paは中心です。th1とth2が弧の範囲を示す角です。steps はきれいな円を描くのと,ぴったりその範囲になるようにするためのものです。弧の大きさによっては200でなく100でも十分ですが,大きめに取っています。
szは線の太さ,colはRGBのカラーコードです。
線の太さやカラーを変数にしたのは,こうすると関数化できるからです。
drawarc(pa,r,th1,th2,sz,col):=(
plot(pa+r*[cos(t),sin(t)],start->th1,stop->th2,steps->200,size->sz,color->col);
);
たとえば,drawarc([1,1],2,0,pi/4,1,[1,0,0]) とすると,(1,1)を中心とした半径2の,円の8分の1の弧が赤で描かれます。
これはこれでよいのですが,いつも線の太さや色を指定するのは面倒です。線の太さだけ,あるいは色だけが指定できたり,両方とも指定せずにデフォルト値で描ける方が便利でしょう。
Cindyscriptのユーザー定義関数では,同じ名前の関数でも,引数の数が異なると,別の関数として解釈する,という便利な仕組みがあります。
そこで,この関数を元にして,引数を減らしたものを作ります。
drawarc(pa,r,th1,th2):=drawarc(pa,r,th1,th2,1,[0,0,1]);
drawarc(pa,r,th1,th2,op):=(
if(islist(op),
drawarc(pa,r,th1,th2,1,op);
,
drawarc(pa,r,th1,th2,op,[0,0,1]);
);
);
一つ目が,中心と半径,角の範囲だけのもの,2番目が,引数に太さまたは色を追加したものです。
追加した引数が1つのとき,それがリストであれば(islist(op))色コードと解釈し,そうでなければ太さと解釈します。引数に不正な値を入れた場合のエラー処理はしていません。自家用の関数ですから。
中心と2点で決まる弧
次に,点Aを中心として,BからCまでの弧を描きます。ABとACが等しくないときは,短い方で描くことにします。
前項の drawarc() を使って描きます。つまり,BとCの位置から,2つの角度を求めればよいのです。
ABとACのなす角は,組み込み関数 arctan2(ベクトル) を用います。これは,ベクトルがx軸の正の方向となす角を求める関数です。
drawarc2(pa,pb,pc,sz,col):=(
regional(th1,th2,r);
th1=arctan2(pb-pa);
th2=arctan2(pc-pa);
if(th1>=th2,th2=th2+2*pi);
r=min([|pa,pb|,|pa,pc|]);
drawarc(pa,r,th1,th2,sz,col);
);
引数の pa,pb,pc が3点です。pa が中心という前提です。
・ regional(th1,th2,r); で,この関数の中で使う変数を局所変数にしています。
・ th1=arctan2(pb-pa); th2=arctan2(pc-pa); で,AB,AC がx軸となす角を求めています。
ここで,arctan2() の戻り値は -π から π です。そこで,弧を反時計回りに掛けるように
if(th1>=th2,th2=th2+2*pi); で調整しています。
この関数も,sz や col を与えなくてもよいようにしておきましょう。
これを使うと,次のようにして,角の印を描くことができます。
3点を pa,pb,pc とします。
pa=[1,1];
pb=[4,2];
pc=[2,3];
draw(pa,pb);
draw(pa,pc);
pt=pa+(pb-pa)/|pa,pb|/2;
drawarc2(pa,pt,pc,1,[1,0,0]);
2点を両端とする弧
2点を両端とする弧を描きます。もちろん,2点だけでは中心、半径が決まりません。そこで,第3の引数で膨らみ具合を決めることにします。
原理を次の図で説明します。
2点A,Bに対し,円の中心は線分ABの垂直二等分線上にあります。この点を設定すれば,「中心と2点で決まる弧」が使えます。
中心を決めるために,この二等辺三角形の底角A(またはB)を指定します。そうすれば,複素数を利用して,回転と拡大で中心が決められます。
底角を th とします。線分ABの半分の長さ(直角三角形の短辺)と斜辺の比が1/cos(th) ですので,BをAのまわりに th だけ回転し,この比で拡大します。
th は第3引数を r としたとき,たとえば th=r*pi/12 で決めることにします。
drawarc3(pa,pb,r,sz,col):=(
regional(z1,z2,z3,radius);
th=r*pi/12;
z1=complex(pa);
z2=complex(pb);
z3=z1+(z2-z1)/2/cos(th)*(cos(th)+i*sin(th));
center=gauss(z3);
drawarc2(center,pa,pb,sz,col);
);
描かれる弧は pa から pb へ反時計回りに描かれます。
これも,引数を減らした場合のものを作っておきましょう。
次のように弧の曲がり具合が変わります。
drawarc3(A,B,1);
drawarc3(D,C,2);
drawarc3(F,E,3);
drawarc3(H,G,4);
これらの中間で,というのであれば,第3引数を小数にすればよいのです。
扇形
Cinderellaの作図ツールには扇形はありません。もちろん,線分と弧で扇形は描けるわけですが,中を塗ろうとするとそこで困ってしまいます。そこで,前項の弧にならって,中を塗る弧も描けるようにします。
Cindyscriptには,領域を塗る関数として,多角形の内部を塗る fillpoly() と,円の内部を塗る fillcircle() しかありません。
そこで,扇形を多角形として作り,fillpoly() で塗ります。
円の一部としての扇形
まず,中を塗らない扇形です。drawarc() で弧を描き,2つの半径を描けばよいので,次のようにできます。
//扇形を描く 点paを中心する半径rの,角th1からth2までの弧で扇形
drawsector(pa,r,th1,th2,sz,col):=(
regional(p1,p2);
p1=pa+r*[cos(th1),sin(th1)];
p2=pa+r*[cos(th2),sin(th2)];
drawarc(pa,r,th1,th2,sz,col);
connect([p1,pa,p2],size->sz,color->col);
);
次に,中を塗る扇形ですが,塗らない場合に plot() で描いていた分を,点のリストにします。
fillsector(pa,r,th1,th2,col):=(
regional(dt,pd);
dt=(th2-th1)/200;
pd=[pa];
forall(0..200,pd=append(pd,pa+r*[cos(th1+#*dt),sin(th1+#*dt)]));
fillpoly(pd,color->col);
);
・ dt=(th2-th1)/200; は,点をとる角度の間隔です。200は多いように思いますが,大きな弧でも滑らかに描けるように大きめにしています.
・ pd=[pa]; 点リストです。はじめに,点Aの座標を入れておきます。
・forall(0..200,pd=append(pd,pa+r*[cos(th1+#*dt),sin(th1+#*dt)]));
0..200 で 0 から 200 までの整数列(のリスト)を作ります。
そのすべての要素(数)について,扇形上の点をとって,リスト pd に追加していきます。
・fillpoly() で多角形を塗ります。
中心と2点で決まる扇形
次に,中心と,弧の両端を指定した扇形です。
// 点paを中心として pb から pc までの弧で扇形 AB!=AC のとき,AB,ACの短い方を半径とする
drawsector2(pa,pb,pc,sz,col):=(
regional(th1,th2,r);
th1=arctan2(pb-pa);
th2=arctan2(pc-pa);
if(th1>=th2,th2=th2+2*pi);
r=min([|pa,pb|,|pa,pc|]);
drawsector(pa,r,th1,th2,sz,col);
);
fillsector2(pa,pb,pc,col):=(
regional(th1,th2,r);
th1=arctan2(pb-pa);
th2=arctan2(pc-pa);
if(th1>=th2,th2=th2+2*pi);
r=min([|pa,pb|,|pa,pc|]);
fillsector(pa,r,th1,th2,col);
);
先ほどの角の印を塗ってみましょう。
pa=[1,1];
pb=[4,2];
pc=[2,3];
pt=pa+(pb-pa)/|pa,pb|/2;
fillsector2(pa,pt,pc,[1,0,0]);
draw(pa,pb);
draw(pa,pc);
先に角の印を描いてから線分を描くのがちょっとした要領です。
矢印と矢じり
作図ツールでは,線分を描いておいて,インスペクタでそれを矢印に変えることができます。しかし,Cindyscriptの draw() で描いた線分ではそれができません。そこで,Cindyscriptでも矢印が描けるようにします。そのために,まず矢じりを描きましょう。
点と方向を与えて矢じりを描く
矢じりの形状をどうするかは,いろいろ考えがあるでしょう。まず,次の図を見てください。
この図では,矢の向いている方向から, 8π/9 という角度で矢線を引いています。開きはこの角度で決まりますので,あとは好みでしょうか。
上の図は線だけの矢じりです。次のような,塗った矢じりも考えられます。
こんどは,切れ込みの部分をどうするかが問題ですが,このあたりを好みに応じて作ることができるのがプログラミングの楽しみでもあります。
さて,矢じりの線分を描くのに,その先端の位置は,上図の点線の線分を回転して取るのがわかりやすいでしょう。回転の方法はいくつかあると思いますが,(たとえば,弧を描くのに使った方法)ここでは,複素数を使ってみます。原点まわりの角 th の回転は cos(th)+i*sin(th) をかければよいので,あとはこれを平行移動するだけです。引数は,点と方向ベクトルの他に,大きさと色にします。
arrowhead(pt,vec,sz,col):=(
regional(th,z1,z2,z3,z,4,z5,p3,p4,p5);
th=pi*8/9; // 矢じりの開き
vec=vec/|vec|;
z1=complex(pt);
z2=complex(vec);
z3=z1+sz*z2*(cos(th)+i*sin(th));
z4=z1+sz*z2*(cos(-th)+i*sin(-th));
z5=z1-sz*0.7*z2; // へこみ具合
p3=gauss(z3);
p4=gauss(z4);
p5=gauss(z5);
fillpoly([pt,p3,p5,p4],color->col)
);
fillpoly() の行を,draw(pt,p3); draw(pt,p4); にすれば,線の矢じりになります。
この中で,3行目の vec=vec/|vec|; は,引数のベクトルを単位ベクトルにしています。これで,方向ベクトルの大きさとは無関係に,サイズ sz で矢じりの大きさが決められます。
矢印
線分を描いて矢じりをつければ矢印になります。
線と矢じりは別々に描いているので,弧に矢じりをつけることもできます。
drawarc([1,1],2,0,pi/4);
p2=[1,1]+2*[cos(pi/4),sin(pi/4)];
arrowhead(p2,[-1,1.2],0.5);
ここで,方向ベクトルを [-1,1.2] としているのは,接線方向の[-1,1] だと中心線がちょっとずれるためです。
多角形
内角を指定した三角形
1辺の長さを与えて,指定した内角の三角形を描きます。言い換えると,二角と夾辺が与えられた三角形です。
考え方は,次の通りです。
(1) 1辺を,端点 p1,p2 を中心に与えられた角だけ回転した点を p3,p4 とする。
(2) p1,p3を通る直線と,p2,p4 を通る直線の交点を interll() で求め,p5とする。
(3) p1,p2,p5を結ぶ。
ここで,interll() は,「応用編01 値・式の取得」にあるものです。
例として,2点(1,1) , (2,5) を結ぶ線分を1辺とし,その両端の角が40°,50°の三角形を描いてみましょう。
p1=[1,1];
p2=[5,2];
th1=40°;
th2=50°;
z1=complex(p1);
z2=complex(p2);
z3=z1+(z2-z1)*(cos(th1)+i*sin(th1));
z4=z2+(z2-z1)*(cos(-th2)+i*sin(-th2));
p3=gauss(z3);
p4=gauss(z4);
p5=interll(p1,p3,p2,p4);
drawpoly([p1,p2,p5]);