- 開発環境
- プログラムは「(仮称)十進BASIC」を使って作成します(「(仮称)十進BASICのセットアップ」を参照)。
スピログラフ(spirograph)というものがあるそうです。
歯車を使って図を描くもののようですが、僕は知りませんでした。もしかしたらどこかで目にしていたりするのかもしれませんが、少なくとも記憶には残っていません。
何となく面白そうなので、プログラムで描いてみます。
この文書のゴールを示すために、まずはデモンストレーションを行います。
次のファイルをダウンロードします。
ファイル庫/Spiro/demo/demo-spiro.bas
demo-spiro.bas
を(仮称)十進BASICで開くと、プログラムコードが表示されます。
ツールバー上の実行ボタン 、あるいはF9キーで実行します。
途中で止める時は、ツールバー上の中断ボタン 、あるいはCtrl+Bキーで止めます。
上のような感じでパラメータの入力を二回求められますが、空欄のままOKボタンを押すかEnterキーを押して構いません。
すると、グラフィックスウィンドウ上にスピログラフが描かれていきます。
大きな円の円周に沿って小さな円が転がり、その転がる円の中の一点が描く軌跡 — それがスピログラフです。
描き終わると次のような図形が現れます。
描かれる図形は、与えるパラメータによって変わってきます。
例えば、最初に求められる入力で「8,5,4」と入力してみます。二つ目の入力は空欄のままで構いません。
すると、次のような図形が描かれます。
とまあ、こんな感じで遊ぶおもちゃです。
物事には段階があります。
まずは、円(circle)を描くところから始めます。
図1のように、中心 C の座標が (xc,yc) 、半径の長さが r の円を考えます。
円の中心 C から見た円周上の点 P の位置は、図のように中心角が θ であるとすると、
と、三角関数を使って表せます。
まあ、三角関数の定義そのもの、といった感じです。
また、円の中心 C の座標が
なので、原点から見た円周上の点 P の座標は、原点→C→Pと求められて、
となります。
式(1)で円を描く場合は、 θ を 0 ~ 2π まで変化させつつ(角度をラジアンで表す場合。度なら0°~360°)、線を引いていきます。
円にはもう一つの表し方があります。
中心がC(xc,yc) 、半径の長さが r の円の円周上の点Pの座標を(x,y) とすると、
という式が成り立ちます。
この式を変形すると、
となります。
式(2)で描く場合、 x の範囲は xc-r ~ xc+r となるので(この範囲を超えると、yは実数では求まらない、つまりそれは円の範囲外ということ)、 x を xc-r から xc+r まで変化させつつ、±の + の場合と - の場合それぞれに線を引いていけば、円を描くことができます。
前項の式を基に、円を描くプログラムを作っていきます。
まずは、原点を中心とした半径1の円(=単位円(unit circle))の円周上に、式(1)においてθ=0°,30°,60,°90°とした時の4つの点を打つことを考えます。
! ! (擬似)円を描く ! Copyright(c) カイン Cain 2006 ! SET WINDOW -1.5, 1.5, -1.5, 1.5 DRAW grid ! PLOT POINTS : COS( 0 ), SIN( 0 ) PLOT POINTS : COS( PI / 6 ), SIN( PI / 6 ) PLOT POINTS : COS( PI / 6 * 2 ), SIN( PI / 6 * 2 ) PLOT POINTS : COS( PI / 6 * 3 ), SIN( PI / 6 * 3 ) END
リスト1を(仮称)十進BASIC上で入力するかコピーするかして実行すると、次のような結果が得られます。
期待した通り、4つの点が打たれています。
このプログラムを、頭から見ていきます。
!
から行末まではコメント(comment)となり、プログラムの実行には影響を与えません。SET WINDOW -1.5, 1.5, -1.5, 1.5で、グラフィックスウィンドウを設定します。
DRAW gridで、グリッド(grid;目盛り線)を表示します。
PLOT POINTS : x座標, y座標で、指定された座標 (x座標, y座標) に点を打ちます。
PLOT POINTS
での座標の指定のように、プログラム中で数値を指定する場所では、数値を直接指定する代わりに、式(expression)で指定することができます。PLOT POINTS
なら点を打つ座標として使われます。PI
は、円周率πです。SIN
、COS
は、数学の三角関数sin、cosと同じです。
COS( 角度 )
のように、角度の指定を必ず括弧でくくります。+
: 足し算-
: 引き算*
: 掛け算/
: 割り算PLOT POINTS : COS( 0 ), SIN( 0 ) PLOT POINTS : COS( PI / 6 ), SIN( PI / 6 ) PLOT POINTS : COS( PI / 6 * 2 ), SIN( PI / 6 * 2 ) PLOT POINTS : COS( PI / 6 * 3 ), SIN( PI / 6 * 3 )として、つまりは
END
を付けます。
こうして、円周上の4つの点が打てました。
(仮称)十進BASIC上での「保存」は、一般的なWindowsアプリケーションとはちょっと違ったやり方になっています。
というわけで、「上書き保存」したい時は、メニューから [ファイル]-[上書き保存] とする必要があります。
円を描くにはもっと点を打たないといけないわけですが、打つ点の数だけ PLOT POINTS
を並べていては、点の数が増えたら大変になります。
そこで、繰り返し(=ループ(loop))を使って簡潔に記述します。
…
FOR i=0 TO 11
PLOT POINTS : COS( PI / 6 * i ), SIN( PI / 6 * i )
NEXT i
END
4つの PLOT POINTS
文を、 FOR
~ NEXT i
で置き換えます。
このプログラムを実行すると、次のように12個の点が打たれます。
i
と名付けられた変数が使われています。FOR
文FOR
文は、繰り返しのための構文で、
FOR 変数名=初めの値 TO 終わりの値 繰り返す処理 NEXT 変数名という形で使います。
i
=0で PLOT POINTS
される。i
=1で PLOT POINTS
される。i
=2で PLOT POINTS
される。i
=11で PLOT POINTS
される。PLOT POINTS
文の前に空白を入れることで、プログラムの構造を分かりやすくしています。こうして、ループでプログラムを簡潔にすることができました。
ループにしてしまえば、打つ点の数を倍にすることも簡単にできます。
繰り返し回数を柔軟に変更できるように、変数で指定することを考えます。
…
LET div = 36
!
FOR t=0 TO 2 * PI STEP 2 * PI / div
PLOT POINTS : COS( t ), SIN( t )
NEXT t
END
リスト2の FOR
~NEXT
を置き換えます。
実行すると、次のように36個の点が打たれます。
LET
文LET 変数名 = 式で、「変数」に「式」を計算して得られた「値」を代入(assign;割り当てる)します。
div
に36という数値を入れて、円周を36分割して描くことを意図しています。STEP
句FOR
文では、何も指定されなければ変数は1ずつ増えますが、
FOR 変数名=初めの値 TO 終わりの値 STEP 一度に増える値 繰り返す処理 NEXT 変数名と
STEP
句を付け加えれば、1度繰り返すごとに変数の値を「一度に増える値」ずつ増やすことができます。t
を 2π/36 ずつ増やしているので、次のようになります。t
は、図1の角度θに対応します。
t
=0=0°で、 ( COS(0°), SIN(0°) ) に点が打たれる。t
=2π/36=10°で、 ( COS(10°), SIN(10°) ) に点が打たれる。t
=2π/36×2=20°で、 ( COS(20°), SIN(20°) ) に点が打たれる。t
=2π/36×35=350°で、 ( COS(350°), SIN(350°) ) に点が打たれる。t
=2π/36×36=360°で、 ( COS(360°), SIN(360°) ) に点が打たれる(0°の点と重なる)。STEP
句でマイナスの値を指定すれば、変数の値を繰り返すごとに「減らす」こともできます。
コンピュータの主な機能は、データを「処理する」ことと「記憶する」ことなわけで、変数を使ってデータを記憶することでぐっとプログラムらしくなります。
ここまでは点を打ってきましたが、点を打つ代わりに直線を引いて、正多角形を描きます。
…
LET prev_x = 1
LET prev_y = 0
FOR t=0 TO 2 * PI STEP 2 * PI / div
PLOT LINES : prev_x, prev_y; COS( t ), SIN( t )
LET prev_x = COS( t )
LET prev_y = SIN( t )
NEXT t
END
リスト3の FOR
~NEXT
を置き換えます。
実行すると、次のように正36角形が描かれます。
見ての通り、パッと見、円に見えます。
これが(擬似)円の「擬似」の意味です。
つまり、正多角形の角数を多くすることで、擬似的に円を描いています。
見方によっては、「これでは円を描いたことにならない」とも言えます。例えば、計算に関する論文でも書こうとするなら、これでは不十分かもしれません。
しかし今回は、そんな厳密さはいりません。それっぽく見えれば十分です。
であればこの場合、簡単に実装できるこの方法が、最適な方法であると言えます。
大事なのは、「目的に応じた方法」です。
PLOT LINES : 始点のx座標, 始点のy座標; 終点のx座標, 終点のy座標とすることで、始点から終点へ直線を引けます。
prev_x
と prev_y
で、一つ前の点の座標を記憶して、線を引く時に>始点の座標として使っています(prevはpreviousの略)。
やっと円らしくなりました。
(擬似)円の中心の座標と半径を指定することを考えます。
また、 prev_x
、 prev_y
を使わない方法も示します。
…
SET WINDOW -1, 5, -4, 2
DRAW grid
!
LET div = 36
LET xc = 2
LET yc = -1
LET r = 2
!
FOR t=0 TO 2 * PI STEP 2 * PI / div
PLOT LINES : xc + r * COS( t ), yc + r * SIN( t );
NEXT t
END
リスト5では、リスト4からの変更箇所を中心に表示しています。
実行すると、次のように中心の座標が(2,-1)、半径2の円が描かれます。
xc
、y座標を yc
、半径を r
として指定し、式(1)に基づいて座標を求めています。SET WINDOW
も、xを-1~5、yを-4~2の範囲に書き換えています。
PLOT LINES : x座標, y座標;と末尾をセミコロン(
;
)で終わらせると、次に PLOT LINES
文が実行された時に、前の PLOT LINES
文で描かれた線に続けることができます。PLOT LINES : 0, 0; PLOT LINES : 1, 1とすれば、原点から(1,1)へ斜めの線が引かれます。
prev_x
と prev_y
は要らなかったのでは?という感じですが、例えばループの中で他の図形を描いたりする場合、prev_x
と prev_y
を使う方法が必要になります。これで、任意の円を描けるようになりました。
(擬似)円の描画プログラムをサブルーチン(subroutine)(routineは決まりきった手順等の意味)にすることを考えます。
サブルーチンとは、プログラムの一部を取り出して独立させたものです。
手続き(procedure)も同じような意味です。
サブルーチンの形にすることには、次のようなメリットがあります。
! ! (擬似)円を描く ! Copyright(c) カイン Cain 2006 ! DECLARE EXTERNAL PICTURE circle SET WINDOW -1, 5, -4, 2 DRAW grid ! DRAW circle( 2, -1, 2 ) END ! ! (擬似)円描画ルーチン ! EXTERNAL PICTURE circle( xc, yc, r ) LET div = 90 ! FOR t=0 TO 2 * PI STEP 2 * PI / div PLOT LINES : xc + r * COS( t ), yc + r * SIN( t ); NEXT t END PICTURE
div=90
になっているので、若干滑らかかもしれませんけどね。
DECLARE EXTERNAL PICTURE 絵の名前
と宣言し、 END
文より後に
EXTERNAL PICTURE 絵の名前( 引数1, 引数2, … ) プログラム END PICTUREとサブルーチン本体を定義したものです。
circle
は言語自体に組み込まれてるのかな?」と、余分な疑問を持つことがなくなります。circle
という絵が定義されていたりします。
DRAW 絵の名前( 引数1, 引数2, … )と、
DRAW
文を使って行います。DRAW circle( 2, -1, 2 )と呼び出すことで、
EXTERNAL PICTURE circle( xc, yc, r )と定義されたサブルーチン側では、
xc
に2、 yc
に-1、 r
に2が代入された上でプログラムが実行されます。DRAW circle(a,b,c)
などと引数として変数を渡すと、サブルーチン側ではその変数がそのまま使われます。つまり、サブルーチン側で引数を書き換えれば、呼び出し元の変数までも書き換えることになります。circle
ルーチン内の変数 div
は、メインプログラム( END
から前の部分)から使うことはできません。div
という名前の変数を使ったとしても、それは circle
ルーチン内の変数 div
とは別物になります。EXTERNAL
をつけたものとメインプログラム;メインプログラムも一つのサブルーチンと解釈)で使われる変数は、基本的にローカル変数になります。DECLARE EXTERNAL PICTURE routine1 … LET a = 0 ! routine1内のaとは別の変数 … DRAW routine1 … END EXTERNAL PICTURE routine1 … LET a = 20 ! メインプログラムのaとは別の変数 … END PICTURE
JIS Full BASICでは、絵定義を呼び出す時に、引数の代わりに変換関数を使うこともできるのですが、今回は変換関数については扱いません。
パラメータを変えようと思ったときに、一々プログラムを書き換えるというのは、スマートではありません。
その代わりに、ユーザーに入力させることを考えます。
… DECLARE EXTERNAL PICTURE circle ! INPUT xc, yc, r LET r = ABS( r ) ! 絶対値 ! SET WINDOW xc - r - 1, xc + r + 1, yc - r - 1, yc + r + 1 DRAW grid ! DRAW circle( xc, yc, r ) END …
実行すると、 xc
、 yc
、 r
の入力を求められます。
そこで、 xc
、 yc
、 r
をカンマ( ,
)で区切って入力すれば、グラフィックウィンドウの座標系も適切に設定されて、円が描かれます。
例えば、 3,1,5
と入力した結果は、次のようになります。
パラメータの入力を省略することはできません。正しく入力しないと(空欄だったりパラメータの数が合ってなかったり)、エラーが表示されて何度でも入力し直しになります。
INPUT
文INPUT 変数1,変数2,…とすることで、ユーザーに各変数の値を入力させることができます。
ABS(r)
は、 r
の絶対値(absolute value)を返す関数です。数学では |r| と書きます。r
にマイナスの値を入力すると、 SET WINDOW
が適切にできないため、半径 r
の絶対値を取っています。
これで、ユーザーとの対話ができるようになりました。
毎回入力しなければならないのではなく、入力を省略すればデフォルト値(default value;特に指定しない場合に使われる値)が使われるようにすることを考えます。
… LET xc = 0 LET yc = 0 LET r = 1 WHEN EXCEPTION IN INPUT xc, yc, r USE END WHEN …
リスト8では、パラメータを完全に入力しなくてもエラーは出ず、入力しなかった分にはデフォルト値 xc=0
、 yc=0
、 r=1
で円が描かれます。
WHEN EXCEPTION IN プログラム USE 例外が発生したときの処理 END WHEN
WHEN EXCEPTION IN
以下のプログラム中で例外が発生したら、 USE
以降の「例外が発生したときの処理」が為されます。USE
~ END WHEN
は無視されて、その次へと進んでいきます。
INPUT
文の例外LET xc = 0 LET yc = 0 LET r = 1 WHEN EXCEPTION IN INPUT xc, yc, r USE END WHEN
INPUT
文で、入力が完全にはされなかった場合、リスト7のように特に何もしなければ、完全な入力がされるまで先へ進みません。WHEN EXCEPTION IN
で例外を受け止めれば、たとえ入力が不完全でも、そのまま先へ進みます(今回は、 USE
~ END WHEN
中で特に何もしていません)。xc=0
、 yc=0
、 r=1
のまま処理が進みます。5,6
なら、 xc=5
、 yc=6
という代入はされますが、 r
は 1
のままで処理が進みます。
以上のように、入力が不完全ならデフォルト値を使う、という処理が実現できました。
ついでなので、円弧も描いてみます。
! ! (擬似)円弧を描く ! Copyright(c) カイン Cain 2006 ! DECLARE EXTERNAL PICTURE arc ! LET xc = 0 LET yc = 0 LET r = 1 LET t1 = 0 LET t2 = 360 WHEN EXCEPTION IN INPUT xc, yc, r USE END WHEN LET r = ABS( r ) ! 絶対値 WHEN EXCEPTION IN INPUT t1, t2 USE END WHEN LET t1 = t1 * PI / 180 ! 度→ラジアン LET t2 = t2 * PI / 180 ! SET WINDOW xc - r - 1, xc + r + 1, yc - r - 1, yc + r + 1 DRAW grid ! DRAW arc( xc, yc, r, t1, t2 ) END ! ! (擬似)円弧描画ルーチン ! EXTERNAL PICTURE arc( xc, yc, r , t1, t2 ) LET div = 360 ! FOR t=t1 TO t2 STEP 2 * PI / div PLOT LINES : xc + r * COS( t ), yc + r * SIN( t ); NEXT t END PICTURE
リスト9を実行すると、xc,yc,r
の入力に続いて、 t1,t2
の入力も求めら得ます。
ここでは、 t1
に円弧の開始角、 t2
に円弧の終了角を 単位度で入力します。
角度の指定の仕方は、図1のθに準じます。
例えば、 xc,yc,r
はデフォルト値のまま、 t1,t2
を 45,180
と入力すると、次のような円弧が描かれます。
LET t1=t1*PI/180
で、単位を度からラジアンに変換t1
へ代入する値を計算するのに、 t1
自体の持っている値を使うことができます。t1*PI/180
を計算する。t1
に代入する。今回はここまで。
命令文 !コメント
END
で終了。END
までがメインプログラム。
SET WINDOW 左端のx座標, 右端のx座標, 下端のy座標, 上端のy座標
DRAW grid
PLOT POINTS : x座標, y座標
PLOT LINES : 始点のx座標, 始点のy座標; 終点のx座標, 終点のy座標末尾にセミコロンをつけることで、次の
PLOT LINES
に繋げることも可。
+
: 加算-
: 減算*
: 乗算/
: 除算LET 変数名 = 式
FOR 変数名=初めの値 TO 終わりの値 STEP 一度に増える値 繰り返す処理 NEXT 変数名
DECLARE EXTERNAL PICTURE 絵の名前
定義:
EXTERNAL PICTURE 絵の名前( 引数1, 引数2, … ) プログラム END PICTURE呼び出し:
DRAW 絵の名前( 引数1, 引数2, … )
INPUT 変数1,変数2,…
LET 変数1 = デフォルト値 LET 変数2 = デフォルト値 … WHEN EXCEPTION IN INPUT 変数1,変数2,… USE END WHEN