【Python/tkinter】惑星の周りを衛星に回転させてキャンバスの”奥行き”の重要性を理解する

惑星の周りを衛星に回転させる方法の解説ページアイキャッチ

今回は下のアニメのように、GUI アプリ上で惑星の周りを衛星に回転させる方法について解説していきたいと思います。

惑星の周りを衛星が回転する様子

ポイントは衛星が惑星の周りを回転する部分と思うかもしれませんが(これも重要ですが)、最大のポイントはキャンバスに描画する図形の「奥行き」情報です。

この奥行きを考慮した図形描画を行うことで、アプリの見た目の自由度が格段にアップします!

是非今回紹介する「衛星が惑星の周りを回転する」アプリの作り方から、この奥行きの重要性について理解していただければと思います!

奥行きを考慮せずに図形を描画する

まずは奥行きを考慮せずに、惑星の周りを衛星が回転するアプリを作成していきたいと思います。

スクリプト

奥行きを考慮せずに惑星の周りを衛星が回転するアプリのスクリプトは下記のようになります。

奥行き考慮なしの描画
import tkinter
import math

# キャンバスのサイズ設定
CANVAS_WIDTH = 600
CANVAS_HEIGHT = 300

# 中央座標
CENTER_X = CANVAS_WIDTH // 2
CENTER_Y = CANVAS_HEIGHT // 2

# 軌道の楕円の横方向・縦方向のサイズ
ORBIT_A = 200  # 横方向
ORBIT_B = 50  # 縦方向

# 惑星と衛星の半径
PLANET_R = 100
SATELITE_R = 20

# 衛星を再配置する時間の間隔
TIME_INTERVAL = 10

# 衛星を再配置する角度の感覚
RAD_INTERVAL = math.pi / 100

# 回転中かどうかのフラグ
rotate_flag = False

# afterのID(キャンセル実行よう)
after_id = None

# 衛星の回転角度
rad = 0

# キャンバス
canvas = None


def rotate():
	'''衛星を回転させる関数'''

	global canvas
	global after_id
	global rad

	# 衛星の中心位置を計算
	x = CENTER_X + ORBIT_A * math.cos(rad)
	y = CENTER_Y + ORBIT_B * math.sin(rad)

	# 求めた位置を中心に衛星再配置
	canvas.coords(
		"satelite",
		x - SATELITE_R, y - SATELITE_R,
		x + SATELITE_R, y + SATELITE_R
	)

	# 衛星を描画する角度を増加
	rad += RAD_INTERVAL
	if rad > math.pi:
		rad -= 2 * math.pi

	# TIME_INTERVAL[ms]後に再度rotate実行
	after_id = canvas.after(TIME_INTERVAL, rotate)


def rotate_switch():
	'''衛星回転をON/OFF切り替える関数'''

	global canvas
	global rotate_flag
	global after_id

	if rotate_flag:
		# 回転動作中に実行された場合

		# rotateの定期実行を終了する
		rotate_flag = False
		canvas.after_cancel(after_id)
	else:
		# 回転動作していない時に実行された場合

		# rotateの定期実行を終了する
		rotate_flag = True

		# 回転実行
		rotate()


def main():
	'''main関数'''

	global canvas

	# メインウィンドウ作成
	app = tkinter.Tk()

	# キャンバス作成
	canvas = tkinter.Canvas(
		app,
		width=CANVAS_WIDTH,
		height=CANVAS_HEIGHT,
		bg="white",
		highlightthickness=0
	)
	canvas.pack()

	# ボタン作成
	button = tkinter.Button(
		app,
		text="ボタン",
		command=rotate_switch  # クリック時に実行する関数
	)
	button.pack()

	# 惑星を表す楕円描画
	canvas.create_oval(
		CENTER_X - PLANET_R, CENTER_Y - PLANET_R,
		CENTER_X + PLANET_R, CENTER_Y + PLANET_R,
		tag="planet",
		fill="orange",
		outline="orange"
	)

	# 衛星の軌道を表す楕円の描画
	canvas.create_oval(
		CENTER_X - ORBIT_A, CENTER_Y - ORBIT_B,
		CENTER_X + ORBIT_A, CENTER_Y + ORBIT_B,
		outline="lightgray",
		width=10
	)

	# 衛星の中心位置を計算
	x = CENTER_X + ORBIT_A * math.cos(rad)
	y = CENTER_Y + ORBIT_B * math.sin(rad)

	# 衛星を表す楕円の描画
	canvas.create_oval(
		x - SATELITE_R, y - SATELITE_R,
		x + SATELITE_R, y + SATELITE_R,
		tag="satelite",
		fill="blue",
		width=0
	)

	# メインループ
	app.mainloop()


