 # -*- coding: utf-8 -*-
from big_ol_pile_of_manim_imports import *


def apply_function_to_center(point_func, mobject):
    mobject.apply_function_to_position(point_func)


def apply_function_to_submobjects(point_func, mobject):
    mobject.apply_function_to_submobject_positions(point_func)


def apply_function_to_points(point_func, mobject):
    mobject.apply_function(point_func)


def get_nested_one_plus_one_over_x(n_terms, bottom_term="x"):
    tex = "1+ {1 \\over" * n_terms + bottom_term + "}" * n_terms
    return TexMobject(tex, substrings_to_isolate=["1", "\\over", bottom_term])


def get_phi_continued_fraction(n_terms):
    return get_nested_one_plus_one_over_x(n_terms, bottom_term="1+\\cdots")


def get_nested_f(n_terms, arg="x"):
    terms = ["f("] * n_terms + [arg] + [")"] * n_terms
    return TexMobject(*terms)


# Scene types

class NumberlineTransformationScene(ZoomedScene):
    CONFIG = {
        "input_line_zero_point": 0.5 * UP + (FRAME_X_RADIUS - 1) * LEFT,
        "output_line_zero_point": 2 * DOWN + (FRAME_X_RADIUS - 1) * LEFT,
        "number_line_config": {
            "include_numbers": True,
            "x_min": -3.5,
            "x_max": 3.5,
            "unit_size": 2,
        },
        # These would override number_line_config
        "input_line_config": {
            "color": BLUE,
        },
        "output_line_config": {},
        "num_inserted_number_line_curves": 20,
        "default_delta_x": 0.1,
        "default_sample_dot_radius": 0.07,
        "default_sample_dot_colors": [RED, YELLOW],
        "default_mapping_animation_config": {
            "run_time": 3,
            # "path_arc": 30 * DEGREES,
        },
        "local_coordinate_num_decimal_places": 2,
        "zoom_factor": 0.1,
        "zoomed_display_height": 2.5,
        "zoomed_display_corner_buff": MED_SMALL_BUFF,
        "mini_line_scale_factor": 2,
        "default_coordinate_value_dx": 0.05,
        "zoomed_camera_background_rectangle_fill_opacity": 1.0,
    }

    def setup(self):
        ZoomedScene.setup(self)
        self.setup_number_lines()
        self.setup_titles()
        self.setup_zoomed_camera_background_rectangle()

    def setup_number_lines(self):
        number_lines = self.number_lines = VGroup()
        added_configs = (self.input_line_config, self.output_line_config)
        zero_opints = (self.input_line_zero_point, self.output_line_zero_point)
        for added_config, zero_point in zip(added_configs, zero_opints):
            full_config = dict(self.number_line_config)
            full_config.update(added_config)
            number_line = NumberLine(**full_config)
            number_line.insert_n_curves(
                self.num_inserted_number_line_curves
            )
            number_line.shift(zero_point - number_line.number_to_point(0))
            number_lines.add(number_line)
        self.input_line, self.output_line = number_lines

        self.add(number_lines)

    def setup_titles(self):
        input_title, output_title = self.titles = VGroup(*[
            TextMobject(word)
            for word in ("Inputs", "Outputs")
        ])
        vects = [UP, DOWN]
        for title, line, vect in zip(self.titles, self.number_lines, vects):
            title.next_to(line, vect, aligned_edge=LEFT)
            title.shift_onto_screen()

        self.add(self.titles)

    def setup_zoomed_camera_background_rectangle(self):
        frame = self.zoomed_camera.frame
        frame.next_to(self.camera.frame, UL)
        self.zoomed_camera_background_rectangle = BackgroundRectangle(
            frame, fill_opacity=self.zoomed_camera_background_rectangle_fill_opacity
        )
        self.zoomed_camera_background_rectangle_anim = UpdateFromFunc(
            self.zoomed_camera_background_rectangle,
            lambda m: m.replace(frame, stretch=True)
        )
        self.zoomed_camera_background_rectangle_group = VGroup(
            self.zoomed_camera_background_rectangle,
        )

    def get_sample_input_points(self, x_min=None, x_max=None, delta_x=None):
        x_min = x_min or self.input_line.x_min
        x_max = x_max or self.input_line.x_max
        delta_x = delta_x or self.default_delta_x
        return [
            self.get_input_point(x)
            for x in np.arange(x_min, x_max + delta_x, delta_x)
        ]

    def get_sample_dots(self, x_min=None, x_max=None,
                        delta_x=None, dot_radius=None, colors=None):
        dot_radius = dot_radius or self.default_sample_dot_radius
        colors = colors or self.default_sample_dot_colors

        dots = VGroup(*[
            Dot(point, radius=dot_radius)
            for point in self.get_sample_input_points(x_min, x_max, delta_x)
        ])
        dots.set_color_by_gradient(*colors)
        return dots

    def get_local_sample_dots(self, x, sample_radius=None, **kwargs):
        zoom_factor = self.get_zoom_factor()
        delta_x = kwargs.get("delta_x", self.default_delta_x * zoom_factor)
        dot_radius = kwargs.get("dot_radius", self.default_sample_dot_radius * zoom_factor)

        if sample_radius is None:
            unrounded_radius = self.zoomed_camera.frame.get_width() / 2
            sample_radius = int(unrounded_radius / delta_x) * delta_x
        config = {
            "x_min": x - sample_radius,
            "x_max": x + sample_radius,
            "delta_x": delta_x,
            "dot_radius": dot_radius,
        }
        config.update(kwargs)
        return self.get_sample_dots(**config)

    def add_sample_dot_ghosts(self, sample_dots, fade_factor=0.5):
        self.sample_dot_ghosts = sample_dots.copy()
        self.sample_dot_ghosts.fade(fade_factor)
        self.add(self.sample_dot_ghosts, sample_dots)

    def get_local_coordinate_values(self, x, dx=None, n_neighbors=1):
        dx = dx or self.default_coordinate_value_dx
        return [
            x + n * dx
            for n in range(-n_neighbors, n_neighbors + 1)
        ]

    # Mapping animations
    def get_mapping_animation(self, func, mobject,
                              how_to_apply_func=apply_function_to_center,
                              **kwargs):
        anim_config = dict(self.default_mapping_animation_config)
        anim_config.update(kwargs)

        point_func = self.number_func_to_point_func(func)

        mobject.generate_target(use_deepcopy=True)
        how_to_apply_func(point_func, mobject.target)
        return MoveToTarget(mobject, **anim_config)

    def get_line_mapping_animation(self, func, **kwargs):
        input_line_copy = self.input_line.deepcopy()
        self.moving_input_line = input_line_copy
        input_line_copy.remove(input_line_copy.numbers)
        # input_line_copy.set_stroke(width=2)
        input_line_copy.insert_n_curves(
            self.num_inserted_number_line_curves
        )
        return AnimationGroup(
            self.get_mapping_animation(
                func, input_line_copy,
                apply_function_to_points
            ),
            self.get_mapping_animation(
                func, input_line_copy.tick_marks,
                apply_function_to_submobjects
            ),
        )

    def get_sample_dots_mapping_animation(self, func, dots, **kwargs):
        return self.get_mapping_animation(
            func, dots, how_to_apply_func=apply_function_to_submobjects
        )

    def get_zoomed_camera_frame_mapping_animation(self, func, x=None, **kwargs):
        frame = self.zoomed_camera.frame
        if x is None:
            point = frame.get_center()
        else:
            point = self.get_input_point(x)
        point_mob = VectorizedPoint(point)
        return AnimationGroup(
            self.get_mapping_animation(func, point_mob),
            UpdateFromFunc(frame, lambda m: m.move_to(point_mob)),
        )

    def apply_function(self, func,
                       apply_function_to_number_line=True,
                       sample_dots=None,
                       local_sample_dots=None,
                       target_coordinate_values=None,
                       added_anims=None,
                       **kwargs
                       ):
        zcbr_group = self.zoomed_camera_background_rectangle_group
        zcbr_anim = self.zoomed_camera_background_rectangle_anim
        frame = self.zoomed_camera.frame

        anims = []
        if apply_function_to_number_line:
            anims.append(self.get_line_mapping_animation(func))
        if hasattr(self, "mini_line"):  # Test for if mini_line is in self?
            anims.append(self.get_mapping_animation(
                func, self.mini_line,
                how_to_apply_func=apply_function_to_center
            ))
        if sample_dots:
            anims.append(
                self.get_sample_dots_mapping_animation(func, sample_dots)
            )
        if self.zoom_activated:
            zoom_anim = self.get_zoomed_camera_frame_mapping_animation(func)
            anims.append(zoom_anim)
            anims.append(zcbr_anim)

            zoom_anim.update(1)
            target_mini_line = Line(frame.get_left(), frame.get_right())
            target_mini_line.scale(self.mini_line_scale_factor)
            target_mini_line.match_style(self.output_line)
            zoom_anim.update(0)
            zcbr_group.submobjects.insert(1, target_mini_line)
        if target_coordinate_values:
            coordinates = self.get_local_coordinates(
                self.output_line,
                *target_coordinate_values
            )
            anims.append(FadeIn(coordinates))
            zcbr_group.add(coordinates)
            self.local_target_coordinates = coordinates
        if local_sample_dots:
            anims.append(
                self.get_sample_dots_mapping_animation(func, local_sample_dots)
            )
            zcbr_group.add(local_sample_dots)
        if added_anims:
            anims += added_anims
        anims.append(Animation(zcbr_group))

        self.play(*anims, **kwargs)

    # Zooming
    def zoom_in_on_input(self, x,
                         local_sample_dots=None,
                         local_coordinate_values=None,
                         pop_out=True,
                         first_added_anims=None,
                         first_anim_kwargs=None,
                         second_added_anims=None,
                         second_anim_kwargs=None,
                         zoom_factor=None,
                         ):

        first_added_anims = first_added_anims or []
        first_anim_kwargs = first_anim_kwargs or {}
        second_added_anims = second_added_anims or []
        second_anim_kwargs = second_anim_kwargs or {}
        input_point = self.get_input_point(x)

        # Decide how to move camera frame into place
        frame = self.zoomed_camera.frame
        frame.generate_target()
        frame.target.move_to(input_point)
        if zoom_factor:
            frame.target.set_height(
                self.zoomed_display.get_height() * zoom_factor
            )
        movement = MoveToTarget(frame)
        zcbr = self.zoomed_camera_background_rectangle
        zcbr_group = self.zoomed_camera_background_rectangle_group
        zcbr_anim = self.zoomed_camera_background_rectangle_anim
        anims = []
        if self.zoom_activated:
            anims.append(movement)
            anims.append(zcbr_anim)
        else:
            movement.update(1)
            zcbr_anim.update(1)
            anims.append(self.get_zoom_in_animation())
            anims.append(FadeIn(zcbr))

        # Make sure frame is in final place
        for anim in anims:
            anim.update(1)

        # Add miniature number_line
        mini_line = self.mini_line = Line(frame.get_left(), frame.get_right())
        mini_line.scale(self.mini_line_scale_factor)
        mini_line.insert_n_curves(self.num_inserted_number_line_curves)
        mini_line.match_style(self.input_line)
        mini_line_copy = mini_line.copy()
        zcbr_group.add(mini_line_copy, mini_line)
        anims += [FadeIn(mini_line), FadeIn(mini_line_copy)]

        # Add tiny coordinates
        if local_coordinate_values is None:
            local_coordinate_values = [x]
        local_coordinates = self.get_local_coordinates(
            self.input_line,
            *local_coordinate_values
        )
        anims.append(FadeIn(local_coordinates))
        zcbr_group.add(local_coordinates)
        self.local_coordinates = local_coordinates

        # Add tiny dots
        if local_sample_dots is not None:
            anims.append(LaggedStartMap(GrowFromCenter, local_sample_dots))
            zcbr_group.add(local_sample_dots)

        if first_added_anims:
            anims += first_added_anims

        anims.append(Animation(zcbr_group))
        if not pop_out:
            self.activate_zooming(animate=False)
        self.play(*anims, **first_anim_kwargs)

        if not self.zoom_activated and pop_out:
            self.activate_zooming(animate=False)
            added_anims = second_added_anims or []
            self.play(
                self.get_zoomed_display_pop_out_animation(),
                *added_anims,
                **second_anim_kwargs
            )

    def get_local_coordinates(self, line, *x_values, **kwargs):
        num_decimal_places = kwargs.get(
            "num_decimal_places", self.local_coordinate_num_decimal_places
        )
        result = VGroup()
        result.tick_marks = VGroup()
        result.numbers = VGroup()
        result.add(result.tick_marks, result.numbers)
        for x in x_values:
            tick_mark = Line(UP, DOWN)
            tick_mark.set_height(
                0.15 * self.zoomed_camera.frame.get_height()
            )
            tick_mark.move_to(line.number_to_point(x))
            result.tick_marks.add(tick_mark)

            number = DecimalNumber(x, num_decimal_places=num_decimal_places)
            number.scale(self.get_zoom_factor())
            number.scale(0.5)  # To make it seem small
            number.next_to(tick_mark, DOWN, buff=0.5 * number.get_height())
            result.numbers.add(number)
        return result

    def get_mobjects_in_zoomed_camera(self, mobjects):
        frame = self.zoomed_camera.frame
        x_min = frame.get_left()[0]
        x_max = frame.get_right()[0]
        y_min = frame.get_bottom()[1]
        y_max = frame.get_top()[1]
        result = VGroup()
        for mob in mobjects:
            for point in mob.get_all_points():
                if (x_min < point[0] < x_max) and (y_min < point[1] < y_max):
                    result.add(mob)
                    break
        return result

    # Helpers
    def get_input_point(self, x):
        return self.input_line.number_to_point(x)

    def get_output_point(self, fx):
        return self.output_line.number_to_point(fx)

    def number_func_to_point_func(self, number_func):
        input_line, output_line = self.number_lines

        def point_func(point):
            input_number = input_line.point_to_number(point)
            output_number = number_func(input_number)
            return output_line.number_to_point(output_number)
        return point_func


class ExampleNumberlineTransformationScene(NumberlineTransformationScene):
    CONFIG = {
        "number_line_config": {
            "x_min": 0,
            "x_max": 5,
            "unit_size": 2.0
        },
        "output_line_config": {
            "x_max": 20,
        },
    }

    def construct(self):
        func = lambda x: x**2
        x = 3
        dx = 0.05

        sample_dots = self.get_sample_dots()
        local_sample_dots = self.get_local_sample_dots(x)

        self.play(LaggedStartMap(GrowFromCenter, sample_dots))
        self.zoom_in_on_input(
            x,
            local_sample_dots=local_sample_dots,
            local_coordinate_values=[x - dx, x, x + dx],
        )
        self.wait()
        self.apply_function(
            func,
            sample_dots=sample_dots,
            local_sample_dots=local_sample_dots,
            target_coordinate_values=[func(x) - dx, func(x), func(x) + dx],
        )
        self.wait()


