from big_ol_pile_of_manim_imports import *

class ShowExampleTest(ExternallyAnimatedScene):
    pass

class IntroducePutnam(Scene):
    CONFIG = {
        "dont_animate" : False,
    }
    def construct(self):
        title = TextMobject("Putnam Competition")
        title.to_edge(UP, buff = MED_SMALL_BUFF)
        title.set_color(BLUE)
        six_hours = TextMobject("6", "hours")
        three_hours = TextMobject("3", "hours")
        for mob in six_hours, three_hours:
            mob.next_to(title, DOWN, MED_LARGE_BUFF)
            # mob.set_color(BLUE)
        three_hours.shift(FRAME_X_RADIUS*LEFT/2)
        three_hours_copy = three_hours.copy()
        three_hours_copy.shift(FRAME_X_RADIUS*RIGHT)

        question_groups = VGroup(*[
            VGroup(*[
                TextMobject("%s%d)"%(c, i))
                for i in range(1, 7)
            ]).arrange(DOWN, buff = MED_LARGE_BUFF)
            for c in ("A", "B")
        ]).arrange(RIGHT, buff = FRAME_X_RADIUS - MED_SMALL_BUFF)
        question_groups.to_edge(LEFT)
        question_groups.to_edge(DOWN, MED_LARGE_BUFF)
        flat_questions = VGroup(*it.chain(*question_groups))

        rects = VGroup()
        for questions in question_groups:
            rect = SurroundingRectangle(questions, buff = MED_SMALL_BUFF)
            rect.set_stroke(WHITE, 2)
            rect.stretch_to_fit_width(FRAME_X_RADIUS - 1)
            rect.move_to(questions.get_left() + MED_SMALL_BUFF*LEFT, LEFT)
            rects.add(rect)

        out_of_tens = VGroup()
        for question in flat_questions:
            out_of_ten = TexMobject("/10")
            out_of_ten.set_color(GREEN)
            out_of_ten.move_to(question)
            dist = rects[0].get_width() - 1.2
            out_of_ten.shift(dist*RIGHT)
            out_of_tens.add(out_of_ten)

        out_of_120 = TexMobject("/120")
        out_of_120.next_to(title, RIGHT, LARGE_BUFF)
        out_of_120.set_color(GREEN)

        out_of_120.generate_target()
        out_of_120.target.to_edge(RIGHT, LARGE_BUFF)
        median = TexMobject("2")
        median.next_to(out_of_120.target, LEFT, SMALL_BUFF)
        median.set_color(RED)
        median.align_to(out_of_120[-1])
        median_words = TextMobject("Typical median $\\rightarrow$")
        median_words.next_to(median, LEFT)

        difficulty_strings = [
            "Pretty hard",
            "Hard",
            "Harder",
            "Very hard",
            "Ughhh",
            "Can I go home?"
        ]
        colors = color_gradient([YELLOW, RED], len(difficulty_strings))
        difficulties = VGroup()
        for i, s, color in zip(it.count(), difficulty_strings, colors):
            for question_group in question_groups:
                question = question_group[i]
                text = TextMobject("\\dots %s \\dots"%s)
                text.scale(0.7)
                text.next_to(question, RIGHT)
                text.set_color(color)
                difficulties.add(text)


        if self.dont_animate:        
            test = VGroup()
            test.rect = rects[0]
            test.questions = question_groups[0]
            test.out_of_tens = VGroup(*out_of_tens[:6])
            test.difficulties = VGroup(*difficulties[::2])
            test.digest_mobject_attrs()
            self.test = test
            return

        self.add(title)
        self.play(Write(six_hours))
        self.play(LaggedStartMap(
            GrowFromCenter, flat_questions,
            run_time = 3,
        ))
        self.play(
            ReplacementTransform(six_hours, three_hours),
            ReplacementTransform(six_hours.copy(), three_hours_copy),
            *list(map(ShowCreation, rects))
        )
        self.wait()
        self.play(LaggedStartMap(
            DrawBorderThenFill, out_of_tens,
            run_time = 3,
            stroke_color = YELLOW
        ))
        self.wait()
        self.play(ReplacementTransform(
            out_of_tens.copy(), VGroup(out_of_120),
            lag_ratio = 0.5,
            run_time = 2,
        ))
        self.wait()
        self.play(
            title.next_to, median_words.copy(), LEFT, LARGE_BUFF,
            MoveToTarget(out_of_120),
            Write(median_words)
        )
        self.play(Write(median))
        for difficulty in difficulties:
            self.play(FadeIn(difficulty))
        self.wait()

class NatureOf5sAnd6s(TeacherStudentsScene):
    CONFIG = {
        "test_scale_val" : 0.65
    }
    def construct(self):
        test = self.get_test()

        self.students.fade(1)
        self.play(
            test.scale, self.test_scale_val,
            test.to_corner, UP+LEFT,
            FadeIn(self.teacher),
            self.get_student_changes(
                *["horrified"]*3,
                look_at_arg = test
            )
        )
        self.wait()

        mover = VGroup(
            test.questions[-1].copy(),
            test.difficulties[-1].copy(),
        )
        mover.generate_target()
        mover.target.scale(1./self.test_scale_val)
        mover.target.next_to(
            self.teacher.get_corner(UP+LEFT), UP,
        )
        new_words = TextMobject("\\dots Potentially very elegant \\dots")
        new_words.set_color(GREEN)
        new_words.set_height(mover.target[1].get_height())
        new_words.next_to(mover.target[0], RIGHT, SMALL_BUFF)

        self.play(
            MoveToTarget(mover),
            self.teacher.change, "raise_right_hand",
        )
        self.change_student_modes(*["pondering"]*3)
        self.play(Transform(mover[1], new_words))
        self.look_at((FRAME_X_RADIUS*RIGHT + FRAME_Y_RADIUS*UP)/2)
        self.wait(4)


    ###

    def get_test(self):
        prev_scene = IntroducePutnam(dont_animate = True)
        return prev_scene.test

class OtherVideoClips(Scene):
    def construct(self):
        rect = ScreenRectangle()
        rect.set_height(6.5)
        rect.center()
        rect.to_edge(DOWN)
        titles = list(map(TextMobject, [
            "Essence of calculus, chapter 1",
            "Pi hiding in prime regularities",
            "How do cryptocurrencies work?"
        ]))

        self.add(rect)
        last_title = None
        for title in titles:
            title.to_edge(UP, buff = MED_SMALL_BUFF)
            if last_title:
                self.play(ReplacementTransform(last_title, title))
            else:
                self.play(FadeIn(title))
            self.wait(3)
            last_title = title