if __name__ == '__main__':
	# main関数実行
	main()

スポンサーリンク

スポンサーリンク

スクリプトの解説

簡単にスクリプトの解説をしておきます。

ウィジェットの作成

スクリプトの main 関数では、まず下の図のように「メインウィンドウ」と「キャンバスウィジェット」と「ボタンウィジェット」を作成しています。

衛星を回転させるアプリのウィジェット

惑星と軌道と衛星の描画

そして、キャンバスウィジェットの中に、惑星を表す楕円と、衛星の軌道を表す楕円と、衛星を表す楕円を順々に描画しています。

惑星と軌道と衛星の描画

衛星の回転

また、ボタンウィジェットがクリックされた時に、rotate_switch 関数が実行されるようにしており、rotate_switch 関数の中で衛星の回転の ON / OFF を切り替えるようにしています。

回転 ON になった場合は、定期的に rotate 関数が実行されるようにしており、rotate 関数の中で衛星の回転を行っています。

定期的に rotate 関数が実行できるように after メソッドを活用しています。

この after メソッドについては下記で詳しく解説していますので、after メソッドをご存知無い方は是非読んでみてください。tkinter を利用する上で必須級に重要なメソッドなので是非使いこなせるようになっていただきたいです。

Tkinterの使い方:after で処理を「遅らせて」or 処理を「定期的」に実行する

衛星の軌道は下の図のような座標・サイズの楕円として描画しているので、

起動を表す楕円

楕円上の衛星の中心座標 (x, y)は下記により求めることができます。

衛星の中心座標
# 衛星の中心位置を計算
x = CENTER_X + ORBIT_A * math.cos(rad)
y = CENTER_Y + ORBIT_B * math.sin(rad)

なので、定期的に実行される rotate 関数の中で、「上記で求まる座標に衛星を表す楕円を移動する」と「回転角度を表す rad の増加」を行うことで、定期的に衛星が軌道上を回転する動きを実現することができます。

楕円を移動するためには Canvas クラスの coords メソッドを利用します。

この coords などのように描画した図形を後から操作するメソッドについては下記ページで解説していますので、詳しく知りたい方はこちらをご覧ください。

tkinterキャンバスの図形の操作方法解説ページのアイキャッチTkinterの使い方:Canvasクラスで描画した図形を操作する

スクリプトの実行結果

で、このスクリプトを実行すると下の図のような画面が表示されます。

奥行きを考慮せずに描画したときのアプリの画面

まずこの時点で見た目が変ですよね…。惑星の裏側にある軌道は惑星で隠れて非表示になって欲しいところですが、普通に表示されてしまっています。

ボタンをクリックすると衛星が回転し始めます(もう一度押すと回転が止まる)。

奥行きを考慮せずに描画したときの衛星の動き

衛星も惑星の裏側にある時も惑星で隠れずに表示されてしまっています。

この理由はキャンバスの「奥行き」情報にあります。

次はこのキャンバスの「奥行き」について解説していきたいと思います。

キャンバスの奥行きについて

キャンバスでは、描画された1つ1つの図形が奥行き情報によって管理されています。

奥行き情報とは、描画された図形が重なった時に、どの図形が前面に表示され、どの図形が背面に表示されるかを決定する情報です。