# Scenes


class WriteOpeningWords(Scene):
    def construct(self):
        raw_string1 = "Dear calculus student,"
        raw_string2 = "You're about to go through your first course. Like " + \
                      "any new topic, it will take some hard work to understand,"
        words1, words2 = [
            TextMobject("\\Large", *rs.split(" "))
            for rs in (raw_string1, raw_string2)
        ]
        words1.next_to(words2, UP, aligned_edge=LEFT, buff=LARGE_BUFF)
        words = VGroup(*it.chain(words1, words2))
        words.set_width(FRAME_WIDTH - 2 * LARGE_BUFF)
        words.to_edge(UP)

        letter_wait = 0.05
        word_wait = 2 * letter_wait
        comma_wait = 5 * letter_wait
        for word in words:
            self.play(LaggedStartMap(
                FadeIn, word,
                run_time=len(word) * letter_wait,
                lag_ratio=1.5 / len(word)
            ))
            self.wait(word_wait)
            if word.get_tex_string()[-1] == ",":
                self.wait(comma_wait)


class StartingCalc101(PiCreatureScene):
    CONFIG = {
        "camera_config": {"background_opacity": 1},
        "image_frame_width": 3.5,
        "image_frame_height": 2.5,
    }

    def construct(self):
        self.show_you()
        self.show_images()
        self.show_mystery_topic()

    def show_you(self):
        randy = self.pi_creature
        title = self.title = Title("Calculus 101")
        you = TextMobject("You")
        arrow = Vector(DL, color=WHITE)
        arrow.next_to(randy, UR)
        you.next_to(arrow.get_start(), UP)

        self.play(
            Write(you),
            GrowArrow(arrow),
            randy.change, "erm", title
        )
        self.wait()
        self.play(Write(title, run_time=1))
        self.play(FadeOut(VGroup(arrow, you)))

    def show_images(self):
        randy = self.pi_creature
        images = self.get_all_images()
        modes = [
            "pondering",  # hard_work_image
            "pondering",  # neat_example_image
            "hesitant",  # not_so_neat_example_image
            "hesitant",  # physics_image
            "horrified",  # piles_of_formulas_image
            "horrified",  # getting_stuck_image
            "thinking",  # aha_image
            "thinking",  # graphical_intuition_image
        ]

        for i, image, mode in zip(it.count(), images, modes):
            anims = []
            if hasattr(image, "fade_in_anim"):
                anims.append(image.fade_in_anim)
                anims.append(FadeIn(image.frame))
            else:
                anims.append(FadeIn(image))

            if i >= 3:
                image_to_fade_out = images[i - 3]
                if hasattr(image_to_fade_out, "fade_out_anim"):
                    anims.append(image_to_fade_out.fade_out_anim)
                else:
                    anims.append(FadeOut(image_to_fade_out))

            if hasattr(image, "continual_animations"):
                self.add(*image.continual_animations)

            anims.append(ApplyMethod(randy.change, mode))
            self.play(*anims)
            self.wait()
            if i >= 3:
                if hasattr(image_to_fade_out, "continual_animations"):
                    self.remove(*image_to_fade_out.continual_animations)
                self.remove(image_to_fade_out.frame)
        self.wait(3)

        self.remaining_images = images[-3:]

    def show_mystery_topic(self):
        images = self.remaining_images
        randy = self.pi_creature

        mystery_box = Rectangle(
            width=self.image_frame_width,
            height=self.image_frame_height,
            stroke_color=YELLOW,
            fill_color=DARK_GREY,
            fill_opacity=0.5,
        )
        mystery_box.scale(1.5)
        mystery_box.next_to(self.title, DOWN, MED_LARGE_BUFF)

        rects = images[-1].rects.copy()
        rects.center()
        rects.set_height(FRAME_HEIGHT - 1)
        # image = rects.get_image()
        open_cv_image = cv2.imread(get_full_raster_image_path("alt_calc_hidden_image"))
        blurry_iamge = cv2.blur(open_cv_image, (50, 50))
        array = np.array(blurry_iamge)[:, :, ::-1]
        im_mob = ImageMobject(array)
        im_mob.replace(mystery_box, stretch=True)
        mystery_box.add(im_mob)

        q_marks = TexMobject("???").scale(3)
        q_marks.space_out_submobjects(1.5)
        q_marks.set_stroke(BLACK, 1)
        q_marks.move_to(mystery_box)
        mystery_box.add(q_marks)

        for image in images:
            if hasattr(image, "continual_animations"):
                self.remove(*image.continual_animations)
            self.play(
                image.shift, DOWN,
                image.fade, 1,
                randy.change, "erm",
                run_time=1.5
            )
            self.remove(image)
        self.wait()
        self.play(
            FadeInFromDown(mystery_box),
            randy.change, "confused"
        )
        self.wait(5)

    # Helpers

    def get_all_images(self):
        # Images matched to narration's introductory list
        images = VGroup(
            self.get_hard_work_image(),
            self.get_neat_example_image(),
            self.get_not_so_neat_example_image(),
            self.get_physics_image(),
            self.get_piles_of_formulas_image(),
            self.get_getting_stuck_image(),
            self.get_aha_image(),
            self.get_graphical_intuition_image(),
        )
        colors = color_gradient([BLUE, YELLOW], len(images))
        for i, image, color in zip(it.count(), images, colors):
            self.adjust_size(image)
            frame = Rectangle(
                width=self.image_frame_width,
                height=self.image_frame_height,
                color=color,
                stroke_width=2,
            )
            frame.move_to(image)
            image.frame = frame
            image.add(frame)
            image.next_to(self.title, DOWN)
            alt_i = (i % 3) - 1
            vect = (self.image_frame_width + LARGE_BUFF) * RIGHT
            image.shift(alt_i * vect)
        return images

    def adjust_size(self, group):
        group.set_width(min(
            group.get_width(),
            self.image_frame_width - 2 * MED_SMALL_BUFF
        ))
        group.set_height(min(
            group.get_height(),
            self.image_frame_height - 2 * MED_SMALL_BUFF
        ))
        return group

    def get_hard_work_image(self):
        new_randy = self.pi_creature.copy()
        new_randy.change_mode("telepath")
        bubble = new_randy.get_bubble(height=3.5, width=4)
        bubble.add_content(TexMobject("\\frac{d}{dx}(\\sin(\\sqrt{x}))"))
        bubble.add(bubble.content)  # Remove?

        return VGroup(new_randy, bubble)

    def get_neat_example_image(self):
        filled_circle = Circle(
            stroke_width=0,
            fill_color=BLUE_E,
            fill_opacity=1
        )
        area = TexMobject("\\pi r^2")
        area.move_to(filled_circle)
        unfilled_circle = Circle(
            stroke_width=3,
            stroke_color=YELLOW,
            fill_opacity=0,
        )
        unfilled_circle.next_to(filled_circle, RIGHT)
        circles = VGroup(filled_circle, unfilled_circle)
        circumference = TexMobject("2\\pi r")
        circumference.move_to(unfilled_circle)
        equation = TexMobject(
            "{d (\\pi r^2) \\over dr}  = 2\\pi r",
            tex_to_color_map={
                "\\pi r^2": BLUE_D,
                "2\\pi r": YELLOW,
            }
        )
        equation.next_to(circles, UP)

        return VGroup(
            filled_circle, area,
            unfilled_circle, circumference,
            equation
        )

    def get_not_so_neat_example_image(self):
        return TexMobject("\\int x \\cos(x) \\, dx")

    def get_physics_image(self):
        t_max = 6.5
        r = 0.2
        spring = ParametricFunction(
            lambda t: op.add(
                r * (np.sin(TAU * t) * RIGHT + np.cos(TAU * t) * UP),
                t * DOWN,
            ),
            t_min=0, t_max=t_max,
            color=WHITE,
            stroke_width=2,
        )
        spring.color_using_background_image("grey_gradient")

        weight = Square()
        weight.set_stroke(width=0)
        weight.set_fill(opacity=1)
        weight.color_using_background_image("grey_gradient")
        weight.set_height(0.4)

        t_tracker = ValueTracker(0)
        group = VGroup(spring, weight)
        group.continual_animations = [
            t_tracker.add_udpater(
                lambda tracker, dt: tracker.set_value(
                    tracker.get_value() + dt
                )
            ),
            spring.add_updater(
                lambda s: s.stretch_to_fit_height(
                    1.5 + 0.5 * np.cos(3 * t_tracker.get_value()),
                    about_edge=UP
                )
            ),
            weight.add_updater(
                lambda w: w.move_to(spring.points[-1])
            )
        ]

        def update_group_style(alpha):
            spring.set_stroke(width=2 * alpha)
            weight.set_fill(opacity=alpha)

        group.fade_in_anim = UpdateFromAlphaFunc(
            group,
            lambda g, a: update_group_style(a)
        )
        group.fade_out_anim = UpdateFromAlphaFunc(
            group,
            lambda g, a: update_group_style(1 - a)
        )
        return group

    def get_piles_of_formulas_image(self):
        return TexMobject("(f/g)' = \\frac{gf' - fg'}{g^2}")

    def get_getting_stuck_image(self):
        creature = self.pi_creature.copy()
        creature.change_mode("angry")
        equation = TexMobject("\\frac{d}{dx}(x^x)")
        equation.set_height(creature.get_height() / 2)
        equation.next_to(creature, RIGHT, aligned_edge=UP)
        creature.look_at(equation)
        return VGroup(creature, equation)

    def get_aha_image(self):
        creature = self.pi_creature.copy()
        creature.change_mode("hooray")
        from old_projects.eoc.chapter3 import NudgeSideLengthOfCube
        scene = NudgeSideLengthOfCube(
            end_at_animation_number=7,
            skip_animations=True
        )
        group = VGroup(
            scene.cube, scene.faces,
            scene.bars, scene.corner_cube,
        )
        group.set_height(0.75 * creature.get_height())
        group.next_to(creature, RIGHT)
        creature.look_at(group)
        return VGroup(creature, group)

    def get_graphical_intuition_image(self):
        gs = GraphScene()
        gs.setup_axes()
        graph = gs.get_graph(
            lambda x: 0.2 * (x - 3) * (x - 5) * (x - 6) + 4,
            x_min=2, x_max=8,
        )
        rects = gs.get_riemann_rectangles(
            graph, x_min=2, x_max=8,
            stroke_width=0.5,
            dx=0.25
        )
        gs.add(graph, rects, gs.axes)
        group = VGroup(*gs.mobjects)
        self.adjust_size(group)
        group.next_to(self.title, DOWN, MED_LARGE_BUFF)
        group.rects = rects
        group.continual_animations = [
            turn_animation_into_updater(Write(rects)),
            turn_animation_into_updater(ShowCreation(graph)),
            turn_animation_into_updater(FadeIn(gs.axes)),
        ]
        self.adjust_size(group)
        return group


class GraphicalIntuitions(GraphScene):
    CONFIG = {
        "func": lambda x: 0.1 * (x - 2) * (x - 5) * (x - 7) + 4,
        "x_labeled_nums": list(range(1, 10)),
    }

    def construct(self):
        self.setup_axes()
        axes = self.axes
        graph = self.get_graph(self.func)

        ss_group = self.get_secant_slope_group(
            x=8, graph=graph, dx=0.01,
            secant_line_length=6,
            secant_line_color=RED,
        )
        rects = self.get_riemann_rectangles(
            graph, x_min=2, x_max=8, dx=0.01, stroke_width=0
        )

        deriv_text = TextMobject(
            "Derivative $\\rightarrow$ slope",
            tex_to_color_map={"slope": ss_group.secant_line.get_color()}
        )
        deriv_text.to_edge(UP)
        integral_text = TextMobject(
            "Integral $\\rightarrow$ area",
            tex_to_color_map={"area": rects[0].get_color()}
        )
        integral_text.next_to(deriv_text, DOWN)

        self.play(
            Succession(Write(axes), ShowCreation(graph, run_time=2)),
            self.get_graph_words_anim(),
        )
        self.animate_secant_slope_group_change(
            ss_group,
            target_x=2,
            rate_func=smooth,
            run_time=2.5,
            added_anims=[
                Write(deriv_text),
                VFadeIn(ss_group, run_time=2),
            ]
        )
        self.play(FadeIn(integral_text))
        self.play(
            LaggedStartMap(
                GrowFromEdge, rects,
                lambda r: (r, DOWN)
            ),
            Animation(axes),
            Animation(graph),
        )
        self.wait()

    def get_graph_words_anim(self):
        words = VGroup(
            TextMobject("Graphs,"),
            TextMobject("graphs,"),
            TextMobject("non-stop graphs"),
            TextMobject("all day"),
            TextMobject("every day"),
            TextMobject("as if to visualize is to graph"),
        )
        for word in words:
            word.add_background_rectangle()
        words.arrange(DOWN)
        words.to_edge(UP)
        return LaggedStartMap(
            FadeIn, words,
            rate_func=there_and_back,
            run_time=len(words) - 1,
            lag_ratio=0.6,
            remover=True
        )


class Wrapper(Scene):
    CONFIG = {
        "title": "",
        "title_kwargs": {},
        "screen_height": 6,
        "wait_time": 2,
    }

    def construct(self):
        rect = self.rect = ScreenRectangle(height=self.screen_height)
        title = self.title = TextMobject(self.title, **self.title_kwargs)
        title.to_edge(UP)
        rect.next_to(title, DOWN)

        self.add(title)
        self.play(ShowCreation(rect))
        self.wait(self.wait_time)


class DomainColoringWrapper(Wrapper):
    CONFIG = {
        "title": "Complex $\\rightarrow$ Complex",
    }


class R2ToR2Wrapper(Wrapper):
    CONFIG = {"title": "$\\mathds{R}^2 \\rightarrow \\mathds{R}^2$"}


class ExampleMultivariableFunction(LinearTransformationScene):
    CONFIG = {
        "show_basis_vectors": False,
        "show_coordinates": True,
    }

    def construct(self):
        def example_function(point):
            x, y, z = point
            return np.array([
                x + np.sin(y),
                y + np.sin(x),
                0
            ])
        self.wait()
        self.apply_nonlinear_transformation(example_function, run_time=5)
        self.wait()


class ChangingVectorFieldWrapper(Wrapper):
    CONFIG = {"title": "$(x, y, t) \\rightarrow (x', y')$"}