class IntroduceTetrahedron(ExternallyAnimatedScene):
    pass

class IntroduceTetrahedronSupplement(Scene):
    def construct(self):
        title = TextMobject("4", "random$^*$ points on sphere")
        title.set_color(YELLOW)
        question = TextMobject("Probability that this tetrahedron \\\\ contains the sphere's center?")
        question.next_to(title, DOWN, MED_LARGE_BUFF)
        group = VGroup(title, question)
        group.set_width(FRAME_WIDTH-1)
        group.to_edge(DOWN)

        for n in range(1, 4):
            num = TextMobject(str(n))
            num.replace(title[0], dim_to_match = 1)
            num.set_color(YELLOW)
            self.add(num)
            self.wait(0.7)
            self.remove(num)
        self.add(title[0])
        self.play(FadeIn(title[1], lag_ratio = 0.5))
        self.wait(2)
        self.play(Write(question))
        self.wait(2)

class IntroduceTetrahedronFootnote(Scene):
    def construct(self):
        words = TextMobject("""
            $^*$Chosen independently with a \\\\
            uniform distribution on the sphere.
        """)
        words.to_corner(UP+LEFT)
        self.add(words)
        self.wait(2)

class HowDoYouStart(TeacherStudentsScene):
    def construct(self):
        self.student_says(
            "How do you even start?",
            target_mode = "raise_left_hand"
        )
        self.change_student_modes("confused", "raise_left_hand", "erm")
        self.wait()
        self.teacher_says("Try a simpler case.")
        self.change_student_modes(*["thinking"]*3)
        self.wait(2)