キャンバスの奥行き情報

そして、基本的にキャンバスに描画された図形は、後から描画された図形ほど前面に現れて表示されるようになります。

先ほどのスクリプトでは「惑星 → 軌道 → 衛星」の順で図形を描画したため、惑星が最も背面に描画されるようになります。

惑星と軌道と衛星の奥行き情報

なので、軌道も衛星も惑星の前面に描画され、惑星でこれらが隠れずにいつでも表示される結果になっていたというわけです。

じゃあ「軌道 → 衛星 → 惑星」の順で描画すれば良いのかというと、これだと惑星が際前面に描画されるので、惑星と軌道や衛星が重なった時には、常に軌道や衛星が隠れてしまうことになります。

惑星と軌道と衛星の奥行き情報2

下のアニメのような動きにするためには、もうちょっと工夫が必要です。

惑星の周りを衛星が回転する様子

ここからはこのアニメのように惑星の裏側にある時だけ衛星と軌道が隠れるようにするスクリプトを紹介していきたいと思います。

スポンサーリンク

キャンバスの奥行きを考慮して改良する1

それではスクリプトを改良していきたいと思います。

スポンサーリンク

考え方

まずは図形の描画順を工夫することで、惑星の裏側にある軌道と衛星が惑星で隠れるようにしていきたいと思います。

この方法は単純です。

要は惑星を上半分と下半分に真っ二つに分解し、下記の順序で図形の描画を行います。

  1. 惑星の下半分
  2. 軌道
  3. 衛星
  4. 惑星の上半分

これにより、惑星の下半分は最背面に描画されるので、軌道や衛星の方が前面に表示されます。

一方惑星の上半分は最前面に描画されるので、軌道や衛星の方が背面に表示され、図形が重なった時は惑星の方が表示されることになります。

惑星と軌道と衛星の奥行き情報3

スクリプト

惑星を上半分と下半分に分割して描画するスクリプトは下記のようになります。

惑星を分割して描画する
import tkinter
import math

# キャンバスのサイズ設定
CANVAS_WIDTH = 600
CANVAS_HEIGHT = 300

# 中央座標
CENTER_X = CANVAS_WIDTH // 2
CENTER_Y = CANVAS_HEIGHT // 2

# 軌道の楕円の横方向・縦方向のサイズ
ORBIT_A = 200  # 横方向
ORBIT_B = 50  # 縦方向

# 惑星と衛星の半径
PLANET_R = 100
SATELITE_R = 20

# 衛星を再配置する時間の間隔
TIME_INTERVAL = 10

# 衛星を再配置する角度の感覚
RAD_INTERVAL = math.pi / 100

# 回転中かどうかのフラグ
rotate_flag = False

# afterのID(キャンセル実行よう)
after_id = None

# 衛星の回転角度
rad = 0

# キャンバス
canvas = None


def rotate():
	'''衛星を回転させる関数'''

	global canvas
	global after_id
	global rad

	# 衛星の中心位置を計算
	x = CENTER_X + ORBIT_A * math.cos(rad)
	y = CENTER_Y + ORBIT_B * math.sin(rad)

	# 求めた位置を中心に衛星再配置
	canvas.coords(
		"satelite",
		x - SATELITE_R, y - SATELITE_R,
		x + SATELITE_R, y + SATELITE_R
	)

	# 衛星を描画する角度を増加
	rad += RAD_INTERVAL
	if rad > math.pi:
		rad -= 2 * math.pi

	# TIME_INTERVAL[ms]後に再度rotate実行
	after_id = canvas.after(TIME_INTERVAL, rotate)


def rotate_switch():
	'''衛星回転をON/OFF切り替える関数'''

	global canvas
	global rotate_flag
	global after_id

	if rotate_flag:
		# 回転動作中に実行された場合

		# rotateの定期実行を終了する
		rotate_flag = False
		canvas.after_cancel(after_id)
	else:
		# 回転動作していない時に実行された場合

		# rotateの定期実行を終了する
		rotate_flag = True

		# 回転実行
		rotate()