class ChangingVectorField(Scene):
    CONFIG = {
        "wait_time": 30,
    }

    def construct(self):
        plane = self.plane = NumberPlane()
        plane.set_stroke(width=2)
        plane.add_coordinates()
        self.add(plane)

        # Obviously a silly thing to do, but I'm sweeping
        # through trying to make sure old scenes don't
        # completely break in spots which used to have
        # Continual animations
        time_tracker = self.time_tracker = ValueTracker(0)
        time_tracker.add_updater(
            lambda t: t.set_value(self.get_time())
        )

        vectors = self.get_vectors()
        vectors.add_updater(self.update_vectors)
        self.add(vectors)
        self.wait(self.wait_time)

    def get_vectors(self):
        vectors = VGroup()
        x_max = int(np.ceil(FRAME_WIDTH))
        y_max = int(np.ceil(FRAME_HEIGHT))
        step = 0.5
        for x in np.arange(-x_max, x_max + 1, step):
            for y in np.arange(-y_max, y_max + 1, step):
                point = x * RIGHT + y * UP
                vectors.add(Vector(RIGHT).shift(point))
        vectors.set_color_by_gradient(YELLOW, RED)
        return vectors

    def update_vectors(self, vectors):
        time = self.time_tracker.get_value()
        for vector in vectors:
            point = vector.get_start()
            out_point = self.func(point, time)
            norm = get_norm(out_point)
            if norm == 0:
                out_point = RIGHT  # Fake it
                vector.set_fill(opacity=0)
            else:
                out_point *= 0.5
                color = interpolate_color(BLUE, RED, norm / np.sqrt(8))
                vector.set_fill(color, opacity=1)
                vector.set_stroke(BLACK, width=1)
            new_x, new_y = out_point[:2]
            vector.put_start_and_end_on(
                point, point + new_x * RIGHT + new_y * UP
            )

    def func(self, point, time):
        x, y, z = point
        return np.array([
            np.sin(time + 0.5 * x + y),
            np.cos(time + 0.2 * x * y + 0.7),
            0
        ])


class MoreTopics(Scene):
    def construct(self):
        calculus = TextMobject("Calculus")
        calculus.next_to(LEFT, LEFT)
        calculus.set_color(YELLOW)
        calculus.add_background_rectangle()
        others = VGroup(
            TextMobject("Multivariable calculus"),
            TextMobject("Complex analysis"),
            TextMobject("Differential geometry"),
            TextMobject("$\\vdots$")
        )
        for word in others:
            word.add_background_rectangle()
        others.arrange(
            DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT,
        )
        others.next_to(RIGHT, RIGHT)
        lines = VGroup(*[
            Line(calculus.get_right(), word.get_left(), buff=MED_SMALL_BUFF)
            for word in others
        ])

        rect = FullScreenFadeRectangle(fill_opacity=0.7)
        self.add(rect)

        self.add(calculus)
        self.play(
            LaggedStartMap(ShowCreation, lines),
            LaggedStartMap(Write, others),
        )
        self.wait()

        self.calculus = calculus
        self.lines = lines
        self.full_screen_rect = rect
        self.other_topics = others


class TransformationalViewWrapper(Wrapper):
    CONFIG = {
        "title": "Transformational view"
    }


class SetTheStage(TeacherStudentsScene):
    def construct(self):
        ordinary = TextMobject("Ordinary visual")
        transformational = TextMobject("Transformational visual")
        for word in ordinary, transformational:
            word.move_to(self.hold_up_spot, DOWN)
            word.shift_onto_screen()

        self.screen.scale(1.25, about_edge=UL)
        self.add(self.screen)
        self.teacher_holds_up(
            ordinary,
            added_anims=[self.get_student_changes(*3 * ["sassy"])]
        )
        self.wait()
        self.play(
            ordinary.shift, UP,
            FadeInFromDown(transformational),
            self.teacher.change, "hooray",
            self.get_student_changes(*3 * ["erm"])
        )
        self.wait(3)
        self.change_all_student_modes("pondering", look_at_arg=self.screen)


class StandardDerivativeVisual(GraphScene):
    CONFIG = {
        "y_max": 8,
        "y_axis_height": 5,
    }

    def construct(self):
        self.add_title()
        self.show_function_graph()
        self.show_slope_of_graph()
        self.encourage_not_to_think_of_slope_as_definition()
        self.show_sensitivity()

    def add_title(self):
        title = self.title = TextMobject("Standard derivative visual")
        title.to_edge(UP)
        h_line = Line(LEFT, RIGHT)
        h_line.set_width(FRAME_WIDTH - 2 * LARGE_BUFF)
        h_line.next_to(title, DOWN)

        self.add(title, h_line)

    def show_function_graph(self):
        self.setup_axes()

        def func(x):
            x -= 5
            return 0.1 * (x + 3) * (x - 3) * x + 3
        graph = self.get_graph(func)
        graph_label = self.get_graph_label(graph, x_val=9.5)

        input_tracker = ValueTracker(4)

        def get_x_value():
            return input_tracker.get_value()

        def get_y_value():
            return graph.underlying_function(get_x_value())

        def get_x_point():
            return self.coords_to_point(get_x_value(), 0)

        def get_y_point():
            return self.coords_to_point(0, get_y_value())

        def get_graph_point():
            return self.coords_to_point(get_x_value(), get_y_value())

        def get_v_line():
            return DashedLine(get_x_point(), get_graph_point(), stroke_width=2)

        def get_h_line():
            return DashedLine(get_graph_point(), get_y_point(), stroke_width=2)

        input_triangle = RegularPolygon(n=3, start_angle=TAU / 4)
        output_triangle = RegularPolygon(n=3, start_angle=0)
        for triangle in input_triangle, output_triangle:
            triangle.set_fill(WHITE, 1)
            triangle.set_stroke(width=0)
            triangle.scale(0.1)

        input_triangle_update = input_tracker.add_updater(
            lambda m: m.move_to(get_x_point(), UP)
        )
        output_triangle_update = output_triangle.add_updater(
            lambda m: m.move_to(get_y_point(), RIGHT)
        )

        x_label = TexMobject("x")
        x_label_update = Mobject.add_updater(
            x_label, lambda m: m.next_to(input_triangle, DOWN, SMALL_BUFF)
        )

        output_label = TexMobject("f(x)")
        output_label_update = Mobject.add_updater(
            output_label, lambda m: m.next_to(
                output_triangle, LEFT, SMALL_BUFF)
        )

        v_line = get_v_line()
        v_line_update = Mobject.add_updater(
            v_line, lambda vl: Transform(vl, get_v_line()).update(1)
        )

        h_line = get_h_line()
        h_line_update = Mobject.add_updater(
            h_line, lambda hl: Transform(hl, get_h_line()).update(1)
        )

        graph_dot = Dot(color=YELLOW)
        graph_dot_update = Mobject.add_updater(
            graph_dot, lambda m: m.move_to(get_graph_point())
        )

        self.play(
            ShowCreation(graph),
            Write(graph_label),
        )
        self.play(
            DrawBorderThenFill(input_triangle, run_time=1),
            Write(x_label),
            ShowCreation(v_line),
            GrowFromCenter(graph_dot),
        )
        self.add_foreground_mobject(graph_dot)
        self.play(
            ShowCreation(h_line),
            Write(output_label),
            DrawBorderThenFill(output_triangle, run_time=1)
        )
        self.add(
            input_triangle_update,
            x_label_update,
            graph_dot_update,
            v_line_update,
            h_line_update,
            output_triangle_update,
            output_label_update,
        )
        self.play(
            input_tracker.set_value, 8,
            run_time=6,
            rate_func=there_and_back
        )

        self.input_tracker = input_tracker
        self.graph = graph

    def show_slope_of_graph(self):
        input_tracker = self.input_tracker
        deriv_input_tracker = ValueTracker(input_tracker.get_value())

        # Slope line
        def get_slope_line():
            return self.get_secant_slope_group(
                x=deriv_input_tracker.get_value(),
                graph=self.graph,
                dx=0.01,
                secant_line_length=4
            ).secant_line

        slope_line = get_slope_line()
        slope_line_update = Mobject.add_updater(
            slope_line, lambda sg: Transform(sg, get_slope_line()).update(1)
        )

        def position_deriv_label(deriv_label):
            deriv_label.next_to(slope_line, UP)
            return deriv_label
        deriv_label = TexMobject(
            "\\frac{df}{dx}(x) =", "\\text{Slope}", "="
        )
        deriv_label.get_part_by_tex("Slope").match_color(slope_line)
        deriv_label_update = Mobject.add_updater(
            deriv_label, position_deriv_label
        )

        slope_decimal = DecimalNumber(slope_line.get_slope())
        slope_decimal.match_color(slope_line)
        slope_decimal.add_updater(
            lambda d: d.set_value(slope_line.get_slope())
        )
        slope_decimal.add_upater(
            lambda d: d.next_to(
                deriv_label, RIGHT, SMALL_BUFF
            ).shift(0.2 * SMALL_BUFF * DOWN)
        )

        self.play(
            ShowCreation(slope_line),
            Write(deriv_label),
            Write(slope_decimal),
            run_time=1
        )
        self.wait()
        self.add(
            slope_line_update,
            # deriv_label_update,
        )
        for x in 9, 2, 4:
            self.play(
                input_tracker.set_value, x,
                deriv_input_tracker.set_value, x,
                run_time=3
            )
            self.wait()

        self.deriv_input_tracker = deriv_input_tracker

    def encourage_not_to_think_of_slope_as_definition(self):
        morty = Mortimer(height=2)
        morty.to_corner(DR)

        self.play(FadeIn(morty))
        self.play(PiCreatureSays(
            morty, "Don't think of \\\\ this as the definition",
            bubble_kwargs={"height": 2, "width": 4}
        ))
        self.play(Blink(morty))
        self.wait()
        self.play(
            RemovePiCreatureBubble(morty),
            UpdateFromAlphaFunc(
                morty, lambda m, a: m.set_fill(opacity=1 - a),
                remover=True
            )
        )

    def show_sensitivity(self):
        input_tracker = self.input_tracker
        deriv_input_tracker = self.deriv_input_tracker

        self.wiggle_input()
        for x in 9, 7, 2:
            self.play(
                input_tracker.set_value, x,
                deriv_input_tracker.set_value, x,
                run_time=3
            )
            self.wiggle_input()

    ###
    def wiggle_input(self, dx=0.5, run_time=3):
        input_tracker = self.input_tracker

        x = input_tracker.get_value()
        x_min = x - dx
        x_max = x + dx
        y, y_min, y_max = list(map(
            self.graph.underlying_function,
            [x, x_min, x_max]
        ))
        x_line = Line(
            self.coords_to_point(x_min, 0),
            self.coords_to_point(x_max, 0),
        )
        y_line = Line(
            self.coords_to_point(0, y_min),
            self.coords_to_point(0, y_max),
        )

        x_rect, y_rect = rects = VGroup(Rectangle(), Rectangle())
        rects.set_stroke(width=0)
        rects.set_fill(YELLOW, 0.5)
        x_rect.match_width(x_line)
        x_rect.stretch_to_fit_height(0.25)
        x_rect.move_to(x_line)
        y_rect.match_height(y_line)
        y_rect.stretch_to_fit_width(0.25)
        y_rect.move_to(y_line)

        self.play(
            ApplyMethod(
                input_tracker.set_value, input_tracker.get_value() + dx,
                rate_func=lambda t: wiggle(t, 6)
            ),
            FadeIn(
                rects,
                rate_func=squish_rate_func(smooth, 0, 0.33),
                remover=True,
            ),
            run_time=run_time,
        )
        self.play(FadeOut(rects))


class EoCWrapper(Scene):
    def construct(self):
        title = Title("Essence of calculus")
        self.play(Write(title))
        self.wait()


class IntroduceTransformationView(NumberlineTransformationScene):
    CONFIG = {
        "func": lambda x: 0.5 * np.sin(2 * x) + x,
        "number_line_config": {
            "x_min": 0,
            "x_max": 6,
            "unit_size": 2.0
        },
    }

    def construct(self):
        self.add_title()
        self.show_animation_preview()
        self.indicate_indicate_point_densities()
        self.show_zoomed_transformation()

    def add_title(self):
        title = self.title = TextMobject("$f(x)$ as a transformation")
        title.to_edge(UP)
        self.add(title)

    def show_animation_preview(self):
        input_points = self.get_sample_input_points()
        output_points = list(map(
            self.number_func_to_point_func(self.func),
            input_points
        ))
        sample_dots = self.get_sample_dots()
        sample_dot_ghosts = sample_dots.copy().fade(0.5)
        arrows = VGroup(*[
            Arrow(ip, op, buff=MED_SMALL_BUFF)
            for ip, op in zip(input_points, output_points)
        ])
        arrows = arrows[1::3]
        arrows.set_stroke(BLACK, 1)

        for sd in sample_dots:
            sd.save_state()
            sd.scale(2)
            sd.fade(1)
        self.play(LaggedStartMap(
            ApplyMethod, sample_dots,
            lambda sd: (sd.restore,),
            run_time=2
        ))
        self.play(LaggedStartMap(
            GrowArrow, arrows,
            run_time=6,
            lag_ratio=0.3,
        ))
        self.add(sample_dot_ghosts)
        self.apply_function(
            self.func, sample_dots=sample_dots,
            run_time=3
        )
        self.wait()
        self.play(LaggedStartMap(FadeOut, arrows, run_time=1))

        self.sample_dots = sample_dots
        self.sample_dot_ghosts = sample_dot_ghosts

    def indicate_indicate_point_densities(self):
        lower_brace = Brace(Line(LEFT, RIGHT), UP)
        upper_brace = lower_brace.copy()
        input_tracker = ValueTracker(0.5)
        dx = 0.5

        def update_upper_brace(brace):
            x = input_tracker.get_value()
            line = Line(
                self.get_input_point(x),
                self.get_input_point(x + dx),
            )
            brace.match_width(line, stretch=True)
            brace.next_to(line, UP, buff=SMALL_BUFF)
            return brace

        def update_lower_brace(brace):
            x = input_tracker.get_value()
            line = Line(
                self.get_output_point(self.func(x)),
                self.get_output_point(self.func(x + dx)),
            )
            brace.match_width(line, stretch=True)
            brace.next_to(line, UP, buff=SMALL_BUFF)
            return brace

        lower_brace_anim = UpdateFromFunc(lower_brace, update_lower_brace)
        upper_brace_anim = UpdateFromFunc(upper_brace, update_upper_brace)

        new_title = TextMobject(
            "$\\frac{df}{dx}(x)$ measures stretch/squishing"
        )
        new_title.move_to(self.title, UP)

        stretch_factor = DecimalNumber(0, color=YELLOW)
        stretch_factor_anim = ChangingDecimal(
            stretch_factor, lambda a: lower_brace.get_width() / upper_brace.get_width(),
            position_update_func=lambda m: m.next_to(lower_brace, UP, SMALL_BUFF)
        )

        self.play(
            GrowFromCenter(upper_brace),
            FadeOut(self.title),
            # FadeIn(new_title)
            Write(new_title, run_time=2)
        )
        self.title = new_title
        self.play(
            ReplacementTransform(upper_brace.copy(), lower_brace),
            GrowFromPoint(stretch_factor, upper_brace.get_center())
        )
        self.play(
            input_tracker.set_value, self.input_line.x_max - dx,
            lower_brace_anim,
            upper_brace_anim,
            stretch_factor_anim,
            run_time=8,
            rate_func=bezier([0, 0, 1, 1])
        )
        self.wait()

        new_sample_dots = self.get_sample_dots()
        self.play(
            FadeOut(VGroup(
                upper_brace, lower_brace, stretch_factor,
                self.sample_dots, self.moving_input_line,
            )),
            FadeIn(new_sample_dots),
        )
        self.sample_dots = new_sample_dots

    def show_zoomed_transformation(self):
        x = 2.75
        local_sample_dots = self.get_local_sample_dots(x)

        self.zoom_in_on_input(
            x,
            local_sample_dots=local_sample_dots,
            local_coordinate_values=self.get_local_coordinate_values(x),
        )
        self.wait()
        self.apply_function(
            self.func,
            sample_dots=self.sample_dots,
            local_sample_dots=local_sample_dots,
            target_coordinate_values=self.get_local_coordinate_values(self.func(x))
        )
        self.wait()