class TwoDCase(Scene):
    CONFIG = {
        "center" : ORIGIN,
        "random_seed" : 4,
        "radius" : 2.5,
        "center_color" : BLUE,
        "point_color" : YELLOW,
        "positive_triangle_color" : BLUE,
        "negative_triangle_color" : RED,
        "triangle_fill_opacity" : 0.25,
        "n_initial_random_choices" : 9,
        "n_p3_random_moves" : 4,
    }
    def construct(self):
        self.add_title()
        self.add_circle()
        self.choose_three_random_points()
        self.simplify_further()
        self.fix_two_points_in_place()
        self.note_special_region()
        self.draw_lines_through_center()
        self.ask_about_probability_p3_lands_in_this_arc()
        self.various_arc_sizes_for_p1_p2_placements()
        self.ask_about_average_arc_size()
        self.fix_p1_in_place()
        self.overall_probability()

    def add_title(self):
        title = TextMobject("2D Case")
        title.to_corner(UP+LEFT)
        self.add(title)
        self.set_variables_as_attrs(title)

    def add_circle(self):
        circle = Circle(radius = self.radius, color = WHITE)
        center_dot = Dot(color = self.center_color).center()
        radius = DashedLine(ORIGIN, circle.radius*RIGHT)
        VGroup(circle, center_dot, radius).shift(self.center)

        self.add(center_dot)
        self.play(ShowCreation(radius))
        self.play(
            ShowCreation(circle),
            Rotating(radius, angle = 2*np.pi, about_point = self.center),
            rate_func = smooth,
            run_time = 2,
        )
        self.play(ShowCreation(
            radius,
            rate_func = lambda t : smooth(1-t),
            remover = True
        ))
        self.wait()

        self.set_variables_as_attrs(circle, center_dot)

    def choose_three_random_points(self):
        point_mobs = self.get_point_mobs()
        point_labels = self.get_point_mob_labels()
        triangle = self.get_triangle()
        self.point_labels_update = self.get_labels_update(point_mobs, point_labels)
        self.triangle_update = self.get_triangle_update(point_mobs, triangle)
        self.update_animations = [
            self.triangle_update,
            self.point_labels_update,
        ]
        for anim in self.update_animations:
            anim.update(0)

        question = TextMobject(
            "Probability that \\\\ this triangle \\\\",
            "contains the center", "?",
            arg_separator = "",
        )
        question.set_color_by_tex("center", self.center_color)
        question.scale(0.8)
        question.to_corner(UP+RIGHT)
        self.question = question

        self.play(LaggedStartMap(DrawBorderThenFill, point_mobs))
        self.play(FadeIn(triangle))
        self.wait()
        self.play(LaggedStartMap(Write, point_labels))
        self.wait()
        self.play(Write(question))
        for x in range(self.n_initial_random_choices):
            self.change_point_mobs_randomly()
            self.wait()
        angles = self.get_point_mob_angles()
        target_angles = [5*np.pi/8, 7*np.pi/8, 0]
        self.change_point_mobs([ta - a for a, ta in zip(angles, target_angles)])
        self.wait()

    def simplify_further(self):
        morty = Mortimer().flip()
        morty.scale(0.75)
        morty.to_edge(DOWN)
        morty.shift(3.5*LEFT)

        bubble = SpeechBubble(
            direction = RIGHT,
            height = 3, width = 3
        )
        bubble.pin_to(morty)
        bubble.to_edge(LEFT, SMALL_BUFF)
        bubble.write("Simplify \\\\ more!")

        self.play(FadeIn(morty))
        self.play(
            morty.change, "hooray",
            ShowCreation(bubble),
            Write(bubble.content)
        )
        self.play(Blink(morty))
        self.wait()
        self.play(
            morty.change, "happy",
            morty.fade, 1,
            *list(map(FadeOut, [bubble, bubble.content]))
        )
        self.remove(morty)

    def fix_two_points_in_place(self):
        push_pins = VGroup()
        for point_mob in self.point_mobs[:-1]:
            push_pin = SVGMobject(file_name = "push_pin")
            push_pin.set_height(0.5)
            push_pin.move_to(point_mob.get_center(), DOWN)
            line = Line(ORIGIN, UP)
            line.set_stroke(WHITE, 2)
            line.set_height(0.1)
            line.move_to(push_pin, UP)
            line.shift(0.3*SMALL_BUFF*(2*DOWN+LEFT))
            push_pin.add(line)
            push_pin.set_fill(LIGHT_GREY)
            push_pin.save_state()
            push_pin.shift(UP)
            push_pin.fade(1)
            push_pins.add(push_pin)

        self.play(LaggedStartMap(
            ApplyMethod, push_pins,
            lambda mob : (mob.restore,)
        ))
        self.add_foreground_mobjects(push_pins)
        d_thetas = 2*np.pi*np.random.random(self.n_p3_random_moves)
        for d_theta in d_thetas:
            self.change_point_mobs([0, 0, d_theta])
            self.wait()

        self.set_variables_as_attrs(push_pins)

    def note_special_region(self):
        point_mobs = self.point_mobs
        angles = self.get_point_mob_angles()

        all_arcs = self.get_all_arcs()
        arc = all_arcs[-1]
        arc_lines = VGroup()
        for angle in angles[:2]:
            line = Line(LEFT, RIGHT).scale(SMALL_BUFF)
            line.shift(self.radius*RIGHT)
            line.rotate(angle + np.pi)
            line.shift(self.center)
            line.set_stroke(arc.get_color())
            arc_lines.add(line)

        self.play(ShowCreation(arc_lines))
        self.change_point_mobs([0, 0, angles[0]+np.pi-angles[2]])
        self.change_point_mobs(
            [0, 0, arc.angle],
            ShowCreation(arc, run_time = 2)
        )
        self.change_point_mobs([0, 0, np.pi/4 - angles[1]])
        self.change_point_mobs([0, 0, 0.99*np.pi], run_time = 4)
        self.wait()

        self.set_variables_as_attrs(all_arcs, arc, arc_lines)

    def draw_lines_through_center(self):
        point_mobs = self.point_mobs
        angles = self.get_point_mob_angles()
        all_arcs = self.all_arcs

        lines = self.get_center_lines()

        self.add_foreground_mobjects(self.center_dot)
        for line in lines:
            self.play(ShowCreation(line))
        self.play(FadeIn(all_arcs), Animation(point_mobs))
        self.remove(self.circle)
        self.wait()
        self.play(
            all_arcs.space_out_submobjects, 1.5,
            Animation(point_mobs),
            rate_func = there_and_back,
            run_time = 1.5,
        )
        self.wait()
        self.change_point_mobs(
            [0, 0, np.mean(angles[:2])+np.pi-angles[2]]
        )
        self.wait()
        for x in range(3):
            self.change_point_mobs([0, 0, np.pi/2])
        self.wait()

    def ask_about_probability_p3_lands_in_this_arc(self):
        arc = self.arc

        arrow = Vector(LEFT, color = BLUE)
        arrow.next_to(arc.get_center(), RIGHT, MED_LARGE_BUFF)
        question = TextMobject("Probability of landing \\\\ in this arc?")
        question.scale(0.8)
        question.next_to(arrow, RIGHT)
        question.shift_onto_screen()
        question.shift(SMALL_BUFF*UP)

        answer = TexMobject(
            "{\\text{Length of arc}", "\\over",
            "\\text{Circumference}}"
        )
        answer.set_color_by_tex("arc", BLUE)
        answer.scale(0.8)
        answer.next_to(arrow, RIGHT)
        equals = TexMobject("=")
        equals.rotate(np.pi/2)
        equals.next_to(answer, UP, buff = 0.35)

        self.play(FadeIn(question), GrowArrow(arrow))
        self.have_p3_jump_around_randomly(15)
        self.play(
            question.next_to, answer, UP, LARGE_BUFF,
            Write(equals),
            FadeIn(answer)
        )
        self.have_p3_jump_around_randomly(4)
        angles = self.get_point_mob_angles()
        self.change_point_mobs(
            [0, 0, 1.35*np.pi - angles[2]],
            run_time = 0,
        )
        self.wait()

        question.add(equals)
        self.arc_prob_question = question
        self.arc_prob = answer
        self.arc_size_arrow = arrow

    def various_arc_sizes_for_p1_p2_placements(self):
        arc = self.arc

        self.triangle.save_state()
        self.play(*list(map(FadeOut, [
            self.push_pins, self.triangle, self.arc_lines
        ])))
        self.update_animations.remove(self.triangle_update)
        self.update_animations += [
            self.get_center_lines_update(self.point_mobs, self.center_lines),
            self.get_arcs_update(self.all_arcs)
        ]

        #90 degree angle
        self.change_point_mobs_to_angles([np.pi/2, np.pi], run_time = 1)
        elbow = VGroup(
            Line(DOWN, DOWN+RIGHT),
            Line(DOWN+RIGHT, RIGHT),
        )
        elbow.scale(0.25)
        elbow.shift(self.center)
        ninety_degrees = TexMobject("90^\\circ")
        ninety_degrees.next_to(elbow, DOWN+RIGHT, buff = 0)
        proportion = DecimalNumber(0.25)
        proportion.set_color(self.center_color)
        # proportion.next_to(arc.point_from_proportion(0.5), DOWN, MED_LARGE_BUFF)
        proportion.next_to(self.arc_size_arrow, DOWN)
        def proportion_update_func(alpha):
            angles = self.get_point_mob_angles()
            diff = abs(angles[1]-angles[0])/(2*np.pi)
            return min(diff, 1-diff)
        proportion_update = ChangingDecimal(proportion, proportion_update_func)

        self.play(ShowCreation(elbow), FadeIn(ninety_degrees))
        self.wait()
        self.play(
            ApplyMethod(
                arc.rotate_in_place, np.pi/12,
                rate_func = wiggle,
            )
        )
        self.play(LaggedStartMap(FadeIn, proportion, run_time = 1))
        self.wait()

        #Non right angles
        angle_pairs = [
            (0.26*np.pi, 1.24*np.pi), 
            (0.73*np.pi, 0.78*np.pi),
            (0.5*np.pi, np.pi),
        ]
        self.update_animations.append(proportion_update)
        for angle_pair in angle_pairs:
            self.change_point_mobs_to_angles(
                angle_pair,
                VGroup(elbow, ninety_degrees).fade, 1,
            )
            self.remove(elbow, ninety_degrees)
            self.wait()

        self.set_variables_as_attrs(proportion, proportion_update)

    def ask_about_average_arc_size(self):
        proportion = self.proportion
        brace = Brace(proportion, DOWN, buff = SMALL_BUFF)
        average = brace.get_text("Average?", buff = SMALL_BUFF)

        self.play(
            GrowFromCenter(brace),
            Write(average)
        )
        for x in range(6):
            self.change_point_mobs_to_angles(
                2*np.pi*np.random.random(2)
            )
        self.change_point_mobs_to_angles(
            [1.2*np.pi, 0.3*np.pi]
        )
        self.wait()

        self.set_variables_as_attrs(brace, average)

    def fix_p1_in_place(self):
        push_pin = self.push_pins[0]
        P1, P2, P3 = point_mobs = self.point_mobs

        self.change_point_mobs_to_angles([0.9*np.pi])
        push_pin.move_to(P1.get_center(), DOWN)
        push_pin.save_state()
        push_pin.shift(UP)
        push_pin.fade(1)
        self.play(push_pin.restore)
        for angle in [0.89999*np.pi, -0.09999*np.pi, 0.4*np.pi]:
            self.change_point_mobs_to_angles(
                [0.9*np.pi, angle],
                run_time = 4,
            )
        self.play(FadeOut(self.average[-1]))

    def overall_probability(self):
        point_mobs = self.point_mobs
        triangle = self.triangle

        one_fourth = TexMobject("1/4")
        one_fourth.set_color(BLUE)
        one_fourth.next_to(self.question, DOWN)

        self.triangle_update.update(1)
        self.play(
            FadeIn(triangle),
            Animation(point_mobs)
        )
        self.update_animations.append(self.triangle_update)
        self.have_p3_jump_around_randomly(8, wait_time = 0.25)
        self.play(ReplacementTransform(
            self.proportion.copy(), VGroup(one_fourth)
        ))
        self.have_p3_jump_around_randomly(32, wait_time = 0.25)

    #####

    def get_point_mobs(self):
        points = np.array([
            self.center + rotate_vector(self.radius*RIGHT, theta)
            for theta in 2*np.pi*np.random.random(3)
        ])
        for index in 0, 1, 0:
            if self.points_contain_center(points):
                break
            points[index] -= self.center
            points[index] *= -1
            points[index] += self.center
        point_mobs = self.point_mobs = VGroup(*[
            Dot().move_to(point) for point in points            
        ])
        point_mobs.set_color(self.point_color)
        return point_mobs

    def get_point_mob_labels(self):
        point_labels = VGroup(*[
            TexMobject("P_%d"%(i+1))
            for i in range(len(self.point_mobs))
        ])
        point_labels.set_color(self.point_mobs.get_color())
        self.point_labels = point_labels
        return point_labels

    def get_triangle(self):
        triangle = self.triangle = RegularPolygon(n = 3)
        triangle.set_fill(WHITE, opacity = self.triangle_fill_opacity)
        return triangle

    def get_center_lines(self):
        angles = self.get_point_mob_angles()
        lines = VGroup()
        for angle in angles[:2]:
            line = DashedLine(
                self.radius*RIGHT, self.radius*LEFT
            )
            line.rotate(angle)
            line.shift(self.center)
            line.set_color(self.point_color)
            lines.add(line)
        self.center_lines = lines
        return lines

    def get_labels_update(self, point_mobs, labels):
        def update_labels(labels):
            for point_mob, label in zip(point_mobs, labels):
                label.move_to(point_mob)
                vect = point_mob.get_center() - self.center
                vect /= get_norm(vect)
                label.shift(MED_LARGE_BUFF*vect)
            return labels
        return UpdateFromFunc(labels, update_labels)

    def get_triangle_update(self, point_mobs, triangle):
        def update_triangle(triangle):
            points = [pm.get_center() for pm in point_mobs]
            triangle.set_points_as_corners(points)
            if self.points_contain_center(points):
                triangle.set_color(self.positive_triangle_color)
            else:
                triangle.set_color(self.negative_triangle_color)
            return triangle
        return UpdateFromFunc(triangle, update_triangle)

    def get_center_lines_update(self, point_mobs, center_lines):
        def update_lines(center_lines):
            for point_mob, line in zip(point_mobs, center_lines):
                point = point_mob.get_center() - self.center
                line.rotate_in_place(
                    angle_of_vector(point) - line.get_angle()
                )
                line.move_to(self.center)
            return center_lines
        return UpdateFromFunc(center_lines, update_lines)

    def get_arcs_update(self, all_arcs):
        def update_arcs(arcs):
            new_arcs = self.get_all_arcs()
            Transform(arcs, new_arcs).update(1)
            return arcs
        return UpdateFromFunc(all_arcs, update_arcs)

    def get_all_arcs(self):
        angles = self.get_point_mob_angles()
        all_arcs = VGroup()
        for da0, da1 in it.product(*[[0, np.pi]]*2):
            arc_angle = (angles[1]+da1) - (angles[0]+da0)
            arc_angle = (arc_angle+np.pi)%(2*np.pi)-np.pi
            arc = Arc(
                start_angle = angles[0]+da0,
                angle = arc_angle,
                radius = self.radius,
                stroke_width = 5,
            )
            arc.shift(self.center)
            all_arcs.add(arc)
        all_arcs.set_color_by_gradient(RED, MAROON_B, PINK, BLUE)
        self.all_arcs = all_arcs
        return all_arcs

    def points_contain_center(self, points):
        p0, p1, p2 = points
        v1 = p1 - p0
        v2 = p2 - p0
        c = self.center - p0
        M = np.matrix([v1[:2], v2[:2]]).T
        M_inv = np.linalg.inv(M)
        coords = np.dot(M_inv, c[:2])
        return np.all(coords > 0) and (np.sum(coords.flatten()) <= 1)

    def get_point_mob_theta_change_anim(self, point_mob, d_theta):
        curr_theta = angle_of_vector(point_mob.get_center() - self.center)
        d_theta = (d_theta + np.pi)%(2*np.pi) - np.pi
        new_theta = curr_theta + d_theta

        def update_point(point_mob, alpha):
            theta = interpolate(curr_theta, new_theta, alpha)
            point_mob.move_to(self.center + self.radius*(
                np.cos(theta)*RIGHT + np.sin(theta)*UP
            ))
            return point_mob
        return UpdateFromAlphaFunc(point_mob, update_point, run_time = 2)

    def change_point_mobs(self, d_thetas, *added_anims, **kwargs):
        anims = it.chain(
            self.update_animations,
            [
                self.get_point_mob_theta_change_anim(pm, dt)
                for pm, dt in zip(self.point_mobs, d_thetas)
            ],
            added_anims
        )
        self.play(*anims, **kwargs)
        for update in self.update_animations:
            update.update(1)
 
    def change_point_mobs_randomly(self, *added_anims, **kwargs):
        d_thetas = 2*np.pi*np.random.random(len(self.point_mobs))
        self.change_point_mobs(d_thetas, *added_anims, **kwargs)

    def change_point_mobs_to_angles(self, target_angles, *added_anims, **kwargs):
        angles = self.get_point_mob_angles()
        n_added_targets = len(angles) - len(target_angles)
        target_angles = list(target_angles) + list(angles[-n_added_targets:])
        self.change_point_mobs(
            [ta-a for a, ta in zip(angles, target_angles)],
            *added_anims, **kwargs
        )

    def get_point_mob_angles(self):
        point_mobs = self.point_mobs
        points = [pm.get_center() - self.center for pm in point_mobs]
        return np.array(list(map(angle_of_vector, points)))

    def have_p3_jump_around_randomly(self, n_jumps, wait_time = 0.75, run_time = 0):
        for x in range(n_jumps):
            self.change_point_mobs(
                [0, 0, 2*np.pi*random.random()],
                run_time = run_time
            )
            self.wait(wait_time)

