from big_ol_pile_of_manim_imports import *
from old_projects.clacks.question import *
from old_projects.div_curl import ShowTwoPopulations


OUTPUT_DIRECTORY = "clacks/solution1"


class FromPuzzleToSolution(MovingCameraScene):
    def construct(self):
        big_rect = FullScreenFadeRectangle()
        big_rect.set_fill(DARK_GREY, 0.5)
        self.add(big_rect)

        rects = VGroup(ScreenRectangle(), ScreenRectangle())
        rects.set_height(3)
        rects.arrange(RIGHT, buff=2)

        titles = VGroup(
            TextMobject("Puzzle"),
            TextMobject("Solution"),
        )

        images = Group(
            ImageMobject("BlocksAndWallExampleMass16"),
            ImageMobject("AnalyzeCircleGeometry"),
        )
        for title, rect, image in zip(titles, rects, images):
            title.scale(1.5)
            title.next_to(rect, UP)
            image.replace(rect)
            self.add(image, rect, title)

        frame = self.camera_frame
        frame.save_state()

        self.play(
            frame.replace, images[0],
            run_time=3
        )
        self.wait()
        self.play(Restore(frame, run_time=3))
        self.play(
            frame.replace, images[1],
            run_time=3,
        )
        self.wait()


class BlocksAndWallExampleMass16(BlocksAndWallExample):
    CONFIG = {
        "sliding_blocks_config": {
            "block1_config": {
                "mass": 16,
                "velocity": -1.5,
            },
        },
        "wait_time": 25,
    }


class Mass16WithElasticLabel(Mass1e1WithElasticLabel):
    CONFIG = {
        "sliding_blocks_config": {
            "block1_config": {
                "mass": 16,
            }
        },
    }


class BlocksAndWallExampleMass64(BlocksAndWallExample):
    CONFIG = {
        "sliding_blocks_config": {
            "block1_config": {
                "mass": 64,
                "velocity": -1.5,
            },
        },
        "wait_time": 25,
    }


class BlocksAndWallExampleMass1e4(BlocksAndWallExample):
    CONFIG = {
        "sliding_blocks_config": {
            "block1_config": {
                "mass": 1e4,
                "velocity": -1.5,
            },
        },
        "wait_time": 25,
    }


class BlocksAndWallExampleMassMillion(BlocksAndWallExample):
    CONFIG = {
        "sliding_blocks_config": {
            "block1_config": {
                "mass": 1e6,
                "velocity": -0.9,
                "label_text": "$100^{3}$ kg"
            },
        },
        "wait_time": 30,
        "million_fade_time": 4,
        "min_time_between_sounds": 0.002,
    }

    def setup(self):
        super().setup()
        self.add_million_label()

    def add_million_label(self):
        first_label = self.blocks.block1.label
        brace = Brace(first_label[:-2], UP, buff=SMALL_BUFF)
        new_label = TexMobject("1{,}000{,}000")
        new_label.next_to(brace, UP, buff=SMALL_BUFF)
        new_label.add(brace)
        new_label.set_color(YELLOW)

        def update_label(label):
            d_time = self.get_time() - self.million_fade_time
            opacity = smooth(d_time)
            label.set_fill(opacity=d_time)

        new_label.add_updater(update_label)
        first_label.add(new_label)


class BlocksAndWallExampleMassTrillion(BlocksAndWallExample):
    CONFIG = {
        "sliding_blocks_config": {
            "block1_config": {
                "mass": 1e12,
                "velocity": -1,
            },
        },
        "wait_time": 30,
        "min_time_between_sounds": 0.001,
    }


class First6DigitsOfPi(DigitsOfPi):
    CONFIG = {"n_digits": 6}


class FavoritesInDescription(Scene):
    def construct(self):
        words = TextMobject("(See the description for \\\\ some favorites)")
        words.scale(1.5)
        self.add(words)


class V1EqualsV2Line(Scene):
    def construct(self):
        line = Line(LEFT, 7 * RIGHT)
        eq = TexMobject("v_1", "=", "v_2")
        eq.set_color_by_tex("v_", RED)
        eq.next_to(RIGHT, UR, SMALL_BUFF)
        self.play(
            Write(eq, run_time=1),
            ShowCreation(line),
        )
        self.wait()


class PhaseSpaceTitle(Scene):
    def construct(self):
        title = TextMobject("Phase space")
        title.scale(1.5)
        title.to_edge(UP)
        rect = ScreenRectangle(height=6)
        rect.next_to(title, DOWN)
        self.add(rect)
        self.play(Write(title, run_time=1))
        self.wait()


class AskAboutFindingNewVelocities(Scene):
    CONFIG = {
        "floor_y": -3,
        "wall_x": -6.5,
        "wall_height": 7,
        "block1_config": {
            "mass": 10,
            "fill_color": BLUE_E,
            "velocity": -1,
        },
        "block2_config": {"mass": 1},
        "block1_start_x": 7,
        "block2_start_x": 3,
        "v_arrow_scale_value": 1.0,
        "is_halted": False,
    }

    def setup(self):
        self.add_clack_sound_file()

    def construct(self):
        self.add_clack_sound_file()
        self.add_floor()
        self.add_wall()
        self.add_blocks()
        self.add_velocity_labels()

        self.ask_about_transfer()
        self.show_ms_and_vs()
        self.show_value_on_equations()

    def add_clack_sound_file(self):
        self.clack_file = os.path.join(SOUND_DIR, "clack.wav")

    def add_floor(self):
        floor = self.floor = Line(
            self.wall_x * RIGHT,
            (FRAME_WIDTH / 2) * RIGHT,
        )
        floor.shift(self.floor_y * UP)
        self.add(floor)

    def add_wall(self):
        wall = self.wall = Wall(height=self.wall_height)
        wall.move_to(
            self.wall_x * RIGHT + self.floor_y * UP,
            DR,
        )
        self.add(wall)

    def add_blocks(self):
        block1 = self.block1 = Block(**self.block1_config)
        block2 = self.block2 = Block(**self.block2_config)
        blocks = self.blocks = VGroup(block1, block2)
        block1.move_to(self.block1_start_x * RIGHT + self.floor_y * UP, DOWN)
        block2.move_to(self.block2_start_x * RIGHT + self.floor_y * UP, DOWN)

        self.add_velocity_phase_space_point()
        # Add arrows
        for block in blocks:
            arrow = Vector(self.block_to_v_vector(block))
            arrow.set_color(RED)
            arrow.set_stroke(BLACK, 1, background=True)
            arrow.move_to(block.get_center(), RIGHT)
            block.arrow = arrow
            block.add(arrow)

            block.v_label = DecimalNumber(
                block.velocity,
                num_decimal_places=2,
                background_stroke_width=2,
            )
            block.v_label.set_color(RED)
            block.add(block.v_label)

        # Add updater
        blocks.add_updater(self.update_blocks)
        self.add(
            blocks,
            block2.arrow, block1.arrow,
            block2.v_label, block1.v_label,
        )

    def add_velocity_phase_space_point(self):
        self.vps_point = VectorizedPoint([
            np.sqrt(self.block1.mass) * self.block1.velocity,
            np.sqrt(self.block2.mass) * self.block2.velocity,
            0
        ])

    def add_velocity_labels(self):
        v_labels = self.get_next_velocity_labels()

        self.add(v_labels)

    def ask_about_transfer(self):
        energy_expression, momentum_expression = \
            self.get_energy_and_momentum_expressions()
        energy_words = TextMobject("Conservation of energy:")
        energy_words.move_to(UP)
        energy_words.to_edge(LEFT, buff=1.5)
        momentum_words = TextMobject("Conservation of momentum:")
        momentum_words.next_to(
            energy_words, DOWN,
            buff=0.7,
        )

        energy_expression.next_to(energy_words, RIGHT, MED_LARGE_BUFF)
        momentum_expression.next_to(energy_expression, DOWN)
        momentum_expression.next_to(momentum_words, RIGHT)

        velocity_labels = self.all_velocity_labels
        randy = Randolph(height=2)
        randy.next_to(velocity_labels, DR)
        randy.save_state()
        randy.fade(1)

        # Up to collisions
        self.go_through_next_collision(include_velocity_label_animation=True)
        self.play(
            randy.restore,
            randy.change, "pondering", velocity_labels[0],
        )
        self.halt()
        self.play(randy.look_at, velocity_labels[-1])
        self.play(Blink(randy))
        self.play(randy.change, "confused")
        self.play(Blink(randy))
        self.wait()
        self.play(
            FadeInFrom(energy_words, RIGHT),
            FadeInFromDown(energy_expression),
            FadeOut(randy),
        )
        self.wait()
        self.play(
            FadeInFrom(momentum_words, RIGHT),
            FadeInFromDown(momentum_expression)
        )
        self.wait()

        self.energy_expression = energy_expression
        self.energy_words = energy_words
        self.momentum_expression = momentum_expression
        self.momentum_words = momentum_words

    def show_ms_and_vs(self):
        block1 = self.block1
        block2 = self.block2
        energy_expression = self.energy_expression
        momentum_expression = self.momentum_expression

        for block in self.blocks:
            block.shift_onto_screen()

        m1_labels = VGroup(
            block1.label,
            energy_expression.get_part_by_tex("m_1"),
            momentum_expression.get_part_by_tex("m_1"),
        )
        m2_labels = VGroup(
            block2.label,
            energy_expression.get_part_by_tex("m_2"),
            momentum_expression.get_part_by_tex("m_2"),
        )
        v1_labels = VGroup(
            block1.v_label,
            energy_expression.get_part_by_tex("v_1"),
            momentum_expression.get_part_by_tex("v_1"),
        )
        v2_labels = VGroup(
            block2.v_label,
            energy_expression.get_part_by_tex("v_2"),
            momentum_expression.get_part_by_tex("v_2"),
        )
        label_groups = VGroup(
            m1_labels, m2_labels,
            v1_labels, v2_labels,
        )
        for group in label_groups:
            group.rects = VGroup(*map(
                SurroundingRectangle,
                group
            ))

        for group in label_groups:
            self.play(LaggedStartMap(
                ShowCreation, group.rects,
                lag_ratio=0.8,
                run_time=1,
            ))
            self.play(FadeOut(group.rects))

    def show_value_on_equations(self):
        energy_expression = self.energy_expression
        momentum_expression = self.momentum_expression
        energy_text = VGroup(energy_expression, self.energy_words)
        momentum_text = VGroup(momentum_expression, self.momentum_words)
        block1 = self.block1
        block2 = self.block2
        block1.save_state()
        block2.save_state()

        v_terms, momentum_v_terms = [
            VGroup(*[
                expr.get_part_by_tex("v_{}".format(d))
                for d in [1, 2]
            ])
            for expr in [energy_expression, momentum_expression]
        ]
        v_braces = VGroup(*[
            Brace(term, UP, buff=SMALL_BUFF)
            for term in v_terms
        ])
        v_decimals = VGroup(*[DecimalNumber(0) for x in range(2)])

        def update_v_decimals(v_decimals):
            values = self.get_velocities()
            for decimal, value, brace in zip(v_decimals, values, v_braces):
                decimal.set_value(value)
                decimal.next_to(brace, UP, SMALL_BUFF)

        update_v_decimals(v_decimals)

        energy_const_brace, momentum_const_brace = [
            Brace(
                expr.get_part_by_tex("const"), UP,
                buff=SMALL_BUFF,
            )
            for expr in [energy_expression, momentum_expression]
        ]

        sqrt_m_vect = np.array([
            np.sqrt(self.block1.mass),
            np.sqrt(self.block2.mass),
            0
        ])

        def get_energy():
            return 0.5 * get_norm(self.vps_point.get_location())**2

        def get_momentum():
            return np.dot(self.vps_point.get_location(), sqrt_m_vect)

        energy_decimal = DecimalNumber(get_energy())
        energy_decimal.next_to(energy_const_brace, UP, SMALL_BUFF)
        momentum_decimal = DecimalNumber(get_momentum())
        momentum_decimal.next_to(momentum_const_brace, UP, SMALL_BUFF)

        VGroup(
            energy_const_brace, energy_decimal,
            momentum_const_brace, momentum_decimal,
        ).set_color(YELLOW)

        self.play(
            ShowCreationThenFadeAround(energy_expression),
            momentum_text.set_fill, {"opacity": 0.25},
            FadeOut(self.all_velocity_labels),
        )
        self.play(*[
            *map(GrowFromCenter, v_braces),
            *map(VFadeIn, v_decimals),
            GrowFromCenter(energy_const_brace),
            FadeIn(energy_decimal),
        ])
        energy_decimal.add_updater(
            lambda m: m.set_value(get_energy())
        )
        v_decimals.add_updater(update_v_decimals)
        self.add(v_decimals)
        self.unhalt()
        self.vps_point.save_state()
        for x in range(8):
            self.go_through_next_collision()
        energy_decimal.clear_updaters()
        momentum_decimal.set_value(get_momentum())
        self.halt()
        self.play(*[
            momentum_text.set_fill, {"opacity": 1},
            FadeOut(energy_text),
            FadeOut(energy_const_brace),
            FadeOut(energy_decimal),
            GrowFromCenter(momentum_const_brace),
            FadeIn(momentum_decimal),
            *[
                ApplyMethod(b.next_to, vt, UP, SMALL_BUFF)
                for b, vt in zip(v_braces, momentum_v_terms)
            ],
        ])
        self.unhalt()
        self.vps_point.restore()
        momentum_decimal.add_updater(
            lambda m: m.set_value(get_momentum())
        )
        momentum_decimal.add_updater(
            lambda m: m.next_to(momentum_const_brace, UP, SMALL_BUFF)
        )
        for x in range(9):
            self.go_through_next_collision()
        self.wait(10)

    # Helpers

    def get_energy_and_momentum_expressions(self):
        tex_to_color_map = {
            "v_1": RED_B,
            "v_2": RED_B,
            "m_1": BLUE_C,
            "m_2": BLUE_C,
        }
        energy_expression = TexMobject(
            "\\frac{1}{2} m_1 (v_1)^2 + ",
            "\\frac{1}{2} m_2 (v_2)^2 = ",
            "\\text{const.}",
            tex_to_color_map=tex_to_color_map,
        )
        momentum_expression = TexMobject(
            "m_1 v_1 + m_2 v_2 =", "\\text{const.}",
            tex_to_color_map=tex_to_color_map
        )
        return VGroup(
            energy_expression,
            momentum_expression,
        )

    def go_through_next_collision(self, include_velocity_label_animation=False):
        block2 = self.block2
        if block2.velocity >= 0:
            self.wait_until(self.blocks_are_hitting)
            self.add_sound(self.clack_file)
            self.transfer_momentum()
            edge = RIGHT
        else:
            self.wait_until(self.block2_is_hitting_wall)
            self.add_sound(self.clack_file)
            self.reflect_block2()
            edge = LEFT
        anims = [Flash(block2.get_edge_center(edge))]
        if include_velocity_label_animation:
            anims.append(self.get_next_velocity_labels_animation())
        self.play(*anims, run_time=0.5)

    def get_next_velocity_labels_animation(self):
        return FadeInFrom(
            self.get_next_velocity_labels(),
            LEFT,
            run_time=0.5
        )

    def get_next_velocity_labels(self, v1=None, v2=None):
        new_labels = self.get_velocity_labels(v1, v2)
        if hasattr(self, "all_velocity_labels"):
            arrow = Vector(RIGHT)
            arrow.next_to(self.all_velocity_labels)
            new_labels.next_to(arrow, RIGHT)
            new_labels.add(arrow)
        else:
            self.all_velocity_labels = VGroup()
        self.all_velocity_labels.add(new_labels)
        return new_labels

    def get_velocity_labels(self, v1=None, v2=None):
        default_vs = self.get_velocities()
        v1 = v1 or default_vs[0]
        v2 = v2 or default_vs[1]
        labels = VGroup(
            TexMobject("v_1 = {:.2f}".format(v1)),
            TexMobject("v_2 = {:.2f}".format(v2)),
        )
        labels.arrange(
            DOWN,
            buff=MED_SMALL_BUFF,
            aligned_edge=LEFT,
        )
        labels.scale(0.9)
        for label in labels:
            label[:2].set_color(RED)
        labels.next_to(self.wall, RIGHT)
        labels.to_edge(UP, buff=MED_SMALL_BUFF)
        return labels

    def update_blocks(self, blocks, dt):
        for block, velocity in zip(blocks, self.get_velocities()):
            block.velocity = velocity
            if not self.is_halted:
                block.shift(block.velocity * dt * RIGHT)
            center = block.get_center()
            block.arrow.put_start_and_end_on(
                center,
                center + self.block_to_v_vector(block),
            )
            max_height = 0.25
            block.v_label.set_value(block.velocity)
            if block.v_label.get_height() > max_height:
                block.v_label.set_height(max_height)
            block.v_label.next_to(
                block.arrow.get_start(), UP,
                buff=SMALL_BUFF,
            )
        return blocks

    def block_to_v_vector(self, block):
        return block.velocity * self.v_arrow_scale_value * RIGHT

    def blocks_are_hitting(self):
        x1 = self.block1.get_left()[0]
        x2 = self.block2.get_right()[0]
        buff = 0.01
        return (x1 < x2 + buff)

    def block2_is_hitting_wall(self):
        x2 = self.block2.get_left()[0]
        buff = 0.01
        return (x2 < self.wall_x + buff)

    def get_velocities(self):
        m1 = self.block1.mass
        m2 = self.block2.mass
        vps_coords = self.vps_point.get_location()
        return [
            vps_coords[0] / np.sqrt(m1),
            vps_coords[1] / np.sqrt(m2),
        ]

    def transfer_momentum(self):
        m1 = self.block1.mass
        m2 = self.block2.mass
        theta = np.arctan(np.sqrt(m2 / m1))
        self.reflect_block2()
        self.vps_point.rotate(2 * theta, about_point=ORIGIN)

    def reflect_block2(self):
        self.vps_point.points[:, 1] *= -1

    def halt(self):
        self.is_halted = True

    def unhalt(self):
        self.is_halted = False


