from big_ol_pile_of_manim_imports import *


class ClosedLoopScene(Scene):
    CONFIG = {
        "loop_anchor_points" : [
            3*RIGHT,
            2*RIGHT+UP,
            3*RIGHT + 3*UP,
            UP,
            2*UP+LEFT,
            2*LEFT + 2*UP,
            3*LEFT,
            2*LEFT+DOWN,
            3*LEFT+2*DOWN,
            2*DOWN+RIGHT,
            LEFT+DOWN,
        ],
        "square_vertices" : [
            2*RIGHT+UP,
            2*UP+LEFT,
            2*LEFT+DOWN,
            2*DOWN+RIGHT
        ],
        "rect_vertices" : [
            0*RIGHT + 1*UP,
            -1*RIGHT +  2*UP,
            -3*RIGHT +  0*UP,
            -2*RIGHT + -1*UP,
        ],
        "dot_color" : YELLOW,
        "connecting_lines_color" : BLUE,
        "pair_colors" : [MAROON_B, PURPLE_B],
    }
    def setup(self):
        self.dots = VGroup()
        self.connecting_lines = VGroup()
        self.add_loop()

    def add_loop(self):
        self.loop = self.get_default_loop()
        self.add(self.loop)

    def get_default_loop(self):
        loop = VMobject()
        loop.set_points_smoothly(
            self.loop_anchor_points + [self.loop_anchor_points[0]]
        )
        return loop

    def get_square(self):
        return Polygon(*self.square_vertices)

    def get_rect_vertex_dots(self, square = False):
        if square:
            vertices = self.square_vertices
        else:
            vertices = self.rect_vertices
        dots = VGroup(*[Dot(v) for v in vertices])
        dots.set_color(self.dot_color)
        return dots

    def get_rect_alphas(self, square = False):
        #Inefficient and silly, but whatever.
        dots = self.get_rect_vertex_dots(square = square)
        return self.get_dot_alphas(dots)

    def add_dot(self, dot):
        self.add_dots(dot)

    def add_dots(self, *dots):
        self.dots.add(*dots)
        self.add(self.dots)

    def add_rect_dots(self, square = False):
        self.add_dots(*self.get_rect_vertex_dots(square = square))

    def add_dots_at_alphas(self, *alphas):
        self.add_dots(*[
            Dot(
                self.loop.point_from_proportion(alpha), 
                color = self.dot_color
            )
            for alpha in alphas
        ])

    def add_connecting_lines(self, cyclic = False):
        if cyclic:
            pairs = adjacent_pairs(self.dots)
        else:
            n_pairs = len(list(self.dots))/2
            pairs = list(zip(self.dots[:n_pairs], self.dots[n_pairs:]))
        for d1, d2 in pairs:
            line = Line(d1.get_center(), d2.get_center())
            line.start_dot = d1 
            line.end_dot = d2
            line.update_anim = UpdateFromFunc(
                line,
                lambda l : l.put_start_and_end_on(
                    l.start_dot.get_center(),
                    l.end_dot.get_center()
                )
            )
            line.set_color(d1.get_color())
            self.connecting_lines.add(line)
        if cyclic:
            self.connecting_lines.set_color(self.connecting_lines_color)
            self.connecting_lines.set_stroke(width = 6)
        self.add(self.connecting_lines, self.dots)

    def get_line_anims(self):
        return [
            line.update_anim
            for line in self.connecting_lines
        ] + [Animation(self.dots)]

    def get_dot_alphas(self, dots = None, precision = 0.005):
        if dots == None:
            dots = self.dots
        alphas = []
        alpha_range = np.arange(0, 1, precision)
        loop_points = np.array(list(map(self.loop.point_from_proportion, alpha_range)))
        for dot in dots:
            vects = loop_points - dot.get_center()
            norms = np.apply_along_axis(get_norm, 1, vects)
            index = np.argmin(norms)
            alphas.append(alpha_range[index])
        return alphas

    def let_dots_wonder(self, run_time = 5, random_seed = None, added_anims = []):
        if random_seed is not None:
            np.random.seed(random_seed)
        start_alphas = self.get_dot_alphas()
        alpha_rates = 0.05 + 0.1*np.random.random(len(list(self.dots)))
        def generate_rate_func(start, rate):
            return lambda t : (start + t*rate*run_time)%1
        anims = [
            MoveAlongPath(
                dot,
                self.loop,
                rate_func = generate_rate_func(start, rate)
            )
            for dot, start, rate in zip(self.dots, start_alphas, alpha_rates)
        ]
        anims += self.get_line_anims()
        anims += added_anims
        self.play(*anims, run_time = run_time)

    def move_dots_to_alphas(self, alphas, run_time = 3):
        assert(len(alphas) == len(list(self.dots)))
        start_alphas = self.get_dot_alphas()
        def generate_rate_func(start_alpha, alpha):
            return lambda t : interpolate(start_alpha, alpha, smooth(t))
        anims = [
            MoveAlongPath(
                dot, self.loop,
                rate_func = generate_rate_func(sa, a),
                run_time = run_time,
            )
            for dot, sa, a in zip(self.dots, start_alphas, alphas)
        ]
        anims += self.get_line_anims()
        self.play(*anims)

    def transform_loop(self, target_loop, added_anims = [], **kwargs):
        alphas = self.get_dot_alphas()
        dot_anims = []
        for dot, alpha in zip(self.dots, alphas):
            dot.generate_target()
            dot.target.move_to(target_loop.point_from_proportion(alpha))
            dot_anims.append(MoveToTarget(dot))
        self.play(
            Transform(self.loop, target_loop),
            *dot_anims + self.get_line_anims() + added_anims,
            **kwargs
        )

    def set_color_dots_by_pair(self):
        n_pairs = len(list(self.dots))/2
        for d1, d2, c in zip(self.dots[:n_pairs], self.dots[n_pairs:], self.pair_colors):
            VGroup(d1, d2).set_color(c)

    def find_square(self):
        alpha_quads = list(it.combinations(
            np.arange(0, 1, 0.02) , 4
        ))
        quads = np.array([
            [
                self.loop.point_from_proportion(alpha)
                for alpha in quad
            ]
            for quad in alpha_quads
        ])
        scores = self.square_scores(quads)
        index = np.argmin(scores)
        return quads[index]

    def square_scores(self, all_quads):
        midpoint_diffs = np.apply_along_axis(
            get_norm, 1,
            0.5*(all_quads[:,0] + all_quads[:,2]) - 0.5*(all_quads[:,1] + all_quads[:,3])
        )
        vects1 = all_quads[:,0] - all_quads[:,2]
        vects2 = all_quads[:,1] - all_quads[:,3]
        distances1 = np.apply_along_axis(get_norm, 1, vects1)
        distances2 = np.apply_along_axis(get_norm, 1, vects2)
        distance_diffs = np.abs(distances1 - distances2)
        midpoint_diffs /= distances1
        distance_diffs /= distances2

        buffed_d1s = np.repeat(distances1, 3).reshape(vects1.shape)
        buffed_d2s = np.repeat(distances2, 3).reshape(vects2.shape)
        unit_v1s = vects1/buffed_d1s
        unit_v2s = vects2/buffed_d2s
        dots = np.abs(unit_v1s[:,0]*unit_v2s[:,0] + unit_v1s[:,1]*unit_v2s[:,1] + unit_v1s[:,2]*unit_v2s[:,2])

        return midpoint_diffs + distance_diffs + dots


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

class Introduction(TeacherStudentsScene):
    def construct(self):
        self.play(self.get_teacher().change_mode, "hooray")
        self.random_blink()
        self.teacher_says("")
        for pi in self.get_students():
            pi.generate_target()
            pi.target.change_mode("happy")            
            pi.target.look_at(self.get_teacher().bubble)
        self.play(*list(map(MoveToTarget, self.get_students())))
        self.random_blink(3)
        self.teacher_says(
            "Here's why \\\\ I'm excited...",
            target_mode = "hooray"
        )
        for pi in self.get_students():
            pi.target.look_at(self.get_teacher().eyes)
        self.play(*list(map(MoveToTarget, self.get_students())))
        self.wait()

