VC++を使ってリサージュ図形(Lissajous' figure)を描く
2021/02/28


ペンデュラムウェーブ(Pendulum Waves)の図形を作っていて、「動く図形を作るのは、けっこう面白い」と思うようになった。
そして、もっと他に面白い画像を描いてみたいと思うようになったのである。

そこで思い出すのが、昔のSFテレビ番組の「タイムトンネル」なのである。

test_pgm105 test_pgm106
その「タイムトンネル」の中でよく出てきた制御盤やらコンピュータの画像の中に、ブラウン管に映し出された「リサージュ波形(Lissajous' figure)」が印象的だったのを思い出したのである。

放送は、私の記憶では1967年で、もう54年も前になる。
当時は、私がまだ中学1年生の夏頃で、落ちこぼれはじめていた私にとっては、何のことやらサッパリ解らないながら、「美しくてカッコ良い!!」という気持ちで見ていたのだった。




まず、あの図形はどういうものなのか?

test_pgm107
単純なサインカーブなら、何ということもなく描けるのであるが、このX座標自体を三角関数で変化させることになる。
つまり、この画面を丸めて横から見た形ということになる。


test_pgm108
上記のサインカーブを、単純に丸めて横から見ると、ちょうど円になる。

X軸が増加〜減少する間に、Y軸も増加〜減少するためである。
※プログラミングでのY軸は下向きに+なので、逆の目盛りになる。

test_pgm109
サインカーブが2つになると、なかなか印象的なカーブが現れる。


test_pgm110 test_pgm111
サインカーブが3つとか、6つとかになると、さらに綺麗なカーブが現れる。




プログラムとしては、この前のペンデュラムウェーブ(Pendulum Waves)のプログラムに追加して作成する。

まず、前回作ったペンデュラムウェーブを表示させる動作と、今回作成するリサージュ図形を表示させる動作の切り替えを行うためのダイアログボックスを作る。

test_pgm112
ダイアログボックスの中は、ラジオボタンでペンデュラムウェーブとリサージュ図形の切り替えと、振り子の数と同期する時間の設定、サインカーブの数を設定可能にしておく。



次に、動作の情報を変数で定義する。
前回作ったpendulum01というプロジェクトのpendulum01View.hに変数を追加して以下のようにする。
//リサージュ図形から下が主な追加点と、exec_....となっているフラグ類が追加となる。

public:
	virtual void data_init();

	int exec_status_timer = 0;
	int exec_status1 = 0;
	int exec_status2;

	int test_count;
	double PI2 = 6.283185307179586;
	
	int dummy;
	int step_count = 20;		// 振り子の数
	double step_Synchro = 60.0;		// 同期する秒数
	int step_val = 25;			// フレーム数、n回で一周
	double step_rad[20];		// ミリ秒毎の角度変化
	double step_rad_all[20];	// 現在の角度

	CString str01;
	SYSTEMTIME systime_s;  // 処理の開始時刻
	SYSTEMTIME systime_e;  // 処理の終了時刻

//	リサージュ図形
	int num_curve = 1;
	double rad1;
	double rad2;
	double sin_curve1;
	double sin_curve2;
	CPoint multi_p1[128];

初期設定を行う処理を独立させ、pendulum01View.cppのコンストラクターは、その処理を呼び出すだけにする。

Cpendulum01View::Cpendulum01View()
{
	// TODO: 構築コードをここに追加します。
	data_init();
}

実際の初期設定は、コンストラクターと、設定のやり直しの両方から呼び出されることになる。

void Cpendulum01View::data_init()
{
	int ix;
	double step_def;
	double y;

	exec_status2 = 0;

	step_def = (PI2 / step_Synchro) / step_val;
	//step_def = step_def / step_val;
	for (ix = 0; ix < step_count; ix++) {
		step_rad[ix] = (PI2 / step_val) + (ix * step_def);
		step_rad_all[ix] = 0.0;
	}

	// リサージュ図形
	sin_curve1 = 0.0;
	sin_curve2 = 0.0;

	rad1 = (PI2 * num_curve) / 128.0;
	rad2 = PI2 / 128.0;

	for (ix = 0; ix < 128; ix++) {

		multi_p1[ix].x = -cos(sin_curve2) * 100.0 + 150;
		sin_curve2 = sin_curve2 + rad2;
//		multi_p1[ix].x = ix * 4 + 50;
		y = -sin(sin_curve1) * 100.0 + 150.0;
		multi_p1[ix].y = y;

		sin_curve1 = sin_curve1 + rad1;
	}
	multi_p1[127].x = multi_p1[0].x;
	multi_p1[127].y = multi_p1[0].y;

}

次に表示のハンドラーにリサージュ図形の表示部分を追加作成する。
exec_status1が0のときはペンディウムウエーブの表示、それ以外はリサージュ図形となる。

void Cpendulum01View::OnDraw(CDC* pDC)
{
	Cpendulum01Doc* pDoc = GetDocument();
	ASSERT_VALID(pDoc);
	if (!pDoc)		return;

	int nx;
	double x;
	double y;
	double r;
	int ix;
	int iy;
	int iy_old;

	// TODO: この場所にネイティブ データ用の描画コードを追加します。

	if (exec_status1 == 0) {

		pDC->SelectStockObject(BLACK_BRUSH);
		//pDC->Ellipse(300, 200, 310, 210);

		iy = 50;
		for (nx = 0; nx < step_count; nx++) {

			x = sin(step_rad_all[nx])*90.0 + 120.0;
			ix = x;
			pDC->Ellipse(ix, iy, ix + 10, iy + 10);
			iy = iy + 10;

		}

		r = 90.0;
		for (nx = 0; nx < step_count; nx++) {

			x = sin(step_rad_all[nx])*(r - nx * 3) + 120.0;
			y = -cos(step_rad_all[nx])*(r - nx * 3) + 400.0;
			ix = x;
			iy = y;
			pDC->Ellipse(ix, iy, ix + 10, iy + 10);

		}

	}
	else {		// if (exec_status1 == 0)

		CPen pen;

		pen.CreatePen(PS_SOLID, 2, RGB(0, 0, 0));
		CPen* pOldPen = pDC->SelectObject(&pen);

		pDC->Polyline(multi_p1, 128);

		pDC->SelectObject(pOldPen);
	}


}

設定のコマンドハンドラー
設定を呼び出した場合は、タイマーが動作中ならタイマーを停止させ、初期設定用のダイアログボックスを表示してデータを取り込む。
返ってきたら、ダイアログボックスで入力した値で再度初期処理を実行させる。

これを見ていたら、OKボタンのチェック処理が入っていないので、CANCELボタンが効かないのに気が付いた。
他にもだいぶバグがありそうであるが、まぁ、少しずつ直せば良いというスタンスで行くことにする。

void Cpendulum01View::On32772()
{
	// TODO: ここにコマンド ハンドラー コードを追加します。
	// 設定

	CInitdialog dlg;
	int retcd;

	if (exec_status_timer != 0) On32774();

	dlg.m_radio1 = exec_status1;
	dlg.m_pend_count = step_count;
	dlg.m_time_val = step_Synchro;
	dlg.m_num_curve = num_curve;

	retcd = dlg.DoModal();

	step_count = dlg.m_pend_count;
	step_Synchro = dlg.m_time_val;
	exec_status1 = dlg.m_radio1;
	num_curve = dlg.m_num_curve;

	data_init();

	InvalidateRect(NULL, TRUE);
}

ステップ実行のコマンドハンドラー
これも、前回作ったものにリサージュ図形の部分を追加したものである。

処理の最後の方で、sin_curve1に0.1を加算しているが、これが図形全体の回転速度である。
また、ずっと角度の加算の処理だけが続くので、リサージュ図形の処理の先頭で、一周以上したら2π分減算する処理を入れてある。

ちょびっとダサイ設計になっているな〜と思うのであるが、私の能力相応だとこうなるのである。

void Cpendulum01View::On32771()
{
	// TODO: ここにコマンド ハンドラー コードを追加します。
	// ステップ実行

	int ix;
	double y;

	for (ix = 0; ix < step_count; ix++) {
		step_rad_all[ix] = step_rad_all[ix] + step_rad[ix];
	}

	// リサージュ図形
	ix = int(sin_curve1 / PI2);
	sin_curve1 = sin_curve1 - (PI2 * ix);
	sin_curve2 = 0.0;

	rad1 = (PI2 * num_curve) / 128.0;
	rad2 = PI2 / 128.0;

	for (ix = 0; ix < 128; ix++) {

		multi_p1[ix].x = -cos(sin_curve2) * 100.0 + 150;
		sin_curve2 = sin_curve2 + rad2;
		//		multi_p1[ix].x = ix * 4 + 50;
		y = -sin(sin_curve1) * 100.0 + 150.0;
		multi_p1[ix].y = y;

		sin_curve1 = sin_curve1 + rad1;
	}
	multi_p1[127].x = multi_p1[0].x;
	multi_p1[127].y = multi_p1[0].y;

	sin_curve1 = sin_curve1 + 0.1;

//	InvalidateRect(NULL, FALSE);
	InvalidateRect(NULL, TRUE);
}

タイマーの開始と終了のプログラムは、前回作ったものに以下のフラグを追加しただけである。

タイマーの開始には「exec_status_timer = 1;」を追加し、タイマーの停止には「exec_status_timer = 0;」を追加した。



今回作った動画は、以下の場所にアップしてあるのである。
https://youtu.be/ASg1JY3G4hc


HOME