class FixThreePointsOnSphere(ExternallyAnimatedScene):
    pass

class AddCenterLinesAndPlanesToSphere(ExternallyAnimatedScene):
    pass

class AverageSizeOfSphericalTriangleSection(ExternallyAnimatedScene):
    pass

class AverageSizeOfSphericalTriangleSectionSupplement(Scene):
    def construct(self):
        words = TextMobject(
            "Average size of \\\\", "this section", "?",
            arg_separator = ""
        )
        words.set_color_by_tex("section", GREEN)
        words.set_width(FRAME_WIDTH - 1)
        words.to_edge(DOWN)
        self.play(Write(words))
        self.wait(3)

class TryASurfaceIntegral(TeacherStudentsScene):
    def construct(self):
        self.student_says("Can you do \\\\ a surface integral?")
        self.change_student_modes("confused", "raise_left_hand", "confused")
        self.wait()
        self.teacher_says(
            "I mean...you can \\emph{try}",
            target_mode = "sassy",
        )
        self.wait(2)

class RevisitTwoDCase(TwoDCase):
    CONFIG = {
        "random_seed" : 4,
        "center" : 3*LEFT + 0.5*DOWN,
        "radius" : 2,
        "n_random_trials" : 200,
    }
    def construct(self):
        self.force_skipping()

        self.setup_circle()
        self.show_probability()
        self.add_lines_and_comment_on_them()
        self.rewrite_random_procedure()
        self.four_possibilities_for_coin_flips()

    def setup_circle(self):
        point_mobs = self.get_point_mobs()
        point_labels = self.get_point_mob_labels()
        triangle = self.get_triangle()
        circle = Circle(radius = self.radius, color = WHITE)
        center_dot = Dot(color = self.center_color)
        VGroup(circle, center_dot).shift(self.center)

        self.point_labels_update = self.get_labels_update(point_mobs, point_labels)
        self.triangle_update = self.get_triangle_update(point_mobs, triangle)
        self.update_animations = [
            self.triangle_update,
            self.point_labels_update,
        ]
        for anim in self.update_animations:
            anim.update(1)

        self.add(
            center_dot, circle, triangle, 
            point_mobs, point_labels
        )
        self.add_foreground_mobjects(center_dot)
        self.set_variables_as_attrs(circle, center_dot)

    def show_probability(self):
        title = TexMobject(
            "P(\\text{triangle contains the center})",
            "=", "1/4"
        )
        title.to_edge(UP, buff = MED_SMALL_BUFF)
        title.set_color_by_tex("1/4", BLUE)
        four = title[-1][-1]
        four_circle = Circle(color = YELLOW)
        four_circle.replace(four, dim_to_match = 1)
        four_circle.scale_in_place(1.2)

        self.n_in = 0
        self.n_out = 0
        frac = TexMobject(
            "{0", "\\over", "\\quad 0", "+", "0 \\quad}", "="
        )
        placeholders = frac.get_parts_by_tex("0")
        positions = [ORIGIN, RIGHT, LEFT]
        frac.next_to(self.circle, RIGHT, 1.5*LARGE_BUFF)

        def place_random_triangles(n, wait_time):
            for x in range(n):
                self.change_point_mobs_randomly(run_time = 0)
                contain_center = self.points_contain_center(
                    [pm.get_center() for pm in self.point_mobs]
                )
                if contain_center:
                    self.n_in += 1
                else:
                    self.n_out += 1
                nums = list(map(Integer, [self.n_in, self.n_in, self.n_out]))
                VGroup(*nums[:2]).set_color(self.positive_triangle_color)
                VGroup(*nums[2:]).set_color(self.negative_triangle_color)
                for num, placeholder, position in zip(nums, placeholders, positions):
                    num.move_to(placeholder, position)
                decimal = DecimalNumber(float(self.n_in)/(self.n_in + self.n_out))
                decimal.next_to(frac, RIGHT, SMALL_BUFF)

                self.add(decimal, *nums)
                self.wait(wait_time)
                self.remove(decimal, *nums)
            return VGroup(decimal, *nums)


        self.play(Write(title))
        self.add(frac)
        self.remove(*placeholders)
        place_random_triangles(10, 0.25)
        nums = place_random_triangles(self.n_random_trials, 0.05)
        self.add(nums)
        self.wait()
        self.play(*list(map(FadeOut, [frac, nums, title])))

    def add_lines_and_comment_on_them(self):
        center_lines = self.get_center_lines()
        center_lines.save_state()
        center_line_shadows = center_lines.copy()
        center_line_shadows.set_stroke(LIGHT_GREY, 2)
        arcs = self.get_all_arcs()

        center_lines.generate_target()
        center_lines.target.to_edge(RIGHT, buff = LARGE_BUFF)
        rect = SurroundingRectangle(center_lines.target, buff = MED_SMALL_BUFF)
        rect.set_stroke(WHITE, 2)

        words1 = TextMobject("Helpful new objects")
        words2 = TextMobject("Reframe problem around these")
        for words in words1, words2:
            words.scale(0.8)
            words.next_to(rect, UP)
            words.shift_onto_screen()

        self.play(LaggedStartMap(ShowCreation, center_lines, run_time = 1))
        self.play(
            LaggedStartMap(FadeIn, arcs, run_time = 1),
            Animation(self.point_mobs),
        )
        self.wait()
        self.add(center_line_shadows)
        self.play(MoveToTarget(center_lines))
        self.play(ShowCreation(rect), Write(words1))
        self.wait(2)
        self.play(ReplacementTransform(words1, words2))
        self.wait(2)
        self.play(
            center_lines.restore,
            center_lines.fade, 1,
            *list(map(FadeOut, [
                rect, words2, center_line_shadows,
                self.triangle, arcs,
                self.point_mobs,
                self.point_labels,
            ]))
        )
        center_lines.restore()
        self.remove(center_lines)

    def rewrite_random_procedure(self):
        point_mobs = self.point_mobs
        center_lines = self.center_lines 

        random_procedure = TextMobject("Random procedure")
        underline = Line(LEFT, RIGHT)
        underline.stretch_to_fit_width(random_procedure.get_width())
        underline.scale(1.1)
        underline.next_to(random_procedure, DOWN)
        group = VGroup(random_procedure, underline)
        group.to_corner(UP+RIGHT)

        words = VGroup(*list(map(TextMobject, [
            "Choose 3 random points",
            "Choose 2 random lines",
            "Flip coin for each line \\\\ to get $P_1$ and $P_2$",
            "Choose $P_3$ at random"
        ])))
        words.scale(0.8)
        words.arrange(DOWN, buff = MED_LARGE_BUFF)
        words.next_to(underline, DOWN)
        words[1].set_color(YELLOW)

        point_label_groups = VGroup()
        for point_mob, label in zip(self.point_mobs, self.point_labels):
            group = VGroup(point_mob, label)
            group.save_state()
            group.move_to(words[0], LEFT)
            group.fade(1)
            point_label_groups.add(group)
        self.point_label_groups = point_label_groups

        cross = Cross(words[0])
        cross.set_stroke(RED, 6)

        self.center_lines_update = self.get_center_lines_update(
            point_mobs, center_lines
        )
        self.update_animations.append(self.center_lines_update)
        self.update_animations.remove(self.triangle_update)

        #Choose random points
        self.play(
            Write(random_procedure),
            ShowCreation(underline)
        )
        self.play(FadeIn(words[0]))
        self.play(LaggedStartMap(
            ApplyMethod, point_label_groups,
            lambda mob : (mob.restore,),
        ))
        self.play(
            ShowCreation(cross), 
            point_label_groups.fade, 1,
        )
        self.wait()

        #Choose two random lines
        self.center_lines_update.update(1)
        self.play(
            FadeIn(words[1]),
            LaggedStartMap(GrowFromCenter, center_lines)
        )
        for x in range(3):
            self.change_point_mobs_randomly(run_time = 1)
        self.change_point_mobs_to_angles([0.8*np.pi, 1.3*np.pi])

        #Flip a coin for each line
        def flip_point_label_back_and_forth(point_mob, label):
            for x in range(6):
                point_mob.rotate(np.pi, about_point = self.center)
                self.point_labels_update.update(1)
                self.wait(0.5)
            self.wait(0.5)

        def choose_p1_and_p2():
            for group in point_label_groups[:2]:
                group.set_fill(self.point_color, 1)
                flip_point_label_back_and_forth(*group)

        choose_p1_and_p2()
        self.play(Write(words[2]))

        #Seems convoluted
        randy = Randolph().flip()
        randy.scale(0.5)
        randy.to_edge(DOWN)
        randy.shift(2*RIGHT)

        self.play(point_label_groups.fade, 1)
        self.change_point_mobs_randomly(run_time = 1)
        choose_p1_and_p2()
        point_label_groups.fade(1)
        self.change_point_mobs_randomly(FadeIn(randy))
        self.play(
            PiCreatureSays(
                randy, "Seems \\\\ convoluted",
                bubble_kwargs = {"height" : 2, "width" : 2},
                target_mode = "confused"
            )
        )
        choose_p1_and_p2()
        self.play(
            FadeOut(randy.bubble),
            FadeOut(randy.bubble.content),
            randy.change, "pondering",
        )
        self.play(Blink(randy))
        self.play(FadeOut(randy))

        #Choosing the third point
        self.change_point_mobs([0, 0, -np.pi/2], run_time = 0)
        p3_group = point_label_groups[2]
        p3_group.save_state()
        p3_group.move_to(words[3], LEFT)

        self.play(Write(words[3], run_time = 1))
        self.play(
            p3_group.restore,
            p3_group.set_fill, YELLOW, 1
        )
        self.wait()
        self.play(Swap(*words[2:4]))
        self.wait()

        #Once the continuous randomness is handled
        rect = SurroundingRectangle(VGroup(words[1], words[3]))
        rect.set_stroke(WHITE, 2)
        brace = Brace(words[2], DOWN)
        brace_text = brace.get_text("4 equally likely outcomes")
        brace_text.scale_in_place(0.8)

        self.play(ShowCreation(rect))
        self.play(GrowFromCenter(brace))
        self.play(Write(brace_text))
        self.wait()

        self.random_procedure_words = words

    def four_possibilities_for_coin_flips(self):
        arcs = self.all_arcs
        point_mobs = self.point_mobs
        arc = arcs[-1]
        point_label_groups = self.point_label_groups
        arc_update = self.get_arcs_update(arcs)
        arc_update.update(1)
        self.update_animations.append(arc_update)

        def second_arc_update_func(arcs):
            VGroup(*arcs[:-1]).set_stroke(width = 0)
            arcs[-1].set_stroke(PINK, 6)
            return arcs
        second_arc_update = UpdateFromFunc(arcs, second_arc_update_func)
        second_arc_update.update(1)
        self.update_animations.append(second_arc_update)
        self.update_animations.append(Animation(point_label_groups))

        def do_the_rounds():
            for index in 0, 1, 0, 1:
                point_mob = point_mobs[index]
                point_mob.generate_target()
                point_mob.target.rotate(
                    np.pi, about_point = self.center,
                )
                self.play(
                    MoveToTarget(point_mob),
                    *self.update_animations,
                    run_time = np.sqrt(2)/4 #Hacky reasons to be irrational
                )
                self.wait()

        self.revert_to_original_skipping_status()
        do_the_rounds()
        self.triangle_update.update(1)
        self.remove(arcs)
        self.update_animations.remove(arc_update)
        self.update_animations.remove(second_arc_update)
        self.play(FadeIn(self.triangle))
        self.wait()
        self.update_animations.insert(0, self.triangle_update)
        do_the_rounds()
        self.wait()
        self.change_point_mobs_randomly()
        for x in range(2):
            do_the_rounds()

