1
1

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×MusicXml]

1
Last updated at Posted at 2026-05-24

はじめに

ということで、九九のうたをManimとMusicXmlで作成しました。

九九の表

Times_Table.png

Manimのコード

画面は以下を利用して作成しました。Youtube Shorts用のアスペクト比を設定しています。

%%writefile MultiplicationTable.py

from manim import *
import numpy as np

# ==========================================
# Youtube Shorts用のアスペクト比・解像度設定
# ==========================================
config.pixel_width = 1080
config.pixel_height = 1920
config.frame_width = 8.0
config.frame_height = 8.0 * (1920 / 1080)  # 約14.22

# ==========================================
# 定数・データ定義
# ==========================================
# カラー設定
BG_COLOR = "#FFF8E1"
TITLE_COLOR = "#FF6F00"
EQ_TEXT_COLOR = "#5D4037"
READ_TEXT_COLOR = "#039BE5"
ANS_TEXT_COLOR = "#FF1744"

# グリッド用カラー設定
HEADER_BG = "#FFE082"
HEADER_TEXT_COLOR = "#E65100"
GRID_STROKE_COLOR = "#BCAAA4"
ROW_COLORS = [
    WHITE,      # ヘッダー用
    "#FFCDD2",  # 1のだん
    "#F8BBD0",  # 2のだん
    "#E1BEE7",  # 3のだん
    "#D1C4E9",  # 4のだん
    "#C5CAE9",  # 5のだん
    "#BBDEFB",  # 6のだん
    "#B2EBF2",  # 7のだん
    "#C8E6C9",  # 8のだん
    "#FFF9C4"   # 9のだん
]

# ==========================================
# アニメーション速度・タイミング設定
# ==========================================
BPM = 117
BEAT = 60 / BPM
SPEED_RATE = 1.0  # 微調整用

# 各フェーズにかかる時間 (単位: ビート数)
INTRO_IN_BEATS   = 1.0  
INTRO_WAIT_BEATS = 5.2
INTRO_OUT_BEATS  = 1.0

  

EQ_IN_BEATS      = 1.0  
EQ_WAIT_BEATS    = 1.0  
ANS_IN_BEATS     = 1.0  
ANS_WAIT_BEATS   = 0.5  
STEP_CLEAR_BEATS = 0.5  

TABLE_CLEAR_BEATS = 2.0 

SUMMARY_IN_BEATS   = 2.0 
SUMMARY_WAIT_BEATS = 4.0 
SUMMARY_OUT_BEATS  = 2.0 

def sec(beats):
    return beats * BEAT * SPEED_RATE
    

# ==========================================
# テキスト・ふりがなデータ
# ==========================================
kuku_titles = {
    1: "1のだん", 2: "2のだん", 3: "3のだん",
    4: "4のだん", 5: "5のだん", 6: "6のだん",
    7: "7のだん", 8: "8のだん", 9: "9のだん"
}

