# -*- coding: utf-8 -*-

from big_ol_pile_of_manim_imports import *

from old_projects.uncertainty import Flash
from old_projects.WindingNumber import *


# Warning, this file uses ContinualChangingDecimal,
# which has since been been deprecated.  Use a mobject
# updater instead


class AltTeacherStudentsScene(TeacherStudentsScene):
    def setup(self):
        TeacherStudentsScene.setup(self)
        self.teacher.set_color(YELLOW_E)

###############


class IntroSceneWrapper(PiCreatureScene):
    CONFIG = {
        "default_pi_creature_kwargs" : {
            "color" : YELLOW_E,
            "flip_at_start" : False,
            "height" : 2,
        },
        "default_pi_creature_start_corner" : DOWN+LEFT,
    }
    def construct(self):
        self.introduce_two_words()
        self.describe_main_topic()
        self.describe_meta_topic()

    def introduce_two_words(self):
        morty = self.pi_creature
        rect = ScreenRectangle(height = 5)
        rect.to_corner(UP+RIGHT)
        self.add(rect)

        h_line = Line(LEFT, RIGHT).scale(2)
        h_line.to_corner(UP+LEFT)
        h_line.shift(0.5*DOWN)

        main_topic, meta_topic = topics = VGroup(
            TextMobject("Main topic"),
            TextMobject("Meta topic"),
        )
        topics.next_to(morty, UP)
        topics.shift_onto_screen()

        self.play(
            morty.change, "raise_left_hand",
            FadeInFromDown(main_topic)
        )
        self.wait()
        self.play(
            morty.change, "raise_right_hand",
            main_topic.next_to, meta_topic.get_top(), UP, MED_SMALL_BUFF,
            FadeInFromDown(meta_topic)
        )
        self.wait()
        self.play(
            morty.change, "happy",
            main_topic.next_to, h_line, UP,
            meta_topic.set_fill, {"opacity" : 0.2},
        )
        self.play(ShowCreation(h_line))
        self.wait()

        self.set_variables_as_attrs(h_line, main_topic, meta_topic)

    def describe_main_topic(self):
        h_line = self.h_line
        morty = self.pi_creature
        main_topic = self.main_topic
        meta_topic = self.meta_topic

        solver = TextMobject("2d equation solver")
        solver.match_width(h_line)
        solver.next_to(h_line, DOWN)
        rainbow_solver1 = solver.copy()
        rainbow_solver2 = solver.copy()
        colors = ["RED", "ORANGE", "YELLOW", "GREEN", BLUE, "PURPLE", PINK]
        rainbow_solver1.set_color_by_gradient(*colors)
        rainbow_solver2.set_color_by_gradient(*reversed(colors))


        xy_equation = TexMobject("""
            \\left[\\begin{array}{c}
                ye^x \\\\
                \\sin(|xy|)
            \\end{array}\\right] = 
            \\left[\\begin{array}{c}
                y^2 \\\\
                3y
            \\end{array}\\right]
        """)
        # xy_equation.set_color_by_tex_to_color_map({
        #     "x" : BLUE,
        #     "y" : YELLOW
        # })
        xy_equation.scale(0.8)
        xy_equation.next_to(solver, DOWN, MED_LARGE_BUFF)

        z_equation = TexMobject("z", "^5", "+", "z", "+", "1", "=", "0")
        z_equation.set_color_by_tex("z", GREEN)
        z_equation.move_to(xy_equation, UP)

        zeta = TexMobject("\\zeta(s) = 0")
        zeta[2].set_color(GREEN)
        zeta.next_to(z_equation, DOWN, MED_LARGE_BUFF)

        self.play(Write(solver))
        self.play(
            LaggedStartMap(FadeIn, xy_equation, run_time = 1),
            morty.change, "pondering"
        )
        self.wait(2)
        self.play(
            FadeOut(xy_equation),
            FadeIn(z_equation)
        )
        self.wait()
        self.play(Write(zeta))
        self.wait()
        solver.save_state()
        for rainbow_solver in rainbow_solver1, rainbow_solver2:
            self.play(Transform(
                solver, rainbow_solver,
                run_time = 2,
                lag_ratio = 0.5
            ))
        self.play(solver.restore)
        self.wait()

        self.play(LaggedStartMap(
            FadeOut, VGroup(solver, z_equation, zeta)
        ))
        self.play(
            main_topic.move_to, meta_topic,
            main_topic.set_fill, {"opacity" : 0.2},
            meta_topic.move_to, main_topic,
            meta_topic.set_fill, {"opacity" : 1},
            morty.change, "hesitant",
            path_arc = TAU/8,
        )

    def describe_meta_topic(self):
        h_line = self.h_line
        morty = self.pi_creature

        words = TextMobject("Seek constructs which \\\\ compose nicely")
        words.scale(0.7)
        words.next_to(h_line, DOWN)

        self.play(Write(words))
        self.play(morty.change, "happy")
        self.wait(3)

class Introduce1DFunctionCase(Scene):
    CONFIG = {
        "search_range_rect_height" : 0.15,
        "arrow_opacity" : 1,
        "show_dotted_line_to_f" : True,
        "arrow_config": {
            "max_tip_length_to_length_ratio" : 0.5,
        },
        "show_midpoint_value" : True,
    }
    def construct(self):
        self.show_axes_one_at_a_time()
        self.show_two_graphs()
        self.transition_to_sqrt_2_case()
        self.show_example_binary_search()

    def show_axes_one_at_a_time(self):
        axes = Axes(
            x_min = -1, x_max = 3.2,
            x_axis_config = {
                "unit_size" : 3,
                "tick_frequency" : 0.25,
                "numbers_with_elongated_ticks" : list(range(-1, 4))
            },
            y_min = -2, y_max = 4.5,
        )
        axes.to_corner(DOWN+LEFT)
        axes.x_axis.add_numbers(*list(range(-1, 4)))
        axes.y_axis.label_direction = LEFT
        axes.y_axis.add_numbers(-1, *list(range(1, 5)))

        inputs = TextMobject("Inputs")
        inputs.next_to(axes.x_axis, UP, aligned_edge = RIGHT)

        outputs = TextMobject("Outputs")
        outputs.next_to(axes.y_axis, UP, SMALL_BUFF)

        self.play(
            ShowCreation(axes.x_axis),
            Write(inputs)
        )
        self.wait()
        self.play(
            ShowCreation(axes.y_axis),
            FadeOut(axes.x_axis.numbers[1], rate_func = squish_rate_func(smooth, 0, 0.2)),
            Write(outputs)
        )
        self.wait()

        self.axes = axes
        self.inputs_label = inputs
        self.outputs_label = outputs

    def show_two_graphs(self):
        axes = self.axes
        f_graph = axes.get_graph(
            lambda x : 2*x*(x - 0.75)*(x - 1.5) + 1,
            color = BLUE
        )
        g_graph = axes.get_graph(
            lambda x : 1.8*np.cos(TAU*x/2),
            color = YELLOW
        )

        label_x_corod = 2
        f_label = TexMobject("f(x)")
        f_label.match_color(f_graph)
        f_label.next_to(axes.input_to_graph_point(label_x_corod, f_graph), LEFT)

        g_label = TexMobject("g(x)")
        g_label.match_color(g_graph)
        g_label.next_to(
            axes.input_to_graph_point(label_x_corod, g_graph), UP, SMALL_BUFF
        )

        solution = 0.24
        cross_point = axes.input_to_graph_point(solution, f_graph)
        l_v_line, r_v_line, v_line = [
            DashedLine(
                axes.coords_to_point(x, 0),
                axes.coords_to_point(x, f_graph.underlying_function(solution)),
            )
            for x in (axes.x_min, axes.x_max, solution)
        ]

        equation = TexMobject("f(x)", "=", "g(x)")
        equation[0].match_color(f_label)
        equation[2].match_color(g_label)
        equation.next_to(cross_point, UP, buff = 1.5, aligned_edge = LEFT)
        equation_arrow = Arrow(
            equation.get_bottom(), cross_point,
            buff = SMALL_BUFF,
            color = WHITE
        )
        equation.target = TexMobject("x^2", "=", "2")
        equation.target.match_style(equation)
        equation.target.to_edge(UP)

        for graph, label in (f_graph, f_label), (g_graph, g_label):
            self.play(
                ShowCreation(graph),
                Write(label, rate_func = squish_rate_func(smooth, 0.5, 1)),
                run_time = 2
            )
        self.wait()
        self.play(
            ReplacementTransform(r_v_line.copy().fade(1), v_line),
            ReplacementTransform(l_v_line.copy().fade(1), v_line),
            run_time = 2
        )
        self.play(
            ReplacementTransform(f_label.copy(), equation[0]),
            ReplacementTransform(g_label.copy(), equation[2]),
            Write(equation[1]),
            GrowArrow(equation_arrow),
        )
        for x in range(4):
            self.play(
                FadeOut(v_line.copy()),
                ShowCreation(v_line, rate_func = squish_rate_func(smooth, 0.5, 1)),
                run_time = 1.5
            )
        self.wait()
        self.play(
            MoveToTarget(equation, replace_mobject_with_target_in_scene = True),
            *list(map(FadeOut, [equation_arrow, v_line]))
        )

        self.set_variables_as_attrs(
            f_graph, f_label, g_graph, g_label,
            equation = equation.target
        )

    def transition_to_sqrt_2_case(self):
        f_graph = self.f_graph
        f_label = VGroup(self.f_label)
        g_graph = self.g_graph
        g_label = VGroup(self.g_label)
        axes = self.axes
        for label in f_label, g_label:
            for x in range(2):
                label.add(VectorizedPoint(label.get_center()))
        for number in axes.y_axis.numbers:
            number.add_background_rectangle()

        squared_graph = axes.get_graph(lambda x : x**2)
        squared_graph.match_style(f_graph)
        two_graph = axes.get_graph(lambda x : 2)
        two_graph.match_style(g_graph)

        squared_label = TexMobject("f(x)", "=", "x^2")
        squared_label.next_to(
            axes.input_to_graph_point(2, squared_graph), RIGHT
        )
        squared_label.match_color(squared_graph)
        two_label = TexMobject("g(x)", "=", "2")
        two_label.next_to(
            axes.input_to_graph_point(3, two_graph), UP,
        )
        two_label.match_color(two_graph)

        find_sqrt_2 = self.find_sqrt_2 = TextMobject("(Find $\\sqrt{2}$)")
        find_sqrt_2.next_to(self.equation, DOWN)

        self.play(
            ReplacementTransform(f_graph, squared_graph),
            ReplacementTransform(f_label, squared_label),
        )
        self.play(
            ReplacementTransform(g_graph, two_graph),
            ReplacementTransform(g_label, two_label),
            Animation(axes.y_axis.numbers)
        )
        self.wait()
        self.play(Write(find_sqrt_2))
        self.wait()

        self.set_variables_as_attrs(
            squared_graph, two_graph,
            squared_label, two_label,
        )

    def show_example_binary_search(self):
        self.binary_search(
            self.squared_graph, self.two_graph,
            x0 = 1, x1 = 2,
            n_iterations = 8
        )

    ##

    def binary_search(
        self, 
        f_graph, g_graph, 
        x0, x1, 
        n_iterations,
        n_iterations_with_sign_mention = 0,
        zoom = False,
        ):

        axes = self.axes
        rect = self.rect = Rectangle()
        rect.set_stroke(width = 0)
        rect.set_fill(YELLOW, 0.5)
        rect.replace(Line(
            axes.coords_to_point(x0, 0),
            axes.coords_to_point(x1, 0),
        ), dim_to_match = 0)
        rect.stretch_to_fit_height(self.search_range_rect_height)

        #Show first left and right
        mention_signs = n_iterations_with_sign_mention > 0
        kwargs = {"mention_signs" : mention_signs}
        leftovers0 = self.compare_graphs_at_x(f_graph, g_graph, x0, **kwargs)
        self.wait()
        leftovers1 = self.compare_graphs_at_x(f_graph, g_graph, x1, **kwargs)
        self.wait()
        self.play(GrowFromCenter(rect))
        self.wait()

        all_leftovers = VGroup(leftovers0, leftovers1)
        end_points = [x0, x1]
        if mention_signs:
            sign_word0 = leftovers0.sign_word
            sign_word1 = leftovers1.sign_word

        midpoint_line = Line(MED_SMALL_BUFF*UP, ORIGIN, color = YELLOW)
        midpoint_line_update = UpdateFromFunc(
            midpoint_line, lambda l : l.move_to(rect)
        )
        decimal = DecimalNumber(
            0,
            num_decimal_places = 3,
            show_ellipsis = True,
        )
        decimal.scale(0.7)
        decimal_update = ChangingDecimal(
            decimal, lambda a : axes.x_axis.point_to_number(rect.get_center()),
            position_update_func = lambda m : m.next_to(
                midpoint_line, DOWN, SMALL_BUFF,
                submobject_to_align = decimal[:-1],
            ),
        )
        if not self.show_midpoint_value:
            decimal.set_fill(opacity = 0)
            midpoint_line.set_stroke(width = 0)

        #Restrict to by a half each time
        kwargs = {
            "mention_signs" : False,
            "show_decimal" : zoom,
        } 
        for x in range(n_iterations - 1):
            x_mid = np.mean(end_points)
            leftovers_mid = self.compare_graphs_at_x(f_graph, g_graph, x_mid, **kwargs)
            if leftovers_mid.too_high == all_leftovers[0].too_high:
                index_to_fade = 0
            else:
                index_to_fade = 1
            edge = [RIGHT, LEFT][index_to_fade]
            to_fade = all_leftovers[index_to_fade]
            all_leftovers.submobjects[index_to_fade] = leftovers_mid
            end_points[index_to_fade] = x_mid

            added_anims = []
            if mention_signs:
                word = [leftovers0, leftovers1][index_to_fade].sign_word
                if x < n_iterations_with_sign_mention:
                    added_anims = [word.next_to, leftovers_mid[0].get_end(), -edge]
                elif word in self.camera.extract_mobject_family_members(self.mobjects):
                    added_anims = [FadeOut(word)]

            rect.generate_target()
            rect.target.stretch(0.5, 0, about_edge = edge)
            rect.target.stretch_to_fit_height(self.search_range_rect_height)
            self.play(
                MoveToTarget(rect),
                midpoint_line_update,
                decimal_update,
                Animation(all_leftovers),
                FadeOut(to_fade),
                *added_anims
            )
            if zoom:
                factor = 2.0/rect.get_width()
                everything = VGroup(*self.mobjects)
                decimal_index = everything.submobjects.index(decimal)
                midpoint_line_index = everything.submobjects.index(midpoint_line)
                everything.generate_target()
                everything.target.scale(factor, about_point = rect.get_center())
                everything.target[decimal_index].scale(1./factor, about_edge = UP)
                everything.target[midpoint_line_index].scale(1./factor)
                if factor > 1:
                    self.play(
                        everything.scale, factor, 
                        {"about_point" : rect.get_center()}
                    )
            else:
                self.wait()

    def compare_graphs_at_x(
        self, f_graph, g_graph, x, 
        mention_signs = False,
        show_decimal = False,
        ):
        axes = self.axes
        f_point = axes.input_to_graph_point(x, f_graph)
        g_point = axes.input_to_graph_point(x, g_graph)
        arrow = Arrow(
            g_point, f_point, buff = 0,
            **self.arrow_config
        )
        too_high = f_point[1] > g_point[1]
        if too_high:
            arrow.set_fill(GREEN, opacity = self.arrow_opacity)
        else:
            arrow.set_fill(RED, opacity = self.arrow_opacity)

        leftovers = VGroup(arrow)
        leftovers.too_high = too_high

        if self.show_dotted_line_to_f:
            v_line = DashedLine(axes.coords_to_point(x, 0), f_point)
            self.play(ShowCreation(v_line))
            leftovers.add(v_line)

        added_anims = []
        if show_decimal:
            decimal = DecimalNumber(
                axes.x_axis.point_to_number(arrow.get_start()),
                num_decimal_places = 3,
                # show_ellipsis = True,
            )
            height = self.rect.get_height()
            decimal.set_height(height)
            next_to_kwargs = {
                "buff" : height,
            }
            if too_high:
                decimal.next_to(arrow, DOWN, **next_to_kwargs)
                if hasattr(self, "last_up_arrow_decimal"):
                    added_anims += [FadeOut(self.last_up_arrow_decimal)]
                self.last_up_arrow_decimal = decimal
            else:
                decimal.next_to(arrow, UP, **next_to_kwargs)
                if hasattr(self, "last_down_arrow_decimal"):
                    added_anims += [FadeOut(self.last_down_arrow_decimal)]
                self.last_down_arrow_decimal = decimal
            line = Line(decimal, arrow, buff = 0)
            # line.match_color(arrow)
            line.set_stroke(WHITE, 1)
            decimal.add(line)
            added_anims += [FadeIn(decimal)]

        if mention_signs:
            if too_high:
                sign_word = TextMobject("Positive")
                sign_word.set_color(GREEN)
                sign_word.scale(0.7)
                sign_word.next_to(arrow.get_end(), RIGHT)
            else:
                sign_word = TextMobject("Negative")
                sign_word.set_color(RED)
                sign_word.scale(0.7)
                sign_word.next_to(arrow.get_end(), LEFT)
            sign_word.add_background_rectangle()
            added_anims += [FadeIn(sign_word)]
            leftovers.sign_word = sign_word

        self.play(GrowArrow(arrow), *added_anims)

        return leftovers