class WhenIWasAKid(TeacherStudentsScene):
    def construct(self):
        children = self.get_children()
        speaker = self.get_speaker()

        self.prepare_everyone(children, speaker)
        self.state_excitement(children, speaker)
        self.students = children
        self.teacher = speaker
        self.run_class()
        self.grow_up()

    def state_excitement(self, children, speaker):
        self.teacher_says(
            """
            Here's why 
            I'm excited!
            """,
            target_mode = "hooray"
        )
        self.change_student_modes(*["happy"]*3)
        self.wait()

        speaker.look_at(children)
        me = children[-1]
        self.play(
            FadeOut(self.get_students()),
            FadeOut(self.get_teacher().bubble),
            FadeOut(self.get_teacher().bubble.content),
            Transform(self.get_teacher(), me)
        )
        self.remove(self.get_teacher())
        self.add(me)
        self.play(*list(map(FadeIn, children[:-1] + [speaker])))
        self.random_blink()

    def run_class(self):
        children = self.students
        speaker = self.teacher
        title = TextMobject("Topology")
        title.to_edge(UP)
        pi1, pi2, pi3, me = children

        self.random_blink()
        self.teacher_says(
            """
            Math! Excitement!
            You are the future!
            """,
            target_mode = "hooray"
        )
        self.play(
            pi1.look_at, pi2.eyes,
            pi1.change_mode, "erm",
            pi2.look_at, pi1.eyes,
            pi2.change_mode, "surprised",
        )
        self.play(
            pi3.look_at, me.eyes,
            pi3.change_mode, "sassy",
            me.look_at, pi3.eyes,
        )
        self.random_blink(2)

        self.play(
            self.teacher.change_mode, "speaking",
            FadeOut(self.teacher.bubble),
            FadeOut(self.teacher.bubble.content),
        )
        self.play(Write(title))
        self.random_blink()
        
        self.play(pi1.change_mode, "raise_right_hand")
        self.random_blink()
        self.play(
            pi2.change_mode, "confused",
            pi3.change_mode, "happy",
            pi2.look_at, pi3.eyes,
            pi3.look_at, pi2.eyes,
        )
        self.random_blink()
        self.play(me.change_mode, "pondering")
        self.wait()
        self.random_blink(2)
        self.play(pi1.change_mode, "raise_left_hand")
        self.wait()
        self.play(pi2.change_mode, "erm")
        self.random_blink()
        self.student_says(
            "How is this math?",
            student_index = -1,
            target_mode = "pleading",
            width = 5, 
            height = 3,
            direction = RIGHT
        )
        self.play(
            pi1.change_mode, "pondering",
            pi2.change_mode, "pondering",
            pi3.change_mode, "pondering",
        )
        self.play(speaker.change_mode, "pondering")
        self.random_blink()

    def grow_up(self):
        me = self.students[-1]
        self.students.remove(me)
        morty = Mortimer(mode = "pondering")
        morty.flip()
        morty.move_to(me, aligned_edge = DOWN)
        morty.to_edge(LEFT)
        morty.look(RIGHT)

        self.play(
            Transform(me, morty),
            *list(map(FadeOut, [
                self.students, self.teacher,
                me.bubble, me.bubble.content
            ]))
        )
        self.remove(me)
        self.add(morty)
        self.play(Blink(morty))
        self.wait()
        self.play(morty.change_mode, "hooray")
        self.wait()


    def prepare_everyone(self, children, speaker):
        self.everyone = list(children) + [speaker]
        for pi in self.everyone:
            pi.bubble = None

    def get_children(self):
        colors = [MAROON_E, YELLOW_D, PINK, GREY_BROWN]
        children = VGroup(*[
            BabyPiCreature(color = color)
            for color in colors
        ])
        children.arrange(RIGHT)
        children.to_edge(DOWN, buff = LARGE_BUFF)
        children.to_edge(LEFT)
        return children

    def get_speaker(self):
        speaker = Mathematician(mode = "happy")
        speaker.flip()
        speaker.to_edge(DOWN, buff = LARGE_BUFF)
        speaker.to_edge(RIGHT)
        return speaker

    def get_pi_creatures(self):
        if hasattr(self, "everyone"):
            return self.everyone
        else:
            return TeacherStudentsScene.get_pi_creatures(self)

class FormingTheMobiusStrip(Scene):
    def construct(self):
        pass

class DrawLineOnMobiusStrip(Scene):
    def construct(self):
        pass

class MugIntoTorus(Scene):
    def construct(self):
        pass

class DefineInscribedSquareProblem(ClosedLoopScene):
    def construct(self):
        self.draw_loop()
        self.cycle_through_shapes()
        self.ask_about_rectangles()

    def draw_loop(self):
        self.title = TextMobject("Inscribed", "square", "problem")
        self.title.to_edge(UP)

        #Draw loop
        self.remove(self.loop)
        self.play(Write(self.title))
        self.wait()
        self.play(ShowCreation(
            self.loop, 
            run_time = 5, 
            rate_func=linear
        ))
        self.wait()
        self.add_rect_dots(square = True)
        self.play(ShowCreation(self.dots, run_time = 2))
        self.wait()
        self.add_connecting_lines(cyclic = True)
        self.play(
            ShowCreation(
                self.connecting_lines,
                lag_ratio = 0,
                run_time = 2
            ),
            Animation(self.dots)
        )
        self.wait(2)

    def cycle_through_shapes(self):
        circle = Circle(radius = 2.5, color = WHITE)
        ellipse = circle.copy()
        ellipse.stretch(1.5, 0)
        ellipse.stretch(0.7, 1)
        ellipse.rotate(-np.pi/2)
        ellipse.set_height(4)
        pi_loop = TexMobject("\\pi")[0]
        pi_loop.set_fill(opacity = 0)
        pi_loop.set_stroke(
            color = WHITE,
            width = DEFAULT_STROKE_WIDTH
        )
        pi_loop.set_height(4)
        randy = Randolph()
        randy.look(DOWN)
        randy.set_width(pi_loop.get_width())
        randy.move_to(pi_loop, aligned_edge = DOWN)
        randy.body.set_fill(opacity = 0)
        randy.mouth.set_stroke(width = 0)

        self.transform_loop(circle)
        self.remove(self.loop)
        self.loop = circle
        self.add(self.loop, self.connecting_lines, self.dots)
        self.wait()
        odd_eigths = np.linspace(1./8, 7./8, 4)
        self.move_dots_to_alphas(odd_eigths)
        self.wait()
        for nudge in 0.1, -0.1, 0:
            self.move_dots_to_alphas(odd_eigths+nudge)
        self.wait()
        self.transform_loop(ellipse)
        self.wait()
        nudge = 0.055
        self.move_dots_to_alphas(
            odd_eigths + [nudge, -nudge, nudge, -nudge]
        )
        self.wait(2)
        self.transform_loop(pi_loop)
        self.let_dots_wonder()
        randy_anims = [
            FadeIn(randy),
            Animation(randy),            
            Blink(randy),
            Animation(randy),         
            Blink(randy),
            Animation(randy),
            Blink(randy, rate_func = smooth)
        ]
        for anim in randy_anims:
            self.let_dots_wonder(
                run_time = 1.5,
                random_seed = 0,
                added_anims = [anim]
            )
        self.remove(randy)
        self.transform_loop(self.get_default_loop())

    def ask_about_rectangles(self):
        morty = Mortimer()
        morty.next_to(ORIGIN, DOWN)
        morty.to_edge(RIGHT)

        new_title = TextMobject("Inscribed", "rectangle", "problem")
        new_title.set_color_by_tex("rectangle", YELLOW)
        new_title.to_edge(UP)
        rect_dots = self.get_rect_vertex_dots()
        rect_alphas = self.get_dot_alphas(rect_dots)

        self.play(FadeIn(morty))
        self.play(morty.change_mode, "speaking")
        self.play(Transform(self.title, new_title))
        self.move_dots_to_alphas(rect_alphas)
        self.wait()
        self.play(morty.change_mode, "hooray")
        self.play(Blink(morty))
        self.wait()
        self.play(FadeOut(self.connecting_lines))
        self.connecting_lines = VGroup()
        self.play(morty.change_mode, "plain")

        dot_pairs = [
            VGroup(self.dots[i], self.dots[j])
            for i, j in [(0, 2), (1, 3)]
        ]
        pair_colors = MAROON_B, PURPLE_B
        diag_lines = [
            Line(d1.get_center(), d2.get_center(), color = c)
            for (d1, d2), c in zip(dot_pairs, pair_colors)
        ]

        for pair, line in zip(dot_pairs, diag_lines):
            self.play(
                FadeIn(line),
                pair.set_color, line.get_color(),
            )