kuku_readings = {
    1: [("いん", "いちが", "いち"), ("いん", "にが", ""), ("いん", "さんが", "さん"), ("いん", "しが", ""), ("いん", "ごが", ""), ("いん", "ろくが", "ろく"), ("いん", "しちが", "しち"), ("いん", "はちが", "はち"), ("いん", "くが", "")],
    2: [("", "いちが", ""), ("", "にんが", ""), ("", "さんが", "ろく"), ("", "しが", "はち"), ("", "", "じゅう"), ("", "ろく", "じゅうに"), ("", "しち", "じゅうし"), ("", "はち", "じゅうろく"), ("", "", "じゅうはち")],
    3: [("さん", "いちが", "さん"), ("さん", "にが", "ろく"), ("", "ざんが", ""), ("さん", "", "じゅうに"), ("さん", "", "じゅうご"), ("さぶ", "ろく", "じゅうはち"), ("さん", "しち", "にじゅういち"), ("さん", "", "にじゅうし"), ("さん", "", "にじゅうしち")],
    4: [("", "いちが", ""), ("", "にが", "はち"), ("", "さん", "じゅうに"), ("", "", "じゅうろく"), ("", "", "にじゅう"), ("", "ろく", "にじゅうし"), ("", "しち", "にじゅうはち"), ("", "", "さんじゅうに"), ("", "", "さんじゅうろく")],
    5: [("", "いちが", ""), ("", "", "じゅう"), ("", "さん", "じゅうご"), ("", "", "にじゅう"), ("", "", "にじゅうご"), ("", "ろく", "さんじゅう"), ("", "しち", "さんじゅうご"), ("", "", "しじゅう"), ("ごっ", "", "しじゅうご")],
    6: [("ろく", "いちが", "ろく"), ("ろく", "", "じゅうに"), ("ろく", "さん", "じゅうはち"), ("ろく", "", "にじゅうし"), ("ろく", "", "さんじゅう"), ("ろく", "ろく", "さんじゅうろく"), ("ろく", "しち", "しじゅうに"), ("ろく", "", "しじゅうはち"), ("ろっ", "", "ごじゅうし")],
    7: [("しち", "いちが", "しち"), ("しち", "", "じゅうし"), ("しち", "さん", "にじゅういち"), ("しち", "", "にじゅうはち"), ("しち", "", "さんじゅうご"), ("しち", "ろく", "しじゅうに"), ("しち", "しち", "しじゅうく"), ("しち", "", "ごじゅうろく"), ("しち", "", "ろくじゅうさん")],
    8: [("はち", "いちが", "はち"), ("はち", "", "じゅうろく"), ("はち", "さん", "にじゅうし"), ("はち", "", "さんじゅうに"), ("はち", "", "しじゅう"), ("はち", "ろく", "しじゅうはち"), ("はち", "しち", "ごじゅうろく"), ("はっ", "", "ろくじゅうし"), ("はっ", "", "しちじゅうに")],
    9: [("", "いちが", ""), ("", "", "じゅうはち"), ("", "さん", "にじゅうしち"), ("", "", "さんじゅうろく"), ("", "", "しじゅうご"), ("", "ろく", "ごじゅうし"), ("", "しち", "ろくじゅうさん"), ("", "", "しちじゅうに"), ("", "", "はちじゅういち")]
}

# ==========================================
# 各段のイラストを描画するクラス群
# ==========================================
class Pencil(VGroup):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        body = Rectangle(width=0.3, height=0.8, color=BLACK, fill_color=YELLOW, fill_opacity=1)
        tip = Polygon([-0.15, -0.4, 0], [0.15, -0.4, 0], [0, -0.7, 0], color=BLACK, fill_color='#D2B48C', fill_opacity=1)
        core = Polygon([-0.05, -0.6, 0], [0.05, -0.6, 0], [0, -0.7, 0], color=BLACK, fill_color=BLACK, fill_opacity=1)
        self.add(body, tip, core)
        self.scale(0.8)

class CherryPair(VGroup):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        left_cherry = Circle(radius=0.25, color=BLACK, fill_color=RED, fill_opacity=1).shift(LEFT * 0.3 + DOWN * 0.3)
        right_cherry = Circle(radius=0.25, color=BLACK, fill_color=RED, fill_opacity=1).shift(RIGHT * 0.3 + DOWN * 0.3)
        stem_center = UP * 0.4
        left_stem = Line(left_cherry.get_top(), stem_center, color="#006400", stroke_width=4)
        right_stem = Line(right_cherry.get_top(), stem_center, color="#006400", stroke_width=4)
        leaf = Ellipse(width=0.3, height=0.15, color=BLACK, fill_color=GREEN, fill_opacity=1).rotate(PI / 6).move_to(stem_center + RIGHT * 0.15 + UP * 0.1)
        self.add(left_stem, right_stem, left_cherry, right_cherry, leaf)
        self.scale(0.8)

class Dango(VGroup):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        stick = Line(UP*0.4, DOWN*0.8, color='#8B4513', stroke_width=6)
        pink = Circle(radius=0.25, color=BLACK, fill_color='#FFB6C1', fill_opacity=1).shift(UP*0.4)
        white = Circle(radius=0.25, color=BLACK, fill_color=WHITE, fill_opacity=1)
        green = Circle(radius=0.25, color=BLACK, fill_color='#98FB98', fill_opacity=1).shift(DOWN*0.4)
        self.add(stick, green, white, pink)
        self.scale(0.8)