class IntroduceVelocityPhaseSpace(AskAboutFindingNewVelocities):
    CONFIG = {
        "wall_height": 1.5,
        "floor_y": -3.5,
        "block1_start_x": 5,
        "block2_start_x": 0,
        "axes_config": {
            "x_axis_config": {
                "x_min": -5.5,
                "x_max": 6,
            },
            "y_axis_config": {
                "x_min": -3.5,
                "x_max": 4,
            },
            "number_line_config": {
                "unit_size": 0.7,
            },
        },
        "momentum_line_scale_factor": 4,
    }

    def construct(self):
        self.add_wall_floor_and_blocks()
        self.show_two_equations()
        self.draw_axes()
        self.draw_ellipse()
        self.rescale_axes()
        self.show_starting_point()
        self.show_initial_collide()
        self.ask_about_where_to_land()
        self.show_conservation_of_momentum_equation()
        self.show_momentum_line()
        self.reiterate_meaning_of_line_and_circle()
        self.reshow_first_jump()
        self.show_bounce_off_wall()
        self.show_reflection_about_x()
        self.show_remaining_collisions()

    def add_wall_floor_and_blocks(self):
        self.add_floor()
        self.add_wall()
        self.add_blocks()
        self.halt()

    def show_two_equations(self):
        equations = self.get_energy_and_momentum_expressions()
        equations.arrange(DOWN, buff=LARGE_BUFF)
        equations.shift(UP)
        v1_terms, v2_terms = v_terms = VGroup(*[
            VGroup(*[
                expr.get_parts_by_tex(tex)
                for expr in equations
            ])
            for tex in ("v_1", "v_2")
        ])
        for eq in equations:
            eq.highlighted_copy = eq.copy()
            eq.highlighted_copy.set_fill(opacity=0)
            eq.highlighted_copy.set_stroke(YELLOW, 3)

        self.add(equations)
        self.play(
            ShowCreation(equations[0].highlighted_copy),
            run_time=0.75,
        )
        self.play(
            FadeOut(equations[0].highlighted_copy),
            ShowCreation(equations[1].highlighted_copy),
            run_time=0.75,
        )
        self.play(
            FadeOut(equations[1].highlighted_copy),
            run_time=0.75,
        )
        self.play(LaggedStartMap(
            Indicate, v_terms,
            lag_ratio=0.75,
            rate_func=there_and_back,
        ))
        self.wait()

        self.equations = equations

    def draw_axes(self):
        equations = self.equations
        energy_expression, momentum_expression = equations

        axes = self.axes = Axes(**self.axes_config)
        axes.to_edge(UP, buff=SMALL_BUFF)
        axes.set_stroke(width=2)

        # Axes labels
        x_axis_labels = VGroup(
            TexMobject("x = ", "v_1"),
            TexMobject("x = ", "\\sqrt{m_1}", "\\cdot", "v_1"),
        )
        y_axis_labels = VGroup(
            TexMobject("y = ", "v_2"),
            TexMobject("y = ", "\\sqrt{m_2}", "\\cdot", "v_2"),
        )
        axis_labels = self.axis_labels = VGroup(x_axis_labels, y_axis_labels)
        for label_group in axis_labels:
            for label in label_group:
                label.set_color_by_tex("v_", RED)
                label.set_color_by_tex("m_", BLUE)
        for label in x_axis_labels:
            label.next_to(axes.x_axis.get_right(), UP)
        for label in y_axis_labels:
            label.next_to(axes.y_axis.get_top(), DR)

        # Introduce axes and labels
        self.play(
            equations.scale, 0.8,
            equations.to_corner, UL, {"buff": MED_SMALL_BUFF},
            Write(axes),
        )
        self.wait()
        self.play(
            momentum_expression.set_fill, {"opacity": 0.2},
            Indicate(energy_expression, scale_factor=1.05),
        )
        self.wait()
        for n in range(2):
            tex = "v_{}".format(n + 1)
            self.play(
                TransformFromCopy(
                    energy_expression.get_part_by_tex(tex),
                    axis_labels[n][0].get_part_by_tex(tex),
                ),
                FadeInFromDown(axis_labels[n][0][0]),
            )

        # Show vps_dot
        vps_dot = self.vps_dot = Dot(color=RED)
        vps_dot.set_stroke(BLACK, 2, background=True)
        vps_dot.add_updater(
            lambda m: m.move_to(axes.coords_to_point(
                *self.get_velocities()
            ))
        )

        vps_point = self.vps_point
        vps_point.save_state()
        kwargs = {
            "path_arc": PI / 3,
            "run_time": 2,
        }
        target_locations = [
            6 * RIGHT + 2 * UP,
            6 * RIGHT + 2 * DOWN,
            6 * LEFT + 1 * UP,
        ]
        self.add(vps_dot)
        for target_location in target_locations:
            self.play(
                vps_point.move_to, target_location,
                **kwargs,
            )
        self.play(Restore(vps_point, **kwargs))
        self.wait()

    def draw_ellipse(self):
        vps_dot = self.vps_dot
        vps_point = self.vps_point
        axes = self.axes
        energy_expression = self.equations[0]

        ellipse = self.ellipse = Circle(color=YELLOW)
        ellipse.set_stroke(BLACK, 5, background=True)
        ellipse.rotate(PI)
        mass_ratio = self.block1.mass / self.block2.mass
        ellipse.replace(
            Polygon(*[
                axes.coords_to_point(x, y * np.sqrt(mass_ratio))
                for x, y in [(1, 0), (0, 1), (-1, 0), (0, -1)]
            ]),
            stretch=True
        )

        self.play(Indicate(energy_expression, scale_factor=1.05))
        self.add(ellipse, vps_dot)
        self.play(
            ShowCreation(ellipse),
            Rotating(vps_point, about_point=ORIGIN),
            run_time=6,
            rate_func=lambda t: smooth(t, 3),
        )
        self.wait()

    def rescale_axes(self):
        ellipse = self.ellipse
        axis_labels = self.axis_labels
        equations = self.equations
        vps_point = self.vps_point
        vps_dot = self.vps_dot
        vps_dot.clear_updaters()
        vps_dot.add_updater(
            lambda m: m.move_to(ellipse.get_left())
        )

        mass_ratio = self.block1.mass / self.block2.mass
        brief_circle = ellipse.copy()
        brief_circle.stretch(np.sqrt(mass_ratio), 0)
        brief_circle.set_stroke(WHITE, 2)

        xy_equation = self.xy_equation = TexMobject(
            "\\frac{1}{2}",
            "\\left(", "x^2", "+", "y^2", "\\right)",
            "=", "\\text{const.}"
        )
        xy_equation.scale(0.8)
        xy_equation.next_to(equations[0], DOWN)

        self.play(ShowCreationThenFadeOut(brief_circle))
        for i, labels, block in zip(it.count(), axis_labels, self.blocks):
            self.play(ShowCreationThenFadeAround(labels[0]))
            self.play(
                ReplacementTransform(labels[0][0], labels[1][0]),
                ReplacementTransform(labels[0][-1], labels[1][-1]),
                FadeInFromDown(labels[1][1:-1]),
                ellipse.stretch, np.sqrt(block.mass), i,
            )
            self.wait()

        vps_dot.clear_updaters()
        vps_dot.add_updater(
            lambda m: m.move_to(self.axes.coords_to_point(
                *self.vps_point.get_location()[:2]
            ))
        )

        self.play(
            FadeInFrom(xy_equation, UP),
            FadeOut(equations[1])
        )
        self.wait()
        curr_x = vps_point.get_location()[0]
        for x in [0.5 * curr_x, 2 * curr_x, curr_x]:
            axes_center = self.axes.coords_to_point(0, 0)
            self.play(
                vps_point.move_to, x * RIGHT,
                UpdateFromFunc(
                    ellipse,
                    lambda m: m.set_width(
                        2 * get_norm(
                            vps_dot.get_center() - axes_center,
                        ),
                    ).move_to(axes_center)
                ),
                run_time=2,
            )
        self.wait()

    def show_starting_point(self):
        vps_dot = self.vps_dot
        block1, block2 = self.blocks

        self.unhalt()
        self.wait(3)
        self.halt()
        self.play(ShowCreationThenFadeAround(vps_dot))
        self.wait()

    def show_initial_collide(self):
        self.unhalt()
        self.go_through_next_collision()
        self.wait()
        self.halt()
        self.wait()

    def ask_about_where_to_land(self):
        self.play(
            Rotating(
                self.vps_point,
                about_point=ORIGIN,
                run_time=6,
                rate_func=lambda t: smooth(t, 3),
            ),
        )
        self.wait(2)

    def show_conservation_of_momentum_equation(self):
        equations = self.equations
        energy_expression, momentum_expression = equations
        momentum_expression.set_fill(opacity=1)
        momentum_expression.shift(MED_SMALL_BUFF * UP)
        momentum_expression.shift(MED_SMALL_BUFF * LEFT)
        xy_equation = self.xy_equation

        momentum_xy_equation = self.momentum_xy_equation = TexMobject(
            "\\sqrt{m_1}", "x", "+",
            "\\sqrt{m_2}", "y", "=",
            "\\text{const.}",
        )
        momentum_xy_equation.set_color_by_tex("m_", BLUE)
        momentum_xy_equation.scale(0.8)
        momentum_xy_equation.next_to(
            momentum_expression, DOWN,
            buff=MED_LARGE_BUFF,
            aligned_edge=RIGHT,
        )

        self.play(
            FadeOut(xy_equation),
            energy_expression.set_fill, {"opacity": 0.2},
            FadeInFromDown(momentum_expression)
        )
        self.play(ShowCreationThenFadeAround(momentum_expression))
        self.wait()
        self.play(FadeInFrom(momentum_xy_equation, UP))
        self.wait()

    def show_momentum_line(self):
        vps_dot = self.vps_dot
        m1 = self.block1.mass
        m2 = self.block2.mass
        line = Line(np.sqrt(m2) * LEFT, np.sqrt(m1) * DOWN)
        line.scale(self.momentum_line_scale_factor)
        line.set_stroke(GREEN, 3)
        line.move_to(vps_dot)

        slope_label = TexMobject(
            "\\text{Slope =}", "-\\sqrt{\\frac{m_1}{m_2}}"
        )
        slope_label.scale(0.8)
        slope_label.next_to(vps_dot, LEFT, LARGE_BUFF)
        slope_arrow = Arrow(
            slope_label.get_right(),
            line.point_from_proportion(0.45),
            buff=SMALL_BUFF,
        )
        slope_group = VGroup(line, slope_label, slope_arrow)
        foreground_mobs = VGroup(
            self.equations[1], self.momentum_xy_equation,
            self.blocks, self.vps_dot
        )
        for mob in foreground_mobs:
            if isinstance(mob, TexMobject):
                mob.set_stroke(BLACK, 3, background=True)

        self.add(line, *foreground_mobs)
        self.play(ShowCreation(line))
        self.play(
            FadeInFrom(slope_label, RIGHT),
            GrowArrow(slope_arrow),
        )
        self.wait()
        self.add(slope_group, *foreground_mobs)
        self.play(slope_group.shift, 4 * RIGHT, run_time=3)
        self.play(slope_group.shift, 5 * LEFT, run_time=3)
        self.play(
            slope_group.shift, RIGHT,
            run_time=1,
            rate_func=lambda t: t**4,
        )
        self.wait()

        self.momentum_line = line
        self.slope_group = slope_group

    def reiterate_meaning_of_line_and_circle(self):
        line_vect = self.momentum_line.get_vector()
        vps_point = self.vps_point

        for x in [0.25, -0.5, 0.25]:
            self.play(
                vps_point.shift, x * line_vect,
                run_time=2
            )
        self.wait()
        self.play(Rotating(
            vps_point,
            about_point=ORIGIN,
            rate_func=lambda t: smooth(t, 3),
        ))
        self.wait()

    def reshow_first_jump(self):
        vps_point = self.vps_point
        curr_point = vps_point.get_location()
        start_point = get_norm(curr_point) * LEFT

        for n in range(8):
            vps_point.move_to(
                [start_point, curr_point][n % 2]
            )
            self.wait(0.5)
        self.wait()

    def show_bounce_off_wall(self):
        self.unhalt()
        self.go_through_next_collision()
        self.halt()

    def show_reflection_about_x(self):
        vps_point = self.vps_point

        curr_location = vps_point.get_location()
        old_location = np.array(curr_location)
        old_location[1] *= -1

        # self.play(
        #     ApplyMethod(
        #         self.block2.move_to, self.wall.get_corner(DR), DL,
        #         path_arc=30 * DEGREES,
        #     )
        # )
        for n in range(4):
            self.play(
                vps_point.move_to,
                [old_location, curr_location][n % 2]
            )
        self.wait()

        group = VGroup(
            self.ellipse,
            self.lines[-1],
            self.vps_dot.copy().clear_updaters()
        )
        for x in range(2):
            self.play(
                Rotate(
                    group, PI, RIGHT,
                    about_point=self.axes.coords_to_point(0, 0)
                ),
            )
        self.remove(group[-1])

    def show_remaining_collisions(self):
        line = self.momentum_line
        # slope_group = self.slope_group
        vps_dot = self.vps_dot
        axes = self.axes
        slope = np.sqrt(self.block2.mass / self.block1.mass)

        end_region = Polygon(
            axes.coords_to_point(0, 0),
            axes.coords_to_point(10, 0),
            axes.coords_to_point(10, slope * 10),
            stroke_width=0,
            fill_color=GREEN,
            fill_opacity=0.3
        )

        self.unhalt()
        for x in range(7):
            self.go_through_next_collision()
            if x == 0:
                self.halt()
                self.play(line.move_to, vps_dot)
                self.wait()
                self.unhalt()
        self.play(FadeIn(end_region))
        self.go_through_next_collision()
        self.wait(5)

    # Helpers
    def add_update_line(self, func):
        if not hasattr(self, "lines"):
            self.lines = VGroup()
        if hasattr(self, "vps_dot"):
            old_vps_point = self.vps_dot.get_center()
            func()
            self.vps_dot.update()
            new_vps_point = self.vps_dot.get_center()
            line = Line(old_vps_point, new_vps_point)
            line.set_stroke(WHITE, 2)
            self.add(line)
            self.lines.add(line)
        else:
            func()

    def transfer_momentum(self):
        self.add_update_line(super().transfer_momentum)

    def reflect_block2(self):
        self.add_update_line(super().reflect_block2)