class RectangleProperties(Scene):
    def construct(self):
        rect = Rectangle(color = BLUE)
        vertex_dots = VGroup(*[
            Dot(anchor, color = YELLOW)
            for anchor in rect.get_anchors_and_handles()[0]
        ])
        dot_pairs = [
            VGroup(vertex_dots[i], vertex_dots[j])
            for i, j in [(0, 2), (1, 3)]
        ]
        colors = [MAROON_B, PURPLE_B]
        diag_lines = [
            Line(d1.get_center(), d2.get_center(), color = c)
            for (d1, d2), c in zip(dot_pairs, colors)
        ]
        braces = [Brace(rect).next_to(ORIGIN, DOWN) for x in range(2)]
        for brace, line in zip(braces, diag_lines):
            brace.stretch_to_fit_width(line.get_length())
            brace.rotate(line.get_angle())
        a, b, c, d = labels = VGroup(*[
            TexMobject(s).next_to(dot, dot.get_center(), buff = SMALL_BUFF)
            for s, dot in zip("abcd", vertex_dots)
        ])
        midpoint = Dot(ORIGIN, color = RED)


        self.play(ShowCreation(rect))
        self.wait()
        self.play(
            ShowCreation(vertex_dots),
            Write(labels)
        )
        self.wait()
        mob_lists = [
            (a, c, dot_pairs[0]),
            (b, d, dot_pairs[1]),
        ]
        for color, mob_list in zip(colors, mob_lists):
            self.play(*[
                ApplyMethod(mob.set_color, color)
                for mob in mob_list
            ])
            self.wait()
        for line, brace in zip(diag_lines, braces):
            self.play(
                ShowCreation(line),
                GrowFromCenter(brace)
            )
            self.wait()
            self.play(FadeOut(brace))
        self.play(FadeIn(midpoint))
        self.wait()

class PairOfPairBecomeRectangle(Scene):
    def construct(self):
        dots = VGroup(
            Dot(4*RIGHT+0.5*DOWN, color = MAROON_B),
            Dot(5*RIGHT+3*UP, color = MAROON_B),
            Dot(LEFT+0.1*DOWN, color = PURPLE_B),
            Dot(2*LEFT+UP, color = PURPLE_B)
        )
        labels = VGroup()
        for dot, char in zip(dots, "acbd"):
            label = TexMobject(char)
            y_coord = dot.get_center()[1]
            label.next_to(dot, np.sign(dot.get_center()[1])*UP)
            label.set_color(dot.get_color())
            labels.add(label)
        lines = [
            Line(
                dots[i].get_center(), 
                dots[j].get_center(), 
                color = dots[i].get_color()
            )
            for i, j in [(0, 1), (2, 3)]
        ]
        groups = [
            VGroup(dots[0], dots[1], labels[0], labels[1], lines[0]),
            VGroup(dots[2], dots[3], labels[2], labels[3], lines[1]),
        ]
        midpoint = Dot(LEFT, color = RED)

        words = VGroup(*list(map(TextMobject, [
            "Common midpoint",
            "Same distance apart",
            "$\\Downarrow$",
            "Rectangle",
        ])))
        words.arrange(DOWN)
        words.to_edge(RIGHT)
        words[-1].set_color(BLUE)

        self.play(
            ShowCreation(dots),
            Write(labels)
        )
        self.play(*list(map(ShowCreation, lines)))
        self.wait()
        self.play(*[
            ApplyMethod(
                group.shift, 
                -group[-1].get_center()+midpoint.get_center()
            )
            for group in groups
        ])
        self.play(
            ShowCreation(midpoint),
            Write(words[0])
        )
        factor = lines[0].get_length()/lines[1].get_length()        
        grower = groups[1].copy()
        new_line = grower[-1]
        new_line.scale_in_place(factor)
        grower[0].move_to(new_line.get_start())
        grower[2].next_to(grower[0], DOWN)
        grower[1].move_to(new_line.get_end())
        grower[3].next_to(grower[1], UP)

        self.play(Transform(groups[1], grower))
        self.play(Write(words[1]))
        self.wait()

        rectangle = Polygon(*[
            dots[i].get_center()
            for i in (0, 2, 1, 3)
        ])
        rectangle.set_color(BLUE)
        self.play(
            ShowCreation(rectangle),
            Animation(dots)
        )
        self.play(*list(map(Write, words[2:])))
        self.wait()

class SearchForRectangleOnLoop(ClosedLoopScene):
    def construct(self):
        self.add_dots_at_alphas(*np.linspace(0.2, 0.8, 4))
        self.set_color_dots_by_pair()
        rect_alphas = self.get_rect_alphas()

        self.play(ShowCreation(self.dots))
        self.add_connecting_lines()
        self.play(ShowCreation(self.connecting_lines))
        self.let_dots_wonder(2)
        self.move_dots_to_alphas(rect_alphas)

        midpoint = Dot(
            center_of_mass([d.get_center() for d in self.dots]),
            color = RED
        )
        self.play(ShowCreation(midpoint))
        self.wait()
        angles = [line.get_angle() for line in self.connecting_lines]
        angle_mean = np.mean(angles)
        self.play(
            *[
                ApplyMethod(line.rotate_in_place, angle_mean-angle)
                for line, angle in zip(self.connecting_lines, angles)
            ] + [Animation(midpoint)],
            rate_func = there_and_back
        )
        self.add(self.connecting_lines.copy(), midpoint)
        self.connecting_lines = VGroup()
        self.wait()
        self.add_connecting_lines(cyclic = True)
        self.play(
            ShowCreation(self.connecting_lines), 
            Animation(self.dots)
        )
        self.wait()

class DeclareFunction(ClosedLoopScene):
    def construct(self):
        self.add_dots_at_alphas(0.2, 0.8)
        self.set_color_dots_by_pair()        
        self.add_connecting_lines()
        VGroup(
            self.loop, self.dots, self.connecting_lines
        ).scale(0.7).to_edge(LEFT).shift(DOWN)
        arrow = Arrow(LEFT, RIGHT).next_to(self.loop)
        self.add(arrow)

        self.add_tex()
        self.let_dots_wonder(10)

    def add_tex(self):
        tex = TexMobject("f", "(A, B)", "=", "(x, y, z)")
        tex.to_edge(UP)
        tex.shift(LEFT)

        ab_brace = Brace(tex[1])
        xyz_brace = Brace(tex[-1], RIGHT)
        ab_brace.add(ab_brace.get_text("Pair of points on the loop"))
        xyz_brace.add(xyz_brace.get_text("Point in 3d space"))
        ab_brace.set_color_by_gradient(MAROON_B, PURPLE_B)
        xyz_brace.set_color(BLUE)

        self.add(tex)
        self.play(Write(ab_brace))
        self.wait()
        self.play(Write(xyz_brace))
        self.wait()

class DefinePairTo3dFunction(Scene):
    def construct(self):
        pass

class LabelMidpoint(Scene):
    def construct(self):
        words = TextMobject("Midpoint $M$")
        words.set_color(RED)
        words.scale(2)
        self.play(Write(words, run_time = 1))
        self.wait()

class LabelDistance(Scene):
    def construct(self):
        words = TextMobject("Distance $d$")
        words.set_color(MAROON_B)
        words.scale(2)
        self.play(Write(words, run_time = 1))
        self.wait()

class DrawingOneLineOfTheSurface(Scene):
    def construct(self):
        pass

class FunctionSurface(Scene):
    def construct(self):
        pass

class PointPairApprocahingEachother3D(Scene):
    def construct(self):
        pass

class InputPairToFunction(Scene):
    def construct(self):
        tex = TexMobject("f(X, X)", "=X")
        tex.set_color_by_tex("=X", BLUE)
        tex.scale(2)
        self.play(Write(tex[0]))
        self.wait(2)
        self.play(Write(tex[1]))
        self.wait(2)

class WigglePairUnderSurface(Scene):
    def construct(self):
        pass        

class WriteContinuous(Scene):
    def construct(self):
        self.play(Write(TextMobject("Continuous").scale(2)))
        self.wait(2)

class DistinctPairCollisionOnSurface(Scene):
    def construct(self):
        pass