class ExamplePlease(TeacherStudentsScene):
    def construct(self):
        self.student_says("Example?", student_index=0)
        self.teacher_holds_up(TexMobject("f(x) = x^2").scale(1.5))
        self.wait(2)


class TalkThroughXSquaredExample(IntroduceTransformationView):
    CONFIG = {
        "func": lambda x: x**2,
        "number_line_config": {
            "x_min": 0,
            "x_max": 5,
            "unit_size": 1.25,
        },
        "output_line_config": {
            "x_max": 25,
        },
        "default_delta_x": 0.2
    }

    def construct(self):
        self.add_title()
        self.show_specific_points_mapping()

    def add_title(self):
        title = self.title = TextMobject("$f(x) = x^2$")
        title.to_edge(UP, buff=MED_SMALL_BUFF)
        self.add(title)

    def show_specific_points_mapping(self):
        # First, just show integers as examples
        int_dots = self.get_sample_dots(1, 6, 1)
        int_dot_ghosts = int_dots.copy().fade(0.5)
        int_arrows = VGroup(*[
            Arrow(
                # num.get_bottom(),
                self.get_input_point(x),
                self.get_output_point(self.func(x)),
                buff=MED_SMALL_BUFF
            )
            for x, num in zip(list(range(1, 6)), self.input_line.numbers[1:])
        ])
        point_func = self.number_func_to_point_func(self.func)

        numbers = self.input_line.numbers
        numbers.next_to(self.input_line, UP, SMALL_BUFF)
        self.titles[0].next_to(numbers, UP, MED_SMALL_BUFF, LEFT)
        # map(TexMobject.add_background_rectangle, numbers)
        # self.add_foreground_mobject(numbers)

        for dot, dot_ghost, arrow in zip(int_dots, int_dot_ghosts, int_arrows):
            arrow.match_color(dot)
            self.play(DrawBorderThenFill(dot, run_time=1))
            self.add(dot_ghost)
            self.play(
                GrowArrow(arrow),
                dot.apply_function_to_position, point_func
            )
        self.wait()

        # Show more sample_dots
        sample_dots = self.get_sample_dots()
        sample_dot_ghosts = sample_dots.copy().fade(0.5)

        self.play(
            LaggedStartMap(DrawBorderThenFill, sample_dots),
            LaggedStartMap(FadeOut, int_arrows),
        )
        self.remove(int_dot_ghosts)
        self.add(sample_dot_ghosts)
        self.apply_function(self.func, sample_dots=sample_dots)
        self.remove(int_dots)
        self.wait()

        self.sample_dots = sample_dots
        self.sample_dot_ghosts = sample_dot_ghosts

    def get_stretch_words(self, factor, color=RED, less_than_one=False):
        factor_str = "$%s$" % str(factor)
        result = TextMobject(
            "Scale \\\\ by", factor_str,
            tex_to_color_map={factor_str: color}
        )
        result.scale(0.7)
        la, ra = TexMobject("\\leftarrow \\rightarrow")
        if less_than_one:
            la, ra = ra, la
        if factor < 0:
            kwargs = {
                "path_arc": np.pi,
            }
            la = Arrow(UP, DOWN, **kwargs)
            ra = Arrow(DOWN, UP, **kwargs)
            for arrow in la, ra:
                arrow.pointwise_become_partial(arrow, 0, 0.9)
                arrow.tip.scale(2)
            VGroup(la, ra).match_height(result)
        la.next_to(result, LEFT)
        ra.next_to(result, RIGHT)
        result.add(la, ra)
        result.next_to(
            self.zoomed_display.get_top(), DOWN, SMALL_BUFF
        )
        return result

    def get_deriv_equation(self, x, rhs, color=RED):
        deriv_equation = self.deriv_equation = TexMobject(
            "\\frac{df}{dx}(", str(x), ")", "=", str(rhs),
            tex_to_color_map={str(x): color, str(rhs): color}
        )
        deriv_equation.next_to(self.title, DOWN, MED_LARGE_BUFF)
        return deriv_equation


class ZoomInOnXSquaredNearOne(TalkThroughXSquaredExample):
    def setup(self):
        TalkThroughXSquaredExample.setup(self)
        self.force_skipping()
        self.add_title()
        self.show_specific_points_mapping()
        self.revert_to_original_skipping_status()

    def construct(self):
        zoom_words = TextMobject("Zoomed view \\\\ near 1")
        zoom_words.next_to(self.zoomed_display, DOWN)
        # zoom_words.shift_onto_screen()

        x = 1
        local_sample_dots = self.get_local_sample_dots(x)
        local_coords = self.get_local_coordinate_values(x, dx=0.1)

        zcbr_anim = self.zoomed_camera_background_rectangle_anim
        zcbr_group = self.zoomed_camera_background_rectangle_group
        frame = self.zoomed_camera.frame
        sample_dot_ghost_copies = self.sample_dot_ghosts.copy()

        self.zoom_in_on_input(x, local_sample_dots, local_coords)
        self.play(FadeIn(zoom_words))
        self.wait()
        local_sample_dots.save_state()
        frame.save_state()
        self.mini_line.save_state()
        self.apply_function(
            self.func,
            apply_function_to_number_line=False,
            sample_dots=sample_dot_ghost_copies,
            local_sample_dots=local_sample_dots,
            target_coordinate_values=local_coords
        )
        self.remove(sample_dot_ghost_copies)
        self.wait()

        # Go back
        self.play(
            frame.restore,
            self.mini_line.restore,
            local_sample_dots.restore,
            zcbr_anim,
            Animation(zcbr_group)
        )
        self.wait()

        # Zoom in even more
        extra_zoom_factor = 0.3
        one_group = VGroup(
            self.local_coordinates.tick_marks[1],
            self.local_coordinates.numbers[1],
        )
        all_other_coordinates = VGroup(
            self.local_coordinates.tick_marks[::2],
            self.local_coordinates.numbers[::2],
            self.local_target_coordinates,
        )
        self.play(frame.scale, extra_zoom_factor)
        new_local_sample_dots = self.get_local_sample_dots(x, delta_x=0.005)
        new_coordinate_values = self.get_local_coordinate_values(x, dx=0.02)
        new_local_coordinates = self.get_local_coordinates(
            self.input_line, *new_coordinate_values
        )

        self.play(
            Write(new_local_coordinates),
            Write(new_local_sample_dots),
            one_group.scale, extra_zoom_factor, {"about_point": self.get_input_point(1)},
            FadeOut(all_other_coordinates),
            *[
                ApplyMethod(dot.scale, extra_zoom_factor)
                for dot in local_sample_dots
            ]
        )
        self.remove(one_group, local_sample_dots)
        zcbr_group.remove(
            self.local_coordinates, self.local_target_coordinates,
            local_sample_dots
        )

        # Transform new zoomed view
        stretch_by_two_words = self.get_stretch_words(2)
        self.add_foreground_mobject(stretch_by_two_words)
        sample_dot_ghost_copies = self.sample_dot_ghosts.copy()
        self.apply_function(
            self.func,
            apply_function_to_number_line=False,
            sample_dots=sample_dot_ghost_copies,
            local_sample_dots=new_local_sample_dots,
            target_coordinate_values=new_coordinate_values,
            added_anims=[FadeIn(stretch_by_two_words)]
        )
        self.remove(sample_dot_ghost_copies)
        self.wait()

        # Write derivative
        deriv_equation = self.get_deriv_equation(1, 2, color=RED)
        self.play(Write(deriv_equation))
        self.wait()


class ZoomInOnXSquaredNearThree(ZoomInOnXSquaredNearOne):
    CONFIG = {
        "zoomed_display_width": 4,
    }

    def construct(self):
        zoom_words = TextMobject("Zoomed view \\\\ near 3")
        zoom_words.next_to(self.zoomed_display, DOWN)

        x = 3
        local_sample_dots = self.get_local_sample_dots(x)
        local_coordinate_values = self.get_local_coordinate_values(x, dx=0.1)
        target_coordinate_values = self.get_local_coordinate_values(self.func(x), dx=0.1)

        color = self.sample_dots[len(self.sample_dots) / 2].get_color()
        sample_dot_ghost_copies = self.sample_dot_ghosts.copy()
        stretch_words = self.get_stretch_words(2 * x, color)
        deriv_equation = self.get_deriv_equation(x, 2 * x, color)

        self.add(deriv_equation)
        self.zoom_in_on_input(
            x,
            pop_out=False,
            local_sample_dots=local_sample_dots,
            local_coordinate_values=local_coordinate_values
        )
        self.play(Write(zoom_words, run_time=1))
        self.wait()
        self.add_foreground_mobject(stretch_words)
        self.apply_function(
            self.func,
            apply_function_to_number_line=False,
            sample_dots=sample_dot_ghost_copies,
            local_sample_dots=local_sample_dots,
            target_coordinate_values=target_coordinate_values,
            added_anims=[Write(stretch_words)]
        )
        self.wait(2)


class ZoomInOnXSquaredNearOneFourth(ZoomInOnXSquaredNearOne):
    CONFIG = {
        "zoom_factor": 0.01,
        "local_coordinate_num_decimal_places": 4,
        "zoomed_display_width": 4,
        "default_delta_x": 0.25,
    }

    def construct(self):
        # Much copy-pasting from previous scenes.  Not great, but
        # the fastest way to get the ease-of-tweaking I'd like.
        zoom_words = TextMobject("Zoomed view \\\\ near $1/4$")
        zoom_words.next_to(self.zoomed_display, DOWN)

        x = 0.25
        local_sample_dots = self.get_local_sample_dots(
            x, sample_radius=2.5 * self.zoomed_camera.frame.get_width(),
        )
        local_coordinate_values = self.get_local_coordinate_values(
            x, dx=0.01,
        )
        target_coordinate_values = self.get_local_coordinate_values(
            self.func(x), dx=0.01,
        )

        color = RED
        sample_dot_ghost_copies = self.sample_dot_ghosts.copy()
        stretch_words = self.get_stretch_words("1/2", color, less_than_one=True)
        deriv_equation = self.get_deriv_equation("1/4", "1/2", color)

        one_fourth_point = self.get_input_point(x)
        one_fourth_arrow = Vector(0.5 * UP, color=WHITE)
        one_fourth_arrow.stem.stretch(0.75, 0)
        one_fourth_arrow.tip.scale(0.75, about_edge=DOWN)
        one_fourth_arrow.next_to(one_fourth_point, DOWN, SMALL_BUFF)
        one_fourth_label = TexMobject("0.25")
        one_fourth_label.match_height(self.input_line.numbers)
        one_fourth_label.next_to(one_fourth_arrow, DOWN, SMALL_BUFF)

        self.add(deriv_equation)
        self.zoom_in_on_input(
            x,
            local_sample_dots=local_sample_dots,
            local_coordinate_values=local_coordinate_values,
            pop_out=False,
            first_added_anims=[
                FadeIn(one_fourth_label),
                GrowArrow(one_fourth_arrow),
            ]
        )
        self.play(Write(zoom_words, run_time=1))
        self.wait()
        self.add_foreground_mobject(stretch_words)
        self.apply_function(
            self.func,
            apply_function_to_number_line=False,
            sample_dots=sample_dot_ghost_copies,
            local_sample_dots=local_sample_dots,
            target_coordinate_values=target_coordinate_values,
            added_anims=[Write(stretch_words)]
        )
        self.wait(2)


class ZoomInOnXSquaredNearZero(ZoomInOnXSquaredNearOne):
    CONFIG = {
        "zoom_factor": 0.1,
        "zoomed_display_width": 4,
        "scale_by_term": "???",
    }

    def construct(self):
        zoom_words = TextMobject(
            "Zoomed %sx \\\\ near 0" % "{:,}".format(int(1.0 / self.zoom_factor))
        )
        zoom_words.next_to(self.zoomed_display, DOWN)

        x = 0
        local_sample_dots = self.get_local_sample_dots(
            x, sample_radius=2 * self.zoomed_camera.frame.get_width()
        )
        local_coordinate_values = self.get_local_coordinate_values(
            x, dx=self.zoom_factor
        )
        # target_coordinate_values = self.get_local_coordinate_values(
        #     self.func(x), dx=self.zoom_factor
        # )

        color = self.sample_dots[len(self.sample_dots) / 2].get_color()
        sample_dot_ghost_copies = self.sample_dot_ghosts.copy()
        stretch_words = self.get_stretch_words(
            self.scale_by_term, color, less_than_one=True
        )
        deriv_equation = self.get_deriv_equation(x, 2 * x, color)

        self.add(deriv_equation)
        self.zoom_in_on_input(
            x,
            pop_out=False,
            local_sample_dots=local_sample_dots,
            local_coordinate_values=local_coordinate_values
        )
        self.play(Write(zoom_words, run_time=1))
        self.wait()
        self.add_foreground_mobject(stretch_words)
        self.apply_function(
            self.func,
            apply_function_to_number_line=False,
            sample_dots=sample_dot_ghost_copies,
            local_sample_dots=local_sample_dots,
            # target_coordinate_values=target_coordinate_values,
            added_anims=[
                Write(stretch_words),
                MaintainPositionRelativeTo(
                    self.local_coordinates,
                    self.zoomed_camera.frame
                )
            ]
        )
        self.wait(2)