class PiCreaturesAreIntrigued(AltTeacherStudentsScene):
    def construct(self):
        self.teacher_says(
            "You can extend \\\\ this to 2d",
            bubble_kwargs = {"width" : 4, "height" : 3}
        )
        self.change_student_modes("pondering", "confused", "erm")
        self.look_at(self.screen)
        self.wait(3)

class TransitionFromEquationSolverToZeroFinder(Introduce1DFunctionCase):
    CONFIG = {
        "show_dotted_line_to_f" : False,
        "arrow_config" : {},
        "show_midpoint_value" : False,
    }
    def construct(self):
        #Just run through these without animating.
        self.force_skipping()
        self.show_axes_one_at_a_time()
        self.show_two_graphs()
        self.transition_to_sqrt_2_case()
        self.revert_to_original_skipping_status()
        ##

        self.transition_to_difference_graph()
        self.show_binary_search_with_signs()

    def transition_to_difference_graph(self):
        axes = self.axes
        equation = x_squared, equals, two = self.equation
        for s in "-", "0":
            tex_mob = TexMobject(s)
            tex_mob.scale(0.01)
            tex_mob.fade(1)
            tex_mob.move_to(equation.get_right())
            equation.add(tex_mob)
        find_sqrt_2 = self.find_sqrt_2
        rect = SurroundingRectangle(VGroup(equation, find_sqrt_2))
        rect.set_color(WHITE)

        f_graph = self.squared_graph
        g_graph = self.two_graph
        new_graph = axes.get_graph(
            lambda x : f_graph.underlying_function(x) - g_graph.underlying_function(x),
            color = GREEN
        )
        zero_graph = axes.get_graph(lambda x : 0)
        zero_graph.set_stroke(BLACK, 0)

        f_label = self.squared_label
        g_label = self.two_label
        new_label = TexMobject("f(x)", "-", "g(x)")
        new_label[0].match_color(f_label)
        new_label[2].match_color(g_label)
        new_label.next_to(
            axes.input_to_graph_point(2, new_graph),
            LEFT
        )

        fg_labels = VGroup(f_label, g_label)
        fg_labels.generate_target()
        fg_labels.target.arrange(DOWN, aligned_edge = LEFT)
        fg_labels.target.to_corner(UP+RIGHT)

        new_equation = TexMobject("x^2", "-", "2", "=", "0")
        new_equation[0].match_style(equation[0])
        new_equation[2].match_style(equation[2])
        new_equation.move_to(equation, RIGHT)
        for tex in equation, new_equation:
            tex.sort_alphabetically()

        self.play(ShowCreation(rect))
        self.play(FadeOut(rect))
        self.play(
            ReplacementTransform(equation, new_equation, path_arc = TAU/4),
            find_sqrt_2.next_to, new_equation, DOWN,
        )
        self.play(MoveToTarget(fg_labels))
        self.play(
            ReplacementTransform(f_graph, new_graph),
            ReplacementTransform(g_graph, zero_graph),
        )
        self.play(
            ReplacementTransform(f_label[0].copy(), new_label[0]),
            ReplacementTransform(g_label[0].copy(), new_label[2]),
            Write(new_label[1]),
        )
        self.wait()

        self.set_variables_as_attrs(new_graph, zero_graph)

    def show_binary_search_with_signs(self):
        self.play(FadeOut(self.axes.x_axis.numbers[2]))
        self.binary_search(
            self.new_graph, self.zero_graph,
            1, 2,
            n_iterations = 9,
            n_iterations_with_sign_mention = 2,
            zoom = True,
        )

class RewriteEquationWithTeacher(AltTeacherStudentsScene):
    def construct(self):
        root_two_equations = VGroup(
            TexMobject("x^2", "", "=", "2", ""),
            TexMobject("x^2", "-", "2", "=", "0"),
        )
        for equation in root_two_equations:
            equation.sort_alphabetically()
            for part in equation.get_parts_by_tex("text"):
                part[2:-1].set_color(YELLOW)
                part[2:-1].scale(0.9)
            equation.move_to(self.hold_up_spot, DOWN)

        brace = Brace(root_two_equations[1], UP)
        f_equals_0 = brace.get_tex("f(x) = 0")

        self.teacher_holds_up(root_two_equations[0])
        self.wait()
        self.play(Transform(
            *root_two_equations, 
            run_time = 1.5,
            path_arc = TAU/2
        ))
        self.play(self.get_student_changes(*["pondering"]*3))
        self.play(
            GrowFromCenter(brace),
            self.teacher.change, "happy"
        )
        self.play(Write(f_equals_0))
        self.change_student_modes(*["happy"]*3)
        self.wait()

        #
        to_remove = VGroup(root_two_equations[0], brace, f_equals_0)
        two_d_equation = TexMobject("""
            \\left[\\begin{array}{c}
                ye^x \\\\
                \\sin(xy)
            \\end{array}\\right] = 
            \\left[\\begin{array}{c}
                y^2 + x^3 \\\\
                3y - x
            \\end{array}\\right]
        """)
        complex_equation = TexMobject("z", "^5 + ", "z", " + 1 = 0")
        z_def = TextMobject(
            "(", "$z$", " is complex, ", "$a + bi$", ")",
            arg_separator = ""
        )
        complex_group = VGroup(complex_equation, z_def)
        complex_group.arrange(DOWN)
        for tex in complex_group:
            tex.set_color_by_tex("z", GREEN)
        complex_group.move_to(self.hold_up_spot, DOWN)

        self.play(
            ApplyMethod(
                to_remove.next_to, FRAME_X_RADIUS*RIGHT, RIGHT,
                remover = True,
                rate_func = running_start,
                path_arc = -TAU/4,
            ),
            self.teacher.change, "hesitant",
            self.get_student_changes(*["erm"]*3)
        )
        self.teacher_holds_up(two_d_equation)
        self.change_all_student_modes("horrified")
        self.wait()
        self.play(
            FadeOut(two_d_equation),
            FadeInFromDown(complex_group),
        )
        self.change_all_student_modes("confused")
        self.wait(3)