class Clover(VGroup):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        stem = Line(ORIGIN, DOWN*0.7, color="#006400", stroke_width=5)
        leaves = VGroup(*[Circle(radius=0.2, color=BLACK, fill_color=GREEN, fill_opacity=1).shift(RIGHT*0.25).rotate(a, about_point=ORIGIN) for a in [PI/4, 3*PI/4, 5*PI/4, 7*PI/4]])
        self.add(stem, leaves)
        self.scale(0.8)

class Hand(VGroup):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        wrist = Rectangle(width=0.25, height=0.4).shift(DOWN * 0.45)
        palm = RoundedRectangle(corner_radius=0.2, width=0.55, height=0.6).shift(DOWN * 0.1)
        parts = [wrist, palm]
        finger_params = [(0.12, 0.35, -15, 0.22, 0.02), (0.13, 0.45, -5, 0.10, 0.10), (0.14, 0.50, 0, -0.02, 0.15), (0.14, 0.45, 8, -0.14, 0.10), (0.16, 0.35, 40, -0.26, -0.05)]
        for w, h, angle, dx, dy in finger_params:
            parts.append(RoundedRectangle(corner_radius=w/2, width=w, height=h).shift(UP * (h / 2)).rotate(angle * DEGREES, about_point=ORIGIN).shift(RIGHT * dx + UP * dy))
        hand_shape = Union(*parts, color='#C99056', stroke_width=2, fill_color='#FAD6B1', fill_opacity=1)
        creases = VGroup(
            ArcBetweenPoints(start=LEFT*0.1+UP*0.1, end=LEFT*0.05+DOWN*0.2, angle=PI/6, color='#E5B887', stroke_width=2),
            ArcBetweenPoints(start=LEFT*0.1+UP*0.08, end=RIGHT*0.15+DOWN*0.05, angle=PI/8, color='#E5B887', stroke_width=2)
        ).shift(DOWN * 0.1)
        self.add(hand_shape, creases)
        self.scale(0.8)

class Insect(VGroup):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        legs = VGroup()
        for sy, mx, my, ex, ey in [
            (0.15, 0.4, 0.35, 0.6, 0.5),
            (0,    0.45, 0,    0.65, 0),
            (-0.15, 0.4, -0.35, 0.6, -0.5)
        ]:
            legs.add(VGroup(Line(RIGHT*0.15+UP*sy, RIGHT*mx+UP*my, color=BLACK, stroke_width=6),
                            Line(RIGHT*mx+UP*my, RIGHT*ex+UP*ey, color=BLACK, stroke_width=6)))
            legs.add(VGroup(Line(LEFT*0.15+UP*sy, LEFT*mx+UP*my, color=BLACK, stroke_width=6),
                            Line(LEFT*mx+UP*my, LEFT*ex+UP*ey, color=BLACK, stroke_width=6)))

        body = Ellipse(width=0.45, height=0.6, color=BLACK, fill_color='#8BC34A', fill_opacity=1)
        line = Line(UP*0.3, DOWN*0.3, color=BLACK, stroke_width=3)
        head = Arc(radius=0.18, start_angle=0, angle=PI, color=BLACK, fill_color=BLACK, fill_opacity=1).shift(UP*0.25)

        antennas = VGroup(
            Line(UP*0.4, LEFT*0.1+UP*0.5, color=BLACK, stroke_width=3),
            Line(UP*0.4, RIGHT*0.1+UP*0.5, color=BLACK, stroke_width=3)
        )
        self.add(legs, antennas, body, line, head)
        self.scale(0.8)

class Keyboard(VGroup):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        white_keys = VGroup(*[Rectangle(width=0.2, height=0.8, color=BLACK, stroke_width=1, fill_color=WHITE, fill_opacity=1).shift(RIGHT * i * 0.2) for i in range(7)]).center()
        black_keys = VGroup()
        for i in [0, 1, 3, 4, 5]:
            b_key = Rectangle(width=0.12, height=0.45, color=BLACK, stroke_width=1, fill_color=BLACK, fill_opacity=1).move_to(white_keys[i].get_right()).align_to(white_keys[i], UP)
            black_keys.add(b_key)
        keys = VGroup(white_keys, black_keys).center()
        frame = Rectangle(width=0.2*7+0.1, height=0.9, color=BLACK, stroke_width=2, fill_color='#8B4513', fill_opacity=1).move_to(keys.get_center())
        frame_top = Rectangle(width=0.2*7+0.1, height=0.2, color=BLACK, stroke_width=2, fill_color='#8B4513', fill_opacity=1).next_to(frame, UP, buff=0)
        self.add(frame_top, frame, keys).center()
        self.scale(0.8)