class ZoomInMoreAndMoreToZero(ZoomInOnXSquaredNearZero):
    def construct(self):
        x = 0
        color = self.sample_dots[len(self.sample_dots) / 2].get_color()
        deriv_equation = self.get_deriv_equation(x, 2 * x, color)
        self.add(deriv_equation)

        frame = self.zoomed_camera.frame
        zoomed_display_height = self.zoomed_display.get_height()

        last_sample_dots = VGroup()
        last_coords = VGroup()
        last_zoom_words = None
        for factor in 0.1, 0.01, 0.001, 0.0001:
            frame.save_state()
            frame.set_height(factor * zoomed_display_height)
            self.local_coordinate_num_decimal_places = int(-np.log10(factor))
            zoom_words = TextMobject(
                "Zoomed", "{:,}x \\\\".format(int(1.0 / factor)),
                "near 0",
            )
            zoom_words.next_to(self.zoomed_display, DOWN)

            sample_dots = self.get_local_sample_dots(x)
            coords = self.get_local_coordinate_values(x, dx=factor)
            frame.restore()

            added_anims = [
                ApplyMethod(last_sample_dots.fade, 1),
                ApplyMethod(last_coords.fade, 1),
            ]
            if last_zoom_words is not None:
                added_anims.append(ReplacementTransform(
                    last_zoom_words, zoom_words
                ))
            else:
                added_anims.append(FadeIn(zoom_words))
            self.zoom_in_on_input(
                x,
                local_sample_dots=sample_dots,
                local_coordinate_values=coords,
                pop_out=False,
                zoom_factor=factor,
                first_added_anims=added_anims
            )
            self.wait()
            last_sample_dots = sample_dots
            last_coords = self.local_coordinates
            last_zoom_words = zoom_words


class ZoomInOnXSquared100xZero(ZoomInOnXSquaredNearZero):
    CONFIG = {
        "zoom_factor": 0.01
    }


class ZoomInOnXSquared1000xZero(ZoomInOnXSquaredNearZero):
    CONFIG = {
        "zoom_factor": 0.001,
        "local_coordinate_num_decimal_places": 3,
    }


class ZoomInOnXSquared10000xZero(ZoomInOnXSquaredNearZero):
    CONFIG = {
        "zoom_factor": 0.0001,
        "local_coordinate_num_decimal_places": 4,
        "scale_by_term": "0",
    }


class XSquaredForNegativeInput(TalkThroughXSquaredExample):
    CONFIG = {
        "input_line_config": {
            "x_min": -4,
            "x_max": 4,
        },
        "input_line_zero_point": 0.5 * UP + 0 * LEFT,
        "output_line_config": {},
        "default_mapping_animation_config": {
            "path_arc": 30 * DEGREES
        },
        "zoomed_display_width": 4,
    }

    def construct(self):
        self.add_title()
        self.show_full_transformation()
        self.zoom_in_on_example()

    def show_full_transformation(self):
        sample_dots = self.get_sample_dots(
            x_min=-4.005,
            delta_x=0.05,
            dot_radius=0.05
        )
        sample_dots.set_fill(opacity=0.8)

        self.play(LaggedStartMap(DrawBorderThenFill, sample_dots))
        self.play(LaggedStartMap(
            ApplyFunction, sample_dots[len(sample_dots) / 2:0:-1],
            lambda mob: (
                lambda m: m.scale(2).shift(SMALL_BUFF * UP).set_color(PINK),
                mob,
            ),
            rate_func=there_and_back,
        ))
        self.add_sample_dot_ghosts(sample_dots)
        self.apply_function(self.func, sample_dots=sample_dots)
        self.wait()

    def zoom_in_on_example(self):
        x = -2

        local_sample_dots = self.get_local_sample_dots(x)
        local_coordinate_values = self.get_local_coordinate_values(
            x, dx=0.1
        )
        target_coordinate_values = self.get_local_coordinate_values(
            self.func(x), dx=0.1
        )
        deriv_equation = self.get_deriv_equation(x, 2 * x, color=BLUE)
        sample_dot_ghost_copies = self.sample_dot_ghosts.copy()
        scale_words = self.get_stretch_words(-4, color=BLUE)

        self.zoom_in_on_input(
            x,
            local_sample_dots=local_sample_dots,
            local_coordinate_values=local_coordinate_values,
        )
        self.wait()
        self.play(Write(deriv_equation))
        self.add_foreground_mobject(scale_words)
        self.play(Write(scale_words))
        self.apply_function(
            self.func,
            sample_dots=sample_dot_ghost_copies,
            local_sample_dots=local_sample_dots,
            target_coordinate_values=target_coordinate_values
        )
        self.wait()


class FeelsALittleCramped(TeacherStudentsScene):
    def construct(self):
        self.student_says(
            "Kind of cramped,\\\\ isn't it?",
            target_mode="sassy"
        )
        self.wait()
        self.teacher_says(
            "Sure, but think \\\\ locally"
        )
        self.change_all_student_modes("pondering", look_at_arg=self.screen)
        self.wait(3)


class HowDoesThisSolveProblems(TeacherStudentsScene):
    def construct(self):
        self.student_says(
            "Is this...useful?",
            target_mode="confused"
        )
        self.change_student_modes("maybe", "confused", "sassy")
        self.play(self.teacher.change, "happy")
        self.wait(3)


class IntroduceContinuedFractionPuzzle(PiCreatureScene):
    CONFIG = {
        "remove_initial_rhs": True,
    }

    def construct(self):
        self.ask_question()
        self.set_equal_to_x()

    def create_pi_creatures(self):
        morty = Mortimer(height=2)
        morty.to_corner(DR)

        friend = PiCreature(color=GREEN, height=2)
        friend.to_edge(DOWN)
        friend.shift(LEFT)

        group = VGroup(morty, friend)
        group.shift(2 * LEFT)

        return morty, friend

    def ask_question(self):
        morty, friend = self.pi_creatures
        frac = get_phi_continued_fraction(9)
        frac.scale(0.8)
        rhs = DecimalNumber(
            (1 - np.sqrt(5)) / 2.0,
            num_decimal_places=5,
            show_ellipsis=True,
        )
        rhs.set_color(YELLOW)
        equals = TexMobject("=")
        equals.next_to(frac.get_part_by_tex("\\over"), RIGHT)
        rhs.next_to(equals, RIGHT)
        group = VGroup(frac, equals, rhs)
        group.scale(1.5)
        group.to_corner(UR)

        self.play(
            LaggedStartMap(
                Write, frac,
                run_time=15,
                lag_ratio=0.15,
            ),
            FadeInFromDown(equals),
            FadeInFromDown(rhs),
            PiCreatureSays(
                friend, "Would this be valid? \\\\ If not, why not?",
                target_mode="confused",
                look_at_arg=frac,
                bubble_kwargs={
                    "direction": RIGHT,
                    "width": 4,
                    "height": 3,
                }
            ),
            morty.change, "pondering",
        )
        self.wait()

        anims = [
            RemovePiCreatureBubble(
                friend, target_mode="pondering",
                look_at_arg=frac
            ),
        ]
        if self.remove_initial_rhs:
            anims += [
                Animation(frac),
                FadeOut(equals),
                rhs.scale, 0.5,
                rhs.to_corner, DL,
            ]
        self.play(*anims)

        self.neg_one_over_phi = rhs
        self.equals = equals
        self.frac = frac

    def set_equal_to_x(self):
        frac = self.frac
        morty, friend = self.get_pi_creatures()

        inner_frac = frac[4:]
        inner_frac_rect = SurroundingRectangle(
            inner_frac, stroke_width=2, buff=0.5 * SMALL_BUFF
        )
        inner_frac_group = VGroup(inner_frac, inner_frac_rect)

        equals = TexMobject("=")
        equals.next_to(frac[3], RIGHT)
        x, new_x = [TexMobject("x") for i in range(2)]
        xs = VGroup(x, new_x)
        xs.set_color(YELLOW)
        xs.scale(1.3)
        x.next_to(equals, RIGHT)
        new_x.next_to(frac[3], DOWN, 2 * SMALL_BUFF)

        fixed_point_words = VGroup(
            TextMobject("Fixed point of"),
            TexMobject(
                "f(x) = 1 + \\frac{1}{x}",
                tex_to_color_map={"x": YELLOW}
            )
        )
        fixed_point_words.arrange(DOWN)

        self.play(Write(x), Write(equals))
        self.wait()
        self.play(ShowCreation(inner_frac_rect))
        self.wait()
        self.play(
            inner_frac_group.scale, 0.75,
            inner_frac_group.center,
            inner_frac_group.to_edge, LEFT,
            ReplacementTransform(
                x.copy(), new_x,
                path_arc=-90 * DEGREES
            )
        )
        self.wait()
        self.play(
            frac[3].stretch, 0.1, 0, {"about_edge": RIGHT},
            MaintainPositionRelativeTo(
                VGroup(frac[2], new_x), frac[3]
            ),
            UpdateFromFunc(
                frac[:2], lambda m: m.next_to(frac[3], LEFT)
            )
        )
        self.wait()
        fixed_point_words.next_to(VGroup(frac[0], xs), DOWN, LARGE_BUFF)
        self.play(
            Write(fixed_point_words),
            morty.change, "hooray",
            friend.change, "happy"
        )
        self.wait(3)


class GraphOnePlusOneOverX(GraphScene):
    CONFIG = {
        "x_min": -6,
        "x_max": 6,
        "x_axis_width": 12,
        "y_min": -4,
        "y_max": 5,
        "y_axis_height": 8,
        "y_axis_label": None,
        "graph_origin": 0.5 * DOWN,
        "num_graph_anchor_points": 100,
        "func_graph_color": GREEN,
        "identity_graph_color": BLUE,
    }

    def construct(self):
        self.add_title()
        self.setup_axes()
        self.draw_graphs()
        self.show_solutions()

    def add_title(self):
        title = self.title = TexMobject(
            "\\text{Solve: }", "1 + \\frac{1}{x}", "=", "x",
        )
        title.set_color_by_tex("x", self.identity_graph_color, substring=False)
        title.set_color_by_tex("frac", self.func_graph_color)
        title.to_corner(UL)
        self.add(title)

    def setup_axes(self):
        GraphScene.setup_axes(self)
        step = 2
        self.x_axis.add_numbers(*list(range(-6, 0, step)) + list(range(step, 7, step)))
        self.y_axis.label_direction = RIGHT
        self.y_axis.add_numbers(*list(range(-2, 0, step)) + list(range(step, 4, step)))

    def draw_graphs(self, animate=True):
        lower_func_graph, upper_func_graph = func_graph = VGroup(*[
            self.get_graph(
                lambda x: 1.0 + 1.0 / x,
                x_min=x_min,
                x_max=x_max,
                color=self.func_graph_color,
            )
            for x_min, x_max in [(-10, -0.1), (0.1, 10)]
        ])
        func_graph.label = self.get_graph_label(
            upper_func_graph, "y = 1 + \\frac{1}{x}",
            x_val=6, direction=UP,
        )

        identity_graph = self.get_graph(
            lambda x: x, color=self.identity_graph_color
        )
        identity_graph.label = self.get_graph_label(
            identity_graph, "y = x",
            x_val=3, direction=UL, buff=SMALL_BUFF
        )

        if animate:
            for graph in func_graph, identity_graph:
                self.play(
                    ShowCreation(graph),
                    Write(graph.label),
                    run_time=2
                )
            self.wait()
        else:
            self.add(
                func_graph, func_graph.label,
                identity_graph, identity_graph.label,
            )

        self.func_graph = func_graph
        self.identity_graph = identity_graph

    def show_solutions(self):
        phi = 0.5 * (1 + np.sqrt(5))
        phi_bro = 0.5 * (1 - np.sqrt(5))

        lines = VGroup()
        for num in phi, phi_bro:
            line = DashedLine(
                self.coords_to_point(num, 0),
                self.coords_to_point(num, num),
                color=WHITE
            )
            line_copy = line.copy()
            line_copy.set_color(YELLOW)
            line.fade(0.5)
            line_anim = ShowCreationThenDestruction(
                line_copy,
                lag_ratio=0.5,
                run_time=2
            )
            cycle_animation(line_anim)
            lines.add(line)

        phi_line, phi_bro_line = lines

        decimal_kwargs = {
            "num_decimal_places": 3,
            "show_ellipsis": True,
            "color": YELLOW,
        }
        arrow_kwargs = {
            "buff": SMALL_BUFF,
            "color": WHITE,
            "tip_length": 0.15,
            "rectangular_stem_width": 0.025,
        }

        phi_decimal = DecimalNumber(phi, **decimal_kwargs)
        phi_decimal.next_to(phi_line, DOWN, LARGE_BUFF)
        phi_arrow = Arrow(
            phi_decimal[:4].get_top(), phi_line.get_bottom(),
            **arrow_kwargs
        )
        phi_label = TexMobject("=", "\\varphi")
        phi_label.next_to(phi_decimal, RIGHT)
        phi_label.set_color_by_tex("\\varphi", YELLOW)

        phi_bro_decimal = DecimalNumber(phi_bro, **decimal_kwargs)
        phi_bro_decimal.next_to(phi_bro_line, UP, LARGE_BUFF)
        phi_bro_decimal.shift(0.5 * LEFT)
        phi_bro_arrow = Arrow(
            phi_bro_decimal[:6].get_bottom(), phi_bro_line.get_top(),
            **arrow_kwargs
        )

        brother_words = TextMobject(
            "$\\varphi$'s little brother",
            tex_to_color_map={"$\\varphi$": YELLOW},
            arg_separator=""
        )
        brother_words.next_to(
            phi_bro_decimal[-2], UP, buff=MED_SMALL_BUFF,
            aligned_edge=RIGHT
        )

        self.add(phi_line.continual_anim)
        self.play(ShowCreation(phi_line))
        self.play(
            Write(phi_decimal),
            GrowArrow(phi_arrow),
        )
        self.play(Write(phi_label))
        self.wait(3)
        self.add(phi_bro_line.continual_anim)
        self.play(ShowCreation(phi_bro_line))
        self.play(
            Write(phi_bro_decimal),
            GrowArrow(phi_bro_arrow),
        )
        self.wait(4)
        self.play(Write(brother_words))
        self.wait(8)