class IntroduceVelocityPhaseSpaceWith16(IntroduceVelocityPhaseSpace):
    CONFIG = {
        "block1_config": {
            "mass": 16,
            "velocity": -0.5,
        },
        "momentum_line_scale_factor": 0,
    }


class SimpleRect(Scene):
    def construct(self):
        self.add(Rectangle(width=6, height=2, color=WHITE))


class SurprisedRandy(Scene):
    def construct(self):
        randy = Randolph()
        self.play(FadeIn(randy))
        self.play(randy.change, "surprised", 3 * UR)
        self.play(Blink(randy))
        self.wait()
        self.play(randy.change, "pondering", 3 * UR)
        self.play(Blink(randy))
        self.wait(2)
        self.play(FadeOut(randy))


class HuntForPi(TeacherStudentsScene):
    def construct(self):
        self.student_says(
            "Hunt for $\\pi$!",
            bubble_kwargs={"direction": LEFT},
            target_mode="hooray"
        )
        self.change_all_student_modes(
            "hooray",
            added_anims=[self.teacher.change, "happy"]
        )
        self.wait()


class StretchBySqrt10(Scene):
    def construct(self):
        arrow = DoubleArrow(2 * LEFT, 2 * RIGHT)
        arrow.tip[1].shift(0.05 * LEFT)
        value = TexMobject("\\sqrt{10}")
        value.next_to(arrow, UP)
        arrow.save_state()
        arrow.stretch(0, 0)
        self.play(
            Restore(arrow),
            Write(value, run_time=1),
        )
        self.wait()


class XCoordNegative(Scene):
    def construct(self):
        rect = Rectangle(height=4, width=4)
        rect.set_stroke(width=0)
        rect.set_fill(RED, 0.5)
        rect.save_state()
        rect.stretch(0, 0, about_edge=RIGHT)
        self.play(Restore(rect))
        self.wait()


class YCoordZero(Scene):
    def construct(self):
        rect = Rectangle(height=4, width=8)
        rect.set_stroke(width=0)
        rect.set_fill(WHITE, 0.5)
        rect.save_state()
        self.play(
            rect.stretch, 0.01, 1,
            rect.set_fill, {"opacity": 1}
        )
        self.wait()


class CircleDiagramFromSlidingBlocks(Scene):
    CONFIG = {
        "BlocksAndWallSceneClass": BlocksAndWallExampleMass1e1,
        "circle_config": {
            "radius": 2,
            "stroke_color": YELLOW,
            "stroke_width": 3,
        },
        "lines_style": {
            "stroke_color": WHITE,
            "stroke_width": 2,
        },
        "axes_config": {
            "style": {
                "stroke_color": LIGHT_GREY,
                "stroke_width": 1,
            },
            "width": 5,
            "height": 4.5,
        },
        "end_zone_style": {
            "stroke_width": 0,
            "fill_color": GREEN,
            "fill_opacity": 0.3,
        },
    }

    def construct(self):
        sliding_blocks_scene = self.BlocksAndWallSceneClass(
            show_flash_animations=False,
            write_to_movie=False,
            wait_time=0,
        )
        blocks = sliding_blocks_scene.blocks
        times = [pair[1] for pair in blocks.clack_data]
        self.mass_ratio = 1 / blocks.mass_ratio
        self.show_circle_lines(
            times=times,
            slope=(-1 / np.sqrt(blocks.mass_ratio))
        )

    def show_circle_lines(self, times, slope):
        circle = self.get_circle()
        axes = self.get_axes()
        lines = self.get_lines(circle.radius, slope)
        end_zone = self.get_end_zone()

        dot = Dot(color=RED, radius=0.06)
        dot.move_to(lines[0].get_start())

        self.add(end_zone, axes, circle, dot)

        last_time = 0
        for time, line in zip(times, lines):
            if time > 300:
                time = last_time + 1
            self.wait(time - last_time)
            last_time = time
            dot.move_to(line.get_end())
            self.add(line, dot)
        self.wait()

    def get_circle(self):
        circle = Circle(**self.circle_config)
        circle.rotate(PI)  # Nice to have start point on left
        return circle

    def get_axes(self):
        config = self.axes_config
        axes = VGroup(
            Line(LEFT, RIGHT).set_width(config["width"]),
            Line(DOWN, UP).set_height(config["height"])
        )
        axes.set_style(**config["style"])
        return axes

    def get_lines(self, radius, slope):
        theta = np.arctan(-1 / slope)
        n_clacks = int(PI / theta)
        points = []
        for n in range(n_clacks + 1):
            theta_mult = (n + 1) // 2
            angle = 2 * theta * theta_mult
            if n % 2 == 0:
                angle *= -1
            new_point = radius * np.array([
                -np.cos(angle), -np.sin(angle), 0
            ])
            points.append(new_point)

        lines = VGroup(*[
            Line(p1, p2)
            for p1, p2 in zip(points, points[1:])
        ])
        lines.set_style(**self.lines_style)
        return lines

    def get_end_zone(self):
        slope = 1 / np.sqrt(self.mass_ratio)
        x = self.axes_config["width"] / 2
        zone = Polygon(
            ORIGIN, x * RIGHT, x * RIGHT + slope * x * UP,
        )
        zone.set_style(**self.end_zone_style)
        return zone


