ご注意
数学的な厳密性やManimの最適な利用方法に関して、改善の余地が含まれている可能性がございます。
完成したアニメーション
この記事のコードを実行すると、『0.999... は 1 と等しい』という結論に至る過程を、動的に可視化するアニメーションが生成されます。
対象読者
- Pythonで数学的な概念を視覚化したい方
実行環境:Google Colab と Manim
このアニメーションを作成するために、以下のツールを使用しました。
-
Google Colaboratory (Colab):
- Googleが提供する無料のクラウドベースのJupyter Notebook環境。
- ブラウザ上でPythonコードを実行できます。
-
Manim:
- Pythonで高品質な数学アニメーションを作成するためのライブラリ。
環境構築やアニメーションの生成 (レンダリング)の方法は、以下の記事と同じです。
コード
%%writefile Proof0999Equals1.py
# manimライブラリから全てのクラス、関数、定数をインポート
from manim import *
# 0.999... = 1 の証明アニメーションを生成するクラス
class Proof0999Equals1(Scene):
# ========================
# Configuration (各種設定値)
# ========================
CONFIG = {
# フォントサイズ関連の設定
"font_sizes": {
"title": 110, # タイトルのフォントサイズ
"main": 96, # メインの数式のフォントサイズ
"conclusion": 110, # 最終結論のフォントサイズ
"persistent_premise": 48, # 画面隅に表示する前提のフォントサイズ
},
# 色関連の設定
"colors": {
"title": ORANGE, # タイトルの色
"calculation_highlight": YELLOW_C, # 計算箇所を囲む色
"cancellation": RED_C, # 約分で消える数字の色
"line": WHITE, # 除算の線の色
"conclusion_rect": YELLOW, # 最終結論を囲む矩形の色
},
# 時間関連の設定 (単位: 秒)
"times": {
"title_wait": 1.5, # タイトル表示後の待機時間
"default_wait": 1.5, # 各ステップ間のデフォルト待機時間
"inter_step_wait": 0.5, # アニメーション中の短い待機時間
"division_anim": 2.0, # 除算ステップ全体のアニメーション時間
"conclusion_anim": 2.0, # 最終結論への変形アニメーション時間
"subtraction_terms_fade_in_time": 0.8, # 引き算の項がフェードインする時間
"subtraction_equation_rearrange_time": 0.7, # 引き算のために元の式が移動する時間
"step3_circumscribe_time": 2.0, # 計算箇所を囲むアニメーションの時間
"step3_replacement_time": 1.0, # 計算結果に置き換わるアニメーションの時間
"premise_to_corner_time": 1.5, # 中央の前提が隅へ移動する時間
},
# バッファ (要素間の空白) やサイズ関連の設定
"buffers": {
"conclusion_rect": 0.3, # 最終結論を囲む矩形の余白
"equation": 0.25, # 数式内の要素間のデフォルトバッファ
"multiply_10_lhs_gap": 0.1, # 10倍ステップでの "10" と "x" の間のバッファ
"division_line_width_factor": 1.1, # 除算の線の幅の係数 (要素幅に対する比率)
"division_line_buff": 0.15, # 除算の線と数値の間の垂直バッファ
"subtraction_element_gap": 0.1, # 引き算の項("-", "x")内部のバッファ
"calculated_9x_internal_buff": 0.05,# 計算結果 "9x" 内部のバッファ
"subtraction_lhs_term_gap": 0.2, # 引き算ステップでの "10x" と "-x" の間のバッファ
"subtraction_rhs_term_gap": 0.2, # 引き算ステップでの "9.999..." と "-0.999..." の間のバッファ
"subtraction_full_equation_buff": 0.4, # 引き算後の完全な式全体を配置する際の要素間バッファ
"persistent_premise_corner_buff": 0.5,# 画面隅に表示する前提と画面端とのバッファ
},
# アニメーションのパラメータ設定
"animation_params": {
"subtraction_anim_shift_vector": DOWN * 0.3, # 引き算の項がフェードインする際のシフト方向と量
"fade_in_shift_vector_short": RIGHT * 0.2, # 短いフェードインシフト (10倍の"10"など)
"subtraction_fade_in_lag_ratio": 0.6, # 引き算の項のLaggedStartでの遅延比率
"division_lines_lag_ratio": 0.3, # 除算の線のLaggedStartでの遅延比率
},
# アニメーション時間の比率設定 (特定の全体時間に対する部分時間の比率)
"animation_timing_ratios": {
"division_lines_creation_ratio": 0.3, # division_animに対する除算線作成時間の比率
"division_cancellation_ratio": 0.4, # division_animに対する約分アニメーション時間の比率
},
# TeX 文字列のマクロ定義
"tex": {
"x": "x",
"equals": "=",
"num_0_999": "0.999...",
"num_10": "10",
"expr_10x": "10x",
"num_9_999": "9.999...",
"minus": "-",
"num_9": "9",
"num_1": "1",
}
}
# 結論表示後の待機時間 (デフォルト待機時間 + 1秒)
CONCLUSION_WAIT_TIME = CONFIG["times"]["default_wait"] + 1
# 最終結論のTeX文字列
TEX_CONCLUSION = "0.999... = 1"
# タイトルのTeX文字列
TEX_TITLE = r"0.999\dots = 1?"
# クラスの初期化メソッド
def __init__(self, **kwargs):
# 親クラス(Scene)の初期化メソッドを呼び出す
super().__init__(**kwargs)
# 永続的に表示する前提オブジェクトを格納するインスタンス変数を初期化
self.persistent_premise_display = VGroup()
# ================
# Helper Methods (補助メソッド)
# ================
# スタイル付きのMathTexオブジェクトを生成するヘルパーメソッド
def _create_styled_math_tex(self, tex_string, color=None, font_size=None):
# フォントサイズが指定されていなければ、CONFIGのメインフォントサイズを使用
if font_size is None: font_size = self.CONFIG["font_sizes"]["main"]
# MathTexオブジェクトを作成
mobj = MathTex(tex_string, font_size=font_size)
# 色が指定されていれば設定
if color: mobj.set_color(color)
# 作成したMathTexオブジェクトを返す
return mobj
# ===========================
# Animation Setup Methods (アニメーション準備メソッド)
# ===========================
# 永続表示用の前提を画面隅にセットアップするメソッド
def _setup_persistent_premise(self, premise_obj_at_center):
"""
中央に表示された前提オブジェクトを、永続表示用に画面隅へ移動・変形させ、
self.persistent_premise_display に格納する。
引数: premise_obj_at_center (中央に表示されている前提オブジェクト VGroup)
"""
# 永続表示用の x, =, 0.999... オブジェクトを小さいフォントサイズで作成
prem_x_p = self._create_styled_math_tex(self.CONFIG["tex"]["x"], font_size=self.CONFIG["font_sizes"]["persistent_premise"])
prem_eq_p = self._create_styled_math_tex(self.CONFIG["tex"]["equals"], font_size=self.CONFIG["font_sizes"]["persistent_premise"])
prem_0999_p = self._create_styled_math_tex(self.CONFIG["tex"]["num_0_999"], font_size=self.CONFIG["font_sizes"]["persistent_premise"])
# 永続表示用の目標となるオブジェクト (VGroup) を作成し、配置・スタイル調整
target_persistent_premise = (
VGroup(prem_x_p, prem_eq_p, prem_0999_p)
.arrange(RIGHT, buff=self.CONFIG["buffers"]["equation"] * 0.5) # 要素間バッファを少し詰める
.to_corner(UP + LEFT, buff=self.CONFIG["buffers"]["persistent_premise_corner_buff"]) # 左上隅に配置
)
# アニメーション: 中央の前提オブジェクトを、目標の隅のオブジェクトに変形させる
self.play(
Transform(premise_obj_at_center, target_persistent_premise), # 変形アニメーションを実行
run_time=self.CONFIG["times"]["premise_to_corner_time"] # 変形にかける時間
)
# Transform後のオブジェクト (premise_obj_at_centerが変形したもの) を永続表示用としてインスタンス変数に保持
self.persistent_premise_display = premise_obj_at_center
# self.add_foreground_mobject(self.persistent_premise_display) # 必要に応じて最前面に表示する場合
# メインの式変形フローで使用する最初の前提を準備するメソッド
def _prepare_main_flow_premise(self):
"""
メインの式変形フローで使用する最初の前提「x = 0.999...」を中央に作成・表示する。
戻り値: 中央に表示された前提オブジェクト (VGroup)
"""
# メインフロー用の x, =, 0.999... オブジェクトをメインフォントサイズで作成
s1_x = self._create_styled_math_tex(self.CONFIG["tex"]["x"])
s1_eq = self._create_styled_math_tex(self.CONFIG["tex"]["equals"])
s1_0999 = self._create_styled_math_tex(self.CONFIG["tex"]["num_0_999"])
# VGroupにまとめて中央に配置
eq_s1_main_flow = VGroup(s1_x, s1_eq, s1_0999).arrange(RIGHT, buff=self.CONFIG["buffers"]["equation"]).move_to(ORIGIN)
# アニメーション: 作成した前提を画面に書き出す
self.play(Write(eq_s1_main_flow))
# デフォルトの待機時間待つ
self.wait(self.CONFIG["times"]["default_wait"])
# 作成・表示した前提オブジェクトを返す
return eq_s1_main_flow
# ===========================
# Animation Sequence Methods (各ステップのアニメーションメソッド)
# ===========================
# タイトルを表示しフェードアウトさせるメソッド
def _display_title(self):
# タイトルオブジェクトを作成
title = self._create_styled_math_tex(
self.TEX_TITLE,
color=self.CONFIG["colors"]["title"],
font_size=self.CONFIG["font_sizes"]["title"]
)
# タイトルを中央に配置
title.move_to(ORIGIN)
# アニメーション: タイトルを書き出す
self.play(Write(title))
# タイトル表示後の待機時間
self.wait(self.CONFIG["times"]["title_wait"])
# アニメーション: タイトルをフェードアウトさせる
self.play(FadeOut(title))
# 次のステップへの短い待機時間
self.wait(self.CONFIG["times"]["inter_step_wait"])
# 最初の前提「x = 0.999...」を中央に表示するメソッド
def _introduce_premise_center(self):
# x, =, 0.999... オブジェクトを作成
x_premise = self._create_styled_math_tex(self.CONFIG["tex"]["x"])
eq_premise = self._create_styled_math_tex(self.CONFIG["tex"]["equals"])
num_0999_premise = self._create_styled_math_tex(self.CONFIG["tex"]["num_0_999"])
# VGroupにまとめて中央に配置
initial_premise_group = VGroup(x_premise, eq_premise, num_0999_premise)
initial_premise_group.arrange(RIGHT, buff=self.CONFIG["buffers"]["equation"]).move_to(ORIGIN)
# アニメーション: 前提を書き出す
self.play(Write(initial_premise_group))
# デフォルトの待機時間
self.wait(self.CONFIG["times"]["default_wait"])
# 表示した前提オブジェクトを返す
return initial_premise_group
# ステップ1 -> ステップ2: x = 0.999... から 10x = 9.999... へ変形
def _animate_step1_to_step2(self, eq1_group):
# 入力 VGroup から各要素を取得
x_obj, eq_obj, num_0999_obj = eq1_group.submobjects
# "10" オブジェクトを作成し、"x" の左隣に配置
ten_obj_anim = self._create_styled_math_tex(self.CONFIG["tex"]["num_10"]).next_to(x_obj, LEFT, buff=self.CONFIG["buffers"]["multiply_10_lhs_gap"])
# 右辺の目標となる "9.999..." オブジェクトを作成 (位置は元の0.999...と同じ)
target_9999_look = self._create_styled_math_tex(self.CONFIG["tex"]["num_9_999"]).move_to(num_0999_obj)
# アニメーション: "10"をフェードイン、"x"を"10"の右へ移動、右辺を"9.999..."に変形
self.play(
FadeIn(ten_obj_anim, shift=self.CONFIG["animation_params"]["fade_in_shift_vector_short"]), # "10" が右からスライドイン
x_obj.animate.next_to(ten_obj_anim, RIGHT, buff=self.CONFIG["buffers"]["multiply_10_lhs_gap"]), # "x" が移動
Transform(num_0999_obj, target_9999_look) # "0.999..." が "9.999..." に変形
)
# 短い待機時間
self.wait(0.1)
# 一時的な左辺グループ ("10" と "x") を作成
temp_lhs_group = VGroup(ten_obj_anim, x_obj)
# 最終的な左辺 "10x" オブジェクトを作成し、一時グループの中心に配置
final_10x_obj = self._create_styled_math_tex(self.CONFIG["tex"]["expr_10x"]).move_to(temp_lhs_group.get_center())
# アニメーション: 一時グループ("10", "x") を "10x" に瞬時に置き換える
self.play(
ReplacementTransform(temp_lhs_group, final_10x_obj),
run_time=0.01 # ほぼゼロの時間を指定して瞬時に見せる
)
# ステップ2の最終的な式グループ ("10x", "=", "9.999...") を作成
eq2_group = VGroup(final_10x_obj, eq_obj, num_0999_obj)
# アニメーション: 式全体を整列させて中央に配置
self.play(
eq2_group.animate.arrange(RIGHT, buff=self.CONFIG["buffers"]["equation"]).move_to(ORIGIN)
)
# デフォルトの待機時間
self.wait(self.CONFIG["times"]["default_wait"])
# ステップ2の式グループを返す
return eq2_group
# ステップ2 -> ステップ3: 10x = 9.999... から 10x - x = 9.999... - 0.999... へ
def _animate_step2_to_step3(self, eq2_group):
# 入力 VGroup から各要素を取得
lhs_10x_obj, eq_obj, rhs_9999_obj = eq2_group.submobjects
# 引き算する "-x" の部分を作成
minus_op_lhs = self._create_styled_math_tex(self.CONFIG["tex"]["minus"])
x_to_subtract = self._create_styled_math_tex(self.CONFIG["tex"]["x"], color=self.CONFIG["colors"]["title"]) # 色をタイトルカラーに
subtract_term_lhs = VGroup(minus_op_lhs, x_to_subtract).arrange(RIGHT, buff=self.CONFIG["buffers"]["subtraction_element_gap"])
# 引き算する "-0.999..." の部分を作成
minus_op_rhs = self._create_styled_math_tex(self.CONFIG["tex"]["minus"])
val_0999_to_subtract = self._create_styled_math_tex(self.CONFIG["tex"]["num_0_999"], color=self.CONFIG["colors"]["title"]) # 色をタイトルカラーに
subtract_term_rhs = VGroup(minus_op_rhs, val_0999_to_subtract).arrange(RIGHT, buff=self.CONFIG["buffers"]["subtraction_element_gap"])
# --- 目標位置の計算 ---
# 元のオブジェクトをコピーして位置計算に使用 (元のオブジェクトはまだ動かさない)
temp_lhs_10x = lhs_10x_obj.copy()
temp_eq = eq_obj.copy()
temp_rhs_9999 = rhs_9999_obj.copy()
temp_subtract_lhs = subtract_term_lhs.copy()
temp_subtract_rhs = subtract_term_rhs.copy()
# 左辺の目標レイアウト ("10x - x") を仮作成
target_lhs_group_layout = VGroup(temp_lhs_10x, temp_subtract_lhs).arrange(RIGHT, buff=self.CONFIG["buffers"]["subtraction_lhs_term_gap"])
# 右辺の目標レイアウト ("9.999... - 0.999...") を仮作成
target_rhs_group_layout = VGroup(temp_rhs_9999, temp_subtract_rhs).arrange(RIGHT, buff=self.CONFIG["buffers"]["subtraction_rhs_term_gap"])
# 式全体の目標レイアウトを仮作成し、中央に配置(これで各要素の最終座標がわかる)
temp_full_eq_for_pos_calc = VGroup(target_lhs_group_layout, temp_eq, target_rhs_group_layout)\
.arrange(RIGHT, buff=self.CONFIG["buffers"]["subtraction_full_equation_buff"])\
.move_to(ORIGIN)
# 各要素の最終的な目標中心座標を取得
target_pos_lhs_10x = temp_full_eq_for_pos_calc[0][0].get_center() # "10x" の目標位置
target_pos_eq = temp_full_eq_for_pos_calc[1].get_center() # "=" の目標位置
target_pos_rhs_9999 = temp_full_eq_for_pos_calc[2][0].get_center()# "9.999..." の目標位置
# 引き算の項 ("-x", "-0.999...") の最終的な目標中心座標を取得
final_pos_subtract_term_lhs = temp_full_eq_for_pos_calc[0][1].get_center() # "-x" の目標位置
final_pos_subtract_term_rhs = temp_full_eq_for_pos_calc[2][1].get_center() # "-0.999..." の目標位置
# アニメーション: 元の式の要素 ("10x", "=", "9.999...") を計算済みの目標位置へ移動 (スペースを作る)
self.play(
lhs_10x_obj.animate.move_to(target_pos_lhs_10x),
eq_obj.animate.move_to(target_pos_eq),
rhs_9999_obj.animate.move_to(target_pos_rhs_9999),
run_time=self.CONFIG["times"]["subtraction_equation_rearrange_time"] # 移動にかける時間
)
# 短い待機時間
self.wait(self.CONFIG["times"]["inter_step_wait"])
# 引き算の項を、アニメーション表示前の準備として、最終的な位置に配置(まだ見えない)
subtract_term_lhs.move_to(final_pos_subtract_term_lhs)
subtract_term_rhs.move_to(final_pos_subtract_term_rhs)
# アニメーション: 引き算の項をフェードインで表示 (下からスライドインする効果付き)
self.play(
LaggedStart( # 左右の項を少しずらして表示開始
FadeIn(subtract_term_lhs, shift=self.CONFIG["animation_params"]["subtraction_anim_shift_vector"]),
FadeIn(subtract_term_rhs, shift=self.CONFIG["animation_params"]["subtraction_anim_shift_vector"]),
lag_ratio=self.CONFIG["animation_params"]["subtraction_fade_in_lag_ratio"] # 遅延比率
),
run_time=self.CONFIG["times"]["subtraction_terms_fade_in_time"] # フェードインにかける時間
)
# デフォルトの待機時間
self.wait(self.CONFIG["times"]["default_wait"])
# ステップ3の最終的な要素リストを作成 (表示順)
final_s3_elements = [
lhs_10x_obj, # 10x
minus_op_lhs, x_to_subtract, # -x (展開して追加)
eq_obj, # =
rhs_9999_obj, # 9.999...
minus_op_rhs, val_0999_to_subtract # -0.999... (展開して追加)
]
# ステップ3の最終的な式グループを作成
eq3_group = VGroup(*final_s3_elements)
# ステップ3の式グループを返す
return eq3_group
# ステップ3 -> ステップ4: 引き算を実行して 9x = 9 へ
def _animate_step3_to_step4(self, eq3_group):
# 入力 VGroup から各要素を取得 (インデックスでアクセスしても良い)
s3_10x, s3_minus_l, s3_x, s3_eq, s3_9999, s3_minus_r, s3_0999 = eq3_group.submobjects
# 計算結果の "9" (係数) と "x" (変数) を作成
res_9_coeff = self._create_styled_math_tex(self.CONFIG["tex"]["num_9"])
res_x_var = self._create_styled_math_tex(self.CONFIG["tex"]["x"])
# 計算結果の左辺 "9x" グループを作成
target_9x_grp = VGroup(res_9_coeff, res_x_var).arrange(RIGHT, buff=self.CONFIG["buffers"]["calculated_9x_internal_buff"])
# 計算結果の右辺 "9" を作成
target_9_rhs = self._create_styled_math_tex(self.CONFIG["tex"]["num_9"])
# 計算対象となる左辺の項 ("10x", "-", "x") をグループ化
lhs_terms_to_calculate = VGroup(s3_10x, s3_minus_l, s3_x)
# 計算対象となる右辺の項 ("9.999...", "-", "0.999...") をグループ化
rhs_terms_to_calculate = VGroup(s3_9999, s3_minus_r, s3_0999)
# 計算結果のオブジェクトを、計算対象オブジェクトの中心に(変形アニメーションの準備として)移動
target_9x_grp.move_to(lhs_terms_to_calculate.get_center())
target_9_rhs.move_to(rhs_terms_to_calculate.get_center())
# アニメーション: 計算対象の項を黄色い枠でゆっくり囲んで消す
self.play(
Circumscribe(lhs_terms_to_calculate, color=self.CONFIG["colors"]["calculation_highlight"], fade_out=True, run_time=self.CONFIG["times"]["step3_circumscribe_time"]),
Circumscribe(rhs_terms_to_calculate, color=self.CONFIG["colors"]["calculation_highlight"], fade_out=True, run_time=self.CONFIG["times"]["step3_circumscribe_time"])
)
# アニメーション: 計算対象の項を計算結果に置き換える (変形)
self.play(
ReplacementTransform(lhs_terms_to_calculate, target_9x_grp), # 左辺を "9x" に変形
ReplacementTransform(rhs_terms_to_calculate, target_9_rhs), # 右辺を "9" に変形
run_time=self.CONFIG["times"]["step3_replacement_time"] # 変形にかける時間
)
# 短い待機時間
self.wait(self.CONFIG["times"]["inter_step_wait"])
# ステップ4の最終的な式グループ ("9x", "=", "9") を作成
eq4_group = VGroup(target_9x_grp, s3_eq, target_9_rhs) # s3_eq (=) はそのまま流用
# アニメーション: 式全体を整列させて中央に配置
self.play(eq4_group.animate.arrange(RIGHT, buff=self.CONFIG["buffers"]["equation"]).move_to(ORIGIN))
# デフォルトの待機時間
self.wait(self.CONFIG["times"]["default_wait"])
# ステップ4の式グループを返す
return eq4_group
# ステップ4 -> ステップ5: 9x = 9 から x = 1 へ (両辺を9で割る)
def _animate_step4_to_step5(self, eq4_group):
# 入力 VGroup から各要素を取得
s4_9x_vgroup, s4_eq_term, s4_9_rhs_obj = eq4_group.submobjects
# 左辺 "9x" から "9" と "x" を取得
num_9_lhs_obj = s4_9x_vgroup.submobjects[0]
num_x_lhs_obj = s4_9x_vgroup.submobjects[1]
# 分母となる "9" オブジェクトを作成 (左右、キャンセル色で)
den_9_lhs_obj = self._create_styled_math_tex(self.CONFIG["tex"]["num_9"], color=self.CONFIG["colors"]["cancellation"])
den_9_rhs_obj = self._create_styled_math_tex(self.CONFIG["tex"]["num_9"], color=self.CONFIG["colors"]["cancellation"])
# 除算の線 (Lineオブジェクト) を作成 (左右)
line_lhs = Line(LEFT, RIGHT, color=self.CONFIG["colors"]["line"])
line_rhs = Line(LEFT, RIGHT, color=self.CONFIG["colors"]["line"])
# 除算線の幅と位置を調整
line_width_factor = self.CONFIG["buffers"]["division_line_width_factor"]
line_buff = self.CONFIG["buffers"]["division_line_buff"]
# 左辺の線: "9x" の下に配置
line_lhs.set_width(VGroup(num_9_lhs_obj, num_x_lhs_obj).width * line_width_factor).next_to(VGroup(num_9_lhs_obj, num_x_lhs_obj), DOWN, buff=line_buff)
# 左辺の分母: 線のさらに下に配置
den_9_lhs_obj.next_to(line_lhs, DOWN, buff=line_buff)
# 右辺の線: "9" の下に配置
line_rhs.set_width(s4_9_rhs_obj.width * line_width_factor).next_to(s4_9_rhs_obj, DOWN, buff=line_buff)
# 右辺の分母: 線のさらに下に配置
den_9_rhs_obj.next_to(line_rhs, DOWN, buff=line_buff)
# 除算アニメーション全体の時間と、線と分母表示部分の時間の比率を取得
division_anim_time = self.CONFIG["times"]["division_anim"]
lines_creation_ratio = self.CONFIG["animation_timing_ratios"]["division_lines_creation_ratio"]
cancellation_ratio = self.CONFIG["animation_timing_ratios"]["division_cancellation_ratio"]
# アニメーション: 除算の線と分母を表示 (LaggedStartで順次表示)
self.play(LaggedStart(Create(line_lhs), Write(den_9_lhs_obj), Create(line_rhs), Write(den_9_rhs_obj),
lag_ratio=self.CONFIG["animation_params"]["division_lines_lag_ratio"]),
run_time=division_anim_time * lines_creation_ratio # 全体時間に対する比率で時間を設定
)
# 短い待機時間
self.wait(self.CONFIG["times"]["inter_step_wait"])
# 約分後の最終結果 "x" と "1" オブジェクトを作成
final_x_obj = self._create_styled_math_tex(self.CONFIG["tex"]["x"])
final_1_obj = self._create_styled_math_tex(self.CONFIG["tex"]["num_1"])
# 最終的な式 "x = 1" のレイアウトを仮計算 (各要素の目標位置を知るため)
temp_final_group_layout = VGroup(final_x_obj.copy(), s4_eq_term.copy(), final_1_obj.copy()).arrange(RIGHT, buff=self.CONFIG["buffers"]["equation"]).move_to(ORIGIN)
# 各要素の目標位置を取得
target_x_pos = temp_final_group_layout[0].get_center()
target_eq_pos = temp_final_group_layout[1].get_center()
target_1_pos = temp_final_group_layout[2].get_center()
# 最終結果オブジェクトを目標位置に移動 (アニメーション前の準備)
final_x_obj.move_to(target_x_pos)
final_1_obj.move_to(target_1_pos)
# 約分で消える部分 (分子の9、分母の9、線) をグループ化 (左右)
lhs_cancel_parts = VGroup(num_9_lhs_obj, den_9_lhs_obj, line_lhs)
rhs_cancel_parts = VGroup(s4_9_rhs_obj, den_9_rhs_obj, line_rhs)
# アニメーション: 約分を実行
self.play(
# 約分される "9" を Indicate (点滅) で強調 (左右の分子・分母)
Indicate(num_9_lhs_obj, color=self.CONFIG["colors"]["cancellation"]), Indicate(den_9_lhs_obj, color=self.CONFIG["colors"]["cancellation"]),
# 左辺のキャンセル部分をフェードアウト
FadeOut(lhs_cancel_parts),
# 左辺の残った "x" を最終的な "x" に変形 (位置は既に合っているはず)
ReplacementTransform(num_x_lhs_obj, final_x_obj),
# 右辺の "9" を Indicate で強調 (分子・分母)
Indicate(s4_9_rhs_obj, color=self.CONFIG["colors"]["cancellation"]), Indicate(den_9_rhs_obj, color=self.CONFIG["colors"]["cancellation"]),
# 右辺のキャンセル部分をフェードアウト
FadeOut(rhs_cancel_parts),
# 最終結果の "1" をフェードイン
FadeIn(final_1_obj),
# "=" を最終的な目標位置に移動
s4_eq_term.animate.move_to(target_eq_pos),
run_time=division_anim_time * cancellation_ratio # 全体時間に対する比率で時間を設定
)
# 短い待機時間
self.wait(self.CONFIG["times"]["inter_step_wait"])
# ステップ5の最終的な式グループ ("x", "=", "1") を作成
eq5_group = VGroup(final_x_obj, s4_eq_term, final_1_obj)
# デフォルトの待機時間
self.wait(self.CONFIG["times"]["default_wait"])
# ステップ5の式グループを返す
return eq5_group
# 最終結論を表示するメソッド
def _display_conclusion(self, final_equation_group_at_origin, persistent_premise_display):
"""
引数:
final_equation_group_at_origin: 中央に表示されている最終結果 "x = 1" (VGroup)
persistent_premise_display: 画面隅に表示されている前提 "x = 0.999..." (VGroup)
"""
# 隅の前提 VGroup から各要素を取得 (persistent_premise_displayはTransformされている可能性があるため、最新の状態を参照)
# 注意: persistent_premise_display は Transform された後のオブジェクトを指している
prem_x_persist, prem_eq_persist, prem_0999_persist = persistent_premise_display.submobjects
# 中央の結果 VGroup から各要素を取得
final_x, final_eq, final_1 = final_equation_group_at_origin.submobjects
# 結論用の大きなフォントサイズのオブジェクトを作成
concl_0999 = self._create_styled_math_tex(self.CONFIG["tex"]["num_0_999"], font_size=self.CONFIG["font_sizes"]["conclusion"])
concl_eq = self._create_styled_math_tex(self.CONFIG["tex"]["equals"], font_size=self.CONFIG["font_sizes"]["conclusion"])
concl_1 = self._create_styled_math_tex(self.CONFIG["tex"]["num_1"], font_size=self.CONFIG["font_sizes"]["conclusion"])
# 結論の目標レイアウト (中央揃え) を作成
target_conclusion_layout = VGroup(concl_0999, concl_eq, concl_1).arrange(RIGHT, buff=self.CONFIG["buffers"]["equation"]).move_to(ORIGIN)
# アニメーション: 隅の前提と中央の結果から、中央の結論を形成
self.play(
# 隅の "x" と "=" を上にスライドしながらフェードアウト
FadeOut(prem_x_persist, shift=UP*0.5),
FadeOut(prem_eq_persist, shift=UP*0.5),
# 隅の "0.999..." を結論の "0.999..." に変形 (位置とサイズが変わる)
Transform(prem_0999_persist, concl_0999.move_to(target_conclusion_layout[0])),
# 中央の "x = 1" の "x" をフェードアウト
FadeOut(final_x),
# 中央の "=" を結論の "=" に変形 (位置とサイズが変わる)
Transform(final_eq, concl_eq.move_to(target_conclusion_layout[1])),
# 中央の "1" を結論の "1" に変形 (位置とサイズが変わる)
Transform(final_1, concl_1.move_to(target_conclusion_layout[2])),
run_time=self.CONFIG["times"]["conclusion_anim"] # 結論アニメーションの時間
)
# デフォルトの待機時間
self.wait(self.CONFIG["times"]["default_wait"])
# 結論として画面に残っているオブジェクト (Transform後のもの) でグループを作成
conclusion_on_screen = VGroup(prem_0999_persist, final_eq, final_1)
# 結論を囲む矩形を作成
rect = SurroundingRectangle(conclusion_on_screen,
color=self.CONFIG["colors"]["conclusion_rect"],
buff=self.CONFIG["buffers"]["conclusion_rect"])
# アニメーション: 矩形を表示
self.play(Create(rect))
# 結論表示後の特別な待機時間
self.wait(self.CONCLUSION_WAIT_TIME)
# アニメーション: 結論と矩形をフェードアウト
self.play(FadeOut(conclusion_on_screen), FadeOut(rect))
# 終了前の短い待機
self.wait()
# ==================
# Main Construct Method (メインのシーン構築メソッド)
# ==================
def construct(self):
# 0. タイトルを表示して消す
self._display_title()
# 1. 最初の前提「x = 0.999...」を中央に表示
premise_at_center = self._introduce_premise_center()
# 2. 中央の前提を隅に移動させ、永続表示オブジェクトとして設定
self._setup_persistent_premise(premise_at_center)
# self.persistent_premise_display がここで設定される
# 3. メインの式変形フローのための最初の前提「x = 0.999...」を中央に(再)表示
eq_s1_main = self._prepare_main_flow_premise()
# 4. 式変形ステップを順次実行
eq_s2 = self._animate_step1_to_step2(eq_s1_main) # 10倍
eq_s3 = self._animate_step2_to_step3(eq_s2) # 引き算導入
eq_s4 = self._animate_step3_to_step4(eq_s3) # 引き算実行 (9x = 9)
eq_s5 = self._animate_step4_to_step5(eq_s4) # 9で割る (x = 1)
# 5. 最終結論を表示 (中央の結果と隅の前提を利用)
self._display_conclusion(eq_s5, self.persistent_premise_display)
1. MathTex と VGroup
Manimの大きな魅力の一つは、LaTeXを使って美しい数式を簡単に描画できる点です。これを実現するのがMathTex
クラスです。例えば、x_obj = MathTex("x")
のように記述するだけで、数式の"x"を表すオブジェクトが生成されます。さらに、複数のMathTex
オブジェクト(例: "x", "=", "0.999...")をVGroup
クラスでひとまとめにすることで、方程式全体としての操作が格段に楽になります。VGroup
に対して.arrange()
メソッドを使えば要素を一括で整列させたり、アニメーションの対象としてまとめて扱ったりすることが可能です。
2. .animate構文
オブジェクトの位置やスタイルをアニメーションさせたい場合、Manimの.animate
構文が強力です。例えば、my_object.animate.shift(RIGHT)
と記述するだけで、my_object
が右にスライドするアニメーションが生成されます。
3. Transform / ReplacementTransform
数式が別の数式に変化する様子や、図形が形を変えるモーフィングアニメーションは、視覚的な理解を深めるのに効果的です。ManimではTransform
クラスがこれを担います。Transform(object_A, object_B)
と記述すると、object_A
がobject_B
の形やスタイルへと滑らかに変化します。また、元のオブジェクトを消しつつ新しいオブジェクトに変形させたい場合はReplacementTransform
が便利です。
おわりに
この記事では、0.999... = 1
という数学の証明を題材に、Manimライブラリを用いた数式アニメーションの作成プロセスと、その具体的なPythonコードをご紹介しました。
最後までお読みいただき、ありがとうございました。