class Octopus(VGroup):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        body = Ellipse(width=0.5, height=0.6, color=BLACK, fill_color='#FF4500', fill_opacity=1).shift(UP * 0.1)
        legs = VGroup()
        for i in range(8):
            pos = i - 3.5
            p0 = np.array([pos*0.04, -0.1, 0])
            p3 = np.array([pos*0.22, -0.7, 0])

            vec = p3 - p0
            normal = np.array([vec[1], -vec[0], 0])
            normal = normal / (np.linalg.norm(normal) + 1e-6)

            amp = 0.15 if i % 2 == 0 else -0.15
            p1 = p0 + vec * 0.33 + normal * amp
            p2 = p0 + vec * 0.66 - normal * amp

            legs.add(CubicBezier(p0.tolist(), p1.tolist(), p2.tolist(), p3.tolist(), color=BLACK, stroke_width=11),
                     CubicBezier(p0.tolist(), p1.tolist(), p2.tolist(), p3.tolist(), color='#FF4500', stroke_width=7))

        mouth = Ellipse(width=0.1, height=0.15, color=BLACK, fill_color='#FFB6C1', fill_opacity=1).shift(DOWN * 0.05)
        mouth_hole = Circle(radius=0.02, color=BLACK, fill_color=BLACK, fill_opacity=1).move_to(mouth)
        hachimaki = Rectangle(width=0.5, height=0.1, color=BLACK, fill_color=WHITE, fill_opacity=1).shift(UP * 0.25)
        k1 = Polygon([0,0,0], [0.1,0.08,0], [0.15,-0.05,0], color=BLACK, fill_color=WHITE, fill_opacity=1).shift(RIGHT * 0.25 + UP * 0.25).rotate(-PI/6)
        k2 = Polygon([0,0,0], [0.1,0.08,0], [0.15,-0.05,0], color=BLACK, fill_color=WHITE, fill_opacity=1).shift(RIGHT * 0.25 + UP * 0.25).rotate(PI/4)
        eyes = VGroup()
        for dx in [-0.12, 0.12]:
            eye = Circle(radius=0.08, color=BLACK, fill_color=WHITE, fill_opacity=1).shift(RIGHT*dx + UP*0.1)
            pupil = Circle(radius=0.04, color=BLACK, fill_color=BLACK, fill_opacity=1).move_to(eye).shift(RIGHT*0.02)
            hl = Circle(radius=0.015, color=WHITE, fill_color=WHITE, fill_opacity=1).move_to(pupil).shift(LEFT*0.01 + UP*0.01)
            eyes.add(eye, pupil, hl)
        self.add(legs, body, mouth, mouth_hole, hachimaki, k1, k2, eyes).center()
        self.scale(0.8)

class BaseballPositions(VGroup):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        angles = np.linspace(PI/4, 3*PI/4, 20)
        field = Polygon(*([[0,0,0]] + [[2.0*np.cos(a), 2.0*np.sin(a), 0] for a in angles]), color=BLACK, stroke_width=2, fill_color='#4CAF50', fill_opacity=1)
        infield = Polygon([0,0,0], [0.7,0.7,0], [0,1.4,0], [-0.7,0.7,0], color=WHITE, stroke_width=2, fill_color='#D7CCC8', fill_opacity=1)
        home = Polygon([0,-0.05,0], [0.05,0,0], [0.05,0.05,0], [-0.05,0.05,0], [-0.05,0,0], color=WHITE, fill_color=WHITE, fill_opacity=1, stroke_width=1)
        positions = [(0, 0), (0, 0.7), (0.75, 0.75), (0.35, 1.15), (-0.75, 0.75), (-0.35, 1.15), (-1.0, 1.7), (0, 1.8), (1.0, 1.7)]
        players = VGroup(*[Circle(radius=0.15, color=BLACK, stroke_width=1.5, fill_color='#FF5722', fill_opacity=1).move_to(RIGHT*x + UP*y) for x, y in positions])
        self.add(field, infield, home, players).center()
        self.scale(0.45)