class CircleDiagramFromSlidingBlocksSameMass(CircleDiagramFromSlidingBlocks):
    CONFIG = {
        "BlocksAndWallSceneClass": BlocksAndWallExampleSameMass
    }


class CircleDiagramFromSlidingBlocksSameMass1e1(CircleDiagramFromSlidingBlocks):
    CONFIG = {
        "BlocksAndWallSceneClass": BlocksAndWallExampleMass1e1
    }


class CircleDiagramFromSlidingBlocks1e2(CircleDiagramFromSlidingBlocks):
    CONFIG = {
        "BlocksAndWallSceneClass": BlocksAndWallExampleMass1e2
    }


class CircleDiagramFromSlidingBlocks1e4(CircleDiagramFromSlidingBlocks):
    CONFIG = {
        "BlocksAndWallSceneClass": BlocksAndWallExampleMass1e4
    }


class AnnouncePhaseDiagram(CircleDiagramFromSlidingBlocks):
    def construct(self):
        pd_words = TextMobject("Phase diagram")
        pd_words.scale(1.5)
        pd_words.move_to(self.hold_up_spot, DOWN)
        pd_words_border = pd_words.copy()
        pd_words_border.set_stroke(YELLOW, 2)
        pd_words_border.set_fill(opacity=0)

        simple_words = TextMobject("Simple but powerful")
        simple_words.next_to(pd_words, UP, LARGE_BUFF)
        simple_words.shift(LEFT)
        simple_words.set_color(BLUE)
        simple_arrow = Arrow(
            simple_words.get_bottom(),
            pd_words.get_top(),
            color=simple_words.get_color(),
        )

        self.play(
            self.teacher.change, "raise_right_hand",
            FadeInFromDown(pd_words)
        )
        self.change_student_modes(
            "pondering", "thinking", "pondering",
            added_anims=[ShowCreationThenFadeOut(pd_words_border)]
        )
        self.wait()
        self.play(
            FadeInFrom(simple_words, RIGHT),
            GrowArrow(simple_arrow),
            self.teacher.change, "hooray",
        )
        self.change_student_modes(
            "thinking", "happy", "thinking",
        )
        self.wait(3)


class AnalyzeCircleGeometry(CircleDiagramFromSlidingBlocks, MovingCameraScene):
    CONFIG = {
        "mass_ratio": 16,
        "circle_config": {
            "radius": 3,
        },
        "axes_config": {
            "width": FRAME_WIDTH,
            "height": FRAME_HEIGHT,
        },
        "lines_style": {
            "stroke_width": 2,
        },
    }

    def construct(self):
        self.add_mass_ratio_label()
        self.add_circle_with_lines()
        self.show_equal_arc_lengths()
        self.use_arc_lengths_to_count()
        self.focus_on_three_points()
        self.show_likewise_for_all_jumps()
        self.drop_arc_for_each_hop()
        self.try_adding_one_more_arc()
        self.zoom_out()

    def add_mass_ratio_label(self, mass_ratio=None):
        mass_ratio = mass_ratio or self.mass_ratio
        mass_ratio_label = TextMobject(
            "Mass ratio =", "{:,} : 1".format(mass_ratio)
        )
        mass_ratio_label.to_corner(UL, buff=MED_SMALL_BUFF)
        self.add(mass_ratio_label)
        self.mass_ratio_label = mass_ratio_label

    def add_circle_with_lines(self):
        circle = self.get_circle()
        axes = self.get_axes()
        axes_labels = self.get_axes_labels(axes)
        slope = -np.sqrt(self.mass_ratio)
        lines = self.get_lines(
            radius=circle.radius,
            slope=slope,
        )
        end_zone = self.get_end_zone()

        end_zone_words = TextMobject("End zone")
        end_zone_words.set_height(0.25)
        end_zone_words.next_to(ORIGIN, UP, SMALL_BUFF)
        end_zone_words.to_edge(RIGHT, buff=MED_SMALL_BUFF)
        end_zone_words.set_color(GREEN)

        self.add(
            axes, axes_labels,
            circle, end_zone, end_zone_words,
        )
        self.play(ShowCreation(lines, run_time=3, rate_func=linear))
        self.wait()

        self.set_variables_as_attrs(
            circle, axes, lines,
            end_zone, end_zone_words,
        )

    def show_equal_arc_lengths(self):
        circle = self.circle
        radius = circle.radius
        theta = self.theta = np.arctan(1 / np.sqrt(self.mass_ratio))
        n_arcs = int(PI / (2 * theta))

        lower_arcs = VGroup(*[
            Arc(
                start_angle=(PI + n * 2 * theta),
                angle=(2 * theta),
                radius=radius
            )
            for n in range(n_arcs + 1)
        ])
        lower_arcs[0::2].set_color(RED)
        lower_arcs[1::2].set_color(BLUE)

        upper_arcs = lower_arcs.copy()
        upper_arcs.rotate(PI, axis=RIGHT, about_point=ORIGIN)
        upper_arcs[0::2].set_color(BLUE)
        upper_arcs[1::2].set_color(RED)

        all_arcs = VGroup(*it.chain(*zip(lower_arcs, upper_arcs)))
        if int(PI / theta) % 2 == 1:
            all_arcs.remove(all_arcs[-1])

        arc_copies = lower_arcs.copy()
        for arc_copy in arc_copies:
            arc_copy.generate_target()
        for arc in arc_copies:
            arc.target.rotate(-(arc.start_angle - PI + theta))

        equal_signs = VGroup(*[
            TexMobject("=") for x in range(len(lower_arcs))
        ])
        equal_signs.scale(0.8)
        for sign in equal_signs:
            sign.generate_target()

        movers = VGroup(*it.chain(*zip(
            arc_copies, equal_signs
        )))
        movers.remove(movers[-1])
        mover_targets = VGroup(*[mover.target for mover in movers])
        mover_targets.arrange(RIGHT, buff=SMALL_BUFF)
        mover_targets.next_to(ORIGIN, DOWN)
        mover_targets.to_edge(LEFT)

        equal_signs.scale(0)
        equal_signs.fade(1)
        equal_signs.move_to(mover_targets)

        all_arcs.save_state()
        for arc in all_arcs:
            arc.rotate(90 * DEGREES)
            arc.fade(1)
            arc.set_stroke(width=20)
        self.play(Restore(
            all_arcs, lag_ratio=0.5,
            run_time=2,
        ))
        self.wait()
        self.play(LaggedStartMap(MoveToTarget, movers))
        self.wait()

        self.arcs_equation = movers
        self.lower_arcs = lower_arcs
        self.upper_arcs = upper_arcs
        self.all_arcs = all_arcs

    def use_arc_lengths_to_count(self):
        all_arcs = self.all_arcs
        lines = self.lines

        arc_counts = VGroup()
        for n, arc in enumerate(all_arcs):
            count_mob = Integer(n + 1)
            count_mob.scale(0.75)
            buff = SMALL_BUFF
            if len(all_arcs) > 100:
                count_mob.scale(0.1)
                count_mob.set_stroke(WHITE, 0.25)
                buff = 0.4 * SMALL_BUFF
            point = arc.point_from_proportion(0.5)
            count_mob.next_to(point, normalize(point), buff)
            arc_counts.add(count_mob)

        self.play(
            FadeOut(lines),
            FadeOut(all_arcs),
            FadeOut(self.arcs_equation),
        )
        self.play(
            ShowIncreasingSubsets(all_arcs),
            ShowIncreasingSubsets(lines),
            ShowIncreasingSubsets(arc_counts),
            run_time=5,
            rate_func=bezier([0, 0, 1, 1])
        )
        self.wait()

        for group in all_arcs, arc_counts:
            targets = VGroup()
            for elem in group:
                elem.generate_target()
                targets.add(elem.target)
            targets.space_out_submobjects(1.2)

        kwargs = {
            "rate_func": there_and_back,
            "run_time": 3,
        }
        self.play(
            LaggedStartMap(MoveToTarget, all_arcs, **kwargs),
            LaggedStartMap(MoveToTarget, arc_counts, **kwargs),
        )

        self.arc_counts = arc_counts

    def focus_on_three_points(self):
        lines = self.lines
        arcs = self.all_arcs
        arc_counts = self.arc_counts
        theta = self.theta

        arc = arcs[4]

        lines.save_state()
        line_pair = lines[3:5]
        lines_to_fade = VGroup(*lines[:3], *lines[5:])

        three_points = [
            line_pair[0].get_start(),
            line_pair[1].get_start(),
            line_pair[1].get_end(),
        ]
        three_dots = VGroup(*map(Dot, three_points))
        three_dots.set_color(RED)

        theta_arc = Arc(
            radius=1,
            start_angle=-90 * DEGREES,
            angle=theta
        )
        theta_arc.shift(three_points[1])
        theta_label = TexMobject("\\theta")
        theta_label.next_to(theta_arc, DOWN, SMALL_BUFF)

        center_lines = VGroup(
            Line(three_points[0], ORIGIN),
            Line(ORIGIN, three_points[2]),
        )
        center_lines.match_style(line_pair)

        two_theta_arc = Arc(
            radius=1,
            start_angle=(center_lines[0].get_angle() + PI),
            angle=2 * theta
        )
        two_theta_label = TexMobject("2\\theta")
        arc_center = two_theta_arc.point_from_proportion(0.5)
        two_theta_label.next_to(
            arc_center, normalize(arc_center), SMALL_BUFF
        )
        two_theta_label.shift(SMALL_BUFF * RIGHT)

        to_fade = VGroup(arc_counts, arcs, lines_to_fade)

        self.play(
            LaggedStartMap(
                FadeOut, VGroup(*to_fade.family_members_with_points())
            )
        )
        lines_to_fade.fade(1)
        self.play(FadeInFromLarge(three_dots[0]))
        self.play(TransformFromCopy(*three_dots[:2]))
        self.play(TransformFromCopy(*three_dots[1:3]))
        self.wait()
        self.play(
            ShowCreation(theta_arc),
            FadeInFrom(theta_label, UP)
        )
        self.wait()
        self.play(
            line_pair.set_stroke, WHITE, 1,
            TransformFromCopy(line_pair, center_lines),
            TransformFromCopy(theta_arc, two_theta_arc),
            TransformFromCopy(theta_label, two_theta_label),
        )
        self.wait()
        self.play(
            TransformFromCopy(two_theta_arc, arc),
            two_theta_label.move_to, 2.7 * arc_center,
        )
        self.wait()

        self.three_dots = three_dots
        self.theta_group = VGroup(theta_arc, theta_label)
        self.center_lines_group = VGroup(
            center_lines, two_theta_arc,
        )
        self.two_theta_label = two_theta_label

    def show_likewise_for_all_jumps(self):
        lines = self.lines
        arcs = self.all_arcs

        every_other_line = lines[::2]

        self.play(
            Restore(
                lines,
                lag_ratio=0.5,
                run_time=2
            ),
            FadeOut(self.center_lines_group),
            FadeOut(self.three_dots),
        )
        self.play(LaggedStartMap(
            ApplyFunction, every_other_line,
            lambda line: (
                lambda l: l.scale(10 / l.get_length()).set_stroke(BLUE, 3),
                line
            )
        ))
        self.play(Restore(lines))
        self.wait()

        # Shift theta label
        last_point = lines[3].get_end()
        last_arc = arcs[4]
        two_theta_label = self.two_theta_label
        theta_group_copy = self.theta_group.copy()
        for line, arc in zip(lines[5:10:2], arcs[6:11:2]):
            new_point = line.get_end()
            arc_point = arc.point_from_proportion(0.5)
            self.play(
                theta_group_copy.shift, new_point - last_point,
                two_theta_label.move_to, 1.1 * arc_point,
                FadeIn(arc),
                FadeOut(last_arc),
            )
            self.wait()
            last_point = new_point
            last_arc = arc
        self.play(
            FadeOut(theta_group_copy),
            FadeOut(two_theta_label),
            FadeOut(last_arc),
        )
        self.wait()

    def drop_arc_for_each_hop(self):
        lines = self.lines
        arcs = self.all_arcs

        two_theta_labels = VGroup()
        wedges = VGroup()
        for arc in arcs:
            label = TexMobject("2\\theta")
            label.scale(0.8)
            label.move_to(1.1 * arc.point_from_proportion(0.5))
            two_theta_labels.add(label)

            wedge = arc.copy()
            wedge.add_line_to(ORIGIN)
            wedge.add_line_to(wedge.points[0])
            wedge.set_stroke(width=0)
            wedge.set_fill(arc.get_color(), 0.2)
            wedges.add(wedge)

        self.remove(lines)
        for line, arc, label, wedge in zip(lines, arcs, two_theta_labels, wedges):
            self.play(
                ShowCreation(line),
                FadeInFrom(arc, normalize(arc.get_center())),
                FadeInFrom(label, normalize(arc.get_center())),
                FadeIn(wedge),
            )

        self.wedges = wedges
        self.two_theta_labels = two_theta_labels

    def try_adding_one_more_arc(self):
        wedges = self.wedges
        theta = self.theta

        last_wedge = wedges[-1]
        new_wedge = last_wedge.copy()
        new_wedge.set_color(PURPLE)
        new_wedge.set_stroke(WHITE, 1)

        self.play(FadeIn(new_wedge))
        for angle in [-2 * theta, 4 * DEGREES, -2 * DEGREES]:
            self.play(Rotate(new_wedge, angle, about_point=ORIGIN))
            self.wait()
        self.play(
            new_wedge.shift, 10 * RIGHT,
            rate_func=running_start,
            path_arc=-30 * DEGREES,
        )
        self.remove(new_wedge)

    def zoom_out(self):
        frame = self.camera_frame
        self.play(
            frame.scale, 2, {"about_point": (TOP + RIGHT_SIDE)},
            run_time=3
        )
        self.wait()

    # Helpers
    def get_axes_labels(self, axes):
        axes_labels = VGroup(
            TexMobject("x = ", "\\sqrt{m_1}", "\\cdot", "v_1"),
            TexMobject("y = ", "\\sqrt{m_2}", "\\cdot", "v_2"),
        )
        for label in axes_labels:
            label.set_height(0.4)
        axes_labels[0].next_to(ORIGIN, DOWN, SMALL_BUFF)
        axes_labels[0].to_edge(RIGHT, MED_SMALL_BUFF)
        axes_labels[1].next_to(ORIGIN, RIGHT, SMALL_BUFF)
        axes_labels[1].to_edge(UP, SMALL_BUFF)
        return axes_labels