class ThisIsWhereItGetsGood(TeacherStudentsScene):
    def construct(self):
        self.teacher_says(
            "This is where \\\\ things get good",
            target_mode = "hooray"
        )
        self.change_student_modes(*["hooray"]*3)
        self.wait(2)

class ContrastTwoRandomProcesses(TwoDCase):
    CONFIG = {
        "radius" : 1.5,
        "random_seed" : 0,
    }
    def construct(self):
        circle = Circle(color = WHITE, radius = self.radius)
        point_mobs = self.get_point_mobs()
        for point in point_mobs:
            point.scale_in_place(1.5)
            point.set_stroke(RED, 1)
        labels = self.get_point_mob_labels()
        self.get_labels_update(point_mobs, labels).update(1)
        center_lines = self.get_center_lines()
        point_label_groups = VGroup(*[
            VGroup(*pair) for pair in zip(point_mobs, labels)
        ])

        right_circles = VGroup(*[
            VGroup(circle, *point_label_groups[:i+1]).copy()
            for i in range(3)
        ])
        left_circles = VGroup(
            VGroup(circle, center_lines).copy(),
            VGroup(
                circle, center_lines, 
                point_label_groups[2]
            ).copy(),
            VGroup(
                circle, center_lines, 
                *point_label_groups[2::-1]
            ).copy(),
        )
        for circles in left_circles, right_circles:
            circles.scale(0.5)
            circles[0].to_edge(UP, buff = MED_LARGE_BUFF)
            circles[2].to_edge(DOWN, buff = MED_LARGE_BUFF)
            for c1, c2 in zip(circles, circles[1:]):
                circles.add(Arrow(c1[0], c2[0], color = GREEN))
        left_circles.shift(3*LEFT)
        right_circles.shift(3*RIGHT)

        vs = TextMobject("vs.")
        self.show_creation_of_circle_group(left_circles)
        self.play(Write(vs))
        self.show_creation_of_circle_group(right_circles)
        self.wait()

    def show_creation_of_circle_group(self, group):
        circles = group[:3]
        arrows = group[3:]

        self.play(
            ShowCreation(circles[0][0]),
            FadeIn(VGroup(*circles[0][1:])),
        )
        for c1, c2, arrow in zip(circles, circles[1:], arrows):
            self.play(
                GrowArrow(arrow),
                ApplyMethod(
                    c1.copy().shift, 
                    c2[0].get_center() - c1[0].get_center(),
                    remover = True
                )
            )
            self.add(c2)
            n = len(c2) - len(c1)
            self.play(*list(map(GrowFromCenter, c2[-n:])))