def main():
	'''main関数'''

	global canvas

	# メインウィンドウ作成
	app = tkinter.Tk()

	# キャンバス作成
	canvas = tkinter.Canvas(
		app,
		width=CANVAS_WIDTH,
		height=CANVAS_HEIGHT,
		bg="white",
		highlightthickness=0
	)
	canvas.pack()

	# ボタン作成
	button = tkinter.Button(
		app,
		text="ボタン",
		command=rotate_switch  # クリック時に実行する関数
	)
	button.pack()

	# 惑星の下半分を表す円弧の描画
	canvas.create_arc(
		CENTER_X - PLANET_R, CENTER_Y - PLANET_R,
		CENTER_X + PLANET_R, CENTER_Y + PLANET_R,
		tag="planet_lower",
		start=180,
		extent=180,
		fill="orange",
		outline="orange"
	)

	# 衛星の軌道を表す楕円の描画
	canvas.create_oval(
		CENTER_X - ORBIT_A, CENTER_Y - ORBIT_B,
		CENTER_X + ORBIT_A, CENTER_Y + ORBIT_B,
		outline="lightgray",
		width=10
	)

	# 衛星の中心位置を計算
	x = CENTER_X + ORBIT_A * math.cos(rad)
	y = CENTER_Y + ORBIT_B * math.sin(rad)

	# 衛星を表す楕円の描画
	canvas.create_oval(
		x - SATELITE_R, y - SATELITE_R,
		x + SATELITE_R, y + SATELITE_R,
		tag="satelite",
		fill="blue",
		width=0
	)

	# 惑星の下半分を表す円弧の描画
	canvas.create_arc(
		CENTER_X - PLANET_R, CENTER_Y - PLANET_R,
		CENTER_X + PLANET_R, CENTER_Y + PLANET_R,
		tag="planet_upper",
		start=0,
		extent=180,
		fill="orange",
		outline="orange"
	)

	# メインループ
	app.mainloop()

if __name__ == '__main__':
	# main関数実行
	main()

スポンサーリンク

スポンサーリンク

スクリプトの解説

最初にお見せしたスクリプトと比べて main 関数では惑星を楕円ではなく、円弧を2つ描画するように変更しています。

この円弧は create_arc メソッドを実行することで描画しています。

create_arc メソッドについては下記ページで解説していますので、詳しく知りたい方は是非こちらもご覧ください。

tkinterキャンバスに図形を描画する方法解説ページのアイキャッチTkinterの使い方:Canvasクラスで図形を描画する

create_arc メソッドに指定するオプションの中でポイントになるのが startextent です。

  • start:円弧を描画開始する角度
  • extent:円弧を描画する角度

図でこの startextent の関係を表すと下の図のようになります。

startとextentの関係

1つ目の円弧を描画する際に実行する create_arc メソッドには start=180extent=180 を指定していますので、惑星(円)の下半分が描画されることになります。

図形の描画としは最初に実行しているので、最背面に表示されることになります。したがって、この後に描画する軌道や衛星は惑星の下半分よりも常に前面に表示されることになります。

2つ目の円弧を描画する際に実行する create_arc メソッドには start=0extent=180 を指定していますので、惑星(円)の上半分が描画されることになります。

図形の描画としは最後に実行しているので、最前面に表示されることになります。したがって、この前に描画される軌道や衛星は惑星の上半分よりも常に背面に表示される(隠れる)ことになります。

スポンサーリンク

スクリプト実行結果

スクリプトを実行してボタンを押すと、下のアニメのように衛星が惑星の周りを回転します。

惑星の周りを衛星が回転する様子

衛星が惑星の上半分と重なる際には、背面に存在する衛星が前面に存在する惑星によって隠れるので、衛星が惑星の後ろ側を回転していることがわかりやすくなったと思います。

キャンバスの奥行きを考慮して改良する2