class ThinkAboutWithRepeatedApplication(IntroduceContinuedFractionPuzzle):
    CONFIG = {
        "remove_initial_rhs": False,
    }

    def construct(self):
        self.force_skipping()
        self.ask_question()
        self.revert_to_original_skipping_status()

        self.obviously_not()
        self.ask_about_fraction()
        self.plug_func_into_self()

    def obviously_not(self):
        morty, friend = self.get_pi_creatures()
        friend.change_mode("confused")
        randy = Randolph()
        randy.match_height(morty)
        randy.to_corner(DL)

        frac = self.frac
        rhs = self.neg_one_over_phi
        plusses = frac[1::4]
        plus_rects = VGroup(*[
            SurroundingRectangle(plus, buff=0) for plus in plusses
        ])
        plus_rects.set_color(PINK)

        self.play(FadeIn(randy))
        self.play(
            PiCreatureSays(
                randy, "Obviously not!",
                bubble_kwargs={"width": 3, "height": 2},
                target_mode="angry",
                run_time=1,
            ),
            morty.change, "guilty",
            friend.change, "hesitant"
        )
        self.wait()
        self.play(
            Animation(frac),
            RemovePiCreatureBubble(randy, target_mode="sassy"),
            morty.change, "confused",
            friend.change, "confused",
        )
        self.play(LaggedStartMap(
            ShowCreationThenDestruction, plus_rects,
            run_time=2,
            lag_ratio=0.35,
        ))
        self.play(WiggleOutThenIn(rhs))
        self.wait(2)
        self.play(
            frac.scale, 0.7,
            frac.to_corner, UL,
            FadeOut(self.equals),
            rhs.scale, 0.5,
            rhs.center,
            rhs.to_edge, LEFT,
            FadeOut(randy),
            morty.change, "pondering",
            friend.change, "pondering",
        )

    def ask_about_fraction(self):
        frac = self.frac
        arrow = Vector(LEFT, color=RED)
        arrow.next_to(frac, RIGHT)
        question = TextMobject("What does this \\\\ actually mean?")
        question.set_color(RED)
        question.next_to(arrow, RIGHT)

        self.play(
            LaggedStartMap(FadeIn, question, run_time=1),
            GrowArrow(arrow),
            LaggedStartMap(
                ApplyMethod, frac,
                lambda m: (m.set_color, RED),
                rate_func=there_and_back,
                lag_ratio=0.2,
                run_time=2
            )
        )
        self.wait()
        self.play(FadeOut(question), FadeOut(arrow))

    def plug_func_into_self(self, value=1, value_str="1"):
        morty, friend = self.pi_creatures

        def func(x):
            return 1 + 1.0 / x

        lines = VGroup()
        value_labels = VGroup()
        for n_terms in range(5):
            lhs = get_nested_f(n_terms, arg="c")
            equals = TexMobject("=")
            rhs = get_nested_one_plus_one_over_x(n_terms, bottom_term=value_str)
            equals.next_to(rhs[0], LEFT)
            lhs.next_to(equals, LEFT)
            lines.add(VGroup(lhs, equals, rhs))

            value_label = TexMobject("= %.3f\\dots" % value)
            value = func(value)
            value_labels.add(value_label)

        lines.arrange(
            DOWN, buff=MED_LARGE_BUFF,
        )
        VGroup(lines, value_labels).scale(0.8)
        lines.to_corner(UR)
        buff = MED_LARGE_BUFF + MED_SMALL_BUFF + value_labels.get_width()
        lines.to_edge(RIGHT, buff=buff)
        for line, value_label in zip(lines, value_labels):
            value_label.move_to(line[1]).to_edge(RIGHT)

        top_line = lines[0]
        colors = [WHITE] + color_gradient([YELLOW, RED, PINK], len(lines) - 1)
        for n in range(1, len(lines)):
            color = colors[n]
            lines[n][0].set_color(color)
            lines[n][0][1:-1].match_style(lines[n - 1][0])
            lines[n][2].set_color(color)
            lines[n][2][4:].match_style(lines[n - 1][2])

        arrow = Vector(0.5 * DOWN, color=WHITE)
        arrow.next_to(value_labels[-1], DOWN)
        q_marks = TexMobject("???")
        q_marks.next_to(arrow, DOWN)

        self.play(
            FadeInFromDown(top_line),
            FadeInFromDown(value_labels[0])
        )
        for n in range(1, len(lines)):
            new_line = lines[n]
            last_line = lines[n - 1]
            value_label = value_labels[n]
            mover, target = [
                VGroup(
                    line[0][0],
                    line[0][-1],
                    line[1],
                    line[2][:4],
                )
                for line in (lines[1], new_line)
            ]
            anims = [ReplacementTransform(
                mover.copy().fade(1), target, path_arc=30 * DEGREES
            )]
            if n == 4:
                morty.generate_target()
                morty.target.change("horrified")
                morty.target.shift(2.5 * DOWN)
                anims.append(MoveToTarget(morty, remover=True))
            self.play(*anims)
            self.wait()
            self.play(
                ReplacementTransform(
                    last_line[0].copy(), new_line[0][1:-1]
                ),
                ReplacementTransform(
                    last_line[2].copy(), new_line[2][4:]
                ),
            )
            self.play(FadeIn(value_label))
            self.wait()
        self.play(
            GrowArrow(arrow),
            Write(q_marks),
            friend.change, "confused"
        )
        self.wait(3)

        self.top_line = VGroup(lines[0], value_labels[0])


class RepeatedApplicationWithPhiBro(ThinkAboutWithRepeatedApplication):
    CONFIG = {
        "value": (1 - np.sqrt(5)) / 2,
        "value_str": "-1/\\varphi",
    }

    def construct(self):
        self.force_skipping()
        self.ask_question()
        self.obviously_not()
        self.revert_to_original_skipping_status()

        self.plug_func_into_self(
            value=self.value,
            value_str=self.value_str
        )


class RepeatedApplicationWithNegativeSeed(RepeatedApplicationWithPhiBro, MovingCameraScene):
    CONFIG = {
        "value": -0.65,
        "value_str": "-0.65"
    }

    def setup(self):
        MovingCameraScene.setup(self)
        RepeatedApplicationWithPhiBro.setup(self)

    def construct(self):
        RepeatedApplicationWithPhiBro.construct(self)

        rect = SurroundingRectangle(self.top_line)
        question = TextMobject("What about a negative seed?")
        question.match_color(rect)
        question.next_to(rect, UP)
        self.play(ShowCreation(rect))
        self.play(
            Write(question),
            self.camera.frame.set_height, FRAME_HEIGHT + 1.5
        )
        self.wait()
        self.play(
            FadeOut(question),
            FadeOut(rect),
            self.camera.frame.set_height, FRAME_HEIGHT
        )


class ShowRepeatedApplication(Scene):
    CONFIG = {
        "title_color": YELLOW,
    }

    def construct(self):
        self.add_func_title()
        self.show_repeated_iteration()

    def add_func_title(self):
        title = self.title = VGroup(
            TexMobject("f(", "x", ")"),
            TexMobject("="),
            get_nested_one_plus_one_over_x(1)
        )
        title.arrange(RIGHT)
        title.to_corner(UL)
        title.set_color(self.title_color)

        self.add(title)

    def show_repeated_iteration(self):
        line = VGroup()
        decimal_kwargs = {
            "num_decimal_places": 3,
            "show_ellipsis": True,
        }
        phi = (1 + np.sqrt(5)) / 2

        def func(x):
            return 1.0 + 1.0 / x

        initial_term = DecimalNumber(2.71828, **decimal_kwargs)
        line.add(initial_term)
        last_term = initial_term

        def get_arrow():
            arrow = TexMobject("\\rightarrow")
            arrow.stretch(1.5, 0)
            arrow.next_to(line[-1], RIGHT)
            tex = TexMobject("f(x)")
            tex.set_color(YELLOW)
            tex.match_width(arrow)
            tex.next_to(arrow, UP, SMALL_BUFF)
            return VGroup(arrow, tex)

        for x in range(2):
            arrow = get_arrow()
            line.add(arrow)

            new_term = DecimalNumber(
                func(last_term.number),
                **decimal_kwargs
            )
            new_term.next_to(arrow[0], RIGHT)
            last_term = new_term
            line.add(new_term)

        line.add(get_arrow())
        line.add(TexMobject("\\dots\\dots").next_to(line[-1][0], RIGHT))
        num_phi_mob = DecimalNumber(phi, **decimal_kwargs)
        line.add(num_phi_mob.next_to(line[-1], RIGHT))
        line.move_to(DOWN)

        rects = VGroup(*[
            SurroundingRectangle(mob)
            for mob in (line[0], line[1:-1], line[-1])
        ])
        rects.set_stroke(BLUE, 2)

        braces = VGroup(*[
            Brace(rect, DOWN, buff=SMALL_BUFF)
            for rect in rects
        ])
        braces.set_color_by_gradient(GREEN, YELLOW)
        brace_texts = VGroup(*[
            brace.get_text(text).scale(0.75, about_edge=UP)
            for brace, text in zip(braces, [
                "Arbitrary \\\\ starting \\\\ value",
                "Repeatedly apply $f(x)$",
                "$\\varphi$ \\\\ ``Golden ratio''"
            ])
        ])
        var_phi_mob = brace_texts[2][0]
        var_phi_mob.scale(2, about_edge=UP).set_color(YELLOW)
        brace_texts[2][1:].next_to(var_phi_mob, DOWN, MED_SMALL_BUFF)

        # Animations
        self.add(line[0])
        self.play(
            GrowFromCenter(braces[0]),
            Write(brace_texts[0])
        )
        self.wait()
        self.play(
            GrowFromEdge(line[1], LEFT),
            FadeIn(braces[1]),
            FadeIn(brace_texts[1]),
        )
        self.play(ReplacementTransform(line[0].copy(), line[2]))
        self.wait()

        self.play(GrowFromEdge(line[3], LEFT))
        self.play(ReplacementTransform(line[2].copy(), line[4]))
        self.wait()

        self.play(GrowFromEdge(line[5], LEFT))
        self.play(LaggedStartMap(GrowFromCenter, line[6]))
        self.wait()

        self.play(FadeIn(line[7]))
        self.play(
            GrowFromCenter(braces[2]),
            FadeIn(brace_texts[2]),
        )
        self.wait()


class NumericalPlayFromOne(ExternallyAnimatedScene):
    pass


class NumericalPlayFromTau(ExternallyAnimatedScene):
    pass


class NumericalPlayFromNegPhi(ExternallyAnimatedScene):
    pass


class NumericalPlayOnNumberLineFromOne(Scene):
    CONFIG = {
        "starting_value": 1,
        "n_jumps": 10,
        "func": lambda x: 1 + 1.0 / x,
        "number_line_config": {
            "x_min": 0,
            "x_max": 2,
            "unit_size": 6,
            "tick_frequency": 0.25,
            "numbers_with_elongated_ticks": [0, 1, 2]
        }
    }

    def construct(self):
        self.add_number_line()
        self.add_phi_label()
        self.add_title()
        self.bounce_around()

    def add_number_line(self):
        number_line = NumberLine(**self.number_line_config)
        number_line.move_to(2 * DOWN)
        number_line.add_numbers()

        self.add(number_line)
        self.number_line = number_line

    def add_phi_label(self):
        number_line = self.number_line
        phi_point = number_line.number_to_point(
            (1 + np.sqrt(5)) / 2
        )
        phi_dot = Dot(phi_point, color=YELLOW)
        arrow = Vector(DL)
        arrow.next_to(phi_point, UR, SMALL_BUFF)
        phi_label = TexMobject("\\varphi = 1.618\\dots")
        phi_label.set_color(YELLOW)
        phi_label.next_to(arrow.get_start(), UP, SMALL_BUFF)

        self.add(phi_dot, phi_label, arrow)

    def add_title(self):
        title = TexMobject("x \\rightarrow 1 + \\frac{1}{x}")
        title.to_corner(UL)
        self.add(title)

    def bounce_around(self):
        number_line = self.number_line
        value = self.starting_value
        point = number_line.number_to_point(value)
        dot = Dot(point)
        dot.set_fill(RED, opacity=0.8)
        arrow = Vector(DR)
        arrow.next_to(point, UL, buff=SMALL_BUFF)
        arrow.match_color(dot)
        start_here = TextMobject("Start here")
        start_here.next_to(arrow.get_start(), UP, SMALL_BUFF)
        start_here.match_color(dot)

        self.play(
            FadeIn(start_here),
            GrowArrow(arrow),
            GrowFromPoint(dot, arrow.get_start())
        )
        self.play(
            FadeOut(start_here),
            FadeOut(arrow)
        )
        self.wait()
        for x in range(self.n_jumps):
            new_value = self.func(value)
            new_point = number_line.number_to_point(new_value)
            if new_value - value > 0:
                path_arc = -120 * DEGREES
            else:
                path_arc = -120 * DEGREES
            arc = Line(
                point, new_point,
                path_arc=path_arc,
                buff=SMALL_BUFF
            )
            self.play(
                ShowCreationThenDestruction(arc, run_time=1.5),
                ApplyMethod(
                    dot.move_to, new_point,
                    path_arc=path_arc
                ),
            )
            self.wait(0.5)

            value = new_value
            point = new_point


class NumericalPlayOnNumberLineFromTau(NumericalPlayOnNumberLineFromOne):
    CONFIG = {
        "starting_value": TAU,
        "number_line_config": {
            "x_min": 0,
            "x_max": 7,
            "unit_size": 2,
        }
    }


class NumericalPlayOnNumberLineFromMinusPhi(NumericalPlayOnNumberLineFromOne):
    CONFIG = {
        "starting_value": -0.61803,
        "number_line_config": {
            "x_min": -3,
            "x_max": 3,
            "unit_size": 2,
            "tick_frequency": 0.25,
        },
        "n_jumps": 25,
    }

    def add_phi_label(self):
        NumericalPlayOnNumberLineFromOne.add_phi_label(self)
        number_line = self.number_line
        new_point = number_line.number_to_point(
            (1 - np.sqrt(5)) / 2
        )
        arrow = Vector(DR)
        arrow.next_to(new_point, UL, SMALL_BUFF)
        arrow.set_color(RED)
        new_label = TexMobject("-\\frac{1}{\\varphi} = -0.618\\dots")
        new_label.set_color(RED)
        new_label.next_to(arrow.get_start(), UP, SMALL_BUFF)
        new_label.shift(RIGHT)
        self.add(new_label, arrow)