class InscribedAngleTheorem(Scene):
    def construct(self):
        self.add_title()
        self.show_circle()
        self.let_point_vary()

    def add_title(self):
        title = TextMobject("Inscribed angle theorem")
        title.scale(1.5)
        title.to_edge(UP)
        self.add(title)
        self.title = title

    def show_circle(self):
        # Boy is this over engineered...
        circle = self.circle = Circle(
            color=BLUE,
            radius=2,
        )
        center_dot = Dot(circle.get_center(), color=WHITE)
        self.add(circle, center_dot)

        angle_trackers = self.angle_trackers = VGroup(
            ValueTracker(TAU / 4),
            ValueTracker(PI),
            ValueTracker(-TAU / 4),
        )

        def get_point(angle):
            return circle.point_from_proportion(
                (angle % TAU) / TAU
            )

        def get_dot(angle):
            dot = Dot(get_point(angle))
            dot.set_color(RED)
            dot.set_stroke(BLACK, 3, background=True)
            return dot

        def get_dots():
            return VGroup(*[
                get_dot(at.get_value())
                for at in angle_trackers
            ])

        def update_labels(labels):
            center = circle.get_center()
            for dot, label in zip(dots, labels):
                label.move_to(
                    center + 1.2 * (dot.get_center() - center)
                )

        def get_lines():
            lines = VGroup(*[
                Line(d1.get_center(), d2.get_center())
                for d1, d2 in zip(dots, dots[1:])
            ])
            lines.set_stroke(WHITE, 3)
            return lines

        def get_center_lines():
            points = [
                dots[0].get_center(),
                circle.get_center(),
                dots[2].get_center(),
            ]
            lines = VGroup(*[
                Line(p1, p2)
                for p1, p2 in zip(points, points[1:])
            ])
            lines.set_stroke(LIGHT_GREY, 3)
            return lines

        def get_angle_label(lines, tex, reduce_angle=True):
            a1 = (lines[0].get_angle() + PI) % TAU
            a2 = lines[1].get_angle()
            diff = (a2 - a1)
            if reduce_angle:
                diff = ((diff + PI) % TAU) - PI
            point = lines[0].get_end()
            arc = Arc(
                start_angle=a1,
                angle=diff,
                radius=0.5,
            )
            arc.shift(point)
            arc_center = arc.point_from_proportion(0.5)
            label = TexMobject(tex)
            vect = (arc_center - point)
            vect = (0.3 + get_norm(vect)) * normalize(vect)
            label.move_to(point + vect)
            return VGroup(arc, label)

        def get_theta_label():
            return get_angle_label(lines, "\\theta")

        def get_2theta_label():
            return get_angle_label(center_lines, "2\\theta", False)

        dots = get_dots()
        lines = get_lines()
        center_lines = get_center_lines()
        labels = VGroup(*[
            TexMobject("P_{}".format(n + 1))
            for n in range(3)
        ])
        update_labels(labels)
        theta_label = get_theta_label()
        two_theta_label = get_2theta_label()

        self.play(
            FadeInFromDown(labels[0]),
            FadeInFromLarge(dots[0]),
        )
        self.play(
            TransformFromCopy(*labels[:2]),
            TransformFromCopy(*dots[:2]),
            ShowCreation(lines[0]),
        )
        self.play(
            ShowCreation(lines[1]),
            TransformFromCopy(*labels[1:3]),
            TransformFromCopy(*dots[1:3]),
            Write(theta_label),
        )
        self.wait()

        # Add updaters
        labels.add_updater(update_labels)
        dots.add_updater(lambda m: m.become(get_dots()))
        lines.add_updater(lambda m: m.become(get_lines()))
        center_lines.add_updater(lambda m: m.become(get_center_lines()))
        theta_label.add_updater(lambda m: m.become(get_theta_label()))
        two_theta_label.add_updater(lambda m: m.become(get_2theta_label()))

        self.add(labels, lines, dots, theta_label)
        # Further animations
        self.play(
            angle_trackers[0].set_value, TAU / 8,
        )
        self.play(
            angle_trackers[2].set_value, -TAU / 8,
        )
        self.wait()
        center_lines.update()
        two_theta_label.update()
        self.play(
            TransformFromCopy(lines.copy().clear_updaters(), center_lines),
            TransformFromCopy(theta_label.copy().clear_updaters(), two_theta_label),
        )
        self.wait()

        self.add(center_lines, two_theta_label)

    def let_point_vary(self):
        p1_tracker, p2_tracker, p3_tracker = self.angle_trackers

        kwargs = {"run_time": 2}
        for angle in [TAU / 4, 3 * TAU / 4]:
            self.play(
                p2_tracker.set_value, angle,
                **kwargs
            )
            self.wait()
        self.play(
            p1_tracker.set_value, PI,
            **kwargs
        )
        self.wait()
        self.play(
            p3_tracker.set_value, TAU / 3,
            **kwargs
        )
        self.wait()
        self.play(
            p2_tracker.set_value, 7 * TAU / 8,
            **kwargs
        )
        self.wait()


class SimpleSlopeLabel(Scene):
    def construct(self):
        label = TexMobject(
            "\\text{Slope}", "=",
            "-\\frac{\\sqrt{m_1}}{\\sqrt{m_2}}"
        )
        vector = Vector(DOWN + 2 * LEFT, color=WHITE)
        vector.move_to(label[0].get_bottom(), UR)
        vector.shift(SMALL_BUFF * DOWN)
        self.play(
            Write(label),
            GrowArrow(vector),
        )
        self.wait()


class AddTwoThetaManyTimes(Scene):
    def construct(self):
        expression = TexMobject(
            "2\\theta", "+",
            "2\\theta", "+",
            "2\\theta", "+",
            "\\cdots", "+",
            "2\\theta",
            "<", "2\\pi",
        )
        expression.to_corner(UL)

        brace = Brace(expression[:-2], DOWN)
        question = brace.get_text("Max number of times?")
        question.set_color(YELLOW)

        central_question_group = self.get_central_question()
        simplified, lil_brace, new_question = central_question_group
        central_question_group.next_to(question, DOWN, LARGE_BUFF)
        new_question.align_to(question, LEFT)

        for n in range(5):
            self.add(expression[:2 * n + 1])
            self.wait(0.25)
        self.play(
            FadeInFrom(expression[-2:], LEFT),
            GrowFromCenter(brace),
            FadeInFrom(question, UP)
        )
        self.wait(3)
        self.play(
            TransformFromCopy(expression[:-2], simplified[:3]),
            TransformFromCopy(expression[-2:], simplified[3:]),
            TransformFromCopy(brace, lil_brace),
        )
        self.play(Write(new_question))
        self.wait()

        self.central_question_group = central_question_group
        self.show_example()

    def get_central_question(self, brace_vect=DOWN):
        expression = TexMobject(
            "N", "\\cdot", "\\theta", "<", "\\pi"
        )
        N = expression[0]
        N.set_color(BLUE)
        brace = Brace(N, brace_vect, buff=SMALL_BUFF)
        question = brace.get_text(
            "Maximal integer?",
        )
        question.set_color(YELLOW)
        result = VGroup(expression, brace, question)
        result.to_corner(UL)
        return result

    def show_example(self):
        equation = self.get_changable_equation(0.01, n_decimal_places=2)
        expression, brace, question = self.central_question_group
        N_mob, dot_theta_eq, rhs, comp_pi = equation

        equation.next_to(expression, DOWN, 2, aligned_edge=LEFT)

        self.play(
            TransformFromCopy(expression[0], N_mob),
            TransformFromCopy(expression[1:4], dot_theta_eq),
            TransformFromCopy(expression[4], rhs),
            TransformFromCopy(expression[4], comp_pi),
        )
        self.wait()
        self.play(
            ChangeDecimalToValue(N_mob, 314, run_time=5)
        )
        self.wait()
        self.play(ChangeDecimalToValue(N_mob, 315))
        self.wait()
        self.play(ChangeDecimalToValue(N_mob, 314))
        self.wait()
        self.play(ShowCreationThenFadeAround(N_mob))

    #
    def get_changable_equation(self, value, tex_string=None, n_decimal_places=10):
        int_mob = Integer(1)
        int_mob.set_color(BLUE)
        formatter = "({:0." + str(n_decimal_places) + "f})"
        tex_string = tex_string or formatter.format(value)
        tex_mob = TexMobject("\\cdot", tex_string, "=")
        rhs = DecimalNumber(value, num_decimal_places=n_decimal_places)

        def align_number(mob):
            y0 = mob[0].get_center()[1]
            y1 = tex_mob[1][1:-1].get_center()[1]
            mob.shift((y1 - y0) * UP)

        int_mob.add_updater(
            lambda m: m.next_to(tex_mob, LEFT, SMALL_BUFF)
        )
        int_mob.add_updater(align_number)
        rhs.add_updater(
            lambda m: m.set_value(value * int_mob.get_value())
        )
        rhs.add_updater(
            lambda m: m.next_to(tex_mob, RIGHT, SMALL_BUFF)
        )
        rhs.add_updater(align_number)

        def get_comp_pi():
            if rhs.get_value() < np.pi:
                result = TexMobject("< \\pi")
                result.set_color(GREEN)
            elif rhs.get_value() > np.pi:
                result = TexMobject("> \\pi")
                result.set_color(RED)
            else:
                result = TexMobject("= \\pi")
            result.next_to(rhs, RIGHT, 2 * SMALL_BUFF)
            result[1].scale(1.5, about_edge=LEFT)
            return result

        comp_pi = always_redraw(get_comp_pi)

        return VGroup(int_mob, tex_mob, rhs, comp_pi)