今度はちょっと違った方法で惑星の周りを衛星が回転しているように見えるようにスクリプトを改良していきたいと思います。

スポンサーリンク

考え方

前述の通り、図形は基本的に後から描画したものが前面に表示されることになります。

ただし、この奥行き情報は後から変更することが可能です。

下記はこの奥行き情報を変更する Canvas クラスのメソッドになります。

  • lower(tagOrId, belowThis)tagOrId で指定した図形を belowThis のすぐ背面に移動する
  • lift(tagOrId, aboveThis)tagOrId で指定した図形を aboveThis のすぐ前面に移動する

衛星を惑星の背面に移動させたい時は lower メソッドを、衛星を惑星の前面に移動させたい時は lift メソッドをそれぞれ実行することで、「衛星を惑星で隠す or 衛星を惑星よりも前面に表示する」を制御することができます。

lower メソッドと lift メソッド等の「描画した図形を操作する方法」については下記ページで詳しく解説していますので、よろしければこちらもご覧ください。

tkinterキャンバスの図形の操作方法解説ページのアイキャッチTkinterの使い方:Canvasクラスで描画した図形を操作する

スクリプト

lower メソッドと lift メソッドを利用したスクリプトは下記のようになります。

lowerとliftで図形を移動する
import tkinter
import math

# キャンバスのサイズ設定
CANVAS_WIDTH = 600
CANVAS_HEIGHT = 300

# 中央座標
CENTER_X = CANVAS_WIDTH // 2
CENTER_Y = CANVAS_HEIGHT // 2

# 軌道の楕円の横方向・縦方向のサイズ
ORBIT_A = 200  # 横方向
ORBIT_B = 50  # 縦方向

# 惑星と衛星の半径
PLANET_R = 100
SATELITE_R = 20

# 衛星を再配置する時間の間隔
TIME_INTERVAL = 10

# 衛星を再配置する角度の感覚
RAD_INTERVAL = math.pi / 100

# 回転中かどうかのフラグ
rotate_flag = False

# afterのID(キャンセル実行よう)
after_id = None

# 衛星の回転角度
rad = 0

# キャンバス
canvas = None


def rotate():
	'''衛星を回転させる関数'''

	global canvas
	global after_id
	global rad

	# 衛星の中心位置を計算
	x = CENTER_X + ORBIT_A * math.cos(rad)
	y = CENTER_Y + ORBIT_B * math.sin(rad)

	# 求めた位置を中心に衛星再配置
	canvas.coords(
		"satelite",
		x - SATELITE_R, y - SATELITE_R,
		x + SATELITE_R, y + SATELITE_R
	)

	if y > CENTER_Y:
		# 衛星が惑星の中心座標よりも下側にある場合

		# 衛星を惑星よりも前面に移動
		canvas.lift("satelite", "planet")
	else:
		# 衛星が惑星の中心座標よりも上側にある場合

		# 衛星を惑星よりも背面に移動
		canvas.lower("satelite", "planet")

	# 衛星を描画する角度を増加
	rad += RAD_INTERVAL
	if rad > math.pi:
		rad -= 2 * math.pi

	# TIME_INTERVAL[ms]後に再度rotate実行
	after_id = canvas.after(TIME_INTERVAL, rotate)


def rotate_switch():
	'''衛星回転をON/OFF切り替える関数'''

	global canvas
	global rotate_flag
	global after_id

	if rotate_flag:
		# 回転動作中に実行された場合

		# rotateの定期実行を終了する
		rotate_flag = False
		canvas.after_cancel(after_id)
	else:
		# 回転動作していない時に実行された場合

		# rotateの定期実行を終了する
		rotate_flag = True

		# 回転実行
		rotate()


