ご注意
数学的な厳密性やManimの最適な利用方法に関して、改善の余地が含まれている可能性がございます。
完成したアニメーション
この記事のコードを実行すると、以下のような三角関数の恒等式 $\cos\left(\frac{\pi}{2} + \theta\right) = -\sin(\theta)$ を動的に可視化するアニメーションが生成されます。
このアニメーションでは、主に以下の2つの点が連動して動くことで、数式と単位円、グラフの関係性を表現しています。
- 単位円上の点 : 角度 $\theta$ の変化に応じて、単位円周上を動きます。この点の座標は $(\cos \theta, \sin \theta)$ で表されます。
- グラフ上の点 : 横軸を角度 $\theta$、縦軸を関数の値として、グラフ上を動きます。この点の座標は $(\theta, \cos\left(\frac{\pi}{2} + \theta\right) )$ で表されます。
数値計算について: コンピュータ上でグラフィックスを描画する際には、計算過程で微小な数値のずれが生じる可能性があります。アニメーション上の点の位置が理論値と完全に一致しない場合がある点、ご了承ください。
対象読者
- Pythonで数学的な概念を視覚化したい方
実行環境:Google Colab と Manim
このアニメーションを作成するために、以下のツールを使用しました。
-
Google Colaboratory (Colab):
- Googleが提供する無料のクラウドベースのJupyter Notebook環境。
- ブラウザ上でPythonコードを実行できます。
-
Manim:
- Pythonで高品質な数学アニメーションを作成するためのライブラリ。
環境構築やアニメーションの生成 (レンダリング)の方法は、以下の記事と同じです。
コード
%%writefile trig_identity_cos_sin_visualization.py
from manim import * # Manimライブラリから全てのクラスや関数をインポートします。
import numpy as np # 数値計算ライブラリNumPyをnpという別名でインポートします。
class TrigIdentityCosSinVisualization(Scene): # ManimのSceneクラスを継承して、三角関数の恒等式を視覚化する新しいシーンクラスを定義します。
def setup_config(self): # アニメーションの設定値をインスタンス属性としてセットアップするメソッドです。
"""Set up configuration values as instance attributes.""" # メソッドの説明(docstring):設定値をインスタンス属性としてセットアップします。
self.screen_height = config.frame_height # Manimの設定から画面の高さを取得し、インスタンス属性に格納します。
self.screen_width = config.frame_width # Manimの設定から画面の幅を取得し、インスタンス属性に格納します。
self.circle_axes_x_range = [-1.5, 1.5, 1] # 単位円を表示する座標軸のX軸の範囲 [最小値, 最大値, 目盛り間隔] を設定します。
self.circle_axes_y_range = [-1.5, 1.5, 1] # 単位円を表示する座標軸のY軸の範囲 [最小値, 最大値, 目盛り間隔] を設定します。
self.circle_axes_length = 4 # 単位円を表示する座標軸の長さを設定します。
self.circle_axes_shift = UP * 1.5 # 単位円の座標軸を画面中央から上に1.5単位移動させるベクトルを設定します。
self.circle_label_buff = 0.1 # 単位円の座標軸ラベルと軸線の間の余白を設定します。
self.graph_axes_x_range = [0, 2 * PI + 0.5, PI] # 関数のグラフを表示する座標軸のX軸の範囲 [最小値, 最大値, 目盛り間隔] を設定します (2πより少し右まで)。
self.graph_axes_y_range = [-1.2, 1.2, 1] # 関数のグラフを表示する座標軸のY軸の範囲 [最小値, 最大値, 目盛り間隔] を設定します。
self.graph_axes_x_length = self.screen_width * 0.7 # 関数のグラフを表示する座標軸のX軸の長さを画面幅の70%に設定します。
self.graph_axes_y_length = 2.5 # 関数のグラフを表示する座標軸のY軸の長さを設定します。
self.graph_axes_buff = 1.0 # 単位円の座標軸とグラフの座標軸の間の垂直方向の余白を設定します。
self.graph_x_tick_buff = 0.15 # グラフのX軸の目盛りラベルと軸線の間の余白を設定します。
self.graph_x_label_buff = 0.1 # グラフのX軸ラベルと軸線の間の余白を設定します。
self.graph_y_label_buff = 0.2 # グラフのY軸ラベルと軸線の間の余白を設定します。
self.unit_circle_color = ORANGE # 単位円の色をオレンジに設定します。
self.dot_radius = 0.08 # 動く点の半径を設定します。
self.dot_circle_color = YELLOW # 単位円上の点の色を黄色に設定します。
self.dot_graph_color = PINK # グラフ上の点の色をピンクに設定します。
self.radius_line_stroke_width = 3 # 単位円の半径を示す線の太さを設定します。
self.radius_line_color = WHITE # 単位円の半径を示す線の色を白に設定します。
self.angle_arc_radius = 0.4 # 角度を示す弧の半径を設定します。
self.angle_arc_color = WHITE # 角度を示す弧の色を白に設定します。
self.connecting_line_stroke_width = 2 # 単位円上の点とグラフ上の点を結ぶ線の太さを設定します。
self.connecting_line_color = RED # 単位円上の点とグラフ上の点を結ぶ線の色を赤に設定します。
self.graph_curve_color = PINK # グラフの曲線の色をピンクに設定します。
self.title_font_size = 40 # タイトルの数式のフォントサイズを設定します。
self.axes_label_font_size = 36 # 座標軸ラベルのフォントサイズを設定します。
self.coords_font_size = 24 # グラフ上の点の座標を表示するテキストのフォントサイズを設定します。
self.radius_label_font_size = 24 # 半径ラベル "r=1" のフォントサイズを設定します。
self.graph_creation_wait = 0.5 # グラフの曲線を描画した後に待機する時間(秒)を設定します。
self.theta_animation_runtime = 10 # 角度θを0から2πまで変化させるアニメーションの実行時間(秒)を設定します。
self.theta_end_wait = 0.1 # θのアニメーションが終了した後に待機する時間(秒)を設定します。
self.final_wait = 2 # 全てのアニメーションが終了した後に待機する時間(秒)を設定します。
self.theta_display_buff = 0.3 # θの値を表示するテキストと、その配置基準点との間の余白を設定します。
self.coord_label_buff = 0.1 # グラフ上の点の座標ラベルと、その点との間の余白を設定します。
self.radius_label_buff = 0.1 # 半径ラベル "r=1" と半径線の間の余白を設定します。
self.title_buff = 0.5 # タイトルと画面の端との間の余白を設定します。
self.y_label_formula = r"\cos(\frac{\pi}{2}+\theta)" # グラフのY軸ラベルとして表示する数式 (cos(π/2 + θ)) を定義します。実際にプロットされるのは -sin(θ) ですが、恒等式の左辺を表示します。
def setup_axes(self): # 単位円用とグラフ用の座標軸とそのラベルを作成するメソッドです。
"""Create Axes and their labels.""" # メソッドの説明(docstring):座標軸とそのラベルを作成します。
self.ax_circle = Axes( # 単位円を表示するための座標軸オブジェクトを作成します。
x_range=self.circle_axes_x_range, # X軸の範囲を設定値から取得します。
y_range=self.circle_axes_y_range, # Y軸の範囲を設定値から取得します。
x_length=self.circle_axes_length, # X軸の長さを設定値から取得します。
y_length=self.circle_axes_length, # Y軸の長さを設定値から取得します。
axis_config={"include_tip": False, "color": WHITE}, # 軸共通の設定:先端の矢印を表示せず、色を白にします。
x_axis_config={"include_numbers": False}, # X軸固有の設定:目盛り数値は表示しません。
y_axis_config={"include_numbers": False}, # Y軸固有の設定:目盛り数値は表示しません。
).shift(self.circle_axes_shift) # 作成した座標軸全体を設定された量だけ上に移動させます。
x_label_circle = self.ax_circle.get_x_axis_label("x", edge=RIGHT, direction=RIGHT, buff=self.circle_label_buff) # 単位円のX軸に "x" ラベルを作成し、軸の右端に配置します。
y_label_circle = self.ax_circle.get_y_axis_label("y", edge=UP, direction=UP, buff=self.circle_label_buff) # 単位円のY軸に "y" ラベルを作成し、軸の上端に配置します。
self.circle_labels = VGroup(x_label_circle, y_label_circle) # 単位円のX軸とY軸のラベルをグループ化して管理します。
self.ax_graph = Axes( # 関数のグラフを表示するための座標軸オブジェクトを作成します。
x_range=self.graph_axes_x_range, # X軸の範囲を設定値から取得します。
y_range=self.graph_axes_y_range, # Y軸の範囲を設定値から取得します。
x_length=self.graph_axes_x_length, # X軸の長さを設定値から取得します。
y_length=self.graph_axes_y_length, # Y軸の長さを設定値から取得します。
axis_config={"include_tip": False, "color": WHITE}, # 軸共通の設定:先端の矢印を表示せず、色を白にします。
x_axis_config={"include_numbers": False}, # X軸固有の設定:目盛り数値は表示しません。
y_axis_config={"include_numbers": True, "decimal_number_config": {"num_decimal_places": 0}}, # Y軸固有の設定:目盛り数値を表示し、小数点以下は0桁にします。
).next_to(self.ax_circle, DOWN, buff=self.graph_axes_buff) # 作成した座標軸を、単位円の座標軸の下に、指定した余白を空けて配置します。
x_tick_labels = VGroup() # グラフのX軸の特定の目盛りラベル(π, 2π)を格納するためのグループを作成します。
x_tick_labels.add(MathTex(r"\pi").next_to(self.ax_graph.c2p(PI, 0), DOWN, buff=self.graph_x_tick_buff)) # X軸のπの位置の下に "π" のラベルを追加します。c2pは座標値(coordinate)を画面上の点(point)に変換します。
x_tick_labels.add(MathTex(r"2\pi").next_to(self.ax_graph.c2p(2*PI, 0), DOWN, buff=self.graph_x_tick_buff)) # X軸の2πの位置の下に "2π" のラベルを追加します。
x_label_graph = self.ax_graph.get_x_axis_label(r"\theta", edge=RIGHT, direction=RIGHT, buff=self.graph_x_label_buff) # グラフのX軸に "θ" ラベルを作成し、軸の右端に配置します。
y_label_text = MathTex(self.y_label_formula, font_size=self.axes_label_font_size) # グラフのY軸に表示する数式 (cos(π/2 + θ)) のMathTexオブジェクトを作成します。
y_label_text.next_to(self.ax_graph.get_y_axis().get_end(), UP, buff=self.graph_y_label_buff) # Y軸ラベルを、Y軸の上端の上に配置します。
self.graph_labels = VGroup(x_label_graph, y_label_text, x_tick_labels) # グラフのX軸ラベル、Y軸ラベル、およびπ, 2πの目盛りラベルをグループ化して管理します。
def setup_static_elements(self): # アニメーション中に変化しない静的な要素(単位円、グラフ曲線、タイトル)を作成するメソッドです。
"""Create static mobjects like the circle, graph curve, and title.""" # メソッドの説明(docstring):円、グラフ曲線、タイトルなどの静的なMobjectを作成します。
self.unit_circle = Circle( # 単位円のCircleオブジェクトを作成します。
radius=self.ax_circle.x_axis.get_unit_size(), # 半径を単位円座標軸の単位長(=1)に設定します。
color=self.unit_circle_color, # 円の色を設定値から取得します。
arc_center=self.ax_circle.c2p(0, 0) # 円の中心を単位円座標軸の原点 (0, 0) に設定します。
) # Circleオブジェクトの作成完了。
self.graph_curve = self.ax_graph.plot(lambda t: -np.sin(t), x_range=[0, 2 * PI], color=self.graph_curve_color) # グラフ座標軸上に y = -sin(t) のグラフをプロットします。xの範囲は0から2πです。
self.title_eq = MathTex(self.y_label_formula, "=", r"-\sin(\theta)", "=", font_size=self.title_font_size) # タイトル部分の数式 "cos(π/2 + θ) = -sin(θ) =" をMathTexオブジェクトとして作成します。最後の "=" の後に値が表示されます。
def setup_dynamic_elements(self): # アニメーション中に変化する動的な要素を作成するメソッドです。ValueTrackerを用いて要素を連動させます。
"""Set up the ValueTracker and call helper methods to create dynamic mobjects.""" # メソッドの説明(docstring):ValueTrackerをセットアップし、ヘルパーメソッドを呼び出して動的なMobjectを作成します。
self.theta = ValueTracker(0) # 角度θの値を追跡・制御するためのValueTrackerオブジェクトを作成し、初期値を0に設定します。
self._setup_circle_dynamic_elements() # 単位円に関連する動的要素(点、半径線、角度弧)を作成する内部メソッドを呼び出します。
self._setup_graph_dynamic_elements() # グラフに関連する動的要素(点、座標ラベル)を作成する内部メソッドを呼び出します。
self._setup_ui_and_connecting_elements() # UI要素(θ表示、タイトル値)と接続線を作成する内部メソッドを呼び出します。
def _setup_circle_dynamic_elements(self): # 単位円に関連する動的要素を作成する内部メソッドです。
"""Create dynamic mobjects related to the unit circle.""" # メソッドの説明(docstring):単位円に関連する動的なMobjectを作成します。
self.dot_circle = Dot(color=self.dot_circle_color, radius=self.dot_radius) # 単位円上を動く点をDotオブジェクトとして作成します。
self.dot_circle.add_updater( # 点の位置をθの値に応じて更新するためのアップデーターを追加します。
lambda m: m.move_to(self.ax_circle.c2p(np.cos(self.theta.get_value()), np.sin(self.theta.get_value()))) # ラムダ式:θの値を取得し、単位円上の座標 (cosθ, sinθ) に点を移動させます。
) # add_updaterの終了。
self.radius_line = Line( # 単位円の中心(原点)から円上の点を結ぶ半径線を作成します。
self.ax_circle.c2p(0, 0), self.dot_circle.get_center(), # 線の始点(原点)と終点(円上の点の現在の中心)を指定します。
color=self.radius_line_color, stroke_width=self.radius_line_stroke_width # 線の色と太さを設定します。
) # Lineオブジェクトの作成完了。
self.radius_line.add_updater( # 半径線の終点を円上の点の動きに合わせて更新するためのアップデーターを追加します。
lambda m: m.put_start_and_end_on(self.ax_circle.c2p(0, 0), self.dot_circle.get_center()) # ラムダ式:線の始点を原点に、終点を常に円上の点の中心に設定し直します。
) # add_updaterの終了。
self.radius_label = MathTex("r=1", font_size=self.radius_label_font_size, color=self.radius_line_color) # 半径が1であることを示すラベル "r=1" を作成します。
self.radius_label.add_updater( # 半径ラベルの位置と向きを半径線に合わせて更新するためのアップデーターを追加します。
lambda m: m.next_to( # ラムダ式:ラベルの位置を決定します。
self.radius_line.get_center(), # 基準点は半径線の中点です。
direction=rotate_vector(self.radius_line.get_vector(), PI/2), # 配置方向は、半径線のベクトルを90度回転させた方向(線に垂直な方向)です。
buff=self.radius_label_buff # 半径線からの距離(余白)を設定します。
) # next_toの終了。
) # add_updaterの終了。
self.angle_arc = Arc( # 現在の角度θを示す弧(円弧)を作成します。
radius=self.angle_arc_radius, start_angle=0, angle=self.theta.get_value(), # 半径、開始角度(0)、弧の角度(現在のθの値)を設定します。
color=self.angle_arc_color, arc_center=self.ax_circle.c2p(0, 0) # 弧の色と中心(原点)を設定します。
) # Arcオブジェクトの作成完了。
self.angle_arc.add_updater( # 角度弧の角度をθの値に合わせて更新するためのアップデーターを追加します。
lambda m: m.become( # ラムダ式:既存の弧(m)を新しい弧で置き換えます (become)。
Arc( # 新しいArcオブジェクトを作成します。
radius=self.angle_arc_radius, start_angle=0, angle=self.theta.get_value(), # 半径と開始角度は同じで、角度を現在のθの値に更新します。
color=self.angle_arc_color, arc_center=self.ax_circle.c2p(0,0) # 色と中心は同じです。
) # 新しいArcオブジェクトの作成完了。
) # becomeの終了。
) # add_updaterの終了。
def _setup_graph_dynamic_elements(self): # グラフに関連する動的要素を作成する内部メソッドです。
"""Create dynamic mobjects related to the graph.""" # メソッドの説明(docstring):グラフに関連する動的なMobjectを作成します。
self.dot_graph = Dot(color=self.dot_graph_color, radius=self.dot_radius) # グラフ上を動く点をDotオブジェクトとして作成します。
self.dot_graph.add_updater( # グラフ上の点の位置をθの値に応じて更新するためのアップデーターを追加します。
lambda m: m.move_to(self.ax_graph.c2p(self.theta.get_value(), -np.sin(self.theta.get_value()))) # ラムダ式:θの値を取得し、グラフ上の座標 (θ, -sinθ) に点を移動させます。
) # add_updaterの終了。
coord_x_val = DecimalNumber( # グラフ上の点のX座標(θの値)を表示するためのDecimalNumberオブジェクトを作成します。
self.theta.get_value(), num_decimal_places=2, font_size=self.coords_font_size, color=self.dot_circle_color # 初期値、小数点以下の桁数、フォントサイズ、色を設定します。色は単位円の点に合わせています。
) # DecimalNumberオブジェクトの作成完了。
coord_y_val = DecimalNumber( # グラフ上の点のY座標(-sinθの値)を表示するためのDecimalNumberオブジェクトを作成します。
-np.sin(self.theta.get_value()), num_decimal_places=2, font_size=self.coords_font_size, color=self.dot_circle_color # 初期値、小数点以下の桁数、フォントサイズ、色を設定します。
) # DecimalNumberオブジェクトの作成完了。
l_paren = MathTex("(", font_size=self.coords_font_size, color=self.dot_circle_color) # 座標表示用の左括弧 "(" を作成します。
comma = MathTex(",", font_size=self.coords_font_size, color=self.dot_circle_color) # 座標表示用のコンマ "," を作成します。
r_paren = MathTex(")", font_size=self.coords_font_size, color=self.dot_circle_color) # 座標表示用の右括弧 ")" を作成します。
self.coord_label = VGroup(l_paren, coord_x_val, comma, coord_y_val, r_paren).arrange(RIGHT, buff=0.05) # 左括弧、X座標値、コンマ、Y座標値、右括弧をグループ化し、横一列に並べて座標ラベル (x, y) を作成します。
coord_x_val.add_updater(lambda m: m.set_value(self.theta.get_value())) # X座標値のDecimalNumberに、θの値が変化するたびに表示値を更新するアップデーターを追加します。
coord_y_val.add_updater(lambda m: m.set_value(-np.sin(self.theta.get_value()))) # Y座標値のDecimalNumberに、-sinθの値が変化するたびに表示値を更新するアップデーターを追加します。
self.coord_label.add_updater( # 座標ラベル全体の表示位置を更新するためのアップデーターを追加します。
lambda m: m.next_to( # ラムダ式:ラベルの位置を決定します。
self.dot_graph, # 基準点はグラフ上の点です。
UR if self.dot_graph.get_y() < self.ax_graph.c2p(0, 0)[1] else DR, # 配置方向を決定します。グラフ上の点がX軸より下にある場合は右上(UR)、上にある場合は右下(DR)に配置し、点と重ならないようにします。
buff=self.coord_label_buff # 点からの距離(余白)を設定します。
) # next_toの終了。
) # add_updaterの終了。
def _setup_ui_and_connecting_elements(self): # UI要素(θ表示、タイトル値)と、2つの点を結ぶ接続線を作成する内部メソッドです。
"""Create UI elements (theta display, title value) and the connecting line.""" # メソッドの説明(docstring):UI要素(θ表示、タイトル値)と接続線を作成します。
theta_value_text = DecimalNumber(0, num_decimal_places=2, font_size=self.axes_label_font_size, color=WHITE) # 現在のθの値を表示するためのDecimalNumberオブジェクトを作成します。
theta_label_tex = MathTex(r"\theta = ", font_size=self.axes_label_font_size, color=WHITE) # "θ = " というテキストラベルを作成します。
self.theta_display = VGroup(theta_label_tex, theta_value_text).arrange(RIGHT, buff=0.1) # "θ = " ラベルと数値表示をグループ化し、横一列に並べます。
theta_value_text.add_updater(lambda m: m.set_value(self.theta.get_value())) # θの数値表示に、θの値が変化するたびに表示値を更新するアップデーターを追加します。
self.theta_display.next_to(self.ax_circle.c2p(1, -1), DR, buff=self.theta_display_buff) # θ表示全体を、単位円座標軸の(1, -1)の点の右下に配置します。
self.connecting_line = Line( # 単位円上の点とグラフ上の点を結ぶ線を作成します。
self.dot_circle.get_center(), # 線の始点を単位円上の点の中心に設定します。
self.dot_graph.get_center(), # 線の終点をグラフ上の点の中心に設定します。
color=self.connecting_line_color, # 線の色を設定値から取得します。
stroke_width=self.connecting_line_stroke_width # 線の太さを設定値から取得します。
) # Lineオブジェクトの作成完了。
self.connecting_line.add_updater( # 接続線の始点と終点を、対応する点の動きに合わせて更新するためのアップデーターを追加します。
lambda m: m.put_start_and_end_on(self.dot_circle.get_center(), self.dot_graph.get_center()) # ラムダ式:線の始点と終点を常に両点の中心に再設定します。
) # add_updaterの終了。
self.current_value = DecimalNumber( # タイトル部分に表示される関数の現在の値 (-sinθ) を表示するためのDecimalNumberオブジェクトを作成します。
0, num_decimal_places=2, show_ellipsis=False, group_with_commas=False, font_size=self.title_font_size # 初期値0、小数点以下2桁、省略記号なし、桁区切りなし、フォントサイズを設定します。
) # DecimalNumberオブジェクトの作成完了。
self.current_value.add_updater( # タイトルの値表示に、-sinθの値が変化するたびに表示値を更新するアップデーターを追加します。
lambda m: m.set_value(-np.sin(self.theta.get_value())) # ラムダ式:表示値を現在の-sinθの値に設定します。
) # add_updaterの終了。
self.title_group = VGroup(self.title_eq, self.current_value).arrange(RIGHT, buff=0.2).to_edge(UL, buff=self.title_buff) # タイトルの数式部分 (self.title_eq) と現在の値表示 (self.current_value) をグループ化し、横一列に並べ、画面左上に配置します。
def play_initial_animations(self): # シーンの初期状態を構築するためのアニメーションを再生するメソッドです。
"""Play animations to create the initial scene setup.""" # メソッドの説明(docstring):初期シーン設定のためのアニメーションを再生します。
self.play( # 複数のアニメーションを同時に再生します。
Create(self.ax_circle), # 単位円の座標軸を作成するアニメーション。
Create(self.ax_graph), # グラフの座標軸を作成するアニメーション。
Write(self.circle_labels), # 単位円の軸ラベルを書き出すアニメーション。
Write(self.graph_labels), # グラフの軸ラベルと目盛りラベルを書き出すアニメーション。
Create(self.unit_circle), # 単位円を作成するアニメーション。
Write(self.title_group), # タイトルを書き出すアニメーション。
Write(self.theta_display) # θ表示を書き出すアニメーション。
) # 同時再生アニメーションの終了。
self.play(Create(self.graph_curve)) # グラフの曲線 (-sinθ) を作成するアニメーションを再生します。
self.wait(self.graph_creation_wait) # 設定された時間だけ一時停止します。
self.add( # 動的な要素(点、線、ラベルなど)をアニメーションなしでシーンに追加します。これらはアップデーターによって初期位置に配置されます。
self.radius_line, self.dot_circle, self.radius_label, # 半径線、単位円上の点、半径ラベルを追加。
self.angle_arc, # 角度を示す弧を追加。
self.dot_graph, self.connecting_line, # グラフ上の点、接続線を追加。
self.coord_label # 座標ラベルを追加。
) # addメソッドの終了。
def play_theta_animation(self): # 角度θを0から2πまで変化させ、それに連動する要素を動かすアニメーションを再生するメソッドです。
"""Animate theta from 0 to 2*PI.""" # メソッドの説明(docstring):θを0から2πまでアニメーションさせます。
self.play( # アニメーションを再生します。
self.theta.animate.set_value(2 * PI), # ValueTracker `self.theta` の値をアニメーションで 2π まで変化させます (`.animate`構文)。
run_time=self.theta_animation_runtime, # アニメーションの実行時間を設定値から取得します。
rate_func=linear # アニメーションの変化速度を一定(線形)にします。
) # アニメーション再生の終了。
self.theta.set_value(2 * PI) # アニメーション終了後、浮動小数点演算の誤差を考慮し、θの値を正確に 2π に設定します。
# --- ここから浮動小数点誤差補正と最終位置設定 ---
# アニメーション終了後、浮動小数点誤差を補正してオブジェクトを正確な理論上の最終位置に配置します。
# 理論的な最終角度 (theta=2*PI) を変数に格納します。
final_theta = 2 * PI # 最終的なθの値(2π)を定義します。
final_cos = 1.0 # θ=2π のときのcosの理論値 (1.0) を定義します(`np.cos(2*PI)`の計算誤差を避けるため)。
final_sin = 0.0 # θ=2π のときのsinの理論値 (0.0) を定義します(`np.sin(2*PI)`の計算誤差を避けるため)。
final_point_circle_coords = (final_cos, final_sin) # 単位円上の点の最終的な理論座標 (1, 0) をタプルとして定義します。
final_point_graph_coords = (final_theta, -final_sin) # グラフ上の点の最終的な理論座標 (2π, 0) をタプルとして定義します (-sin(2π) = 0)。
final_point_circle = self.ax_circle.c2p(*final_point_circle_coords) # 単位円座標軸上の理論座標 (1, 0) を画面上の点 (Point) に変換します (`*`でタプルを展開)。
final_point_graph = self.ax_graph.c2p(*final_point_graph_coords) # グラフ座標軸上の理論座標 (2π, 0) を画面上の点 (Point) に変換します (`*`でタプルを展開)。
origin_circle = self.ax_circle.c2p(0, 0) # 単位円座標軸の原点 (0, 0) を画面上の点 (Point) に変換し、半径線の始点として使用します。
# オブジェクトを計算された最終的な理論位置に強制的に移動させます。
# これにより、updaterが計算時に生じさせる可能性のある微小な浮動小数点誤差が上書きされ、見た目のズレが解消されます。
self.dot_circle.move_to(final_point_circle) # 単位円上の点 (`dot_circle`) を、計算した理論上の最終位置 (`final_point_circle`) に移動させます。
self.radius_line.put_start_and_end_on(origin_circle, final_point_circle) # 半径線 (`radius_line`) の始点を原点 (`origin_circle`) に、終点を単位円上の点の最終位置 (`final_point_circle`) に設定し直します。
# 半径ラベル (`radius_label`) は `radius_line` に追従するupdaterを持つため、`radius_line`が更新されれば自動的に正しい位置に移動します。別途設定は不要です。
self.dot_graph.move_to(final_point_graph) # グラフ上の点 (`dot_graph`) を、計算した理論上の最終位置 (`final_point_graph`) に移動させます。
self.connecting_line.put_start_and_end_on(final_point_circle, final_point_graph) # 接続線 (`connecting_line`) の始点と終点を、それぞれ単位円上の点とグラフ上の点の最終位置に設定し直します。
# 角度を示す弧 (`angle_arc`) の最終状態を設定します。
# `theta = 2 * PI` の場合、updaterによって `angle=2*PI` のArc (完全な円) が描画される可能性があります。
# 完全な円は不要な場合や、角度がリセットされた状態を示したい場合は、`become` を使って状態を変更します。
# `angle = final_theta` (2*PI) にすると完全な円になります:
# self.angle_arc.become(Arc(radius=self.angle_arc_radius, start_angle=0, angle=final_theta, color=self.angle_arc_color, arc_center=origin_circle))
# ここでは、角度が0に戻った状態(ただしangle=0だと表示されない)を示すために、微小な角度を持つArcで置き換えます:
self.angle_arc.become(Arc(radius=self.angle_arc_radius, start_angle=0, angle=0.0001, color=self.angle_arc_color, arc_center=origin_circle)) # 角度をほぼ0にして、アニメーション終了時に弧が見えないようにします。
# DecimalNumberなどの値表示は、`theta.set_value(2 * PI)` の時点でupdaterが再計算するため、通常は明示的な値設定は不要です。
# --- 誤差補正と最終位置設定ここまで ---
self.wait(self.theta_end_wait) # 設定された短い時間 (`theta_end_wait`) だけ一時停止し、最終状態を見せます。
def construct(self): # シーン全体の構成とアニメーションの実行順序を定義するメインメソッドです。
"""Main construction method orchestrating the scene creation.""" # メソッドの説明(docstring):シーン作成を統括する主要な構築メソッドです。
self.setup_config() # 設定値をロードします。
self.setup_axes() # 座標軸を作成します。
self.setup_static_elements() # 静的な要素(円、グラフ曲線、タイトルの一部)を作成します。
self.setup_dynamic_elements() # 動的な要素(点、線、ラベル、値表示)とそれらのアップデーターを設定します。
self.play_initial_animations() # 作成した静的要素と一部の動的要素を表示する初期アニメーションを再生します。
self.play_theta_animation() # θを変化させ、動的要素を動かすメインのアニメーションを再生します。
self.wait(self.final_wait) # 全てのアニメーションが終了した後、設定された時間だけ待機してシーンを終了します。
Manimのadd_updaterについて
Manimのadd_updater
は、オブジェクトに「常に特定のルールに従って見た目を更新し続ける」よう指示する強力なメソッドです。例えば、ValueTracker
で管理する数値(例:時間や角度)が変化するたびに、その数値に基づいてオブジェクトの位置や形を自動的に再計算し、描画し直します。これにより、毎フレームの細かな描画処理を記述する必要がなくなり、宣言的に複雑な連動アニメーションを簡単に作成できます。
ManimのValueTracker について
ManimのValueTracker
は、アニメーションの進行に合わせて変化する「数値」を管理するための特別なオブジェクトです。この数値を時間、角度、進行度などに見立て、.animate.set_value()
で値を滑らかに変化させることができます。そして、add_updater
と組み合わせることで、ValueTracker
の値が変わるたびに、それに依存する複数のオブジェクトの見た目(位置、大きさ、色など)を自動的に更新させることが可能です。
Manimのself.play について
Manimにおけるself.play()
は、アニメーションを実際に画面上で再生するための中心的な命令です。Create
(オブジェクトの出現)、Write
(テキストの書き出し)、Transform
(変形)、FadeOut
(消滅)といった様々なアニメーションクラスのインスタンスを引数に取ります。カンマで区切って複数のアニメーションを渡せばそれらを同時に実行し、self.play()
を連続して呼び出せばアニメーションを順番に再生します。
カスタマイズの可能性
このコードをベースに、他の三角関数の恒等式を可視化することも可能です。例えば、
$$ \sin\left(\frac{\pi}{2} + \theta\right) = \cos(\theta) $$
を可視化したい場合は、関連する数式や関数定義を変更することで、様々な三角関数の関係性を視覚的に探求できます。
まとめ
単位円とグラフを組み合わせることで、抽象的な数式の意味をより直感的に理解する助けとなります。Manimは、このような数学的な概念の視覚化・教育コンテンツ作成において非常に強力なツールです。ぜひ、色々な数式や概念をManimで表現してみてください。