class AskAboutTheta(TeacherStudentsScene):
    def construct(self):
        self.student_says(
            "But what is $\\theta$?",
            target_mode="raise_left_hand",
        )
        self.change_student_modes(
            "confused", "sassy", "raise_left_hand",
            added_anims=[self.teacher.change, "happy"]
        )
        self.wait(3)


class ComputeThetaFor1e4(AnalyzeCircleGeometry):
    CONFIG = {
        "mass_ratio": 100,
    }

    def construct(self):
        self.add_mass_ratio_label()
        self.add_circle_with_three_lines()
        self.write_slope()
        self.show_tangent()

    def add_circle_with_three_lines(self):
        circle = self.get_circle()
        axes = self.get_axes()
        slope = -np.sqrt(self.mass_ratio)
        lines = self.get_lines(
            radius=circle.radius,
            slope=slope,
        )
        end_zone = self.get_end_zone()
        axes_labels = self.get_axes_labels(axes)
        axes.add(axes_labels)

        lines_to_fade = VGroup(*lines[:11], *lines[13:])
        two_lines = lines[11:13]

        theta = self.theta = np.arctan(-1 / slope)
        arc = Arc(
            start_angle=(-90 * DEGREES),
            angle=theta,
            radius=2,
            arc_center=two_lines[0].get_end(),
        )
        theta_label = TexMobject("\\theta")
        theta_label.scale(0.8)
        theta_label.next_to(arc, DOWN, SMALL_BUFF)

        self.add(end_zone, axes, circle)
        self.play(ShowCreation(lines, rate_func=linear))
        self.play(
            lines_to_fade.set_stroke, WHITE, 1, 0.3,
            ShowCreation(arc),
            FadeInFrom(theta_label, UP)
        )

        self.two_lines = two_lines
        self.lines = lines
        self.circle = circle
        self.axes = axes
        self.theta_label_group = VGroup(theta_label, arc)

    def write_slope(self):
        line = self.two_lines[1]
        slope_label = TexMobject(
            "\\text{Slope}", "=",
            "\\frac{\\text{rise}}{\\text{run}}", "=",
            "\\frac{-\\sqrt{m_1}}{\\sqrt{m_2}}", "=", "-10"
        )
        for mob in slope_label:
            mob.add_to_back(mob.copy().set_stroke(BLACK, 6))
        slope_label.next_to(line.get_center(), UR, buff=1)
        slope_arrow = Arrow(
            slope_label[0].get_bottom(),
            line.point_from_proportion(0.45),
            color=RED,
            buff=SMALL_BUFF,
        )
        new_line = line.copy().set_color(RED)

        self.play(
            FadeInFromDown(slope_label[:3]),
            ShowCreation(new_line),
            GrowArrow(slope_arrow),
        )
        self.remove(new_line)
        line.match_style(new_line)
        self.play(
            FadeInFrom(slope_label[3:5], LEFT)
        )
        self.wait()
        self.play(
            Write(slope_label[5]),
            TransformFromCopy(
                self.mass_ratio_label[1][:3],
                slope_label[6]
            )
        )
        self.wait()

        self.slope_label = slope_label
        self.slope_arrow = slope_arrow

    def show_tangent(self):
        l1, l2 = self.two_lines
        theta = self.theta
        theta_label_group = self.theta_label_group

        tan_equation = TexMobject(
            "\\tan", "(", "\\theta", ")", "=",
            "{\\text{run}", "\\over", "-\\text{rise}}", "=",
            "\\frac{1}{10}",
        )
        tan_equation.scale(0.9)
        tan_equation.to_edge(LEFT, buff=MED_SMALL_BUFF)
        tan_equation.shift(2 * UP)
        run_word = tan_equation.get_part_by_tex("run")
        rise_word = tan_equation.get_part_by_tex("rise")

        p1, p2 = l1.get_start(), l1.get_end()
        p3 = p1 + get_norm(p2 - p1) * np.tan(theta) * RIGHT
        triangle = Polygon(p1, p2, p3)
        triangle.set_stroke(width=0)
        triangle.set_fill(GREEN, 0.5)

        opposite = Line(p1, p3)
        adjacent = Line(p1, p2)
        opposite.set_stroke(BLUE, 3)
        adjacent.set_stroke(PINK, 3)

        arctan_equation = TexMobject(
            "\\theta", "=", "\\arctan", "(", "1 / 10", ")"
        )
        arctan_equation.next_to(tan_equation, DOWN, MED_LARGE_BUFF)

        self.play(
            FadeInFromDown(tan_equation[:8]),
        )
        self.play(
            TransformFromCopy(theta_label_group[1], opposite),
            run_word.set_color, opposite.get_color()
        )
        self.play(WiggleOutThenIn(run_word))
        self.play(
            TransformFromCopy(opposite, adjacent),
            rise_word.set_color, adjacent.get_color()
        )
        self.play(WiggleOutThenIn(rise_word))
        self.wait()
        self.play(TransformFromCopy(
            self.slope_label[-1],
            tan_equation[-2:],
        ))
        self.wait(2)

        indices = [2, 4, 0, 1, -1, 3]
        movers = VGroup(*[tan_equation[i] for i in indices]).copy()
        for mover, target in zip(movers, arctan_equation):
            mover.target = target
        # Swap last two
        sm = movers.submobjects
        sm[-1], sm[-2] = sm[-2], sm[-1]
        self.play(LaggedStartMap(
            Transform, movers[:-1],
            lambda m: (m, m.target),
            lag_ratio=1,
            run_time=1,
            path_arc=PI / 6,
        ))
        self.play(MoveToTarget(movers[-1]))
        self.remove(movers)
        self.add(arctan_equation)
        self.play(ShowCreationThenFadeAround(arctan_equation))
        self.wait()


class ThetaChart(Scene):
    def construct(self):
        self.create_columns()
        self.populate_columns()
        self.show_values()
        self.highlight_example(2)
        self.highlight_example(3)

    def create_columns(self):
        titles = VGroup(*[
            TextMobject("Mass ratio"),
            TextMobject("$\\theta$ formula"),
            TextMobject("$\\theta$ value"),
        ])
        titles.scale(1.5)
        titles.arrange(RIGHT, buff=1.5)
        titles[1].shift(MED_SMALL_BUFF * LEFT)
        titles[2].shift(MED_SMALL_BUFF * RIGHT)
        titles.to_corner(UL)

        lines = VGroup()
        for t1, t2 in zip(titles, titles[1:]):
            line = Line(TOP, BOTTOM)
            x = np.mean([t1.get_center()[0], t2.get_center()[0]])
            line.shift(x * RIGHT)
            lines.add(line)

        h_line = Line(LEFT_SIDE, RIGHT_SIDE)
        h_line.next_to(titles, DOWN)
        h_line.to_edge(LEFT, buff=0)
        lines.add(h_line)
        lines.set_stroke(WHITE, 1)

        self.play(
            LaggedStartMap(FadeInFromDown, titles),
            LaggedStartMap(ShowCreation, lines, lag_ratio=0.8),
        )

        self.h_line = h_line
        self.titles = titles

    def populate_columns(self):
        top_h_line = self.h_line
        x_vals = [t.get_center()[0] for t in self.titles]

        entries = [
            (
                "$m_1$ : $m_2$",
                "$\\arctan(\\sqrt{m_2} / \\sqrt{m_1})$",
                ""
            )
        ] + [
            (
                "{:,} : 1".format(10**(2 * exp)),
                "$\\arctan(1 / {:,})$".format(10**exp),
                self.get_theta_decimal(exp),
            )
            for exp in [1, 2, 3, 4, 5]
        ]

        h_lines = VGroup(top_h_line)
        entry_mobs = VGroup()
        for entry in entries:
            mobs = VGroup(*map(TextMobject, entry))
            for mob, x in zip(mobs, x_vals):
                mob.shift(x * RIGHT)
            delta_y = (mobs.get_height() / 2) + MED_SMALL_BUFF
            y = h_lines[-1].get_center()[1] - delta_y
            mobs.shift(y * UP)
            mobs[0].set_color(BLUE)
            mobs[2].set_color(YELLOW)
            entry_mobs.add(mobs)

            h_line = DashedLine(LEFT_SIDE, RIGHT_SIDE)
            h_line.shift((y - delta_y) * UP)
            h_lines.add(h_line)

        self.play(
            LaggedStartMap(
                FadeInFromDown,
                VGroup(*[em[:2] for em in entry_mobs]),
            ),
            LaggedStartMap(ShowCreation, h_lines[1:]),
            lag_ratio=0.1,
            run_time=5,
        )

        self.entry_mobs = entry_mobs
        self.h_lines = h_lines

    def show_values(self):
        values = VGroup(*[em[2] for em in self.entry_mobs])
        for value in values:
            self.play(LaggedStartMap(
                FadeIn, value,
                lag_ratio=0.1,
                run_time=0.5
            ))
            self.wait(0.5)

    def highlight_example(self, exp):
        entry_mobs = self.entry_mobs
        example = entry_mobs[exp]
        other_entries = VGroup(*entry_mobs[:exp], *entry_mobs[exp + 1:])

        value = example[-1]
        rhs = TexMobject("\\approx {:}".format(10**(-exp)))
        rhs.next_to(value, RIGHT)
        rhs.to_edge(RIGHT, buff=MED_SMALL_BUFF)
        value.generate_target()
        value.target.set_fill(opacity=1)
        value.target.scale(0.9)
        value.target.next_to(rhs, LEFT, SMALL_BUFF)

        self.play(
            other_entries.set_fill, {"opacity": 0.25},
            example.set_fill, {"opacity": 1},
            ShowCreationThenFadeAround(example)
        )
        self.wait()
        self.play(
            MoveToTarget(value),
            Write(rhs),
        )
        self.wait()
        value.add(rhs)

    def get_theta_decimal(self, exp):
        theta = np.arctan(10**(-exp))
        rounded_theta = np.floor(1e10 * theta) / 1e10
        return "{:0.10f}\\dots".format(rounded_theta)