class RepeatedApplicationGraphically(GraphOnePlusOneOverX, PiCreatureScene):
    CONFIG = {
        "starting_value": 1,
        "n_jumps": 5,
        "n_times_to_show_identity_property": 2,
    }

    def setup(self):
        GraphOnePlusOneOverX.setup(self)
        PiCreatureScene.setup(self)

    def construct(self):
        self.setup_axes()
        self.draw_graphs(animate=False)
        self.draw_spider_web()

    def create_pi_creature(self):
        randy = Randolph(height=2)
        randy.flip()
        randy.to_corner(DR)
        return randy

    def get_new_randy_mode(self):
        randy = self.pi_creature
        if not hasattr(self, "n_mode_changes"):
            self.n_mode_changes = 0
        else:
            self.n_mode_changes += 1
        if self.n_mode_changes % 3 != 0:
            return randy.get_mode()
        return random.choice([
            "confused",
            "erm",
            "maybe"
        ])

    def draw_spider_web(self):
        randy = self.pi_creature
        func = self.func_graph[0].underlying_function
        x_val = self.starting_value
        curr_output = 0
        dot = Dot(color=RED, fill_opacity=0.7)
        dot.move_to(self.coords_to_point(x_val, curr_output))

        self.play(FadeInFrom(dot, 2 * UR))
        self.wait()

        for n in range(self.n_jumps):
            new_output = func(x_val)
            func_graph_point = self.coords_to_point(x_val, new_output)
            id_graph_point = self.coords_to_point(new_output, new_output)
            v_line = DashedLine(dot.get_center(), func_graph_point)
            h_line = DashedLine(func_graph_point, id_graph_point)

            curr_output = new_output
            x_val = new_output

            for line in v_line, h_line:
                line_end = line.get_end()
                self.play(
                    ShowCreation(line),
                    dot.move_to, line_end,
                    randy.change, self.get_new_randy_mode()
                )
                self.wait()

            if n < self.n_times_to_show_identity_property:
                x_point = self.coords_to_point(new_output, 0)
                y_point = self.coords_to_point(0, new_output)
                lines = VGroup(*[
                    Line(dot.get_center(), point)
                    for point in (x_point, y_point)
                ])
                lines.set_color(YELLOW)
                self.play(ShowCreationThenDestruction(
                    lines, run_time=2
                ))
                self.wait(0.25)


class RepeatedApplicationGraphicallyFromNegPhi(RepeatedApplicationGraphically):
    CONFIG = {
        "starting_value": -0.61,
        "n_jumps": 13,
        "n_times_to_show_identity_property": 0,
    }


class LetsSwitchToTheTransformationalView(TeacherStudentsScene):
    def construct(self):
        self.teacher_says(
            "Lose the \\\\ graphs!",
            target_mode="hooray"
        )
        self.change_student_modes("hooray", "erm", "surprised")
        self.wait(5)


class AnalyzeFunctionWithTransformations(NumberlineTransformationScene):
    CONFIG = {
        "input_line_zero_point": 0.5 * UP,
        "output_line_zero_point": 2 * DOWN,
        "func": lambda x: 1 + 1.0 / x,
        "num_initial_applications": 10,
        "num_repeated_local_applications": 7,
        "zoomed_display_width": 3.5,
        "zoomed_display_height": 2,
        "default_mapping_animation_config": {},
    }

    def construct(self):
        self.add_function_title()
        self.repeatedly_apply_function()
        self.show_phi_and_phi_bro()
        self.zoom_in_on_phi()
        self.zoom_in_on_phi_bro()

    def setup_number_lines(self):
        NumberlineTransformationScene.setup_number_lines(self)
        for line in self.input_line, self.output_line:
            VGroup(line, line.tick_marks).set_stroke(width=2)

    def add_function_title(self):
        title = TexMobject("f(x)", "=", "1 +", "\\frac{1}{x}")
        title.to_edge(UP)
        self.add(title)
        self.title = title

    def repeatedly_apply_function(self):
        input_zero_point = self.input_line.number_to_point(0)
        output_zero_point = self.output_line.number_to_point(0)
        sample_dots = self.get_sample_dots(
            delta_x=0.05,
            dot_radius=0.05,
            x_min=-10,
            x_max=10,
        )
        sample_dots.set_stroke(BLACK, 0.5)
        sample_points = list(map(Mobject.get_center, sample_dots))

        self.play(LaggedStartMap(
            FadeInFrom, sample_dots,
            lambda m: (m, UP)
        ))
        self.show_arrows(sample_points)
        self.wait()
        for x in range(self.num_initial_applications):
            self.apply_function(
                self.func,
                apply_function_to_number_line=False,
                sample_dots=sample_dots
            )
            self.wait()

            shift_vect = input_zero_point - output_zero_point
            shift_vect[0] = 0
            lower_output_line = self.output_line.copy()
            upper_output_line = self.output_line.copy()
            lower_output_line.shift(-shift_vect)
            lower_output_line.fade(1)

            self.remove(self.output_line)
            self.play(
                ReplacementTransform(lower_output_line, self.output_line),
                upper_output_line.shift, shift_vect,
                upper_output_line.fade, 1,
                sample_dots.shift, shift_vect,
            )
            self.remove(upper_output_line)
            self.wait()
        self.play(FadeOut(sample_dots))

    def show_arrows(self, sample_points):
        input_zero_point = self.input_line.number_to_point(0)
        point_func = self.number_func_to_point_func(self.func)
        alt_point_func = self.number_func_to_point_func(lambda x: 1.0 / x)
        arrows, alt_arrows = [
            VGroup(*[
                Arrow(
                    point, func(point), buff=SMALL_BUFF,
                    tip_length=0.15
                )
                for point in sample_points
                if get_norm(point - input_zero_point) > 0.3
            ])
            for func in (point_func, alt_point_func)
        ]
        for group in arrows, alt_arrows:
            group.set_stroke(WHITE, 0.5)
            group.set_color_by_gradient(RED, YELLOW)
            for arrow in group:
                arrow.tip.set_stroke(BLACK, 0.5)

        one_plus = self.title.get_part_by_tex("1 +")
        one_plus_rect = BackgroundRectangle(one_plus)
        one_plus_rect.set_fill(BLACK, opacity=0.8)

        self.play(LaggedStartMap(GrowArrow, arrows))
        self.wait()
        self.play(LaggedStartMap(
            ApplyMethod, arrows,
            lambda a: (a.scale, 0.7),
            rate_func=there_and_back,
        ))
        self.wait()
        self.play(
            Transform(arrows, alt_arrows),
            FadeIn(one_plus_rect, remover=True),
            rate_func=there_and_back_with_pause,
            run_time=4,
        )
        self.wait()

        self.all_arrows = arrows

    def show_phi_and_phi_bro(self):
        phi = (1 + np.sqrt(5)) / 2
        phi_bro = (1 - np.sqrt(5)) / 2

        input_phi_point = self.input_line.number_to_point(phi)
        output_phi_point = self.output_line.number_to_point(phi)
        input_phi_bro_point = self.input_line.number_to_point(phi_bro)
        output_phi_bro_point = self.output_line.number_to_point(phi_bro)

        tick = Line(UP, DOWN)
        tick.set_stroke(YELLOW, 3)
        tick.match_height(self.input_line.tick_marks)
        phi_tick = tick.copy().move_to(input_phi_point, DOWN)
        phi_bro_tick = tick.copy().move_to(input_phi_bro_point, DOWN)
        VGroup(phi_tick, phi_bro_tick).shift(SMALL_BUFF * DOWN)

        phi_label = TexMobject("1.618\\dots")
        phi_label.next_to(phi_tick, UP)
        phi_bro_label = TexMobject("-0.618\\dots")
        phi_bro_label.next_to(phi_bro_tick, UP)
        VGroup(phi_label, phi_bro_label).set_color(YELLOW)

        arrow_kwargs = {
            "buff": SMALL_BUFF,
            "rectangular_stem_width": 0.035,
            "tip_length": 0.2,
        }
        phi_arrow = Arrow(phi_tick, output_phi_point, **arrow_kwargs)
        phi_bro_arrow = Arrow(phi_bro_tick, output_phi_bro_point, **arrow_kwargs)

        def fade_arrow(arrow):
            # arrow.set_stroke(DARK_GREY, 0.5)
            arrow.set_stroke(width=0.1)
            arrow.tip.set_fill(opacity=0)
            arrow.tip.set_stroke(width=0)
            return arrow

        self.play(
            LaggedStartMap(
                ApplyFunction, self.all_arrows,
                lambda a: (fade_arrow, a)
            ),
            FadeIn(phi_arrow),
            FadeIn(phi_bro_arrow),
        )
        self.play(
            Write(phi_label),
            GrowFromCenter(phi_tick)
        )
        self.play(
            Write(phi_bro_label),
            GrowFromCenter(phi_bro_tick)
        )

        self.set_variables_as_attrs(
            input_phi_point, output_phi_point,
            input_phi_bro_point, output_phi_bro_point,
            phi_label, phi_tick,
            phi_bro_label, phi_bro_tick,
            phi_arrow, phi_bro_arrow
        )

    def zoom_in_on_phi(self):
        phi = (1 + np.sqrt(5)) / 2
        # phi_point = self.get_input_point(phi)
        local_sample_dots = self.get_local_sample_dots(
            phi, dot_radius=0.005, sample_radius=1
        )
        local_coordinate_values = [1.55, 1.6, 1.65, 1.7]

        # zcbr = self.zoomed_camera_background_rectangle
        zcbr_group = self.zoomed_camera_background_rectangle_group
        zcbr_group.add(self.phi_tick)

        title = self.title
        deriv_text = TexMobject(
            "|", "\\frac{df}{dx}(\\varphi)", "|", "< 1",
            tex_to_color_map={"\\varphi": YELLOW}
        )
        deriv_text.get_parts_by_tex("|").match_height(
            deriv_text, stretch=True
        )
        deriv_text.move_to(title, UP)
        approx_value = TexMobject("\\approx |%.2f|" % (-1 / phi**2))
        approx_value.move_to(deriv_text)
        deriv_text_lhs = deriv_text[:-1]
        deriv_text_rhs = deriv_text[-1]

        self.zoom_in_on_input(
            phi,
            local_sample_dots=local_sample_dots,
            local_coordinate_values=local_coordinate_values
        )
        self.wait()
        self.apply_function(
            self.func,
            apply_function_to_number_line=False,
            local_sample_dots=local_sample_dots,
            target_coordinate_values=local_coordinate_values,
        )
        self.wait()
        self.play(
            FadeInFromDown(deriv_text_lhs),
            FadeInFromDown(deriv_text_rhs),
            title.to_corner, UL
        )
        self.wait()
        self.play(
            deriv_text_lhs.next_to, approx_value, LEFT,
            deriv_text_rhs.next_to, approx_value, RIGHT,
            FadeIn(approx_value)
        )
        self.wait()
        for n in range(self.num_repeated_local_applications):
            self.apply_function(
                self.func,
                apply_function_to_number_line=False,
                local_sample_dots=local_sample_dots,
                path_arc=60 * DEGREES,
                run_time=2
            )

        self.deriv_text = VGroup(
            deriv_text_lhs, deriv_text_rhs, approx_value
        )

    def zoom_in_on_phi_bro(self):
        zcbr = self.zoomed_camera_background_rectangle
        # zcbr_group = self.zoomed_camera_background_rectangle_group
        zoomed_frame = self.zoomed_camera.frame

        phi_bro = (1 - np.sqrt(5)) / 2
        # phi_bro_point = self.get_input_point(phi_bro)
        local_sample_dots = self.get_local_sample_dots(phi_bro)
        local_coordinate_values = [-0.65, -0.6, -0.55]

        deriv_text = TexMobject(
            "\\left| \\frac{df}{dx}\\left(\\frac{-1}{\\varphi}\\right) \\right|",
            "\\approx |%.2f|" % (-1 / (phi_bro**2)),
            "> 1"
        )
        deriv_text.move_to(self.deriv_text, UL)
        deriv_text[0][10:14].set_color(YELLOW)

        self.play(
            zoomed_frame.set_height, 4,
            zoomed_frame.center,
            self.deriv_text.fade, 1,
            run_time=2
        )
        self.wait()
        zcbr.set_fill(opacity=0)
        self.zoom_in_on_input(
            phi_bro,
            local_sample_dots=local_sample_dots,
            local_coordinate_values=local_coordinate_values,
            zoom_factor=self.zoom_factor,
            first_anim_kwargs={"run_time": 2},
        )
        self.wait()
        self.play(FadeInFromDown(deriv_text))
        self.wait()
        zcbr.set_fill(opacity=1)
        self.apply_function(
            self.func,
            apply_function_to_number_line=False,
            local_sample_dots=local_sample_dots,
            target_coordinate_values=local_coordinate_values,
        )
        self.wait()
        for n in range(self.num_repeated_local_applications):
            self.apply_function(
                self.func,
                apply_function_to_number_line=False,
                local_sample_dots=local_sample_dots,
                path_arc=20 * DEGREES,
                run_time=2,
            )


class StabilityAndInstability(AnalyzeFunctionWithTransformations):
    CONFIG = {
        "num_initial_applications": 0,
    }

    def construct(self):
        self.force_skipping()
        self.add_function_title()
        self.repeatedly_apply_function()
        self.show_phi_and_phi_bro()
        self.revert_to_original_skipping_status()

        self.label_stability()
        self.write_derivative_fact()

    def label_stability(self):
        self.title.to_corner(UL)

        stable_label = TextMobject("Stable fixed point")
        unstable_label = TextMobject("Unstable fixed point")
        labels = VGroup(stable_label, unstable_label)
        labels.scale(0.8)
        stable_label.next_to(self.phi_label, UP, aligned_edge=ORIGIN)
        unstable_label.next_to(self.phi_bro_label, UP, aligned_edge=ORIGIN)

        phi_point = self.input_phi_point
        phi_bro_point = self.input_phi_bro_point

        arrow_groups = VGroup()
        for point in phi_point, phi_bro_point:
            arrows = VGroup(*[a for a in self.all_arrows if get_norm(a.get_start() - point) < 0.75]).copy()
            arrows.set_fill(PINK, 1)
            arrows.set_stroke(PINK, 3)
            arrows.second_anim = LaggedStartMap(
                ApplyMethod, arrows,
                lambda m: (m.set_color, YELLOW),
                rate_func=there_and_back_with_pause,
                lag_ratio=0.7,
                run_time=2,
            )
            arrows.anim = AnimationGroup(*list(map(GrowArrow, arrows)))
            arrow_groups.add(arrows)
        phi_arrows, phi_bro_arrows = arrow_groups

        self.add_foreground_mobjects(self.phi_arrow, self.phi_bro_arrow)
        self.play(
            Write(stable_label),
            phi_arrows.anim,
        )
        self.play(phi_arrows.second_anim)
        self.play(
            Write(unstable_label),
            phi_bro_arrows.anim,
        )
        self.play(phi_bro_arrows.second_anim)
        self.wait()

        self.stable_label = stable_label
        self.unstable_label = unstable_label
        self.phi_arrows = phi_arrows
        self.phi_bro_arrows = phi_bro_arrows

    def write_derivative_fact(self):
        stable_label = self.stable_label
        unstable_label = self.unstable_label
        labels = VGroup(stable_label, unstable_label)
        phi_arrows = self.phi_arrows
        phi_bro_arrows = self.phi_bro_arrows
        arrow_groups = VGroup(phi_arrows, phi_bro_arrows)

        deriv_labels = VGroup()
        for char, label in zip("<>", labels):
            deriv_label = TexMobject(
                "\\big|", "\\frac{df}{dx}(", "x", ")", "\\big|",
                char, "1"
            )
            deriv_label.get_parts_by_tex("\\big|").match_height(
                deriv_label, stretch=True
            )
            deriv_label.set_color_by_tex("x", YELLOW, substring=False)
            deriv_label.next_to(label, UP)
            deriv_labels.add(deriv_label)

        dot_groups = VGroup()
        for arrow_group in arrow_groups:
            dot_group = VGroup()
            for arrow in arrow_group:
                start_point, end_point = [
                    line.number_to_point(line.point_to_number(p))
                    for line, p in [
                        (self.input_line, arrow.get_start()),
                        (self.output_line, arrow.get_end()),
                    ]
                ]
                dot = Dot(start_point, radius=0.05)
                dot.set_color(YELLOW)
                dot.generate_target()
                dot.target.move_to(end_point)
                dot_group.add(dot)
            dot_groups.add(dot_group)

        for deriv_label, dot_group in zip(deriv_labels, dot_groups):
            self.play(FadeInFromDown(deriv_label))
            self.play(LaggedStartMap(GrowFromCenter, dot_group))
            self.play(*list(map(MoveToTarget, dot_group)), run_time=2)
            self.wait()