class Rewrite3DRandomProcedure(Scene):
    def construct(self):
        random_procedure = TextMobject("Random procedure")
        underline = Line(LEFT, RIGHT)
        underline.stretch_to_fit_width(random_procedure.get_width())
        underline.scale(1.1)
        underline.next_to(random_procedure, DOWN)
        group = VGroup(random_procedure, underline)
        group.to_corner(UP+LEFT)
        
        words = VGroup(*list(map(TextMobject, [
            "Choose 4 random points",
            "Choose 3 random lines",
            "Choose $P_4$ at random",
            "Flip coin for each line \\\\ to get $P_1$, $P_2$, $P_3$",
        ])))
        words.scale(0.8)
        words.arrange(DOWN, buff = MED_LARGE_BUFF)
        words.next_to(underline, DOWN)
        words[1].set_color(YELLOW)
        cross = Cross(words[0])
        cross.set_stroke(RED, 6)

        self.play(
            Write(random_procedure),
            ShowCreation(underline)
        )
        self.play(FadeIn(words[0]))
        self.play(ShowCreation(cross))
        self.wait()
        self.play(LaggedStartMap(FadeIn, words[1]))
        self.play(LaggedStartMap(FadeIn, words[2]))
        self.wait(2)
        self.play(Write(words[3]))
        self.wait(3)