class PairsOfPointsOnLoop(ClosedLoopScene):
    def construct(self):
        self.add_dots_at_alphas(0.2, 0.5)
        self.dots.set_color(MAROON_B)
        self.add_connecting_lines()
        self.let_dots_wonder(run_time = 10)

class PairOfRealsToPlane(Scene):
    def construct(self):
        r1, r2 = numbers = -3, 2
        colors = GREEN, RED
        dot1, dot2 = dots = VGroup(*[Dot(color = c) for c in colors])
        for dot, number in zip(dots, numbers):
            dot.move_to(number*RIGHT)
        pair_label = TexMobject("(", str(r1), ",", str(r2), ")")
        for number, color in zip(numbers, colors):
            pair_label.set_color_by_tex(str(number), color)
        pair_label.next_to(dots, UP, buff = 2)
        arrows = VGroup(*[
            Arrow(pair_label[i], dot, color = dot.get_color())
            for i, dot in zip([1, 3], dots)
        ])
        two_d_point = Dot(r1*RIGHT + r2*UP, color = YELLOW)
        pair_label.add_background_rectangle()

        x_axis = NumberLine(color = BLUE)
        y_axis = NumberLine(color = BLUE)
        plane = NumberPlane().fade()

        self.add(x_axis, y_axis, dots, pair_label)
        self.play(ShowCreation(arrows, run_time = 2))
        self.wait()
        self.play(
            pair_label.next_to, two_d_point, UP+LEFT, SMALL_BUFF,
            Rotate(y_axis, np.pi/2),
            Rotate(dot2, np.pi/2),
            FadeOut(arrows)
        )
        lines = VGroup(*[
            DashedLine(dot, two_d_point, color = dot.get_color())
            for dot in dots
        ])
        self.play(*list(map(ShowCreation, lines)))
        self.play(ShowCreation(two_d_point))
        everything = VGroup(*self.get_mobjects())
        self.play(
            FadeIn(plane), 
            Animation(everything),
            Animation(dot2)
        )
        self.wait()

class SeekSurfaceForPairs(ClosedLoopScene):
    def construct(self):
        self.loop.to_edge(LEFT)
        self.add_dots_at_alphas(0.2, 0.3)
        self.set_color_dots_by_pair()        
        self.add_connecting_lines()

        arrow = Arrow(LEFT, RIGHT).next_to(self.loop)
        words = TextMobject("Some 2d surface")
        words.next_to(arrow, RIGHT)

        anims = [
            ShowCreation(arrow),
            Write(words)
        ]
        for anim in anims:
            self.let_dots_wonder(
                random_seed = 1,
                added_anims = [anim],
                run_time = anim.run_time
            )
        self.let_dots_wonder(random_seed = 1, run_time = 10)

class AskAbouPairType(TeacherStudentsScene):
    def construct(self):
        self.student_says("""
            Do you mean ordered
            or unordered pairs?
        """)
        self.play(*[
            ApplyMethod(self.get_students()[i].change_mode, "confused")
            for i in (0, 2)
        ])
        self.random_blink(3)

class DefineOrderedPair(ClosedLoopScene):
    def construct(self):
        title = TextMobject("Ordered pairs")
        title.to_edge(UP)
        subtitle = TexMobject(
            "(", "a", ",", "b", ")", 
            "\\ne", 
            "(", "b", ",", "a", ")"
        )
        labels_start = VGroup(subtitle[1], subtitle[3])
        labels_end = VGroup(subtitle[9], subtitle[7])
        subtitle.next_to(title, DOWN)
        colors = GREEN, RED
        for char, color in zip("ab", colors):
            subtitle.set_color_by_tex(char, color)
        self.loop.next_to(subtitle, DOWN)
        self.add(title, subtitle)

        self.add_dots_at_alphas(0.5, 0.6)
        dots = self.dots
        for dot, color, char in zip(dots, colors, "ab"):
            dot.set_color(color)
            label = TexMobject(char)
            label.set_color(color)
            label.next_to(dot, RIGHT, buff = SMALL_BUFF)
            dot.label = label
        self.dots[1].label.shift(0.3*UP)
        first = TextMobject("First")
        first.next_to(self.dots[0], UP+2*LEFT, LARGE_BUFF)
        arrow = Arrow(first.get_bottom(), self.dots[0], color = GREEN)

        self.wait()
        self.play(*[
            Transform(label.copy(), dot.label)
            for label, dot in zip(labels_start, dots)
        ])
        self.remove(*self.get_mobjects_from_last_animation())
        self.add(*[d.label for d in dots])
        self.wait()
        self.play(
            Write(first),
            ShowCreation(arrow)
        )
        self.wait()

class DefineUnorderedPair(ClosedLoopScene):
    def construct(self):
        title = TextMobject("Unordered pairs")
        title.to_edge(UP)
        subtitle = TexMobject(
            "\\{a,b\\}",
            "=",
            "\\{b,a\\}",
        )
        subtitle.next_to(title, DOWN)
        for char in "ab":
            subtitle.set_color_by_tex(char, PURPLE_B)
        self.loop.next_to(subtitle, DOWN)
        self.add(title, subtitle)

        self.add_dots_at_alphas(0.5, 0.6)
        dots = self.dots
        dots.set_color(PURPLE_B)

        labels = VGroup(*[subtitle[i].copy() for i in (0, 2)])
        for label, vect in zip(labels, [LEFT, RIGHT]):
            label.next_to(dots, vect, LARGE_BUFF)
        arrows = [
            Arrow(*pair, color = PURPLE_B)
            for pair in it.product(labels, dots)
        ]
        arrow_pairs = [VGroup(*arrows[:2]), VGroup(*arrows[2:])]

        for label, arrow_pair in zip(labels, arrow_pairs):
            self.play(*list(map(FadeIn, [label, arrow_pair])))
            self.wait()
        for x in range(2):
            self.play(
                dots[0].move_to, dots[1],
                dots[1].move_to, dots[0],
                path_arc = np.pi/2
            )
            self.wait()

class BeginWithOrdered(TeacherStudentsScene):
    def construct(self):
        self.teacher_says("""
            One must know order
            before he can ignore it.
        """)
        self.random_blink(3)

class DeformToInterval(ClosedLoopScene):
    def construct(self):
        interval = UnitInterval(color = WHITE)
        interval.shift(2*DOWN)
        numbers = interval.get_number_mobjects(0, 1)
        line = Line(interval.get_left(), interval.get_right())
        line.insert_n_curves(self.loop.get_num_curves())
        line.make_smooth()

        self.loop.scale(0.7)
        self.loop.to_edge(UP)
        original_loop = self.loop.copy()
        cut_loop = self.loop.copy()
        cut_loop.points[0] += 0.3*(UP+RIGHT)
        cut_loop.points[-1] += 0.3*(DOWN+RIGHT)

        #Unwrap loop
        self.transform_loop(cut_loop, path_arc = np.pi)
        self.wait()
        self.transform_loop(
            line,
            run_time = 3,
            path_arc = np.pi/2
        )
        self.wait()
        self.play(ShowCreation(interval))
        self.play(Write(numbers))
        self.wait()

        #Follow points
        self.loop = original_loop.copy()
        self.play(FadeIn(self.loop))
        self.add(original_loop)
        self.add_dots_at_alphas(*np.linspace(0, 1, 20))
        self.dots.set_color_by_gradient(BLUE, MAROON_C, BLUE)
        dot_at_1 = self.dots[-1]
        dot_at_1.generate_target()
        dot_at_1.target.move_to(interval.get_right())
        dots_copy = self.dots.copy()
        fading_dots = VGroup(*list(self.dots)+list(dots_copy))
        end_dots = VGroup(
            self.dots[0], self.dots[-1],
            dots_copy[0], dots_copy[-1]
        )
        fading_dots.remove(*end_dots)

        self.play(Write(self.dots))
        self.add(dots_copy)
        self.wait()
        self.transform_loop(
            line, 
            added_anims = [MoveToTarget(dot_at_1)],
            run_time = 3
        )
        self.wait()
        self.loop = original_loop
        self.dots = dots_copy
        dot_at_1 = self.dots[-1]
        dot_at_1.target.move_to(cut_loop.points[-1])
        self.transform_loop(
            cut_loop,
            added_anims = [MoveToTarget(dot_at_1)]
        )
        self.wait()
        fading_dots.generate_target()
        fading_dots.target.set_fill(opacity = 0.3)
        self.play(MoveToTarget(fading_dots))
        self.play(
            end_dots.shift, 0.2*UP, 
            rate_func = wiggle
        )
        self.wait()