class CentralQuestionFor1e2(AddTwoThetaManyTimes):
    CONFIG = {
        "exp": 2,
    }

    def construct(self):
        exp = self.exp
        question = self.get_central_question(UP)
        pi_value = TexMobject(" = {:0.10f}\\dots".format(PI))
        pi_value.next_to(question[0][-1], RIGHT, SMALL_BUFF)
        pi_value.shift(0.3 * SMALL_BUFF * UP)
        question.add(pi_value)

        max_count = int(PI * 10**exp)

        question.center().to_edge(UP)
        self.add(question)

        b10_equation = self.get_changable_equation(
            10**(-exp), n_decimal_places=exp
        )
        b10_equation.next_to(question, DOWN, buff=1.5)
        arctan_equation = self.get_changable_equation(
            np.arctan(10**(-exp)), n_decimal_places=10,
        )
        arctan_equation.next_to(b10_equation, DOWN, MED_LARGE_BUFF)
        eq_centers = [
            eq[1][2].get_center()
            for eq in [b10_equation, arctan_equation]
        ]
        arctan_equation.shift((eq_centers[1][0] - eq_centers[1][0]) * RIGHT)

        # b10_brace = Brace(b10_equation[1][1][1:-1], UP)
        arctan_brace = Brace(arctan_equation[1][1][1:-1], DOWN)
        # b10_tex = b10_brace.get_tex("1 / 10")
        arctan_tex = arctan_brace.get_tex(
            "\\theta = \\arctan(1 / {:,})".format(10**exp)
        )

        int_mobs = b10_equation[0], arctan_equation[0]

        self.add(*b10_equation, *arctan_equation)
        # self.add(b10_brace, b10_tex)
        self.add(arctan_brace, arctan_tex)

        self.wait()
        self.play(*[
            ChangeDecimalToValue(int_mob, max_count, run_time=8)
            for int_mob in int_mobs
        ])
        self.wait()
        self.play(*[
            ChangeDecimalToValue(int_mob, max_count + 1, run_time=1)
            for int_mob in int_mobs
        ])
        self.wait()
        self.play(*[
            ChangeDecimalToValue(int_mob, max_count, run_time=1)
            for int_mob in int_mobs
        ])
        self.play(ShowCreationThenFadeAround(int_mobs[1]))
        self.wait()


class AnalyzeCircleGeometry1e2(AnalyzeCircleGeometry):
    CONFIG = {
        "mass_ratio": 100,
    }


class CentralQuestionFor1e3(CentralQuestionFor1e2):
    CONFIG = {"exp": 3}


class AskAboutArctanOfSmallValues(TeacherStudentsScene):
    def construct(self):
        self.add_title()

        equation1 = TexMobject(
            "\\arctan", "(", "x", ")", "\\approx", "x"
        )
        equation1.set_color_by_tex("arctan", YELLOW)
        equation2 = TexMobject(
            "x", "\\approx", "\\tan", "(", "x", ")",
        )
        equation2.set_color_by_tex("tan", BLUE)
        for mob in equation1, equation2:
            mob.move_to(self.hold_up_spot, DOWN)

        self.play(
            FadeInFromDown(equation1),
            self.teacher.change, "raise_right_hand",
            self.get_student_changes(
                "erm", "sassy", "confused"
            )
        )
        self.look_at(3 * UL)
        self.play(equation1.shift, UP)
        self.play(
            TransformFromCopy(
                VGroup(*[equation1[i] for i in (2, 4, 5)]),
                VGroup(*[equation2[i] for i in (0, 1, 4)]),
            )
        )
        self.play(
            TransformFromCopy(
                VGroup(*[equation1[i] for i in (0, 1, 3)]),
                VGroup(*[equation2[i] for i in (2, 3, 5)]),
            ),
            self.get_student_changes(
                "confused", "erm", "sassy",
            ),
        )
        self.look_at(3 * UL)
        self.wait(3)
        # self.student_says("Why?", target_mode="maybe")
        # self.wait(3)

    def add_title(self):
        title = TextMobject("For small $x$")
        subtitle = TextMobject("(e.g. $x = 0.001$)")
        subtitle.scale(0.75)
        subtitle.next_to(title, DOWN)
        title.add(subtitle)
        # title.scale(1.5)
        # title.to_edge(UP, buff=MED_SMALL_BUFF)
        title.move_to(self.hold_up_spot)
        title.to_edge(UP)
        self.add(title)


class ActanAndTanGraphs(GraphScene):
    CONFIG = {
        "x_min": -PI / 8,
        "x_max": 5 * PI / 8,
        "y_min": -PI / 8,
        "y_max": 4 * PI / 8,
        "x_tick_frequency": PI / 8,
        "x_leftmost_tick": -PI / 8,
        "y_tick_frequency": PI / 8,
        "y_leftmost_tick": -PI / 8,
        "x_axis_width": 10,
        "y_axis_height": 7,
        "graph_origin": 2.5 * DOWN + 5 * LEFT,
        "num_graph_anchor_points": 500,
    }

    def construct(self):
        self.setup_axes()
        axes = self.axes
        labels = VGroup(
            TexMobject("\\pi / 8"),
            TexMobject("\\pi / 4"),
            TexMobject("3\\pi / 8"),
            TexMobject("\\pi / 2"),
        )
        for n, label in zip(it.count(1), labels):
            label.scale(0.75)
            label.next_to(self.coords_to_point(n * PI / 8, 0), DOWN)
            self.add(label)

        id_graph = self.get_graph(lambda x: x, x_max=1.5)
        arctan_graph = self.get_graph(np.arctan, x_max=1.5)
        tan_graph = self.get_graph(np.tan, x_max=1.5)
        graphs = VGroup(id_graph, arctan_graph, tan_graph)

        id_label = TexMobject("f(x) = x")
        arctan_label = TexMobject("\\arctan(x)")
        tan_label = TexMobject("\\tan(x)")
        labels = VGroup(id_label, arctan_label, tan_label)
        for label, graph in zip(labels, graphs):
            label.match_color(graph)
            label.next_to(graph.points[-1], RIGHT)
            if label.get_bottom()[1] > FRAME_HEIGHT / 2:
                label.next_to(graph.point_from_proportion(0.75), LEFT)

        arctan_x_tracker = ValueTracker(3 * PI / 8)
        arctan_v_line = always_redraw(
            lambda: self.get_vertical_line_to_graph(
                arctan_x_tracker.get_value(),
                arctan_graph,
                line_class=DashedLine,
                color=WHITE,
            )
        )
        tan_x_tracker = ValueTracker(2 * PI / 8)
        tan_v_line = always_redraw(
            lambda: self.get_vertical_line_to_graph(
                tan_x_tracker.get_value(),
                tan_graph,
                line_class=DashedLine,
                color=WHITE,
            )
        )

        self.add(axes)
        self.play(
            ShowCreation(id_graph),
            Write(id_label)
        )
        self.play(
            ShowCreation(arctan_graph),
            Write(arctan_label)
        )
        self.add(arctan_v_line)
        self.play(arctan_x_tracker.set_value, 0, run_time=2)
        self.wait()
        self.play(
            TransformFromCopy(arctan_graph, tan_graph),
            TransformFromCopy(arctan_label, tan_label),
        )
        self.add(tan_v_line)
        self.play(tan_x_tracker.set_value, 0, run_time=2)
        self.wait()


class UnitCircleIntuition(Scene):
    def construct(self):
        self.draw_unit_circle()
        self.show_angle()
        self.show_fraction()
        self.show_fraction_approximation()

    def draw_unit_circle(self):
        unit_size = 2.5
        axes = Axes(
            number_line_config={"unit_size": unit_size},
            x_min=-2.5, x_max=2.5,
            y_min=-1.5, y_max=1.5,
        )
        axes.set_stroke(width=1)
        self.add(axes)

        radius_line = Line(ORIGIN, axes.coords_to_point(1, 0))
        radius_line.set_color(BLUE)
        r_label = TexMobject("1")
        r_label.add_updater(
            lambda m: m.next_to(radius_line.get_center(), DOWN, SMALL_BUFF)
        )
        circle = Circle(radius=unit_size, color=WHITE)

        self.add(radius_line, r_label)
        self.play(
            Rotating(radius_line, about_point=ORIGIN),
            ShowCreation(circle),
            run_time=2,
            rate_func=smooth,
        )

        self.radius_line = radius_line
        self.r_label = r_label
        self.circle = circle
        self.axes = axes

    def show_angle(self):
        circle = self.circle

        tan_eq = TexMobject(
            "\\tan", "(", "\\theta", ")", "=",
            tex_to_color_map={"\\theta": RED},
        )
        tan_eq.next_to(ORIGIN, RIGHT, LARGE_BUFF)
        tan_eq.to_edge(UP, buff=LARGE_BUFF)

        theta_tracker = ValueTracker(0)
        get_theta = theta_tracker.get_value

        def get_r_line():
            return Line(
                circle.get_center(),
                circle.point_at_angle(get_theta())
            )
        r_line = always_redraw(get_r_line)

        def get_arc(radius=None, **kwargs):
            if radius is None:
                alpha = inverse_interpolate(0, 20 * DEGREES, get_theta())
                radius = interpolate(2, 1, alpha)
            return Arc(
                radius=radius,
                start_angle=0,
                angle=get_theta(),
                arc_center=circle.get_center(),
                **kwargs
            )
        arc = always_redraw(get_arc)
        self.circle_arc = always_redraw(
            lambda: get_arc(radius=circle.radius, color=RED)
        )

        def get_theta_label():
            label = TexMobject("\\theta")
            label.set_height(min(arc.get_height(), 0.3))
            label.set_color(RED)
            center = circle.get_center()
            vect = arc.point_from_proportion(0.5) - center
            vect = (get_norm(vect) + 2 * SMALL_BUFF) * normalize(vect)
            label.move_to(center + vect)
            return label
        theta_label = always_redraw(get_theta_label)

        def get_height_line():
            p2 = circle.point_at_angle(get_theta())
            p1 = np.array(p2)
            p1[1] = circle.get_center()[1]
            return Line(
                p1, p2,
                stroke_color=YELLOW,
                stroke_width=3,
            )
        self.height_line = always_redraw(get_height_line)

        def get_width_line():
            p2 = circle.get_center()
            p1 = circle.point_at_angle(get_theta())
            p1[1] = p2[1]
            return Line(
                p1, p2,
                stroke_color=PINK,
                stroke_width=3,
            )
        self.width_line = always_redraw(get_width_line)

        def get_h_label():
            label = TexMobject("h")
            height_line = self.height_line
            label.match_color(height_line)
            label.set_height(min(height_line.get_height(), 0.3))
            label.set_stroke(BLACK, 3, background=True)
            label.next_to(height_line, RIGHT, SMALL_BUFF)
            return label
        self.h_label = always_redraw(get_h_label)

        def get_w_label():
            label = TexMobject("w")
            width_line = self.width_line
            label.match_color(width_line)
            label.next_to(width_line, DOWN, SMALL_BUFF)
            return label
        self.w_label = always_redraw(get_w_label)

        self.add(r_line, theta_label, arc, self.radius_line)
        self.play(
            FadeInFromDown(tan_eq),
            theta_tracker.set_value, 20 * DEGREES,
        )
        self.wait()

        self.tan_eq = tan_eq
        self.theta_tracker = theta_tracker

    def show_fraction(self):
        height_line = self.height_line
        width_line = self.width_line
        h_label = self.h_label
        w_label = self.w_label
        tan_eq = self.tan_eq

        rhs = TexMobject(
            "{\\text{height}", "\\over", "\\text{width}}"
        )
        rhs.next_to(tan_eq, RIGHT)
        rhs.get_part_by_tex("height").match_color(height_line)
        rhs.get_part_by_tex("width").match_color(width_line)

        for mob in [height_line, width_line, h_label, w_label]:
            mob.update()

        self.play(
            ShowCreation(height_line.copy().clear_updaters(), remover=True),
            FadeInFrom(h_label.copy().clear_updaters(), RIGHT, remover=True),
            Write(rhs[:2])
        )
        self.add(height_line, h_label)
        self.play(
            ShowCreation(width_line.copy().clear_updaters(), remover=True),
            FadeInFrom(w_label.copy().clear_updaters(), UP, remover=True),
            self.r_label.fade, 1,
            Write(rhs[2])
        )
        self.add(width_line, w_label)
        self.wait()

        self.rhs = rhs

    def show_fraction_approximation(self):
        theta_tracker = self.theta_tracker
        approx_rhs = TexMobject(
            "\\approx", "{\\theta", "\\over", "1}",
        )
        height, over1, width = self.rhs
        approx, theta, over2, one = approx_rhs
        approx_rhs.set_color_by_tex("\\theta", RED)
        approx_rhs.next_to(self.rhs, RIGHT, MED_SMALL_BUFF)

        self.play(theta_tracker.set_value, 5 * DEGREES)
        self.play(Write(VGroup(approx, over2)))
        self.wait()
        self.play(Indicate(width))
        self.play(TransformFromCopy(width, one))
        self.wait()
        self.play(Indicate(height))
        self.play(TransformFromCopy(height, theta))
        self.wait()