class InputOutputScene(Scene):
    CONFIG = {
        "plane_width" : 6,
        "plane_height" : 6,
        "x_shift" : FRAME_X_RADIUS/2,
        "y_shift" : MED_LARGE_BUFF,
        "output_scalar" : 10,
        "non_renormalized_func" : plane_func_by_wind_spec(
            (-2, -1, 2), 
            (1, 1, 1), 
            (2, -2, -1),
        ),
    }

    ###

    def func(self, coord_pair):
        out_coords = np.array(self.non_renormalized_func(coord_pair))
        out_norm = get_norm(out_coords)
        if out_norm > 1:
            angle = angle_of_vector(out_coords)
            factor = 0.5-0.1*np.cos(4*angle)
            target_norm = factor*np.log(out_norm)
            out_coords *= target_norm / out_norm
        else:
            out_coords = (0, 0)
        return tuple(out_coords)

    def point_function(self, point):
        in_coords = self.input_plane.point_to_coords(point)
        out_coords = self.func(in_coords)
        return self.output_plane.coords_to_point(*out_coords)

    def get_colorings(self):
        in_cmos = ColorMappedObjectsScene(
            func = lambda p : self.non_renormalized_func(
                (p[0]+self.x_shift, p[1]+self.y_shift)
            )
        )
        scalar = self.output_scalar
        out_cmos = ColorMappedObjectsScene(
            func = lambda p : (
                scalar*(p[0]-self.x_shift), scalar*(p[1]+self.y_shift)
            )
        )

        input_coloring = Rectangle(
            height = self.plane_height,
            width = self.plane_width,
            stroke_width = 0,
            fill_color = WHITE,
            fill_opacity = 1,
        )
        output_coloring = input_coloring.copy()
        colorings = VGroup(input_coloring, output_coloring)
        vects = [LEFT, RIGHT]
        cmos_pair = [in_cmos, out_cmos]
        for coloring, vect, cmos in zip(colorings, vects, cmos_pair):
            coloring.move_to(self.x_shift*vect + self.y_shift*DOWN)
            coloring.color_using_background_image(cmos.background_image_file)
        return colorings

    def get_planes(self):
        input_plane = self.input_plane = NumberPlane(
            x_radius = self.plane_width / 2.0,
            y_radius = self.plane_height / 2.0,
        )
        output_plane = self.output_plane = input_plane.deepcopy()
        planes = VGroup(input_plane, output_plane)
        vects = [LEFT, RIGHT]
        label_texts = ["Input", "Output"]
        label_colors = [GREEN, RED]
        for plane, vect, text, color in zip(planes, vects, label_texts, label_colors):
            plane.stretch_to_fit_width(self.plane_width)
            plane.add_coordinates(x_vals = list(range(-2, 3)), y_vals = list(range(-2, 3)))
            plane.white_parts = VGroup(plane.axes, plane.coordinate_labels)
            plane.coordinate_labels.set_background_stroke(width=0)
            plane.lines_to_fade = VGroup(planes, plane.secondary_lines)
            plane.move_to(vect*FRAME_X_RADIUS/2 + self.y_shift*DOWN)
            label = TextMobject(text)
            label.scale(1.5)
            label.add_background_rectangle()
            label.move_to(plane)
            label.to_edge(UP, buff = MED_SMALL_BUFF)
            plane.add(label)
            plane.label = label
            for submob in plane.get_family():
                if isinstance(submob, TexMobject) and hasattr(submob, "background_rectangle"):
                    submob.remove(submob.background_rectangle)

        return planes

    def get_v_line(self):
        v_line = Line(UP, DOWN).scale(FRAME_Y_RADIUS)
        v_line.set_stroke(WHITE, 5)
        return v_line

    def get_dots(self, input_plane, output_plane):
        step = self.dot_density
        x_min = -3.0
        x_max = 3.0
        y_min = -3.0
        y_max = 3.0
        dots = VGroup()
        for x in np.arange(x_min, x_max + step, step):
            for y in np.arange(y_max, y_min - step, -step):
                out_coords = self.func((x, y))
                dot = Dot(radius = self.dot_radius)
                dot.set_stroke(BLACK, 1)
                dot.move_to(input_plane.coords_to_point(x, y))
                dot.original_position = dot.get_center()
                dot.generate_target()
                dot.target.move_to(output_plane.coords_to_point(*out_coords))
                dot.target_color = rgba_to_color(point_to_rgba(
                    tuple(self.output_scalar*np.array(out_coords))
                ))
                dots.add(dot)
        return dots

class IntroduceInputOutputScene(InputOutputScene):
    CONFIG = {
        "dot_radius" : 0.05,
        "dot_density" : 0.25,
    }
    def construct(self):
        self.setup_planes()
        self.map_single_point_to_point()

    def setup_planes(self):
        self.input_plane, self.output_plane = self.get_planes()
        self.v_line = self.get_v_line()
        self.add(self.input_plane, self.output_plane, self.v_line)

    def map_single_point_to_point(self):
        input_plane = self.input_plane
        output_plane = self.output_plane

        #Dots
        dots = self.get_dots()

        in_dot = dots[int(0.55*len(dots))].copy()
        out_dot = in_dot.target
        for mob in in_dot, out_dot:
            mob.scale(1.5)
        in_dot.set_color(YELLOW)
        out_dot.set_color(PINK)

        input_label_arrow = Vector(DOWN+RIGHT)
        input_label_arrow.next_to(in_dot, UP+LEFT, SMALL_BUFF)
        input_label = TextMobject("Input point")
        input_label.next_to(input_label_arrow.get_start(), UP, SMALL_BUFF)
        for mob in input_label, input_label_arrow:
            mob.match_color(in_dot)
        input_label.add_background_rectangle()
        
        output_label_arrow = Vector(DOWN+LEFT)
        output_label_arrow.next_to(out_dot, UP+RIGHT, SMALL_BUFF)
        output_label = TextMobject("Output point")
        output_label.next_to(output_label_arrow.get_start(), UP, SMALL_BUFF)
        for mob in output_label, output_label_arrow:
            mob.match_color(out_dot)
        output_label.add_background_rectangle()

        path_arc = -TAU/4
        curved_arrow = Arrow(
            in_dot, out_dot,
            buff = SMALL_BUFF,
            path_arc = path_arc,
            color = WHITE,
        )
        curved_arrow.pointwise_become_partial(curved_arrow, 0, 0.95)
        function_label = TexMobject("f(", "\\text{2d input}", ")")
        function_label.next_to(curved_arrow, UP)
        function_label.add_background_rectangle()


        self.play(LaggedStartMap(GrowFromCenter, dots))
        self.play(LaggedStartMap(
            MoveToTarget, dots,
            path_arc = path_arc
        ))
        self.wait()
        self.play(FadeOut(dots))
        self.play(
            GrowFromCenter(in_dot),
            GrowArrow(input_label_arrow),
            FadeIn(input_label)
        )
        self.wait()
        self.play(
            ShowCreation(curved_arrow),
            ReplacementTransform(
                in_dot.copy(), out_dot,
                path_arc = path_arc
            ),
            FadeIn(function_label),
        )
        self.play(
            GrowArrow(output_label_arrow),
            FadeIn(output_label)
        )
        self.wait()
        self.play(*list(map(FadeOut, [
            input_label_arrow, input_label,
            output_label_arrow, output_label,
            curved_arrow, function_label,
        ])))

        #General movements and wiggles
        out_dot_continual_update = self.get_output_dot_continual_update(in_dot, out_dot)
        self.add(out_dot_continual_update)

        for vect in UP, RIGHT:
            self.play(
                in_dot.shift, 0.25*vect,
                rate_func = lambda t : wiggle(t, 8),
                run_time = 2
            )
        for vect in compass_directions(4, UP+RIGHT):
            self.play(Rotating(
                in_dot, about_point = in_dot.get_corner(vect),
                radians = TAU,
                run_time = 1
            ))
        self.wait()
        for coords in (-2, 2), (-2, -2), (2, -2), (1.5, 1.5):
            self.play(
                in_dot.move_to, input_plane.coords_to_point(*coords),
                path_arc = -TAU/4,
                run_time = 2
            )
        self.wait()

    ###

    def get_dots(self):
        input_plane = self.input_plane
        dots = VGroup()
        step = self.dot_density
        x_max = input_plane.x_radius
        x_min = -x_max
        y_max = input_plane.y_radius
        y_min = -y_max

        reverse = False
        for x in np.arange(x_min+step, x_max, step):
            y_range = list(np.arange(x_min+step, x_max, step))
            if reverse:
                y_range.reverse()
            reverse = not reverse
            for y in y_range:
                dot = Dot(radius = self.dot_radius)
                dot.move_to(input_plane.coords_to_point(x, y))
                dot.generate_target()
                dot.target.move_to(self.point_function(dot.get_center()))
                dots.add(dot)
        return dots

    def get_output_dot_continual_update(self, input_dot, output_dot):
        return Mobject.add_updater(
            output_dot, 
            lambda od : od.move_to(self.point_function(input_dot.get_center()))
        )

class IntroduceVectorField(IntroduceInputOutputScene):
    CONFIG = {
        "dot_density" : 0.5,
    }
    def construct(self):
        self.setup_planes()
        input_plane, output_plane = self.input_plane, self.output_plane
        dots = self.get_dots()

        in_dot = dots[0].copy()
        in_dot.move_to(input_plane.coords_to_point(1.5, 1.5))
        out_dot = in_dot.copy()
        out_dot_continual_update = self.get_output_dot_continual_update(in_dot, out_dot)
        for mob in in_dot, out_dot:
            mob.scale(1.5)
        in_dot.set_color(YELLOW)
        out_dot.set_color(PINK)

        out_vector = Arrow(
            LEFT, RIGHT, 
            color = out_dot.get_color(),
        )
        out_vector.set_stroke(BLACK, 1)
        continual_out_vector_update = Mobject.add_updater(
            out_vector, lambda ov : ov.put_start_and_end_on(
                output_plane.coords_to_point(0, 0),
                out_dot.get_center(),
            )
        )

        in_vector = out_vector.copy()
        def update_in_vector(in_vector):
            Transform(in_vector, out_vector).update(1)
            in_vector.scale(0.5)
            in_vector.shift(in_dot.get_center() - in_vector.get_start())
        continual_in_vector_update = Mobject.add_updater(
            in_vector, update_in_vector
        )
        continual_updates = [
            out_dot_continual_update,
            continual_out_vector_update, 
            continual_in_vector_update
        ]

        self.add(in_dot, out_dot)
        self.play(GrowArrow(out_vector, run_time = 2))
        self.wait()
        self.add_foreground_mobjects(in_dot)
        self.play(ReplacementTransform(out_vector.copy(), in_vector))
        self.wait()
        self.add(*continual_updates)
        path = Square().rotate(-90*DEGREES)
        path.replace(Line(
            input_plane.coords_to_point(-1.5, -1.5),
            input_plane.coords_to_point(1.5, 1.5),
        ), stretch = True)
        in_vectors = VGroup()
        self.add(in_vectors)
        for a in np.linspace(0, 1, 25):
            self.play(
                in_dot.move_to, path.point_from_proportion(a),
                run_time = 0.2,
                rate_func=linear,
            )
            in_vectors.add(in_vector.copy())

        # Full vector field
        newer_in_vectors = VGroup()
        self.add(newer_in_vectors)
        for dot in dots:
            self.play(in_dot.move_to, dot, run_time = 1./15)
            newer_in_vectors.add(in_vector.copy())
        self.remove(*continual_updates)
        self.remove()
        self.play(*list(map(FadeOut, [
            out_dot, out_vector, in_vectors, in_dot, in_vector
        ])))
        self.wait()
        target_length = 0.4
        for vector in newer_in_vectors:
            vector.generate_target()
            if vector.get_length() == 0:
                continue
            factor = target_length / vector.get_length()
            vector.target.scale(factor, about_point = vector.get_start())

        self.play(LaggedStartMap(MoveToTarget, newer_in_vectors))
        self.wait()

class TwoDScreenInOurThreeDWorld(AltTeacherStudentsScene, ThreeDScene):
    def construct(self):
        self.ask_about_2d_functions()
        self.show_3d()

    def ask_about_2d_functions(self):
        in_plane = NumberPlane(x_radius = 2.5, y_radius = 2.5)
        in_plane.add_coordinates()
        in_plane.set_height(3)
        out_plane = in_plane.copy()

        in_text = TextMobject("Input space")
        out_text = TextMobject("Output space")
        VGroup(in_text, out_text).scale(0.75)
        in_text.next_to(in_plane, UP, SMALL_BUFF)
        out_text.next_to(out_plane, UP, SMALL_BUFF)
        in_plane.add(in_text)
        out_plane.add(out_text)

        arrow = Arrow(
            LEFT, RIGHT, 
            path_arc = -TAU/4,
            color = WHITE
        )
        arrow.pointwise_become_partial(arrow, 0.0, 0.97)
        group = VGroup(in_plane, arrow, out_plane)
        group.arrange(RIGHT)
        arrow.shift(UP)
        group.move_to(self.students)
        group.to_edge(UP)

        dots = VGroup()
        dots_target = VGroup()
        for x in np.arange(-2.5, 3.0, 0.5):
            for y in np.arange(-2.5, 3.0, 0.5):
                dot = Dot(radius = 0.05)
                dot.move_to(in_plane.coords_to_point(x, y))
                dot.generate_target()
                dot.target.move_to(out_plane.coords_to_point(
                    x + 0.25*np.cos(5*y), y + 0.25*np.sin(3*x)
                ))
                dots.add(dot)
                dots_target.add(dot.target)
        dots.set_color_by_gradient(YELLOW, RED)
        dots_target.set_color_by_gradient(YELLOW, RED)

        self.play(
            self.teacher.change, "raise_right_hand",
            Write(in_plane, run_time = 1)
        )
        self.play(
            ShowCreation(arrow),
            ReplacementTransform(
                in_plane.copy(), out_plane,
                path_arc = -TAU/4,
            )
        )
        self.play(
            LaggedStartMap(GrowFromCenter, dots, run_time = 1),
            self.get_student_changes(*3*["erm"]),
        )
        self.play(LaggedStartMap(MoveToTarget, dots, path_arc = -TAU/4))
        self.wait(3)


    def show_3d(self):
        laptop = Laptop().scale(2)
        laptop.rotate(-TAU/12, DOWN)
        laptop.rotate(-5*TAU/24, LEFT)
        laptop.rotate(TAU/8, LEFT)
        laptop.scale(2.3*FRAME_X_RADIUS/laptop.screen_plate.get_width())
        laptop.shift(-laptop.screen_plate.get_center() + 0.1*IN)
        should_shade_in_3d(laptop)

        everything = VGroup(laptop, *self.mobjects)
        everything.generate_target()
        # for mob in everything.target.get_family():
        #     if isinstance(mob, PiCreature):
        #         mob.change_mode("confused")
        everything.target.rotate(TAU/12, LEFT)
        everything.target.rotate(TAU/16, UP)
        everything.target.shift(4*UP)

        self.move_camera(
            distance = 12,
            run_time = 4,
            added_anims = [MoveToTarget(everything, run_time = 4)],
        )
        always_rotate(everything, axis=UP, rate=3 * DEGREES)
        self.wait(10)