class RepresentPairInUnitSquare(ClosedLoopScene):
    def construct(self):
        interval = UnitInterval(color = WHITE)
        interval.shift(2.5*DOWN)
        interval.shift(LEFT)
        numbers = interval.get_number_mobjects(0, 1)
        line = Line(interval.get_left(), interval.get_right())
        line.insert_n_curves(self.loop.get_num_curves())
        line.make_smooth()
        vert_interval = interval.copy()
        square = Square()
        square.set_width(interval.get_width())
        square.set_stroke(width = 0)
        square.set_fill(color = BLUE, opacity = 0.3)
        square.move_to(
            interval.get_left(),
            aligned_edge = DOWN+LEFT
        )

        right_words = VGroup(*[
            TextMobject("Pair of\\\\ loop points"),
            TexMobject("\\Downarrow"),
            TextMobject("Point in \\\\ unit square")
        ])
        right_words.arrange(DOWN)
        right_words.to_edge(RIGHT)

        dot_coords = (0.3, 0.7)
        self.loop.scale(0.7)
        self.loop.to_edge(UP)
        self.add_dots_at_alphas(*dot_coords)
        self.dots.set_color_by_gradient(GREEN, RED)

        self.play(
            Write(self.dots),
            Write(right_words[0])
        )
        self.wait()
        self.transform_loop(line)
        self.play(
            ShowCreation(interval),
            Write(numbers),
            Animation(self.dots)
        )
        self.wait()
        self.play(*[
            Rotate(mob, np.pi/2, about_point = interval.get_left())
            for mob in (vert_interval, self.dots[1])
        ])

        #Find interior point
        point = self.dots[0].get_center()[0]*RIGHT
        point += self.dots[1].get_center()[1]*UP
        inner_dot = Dot(point, color = YELLOW)
        dashed_lines = VGroup(*[
            DashedLine(dot, inner_dot, color = dot.get_color())
            for dot in self.dots
        ])
        self.play(ShowCreation(dashed_lines))
        self.play(ShowCreation(inner_dot))
        self.play(
            FadeIn(square),
            Animation(self.dots),
            *list(map(Write, right_words[1:]))
        )
        self.wait()

        #Shift point in square

        movers = list(dashed_lines)+list(self.dots)+[inner_dot]
        for mob in movers:
            mob.generate_target()
        shift_vals = [
            RIGHT+DOWN, 
            LEFT+DOWN, 
            LEFT+2*UP,
            3*DOWN,
            2*RIGHT+UP,
            RIGHT+UP,
            3*LEFT+3*DOWN
        ]
        for shift_val in shift_vals:
            inner_dot.target.shift(shift_val)
            self.dots[0].target.shift(shift_val[0]*RIGHT)
            self.dots[1].target.shift(shift_val[1]*UP)
            for line, dot in zip(dashed_lines, self.dots):
                line.target.put_start_and_end_on(
                    dot.target.get_center(), 
                    inner_dot.target.get_center()
                )
            self.play(*list(map(MoveToTarget, movers)))
        self.wait()
        self.play(*list(map(FadeOut, [dashed_lines, self.dots])))

class EdgesOfSquare(Scene):
    def construct(self):
        square = self.add_square()
        x_edges, y_edges = self.get_edges(square)
        label_groups = self.get_coordinate_labels(square)
        arrow_groups = self.get_arrows(x_edges, y_edges)

        for edge in list(x_edges) + list(y_edges):
            self.play(ShowCreation(edge))
        self.wait()
        for label_group in label_groups:
            for label in label_group[:3]:
                self.play(FadeIn(label))
            self.wait()
            self.play(Write(VGroup(*label_group[3:])))
            self.wait()
        self.play(FadeOut(VGroup(*label_groups)))
        for arrows in arrow_groups:
            self.play(ShowCreation(arrows, run_time = 2))
            self.wait()
        self.play(*[
            ApplyMethod(
                n.next_to,
                square.get_corner(vect+LEFT),
                LEFT,
                MED_SMALL_BUFF,
                path_arc = np.pi/2
            )
            for n, vect in zip(self.numbers, [DOWN, UP])
        ])
        self.wait()

    def add_square(self):
        interval = UnitInterval(color = WHITE)
        interval.shift(2.5*DOWN)
        bottom_left = interval.get_left()
        for tick in interval.tick_marks:
            height = tick.get_height()
            tick.scale_in_place(0.5)
            tick.shift(height*DOWN/4.)
        self.numbers = interval.get_number_mobjects(0, 1)
        vert_interval = interval.copy()
        vert_interval.rotate(np.pi, axis = UP+RIGHT, about_point = bottom_left)
        square = Square()
        square.set_width(interval.get_width())
        square.set_stroke(width = 0)
        square.set_fill(color = BLUE, opacity = 0.3)
        square.move_to(
            bottom_left,
            aligned_edge = DOWN+LEFT
        )
        self.add(interval, self.numbers, vert_interval, square)
        return square

    def get_edges(self, square):
        y_edges = VGroup(*[
            Line(
                square.get_corner(vect+LEFT),
                square.get_corner(vect+RIGHT),
            )
            for vect in (DOWN, UP)
        ])
        y_edges.set_color(BLUE)
        x_edges = VGroup(*[
            Line(
                square.get_corner(vect+DOWN),
                square.get_corner(vect+UP),
            )
            for vect in (LEFT, RIGHT)
        ])
        x_edges.set_color(MAROON_B)
        return x_edges, y_edges

    def get_coordinate_labels(self, square):
        alpha_range = np.arange(0, 1.1, 0.1)
        dot_groups = [
            VGroup(*[
                Dot(interpolate(
                    square.get_corner(DOWN+vect),
                    square.get_corner(UP+vect),
                    alpha
                ))
                for alpha in alpha_range
            ])
            for vect in (LEFT, RIGHT)            
        ]
        for group in dot_groups:
            group.set_color_by_gradient(YELLOW, PURPLE_B)
        label_groups = [
            VGroup(*[
                TexMobject("(%s, %s)"%(a, b)).scale(0.7)
                for b in alpha_range
            ])
            for a in (0, 1)
        ]
        for dot_group, label_group in zip(dot_groups, label_groups):
            for dot, label in zip(dot_group, label_group):
                label[1].set_color(MAROON_B)
                label.next_to(dot, RIGHT*np.sign(dot.get_center()[0]))
                label.add(dot)
        return label_groups

    def get_arrows(self, x_edges, y_edges):
        alpha_range = np.linspace(0, 1, 4)
        return [
            VGroup(*[
                VGroup(*[
                    Arrow(
                        edge.point_from_proportion(a1),
                        edge.point_from_proportion(a2),
                        buff = 0
                    )
                    for a1, a2 in zip(alpha_range, alpha_range[1:])
                ])
                for edge in edges
            ]).set_color(edges.get_color())
            for edges in (x_edges, y_edges)
        ]

class EndpointsGluedTogether(ClosedLoopScene):
    def construct(self):
        interval = UnitInterval(color = WHITE)
        interval.shift(2*DOWN)
        numbers = interval.get_number_mobjects(0, 1)
        line = Line(interval.get_left(), interval.get_right())
        line.insert_n_curves(self.loop.get_num_curves())
        line.make_smooth()

        self.loop.scale(0.7)
        self.loop.to_edge(UP)
        original_loop = self.loop
        self.remove(original_loop)

        self.loop = line
        dots = VGroup(*[
            Dot(line.get_critical_point(vect))
            for vect in (LEFT, RIGHT)
        ])
        dots.set_color(BLUE)

        self.add(interval, dots)
        self.play(dots.rotate_in_place, np.pi/20, rate_func = wiggle)
        self.wait()
        self.transform_loop(
            original_loop,
            added_anims = [
                ApplyMethod(dot.move_to, original_loop.points[0])
                for dot in dots
            ],
            run_time = 3
        )
        self.wait()

class WrapUpToTorus(Scene):
    def construct(self):
        pass