class AntipodalViewOfThreeDCase(ExternallyAnimatedScene):
    pass

class ThreeDAnswer(Scene):
    def construct(self):
        words = TextMobject(
            "Probability that the tetrahedron contains center:", 
            "$\\frac{1}{8}$"
        )
        words.set_width(FRAME_WIDTH - 1)
        words.to_edge(DOWN)
        words[1].set_color(BLUE)

        self.play(Write(words))
        self.wait(2)

class FormalWriteupScreenCapture(ExternallyAnimatedScene):
    pass

class Formality(TeacherStudentsScene):
    def construct(self):
        words = TextMobject(
            "Write-up by Ralph Howard and Paul Sisson (link below)"
        )
        words.scale(0.7)
        words.to_corner(UP+LEFT, buff = MED_SMALL_BUFF)

        self.student_says(
            "How would you \\\\ write that down?",
            target_mode = "sassy"
        )
        self.change_student_modes("confused", "sassy", "erm")
        self.wait()
        self.play(
            Write(words),
            FadeOut(self.students[1].bubble),
            FadeOut(self.students[1].bubble.content),
            self.teacher.change, "raise_right_hand"
        )
        self.change_student_modes(
            *["pondering"]*3,
            look_at_arg = words
        )
        self.wait(8)

class ProblemSolvingTakeaways(Scene):
    def construct(self):
        title = TextMobject("Problem solving takeaways")
        underline = Line(LEFT, RIGHT)
        underline.set_width(title.get_width()*1.1)
        underline.next_to(title, DOWN)
        group = VGroup(title, underline)
        group.to_corner(UP+LEFT)

        points = VGroup(*[
            TextMobject(string, alignment = "")
            for string in [
                "Ask a simpler version \\\\ of the question",
                "Try reframing the question \\\\ around new constructs",
            ]
        ])
        points[0].set_color(BLUE)
        points[1].set_color(YELLOW)
        points.arrange(
            DOWN, buff = LARGE_BUFF,
            aligned_edge = LEFT
        )
        points.next_to(group, DOWN, LARGE_BUFF)

        self.play(Write(title), ShowCreation(underline))
        self.wait()
        for point in points:
            self.play(Write(point))
            self.wait(3)