class EveryOutputPointHasAColor(ColorMappedObjectsScene):
    CONFIG = {
        "func" : lambda p : p,
        "dot_spacing" : 0.1,
        "dot_radius" : 0.01,
    }
    def construct(self):
        full_rect = FullScreenRectangle()
        full_rect.set_fill(WHITE, 1)
        full_rect.set_stroke(WHITE, 0)
        full_rect.color_using_background_image(self.background_image_file)

        title = TextMobject("Output Space")
        title.scale(1.5)
        title.to_edge(UP, buff = MED_SMALL_BUFF)
        title.set_stroke(BLACK, 1)
        # self.add_foreground_mobjects(title)

        plane = NumberPlane()
        plane.fade(0.5)
        plane.axes.set_stroke(WHITE, 3)
        # plane.add(BackgroundRectangle(title))
        self.add(plane)


        dots = VGroup()
        step = self.dot_spacing
        for x in np.arange(-FRAME_X_RADIUS, FRAME_X_RADIUS+step, step):
            for y in np.arange(-FRAME_Y_RADIUS, FRAME_Y_RADIUS+step, step):
                dot = Dot(color = WHITE)
                dot.color_using_background_image(self.background_image_file)
                dot.move_to(x*RIGHT + y*UP)
                dots.add(dot)
        random.shuffle(dots.submobjects)

        m = 3 #exponential factor        
        n = 1
        dot_groups = VGroup()
        while n <= len(dots):
            dot_groups.add(dots[n-1:m*n-1])
            n *= m
        self.play(LaggedStartMap(
            LaggedStartMap, dot_groups,
            lambda dg : (GrowFromCenter,  dg),
            run_time = 8,
            lag_ratio = 0.2,
        ))

class DotsHoppingToColor(InputOutputScene):
    CONFIG = {
        "dot_radius" : 0.05,
        "dot_density" : 0.25,
    }
    def construct(self):
        input_coloring, output_coloring = self.get_colorings()
        input_plane, output_plane = self.get_planes()
        v_line = self.get_v_line()

        dots = self.get_dots(input_plane, output_plane)

        right_half_block = Rectangle(
            height = FRAME_HEIGHT,
            width = FRAME_X_RADIUS - SMALL_BUFF,
            stroke_width = 0,
            fill_color = BLACK,
            fill_opacity = 0.8,
        )
        right_half_block.to_edge(RIGHT, buff = 0)

        #Introduce parts
        self.add(input_plane, output_plane, v_line)
        self.play(
            FadeIn(output_coloring), 
            Animation(output_plane),
            output_plane.white_parts.set_color, BLACK,
            output_plane.lines_to_fade.set_stroke, {"width" : 0},
        )
        self.wait()
        self.play(LaggedStartMap(GrowFromCenter, dots, run_time = 3))
        self.wait()

        #Hop over and back
        self.play(LaggedStartMap(
            MoveToTarget, dots, 
            path_arc = -TAU/4,
            run_time = 3,
        ))
        self.wait()
        self.play(LaggedStartMap(
            ApplyMethod, dots,
            lambda d : (d.set_fill, d.target_color),
        ))
        self.wait()
        self.play(LaggedStartMap(
            ApplyMethod, dots,
            lambda d : (d.move_to, d.original_position),
            path_arc = TAU/4,
            run_time = 3,
        ))
        self.wait()
        self.play(
            FadeIn(input_coloring),
            Animation(input_plane),
            input_plane.white_parts.set_color, BLACK,
            input_plane.lines_to_fade.set_stroke, {"width" : 0},
            FadeOut(dots),
        )
        self.wait()

        #Cover output half
        right_half_block.save_state()
        right_half_block.next_to(FRAME_X_RADIUS*RIGHT, RIGHT)
        self.play(right_half_block.restore)
        self.wait()

        # Show yellow points
        inspector = DashedLine(
            ORIGIN, TAU*UP,
            dash_length = TAU/24,
            fill_opacity = 0,
            stroke_width = 3,
            stroke_color = WHITE,
        )
        inspector.add(*inspector.copy().set_color(BLACK).shift((TAU/24)*UP))
        inspector.apply_complex_function(np.exp)
        inspector.scale(0.15)

        inspector_image = inspector.copy()
        def update_inspector_image(inspector_image):
            inspector_image.move_to(self.point_function(inspector.get_center()))

        inspector_image_update_anim = UpdateFromFunc(
            inspector_image, update_inspector_image
        )
        pink_points_label = TextMobject("Pink points")
        pink_points_label.scale(0.7)
        pink_points_label.set_color(BLACK)

        self.play(
            inspector.move_to, input_plane.coords_to_point(-2.75, 2.75),
            inspector.set_stroke, {"width" : 2},
        )
        pink_points_label.next_to(inspector, RIGHT)
        self.play(
            Rotating(
                inspector, about_point = inspector.get_center(),
                rate_func = smooth,
                run_time = 2,
            ),
            Write(pink_points_label)
        )
        self.wait()
        self.play(right_half_block.next_to, FRAME_X_RADIUS*RIGHT, RIGHT)
        inspector_image_update_anim.update(0)
        self.play(ReplacementTransform(
            inspector.copy(), inspector_image,
            path_arc = -TAU/4,
        ))
        self.play(
            ApplyMethod(
                inspector.move_to, 
                input_plane.coords_to_point(-2, 0),
                path_arc = -TAU/8,
                run_time = 3,
            ),
            inspector_image_update_anim
        )
        self.play(
            ApplyMethod(
                inspector.move_to, 
                input_plane.coords_to_point(-2.75, 2.75),
                path_arc = TAU/8,
                run_time = 3,
            ),
            inspector_image_update_anim
        )
        self.play(FadeOut(pink_points_label))

        # Show black zero
        zeros = tuple(it.starmap(input_plane.coords_to_point, [
            (-2., -1), (1, 1), (2, -2),
        ]))
        for x in range(2):
            for zero in zeros:
                path = ParametricFunction(
                    bezier([
                        inspector.get_center(), 
                        input_plane.coords_to_point(0, 0),
                        zero
                    ]),
                    t_min = 0, t_max = 1
                )
                self.play(
                    MoveAlongPath(inspector, path, run_time = 2),
                    inspector_image_update_anim,
                )
                self.wait()
        self.play(FadeOut(VGroup(inspector, inspector_image)))

        # Show all dots and slowly fade them out
        for dot in dots:
            dot.scale(1.5)
        self.play(
            FadeOut(input_coloring),
            input_plane.white_parts.set_color, WHITE,
            LaggedStartMap(GrowFromCenter, dots)
        )
        self.wait()
        random.shuffle(dots.submobjects)
        self.play(LaggedStartMap(
            FadeOut, dots,
            lag_ratio = 0.05,
            run_time = 10,
        ))

        # Ask about whether a region contains a zero
        question = TextMobject("Does this region \\\\ contain a zero?")
        question.add_background_rectangle(opacity = 1)
        question.next_to(input_plane.label, DOWN)
        square = Square()
        square.match_background_image_file(input_coloring)
        square.move_to(input_plane)

        self.play(ShowCreation(square), Write(question))
        self.wait()
        quads = [
            (0, 0.5, 6, 6.25),
            (1, 1, 0.5, 2),
            (-1, -1, 3, 4.5),
            (0, 1.25, 5, 1.7),
            (-2, -1, 1, 1),
        ]
        for x, y, width, height in quads:
            self.play(
                square.stretch_to_fit_width, width,
                square.stretch_to_fit_height, height,
                square.move_to, input_plane.coords_to_point(x, y)
            )
            self.wait()

class SoWeFoundTheZeros(AltTeacherStudentsScene):
    def construct(self):
        self.student_says(
            "Aha! So we \\\\ found the solutions!",
            target_mode = "hooray",
            student_index = 2,
            bubble_kwargs = {"direction" : LEFT},
        )
        self.wait()
        self.teacher_says(
            "Er...only \\\\ kind of",
            target_mode = "hesitant"
        )
        self.wait(3)

class Rearrange2DEquation(AltTeacherStudentsScene):
    def construct(self):
        f_tex, g_tex, h_tex = [
            "%s(\\text{2d point})"%char
            for char in ("f", "g", "h") 
        ]
        zero_tex = "\\vec{\\textbf{0}}"
        equations = VGroup(
            TexMobject(g_tex, "", "=", h_tex, ""),
            TexMobject(g_tex, "-", h_tex, "=", zero_tex),
        )
        equations.move_to(self.hold_up_spot, DOWN)
        equations.shift_onto_screen()

        brace = Brace(equations[1], UP)
        zero_eq = brace.get_tex("%s = %s"%(f_tex, zero_tex))

        for equation in equations:
            equation.set_color_by_tex(g_tex, BLUE)
            equation.set_color_by_tex(h_tex, YELLOW)
            equation.sort_alphabetically()


        self.teacher_holds_up(equations[0])
        self.change_all_student_modes("pondering")
        self.play(Transform(
            *equations,
            run_time = 1.5,
            path_arc = TAU/2
        ))
        self.play(
            Succession(
                GrowFromCenter(brace),
                Write(zero_eq, run_time = 1)
            ),
            self.get_student_changes(*["happy"]*3)
        )
        self.play(*[
            ApplyMethod(pi.change, "thinking", self.screen)
            for pi in self.pi_creatures
        ])
        self.wait(3)

class SearchForZerosInInputSpace(ColorMappedObjectsScene):
    CONFIG = {
        "func" : example_plane_func,
    }
    def construct(self):
        ColorMappedObjectsScene.construct(self)
        title = TextMobject("Input space")
        title.scale(2)
        title.to_edge(UP)
        title.set_stroke(BLACK, 1)
        title.add_background_rectangle()

        plane = NumberPlane()
        plane.fade(0.5)
        plane.axes.set_stroke(WHITE, 3)

        self.add(plane, title)

        looking_glass = Circle()
        looking_glass.set_stroke(WHITE, 3)
        looking_glass.set_fill(WHITE, 0.6)
        looking_glass.color_using_background_image(self.background_image_file)
        question = TextMobject("Which points go to 0?")
        question.next_to(looking_glass, DOWN)
        question.add_background_rectangle()

        mover = VGroup(looking_glass, question)
        mover.move_to(4*LEFT + UP)

        self.play(FadeIn(mover))
        points = [4*RIGHT+UP, 2*RIGHT+2*DOWN, 2*LEFT+2*DOWN, 3*RIGHT+2.5*DOWN]
        for point in points:
            self.play(mover.move_to, point, run_time = 1.5)
            self.wait()