class TorusPlaneAnalogy(ClosedLoopScene):
    def construct(self):
        top_arrow = DoubleArrow(LEFT, RIGHT)
        top_arrow.to_edge(UP, buff = 2*LARGE_BUFF)
        single_pointed_top_arrow = Arrow(LEFT, RIGHT)
        single_pointed_top_arrow.to_edge(UP, buff = 2*LARGE_BUFF)        
        low_arrow = DoubleArrow(LEFT, RIGHT).shift(2*DOWN)
        self.loop.scale(0.5)
        self.loop.next_to(top_arrow, RIGHT)
        self.loop.shift_onto_screen()
        self.add_dots_at_alphas(0.3, 0.5)
        self.dots.set_color_by_gradient(GREEN, RED)

        plane = NumberPlane()
        plane.scale(0.3).next_to(low_arrow, LEFT)
        number_line = NumberLine()
        number_line.scale(0.3)
        number_line.next_to(low_arrow, RIGHT)
        number_line.add(
            Dot(number_line.number_to_point(3), color = GREEN),
            Dot(number_line.number_to_point(-2), color = RED),
        )

        self.wait()
        self.play(ShowCreation(single_pointed_top_arrow))
        self.wait()
        self.play(ShowCreation(top_arrow))
        self.wait()
        self.play(ShowCreation(plane))
        self.play(ShowCreation(low_arrow))
        self.play(ShowCreation(number_line))
        self.wait()

class WigglingPairOfPoints(ClosedLoopScene):
    def construct(self):
        alpha_pairs = [
            (0.4, 0.6),
            (0.42, 0.62),
        ]
        self.add_dots_at_alphas(*alpha_pairs[-1])
        self.add_connecting_lines()
        self.dots.set_color_by_gradient(GREEN, RED)
        self.connecting_lines.set_color(YELLOW)
        for x, pair in zip(list(range(20)), it.cycle(alpha_pairs)):
            self.move_dots_to_alphas(pair, run_time = 0.3)


class WigglingTorusPoint(Scene):
        def construct(self):
            pass    

class WhatAboutUnordered(TeacherStudentsScene):
    def construct(self):
        self.student_says(
            "What about \\\\ unordered pairs?"
        )
        self.play(self.get_teacher().change_mode, "pondering")
        self.random_blink(2)

class TrivialPairCollision(ClosedLoopScene):
    def construct(self):
        self.loop.to_edge(RIGHT)
        self.add_dots_at_alphas(0.35, 0.55)
        self.dots.set_color_by_gradient(BLUE, YELLOW)
        a, b = self.dots
        a_label = TexMobject("a").next_to(a, RIGHT)
        a_label.set_color(a.get_color())
        b_label = TexMobject("b").next_to(b, LEFT)
        b_label.set_color(b.get_color())
        line = Line(
            a.get_corner(DOWN+LEFT),
            b.get_corner(UP+RIGHT),
            color = MAROON_B
        )
        midpoint = Dot(self.dots.get_center(), color = RED)
        randy = Randolph(mode = "pondering")
        randy.next_to(self.loop, LEFT, aligned_edge = DOWN)
        randy.look_at(b)
        self.add(randy)

        for label in a_label, b_label:
            self.play(
                Write(label, run_time = 1),
                randy.look_at, label
            )
        self.play(Blink(randy))
        self.wait()
        swappers = [a, b, a_label, b_label]
        for mob in swappers:
            mob.save_state()
        self.play(
            a.move_to, b,
            b.move_to, a,
            a_label.next_to, b, LEFT,
            b_label.next_to, a, RIGHT,
            randy.look_at, a,
            path_arc = np.pi
        )
        self.play(ShowCreation(midpoint))
        self.play(ShowCreation(line), Animation(midpoint))
        self.play(randy.change_mode, "erm", randy.look_at, b)
        self.play(
            randy.look_at, a,
            *[m.restore for m in swappers],
            path_arc = -np.pi
        )
        self.play(Blink(randy))
        self.wait()

class NotHelpful(Scene):
    def construct(self):
        morty = Mortimer()
        morty.next_to(ORIGIN, DOWN)
        bubble = morty.get_bubble(SpeechBubble, width = 4, height = 3)
        bubble.write("Not helpful!")

        self.add(morty)
        self.play(
            FadeIn(bubble),
            FadeIn(bubble.content),
            morty.change_mode, "angry",
            morty.look, OUT
        )
        self.play(Blink(morty))
        self.wait()

class FoldUnitSquare(EdgesOfSquare):
    def construct(self):    
        self.add_triangles()
        self.add_arrows()
        self.show_points_to_glue()
        self.perform_fold()
        self.show_singleton_pairs()
        self.ask_about_gluing()
        self.clarify_edge_gluing()

    def add_triangles(self):
        square = self.add_square()
        triangles = VGroup(*[
            Polygon(*[square.get_corner(vect) for vect in vects])
            for vects in [
                (DOWN+LEFT, UP+RIGHT, UP+LEFT),            
                (DOWN+LEFT, UP+RIGHT, DOWN+RIGHT),
            ]
        ])
        triangles.set_stroke(width = 0)
        triangles.set_fill(
            color = square.get_color(), 
            opacity = square.get_fill_opacity()
        )
        self.remove(square)
        self.square = square
        self.add(triangles)
        self.triangles = triangles

    def add_arrows(self):
        start_arrows = VGroup()
        end_arrows = VGroup()
        colors = MAROON_B, BLUE
        for a in 0, 1:        
            for color in colors:
                b_range = np.linspace(0, 1, 4)
                for b1, b2 in zip(b_range, b_range[1:]):
                    arrow = Arrow(
                        self.get_point_from_coords(a, b1),
                        self.get_point_from_coords(a, b2),
                        buff = 0,
                        color = color
                    )
                    if color is BLUE:
                        arrow.rotate(
                            -np.pi/2, 
                            about_point = self.square.get_center()
                        )
                    if (a is 0):
                        start_arrows.add(arrow)
                    else:
                        end_arrows.add(arrow)
        self.add(start_arrows, end_arrows)
        self.start_arrows = start_arrows
        self.end_arrows = VGroup(*list(end_arrows[3:])+list(end_arrows[:3])).copy()
        self.end_arrows.set_color(
            color_gradient([MAROON_B, BLUE], 3)[1]
        )

    def show_points_to_glue(self):
        colors = YELLOW, MAROON_B, PINK
        pairs = [(0.2, 0.3), (0.5, 0.7), (0.25, 0.6)]
        unit = self.square.get_width()

        start_dots = VGroup()
        end_dots = VGroup()
        for (x, y), color in zip(pairs, colors):
            old_x_line, old_y_line = None, None
            for (a, b) in (x, y), (y, x):
                point = self.get_point_from_coords(a, b)
                dot = Dot(point)
                dot.set_color(color)
                if color == colors[-1]:
                    s = "(x, y)" if a < b else "(y, x)"
                    label = TexMobject(s)
                else:
                    label = TexMobject("(%.01f, %.01f)"%(a, b))
                vect = UP+RIGHT if a < b else DOWN+RIGHT
                label.next_to(dot, vect, buff = SMALL_BUFF)

                self.play(*list(map(FadeIn, [dot, label])))
                x_line = Line(point+a*unit*LEFT, point)
                y_line = Line(point+b*unit*DOWN, point)
                x_line.set_color(GREEN)
                y_line.set_color(RED)
                if old_x_line is None:
                    self.play(ShowCreation(x_line), Animation(dot))
                    self.play(ShowCreation(y_line), Animation(dot))
                    old_x_line, old_y_line = y_line, x_line
                else:
                    self.play(Transform(old_x_line, x_line), Animation(dot))
                    self.play(Transform(old_y_line, y_line), Animation(dot))
                    self.remove(old_x_line, old_y_line)
                    self.add(x_line, y_line, dot)
                self.wait(2)
                self.play(FadeOut(label))
                if a < b:
                    start_dots.add(dot)
                else:
                    end_dots.add(dot)
            self.play(*list(map(FadeOut, [x_line, y_line])))
        self.start_dots, self.end_dots = start_dots, end_dots

    def perform_fold(self):
        diag_line = DashedLine(
            self.square.get_corner(DOWN+LEFT),
            self.square.get_corner(UP+RIGHT),
            color = RED
        )

        self.play(ShowCreation(diag_line))
        self.wait()
        self.play(
            Transform(*self.triangles),
            Transform(self.start_dots, self.end_dots),
            Transform(self.start_arrows, self.end_arrows),
        )
        self.wait()
        self.diag_line = diag_line

    def show_singleton_pairs(self):
        xs = [0.7, 0.4, 0.5]
        old_label = None
        old_dot = None
        for x in xs:
            point = self.get_point_from_coords(x, x)
            dot = Dot(point)
            if x is xs[-1]:
                label = TexMobject("(x, x)")
            else:
                label = TexMobject("(%.1f, %.1f)"%(x, x))
            label.next_to(dot, UP+LEFT, buff = SMALL_BUFF)
            VGroup(dot, label).set_color(RED)
            if old_label is None:
                self.play(
                    ShowCreation(dot),
                    Write(label)
                )
                old_label = label
                old_dot = dot
            else:
                self.play(
                    Transform(old_dot, dot),
                    Transform(old_label, label),
                )
            self.wait()
        #Some strange bug necesitating this
        self.remove(old_label)
        self.add(label)

    def ask_about_gluing(self):
        keepers = VGroup(
            self.triangles[0],
            self.start_arrows,
            self.diag_line
        ).copy()
        faders = VGroup(*self.get_mobjects())
        randy = Randolph()
        randy.next_to(ORIGIN, DOWN)
        bubble = randy.get_bubble(height = 4, width = 6)
        bubble.write("How do you \\\\ glue those arrows?")

        self.play(
            FadeOut(faders),
            Animation(keepers)
        )
        self.play(
            keepers.scale, 0.6,
            keepers.shift, 4*RIGHT + UP,
            FadeIn(randy)
        )
        self.play(
            randy.change_mode, "pondering",
            randy.look_at, keepers,
            ShowCreation(bubble),
            Write(bubble.content)
        )
        self.play(Blink(randy))
        self.wait()
        self.randy = randy

    def clarify_edge_gluing(self):
        dots = VGroup(*[
            Dot(self.get_point_from_coords(*coords), radius = 0.1)
            for coords in [
                (0.1, 0),
                (1, 0.1),
                (0.9, 0),
                (1, 0.9),
            ]
        ])
        dots.scale(0.6)
        dots.shift(4*RIGHT + UP)
        for dot in dots[:2]:
            dot.set_color(YELLOW)
            self.play(
                ShowCreation(dot),
                self.randy.look_at, dot
            )
        self.wait()
        for dot in dots[2:]:
            dot.set_color(MAROON_B)
            self.play(
                ShowCreation(dot),
                self.randy.look_at, dot
            )
        self.play(Blink(self.randy))
        self.wait()

    def get_point_from_coords(self, x, y):
        left, right, bottom, top = [
            self.triangles.get_edge_center(vect)
            for vect in (LEFT, RIGHT, DOWN, UP)
        ]
        x_point = interpolate(left, right, x)
        y_point = interpolate(bottom, top, y)
        return x_point[0]*RIGHT + y_point[1]*UP