# ==========================================
# 共通関数・共通クラス群
# ==========================================
def get_item_for_table(table_num):
    ITEM_CLASSES = {
        1: Pencil, 2: CherryPair, 3: Dango, 4: Clover, 5: Hand,
        6: Insect, 7: Keyboard, 8: Octopus, 9: BaseballPositions
    }
    return ITEM_CLASSES.get(table_num, lambda: Circle(color=BLACK))()

class GridGenerator:
    """まとめ表の生成ロジック (縦長比率に調整)"""
    @staticmethod
    def create_grid(cell_width=0.9, cell_height=1.1):
        grid = VGroup()
        for r in range(10):
            for c in range(10):
                x = (c - 4.5) * cell_width
                y = (4.5 - r) * cell_height - 0.25
                cell = GridGenerator._create_cell(r, c, x, y, cell_width, cell_height)
                grid.add(cell)
        return grid

    @staticmethod
    def _create_cell(r, c, x, y, cw, ch):
        if r == 0 and c == 0:
            content = Text("×", font_size=24, color=HEADER_TEXT_COLOR)
            bg_color = HEADER_BG
            opacity = 1
        elif r == 0:
            num_text = Text(str(c), font_size=20, color=HEADER_TEXT_COLOR)
            squares = VGroup(*[
                Square(side_length=0.08, fill_color=HEADER_TEXT_COLOR, fill_opacity=1, stroke_width=0)
                .move_to(RIGHT * (j % 3) * 0.11 + DOWN * (j // 3) * 0.11)
                for j in range(c)
            ]).center()
            content = VGroup(num_text, squares).arrange(DOWN, buff=0.05)
            bg_color = HEADER_BG
            opacity = 1
        elif c == 0:
            content = Text(str(r), font_size=24, color=HEADER_TEXT_COLOR)
            bg_color = HEADER_BG
            opacity = 1
        else:
            return GridGenerator._create_body_cell(r, c, x, y, cw, ch)

        box = Rectangle(width=cw, height=ch, fill_color=bg_color, fill_opacity=opacity, stroke_color=GRID_STROKE_COLOR, stroke_width=1)
        return VGroup(box, content).move_to(RIGHT * x + UP * y)

    @staticmethod
    def _create_body_cell(table_num, i, x, y, cw, ch):
        bg_color = ROW_COLORS[table_num]
        box = Rectangle(width=cw, height=ch, fill_color=bg_color, fill_opacity=0.5, stroke_color=GRID_STROKE_COLOR, stroke_width=1)

        eq_mob = Text(f"{table_num}×{i}{table_num * i}", font_size=16, color="#3E2723")
        read_text_left, read_text_right, ans_read_text = kuku_readings[table_num][i - 1]
        read_mob = Text(f"{read_text_left}{read_text_right}{ans_read_text}", font_size=12, color="#039BE5")

        items = VGroup()
        for j in range(i):
            item = get_item_for_table(table_num).scale(0.12)
            for submob in item.get_family():
                if isinstance(submob, VMobject):
                    submob.stroke_width = submob.get_stroke_width() * 0.05
            item.move_to(RIGHT * (j % 3) * 0.18 + DOWN * (j // 3) * 0.18)
            items.add(item)
        items.center()

        content = VGroup(eq_mob, read_mob, items).arrange(DOWN, buff=0.05)

        for target_size, actual_size in [(ch * 0.9, content.height), (cw * 0.95, content.width)]:
            if actual_size > target_size:
                scale_factor = target_size / actual_size
                content.scale(scale_factor)
                for submob in items.get_family():
                    if isinstance(submob, VMobject):
                        submob.stroke_width *= scale_factor

        return VGroup(box, content).move_to(RIGHT * x + UP * y)


# ==========================================
# メインのアニメーション(ベースとなる親クラス)
# ==========================================
class BaseMultiplicationTable(Scene):
    def setup(self):
        self.camera.background_color = BG_COLOR

    def play_table(self, table_num):
        # --- タイトルと「該当のだん」の大きな表表示 ---
        title = Text(kuku_titles[table_num], font_size=80, color=TITLE_COLOR).to_edge(UP, buff=1.0)

        row_group = VGroup()
        for i in range(1, 10):
            # 縦長画面用サイズに調整
            cw, ch = 2.4, 2.4 
            idx = i - 1
            x = (idx % 3 - 1) * cw
            y = (1 - idx // 3) * ch

            bg_color = ROW_COLORS[table_num]
            box = Rectangle(width=cw, height=ch, fill_color=bg_color, fill_opacity=0.5, stroke_color=GRID_STROKE_COLOR, stroke_width=2)

            eq_mob = Text(f"{table_num}×{i}{table_num * i}", font_size=36, color="#3E2723")
            read_left, read_right, read_ans = kuku_readings[table_num][i - 1]
            read_mob = Text(f"{read_left}{read_right}{read_ans}", font_size=24, color="#039BE5")

            items = VGroup()
            for j in range(i):
                item = get_item_for_table(table_num).scale(0.35)
                for submob in item.get_family():
                    if isinstance(submob, VMobject):
                        submob.stroke_width = submob.get_stroke_width() * 0.3
                item.move_to(RIGHT * (j % 3) * 0.45 + DOWN * (j // 3) * 0.45)
                items.add(item)
            items.center()

            content = VGroup(eq_mob, read_mob, items).arrange(DOWN, buff=0.15)

            for target_size, actual_size in [(ch * 0.9, content.height), (cw * 0.95, content.width)]:
                if actual_size > target_size:
                    scale_factor = target_size / actual_size
                    content.scale(scale_factor)
                    for submob in items.get_family():
                        if isinstance(submob, VMobject):
                            submob.stroke_width *= scale_factor

            cell = VGroup(box, content).move_to(RIGHT * x + UP * y)
            row_group.add(cell)

        row_group.move_to(DOWN * 0.5)
        
        # 画面幅をはみ出さないようにスケール調整
        if row_group.width > config.frame_width * 0.9:
            row_group.width = config.frame_width * 0.9

        self.play(Write(title), FadeIn(row_group, lag_ratio=0.05), run_time=sec(INTRO_IN_BEATS))
        self.wait(sec(INTRO_WAIT_BEATS))
        self.play(FadeOut(title), FadeOut(row_group), run_time=sec(INTRO_OUT_BEATS))

        # --- 各計算式の表示 ---
        drawn_items = VGroup()

        # Shorts縦長画面に合わせて配置を上部&中央に寄せる
        Y_EQ = 4.5
        X_NUM1 = -2.2
        X_TIMES = -1.1
        X_NUM2 = 0.0
        X_EQ = 1.1

        num1_mob = Text(str(table_num), font_size=70, color=EQ_TEXT_COLOR).move_to(RIGHT * X_NUM1 + UP * Y_EQ)
        times_mob = Text("×", font_size=70, color=EQ_TEXT_COLOR).move_to(RIGHT * X_TIMES + UP * Y_EQ)
        eq_sign_mob = Text("", font_size=70, color=EQ_TEXT_COLOR).move_to(RIGHT * X_EQ + UP * Y_EQ)

        last_mobs_to_clear = []

        for i in range(1, 10):
            num2_mob = Text(str(i), font_size=70, color=EQ_TEXT_COLOR).move_to(RIGHT * X_NUM2 + UP * Y_EQ)
            ans_mob = Text(str(table_num * i), font_size=80, color=ANS_TEXT_COLOR)
            ans_mob.move_to(RIGHT * (X_EQ + 0.8) + UP * Y_EQ, aligned_edge=LEFT)

            read_left, read_right, read_ans = kuku_readings[table_num][i - 1]
            read1_mob = Text(read_left, font_size=30, color=READ_TEXT_COLOR).next_to(num1_mob, DOWN, buff=0.3)
            read2_mob = Text(read_right, font_size=30, color=READ_TEXT_COLOR).next_to(num2_mob, DOWN, buff=0.3)
            ans_read_mob = Text(read_ans, font_size=30, color=ANS_TEXT_COLOR).next_to(ans_mob, DOWN, buff=0.3)

            # イラストを縦長スペースに均等配置
            new_item = get_item_for_table(table_num)
            idx = i - 1
            x_offset = (idx % 3 - 1) * 2.2
            y_offset = (1 - idx // 3) * 2.5
            new_item.move_to(DOWN * 1.5 + RIGHT * x_offset + UP * y_offset)
            drawn_items.add(new_item)

            anims_to_play = [
                Write(num2_mob), Write(read1_mob), Write(read2_mob),
                FadeIn(new_item, shift=DOWN * 0.2)
            ]
            if i == 1:
                anims_to_play = [Write(num1_mob), Write(times_mob), Write(eq_sign_mob)] + anims_to_play

            self.play(*anims_to_play, run_time=sec(EQ_IN_BEATS))
            self.wait(sec(EQ_WAIT_BEATS))

            self.play(Write(ans_mob), Write(ans_read_mob), run_time=sec(ANS_IN_BEATS))
            self.wait(sec(ANS_WAIT_BEATS))

            # iが9より小さい場合は先に数字や読み仮名だけフェードアウトさせる
            if i < 9:
                self.play(
                    *[FadeOut(m) for m in [num2_mob, read1_mob, read2_mob, ans_mob, ans_read_mob]],
                    run_time=sec(STEP_CLEAR_BEATS)
                )
            else:
                # i == 9 の場合は最後に全体と同時にフェードアウトさせるため保持しておく
                last_mobs_to_clear = [num2_mob, read1_mob, read2_mob, ans_mob, ans_read_mob]

        # 最後に数式・文字・図をすべて同時にフェードアウト
        self.play(
            FadeOut(drawn_items), 
            FadeOut(num1_mob), 
            FadeOut(times_mob), 
            FadeOut(eq_sign_mob), 
            *[FadeOut(m) for m in last_mobs_to_clear],
            run_time=sec(TABLE_CLEAR_BEATS)
        )


# 個別レンダリング用のクラス
class Table1(BaseMultiplicationTable):
    def construct(self): self.play_table(1)

class Table2(BaseMultiplicationTable):
    def construct(self): self.play_table(2)

class Table3(BaseMultiplicationTable):
    def construct(self): self.play_table(3)

class Table4(BaseMultiplicationTable):
    def construct(self): self.play_table(4)

class Table5(BaseMultiplicationTable):
    def construct(self): self.play_table(5)

class Table6(BaseMultiplicationTable):
    def construct(self): self.play_table(6)

class Table7(BaseMultiplicationTable):
    def construct(self): self.play_table(7)

class Table8(BaseMultiplicationTable):
    def construct(self): self.play_table(8)

class Table9(BaseMultiplicationTable):
    def construct(self): self.play_table(9)


# ==========================================
# まとめ表・動画の結合クラス
# ==========================================
class MultiplicationTableGrid(Scene):
    def construct(self):
        self.camera.background_color = BG_COLOR
        title = Text("くくのひょう", font_size=60, color=TITLE_COLOR).to_edge(UP, buff=1.0)
        grid = GridGenerator.create_grid()
        
        # 画面幅をはみ出さないようにスケール調整
        grid.width = config.frame_width * 0.95

        self.play(AnimationGroup(FadeIn(title), FadeIn(grid, lag_ratio=0.01), lag_ratio=0.1), run_time=sec(SUMMARY_IN_BEATS))
        self.wait(sec(SUMMARY_WAIT_BEATS))


class FullMultiplicationVideo(BaseMultiplicationTable):
    def show_summary_table(self, is_start=False):
        title = Text("くくのひょう", font_size=60, color=TITLE_COLOR).to_edge(UP, buff=1.0)
        grid = GridGenerator.create_grid()
        grid.width = config.frame_width * 0.95

        self.play(AnimationGroup(FadeIn(title), FadeIn(grid, lag_ratio=0.01), lag_ratio=0.1), run_time=sec(SUMMARY_IN_BEATS))
        self.wait(sec(SUMMARY_WAIT_BEATS))

        if is_start:
            self.play(FadeOut(title), FadeOut(grid), run_time=sec(SUMMARY_OUT_BEATS))
            self.clear()

    def construct(self):
        self.show_summary_table(is_start=True)

        for i in range(1, 10):
            self.play_table(i)
            self.clear()
            self.camera.background_color = BG_COLOR

        self.show_summary_table(is_start=False)

VoiceVoxに読み込んだMusicXml(メロディライン)

以下に掲載しています。

MuseScoreに読み込んだMusicXml(BGM)

以下に掲載しています。

各九九の段の詳細

それぞれ以下に掲載しています。

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?