class OneDRegionBoundary(Scene):
    CONFIG = {
        "graph_color" : BLUE,
        "region_rect_height" : 0.1,
    }
    def construct(self):
        x0 = self.x0 = 3 
        x1 = self.x1 = 6
        fx0 = self.fx0 = -2
        fx1 = self.fx1 = 2

        axes = self.axes = Axes(
            x_min = -1, x_max = 10,
            y_min = -3, y_max = 3,
        )
        axes.center()
        axes.set_stroke(width = 2)

        input_word = TextMobject("Input")
        input_word.next_to(axes.x_axis, UP, SMALL_BUFF, RIGHT)
        output_word = TextMobject("Output")
        output_word.next_to(axes.y_axis, UP)
        axes.add(input_word, output_word)
        self.add(axes)

        graph = self.get_graph_part(1, 1)
        alt_graphs = [
            self.get_graph_part(*points)
            for points in [
                (-1, -2),
                (-1, -1, -1),
                (1, 1, 1),
                (-0.75, 0, 1.75),
                (-3, -2, -1),
            ]
        ]

        #Region and boundary
        line = Line(axes.coords_to_point(x0, 0), axes.coords_to_point(x1, 0))
        region = Rectangle(
            stroke_width = 0,
            fill_color = YELLOW,
            fill_opacity = 0.5,
            height = self.region_rect_height
        )
        region.match_width(line, stretch = True)
        region.move_to(line)

        region_words = TextMobject("Input region")
        region_words.set_width(0.8*region.get_width())
        region_words.next_to(region, UP)

        x0_arrow, x1_arrow = arrows = VGroup(*[
            Arrow(
                axes.coords_to_point(x, 0),
                axes.coords_to_point(x, fx),
                color = color,
                buff = 0
            )
            for x, fx, color in [(x0, fx0, RED), (x1, fx1, GREEN)]
        ])
        minus = TexMobject("-")
        minus.match_color(x0_arrow)
        minus.next_to(x0_arrow, UP)
        plus = TexMobject("+")
        plus.match_color(x1_arrow)
        plus.next_to(x1_arrow, DOWN)
        signs = VGroup(plus, minus)


        self.play(
            GrowFromCenter(region),
            FadeIn(region_words)
        )
        self.wait()
        self.play(*it.chain(
            list(map(GrowArrow, arrows)),
            list(map(Write, signs))
        ))
        self.wait()
        self.play(
            ShowCreation(graph), 
            FadeOut(region_words),
        )
        self.wait()
        for alt_graph in alt_graphs + alt_graphs:
            self.play(Transform(graph, alt_graph, path_arc = 0.1*TAU))
        self.wait()


    ###

    def get_graph_part(self, *interim_values):
        result = VMobject()
        result.set_stroke(self.graph_color, 3)
        result.set_fill(opacity = 0)
        values = [self.fx0] + list(interim_values) + [self.fx1]
        result.set_points_smoothly([
            self.axes.coords_to_point(x, fx)
            for x, fx in zip(
                np.linspace(self.x0, self.x1, len(values)),
                values
            )
        ])
        return result

class DirectionOfA2DFunctionAlongABoundary(InputOutputScene):
    def construct(self):
        colorings = self.get_colorings()
        colorings.set_fill(opacity = 0.25)
        input_plane, output_plane = planes = self.get_planes()
        for plane in planes:
            plane.lines_to_fade.set_stroke(width = 0)
        v_line = self.get_v_line()

        rect = Rectangle()
        rect.set_stroke(WHITE, 5)
        rect.set_fill(WHITE, 0)
        line = Line(
            input_plane.coords_to_point(-0.75, 2.5),
            input_plane.coords_to_point(2.5, -1.5),
        )
        rect.replace(line, stretch = True)
        rect.insert_n_curves(50)
        rect.match_background_image_file(colorings[0])

        rect_image = rect.copy()
        rect_image.match_background_image_file(colorings[1])
        def update_rect_image(rect_image):
            rect_image.points = np.array(rect.points)
            rect_image.apply_function(self.point_function)
        rect_image_update_anim = UpdateFromFunc(rect_image, update_rect_image)


        def get_input_point():
            return rect.points[-1]

        def get_output_coords():
            in_coords = input_plane.point_to_coords(get_input_point())
            return self.func(in_coords)

        def get_angle():
            return angle_of_vector(get_output_coords())

        def get_color():
            return rev_to_color(get_angle()/TAU) #Negative?


        out_vect = Vector(RIGHT, color = WHITE)
        out_vect_update_anim = UpdateFromFunc(
            out_vect,
            lambda ov : ov.put_start_and_end_on(
                output_plane.coords_to_point(0, 0),
                rect_image.points[-1]
            ).set_color(get_color())
        )

        dot = Dot()
        dot.set_stroke(BLACK, 1)
        dot_update_anim = UpdateFromFunc(
            dot, lambda d : d.move_to(get_input_point()).set_fill(get_color())
        )

        in_vect = Vector(RIGHT)
        def update_in_vect(in_vect):
            in_vect.put_start_and_end_on(ORIGIN, 0.5*RIGHT)
            in_vect.rotate(get_angle())
            in_vect.set_color(get_color())
            in_vect.shift(get_input_point() - in_vect.get_start())
            return in_vect
        in_vect_update_anim = UpdateFromFunc(in_vect, update_in_vect)

        self.add(colorings, planes, v_line)

        self.play(
            GrowArrow(out_vect),
            GrowArrow(in_vect),
            Animation(dot),
        )
        self.play(
            ShowCreation(rect),
            ShowCreation(rect_image),
            out_vect_update_anim,
            in_vect_update_anim,
            dot_update_anim,
            rate_func = bezier([0, 0, 1, 1]),
            run_time = 10,
        )

class AskAboutHowToGeneralizeSigns(AltTeacherStudentsScene):
    def construct(self):
        # 2d plane
        plane = NumberPlane(x_radius = 2.5, y_radius = 2.5)
        plane.scale(0.8)
        plane.to_corner(UP+LEFT)
        plane.add_coordinates()

        dot = Dot(color = YELLOW)
        label = TextMobject("Sign?")
        label.add_background_rectangle()
        label.scale(0.5)
        label.next_to(dot, UP, SMALL_BUFF)
        dot.add(label)
        dot.move_to(plane.coords_to_point(1, 1))
        dot.save_state()
        dot.fade(1)
        dot.center()

        question = TextMobject(
            "Wait...what would \\\\ positive and negative \\\\ be in 2d?",
        )
        # question.set_color_by_tex_to_color_map({
        #     "+" : "green", 
        #     "textminus" : "red"
        # })


        self.student_says(
            question,
            target_mode = "sassy",
            student_index = 2,
            added_anims = [
                self.teacher.change, "plain",
            ],
            bubble_kwargs = {"direction" : LEFT},
            run_time = 1,
        )
        self.play(
            Write(plane, run_time = 1),
            self.students[0].change, "confused",
            self.students[1].change, "confused",
        )
        self.play(dot.restore)
        for coords in (-1, 1), (1, -1), (0, -2), (-2, 1):
            self.wait(0.5)
            self.play(dot.move_to, plane.coords_to_point(*coords))
        self.wait()

class HypothesisAboutFullyColoredBoundary(ColorMappedObjectsScene):
    CONFIG = {
        "func" : plane_func_from_complex_func(lambda z : z**3),
    }
    def construct(self):
        ColorMappedObjectsScene.construct(self)
        square = Square(side_length = 4)
        square.color_using_background_image(self.background_image_file)
        hypothesis = TextMobject(
           "Working Hypothesis: \\\\",
           "If a 2d function hits outputs of all possible colors \\\\" + 
           "on the boundary of a 2d region,", 
           "that region \\\\ contains a zero.",
           alignment = "",
        )
        hypothesis[0].next_to(hypothesis[1:], UP)
        hypothesis[0].set_color(YELLOW)
        s = hypothesis[1].get_tex_string()
        s = [c for c in s if c not in string.whitespace]
        n = s.index("colors")
        hypothesis[1][n:n+len("colors")].set_color_by_gradient(
            # RED, GOLD_E, YELLOW, GREEN, BLUE, PINK,
            BLUE, PINK, YELLOW,
        )
        hypothesis.to_edge(UP)
        square.next_to(hypothesis, DOWN, MED_LARGE_BUFF)

        self.add(hypothesis[0])
        self.play(
            LaggedStartMap(FadeIn, hypothesis[1]),
            ShowCreation(square, run_time = 8)
        )
        self.play(LaggedStartMap(FadeIn, hypothesis[2]))
        self.play(square.set_fill, {"opacity" : 1}, run_time = 2)
        self.wait()

class PiCreatureAsksWhatWentWrong(PiCreatureScene):
    def construct(self):
        randy = self.pi_creature
        randy.set_color(YELLOW_E)
        randy.flip()
        randy.to_corner(DOWN+LEFT)
        question = TextMobject("What went wrong?")
        question.next_to(randy, UP)
        question.shift_onto_screen()
        question.save_state()
        question.shift(DOWN).fade(1)

        self.play(randy.change, "erm")
        self.wait(2)
        self.play(
            Animation(VectorizedPoint(ORIGIN)),
            question.restore,
            randy.change, "confused",
        )
        self.wait(5)

class ForeverNarrowingLoop(InputOutputScene):
    CONFIG = {
        "target_coords" : (1, 1),
        "input_plane_corner" : UP+RIGHT,
        "shrink_time" : 20,
        "circle_start_radius" : 2.25,
        "start_around_target" : False,

        # Added as a flag to not mess up one clip already used and fine-timed
        # but to make it more convenient to do the other TinyLoop edits
        "add_convenient_waits" : False
    }
    def construct(self):
        input_coloring, output_coloring = colorings = VGroup(*self.get_colorings())
        input_plane, output_plane = planes = VGroup(*self.get_planes())
        for plane in planes:
            plane.white_parts.set_color(BLACK)
            plane.lines_to_fade.set_stroke(width = 0)

        v_line = Line(UP, DOWN).scale(FRAME_Y_RADIUS)
        v_line.set_stroke(WHITE, 5)

        self.add(colorings, v_line, planes)
        self.play(*it.chain(
            [
                ApplyMethod(coloring.set_fill, {"opacity" : 0.2})
                for coloring in colorings
            ],
            [
                ApplyMethod(plane.white_parts.set_color, WHITE)
                for plane in planes
            ]
        ), run_time = 2)

        # circle
        circle = Circle(color = WHITE, radius = self.circle_start_radius)
        circle.flip(axis = RIGHT)
        circle.insert_n_curves(50)
        if self.start_around_target:
            circle.move_to(input_plane.coords_to_point(*self.target_coords))
        else:
            circle.next_to(
                input_coloring.get_corner(self.input_plane_corner), 
                -self.input_plane_corner, 
                SMALL_BUFF
            )
        circle.set_stroke(width = 5)
        circle_image = circle.copy()
        circle.match_background_image_file(input_coloring)
        circle_image.match_background_image_file(output_coloring)

        def update_circle_image(circle_image):
            circle_image.points = circle.points
            circle_image.apply_function(self.point_function)
            circle_image.make_smooth()

        circle_image_update_anim = UpdateFromFunc(
            circle_image, update_circle_image
        )

        def optional_wait():
            if self.add_convenient_waits:
                self.wait()

        optional_wait()
        self.play(
            ShowCreation(circle),
            ShowCreation(circle_image),
            run_time = 3,
            rate_func = bezier([0, 0, 1, 1])
        )
        optional_wait()
        self.play(
            circle.scale, 0,
            circle.move_to, input_plane.coords_to_point(*self.target_coords),
            circle_image_update_anim,
            run_time = self.shrink_time,
            rate_func = bezier([0, 0, 1, 1])
        )

class AltForeverNarrowingLoop(ForeverNarrowingLoop):
    CONFIG = {
        "target_coords" : (-2, -1),
        "input_plane_corner" : DOWN+LEFT,
        "shrink_time" : 3,
    }

class TinyLoop(ForeverNarrowingLoop):
    CONFIG = {
        "circle_start_radius" : 0.5,
        "start_around_target" : True,
        "shrink_time" : 1,
        "add_convenient_waits" : True,
    }

class TinyLoopAroundZero(TinyLoop):
    CONFIG = {
        "target_coords" : (1, 1),
    }

class TinyLoopAroundBlue(TinyLoop):
    CONFIG = {
        "target_coords" : (2.4, 0),
    }

class TinyLoopAroundYellow(TinyLoop):
    CONFIG = {
        "target_coords" : (0, -1.3),
    }

class TinyLoopAroundOrange(TinyLoop):
    CONFIG = {
        "target_coords" : (0, -0.5),
    }

class TinyLoopAroundRed(TinyLoop):
    CONFIG = {
        "target_coords" : (-1, 1),
    }

class ConfusedPiCreature(PiCreatureScene):
    def construct(self):
        morty = self.pi_creature
        morty.set_color(YELLOW_E)
        morty.flip()
        morty.center()

        self.play(morty.change, "awe", DOWN+3*RIGHT)
        self.wait(2)
        self.play(morty.change, "confused")
        self.wait(2)
        self.play(morty.change, "pondering")
        self.wait(2)