class PrepareForMobiusStrip(Scene):
    def construct(self):
        self.add_triangles()
        self.perform_cut()
        self.rearrange_pieces()

    def add_triangles(self):
        triangles = VGroup(
            Polygon(
                DOWN+LEFT,
                DOWN+RIGHT,
                ORIGIN,
            ),
            Polygon(
                DOWN+RIGHT,
                UP+RIGHT,          
                ORIGIN,
            ),
        )
        triangles.set_fill(color = BLUE, opacity = 0.6)
        triangles.set_stroke(width = 0)
        triangles.center()
        triangles.scale(2)
        arrows_color = color_gradient([PINK, BLUE], 3)[1]
        for tri in triangles:
            anchors = tri.get_anchors_and_handles()[0]
            alpha_range = np.linspace(0, 1, 4)
            arrows = VGroup(*[
                Arrow(
                    interpolate(anchors[0], anchors[1], a),
                    interpolate(anchors[0], anchors[1], b),
                    buff = 0,
                    color = arrows_color
                )
                for a, b in zip(alpha_range, alpha_range[1:])
            ])
            tri.original_arrows = arrows
            tri.add(arrows)
            i, j, k = (0, 2, 1) if tri is triangles[0] else (1, 2, 0)
            dashed_line = DashedLine(
                anchors[i], anchors[j], 
                color = RED
            )
            tri.add(dashed_line)

            #Add but don't draw cut_arrows
            start, end = anchors[j], anchors[k]
            cut_arrows = VGroup(*[
                Arrow(
                    interpolate(start, end, a),
                    interpolate(start, end, b),
                    buff = 0,
                    color = YELLOW
                )
                for a, b in zip(alpha_range, alpha_range[1:])
            ])
            tri.cut_arrows = cut_arrows
        self.add(triangles)
        self.triangles = triangles

    def perform_cut(self):
        tri1, tri2 = self.triangles


        self.play(ShowCreation(tri1.cut_arrows))
        for tri in self.triangles:
            tri.add(tri.cut_arrows)
        self.wait()
        self.play(
            tri1.shift, (DOWN+LEFT)/2.,
            tri2.shift, (UP+RIGHT)/2.,
        )
        self.wait()

    def rearrange_pieces(self):
        tri1, tri2 = self.triangles
        self.play(
            tri1.rotate, np.pi, UP+RIGHT,
            tri1.next_to, ORIGIN, RIGHT,
            tri2.next_to, ORIGIN, LEFT,
        )
        self.wait()
        self.play(*[
            ApplyMethod(tri.shift, tri.points[0][0]*LEFT)
            for tri in self.triangles
        ])
        self.play(*[
            FadeOut(tri.original_arrows)
            for tri in self.triangles
        ])
        for tri in self.triangles:
            tri.remove(tri.original_arrows)
        self.wait()
        # self.play(*[
        #     ApplyMethod(tri.rotate, -np.pi/4)
        #     for tri in self.triangles
        # ])
        # self.wait()

class FoldToMobius(Scene):
    def construct(self):
        pass

class MobiusPlaneAnalogy(ClosedLoopScene):
    def construct(self):
        top_arrow = Arrow(LEFT, RIGHT)
        top_arrow.to_edge(UP, buff = 2*LARGE_BUFF)
        low_arrow = Arrow(LEFT, RIGHT).shift(2*DOWN)
        self.loop.scale(0.5)
        self.loop.next_to(top_arrow, RIGHT)
        self.loop.shift_onto_screen()
        self.add_dots_at_alphas(0.3, 0.5)
        self.dots.set_color(PURPLE_B)

        plane = NumberPlane()
        plane.scale(0.3).next_to(low_arrow, LEFT)
        number_line = NumberLine()
        number_line.scale(0.3)
        number_line.next_to(low_arrow, RIGHT)
        number_line.add(
            Dot(number_line.number_to_point(3), color = GREEN),
            Dot(number_line.number_to_point(-2), color = RED),
        )

        self.wait()
        self.play(ShowCreation(top_arrow))
        self.wait()
        self.play(ShowCreation(plane))
        self.play(ShowCreation(low_arrow))
        self.play(ShowCreation(number_line))
        self.wait()

class DrawRightArrow(Scene):
    CONFIG = {
        "tex" : "\\Rightarrow"
    }
    def construct(self):
        arrow = TexMobject(self.tex)
        arrow.scale(4)
        self.play(Write(arrow))
        self.wait()

class DrawLeftrightArrow(DrawRightArrow):
    CONFIG = {
        "tex" : "\\Leftrightarrow"
    }

class MobiusToPairToSurface(ClosedLoopScene):
    def construct(self):
        self.loop.scale(0.5)
        self.loop.next_to(ORIGIN, RIGHT)
        self.loop.to_edge(UP)
        self.add_dots_at_alphas(0.4, 0.6)
        self.dots.set_color(MAROON_B)
        self.add_connecting_lines()
        strip_dot = Dot().next_to(self.loop, LEFT, buff = 2*LARGE_BUFF)
        surface_dot = Dot().next_to(self.loop, DOWN, buff = 2*LARGE_BUFF)

        top_arrow = Arrow(strip_dot, self.loop)
        right_arrow = Arrow(self.loop, surface_dot)
        diag_arrow = Arrow(strip_dot, surface_dot)

        randy = self.randy = Randolph(mode = "pondering")
        randy.next_to(ORIGIN, DOWN+LEFT)

        self.look_at(strip_dot)
        self.play(
            ShowCreation(top_arrow),
            randy.look_at, self.loop
        )
        self.wait()
        self.look_at(strip_dot, surface_dot)
        self.play(ShowCreation(diag_arrow))
        self.play(Blink(randy))
        self.look_at(strip_dot, self.loop)
        self.wait()
        self.play(
            ShowCreation(right_arrow),
            randy.look_at, surface_dot
        )
        self.play(Blink(randy))
        self.play(randy.change_mode, "happy")
        self.play(Blink(randy))
        self.wait()


    def look_at(self, *things):
        for thing in things:
            self.play(self.randy.look_at, thing)

