0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python(Manim)による積分アニメーション

Last updated at Posted at 2025-04-12

ご注意
数学的な厳密性やManimの最適な利用方法に関して、改善の余地が含まれている可能性がございます。

完成したアニメーション

この記事の手順を実行すると、以下のようなリーマン和から定積分への遷移を視覚化するアニメーションが生成されます。

この動画は、定積分(グラフ下の面積)の概念を視覚化したものです。
細い長方形の集まり(リーマン和)で面積を近似し、分割数を無限に増やすことで、リーマン和が真の面積(定積分)へと収束する過程を描いています。

実行環境:Google Colab と Manim

このアニメーションを作成するために、以下のツールを使用しました。

  • Google Colaboratory (Colab):
    • Googleが提供する無料のクラウドベースのJupyter Notebook環境。
    • ブラウザ上でPythonコードを実行できます。
  • Manim:
    • Pythonで高品質な数学アニメーションを作成するためのライブラリ。
    • 数式を美しく表示したり、グラフや図形をプログラム制御で滑らかに動かしたりすることに長けています。

Colabの手軽さとManimの表現力を組み合わせることで、数学の概念を視覚化するアニメーション制作を効率的に進めることができます。

実行コード

以下に、Google Colabでこの積分アニメーションを作成するための手順を示します。

1. 環境構築

Colabノートブックを開き、最初のセルでManimとその動作に必要なライブラリをインストールします。

# パッケージリストを最新の状態に更新します。
!sudo apt update

# Manimの動作に必要なシステムライブラリをインストールします。
# - libcairo2-dev: グラフィック描画ライブラリCairoの開発用ファイル
# - ffmpeg: 動画や音声の処理・変換ツール
# - texlive, texlive-latex-extra, texlive-latex-recommended: 数式表示のための組版システムLaTeX関連
# - libpango1.0-dev: テキストレイアウトを行うライブラリPangoの開発用ファイル
!sudo apt install -y libcairo2-dev ffmpeg \
  texlive texlive-latex-extra \
  texlive-latex-recommended \
  libpango1.0-dev

# Pythonの数学アニメーションライブラリManimをインストールします。
!pip install manim

2. アニメーションコードの作成

次に、アニメーションのロジックを記述したPythonコードを integrals_scene.py というファイル名でColab環境内に保存します。以下のセルを実行してください。

%%writefile は、セル内のテキストを指定したファイル名で保存するColabのマジックコマンドです。

ご注意: 本記事で提供しているPythonコード内のテキスト(タイトル、ラベル等)は英語表記です。冒頭の完成動画サンプルは、説明のため日本語に修正したバージョンを掲載しています。



%%writefile integrals_scene.py
# manimライブラリから全てのクラス、関数、定数をインポート
from manim import *
# 数値計算ライブラリnumpyをnpという別名でインポート
import numpy as np

# グローバル変数として日本語フォント名を定義(通常はNone、必要に応じてフォント名を設定)
JP_FONT_GLOBAL = None