class BrilliantPuzzle(PiCreatureScene):
    CONFIG = {
        "random_seed" : 2,
    }
    def construct(self):
        students = self.students
        tests = VGroup()
        for student in students:
            test = self.get_test()
            test.move_to(0.75*student.get_center())
            tests.add(test)
            student.test = test
        for i, student in enumerate(students):
            student.right = students[(i+1)%len(students)]
            student.left = students[(i-1)%len(students)]
        arrows = VGroup()
        for s1, s2 in adjacent_pairs(self.students):
            arrow = Arrow(
                s1.get_center(), s2.get_center(), 
                path_arc = np.pi/2,
                buff = 0.8
            )
            arrow.tip.shift(SMALL_BUFF*arrow.get_vector())
            arrow.tip.shift(-0.1*SMALL_BUFF*arrow.tip.get_center())
            # arrow.shift(-MED_SMALL_BUFF*arrow.get_vector())
            arrow.set_color(RED)
            arrow.pointing_right = True
            arrows.add(arrow)
            s1.arrow = arrow
            arrow.student = s1

        title = TextMobject("Puzzle from Brilliant")
        title.scale(0.75)
        title.to_corner(UP+LEFT)

        question = TextMobject("Expected number of \\\\ circled students?")
        question.to_corner(UP+RIGHT)

        self.remove(students)
        self.play(Write(title))
        self.play(LaggedStartMap(GrowFromCenter, students))
        self.play(
            LaggedStartMap(Write, tests),
            LaggedStartMap(
                ApplyMethod, students,
                lambda m : (m.change, "horrified", m.test)
            )
        )
        self.wait()
        self.play(LaggedStartMap(
            ApplyMethod, students,
            lambda m : (m.change, "conniving")
        ))
        self.play(LaggedStartMap(ShowCreation, arrows))
        for x in range(2):
            self.swap_arrows_randomly(arrows)
        self.wait()
        circles = self.circle_students()
        self.play(Write(question))
        for x in range(10):
            self.swap_arrows_randomly(arrows, FadeOut(circles))
            circles = self.circle_students()
            self.wait()

    ####

    def get_test(self):
        lines = VGroup(*[Line(ORIGIN, 0.5*RIGHT) for x in range(6)])
        lines.arrange(DOWN, buff = SMALL_BUFF)
        rect = SurroundingRectangle(lines)
        rect.set_stroke(WHITE)
        lines.set_stroke(WHITE, 2)
        test = VGroup(rect, lines)
        test.set_height(0.5)
        return test

    def create_pi_creatures(self):
        self.students = VGroup(*[
            PiCreature(
                color = random.choice([BLUE_C, BLUE_D, BLUE_E, GREY_BROWN])
            ).scale(0.25).move_to(3*vect)
            for vect in compass_directions(8)
        ])
        return self.students

    def get_arrow_swap_anim(self, arrow):
        arrow.generate_target()
        if arrow.pointing_right:
            target_color = GREEN
            target_angle = np.pi - np.pi/4
        else:
            target_color = RED
            target_angle = np.pi + np.pi/4
        arrow.target.set_color(target_color)
        arrow.target.rotate(
            target_angle, 
            about_point = arrow.student.get_center()
        )
        arrow.pointing_right = not arrow.pointing_right
        return MoveToTarget(arrow, path_arc = np.pi)

    def swap_arrows_randomly(self, arrows, *added_anims):
        anims = []
        for arrow in arrows:
            if random.choice([True, False]):
                anims.append(self.get_arrow_swap_anim(arrow))
        self.play(*anims + list(added_anims))

    def circle_students(self):
        circles = VGroup()
        circled_students = list(self.students)
        for student in self.students:
            if student.arrow.pointing_right:
                to_remove = student.right
            else:
                to_remove = student.left
            if to_remove in circled_students:
                circled_students.remove(to_remove)
        for student in circled_students:
            circle = Circle(color = YELLOW)
            circle.set_height(1.2*student.get_height())
            circle.move_to(student)
            circles.add(circle)
            self.play(ShowCreation(circle))
        return circles

class ScrollThroughBrilliantCourses(ExternallyAnimatedScene):
    pass

class BrilliantProbability(ExternallyAnimatedScene):
    pass

class Promotion(PiCreatureScene):
    CONFIG = {
        "seconds_to_blink" : 5,
    }
    def construct(self):
        url = TextMobject("https://brilliant.org/3b1b/")
        url.to_corner(UP+LEFT)

        rect = Rectangle(height = 9, width = 16)
        rect.set_height(5.5)
        rect.next_to(url, DOWN)
        rect.to_edge(LEFT)

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

        self.remove(rect)
        self.play(
            url.next_to, self.pi_creature, UP+LEFT
        )
        url_rect = SurroundingRectangle(url)
        self.play(ShowCreation(url_rect))
        self.play(FadeOut(url_rect))
        self.wait(3)

class AddedPromoWords(Scene):
    def construct(self):
        words = TextMobject(
            "First", "$2^8$", "vistors get",
            "$(e^\\pi - \\pi)\\%$", "off"
        )
        words.set_width(FRAME_WIDTH - 1)
        words.to_edge(DOWN)
        words.set_color_by_tex("2^8", YELLOW)
        words.set_color_by_tex("pi", PINK)

        self.play(Write(words))
        self.wait()

class PatreonThanks(PatreonEndScreen):
    CONFIG = {
        "specific_patrons" : [
            "Randall Hunt",
            "Burt Humburg",
            "CrypticSwarm",
            "Juan Benet",
            "David Kedmey",
            "Marcus Schiebold",
            "Ali Yahya",
            "Mayank M. Mehrotra",
            "Lukas Biewald",
            "Yana Chernobilsky",
            "Kaustuv DeBiswas",
            "Kathryn Schmiedicke",
            "Yu Jun",
            "Dave Nicponski",
            "Damion Kistler",
            "Jordan Scales",
            "Markus Persson",
            "Egor Gumenuk",
            "Yoni Nazarathy",
            "Ryan Atallah",
            "Joseph John Cox",
            "Luc Ritchie",
            "James Park",
            "Samantha D. Suplee",
            "Delton",
            "Thomas Tarler",
            "Jake Alzapiedi",
            "Jonathan Eppele",
            "Taro Yoshioka",
            "1stViewMaths",
            "Jacob Magnuson",
            "Mark Govea",
            "Dagan Harrington",
            "Clark Gaebel",
            "Eric Chow",
            "Mathias Jansson",
            "David Clark",
            "Michael Gardner",
            "Erik Sundell",
            "Awoo",
            "Dr. David G. Stork",
            "Tianyu Ge",
            "Ted Suzman",
            "Linh Tran",
            "Andrew Busey",
            "John Haley",
            "Ankalagon",
            "Eric Lavault",
            "Boris Veselinovich",
            "Julian Pulgarin",
            "Jeff Linse",
            "Cooper Jones",
            "Ryan Dahl",
            "Robert Teed",
            "Jason Hise",
            "Meshal Alshammari",
            "Bernd Sing",
            "Mustafa Mahdi",
            "Mathew Bramson",
            "Jerry Ling",
            "Shimin Kuang",
            "Rish Kundalia",
            "Achille Brighton",
            "Ripta Pasay",
        ]
    }


