class FailureOfComposition(ColorMappedObjectsScene):
    CONFIG = {
        "func" : lambda p : (
            np.cos(TAU*p[1]/3.5), 
            np.sin(TAU*p[1]/3.5)
        )
    }
    def construct(self):
        ColorMappedObjectsScene.construct(self)

        big_square = Square(side_length = 4)
        big_square.move_to(ORIGIN, RIGHT)
        small_squares = VGroup(*[
            Square(side_length = 2) for x in range(2)
        ])
        small_squares.match_width(big_square, stretch = True)
        small_squares.arrange(DOWN, buff = 0)
        small_squares.move_to(big_square)
        small_squares.space_out_submobjects(1.1)
        all_squares = VGroup(big_square, *small_squares)
        all_squares.set_stroke(width = 6)

        for square in all_squares:
            square.set_color(WHITE)
            square.color_using_background_image(self.background_image_file)

        question = TextMobject("Does my border go through every color?")
        question.to_edge(UP)
        no_answers = VGroup()
        yes_answers = VGroup()
        for square in all_squares:
            if square is big_square:
                square.answer = TextMobject("Yes")
                square.answer.set_color(GREEN)
                yes_answers.add(square.answer)
            else:
                square.answer = TextMobject("No")
                square.answer.set_color(RED)
                no_answers.add(square.answer)
            square.answer.move_to(square)

        no_answers_in_equation = no_answers.copy()
        yes_answers_in_equation = yes_answers.copy()
        plus, equals = plus_equals = TexMobject("+=")
        equation = VGroup(
            no_answers_in_equation[0], plus,
            no_answers_in_equation[1], equals,
            yes_answers_in_equation
        )
        equation.arrange(RIGHT, buff = SMALL_BUFF)
        equation.next_to(big_square, RIGHT, MED_LARGE_BUFF)
        q_marks = TexMobject("???")
        q_marks.next_to(equals, UP)


        self.add(question)
        self.play(LaggedStartMap(ShowCreation, small_squares, lag_ratio = 0.8))
        self.play(LaggedStartMap(Write, no_answers))
        self.wait()
        self.play(
            small_squares.arrange, DOWN, {"buff" : 0},
            small_squares.move_to, big_square,
            no_answers.space_out_submobjects, 0.9,
        )
        self.add(big_square)
        no_answers_copy = no_answers.copy()
        small_squares.save_state()
        self.play(
            Transform(no_answers, no_answers_in_equation),
            Write(plus_equals),
            small_squares.set_stroke, {"width" : 0},
        )
        self.play(
            Write(yes_answers),
            Write(yes_answers_in_equation),
        )
        self.play(LaggedStartMap(FadeIn, q_marks, run_time = 1, lag_ratio = 0.8))
        self.wait(2)
        self.play(
            small_squares.restore,
            FadeOut(yes_answers),
            FadeIn(no_answers_copy),
        )
        self.wait()
        self.play(
            small_squares.set_stroke, {"width" : 0},
            FadeOut(no_answers_copy),
            FadeIn(yes_answers),
        )
        self.wait()

        # We can find a better notion of what we want

        cross = Cross(question)

        self.play(
            ShowCreation(cross, run_time = 2),
            FadeOut(equation),
            FadeOut(no_answers),
            FadeOut(q_marks),
            FadeOut(yes_answers),
        )

        x, plus, y = x_plus_y = TexMobject("x+y")
        x_plus_y.move_to(big_square)
        x_plus_y.save_state()
        x.move_to(no_answers_copy[0])
        y.move_to(no_answers_copy[1])
        plus.fade(1)

        for square, char in zip(small_squares, [x, y]):
            ghost = square.copy()
            ghost.set_stroke(width = 5)
            ghost.background_image_file = None
            self.play(
                small_squares.restore,
                ShowPassingFlash(ghost),
                Write(char)
            )
        self.wait()
        ghost = big_square.copy()
        ghost.background_image_file = None
        self.play(
            small_squares.set_stroke, {"width" : 0},
            x_plus_y.restore,
        )
        self.play(ShowPassingFlash(ghost))
        self.wait()

class PathContainingZero(InputOutputScene, PiCreatureScene):
    CONFIG = {
        "default_pi_creature_kwargs" : {
            "flip_at_start" : False,
            "height" : 1.5,
        },
        "default_pi_creature_start_corner" : DOWN+LEFT,
    }
    def construct(self):
        self.setup_planes()
        self.draw_path_hitting_zero()
        self.comment_on_zero()

    def setup_planes(self):
        colorings = VGroup(*self.get_colorings())
        self.input_coloring, self.output_coloring = colorings
        colorings.set_fill(opacity = 0.3)

        planes = VGroup(*self.get_planes())
        self.input_plane, self.output_plane = planes
        for plane in planes:
            # plane.white_parts.set_color(BLACK)
            plane.lines_to_fade.set_stroke(width = 0)

        v_line = Line(UP, DOWN).scale(FRAME_Y_RADIUS)
        v_line.set_stroke(WHITE, 5)

        self.add(colorings, planes)
        self.add(v_line)

    def draw_path_hitting_zero(self):
        morty = self.pi_creature

        path = self.path = VMobject(
            stroke_width = 5,
            stroke_color = WHITE,
            fill_opacity = 0,
        )
        path.match_background_image_file(self.input_coloring)
        path.set_points_smoothly(list(it.starmap(
            self.input_plane.coords_to_point, 
            [(1, 2.5), (2.5, 2.5), (2, 0.5), (1, 1), (0.5, 1), (0.5, 2), (1, 2.5)]
        )))

        out_path = self.out_path = path.copy()
        out_path.apply_function(self.point_function)
        out_path.match_background_image_file(self.output_coloring)
        out_path.make_smooth()

        self.play(
            Flash(
                VectorizedPoint(self.output_plane.coords_to_point(0, 0)),
                color = WHITE,
                flash_radius = 0.3,
                line_length = 0.2,
                num_lines = 13,
                rate_func = squish_rate_func(smooth, 0.5, 0.6),
            ),
            morty.change, "pondering",
            *[
                ShowCreation(mob, rate_func = bezier([0, 0, 1, 1]))
                for mob in (path, out_path)
            ],
            run_time = 5
        )

    def comment_on_zero(self):
        morty = self.pi_creature

        words = TextMobject(
            "Output is zero \\\\",
            "which has no direction"
        )
        origin = self.output_plane.coords_to_point(0, 0)
        words.to_edge(DOWN, buff = LARGE_BUFF)
        background_rect = BackgroundRectangle(
            words, buff = SMALL_BUFF,
            opacity = 1.0
        )
        background_rect.stretch_to_fit_width(0.1)

        arrow = Arrow(words.get_top(), origin)

        circles = VGroup()
        for point in self.input_plane.coords_to_point(1, 1), origin:
            circle = Circle(color = BLACK, radius = 0.5, stroke_width = 0)
            circle.move_to(point)
            circle.generate_target()
            circle.target.scale(0)
            circle.target.set_stroke(width = 4)
            circles.add(circle)
        in_circle, out_circle = circles

        new_words = TextMobject(
            "But we want $\\vec{\\textbf{x}}$ \\\\",
            "where $f(\\vec{\\textbf{x}}) = 0$",
        )
        new_words.move_to(words)

        self.play(
            FadeIn(background_rect),
            Write(words[0]),
            GrowArrow(arrow),
        )
        self.play(
            Write(words[1]),
            morty.change, "pleading",
            MoveToTarget(out_circle, run_time = 2)
        )
        self.wait()
        self.play(FadeOut(words))
        self.play(
            FadeIn(new_words),
            morty.change, "happy"
        )
        self.play(MoveToTarget(in_circle, run_time = 2))
        self.play(morty.change, "hooray")
        self.wait(3)

class TransitionFromPathsToBoundaries(ColorMappedObjectsScene):
    CONFIG = {
        "func" : plane_func_by_wind_spec(
            (-2, 0, 2), (2, 0, 1)
        ),
        "dot_fill_opacity" : 1,
        "dot_stroke_width" : 1,
        "include_walkers" : True,
        "include_question_mark" : True,
    }
    def construct(self):
        ColorMappedObjectsScene.construct(self)

        #Setup paths
        squares, joint_rect = self.get_squares_and_joint_rect()
        left_square, right_square = squares

        path1, path2 = paths = VGroup(*[
            Line(square.get_corner(UP+LEFT), square.get_corner(UP+RIGHT))
            for square in squares
        ])
        joint_path = Line(path1.get_start(), path2.get_end())

        for mob in it.chain(paths, [joint_path]):
            mob.set_stroke(WHITE, 4)
            mob.color_using_background_image(self.background_image_file)

        dot = self.get_dot_and_add_continual_animations()

        #Setup path braces
        for mob, tex in (path1, "x"), (path2, "y"), (joint_path, "x+y"):
            mob.brace = Brace(mob, DOWN)
            label = TextMobject("Winding =", "$%s$"%tex)
            label.next_to(mob.brace, DOWN)
            mob.brace.add(label)

        #Setup region labels

        sum_tex = "x+y"
        if self.include_question_mark:
            sum_tex += "\\, ?"
        for square, tex in (left_square, "x"), (right_square, "y"), (joint_rect, sum_tex):
            square.label = TextMobject("Winding = ", "$%s$"%tex)
            square.label.move_to(square)

        #Add paths
        self.position_dot(path1.get_start())
        for path in path1, path2:
            self.position_dot(path.get_start())
            self.play(
                MoveAlongPath(dot, path.copy()),
                ShowCreation(path),
                run_time = 2
            )
            self.play(GrowFromCenter(path.brace))
        self.wait()
        self.position_dot(joint_path.get_start())
        self.play(
            MoveAlongPath(dot, joint_path, run_time = 3),
            FadeOut(VGroup(path1.brace, path2.brace)),
            FadeIn(joint_path.brace),
        )
        self.wait()

        #Add regions
        self.play(
            FadeOut(paths),
            FadeOut(joint_path.brace), 
            dot.move_to, path1.get_start()
        )
        for square in squares:
            self.position_dot(square.points[0])
            kwargs = {
                "run_time" : 4,
                "rate_func" : bezier([0, 0, 1, 1]),
            }
            self.play(
                MoveAlongPath(dot, square.copy(), **kwargs),
                ShowCreation(square, **kwargs),
                Write(square.label, run_time = 2),
            )
            self.wait()
        self.play(
            dot.move_to, joint_rect.points[0],
            FadeOut(squares),
            FadeIn(joint_rect),
        )
        self.position_dot(joint_rect.points[0])
        self.play(
            Transform(left_square.label[0], joint_rect.label[0]),
            Transform(
                left_square.label[1], joint_rect.label[1][0],
                path_arc = TAU/6
            ),
            FadeIn(joint_rect.label[1][1]),
            FadeIn(joint_rect.label[1][3:]),
            FadeOut(right_square.label[0]),
            Transform(
                right_square.label[1], joint_rect.label[1][2],
                path_arc = TAU/6
            ),
            MoveAlongPath(
                dot, joint_rect,
                run_time = 6,
                rate_func = bezier([0, 0, 1, 1])
            )
        )
        self.wait()

    ###

    def get_squares_and_joint_rect(self):
        squares = VGroup(*[
            Square(side_length = 4).next_to(ORIGIN, vect, buff = 0)
            for vect in (LEFT, RIGHT)
        ])
        joint_rect = SurroundingRectangle(squares, buff = 0)
        for mob in it.chain(squares, [joint_rect]):
            mob.set_stroke(WHITE, 4)
            mob.color_using_background_image(self.background_image_file)
        return squares, joint_rect

    def get_dot_and_add_continual_animations(self):
        #Define important functions for updates
        get_output = lambda : self.func(tuple(dot.get_center()[:2]))
        get_output_color = lambda : rgba_to_color(point_to_rgba(get_output()))
        get_output_rev = lambda : -point_to_rev(get_output())
        self.get_output_rev = get_output_rev

        self.start_rev = 0
        self.curr_winding = 0
        def get_total_winding(dt = 0):
            rev = (get_output_rev() - self.start_rev)%1
            possible_windings = [
                np.floor(self.curr_winding)+k+rev
                for k in (-1, 0, 1)
            ]
            i = np.argmin([abs(pw - self.curr_winding) for pw in possible_windings])
            self.curr_winding = possible_windings[i]
            return self.curr_winding


        #Setup dot, arrow and label
        dot = self.dot = Dot(radius = 0.1)
        dot.set_stroke(WHITE, self.dot_stroke_width)
        update_dot_color = Mobject.add_updater(
            dot, lambda d : d.set_fill(
                get_output_color(),
                self.dot_fill_opacity
            )
        )

        label = DecimalNumber(0, num_decimal_places = 1)
        label_upadte = ContinualChangingDecimal(
            label, get_total_winding,
            position_update_func = lambda l : l.next_to(dot, UP+LEFT, SMALL_BUFF)
        )

        arrow_length = 0.75
        arrow = Vector(arrow_length*RIGHT)
        arrow.set_stroke(WHITE, self.dot_stroke_width)
        def arrow_update_func(arrow):
            arrow.set_fill(get_output_color(), 1)
            arrow.rotate(-TAU*get_output_rev() - arrow.get_angle())
            arrow.scale(arrow_length/arrow.get_length())
            arrow.shift(dot.get_center() - arrow.get_start())
            return arrow
        update_arrow = Mobject.add_updater(arrow, arrow_update_func)

        if self.include_walkers:
            self.add(update_arrow, update_dot_color, label_upadte)
        return dot

    def position_dot(self, point):
        self.dot.move_to(point)
        self.start_rev = self.get_output_rev()
        self.curr_winding = 0