# ManimのSceneクラスを継承して、積分を説明するアニメーションシーンクラスを定義
class IntegralsExplained(Scene):
    # 設定変数をインスタンス属性としてセットアップするメソッドを定義
    def setup_config(self):
        # --- フォント設定 ---
        # 日本語フォントがグローバルで設定されていればそれを使用、なければ"serif"を使用
        _font_name = JP_FONT_GLOBAL if JP_FONT_GLOBAL else "serif" 
        # インスタンス属性としてローカルフォント名を保存
        self.local_font = _font_name

        # --- 色設定 ---
        # アニメーションで使用する各要素の色を辞書で定義
        self.colors = {
            # タイトルの色をBLUE_Dに設定
            "title": BLUE_D,
            # 座標軸の色をBLUE_Cに設定
            "axes": BLUE_C,
            # グラフの色をYELLOW_Cに設定
            "graph": YELLOW_C,
            # 垂直線の色をBLUE_Bに設定
            "vline": BLUE_B,
            # 軸ラベルの色をBLUE_Cに設定
            "axis_label": BLUE_C,
            # リーマン和前の面積の色をGREEN_DとGREEN_Eのリストに設定
            "area": [GREEN_D, GREEN_E],
            # 最終的な積分エリアの色をGREEN_CとGREEN_Dのリストに設定
            "area_final": [GREEN_C, GREEN_D],
            # リーマン和の矩形の色をBLUE_DとGREEN_Dのリストに設定
            "riemann": [BLUE_D, GREEN_D],
            # リーマン和の矩形の境界線の色をGREY_Bに設定
            "riemann_stroke": GREY_B,
            # 分割数テキストの色をYELLOW_Cに設定
            "subdivision_text": YELLOW_C,
            # 数式の色をWHITEに設定
            "formula": WHITE,
            # 強調表示の色をYELLOW_Cに設定
            "highlight": YELLOW_C,
        }

        # --- スケール(大きさ)と不透明度の設定 ---
        # 各要素のスケール(拡大率)を辞書で定義
        self.scales = {
            # タイトルのスケールを1.0に設定
            "title": 1.0,
            # 面積テキストのスケールを0.8に設定
            "area_text": 0.8,
            # 分割数テキストのスケールを0.7に設定
            "subdivision_text": 0.7,
            # 数式のスケールを0.65に設定
            "formula": 0.65,
            # 最終的な数式のスケールに乗算する係数を1.05に設定
            "formula_final_factor": 1.05,
        }
        # 各要素の不透明度を辞書で定義
        self.opacities = {
            # リーマン和前の面積の不透明度を0.6に設定
            "area": 0.6,
            # 最終的な積分エリアの不透明度を0.75に設定
            "area_final": 0.75,
            # リーマン和の矩形の不透明度を0.7に設定
            "riemann": 0.7,
        }

        # --- タイミング設定 ---
        # 各アニメーションや待機時間の長さを辞書で定義
        self.timing = {
            # 待機時間の設定
            "wait": {
                # タイトル表示後の待機時間を0.8秒に設定
                "title": 0.8,
                # グラフ表示後の待機時間を0.5秒に設定
                "graph": 0.5,
                # 面積定義表示後の待機時間を1.0秒に設定
                "area_def": 1.0,
                # 面積テキストフェードアウト後の待機時間を0.7秒に設定
                "area_text_fade": 0.7,
                # リーマン和導入後の待機時間を1.0秒に設定
                "riemann_intro": 1.0,
                # リーマン和更新後の待機時間を0.6秒に設定
                "riemann_update": 0.6,
                # 積分への遷移後の待機時間を0.8秒に設定
                "integral_transition": 0.8,
                # 最終的な数式のハイライト後の待機時間を1.8秒に設定
                "final_highlight": 1.8,
                # クリーンアップ前の待機時間を0.5秒に設定
                "cleanup": 0.5,
                # シーン終了前の待機時間を0.8秒に設定
                "final_scene": 0.8,
            },
            # アニメーションの再生時間の設定
            "anim": {
                # タイトル書き込みアニメーションの時間を0.6秒に設定
                "title_write": 0.6,
                # 座標軸作成アニメーションの時間を2.2秒に設定
                "axes_create": 2.2,
                # グラフ作成アニメーションの時間を1.0秒に設定
                "graph_create": 1.0,
                # 面積表示アニメーションの時間を1.5秒に設定
                "area_show": 1.5,
                # 面積テキストフェードアウトアニメーションの時間を0.5秒に設定
                "area_text_fade_out": 0.5,
                # 初期のリーマン和表示アニメーションの時間を1.5秒に設定
                "riemann_initial": 1.5,
                # リーマン和更新アニメーションの時間を0.8秒に設定
                "riemann_update": 0.8,
                # 積分への遷移アニメーション(FadeOut/FadeIn)の時間を1.5秒に設定
                "integral_transition": 1.5,
                # 最終的な数式の囲み枠作成アニメーションの時間を0.5秒に設定
                "final_box": 0.5,
                # クリーンアップ(フェードアウト)アニメーションの時間を1.5秒に設定
                "cleanup": 1.5,
            }
        }

        # --- 座標軸とグラフの設定 ---
        # 座標軸の設定を辞書で定義
        self.axes_config = {
            # x軸の範囲を0から5まで、目盛り間隔1に設定
            "x_range": [0, 5, 1],
            # y軸の範囲を0から7まで、目盛り間隔1に設定
            "y_range": [0, 7, 1],
            # x軸の長さを7ユニットに設定
            "x_length": 7,
            # y軸の長さを4.5ユニットに設定
            "y_length": 4.5,
            # 軸共通の設定: 矢印を含め、色を座標軸の色に、数値ラベルは非表示に設定
            "axis_config": {"include_tip": True, "color": self.colors["axes"], "include_numbers": False},
            # x軸固有の設定: 数値ラベルを非表示に設定
            "x_axis_config": {"include_numbers": False},
            # y軸固有の設定: 数値ラベルを非表示に設定
            "y_axis_config": {"include_numbers": False},
        }
        # グラフの設定を辞書で定義
        self.graph_config = {"color": self.colors["graph"], "x_range": [0, 5], "use_smoothing": True}
        # グラフのラベル設定を辞書で定義
        self.graph_label_config = {
            # ラベルのテキストを "f(x)" に設定
            "label": "f(x)",
            # ラベルの色をグラフの色に設定
            "color": self.colors["graph"],
            # ラベルを配置するx座標を4.5に設定
            "x_val": 4.5,
            # ラベルの位置をグラフ上の点から上方向2.5ユニットに設定
            "direction_vec": UP * 2.5
        }

        # --- 積分と面積の設定 ---
        # 積分の範囲を辞書で定義 (a=1.0, b=4.5)
        self.integral_range = {"a": 1.0, "b": 4.5}
        # 垂直線の設定を辞書で定義
        self.vline_config = {"color": self.colors["vline"], "stroke_width": 2, "line_func": DashedLine}
        # 軸ラベル(a, b)の設定を辞書で定義
        self.axis_label_config = {"color": self.colors["axis_label"], "buff": 0.2}
        # リーマン和前の面積オブジェクトの設定を辞書で定義
        self.area_config = {"color": self.colors["area"], "opacity": self.opacities["area"]}
        # 面積テキスト ("Area = ?") の設定を辞書で定義
        self.area_text_config = {
            # テキストの内容を設定
            "text": "Area = ?",
            # テキストの色を設定
            "color": self.colors["formula"],
            # テキストのスケールを設定
            "scale": self.scales["area_text"],
            # テキストの位置を面積オブジェクトの上、少し離れた場所に設定
            "position_args": {"direction": UP, "buff": SMALL_BUFF}
        }
        # 最終的な積分エリアオブジェクトの設定を辞書で定義
        self.final_area_config = {"color": self.colors["area_final"], "opacity": self.opacities["area_final"]}

        # --- リーマン和の設定 ---
        # リーマン和の矩形の設定を辞書で定義
        self.riemann_config = {
            # 矩形の色を設定
            "color": self.colors["riemann"],
            # 矩形の境界線の太さを設定
            "stroke_width": 1.5,
            # 矩形の境界線の色を設定
            "stroke_color": self.colors["riemann_stroke"],
            # 矩形の塗りつぶしの不透明度を設定
            "fill_opacity": self.opacities["riemann"],
            # 更新後の矩形の境界線の太さを設定
            "updated_stroke_width": 0.25,
            # リーマン和の計算方法(左端、右端、中央)を左端 ("left") に設定
            "input_sample_type": "left"
        }
        # リーマン和のアニメーションで使用する分割数のリスト (5, 15, 40, 100)
        self.subdivision_counts = [5, 15, 40, 100] 
        # 分割数テキストの位置を左上隅から少し下にずらした位置に設定
        self.subdivision_text_pos = {"corner": UL, "shift_vec": DOWN * 1.5 + RIGHT * 0.5}

        # --- 数式の設定 ---
        # 数式の共通設定を辞書で定義
        self.formula_config = {
            # 数式の色を設定
            "color": self.colors["formula"],
            # 数式のスケールを設定
            "scale": self.scales["formula"],
            # 数式の位置を画面下端、少し上に設定
            "pos_formula": {"edge": DOWN, "buff": MED_SMALL_BUFF},
        }
        # 最終的な数式を囲むハイライトボックスの設定を辞書で定義
        self.highlight_box_config = {"color": self.colors["highlight"], "buff": 0.1}

        # --- 関数定義 ---
        # アニメーションで使用する関数 f(x) = 2 * sin(x * pi / 2.5) + 3 をラムダ式で定義
        self.func = lambda x: 2 * np.sin(x * PI / 2.5) + 3

        # --- オブジェクト参照 (Noneで初期化) ---
        # アニメーション中に作成される各オブジェクトへの参照を保持する属性を初期化
        # タイトルオブジェクトの参照
        self.title_obj = None
        # 座標軸オブジェクトの参照
        self.axes = None
        # グラフオブジェクトの参照
        self.graph = None
        # グラフラベルオブジェクトの参照
        self.graph_label = None
        # 積分区間始点aを示す垂直線の参照
        self.vert_line_a = None
        # 積分区間終点bを示す垂直線の参照
        self.vert_line_b = None
        # ラベル 'a' の参照
        self.label_a = None
        # ラベル 'b' の参照
        self.label_b = None
        # 最初の面積オブジェクト(リーマン和前)の参照
        self.area_obj = None
        # 面積テキスト ("Area = ?") オブジェクトの参照
        self.area_text_obj = None
        # 分割数テキストオブジェクトの参照
        self.subdivision_text = None
        # 最後に表示された(リーマン和の)数式オブジェクトの参照
        self.last_formula = None
        # 現在表示されているリーマン和の矩形群の参照
        self.current_rects = None
        # 最終的な積分エリアオブジェクトの参照
        self.final_area_obj = None
        # 積分記号を使った最終的な数式オブジェクトの参照
        self.integral_formula = None
        # 最終的な数式を囲むハイライトボックスの参照
        self.final_formula_box = None


    # --- ヘルパーメソッド ---
    # 座標軸の数値ラベルを非表示にする(小さくスケールする)ヘルパーメソッド
    def _hide_axes_numbers(self, axes):
        # 非表示にする数値ラベルを格納するVGroup(Manimのグループオブジェクト)を作成
        nums_to_hide = VGroup()
        # 座標軸オブジェクトのx軸から数値ラベルを取得(存在しない場合はNone)
        x_nums = getattr(axes.x_axis, 'numbers', None)
        # 座標軸オブジェクトのy軸から数値ラベルを取得(存在しない場合はNone)
        y_nums = getattr(axes.y_axis, 'numbers', None)
        # x軸の数値ラベルが存在すれば、非表示グループに追加
        if x_nums: nums_to_hide.add(*x_nums)
        # y軸の数値ラベルが存在すれば、非表示グループに追加
        if y_nums: nums_to_hide.add(*y_nums)
        # 非表示グループにオブジェクトがあれば、非常に小さくスケールして見えなくする
        if nums_to_hide: nums_to_hide.scale(1e-9)

    # 分割数を示すテキストオブジェクトを作成するヘルパーメソッド
    def _create_subdivision_text(self, text_content):
        # 指定されたテキスト内容でMathTex(数式用テキスト)オブジェクトを作成
        return ( 
            MathTex(
                # テキストの内容(例: "N = 5")
                text_content,
                # テキストの色を設定
                color=self.colors["subdivision_text"],
            ) 
            # テキストのスケールを設定 
            .scale(self.scales["subdivision_text"])
            # テキストを指定されたコーナー(左上)に配置 
            .to_corner(self.subdivision_text_pos["corner"])
            # 設定されたオフセットベクトル分だけ位置をシフト 
            .shift(self.subdivision_text_pos["shift_vec"])
        ) 

    # --- アニメーションステップメソッド ---
    # タイトルを表示するメソッド
    def _show_title(self):
        # Textオブジェクトでタイトルを作成(フォント、色、スケールを設定)
        self.title_obj = ( 
            Text(
                # タイトルの文字列
                "Integrals Explained",
                # 使用するフォント
                font=self.local_font,
                # タイトルの色
                color=self.colors["title"]
            ) 
            # タイトルのスケールを設定 
            .scale(self.scales["title"])
        ) 
        # タイトルを画面の上端に配置
        self.title_obj.to_edge(UP)
        # タイトルをWriteアニメーション(書き込むような表示)で表示し、再生時間を設定
        self.play(Write(self.title_obj), run_time=self.timing["anim"]["title_write"])
        # 設定された時間だけ待機
        self.wait(self.timing["wait"]["title"])

    # シーンの基本的な要素(座標軸、グラフ)をセットアップするメソッド
    def _setup_scene(self):
        # 設定に基づいて座標軸オブジェクトを作成し、座標値を追加
        self.axes = Axes(**self.axes_config).add_coordinates()
        # 座標軸をタイトルの下に、指定したバッファ(間隔)を空けて配置
        self.axes.next_to(self.title_obj, DOWN, buff=MED_SMALL_BUFF)
        # ヘルパーメソッドを呼び出して座標軸の数値を非表示にする
        self._hide_axes_numbers(self.axes)
        # 設定に基づいて関数グラフオブジェクトを作成
        self.graph = self.axes.plot(self.func, **self.graph_config)
        # グラフのラベル ("f(x)") を作成し、設定に従って配置
        self.graph_label = self.axes.get_graph_label(
            # ラベルを付ける対象のグラフ
            self.graph,
            # ラベルのテキスト
            label=self.graph_label_config["label"],
            # ラベルの色
            color=self.graph_label_config["color"],
            # ラベルを配置するx座標
            x_val=self.graph_label_config["x_val"],
            # ラベルの配置方向(グラフ上の点からの相対位置)
            direction=self.graph_label_config["direction_vec"]
        )
        # 座標軸をCreateアニメーション(徐々に描画される表示)で表示し、再生時間を設定
        self.play(Create(self.axes), run_time=self.timing["anim"]["axes_create"])
        # グラフをCreateアニメーションで、グラフラベルをWriteアニメーションで同時に表示し、再生時間を設定
        self.play(Create(self.graph), Write(self.graph_label), run_time=self.timing["anim"]["graph_create"])
        # 設定された時間だけ待機
        self.wait(self.timing["wait"]["graph"])

    # 積分区間と面積の定義を表示するメソッド
    def _show_area_definition(self):
        # 積分区間の始点 a を取得
        a = self.integral_range["a"]
        # 積分区間の終点 b を取得
        b = self.integral_range["b"]
        # グラフとx軸、垂直線a, bで囲まれた領域の面積オブジェクトを作成
        self.area_obj = self.axes.get_area(self.graph, x_range=(a, b), **self.area_config)
        # 始点 a における垂直線(破線)オブジェクトを作成
        self.vert_line_a = self.axes.get_vertical_line(self.axes.i2gp(a, self.graph), **self.vline_config)
        # 終点 b における垂直線(破線)オブジェクトを作成
        self.vert_line_b = self.axes.get_vertical_line(self.axes.i2gp(b, self.graph), **self.vline_config)
        # ラベル "a" のMathTexオブジェクトを作成
        label_a_tex = MathTex("a", color=self.axis_label_config["color"])
        # ラベル "b" のMathTexオブジェクトを作成
        label_b_tex = MathTex("b", color=self.axis_label_config["color"])
        # 垂直線 a の下端の座標を取得
        vert_line_a_bottom = self.vert_line_a.get_bottom()
        # 垂直線 b の下端の座標を取得
        vert_line_b_bottom = self.vert_line_b.get_bottom()
        # ラベル "a" を垂直線 a の下端の下に配置
        self.label_a = label_a_tex.next_to(vert_line_a_bottom, DOWN, buff=self.axis_label_config["buff"])
        # ラベル "b" を垂直線 b の下端の下に配置
        self.label_b = label_b_tex.next_to(vert_line_b_bottom, DOWN, buff=self.axis_label_config["buff"])
        # "Area = ?" テキストのMathTexオブジェクトを作成
        self.area_text_obj = ( 
            MathTex(
                # テキストの内容
                self.area_text_config["text"],
                # テキストの色
                color=self.area_text_config["color"]
            ) 
            # テキストのスケールを設定 
            .scale(self.area_text_config["scale"])
        ) 
        # "Area = ?" テキストを面積オブジェクトの上に配置
        self.area_text_obj.next_to(
            # 基準となる面積オブジェクト
            self.area_obj,
            # 配置方向(上)
            self.area_text_config["position_args"]["direction"],
            # バッファ(間隔)
            buff=self.area_text_config["position_args"]["buff"]
        )
        # 垂直線a, b、ラベルa, b、面積オブジェクト、面積テキストを同時にアニメーション表示
        self.play(
            # 垂直線aをCreateアニメーションで表示
            Create(self.vert_line_a),
            # 垂直線bをCreateアニメーションで表示
            Create(self.vert_line_b),
            # ラベルaをWriteアニメーションで表示
            Write(self.label_a),
            # ラベルbをWriteアニメーションで表示
            Write(self.label_b),
            # 面積オブジェクトをFadeIn(徐々に現れる)アニメーションで、少し拡大しながら表示
            FadeIn(self.area_obj, scale=0.5),
            # 面積テキストをWriteアニメーションで表示
            Write(self.area_text_obj),
            # アニメーション全体の再生時間を設定
            run_time=self.timing["anim"]["area_show"]
        )
        # 設定された時間だけ待機
        self.wait(self.timing["wait"]["area_def"])
        # 面積テキスト ("Area = ?") をFadeOutアニメーション(徐々に消える)で非表示
        self.play(FadeOut(self.area_text_obj), run_time=self.timing["anim"]["area_text_fade_out"])
        # 設定された時間だけ待機
        self.wait(self.timing["wait"]["area_text_fade"])

    # リーマン和のアニメーション(分割数を増やしていく)を実行するメソッド
    def _animate_riemann_sums(self):
        # 積分区間の始点 a を取得
        a = self.integral_range["a"]
        # 積分区間の終点 b を取得
        b = self.integral_range["b"]
        # 最初の分割数をリストから取得
        n = self.subdivision_counts[0]
        # 最初の分割数を示すテキストオブジェクトを作成(ヘルパーメソッド使用)
        self.subdivision_text = self._create_subdivision_text(f"N = {n}")
        # リーマン和の矩形設定をコピー(元の辞書を変更しないため)
        current_riemann_kwargs = self.riemann_config.copy()
        # 更新後の線の太さの設定は初回には不要なので削除
        current_riemann_kwargs.pop("updated_stroke_width", None)
        # 最初のリーマン和の矩形群オブジェクトを作成
        self.current_rects = self.axes.get_riemann_rectangles(
            # 対象のグラフ
            self.graph,
            # x軸の範囲
            x_range=(a, b),
            # 各矩形の幅 (dx)
            dx=(b - a) / n,
            # その他の矩形設定(色、不透明度など)
            **current_riemann_kwargs
        )
        # 数式を配置する位置の設定を取得
        pos_config = self.formula_config["pos_formula"]
        # 最初のリーマン和の近似式を表すMathTexオブジェクトを作成
        self.last_formula = ( 
            MathTex(
                # LaTeX形式の数式文字列(Area ≈ Σ f(x*) Δx)
                fr"\text{{Area}} \approx \sum_{{i=1}}^{{{n}}} f(x_i^*) \Delta x",
                # 数式の色を設定
                color=self.formula_config["color"]
            ) 
            # 数式のスケールを設定 
            .scale(self.scales["formula"])
            # 数式を画面下端に配置 
            .to_edge(pos_config["edge"], buff=pos_config["buff"])
        )

        # 初期のリーマン和関連オブジェクトをアニメーション表示
        self.play(
            # 元の面積オブジェクトをフェードアウト
            FadeOut(self.area_obj),
            # 分割数テキストをフェードイン
            FadeIn(self.subdivision_text),
            # リーマン和の矩形群をCreateアニメーションで表示
            Create(self.current_rects),
            # リーマン和の近似式をフェードイン
            FadeIn(self.last_formula),
            # アニメーション全体の再生時間を設定
            run_time=self.timing["anim"]["riemann_initial"]
        )
        # 設定された時間だけ待機
        self.wait(self.timing["wait"]["riemann_intro"])

        # 分割数のリストの2番目以降の要素についてループ処理
        for i in range(1, len(self.subdivision_counts)):
            # 新しい分割数をリストから取得
            n = self.subdivision_counts[i]
            # 新しい分割数を示すテキストオブジェクトを作成
            new_subdivision_text = self._create_subdivision_text(f"N = {n}")
            # リーマン和の矩形設定をコピー
            updated_riemann_kwargs = self.riemann_config.copy()
            # 矩形の境界線の太さを更新後の値に設定
            updated_riemann_kwargs["stroke_width"] = self.riemann_config["updated_stroke_width"]
            # 不要になった更新後線の太さの設定キーを削除
            updated_riemann_kwargs.pop("updated_stroke_width", None)
            # 新しい分割数に基づいたリーマン和の矩形群オブジェクトを作成
            new_riemann_rects = self.axes.get_riemann_rectangles(
                # 対象のグラフ
                self.graph,
                # x軸の範囲
                x_range=(a, b),
                # 新しい矩形の幅 (dx)
                dx=(b - a) / n,
                # 更新された矩形設定
                **updated_riemann_kwargs
            )
            # 新しい分割数を含むリーマン和の近似式を表すMathTexオブジェクトを作成
            new_formula = ( 
                MathTex(
                    # LaTeX形式の数式文字列(nを更新)
                    fr"\text{{Area}} \approx \sum_{{i=1}}^{{{n}}} f(x_i^*) \Delta x",
                    # 数式の色を設定
                    color=self.formula_config["color"]
                ) # << MathTexの括弧を閉じる
                # 数式のスケールを設定 
                .scale(self.scales["formula"])
                # 数式を画面下端に配置 
                .to_edge(pos_config["edge"], buff=pos_config["buff"])
            ) 

            # リーマン和の矩形、分割数テキスト、近似式を同時に更新するアニメーションを実行
            self.play(
                # 現在の矩形群を新しい矩形群に置き換え変形 (ReplacementTransform)
                ReplacementTransform(self.current_rects, new_riemann_rects),
                # 現在の分割数テキストを新しいテキストに変形 (Transform)
                Transform(self.subdivision_text, new_subdivision_text),
                # 現在の近似式を新しい近似式に置き換え変形 (ReplacementTransform)
                ReplacementTransform(self.last_formula, new_formula),
                # アニメーション全体の再生時間を設定
                run_time=self.timing["anim"]["riemann_update"]
            )
            # 現在の矩形群の参照を新しいものに更新
            self.current_rects = new_riemann_rects
            # 現在の数式の参照を新しいものに更新
            self.last_formula = new_formula
            # 設定された時間だけ待機
            self.wait(self.timing["wait"]["riemann_update"])

    # リーマン和から定積分へと遷移するアニメーションを実行するメソッド
    def _transition_to_integral(self):
        # 積分区間の始点 a を取得
        a = self.integral_range["a"]
        # 積分区間の終点 b を取得
        b = self.integral_range["b"]
        # "N → ∞" を示すテキストオブジェクトを作成
        infinity_text = self._create_subdivision_text("N \\to \\infty")
        # 最終的な積分エリアオブジェクトを事前に作成(FadeInアニメーション用)
        self.final_area_obj = self.axes.get_area(self.graph, x_range=(a, b), **self.final_area_config)
        # 最終的な数式のスケールを計算(基本スケールに係数を乗算)
        final_formula_scale = self.scales["formula"] * self.scales["formula_final_factor"]
        # 数式を配置する位置の設定を取得
        pos_config = self.formula_config["pos_formula"]
        # 定積分の数式を表すMathTexオブジェクトを作成
        self.integral_formula = ( 
            MathTex(
                # LaTeX形式の定積分式 (Area = ∫[a to b] f(x) dx)
                r"\text{Area} = \int_{a}^{b} f(x) \, dx",
                # 数式の色を設定
                color=self.formula_config["color"]
            ) 
            # 計算された最終スケールを設定 
            .scale(final_formula_scale)
            # 数式を画面下端に配置 
            .to_edge(pos_config["edge"], buff=pos_config["buff"])
        ) 

        # リーマン和から積分への遷移アニメーション (ReplacementTransform から FadeOut/FadeIn へ変更)
        self.play(
            # 現在のリーマン和の矩形群をフェードアウト
            FadeOut(self.current_rects),
            # 最終的な積分エリアオブジェクトをフェードイン
            FadeIn(self.final_area_obj),
            # 分割数テキストを "N → ∞" テキストに変形 (Transform)
            Transform(self.subdivision_text, infinity_text),
            # リーマン和の近似式を定積分の式に置き換え変形 (ReplacementTransform)
            ReplacementTransform(self.last_formula, self.integral_formula),
            # アニメーション全体の再生時間を設定
            run_time=self.timing["anim"]["integral_transition"]
        )

        
        # 設定された時間だけ待機
        self.wait(self.timing["wait"]["integral_transition"])
        # 定積分式を囲むハイライトボックス(SurroundingRectangle)を作成
        self.final_formula_box = SurroundingRectangle(self.integral_formula, **self.highlight_box_config)
        # ハイライトボックスをCreateアニメーションで表示し、再生時間を設定
        self.play(Create(self.final_formula_box), run_time=self.timing["anim"]["final_box"])
        # 設定された時間だけ待機
        self.wait(self.timing["wait"]["final_highlight"])

    # シーンの最後にオブジェクトを消去するメソッド
    def _cleanup_scene(self):
        # フェードアウトさせるオブジェクトのリストを作成
        objects_to_fade = [
            # タイトル
            self.title_obj,
            # 座標軸
            self.axes,
            # グラフ
            self.graph,
            # グラフラベル
            self.graph_label,
            # 垂直線 a
            self.vert_line_a,
            # 垂直線 b
            self.vert_line_b,
            # ラベル a
            self.label_a,
            # ラベル b
            self.label_b,
            # 最終的な面積オブジェクト
            self.final_area_obj,
            # 分割数テキスト("N → ∞" になっている)
            self.subdivision_text,
            # 定積分式
            self.integral_formula,
            # 定積分式のハイライトボックス
            self.final_formula_box
        ]
        # リスト内のNoneでないオブジェクトだけをVGroupにまとめる
        mobjects_to_fade = VGroup(*[obj for obj in objects_to_fade if obj is not None])
        # フェードアウトさせるオブジェクトが存在する場合
        if mobjects_to_fade:
            # 設定された時間だけ待機
            self.wait(self.timing["wait"]["cleanup"])
            # VGroup内の全オブジェクトをFadeOutアニメーションで消去し、再生時間を設定
            self.play(FadeOut(mobjects_to_fade), run_time=self.timing["anim"]["cleanup"])

    # アニメーション全体の構成(実行順序)を定義するメソッド (Manimが自動的に呼び出す)
    def construct(self):
        # 設定を初期化するメソッドを呼び出し
        self.setup_config()
        # タイトルを表示するメソッドを呼び出し
        self._show_title()
        # 座標軸とグラフをセットアップするメソッドを呼び出し
        self._setup_scene()
        # 面積の定義(積分区間)を表示するメソッドを呼び出し
        self._show_area_definition()
        # リーマン和のアニメーションを実行するメソッドを呼び出し
        self._animate_riemann_sums()
        # リーマン和から積分へ遷移するメソッドを呼び出し
        self._transition_to_integral() # 修正されたメソッドが呼び出される
        # シーンのオブジェクトを消去するメソッドを呼び出し
        self._cleanup_scene()
        # シーン終了前に設定された時間だけ待機
        self.wait(self.timing["wait"]["final_scene"])