class MapMobiusStripOntoSurface(Scene):
    def construct(self):
        pass

class StripMustIntersectItself(TeacherStudentsScene):
    def construct(self):
        self.teacher_says(
            """
            The strip must 
            intersect itself
            during this process
            """,
            width = 4
        )
        dot = Dot(2*UP + 4*LEFT)
        for student in self.get_students():
            student.generate_target()
            student.target.change_mode("pondering")
            student.target.look_at(dot)
        self.play(*list(map(MoveToTarget, self.get_students())))
        self.random_blink(4)

class PairOfMobiusPointsLandOnEachother(Scene):
    def construct(self):
        pass

class ThatsTheProof(TeacherStudentsScene):
    def construct(self):
        self.teacher_says(
            """
            Bada boom
            bada bang!
            """,
            target_mode = "hooray",
            width = 4
        )
        self.change_student_modes(*["hooray"]*3)
        self.random_blink()
        self.change_student_modes(
            "confused", "sassy", "erm"
        )
        self.teacher_says(
            """
            If you trust
            the mobius strip 
            fact...
            """,
            target_mode = "guilty",            
            width = 4,
        )
        self.random_blink()

class TryItYourself(TeacherStudentsScene):
    def construct(self):
        self.teacher_says("""
            It's actually an
            edifying exercise.
        """)
        self.random_blink()
        self.change_student_modes(*["pondering"]*3)
        self.random_blink(2)

        pi = self.get_students()[1]
        bubble = pi.get_bubble(
            "thought", 
            width = 4, height = 4,
            direction = RIGHT
        )
        bubble.set_fill(BLACK, opacity = 1)
        bubble.write("Orientation seem\\\\ to matter...")
        self.play(
            FadeIn(bubble),
            Write(bubble.content)
        )
        self.random_blink(3)

class OneMoreAnimation(TeacherStudentsScene):
    def construct(self):
        self.teacher_says("""
            One more animation,
            but first...
        """)
        self.change_student_modes(*["happy"]*3)
        self.random_blink()

class PatreonThanks(Scene):
    CONFIG = {
        "specific_patrons" : [
            "Loo Yu Jun",
            "Tom",
            "Othman Alikhan",
            "Juan Batiz-Benet",
            "Markus Persson",
            "Joseph John Cox",
            "Achille Brighton",
            "Kirk Werklund",
            "Luc Ritchie",
            "Ripta Pasay",
            "PatrickJMT  ",
            "Felipe Diniz",
        ]
    }
    def construct(self):
        morty = Mortimer()
        morty.next_to(ORIGIN, DOWN)

        n_patrons = len(self.specific_patrons)
        special_thanks = TextMobject("Special thanks to:")
        special_thanks.set_color(YELLOW)
        special_thanks.shift(2*UP)

        left_patrons = VGroup(*list(map(TextMobject, 
            self.specific_patrons[:n_patrons/2]
        )))
        right_patrons = VGroup(*list(map(TextMobject, 
            self.specific_patrons[n_patrons/2:]
        )))
        for patrons, vect in (left_patrons, LEFT), (right_patrons, RIGHT):
            patrons.arrange(DOWN, aligned_edge = LEFT)
            patrons.next_to(special_thanks, DOWN)
            patrons.to_edge(vect, buff = LARGE_BUFF)

        self.play(morty.change_mode, "gracious")
        self.play(Write(special_thanks, run_time = 1))
        self.play(
            Write(left_patrons),
            morty.look_at, left_patrons
        )
        self.play(
            Write(right_patrons),
            morty.look_at, right_patrons
        )
        self.play(Blink(morty))
        for patrons in left_patrons, right_patrons:
            for index in 0, -1:
                self.play(morty.look_at, patrons[index])
                self.wait()

class CreditTWo(Scene):
    def construct(self):
        morty = Mortimer()
        morty.next_to(ORIGIN, DOWN)
        morty.to_edge(RIGHT)

        brother = PiCreature(color = GOLD_E)
        brother.next_to(morty, LEFT)
        brother.look_at(morty.eyes)
        
        headphones = Headphones(height = 1)
        headphones.move_to(morty.eyes, aligned_edge = DOWN)
        headphones.shift(0.1*DOWN)

        url = TextMobject("www.audible.com/3b1b")
        url.to_corner(UP+RIGHT, buff = LARGE_BUFF)

        self.add(morty)
        self.play(Blink(morty))
        self.play(
            FadeIn(headphones), 
            Write(url),
            Animation(morty)
        )
        self.play(morty.change_mode, "happy")
        self.wait()
        self.play(Blink(morty))
        self.wait()
        self.play(
            FadeIn(brother),
            morty.look_at, brother.eyes
        )
        self.play(brother.change_mode, "surprised")
        self.play(Blink(brother))
        self.wait()
        self.play(
            morty.look, LEFT,
            brother.change_mode, "happy",
            brother.look, LEFT
        )
        self.play(Blink(morty))
        self.wait()

class CreditThree(Scene):
    def construct(self):
        logo_dot = Dot().to_edge(UP).shift(3*RIGHT)
        randy = Randolph()
        randy.next_to(ORIGIN, DOWN)
        randy.to_edge(LEFT)
        randy.look(RIGHT)
        self.add(randy)
        bubble = randy.get_bubble(width = 2, height = 2)

        domains = VGroup(*list(map(TextMobject, [
            "visualnumbertheory.com",
            "buymywidgets.com",
            "learnwhatilearn.com",
        ])))
        domains.arrange(DOWN, aligned_edge = LEFT)
        domains.next_to(randy, UP, buff = LARGE_BUFF)
        domains.shift_onto_screen()

        promo_code = TextMobject("Promo code: TOPOLOGY")
        promo_code.shift(3*RIGHT)
        self.add(promo_code)
        whois = TextMobject("Free WHOIS privacy")
        whois.next_to(promo_code, DOWN, buff = LARGE_BUFF)

        self.play(Blink(randy))
        self.play(
            randy.change_mode, "happy",
            randy.look_at, logo_dot
        )
        self.wait()
        self.play(
            ShowCreation(bubble),
            randy.change_mode, "pondering",
            run_time = 2
        )
        self.play(Blink(randy))
        self.play(
            Transform(bubble, VectorizedPoint(randy.get_corner(UP+LEFT))),
            randy.change_mode, "sad"
        )
        self.wait()
        self.play(
            Write(domains, run_time = 5),
            randy.look_at, domains
        )
        self.wait()
        self.play(Blink(randy))
        self.play(
            randy.change_mode, "hooray",
            randy.look_at, logo_dot,
            FadeOut(domains)
        )
        self.wait()
        self.play(
            Write(whois),
            randy.change_mode, "confused",
            randy.look_at, whois
        )
        self.wait(2)
        self.play(randy.change_mode, "sassy")
        self.wait(2)
        self.play(
            randy.change_mode, "happy",
            randy.look_at, logo_dot
        )
        self.play(Blink(randy))
        self.wait()


class ShiftingLoopPairSurface(Scene):
    def construct(self):
        pass

class ThumbnailImage(ClosedLoopScene):
    def construct(self):
        self.add_rect_dots(square = True)
        for dot in self.dots:
            dot.scale_in_place(1.5)
        self.add_connecting_lines(cyclic = True)
        self.connecting_lines.set_stroke(width = 10)
        self.loop.add(self.connecting_lines, self.dots)

        title = TextMobject("Unsolved")
        title.scale(2.5)
        title.to_edge(UP)
        title.set_color_by_gradient(YELLOW, MAROON_B)
        self.add(title)
        self.loop.next_to(title, DOWN, buff = MED_SMALL_BUFF)
        self.loop.shift(2*LEFT)