class TransitionFromPathsToBoundariesArrowless(TransitionFromPathsToBoundaries):
    CONFIG = {
        "func" : plane_func_by_wind_spec(
            (-2, 0, 2), (2, 0, 1)
        ),
        "dot_fill_opacity" : 0,
        "dot_stroke_width" : 0,
        "include_walkers" : False,
        "include_question_mark" : False,
    }

class BreakDownLoopWithNonzeroWinding(TransitionFromPathsToBoundaries):
    def construct(self):
        TransitionFromPathsToBoundaries.construct(self)
        zero_point = 2*LEFT

        squares, joint_rect = self.get_squares_and_joint_rect()
        left_square, right_square = squares
        VGroup(squares, joint_rect).shift(MED_LARGE_BUFF*DOWN)

        dot = self.get_dot_and_add_continual_animations()

        for rect, tex in (left_square, "x"), (right_square, "y"), (joint_rect, "3"):
            rect.label = TextMobject("Winding = ", "$%s$"%tex)
            rect.label.move_to(rect)
        sum_label = TexMobject("x", "+", "y", "=", "3")
        x, plus, y, equals, three = sum_label
        sum_label.next_to(joint_rect, UP)

        both_cannot_be_zero = TextMobject("These cannot both be 0")
        both_cannot_be_zero.move_to(plus)
        both_cannot_be_zero.to_edge(UP)
        arrows = VGroup(*[
            Arrow(both_cannot_be_zero.get_bottom(), var.get_top(), buff = SMALL_BUFF)
            for var in (x, y)
        ])

        self.position_dot(joint_rect.points[0])
        self.add(joint_rect)
        self.play(
            MoveAlongPath(dot, joint_rect, rate_func = bezier([0, 0, 1, 1])),
            Write(joint_rect.label, rate_func = squish_rate_func(smooth, 0.7, 1)),
            run_time = 4
        )
        self.wait()
        self.play(
            ReplacementTransform(joint_rect.label, left_square.label),
            ReplacementTransform(joint_rect.label.copy(), right_square.label),
            ReplacementTransform(joint_rect.label[1].copy(), three),
            FadeIn(left_square),
            FadeIn(right_square),
        )
        self.play(
            ReplacementTransform(left_square.label[1].copy(), x),
            ReplacementTransform(right_square.label[1].copy(), y),
            FadeIn(plus),
            FadeIn(equals),
        )
        self.play(
            FadeIn(both_cannot_be_zero),
            *list(map(GrowArrow, arrows))
        )
        self.wait()

class BackToEquationSolving(AltTeacherStudentsScene):
    def construct(self):
        self.teacher_says(
            "Back to solving \\\\ equations"
        )
        self.change_all_student_modes("hooray")
        self.play(*[
            ApplyMethod(pi.look_at, self.screen)
            for pi in self.pi_creatures
        ])
        self.wait(3)

class MonomialTerm(PathContainingZero):
    CONFIG = {
        "non_renormalized_func" : plane_func_from_complex_func(lambda z : z**5),
        "full_func_label" : "f(x) = x^5",
        "func_label" : "x^5",
        "loop_radius" : 1.1,
        "label_buff" : 0.3,
        "label_move_to_corner" : ORIGIN,
        "should_end_with_rescaling" : True,
    }
    def construct(self):
        self.setup_planes()
        self.relabel_planes()
        self.add_function_label()
        self.show_winding()
        if self.should_end_with_rescaling:
            self.rescale_output_plane()

    def relabel_planes(self):
        for plane in self.input_plane, self.output_plane:
            for mob in plane:
                if isinstance(mob, TexMobject):
                    plane.remove(mob)

            if hasattr(plane, "numbers_to_show"):
                _range = plane.numbers_to_show
            else:
                _range = list(range(-2, 3))
            for x in _range:
                if x == 0:
                    continue
                label = TexMobject(str(x))
                label.scale(0.5)
                point = plane.coords_to_point(x, 0)
                label.next_to(point, DOWN, MED_SMALL_BUFF)
                plane.add(label)
                self.add_foreground_mobject(label)
                tick = Line(SMALL_BUFF*DOWN, SMALL_BUFF*UP)
                tick.move_to(point)
                plane.add(tick)
            for y in _range:
                if y == 0:
                    continue
                label = TexMobject("%di"%y)
                label.scale(0.5)
                point = plane.coords_to_point(0, y)
                label.next_to(point, LEFT, MED_SMALL_BUFF)
                plane.add(label)
                self.add_foreground_mobject(label)
                tick = Line(SMALL_BUFF*LEFT, SMALL_BUFF*RIGHT)
                tick.move_to(point)
                plane.add(tick)
        self.add(self.input_plane, self.output_plane)

    def add_function_label(self):
        label = TexMobject(self.full_func_label)
        label.add_background_rectangle(opacity = 1, buff = SMALL_BUFF)
        arrow = Arrow(
            2*LEFT, 2*RIGHT, path_arc = -TAU/3,
        )
        arrow.pointwise_become_partial(arrow, 0, 0.95)
        label.next_to(arrow, UP)
        VGroup(arrow, label).to_edge(UP)
        self.add(label, arrow)

    def show_winding(self):
        loop = Arc(color = WHITE, angle = 1.02*TAU, num_anchors = 42)
        loop.scale(self.loop_radius)
        loop.match_background_image_file(self.input_coloring)
        loop.move_to(self.input_plane.coords_to_point(0, 0))

        out_loop = loop.copy()
        out_loop.apply_function(self.point_function)
        out_loop.match_background_image_file(self.output_coloring)

        get_in_point = lambda : loop.points[-1]
        get_out_point = lambda : out_loop.points[-1]
        in_origin = self.input_plane.coords_to_point(0, 0)
        out_origin = self.output_plane.coords_to_point(0, 0)

        dot = Dot()
        update_dot = UpdateFromFunc(dot, lambda d : d.move_to(get_in_point()))

        out_dot = Dot()
        update_out_dot = UpdateFromFunc(out_dot, lambda d : d.move_to(get_out_point()))

        buff = self.label_buff
        def generate_label_update(label, point_func, origin):
            return UpdateFromFunc(
                label, lambda m : m.move_to(
                    (1+buff)*point_func() - buff*origin,
                    self.label_move_to_corner
                )
            )
        x = TexMobject("x")
        fx = TexMobject(self.func_label)
        update_x = generate_label_update(x, get_in_point, in_origin)
        update_fx = generate_label_update(fx, get_out_point, out_origin)

        morty = self.pi_creature

        kwargs = {
            "run_time" : 15,
            "rate_func" : None,
        }
        self.play(
            ShowCreation(loop, **kwargs),
            ShowCreation(out_loop, **kwargs),
            update_dot,
            update_out_dot,
            update_x,
            update_fx,
            ApplyMethod(morty.change, "pondering", out_dot),
        )
        self.play(
            FadeOut(VGroup(dot, out_dot, x, fx))
        )
        self.loop = loop
        self.out_loop = out_loop

    def rescale_output_plane(self):
        output_stuff = VGroup(self.output_plane, self.output_coloring)
        self.play(*list(map(FadeOut, [self.loop, self.out_loop])))
        self.play(
            output_stuff.scale, 3.0/50, run_time = 2
        )
        self.wait()

    ###

    def func(self, coords):
        return self.non_renormalized_func(coords)

class PolynomialTerms(MonomialTerm):
    CONFIG = {
        "non_renormalized_func" : plane_func_from_complex_func(lambda z : z**5 - z - 1),
        "full_func_label" : "f(x) = x^5 - x - 1",
        "func_label" : "x^5 + \\cdots",
        "loop_radius" : 2.0,
        "label_buff" : 0.15,
        "label_move_to_corner" : DOWN+LEFT,
        "should_end_with_rescaling" : False,
    }
    def construct(self):
        self.pi_creature.change("pondering", VectorizedPoint(ORIGIN))
        MonomialTerm.construct(self)
        self.cinch_loop()
        # self.sweep_through_loop_interior()

    def relabel_planes(self):
        self.output_plane.x_radius = 50
        self.output_plane.y_radius = 50
        self.output_plane.numbers_to_show = list(range(-45, 50, 15))
        MonomialTerm.relabel_planes(self)

    def sweep_through_loop_interior(self):
        loop = self.loop
        morty = self.pi_creature

        line, line_target = [
            Line(
                loop.get_left(), loop.get_right(),
                path_arc = u*TAU/2,
                n_arc_anchors = 40,
                background_image_file = self.input_coloring.background_image_file ,
                stroke_width = 4,
            )
            for u in (-1, 1)
        ]
        out_line = line.copy()
        update_out_line = UpdateFromFunc(
            out_line, 
            lambda m : m.set_points(line.points).apply_function(self.point_function),
        )

        self.play(
            Transform(
                line, line_target,
                run_time = 10,
                rate_func = there_and_back
            ),
            update_out_line,
            morty.change, "hooray"
        )
        self.wait()

    def cinch_loop(self):
        loop = self.loop
        out_loop = self.out_loop
        morty = self.pi_creature

        update_out_loop = UpdateFromFunc(
            out_loop,
            lambda m : m.set_points(loop.points).apply_function(self.point_function)
        )

        self.add(
            loop.copy().set_stroke(width = 1),
            out_loop.copy().set_stroke(width = 1),
        )
        self.play(
            ApplyMethod(
                loop.scale, 0, {"about_point" : self.input_plane.coords_to_point(0.2, 1)},
                run_time = 12,
                rate_func = bezier([0, 0, 1, 1])
            ),
            update_out_loop,
            morty.change, "hooray"
        )
        self.wait()

class SearchSpacePerimeterVsArea(EquationSolver2d):
    CONFIG = {
        "func" : example_plane_func,
        "num_iterations" : 15,
        "display_in_parallel" : False,
        "use_fancy_lines" : True,
    }
    def construct(self):
        self.force_skipping()
        EquationSolver2d.construct(self)
        self.revert_to_original_skipping_status()

        all_parts = VGroup(*self.get_mobjects())
        path_parts = VGroup()
        non_path_parts = VGroup()
        for part in all_parts:
            if part.get_background_image_file() is not None:
                path_parts.add(part)
            else:
                non_path_parts.add(part)
        path_parts.save_state()
        path_parts.generate_target()
        for path_target in path_parts.target:
            if isinstance(path_target, Line):
                path_target.rotate(-path_target.get_angle())
        path_parts.target.arrange(DOWN, buff = MED_SMALL_BUFF)
        alt_path_parts = path_parts.copy()
        size = lambda m : m.get_height() + m.get_width()
        alt_path_parts.submobjects.sort(
            key=lambda m1: -size(m1)
        )

        full_rect = SurroundingRectangle(
            path_parts,
            stroke_width = 0,
            fill_color = WHITE,
            fill_opacity = 1,
            background_image_file = path_parts[0].background_image_file
        )
        full_rect.save_state()
        full_rect.stretch(0, 1, about_edge = UP)

        self.play(
            FadeOut(non_path_parts),
            path_parts.set_stroke, {"width" : 1},
        )
        self.remove(all_parts)
        for x in range(2):
            alt_path_parts.save_state()
            self.play(LaggedStartMap(
                FadeIn, alt_path_parts,
                rate_func = there_and_back,
                lag_ratio = 0.3,
                run_time = 3,
                remover = True
            ))
            alt_path_parts.restore()
        self.play(
            full_rect.restore,
            run_time = 2,
        )
        self.wait()
        self.play(FadeOut(full_rect))
        self.wait()

class ShowPolynomialFinalState(SolveX5MinusXMinus1):
    CONFIG = {
        "num_iterations" : 15,
    }
    def construct(self):
        self.force_skipping()
        SolveX5MinusXMinus1.construct(self)
        self.revert_to_original_skipping_status()

class PiCreatureInAwe(Scene):
    def construct(self):
        randy = Randolph()


        self.play(randy.change, "awe")
        self.play(Blink(randy))
        self.play(randy.look, UP, run_time = 2)
        self.play(
            randy.look, RIGHT, 
            run_time = 4, 
            rate_func = there_and_back,
            path_arc = -TAU/4
        )
        self.wait()