Axes.get_riemann_rectangles() について

ManimのAxes.get_riemann_rectangles()は、積分を理解する上で重要なリーマン和の短冊(矩形)群を驚くほど簡単に生成するメソッドです。グラフオブジェクト、積分範囲、短冊の幅(または分割数)、短冊の高さを決定するルール(左端、右端など)を指定するだけで、Manimが自動的に多数の矩形を描画してくれます。これにより、分割数を増やして積分に近づく様子や、異なるリーマン和のタイプを視覚的に比較するアニメーションを効率的に作成できます。

MathTex() について

ManimのMathTex()は、LaTeXの組版能力を利用して、アニメーション内に美しく正確な数式を埋め込むためのクラスです。r"LaTeX文字列"の形式で数式を記述すれば、積分記号、総和記号、分数、行列といった複雑な表現も高品質にレンダリングされます。

ReplacementTransform() について

ManimのReplacementTransform()は、あるオブジェクトが別のオブジェクトに「置き換わるように」滑らかに変化するアニメーションを実現する、Manimの強力なアニメーションクラスの一つです。self.play()メソッドの引数として使用され、アニメーション開始時には元のオブジェクトが表示され、終了時には新しいオブジェクトに完全に入れ替わり、元のオブジェクトはシーンから除去されます。数値の更新、数式の書き換え、あるいは図形が段階的に変化していく様子を表現するのに最適です。視覚的な連続性を保ちながら状態の遷移を効果的に見せられるため、複雑な概念をステップバイステップで解説する際に非常に役立ちます。