def main():
	'''main関数'''

	global canvas

	# メインウィンドウ作成
	app = tkinter.Tk()

	# キャンバス作成
	canvas = tkinter.Canvas(
		app,
		width=CANVAS_WIDTH,
		height=CANVAS_HEIGHT,
		bg="white",
		highlightthickness=0
	)
	canvas.pack()

	# ボタン作成
	button = tkinter.Button(
		app,
		text="ボタン",
		command=rotate_switch  # クリック時に実行する関数
	)
	button.pack()

	# 惑星を表す楕円描画
	canvas.create_oval(
		CENTER_X - PLANET_R, CENTER_Y - PLANET_R,
		CENTER_X + PLANET_R, CENTER_Y + PLANET_R,
		tag="planet",
		fill="orange",
		outline="orange"
	)

	# 衛星の中心位置を計算
	x = CENTER_X + ORBIT_A * math.cos(rad)
	y = CENTER_Y + ORBIT_B * math.sin(rad)

	# 衛星を表す楕円の描画
	canvas.create_oval(
		x - SATELITE_R, y - SATELITE_R,
		x + SATELITE_R, y + SATELITE_R,
		tag="satelite",
		fill="blue",
		width=0
	)

	# メインループ
	app.mainloop()


if __name__ == '__main__':
	# main関数実行
	main()

スポンサーリンク

スポンサーリンク

スクリプトの解説

今回は lower メソッドと lift メソッドの解説に焦点を当てたいと思ったので、軌道は省略させていただいています。

このスクリプトのポイントは rotate 関数で lower メソッドと lift メソッドを実行している下記部分です。

lowerとliftの実行
if y > CENTER_Y:
	# 衛星が惑星の中心座標よりも下側にある場合

	# 衛星を惑星よりも前面に移動
	canvas.lift("satelite", "planet")
else:
	# 衛星が惑星の中心座標よりも上側にある場合

	# 衛星を惑星よりも背面に移動
	canvas.lower("satelite", "planet")

y は回転中の衛星の縦方向の中心座標、CENTER_Y は惑星の縦方向の中心座標になります。

また lift と lower の引数で指定している "satelite""planet" は、それぞれ衛星と惑星を表す楕円を描画する時に tag オプションで指定したタグ名です。

衛星を惑星の前面に表示したいのは「衛星の中心座標が軌道の中心座標よりも下側にいる」時なので、y > CENTER_Y を満たすかどうかを判断し、満たす場合は lift を実行して衛星を惑星よりも前面に表示するようにしています

さらに、y > CENTER_Y を満たさない場合は、lower を実行して衛星を惑星よりも背面に表示するようにしています。この場合は衛星は惑星よりも背面に表示されるので、惑星と重なった時に隠れます。

MEMO

キャンバスの座標では、左上が原点かつ縦の正方向が下なので、y > CENTER_Y を満たす yCENTER_Y よりも下側に存在することになります

スポンサーリンク

スクリプト実行結果

スクリプトを実行してボタンを押すと、下のアニメのように衛星が惑星の周りを回転します。

今回は軌道の描画は省略したので、軌道は表示されません。

惑星の周りを衛星が回転する様子

lift と lower メソッドの実行により、衛星と惑星の前面・背面関係が交互に入れ替わっていることが確認できると思います。

まとめ

今回は、特に「キャンバスの奥行き」について理解していただくために「惑星の周りを衛星に回転させる」アプリの作り方について解説しました。

具体的には、下記の2つの方法を用いて奥行きを考慮した図形の表示を実現しました。

  • 図形を背面・前面に表示したい部分に分割して描画する
  • 図形を lift と lower メソッドで背面 or 前面に移動する

図形は先に描画した図形から順に背面に表示されるようになるので、基本的にはこれを利用して図形の描画順序を工夫するのが良いと思います。

ただし、前面・背面に表示したい部分を細かく制御したいような場合は「図形の分割」、ユーザーの操作によって図形を前面・背面入れ替えたいような場合は「lift と lower メソッド」が有効です。

これらのテクニック・メソッドを知っておくことで作成できるアプリ・実現できるアプリの見た目の自由度も上がりますので、是非この機会に覚えておいてください!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です