class ShowComplexFunction(Scene):
    def construct(self):
        plane = ComplexPlane()
        plane.add_coordinates()
        four_i = plane.coordinate_labels[-1]
        plane.coordinate_labels.remove(four_i)
        plane.remove(four_i)

        title = TextMobject("Complex Plane")
        title.to_edge(UP, buff = MED_SMALL_BUFF)
        rect = BackgroundRectangle(title, fill_opacity = 1, buff = MED_SMALL_BUFF)

        x = complex(1, 0.4)
        f = lambda x : x**5 - x - 1

        x_point = plane.number_to_point(x)
        fx_point = plane.number_to_point(f(x))

        x_dot = Dot(x_point)
        fx_dot = Dot(fx_point, color = YELLOW)
        arrow = Arrow(
            x_point, fx_point,
            path_arc = TAU/3,
            color = YELLOW
        )
        arrow.pointwise_become_partial(arrow, 0, 0.95)

        x_label = TexMobject("x = %d+%.1fi"%(x.real, x.imag))
        x_label.next_to(x_dot, RIGHT)
        x_label.add_background_rectangle()

        fx_label = TexMobject("f(x) = x^5 - x - 1")
        fx_label.next_to(fx_dot, DOWN, SMALL_BUFF)
        fx_label.set_color(YELLOW)
        fx_label.add_background_rectangle()
        fx_label.generate_target()
        fx_label.target.move_to(title)
        fx_label.target[1].set_color(WHITE)

        self.play(
            Write(plane),
            FadeIn(rect),
            LaggedStartMap(FadeIn, title)
        )
        self.play(*list(map(FadeIn, [x_dot, x_label])))
        self.wait()
        self.play(
            ReplacementTransform(x_dot.copy(), fx_dot, path_arc = arrow.path_arc),
            ShowCreation(arrow, rate_func = squish_rate_func(smooth, 0.2, 1))
        )
        self.play(FadeIn(fx_label))
        self.wait(2)
        self.play(
            MoveToTarget(fx_label),
            *list(map(FadeOut, [title, x_dot, x_label, arrow, fx_dot]))
        )
        self.play(FadeOut(plane.coordinate_labels))
        self.wait()

class WindingNumbersInInputOutputContext(PathContainingZero):
    CONFIG = {
        "in_loop_center_coords" : (-2, -1),
        "run_time" : 10,
    }
    def construct(self):
        self.remove(self.pi_creature)
        self.setup_planes()

        in_loop = Circle()
        in_loop.flip(RIGHT)
        # in_loop = Square(side_length = 2)
        in_loop.insert_n_curves(100)
        in_loop.move_to(self.input_plane.coords_to_point(
            *self.in_loop_center_coords
        ))
        in_loop.match_background_image_file(self.input_coloring)

        out_loop = in_loop.copy()
        out_loop.match_background_image_file(self.output_coloring)
        update_out_loop = Mobject.add_updater(
            out_loop,
            lambda m : m.set_points(in_loop.points).apply_function(self.point_function)
        )
        # self.add(update_out_loop)

        in_dot = Dot(radius = 0.04)
        update_in_dot = Mobject.add_updater(
            in_dot, lambda d : d.move_to(in_loop.point_from_proportion(1))
        )
        self.add(update_in_dot)

        out_arrow = Arrow(LEFT, RIGHT)
        update_out_arrow = Mobject.add_updater(
            out_arrow, 
            lambda a : a.put_start_and_end_on(
                self.output_plane.coords_to_point(0, 0),
                out_loop.point_from_proportion(1)
            )
        )
        update_out_arrow_color = Mobject.add_updater(
            out_arrow,
            lambda a : a.set_color(rev_to_color(a.get_angle()/TAU))
        )
        self.add(update_out_arrow, update_out_arrow_color)

        words = TextMobject(
            "How many times does \\\\ the output wind around?"
        )
        label = self.output_plane.label
        words.move_to(label, UP)
        self.output_plane.remove(label)
        self.add(words)

        decimal = DecimalNumber(0)
        decimal.next_to(self.output_plane.get_corner(UP+RIGHT), DOWN+LEFT)


        self.play(
            ShowCreation(in_loop),
            ShowCreation(out_loop),
            ChangeDecimalToValue(decimal, 2),
            Animation(in_dot),
            run_time = self.run_time,
            rate_func = bezier([0, 0, 1, 1])
        )

class SolveX5SkipToEnd(SolveX5MinusXMinus1):
    CONFIG = {
        "num_iterations" : 4,
    }
    def construct(self):
        self.force_skipping()
        SolveX5MinusXMinus1.construct(self)
        self.revert_to_original_skipping_status()

        mobjects = VGroup(*self.get_mobjects())
        lines = VGroup()
        rects = VGroup()
        for mob in mobjects:
            if mob.background_image_file is not None:
                mob.set_stroke(width = 2)
                lines.add(mob)
            elif isinstance(mob, Polygon):
                rects.add(mob)
            else:
                self.remove(mob)

        self.clear()
        self.add(lines, rects)

class ZeroFoundOnBoundary(Scene):
    def construct(self):
        arrow = Vector(DOWN+LEFT, color = WHITE)
        words = TextMobject("Found zero on boundary!")
        words.next_to(arrow.get_start(), UP)
        words.shift(1.5*RIGHT)

        point = VectorizedPoint()
        point.next_to(arrow, DOWN+LEFT)

        self.play(Flash(point))
        self.play(
            GrowArrow(arrow),
            Write(words),
        )
        self.wait()

class AllOfTheVideos(Scene):
    CONFIG = {
        "camera_config" : {
            "background_opacity" : 1,
        }
    }
    def construct(self):
        thumbnail_dir = os.path.join(MEDIA_DIR, "3b1b_videos/Winding/OldThumbnails")
        n = 4
        images = Group(*[
            ImageMobject(os.path.join(thumbnail_dir, file))
            for file in os.listdir(thumbnail_dir)[:n**2]
        ])
        for image in images:
            rect = SurroundingRectangle(image, buff = 0)
            rect.set_stroke(WHITE, 1)
            image.add(rect)
        images.arrange_in_grid(n, n, buff = 0)
        images.set_height(FRAME_HEIGHT)
        random.shuffle(images.submobjects)

        self.play(LaggedStartMap(FadeIn, images, run_time = 4))
        self.wait()

class EndingCredits(Scene):
    def construct(self):
        text = TextMobject(
            "Written and animated by: \\\\",
            "Sridhar Ramesh \\\\",
            "Grant Sanderson"
        )
        text[0].shift(MED_SMALL_BUFF*UP)
        text.to_edge(UP)

        pi = PiCreature(color = YELLOW_E, height = 2)
        pi.to_edge(DOWN)
        pi.change_mode("happy")
        self.add(pi)

        self.play(LaggedStartMap(FadeIn, text), pi.look_at, text)
        self.play(pi.change, "wave_1", text)
        self.play(Blink(pi))
        self.play(pi.change, "happy")
        self.wait()

class MentionQAndA(Scene):
    def construct(self):
        title = TextMobject("Q\\&A with ", "Ben", "and", "Sridhar\\\\", "at", "Patreon")
        title.set_color_by_tex_to_color_map({
            "Ben" : MAROON,
            "Sridhar" : YELLOW,
        })
        patreon_logo = VGroup(*PatreonLogo().family_members_with_points())
        patreon_logo.sort()
        patreon_logo.replace(title.get_parts_by_tex("Patreon"))
        patreon_logo.scale(1.3, about_edge = LEFT)
        patreon_logo.shift(0.5*SMALL_BUFF*DOWN)
        title.submobjects[-1] = patreon_logo

        title.to_edge(UP)
        self.add(title)

        questions = VGroup(*list(map(TextMobject, [
            "If you think of the current videos as short stories, \\\\ what is the novel that you want to write?",
            "How did you get into mathematics?",
            "What motivated you to join 3b1b?",
            "$\\vdots$",
        ])))
        questions.arrange(DOWN, buff = 0.75)
        questions.next_to(title, DOWN, LARGE_BUFF)

        self.play(LaggedStartMap(FadeIn, questions, run_time = 3))
        self.wait(2)
        self.play(FadeOut(questions))
        self.wait()

class TickingClock(Scene):
    CONFIG = {
        "run_time" : 90,
    }
    def construct(self):
        clock = Clock()
        clock.set_height(FRAME_HEIGHT - 1)
        clock.to_edge(LEFT)
        lines = [clock.hour_hand, clock.minute_hand]
        def update_line(line):
            rev = line.get_angle()/TAU
            line.set_color(rev_to_color(rev))

        for line in lines:
            self.add(Mobject.add_updater(line, update_line))

        run_time = self.run_time
        self.play(ClockPassesTime(
            clock, 
            run_time = run_time,
            hours_passed = 0.1*run_time
        ))

class InfiniteListOfTopics(Scene):
    def construct(self):
        rect = Rectangle(width = 5, height = 7)
        rect.to_edge(RIGHT)
        title = TextMobject("Infinite list \\\\ of topics")
        title.next_to(rect.get_top(), DOWN)
        lines = VGroup(*[
            TextMobject(words).scale(0.5)
            for words in [
                "Winding number",
                "Laplace transform",
                "Wallis product",
                "Quantum information",
                "Elliptic curve cryptography",
                "Strange attractors",
                "Convolutional neural networks",
                "Fixed points",
            ]
        ] + [TexMobject("\\vdots")])
        lines.arrange(DOWN, buff = MED_SMALL_BUFF, aligned_edge = LEFT)
        lines.next_to(title, DOWN, MED_LARGE_BUFF)
        lines[-1].next_to(lines[-2], DOWN)

        self.add(rect, title)
        self.play(LaggedStartMap(FadeIn, lines, run_time = 5))
        self.wait()

class ManyIterations(Scene):
    def construct(self):
        words = VGroup(*[
            TextMobject(word, alignment = "")
            for word in [
                "Winding numbers, v1",
                "Winding numbers, v2 \\\\ (center on domain coloring)",
                "Winding numbers, v3 \\\\ (clarify visuals of 2d functions)",
                "Winding numbers, v4 \\\\ (postpone topology examples for part 2)",
                "Winding numbers, v5 \\\\ (start down wrong path)",
            ]
        ])
        words.arrange(DOWN, buff = MED_LARGE_BUFF, aligned_edge = LEFT)
        words.scale(0.75)
        words.to_edge(RIGHT)

        self.add(words[0])
        for last_word, word in zip(words, words[1:]):
            cross = Cross(last_word)
            self.play(ShowCreation(cross))
            self.play(FadeIn(word))
        self.wait()

class MentionFree(PiCreatureScene):
    CONFIG = {
        "default_pi_creature_kwargs" : {
            "flip_at_start" : False,
        },
        "default_pi_creature_start_corner" : DOWN,
    }
    def construct(self):
        morty = self.pi_creature
        morty.shift(RIGHT)

        items = VGroup(
            TextMobject("Movie:", "$>\\$10.00$"),
            TextMobject("College course:", "$>\\$1{,}000.00$"),
            TextMobject("YouTube video:", "$=\\$0.00$"),
        )
        # items.arrange(DOWN, buff = MED_LARGE_BUFF)
        items.next_to(morty, UP, LARGE_BUFF)
        right_x = morty.get_right()[0]
        for item in items:
            item[1].set_color(GREEN)
            item.shift((right_x - item[0].get_right()[0])*RIGHT)

        self.play(
            morty.change, "raise_right_hand",
            FadeInFromDown(items[0])
        )
        self.wait()
        self.play(
            FadeInFromDown(items[1]),
            items[0].shift, UP,
        )
        self.wait()
        self.play(
            items[:2].shift, UP,
            FadeInFromDown(items[2]),
            morty.change, "surprised"
        )
        self.wait(4)
        self.play(
            morty.change, "raise_left_hand", VectorizedPoint(3*LEFT)
        )
        self.wait(4)
        self.play(morty.change, "gracious", OUT)
        self.wait(4)


class EndScreen(PatreonEndScreen, PiCreatureScene):
    CONFIG = {
        "run_time" : 0,
    }
    def construct(self):
        self.remove(self.pi_creature)
        PatreonEndScreen.construct(self)
        randy, morty = self.pi_creatures
        randy.change("plain")
        morty.change("plain")

        for mode in "thinking", "confused", "pondering", "hooray":
            self.play(randy.change, mode)
            self.wait()
            self.play(morty.change, mode)
            self.wait(2)

class Thumbnail(SearchSpacePerimeterVsArea):
    CONFIG = {
        "num_iterations" : 18,
        "func" : plane_func_by_wind_spec(
            (-3, -1.3, 2), (0.1, 0.2, 1), (2.8, -2, 1)
        ),
    }
    def construct(self):
        self.force_skipping()
        EquationSolver2d.construct(self)
        self.revert_to_original_skipping_status()

        mobjects = VGroup(*self.get_mobjects())
        lines = VGroup()
        rects = VGroup()
        get_length = lambda mob : max(mob.get_width(), mob.get_height())
        for mob in mobjects:
            if mob.background_image_file is not None:
                mob.set_stroke(width = 4*np.sqrt(get_length(mob)))
                lines.add(mob)
            elif isinstance(mob, Polygon):
                rects.add(mob)
            else:
                self.remove(mob)

        self.clear()
        self.add(lines)