class TangentTaylorSeries(TeacherStudentsScene):
    def construct(self):
        series = TexMobject(
            "\\tan", "(", "\\theta", ")", "=", "\\theta", "+",
            "\\frac{1}{3}", "\\theta", "^3", "+",
            "\\frac{2}{15}", "\\theta", "^5", "+", "\\cdots",
            tex_to_color_map={"\\theta": YELLOW},
        )
        series.move_to(2 * UP)
        series.move_to(self.hold_up_spot, DOWN)
        series_error = series[7:]
        series_error_rect = SurroundingRectangle(series_error)

        example = TexMobject(
            "\\tan", "\\left(", "\\frac{1}{100}", "\\right)",
            "=", "\\frac{1}{100}", "+",
            "\\frac{1}{3}", "\\left(",
            "\\frac{1}{1{,}000{,}000}",
            "\\right)", "+",
            "\\frac{2}{15}", "\\left(",
            "\\frac{1}{10{,}000{,}000{,}000}",
            "\\right)", "+", "\\cdots",
        )
        example.set_color_by_tex("\\frac{1}{1", BLUE)
        example.set_width(FRAME_WIDTH - 1)
        example.next_to(self.students, UP, buff=2)
        example.shift_onto_screen()
        error = example[7:]
        error_rect = SurroundingRectangle(error)
        error_rect.set_color(RED)
        error_decimal = DecimalNumber(
            np.tan(0.01) - 0.01,
            num_decimal_places=15,
        )
        error_decimal.next_to(error_rect, DOWN)
        approx = TexMobject("\\approx")
        approx.next_to(error_decimal, LEFT)
        error_decimal.add(approx)
        error_decimal.match_color(error_rect)

        self.play(
            FadeInFromDown(series),
            self.teacher.change, "raise_right_hand",
        )
        self.play(
            ShowCreation(series_error_rect),
            self.get_student_changes(*3 * ["pondering"])
        )
        self.play(FadeOut(series_error_rect))
        self.play(
            series.center, series.to_edge, UP,
        )
        self.look_at(series)
        self.play(
            TransformFromCopy(series[:8], example[:8]),
            TransformFromCopy(series[8], example[9]),
            TransformFromCopy(series[10:12], example[11:13]),
            TransformFromCopy(series[12], example[14]),
            TransformFromCopy(series[14:], example[16:]),
            *map(GrowFromCenter, [example[i] for i in (8, 10, 13, 15)])
        )
        self.change_student_modes("happy", "confused", "sad")
        self.play(ShowCreation(error_rect))
        self.play(ShowIncreasingSubsets(error_decimal))
        self.change_all_student_modes("hooray")
        self.wait(3)


class AnalyzeCircleGeometry1e4(AnalyzeCircleGeometry):
    CONFIG = {
        "mass_ratio": 10000,
    }


class SumUpWrapper(Scene):
    def construct(self):
        title = TextMobject("To sum up:")
        title.scale(1.5)
        title.to_edge(UP)
        screen_rect = ScreenRectangle(height=6)
        screen_rect.set_fill(BLACK, 1)
        screen_rect.next_to(title, DOWN)
        self.add(FullScreenFadeRectangle(
            fill_color=DARK_GREY,
            fill_opacity=0.5
        ))
        self.play(
            FadeInFromDown(title),
            FadeIn(screen_rect),
        )
        self.wait()


class ConservationLawSummary(Scene):
    def construct(self):
        energy_eq = TexMobject(
            "\\frac{1}{2}", "m_1", "(", "v_1", ")", "^2", "+",
            "\\frac{1}{2}", "m_2", "(", "v_2", ")", "^2", "=",
            "\\text{const.}",
        )
        energy_word = TextMobject("Energy")
        energy_word.scale(2)
        circle = Circle(color=YELLOW, radius=2)
        energy_group = VGroup(energy_word, energy_eq, circle)
        momentum_eq = TexMobject(
            "m_1", "v_1", "+", "m_2", "v_2", "=",
            "\\text{const.}",
        )
        momentum_word = TextMobject("Momentum")
        momentum_word.scale(2)
        line = Line(ORIGIN, RIGHT + np.sqrt(10) * DOWN)
        line.set_color(GREEN)
        momentum_group = VGroup(momentum_word, momentum_eq, line)

        equations = VGroup(energy_eq, momentum_eq)
        words = VGroup(energy_word, momentum_word)

        for equation in equations:
            equation.set_color_by_tex("m_", BLUE)
            equation.set_color_by_tex("v_", RED)

        words.arrange(
            DOWN, buff=3,
        )
        words.to_edge(LEFT, buff=1.5)

        for group in energy_group, momentum_group:
            arrow = Arrow(
                LEFT, 2 * RIGHT,
                rectangular_stem_width=0.1,
                tip_length=0.5,
                color=WHITE
            )
            arrow.next_to(group[0], RIGHT)
            group[1].next_to(group[0], DOWN)
            group[2].next_to(arrow, RIGHT)
            group[2].set_stroke(width=6)
            group.add(arrow)
        # line.scale(4, about_edge=DR)
        red_energy_word = energy_word.copy()
        red_energy_word.set_fill(opacity=0)
        red_energy_word.set_stroke(RED, 2)

        self.add(energy_group, momentum_group)
        self.wait()
        self.play(
            LaggedStartMap(
                ShowCreationThenDestruction,
                red_energy_word
            ),
        )
        for color in [RED, BLUE, PINK, YELLOW]:
            self.play(ShowCreation(
                circle.copy().set_color(color),
            ))


class FinalCommentsOnPhaseSpace(Scene):
    def construct(self):
        self.add_title()
        self.show_related_fields()
        self.state_to_point()
        self.puzzle_as_remnant()

    def add_title(self):
        title = self.title = TextMobject("Phase space")
        title.scale(2)
        title.to_edge(UP)
        title.set_color(YELLOW)

        self.play(Write(title))

    def show_related_fields(self):
        title = self.title

        images = Group(
            ImageMobject("ClacksThumbnail"),
            ImageMobject("PictoralODE"),
            # ImageMobject("DoublePendulumStart"),
            ImageMobject("MobiusStrip"),
        )
        colors = [BLUE_D, GREY_BROWN, BLUE_C]
        for image, color in zip(images, colors):
            image.set_height(2.5)
            image.add(SurroundingRectangle(
                image,
                color=color,
                stroke_width=5,
                buff=0,
            ))
        images.arrange(RIGHT)
        images.move_to(DOWN)

        arrows = VGroup(*[
            Arrow(
                title.get_bottom(), image.get_top(),
                color=WHITE,
            )
            for image in images
        ])

        for image, arrow in zip(images, arrows):
            self.play(
                GrowArrow(arrow),
                GrowFromPoint(image, title.get_bottom()),
            )
            self.wait()
        self.wait()

        self.to_fade = Group(images, arrows)

    def state_to_point(self):
        state = TextMobject("State")
        arrow = Arrow(
            2 * LEFT, 2 * RIGHT,
            color=WHITE,
            rectangular_stem_width=0.1,
            tip_length=0.5
        )
        point = TextMobject("Point")
        dynamics = TextMobject("Dynamics")
        geometry = TextMobject("Geometry")
        words = VGroup(state, point, dynamics, geometry)
        for word in words:
            word.scale(2)

        group = VGroup(state, arrow, point)
        group.arrange(RIGHT, buff=MED_LARGE_BUFF)
        group.move_to(2.5 * DOWN)

        dynamics.move_to(state, RIGHT)
        geometry.move_to(point, LEFT)

        self.play(
            FadeOutAndShift(self.to_fade, UP),
            FadeInFrom(state, UP)
        )
        self.play(
            GrowArrow(arrow),
            FadeInFrom(point, LEFT)
        )
        self.wait(2)
        for w1, w2 in [(state, dynamics), (point, geometry)]:
            self.play(
                FadeOutAndShift(w1, UP),
                FadeInFrom(w2, DOWN),
            )
            self.wait()
        self.wait()

    def puzzle_as_remnant(self):
        pass


class AltShowTwoPopulations(ShowTwoPopulations):
    CONFIG = {
        "count_word_scale_val": 2,
    }


class SimpleTeacherHolding(TeacherStudentsScene):
    def construct(self):
        self.play(self.teacher.change, "raise_right_hand")
        self.change_all_student_modes("pondering")
        self.wait(3)


class EndScreen(PatreonEndScreen):
    CONFIG = {
        "specific_patrons": [
            "Juan Benet",
            "Vassili Philippov",
            "Burt Humburg",
            "Matt Russell",
            "soekul",
            "Richard Barthel",
            "Nathan Jessurun",
            "Ali Yahya",
            "dave nicponski",
            "Yu Jun",
            "Kaustuv DeBiswas",
            "Yana Chernobilsky",
            "Lukas Biewald",
            "Arthur Zey",
            "Roy Larson",
            "Joseph Kelly",
            "Peter Mcinerney",
            "Scott Walter, Ph.D.",
            "Magnus Lysfjord",
            "Evan Phillips",
            "Graham",
            "Mauricio Collares",
            "Quantopian",
            "Jordan Scales",
            "Lukas -krtek.net- Novy",
            "John Shaughnessy",
            "Joseph John Cox",
            "Ryan Atallah",
            "Britt Selvitelle",
            "Jonathan Wilson",
            "Randy C. Will",
            "Magnus Dahlström",
            "David Gow",
            "J",
            "Luc Ritchie",
            "Rish Kundalia",
            "Bob Sanderson",
            "Mathew Bramson",
            "Mustafa Mahdi",
            "Robert Teed",
            "Cooper Jones",
            "Jeff Linse",
            "John Haley",
            "Boris Veselinovich",
            "Andrew Busey",
            "Awoo",
            "Linh Tran",
            "Ripta Pasay",
            "David Clark",
            "Mathias Jansson",
            "Clark Gaebel",
            "Bernd Sing",
            "Jason Hise",
            "Ankalagon",
            "Dave B",
            "Ted Suzman",
            "Chris Connett",
            "Eric Younge",
            "1stViewMaths",
            "Jacob Magnuson",
            "Jonathan Eppele",
            "Delton Ding",
            "James Hughes",
            "Stevie Metke",
            "Yaw Etse",
            "John Griffith",
            "Magister Mugit",
            "Ludwig Schubert",
            "Giovanni Filippi",
            "Matt Langford",
            "Matt Roveto",
            "Jameel Syed",
            "Richard Burgmann",
            "Solara570",
            "Alexis Olson",
            "Jeff Straathof",
            "John V Wertheim",
            "Sindre Reino Trosterud",
            "Song Gao",
            "Peter Ehrnstrom",
            "Valeriy Skobelev",
            "Art Ianuzzi",
            "Michael Faust",
            "Omar Zrien",
            "Adrian Robinson",
            "Federico Lebron",
            "Kai-Siang Ang",
            "Michael Hardel",
            "Nero Li",
            "Ryan Williams",
            "Charles Southerland",
            "Devarsh Desai",
            "Hal Hildebrand",
            "Jan Pijpers",
            "L0j1k",
            "Mark B Bahu",
            "Márton Vaitkus",
            "Richard Comish",
            "Zach Cardwell",
            "Brian Staroselsky",
            "Matthew Cocke",
            "Christian Kaiser",
            "Danger Dai",
            "Dave Kester",
            "eaglle",
            "Florian Chudigiewitsch",
            "Roobie",
            "Xavier Bernard",
            "YinYangBalance.Asia",
            "Eryq Ouithaqueue",
            "Kanan Gill",
            "j eduardo perez",
            "Antonio Juarez",
            "Owen Campbell-Moore",
        ],
    }


class SolutionThumbnail(Thumbnail):
    CONFIG = {
        "sliding_blocks_config": {
            "block1_config": {
                "label_text": "$100^{d}$ kg",
            },
            "collect_clack_data": False,
        },
    }

    def add_text(self):
        word = TextMobject("Solution")
        question = TextMobject("How many collisions?")
        word.set_width(7)
        question.match_width(word)
        question.next_to(word, UP)
        group = VGroup(word, question)
        group.to_edge(UP, buff=MED_LARGE_BUFF)
        word.set_color(RED)
        question.set_color(YELLOW)
        group.set_stroke(RED, 2, background=True)
        self.add(group)