class StaticAlgebraicObject(Scene):
    def construct(self):
        frac = get_phi_continued_fraction(40)
        frac.set_width(FRAME_WIDTH - 1)
        # frac.shift(2 * DOWN)
        frac.to_edge(DOWN)
        frac.set_stroke(WHITE, width=0.5)

        title = TexMobject(
            "\\infty \\ne \\lim",
            tex_to_color_map={"\\ne": RED}
        )
        title.scale(1.5)
        title.to_edge(UP)

        polynomial = TexMobject("x^2 - x - 1 = 0")
        polynomial.move_to(title)

        self.add(title)
        self.play(LaggedStartMap(
            GrowFromCenter, frac,
            lag_ratio=0.1,
            run_time=3
        ))
        self.wait()
        factor = 1.1
        self.play(frac.scale, factor, run_time=0.5)
        self.play(
            frac.scale, 1 / factor,
            frac.set_color, LIGHT_GREY,
            run_time=0.5, rate_func=lambda t: t**5,
        )
        self.wait()
        self.play(
            FadeOut(title),
            FadeIn(polynomial)
        )
        self.wait(2)


class NotBetterThanGraphs(TeacherStudentsScene):
    def construct(self):
        self.student_says(
            "Um, yeah, I'll stick \\\\ with graphs thanks",
            target_mode="sassy",
        )
        self.play(
            self.teacher.change, "guilty",
            self.get_student_changes("sad", "sassy", "hesitant")
        )
        self.wait(2)
        self.play(
            RemovePiCreatureBubble(self.students[1]),
            self.teacher.change, "raise_right_hand"
        )
        self.change_all_student_modes(
            "confused", look_at_arg=self.screen
        )
        self.wait(3)
        self.teacher_says(
            "You must flex those \\\\ conceptual muscles",
            added_anims=[self.get_student_changes(
                *3 * ["thinking"],
                look_at_arg=self.teacher.eyes
            )]
        )
        self.wait(3)


class WhatComesAfterWrapper(Wrapper):
    CONFIG = {"title": "Beyond the first year"}

    def construct(self):
        Wrapper.construct(self)
        new_title = TextMobject("Next video")
        new_title.set_color(BLUE)
        new_title.move_to(self.title)

        self.play(
            FadeInFromDown(new_title),
            self.title.shift, UP,
            self.title.fade, 1,
        )
        self.wait(3)


class TopicsAfterSingleVariable(PiCreatureScene, MoreTopics):
    CONFIG = {
        "pi_creatures_start_on_screen": False,
    }

    def construct(self):
        MoreTopics.construct(self)
        self.show_horror()
        self.zero_in_on_complex_analysis()

    def create_pi_creatures(self):
        creatures = VGroup(*[
            PiCreature(color=color)
            for color in [BLUE_E, BLUE_C, BLUE_D]
        ])
        creatures.arrange(RIGHT, buff=LARGE_BUFF)
        creatures.scale(0.5)
        creatures.to_corner(DR)
        return creatures

    def show_horror(self):
        creatures = self.get_pi_creatures()
        modes = ["horrified", "tired", "horrified"]
        for creature, mode in zip(creatures, modes):
            creature.generate_target()
            creature.target.change(mode, self.other_topics)
        creatures.fade(1)

        self.play(LaggedStartMap(MoveToTarget, creatures))
        self.wait(2)

    def zero_in_on_complex_analysis(self):
        creatures = self.get_pi_creatures()
        complex_analysis = self.other_topics[1]
        self.other_topics.remove(complex_analysis)

        self.play(
            complex_analysis.scale, 1.25,
            complex_analysis.center,
            complex_analysis.to_edge, UP,
            LaggedStartMap(FadeOut, self.other_topics),
            LaggedStartMap(FadeOut, self.lines),
            FadeOut(self.calculus),
            *[
                ApplyMethod(creature.change, "pondering")
                for creature in creatures
            ]
        )
        self.wait(4)


class ShowJacobianZoomedIn(LinearTransformationScene, ZoomedScene):
    CONFIG = {
        "show_basis_vectors": False,
        "show_coordinates": True,
        "zoom_factor": 0.05,
    }

    def setup(self):
        LinearTransformationScene.setup(self)
        ZoomedScene.setup(self)

    def construct(self):
        def example_function(point):
            x, y, z = point
            return np.array([
                x + np.sin(y),
                y + np.sin(x),
                0
            ])

        zoomed_camera = self.zoomed_camera
        zoomed_display = self.zoomed_display
        frame = zoomed_camera.frame
        frame.move_to(3 * LEFT + 1 * UP)
        frame.set_color(YELLOW)
        zoomed_display.display_frame.set_color(YELLOW)
        zd_rect = BackgroundRectangle(
            zoomed_display,
            fill_opacity=1,
            buff=MED_SMALL_BUFF,
        )
        self.add_foreground_mobject(zd_rect)
        zd_rect.anim = UpdateFromFunc(
            zd_rect,
            lambda rect: rect.replace(zoomed_display).scale(1.1)
        )
        zd_rect.next_to(FRAME_HEIGHT * UP, UP)

        tiny_grid = NumberPlane(
            x_radius=2,
            y_radius=2,
            color=BLUE_E,
            secondary_color=DARK_GREY,
        )
        tiny_grid.replace(frame)

        jacobian_words = TextMobject("Jacobian")
        jacobian_words.add_background_rectangle()
        jacobian_words.scale(1.5)
        jacobian_words.move_to(zoomed_display, UP)
        zoomed_display.next_to(jacobian_words, DOWN)

        self.play(self.get_zoom_in_animation())
        self.activate_zooming()
        self.play(
            self.get_zoomed_display_pop_out_animation(),
            zd_rect.anim
        )
        self.play(
            ShowCreation(tiny_grid),
            Write(jacobian_words),
            run_time=2
        )
        self.add_transformable_mobject(tiny_grid)
        self.add_foreground_mobject(jacobian_words)
        self.wait()
        self.apply_nonlinear_transformation(
            example_function,
            added_anims=[MaintainPositionRelativeTo(
                zoomed_camera.frame, tiny_grid,
            )],
            run_time=5
        )
        self.wait()


class PrinciplesOverlay(PiCreatureScene):
    CONFIG = {
        "default_pi_creature_kwargs": {
            "color": GREY_BROWN,
            "flip_at_start": True,
        },
        "default_pi_creature_start_corner": DR,
    }

    def construct(self):
        morty = self.pi_creature
        q_marks = VGroup(*[TexMobject("?") for x in range(40)])
        q_marks.arrange_in_grid(4, 10)
        q_marks.space_out_submobjects(1.4)
        for mark in q_marks:
            mark.shift(
                random.random() * RIGHT,
                random.random() * UP,
            )
            mark.scale(1.5)
            mark.set_stroke(BLACK, 1)
        q_marks.next_to(morty, UP)
        q_marks.shift_onto_screen()
        q_marks.sort(
            lambda p: get_norm(p - morty.get_top())
        )

        self.play(morty.change, "pondering")
        self.wait(2)
        self.play(morty.change, "raise_right_hand")
        self.wait()
        self.play(morty.change, "thinking")
        self.wait(4)
        self.play(FadeInFromDown(q_marks[0]))
        self.wait(2)
        self.play(LaggedStartMap(
            FadeInFromDown, q_marks[1:],
            run_time=3
        ))
        self.wait(3)


class ManyInfiniteExpressions(Scene):
    def construct(self):
        frac = get_phi_continued_fraction(10)
        frac.set_height(2)
        frac.to_corner(UL)

        n = 9
        radical_str_parts = [
            "%d + \\sqrt{" % d
            for d in range(1, n + 1)
        ]
        radical_str_parts += ["\\cdots"]
        radical_str_parts += ["}"] * n
        radical = TexMobject("".join(radical_str_parts))
        radical.to_corner(UR)
        radical.to_edge(DOWN)
        radical.set_color_by_gradient(YELLOW, RED)

        n = 12
        power_tower = TexMobject(
            *["\\sqrt{2}^{"] * n + ["\\dots"] + ["}"] * n
        )
        power_tower.to_corner(UR)
        power_tower.set_color_by_gradient(BLUE, GREEN)

        self.play(*[
            LaggedStartMap(
                GrowFromCenter, group,
                lag_ratio=0.1,
                run_time=8,
            )
            for group in (frac, radical, power_tower)
        ])
        self.wait(2)


class HoldUpPromo(PrinciplesOverlay):
    def construct(self):
        morty = self.pi_creature

        url = TextMobject("https://brilliant.org/3b1b/")
        url.to_corner(UL)

        rect = ScreenRectangle(height=5.5)
        rect.next_to(url, DOWN)
        rect.to_edge(LEFT)

        self.play(
            Write(url),
            self.pi_creature.change, "raise_right_hand"
        )
        self.play(ShowCreation(rect))
        self.wait(2)
        self.change_mode("thinking")
        self.wait()
        self.look_at(url)
        self.wait(10)
        self.change_mode("happy")
        self.wait(10)
        self.change_mode("raise_right_hand")
        self.wait(10)

        self.play(FadeOut(rect), FadeOut(url))
        self.play(morty.change, "raise_right_hand")
        self.wait()
        self.play(morty.change, "hooray")
        self.wait(3)


class EndScreen(PatreonEndScreen):
    CONFIG = {
        "specific_patrons": [
            "Juan Benet",
            "Keith Smith",
            "Chloe Zhou",
            "Desmos",
            "Burt Humburg",
            "CrypticSwarm",
            "Andrew Sachs",
            "Ho\\`ang T\\`ung L\\^am",
            # "Hoàng Tùng Lâm",
            "Devin Scott",
            "Akash Kumar",
            "Felix Tripier",
            "Arthur Zey",
            "David Kedmey",
            "Ali Yahya",
            "Mayank M. Mehrotra",
            "Lukas Biewald",
            "Yana Chernobilsky",
            "Kaustuv DeBiswas",
            "Yu Jun",
            "dave nicponski",
            "Damion Kistler",
            "Jordan Scales",
            "Markus Persson",
            "Fela",
            "Fred Ehrsam",
            "Britt Selvitelle",
            "Jonathan Wilson",
            "Ryan Atallah",
            "Joseph John Cox",
            "Luc Ritchie",
            "Matt Roveto",
            "Jamie Warner",
            "Marek Cirkos",
            "Magister Mugit",
            "Stevie Metke",
            "Cooper Jones",
            "James Hughes",
            "John V Wertheim",
            "Chris Giddings",
            "Song Gao",
            "Alexander Feldman",
            "Matt Langford",
            "Max Mitchell",
            "Richard Burgmann",
            "John Griffith",
            "Chris Connett",
            "Steven Tomlinson",
            "Jameel Syed",
            "Bong Choung",
            "Ignacio Freiberg",
            "Zhilong Yang",
            "Giovanni Filippi",
            "Eric Younge",
            "Prasant Jagannath",
            "Cody Brocious",
            "James H. Park",
            "Norton Wang",
            "Kevin Le",
            "Tianyu Ge",
            "David MacCumber",
            "Oliver Steele",
            "Yaw Etse",
            "Dave B",
            "Waleed Hamied",
            "George Chiesa",
            "supershabam",
            "Delton Ding",
            "Thomas Tarler",
            "Isak Hietala",
            "1st ViewMaths",
            "Jacob Magnuson",
            "Mark Govea",
            "Clark Gaebel",
            "Mathias Jansson",
            "David Clark",
            "Michael Gardner",
            "Mads Elvheim",
            "Awoo",
            "Dr . David G. Stork",
            "Ted Suzman",
            "Linh Tran",
            "Andrew Busey",
            "John Haley",
            "Ankalagon",
            "Eric Lavault",
            "Boris Veselinovich",
            "Julian Pulgarin",
            "Jeff Linse",
            "Robert Teed",
            "Jason Hise",
            "Meshal Alshammari",
            "Bernd Sing",
            "James Thornton",
            "Mustafa Mahdi",
            "Mathew Bramson",
            "Jerry Ling",
            "Sh\\`im\\'in Ku\\=ang",
            "Rish Kundalia",
            "Achille Brighton",
            "Ripta Pasay",
        ]
    }


# class Thumbnail(GraphicalIntuitions):
class Thumbnail(AnalyzeFunctionWithTransformations):
    CONFIG = {
        "x_axis_width": 12,
        "graph_origin": 1.5 * DOWN + 4 * LEFT,
        "num_initial_applications": 1,
        "input_line_zero_point": 2 * UP,
    }

    def construct(self):
        self.add_function_title()
        self.title.fade(1)
        self.titles.fade(1)
        self.repeatedly_apply_function()
        self.all_arrows.set_stroke(width=1)

        full_rect = FullScreenFadeRectangle()
        cross = Cross(full_rect)
        cross.set_stroke(width=40)

        # self.add(cross)