3. アニメーションの生成 (レンダリング)

integrals_scene.py ファイルを作成したら、以下のManimコマンドを実行して動画ファイルを生成します。

# Manimコマンドを実行してアニメーションをレンダリング
# オプション:
# -q: 品質指定
# -l: 低品質 (low quality)。
# !rm -rf media # 既存の出力ディレクトリ 'media' を削除
# !manim -qh integrals_scene.py IntegralsExplained # 高品質(-qh)でレンダリング
!manim -pql integrals_scene.py IntegralsExplained

このコマンドを実行すると、レンダリングが開始され、進行状況が表示されます。完了すると、指定した品質に応じたディレクトリ内に .mp4 ファイルが作成されます。

4. 生成された動画の表示

レンダリングが完了したら、以下のコードでColab上に動画を表示して確認します。

from IPython.display import Video, display

# 生成された動画ファイルのパス (スクリプト名とクラス名を確認)
video_path = "/content/media/videos/integrals_scene/480p15/IntegralsExplained.mp4"
# video_path = "/content/media/videos/integrals_scene/1080p60/IntegralsExplained.mp4" # 高品質(-qh)でレンダリングした場合
    

# 動画を表示 (ファイルが存在するか確認)
try:
    display(Video(video_path, embed=True, width=600))
except FileNotFoundError:
    print(f"エラー: 動画ファイルが見つかりません: {video_path}")
    print("ファイルパスが正しいか、Manimのレンダリングが正常に完了したか確認してください。")
    print("以下のコマンドで生成されたファイルを確認できます:")

追記

※20250504_「解の公式の導出」に関する動画も作成したので追加で掲載します

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?