
from big_ol_pile_of_manim_imports import *

import mpmath
mpmath.mp.dps = 7


def zeta(z):
    max_norm = FRAME_X_RADIUS
    try:
        return np.complex(mpmath.zeta(z))
    except:
        return np.complex(max_norm, 0)

def d_zeta(z):
    epsilon = 0.01
    return (zeta(z + epsilon) - zeta(z))/epsilon

class ZetaTransformationScene(ComplexTransformationScene):
    CONFIG = {
        "anchor_density" : 35,
        "min_added_anchors" : 10,
        "max_added_anchors" : 300,
        "num_anchors_to_add_per_line" : 75,
        "post_transformation_stroke_width" : 2,
        "default_apply_complex_function_kwargs" : {
            "run_time" : 5,
        },
        "x_min" : 1,
        "x_max" : int(FRAME_X_RADIUS+2),
        "extra_lines_x_min" : -2,
        "extra_lines_x_max" : 4,
        "extra_lines_y_min" : -2,
        "extra_lines_y_max" : 2,
    }
    def prepare_for_transformation(self, mob):
        for line in mob.family_members_with_points():
            #Find point of line cloest to 1 on C
            if not isinstance(line, Line):
                line.insert_n_curves(self.min_added_anchors)
                continue
            p1 = line.get_start()+LEFT
            p2 = line.get_end()+LEFT
            t = (-np.dot(p1, p2-p1))/(get_norm(p2-p1)**2)
            closest_to_one = interpolate(
                line.get_start(), line.get_end(), t
            )
            #See how big this line will become
            diameter = abs(zeta(complex(*closest_to_one[:2])))
            target_num_curves = np.clip(
                int(self.anchor_density*np.pi*diameter),
                self.min_added_anchors,
                self.max_added_anchors,
            )
            num_curves = line.get_num_curves()
            if num_curves < target_num_curves:
                line.insert_n_curves(target_num_curves-num_curves)
            line.make_smooth()

    def add_extra_plane_lines_for_zeta(self, animate = False, **kwargs):
        dense_grid = self.get_dense_grid(**kwargs)
        if animate:
            self.play(ShowCreation(dense_grid))
        self.plane.add(dense_grid)
        self.add(self.plane)

    def get_dense_grid(self, step_size = 1./16):
        epsilon = 0.1
        x_range = np.arange(
            max(self.x_min, self.extra_lines_x_min),
            min(self.x_max, self.extra_lines_x_max),
            step_size
        )
        y_range = np.arange(
            max(self.y_min, self.extra_lines_y_min),
            min(self.y_max, self.extra_lines_y_max),
            step_size
        )
        vert_lines = VGroup(*[
            Line(
                self.y_min*UP,
                self.y_max*UP,
            ).shift(x*RIGHT)
            for x in x_range
            if abs(x-1) > epsilon
        ])
        vert_lines.set_color_by_gradient(
            self.vert_start_color, self.vert_end_color
        )
        horiz_lines = VGroup(*[
            Line(
                self.x_min*RIGHT,
                self.x_max*RIGHT,
            ).shift(y*UP)
            for y in y_range
            if abs(y) > epsilon
        ])
        horiz_lines.set_color_by_gradient(
            self.horiz_start_color, self.horiz_end_color
        )
        dense_grid = VGroup(horiz_lines, vert_lines)
        dense_grid.set_stroke(width = 1)
        return dense_grid

    def add_reflected_plane(self, animate = False):
        reflected_plane = self.get_reflected_plane()
        if animate:
            self.play(ShowCreation(reflected_plane, run_time = 5))
        self.plane.add(reflected_plane)
        self.add(self.plane)

    def get_reflected_plane(self):
        reflected_plane = self.plane.copy()
        reflected_plane.rotate(np.pi, UP, about_point = RIGHT)
        for mob in reflected_plane.family_members_with_points():
            mob.set_color(
                Color(rgb = 1-0.5*color_to_rgb(mob.get_color()))
            )
        self.prepare_for_transformation(reflected_plane)
        reflected_plane.submobjects = list(reversed(
            reflected_plane.family_members_with_points()
        ))
        return reflected_plane

    def apply_zeta_function(self, **kwargs):
        transform_kwargs = dict(self.default_apply_complex_function_kwargs)
        transform_kwargs.update(kwargs)
        self.apply_complex_function(zeta, **kwargs)

class TestZetaOnHalfPlane(ZetaTransformationScene):
    CONFIG = {
        "anchor_density" : 15,
    }
    def construct(self):
        self.add_transformable_plane()
        self.add_extra_plane_lines_for_zeta()
        self.prepare_for_transformation(self.plane)
        print(sum([
            mob.get_num_points()
            for mob in self.plane.family_members_with_points()
        ]))
        print(len(self.plane.family_members_with_points()))
        self.apply_zeta_function()
        self.wait()

class TestZetaOnFullPlane(ZetaTransformationScene):
    def construct(self):
        self.add_transformable_plane(animate = True)
        self.add_extra_plane_lines_for_zeta(animate = True)
        self.add_reflected_plane(animate = True)
        self.apply_zeta_function()


class TestZetaOnLine(ZetaTransformationScene):
    def construct(self):
        line = Line(UP+20*LEFT, UP+20*RIGHT)
        self.add_transformable_plane()
        self.plane.submobjects = [line]
        self.apply_zeta_function()
        self.wait(2)
        self.play(ShowCreation(line, run_time = 10))
        self.wait(3)

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

class IntroduceZeta(ZetaTransformationScene):
    CONFIG = {
        "default_apply_complex_function_kwargs" : {
            "run_time" : 8,
        }
    }
    def construct(self):
        title = TextMobject("Riemann zeta function")
        title.add_background_rectangle()
        title.to_corner(UP+LEFT)
        func_mob = VGroup(
            TexMobject("\\zeta(s) = "),
            TexMobject("\\sum_{n=1}^\\infty \\frac{1}{n^s}")
        )
        func_mob.arrange(RIGHT, buff = 0)
        for submob in func_mob:
            submob.add_background_rectangle()
        func_mob.next_to(title, DOWN)

        randy = Randolph().flip()
        randy.to_corner(DOWN+RIGHT)

        self.add_foreground_mobjects(title, func_mob)
        self.add_transformable_plane()
        self.add_extra_plane_lines_for_zeta()
        self.play(ShowCreation(self.plane, run_time = 2))
        reflected_plane = self.get_reflected_plane()
        self.play(ShowCreation(reflected_plane, run_time = 2))
        self.plane.add(reflected_plane)
        self.wait()
        self.apply_zeta_function()
        self.wait(2)
        self.play(FadeIn(randy))
        self.play(
            randy.change_mode, "confused",
            randy.look_at, func_mob,
        )
        self.play(Blink(randy))
        self.wait()

class WhyPeopleMayKnowIt(TeacherStudentsScene):
    def construct(self):
        title = TextMobject("Riemann zeta function")
        title.to_corner(UP+LEFT)
        func_mob = TexMobject(
            "\\zeta(s) = \\sum_{n=1}^\\infty \\frac{1}{n^s}"
        )
        func_mob.next_to(title, DOWN, aligned_edge = LEFT)
        self.add(title, func_mob)

        mercenary_thought = VGroup(
            TexMobject("\\$1{,}000{,}000").set_color_by_gradient(GREEN_B, GREEN_D),
            TexMobject("\\zeta(s) = 0")
        )
        mercenary_thought.arrange(DOWN)
        divergent_sum = VGroup(
            TexMobject("1+2+3+4+\\cdots = -\\frac{1}{12}"),
            TexMobject("\\zeta(-1) = -\\frac{1}{12}")
        )
        divergent_sum.arrange(DOWN)
        divergent_sum[0].set_color_by_gradient(YELLOW, MAROON_B)
        divergent_sum[1].set_color(BLACK)

        #Thoughts
        self.play(*it.chain(*[
            [pi.change_mode, "pondering", pi.look_at, func_mob]
            for pi in self.get_pi_creatures()
        ]))
        self.random_blink()
        self.student_thinks(
            mercenary_thought, student_index = 2,
            target_mode = "surprised",
        )
        student = self.get_students()[2]
        self.random_blink()
        self.wait(2)
        self.student_thinks(
            divergent_sum, student_index = 1,
            added_anims = [student.change_mode, "plain"]
        )
        student = self.get_students()[1]
        self.play(
            student.change_mode, "confused",
            student.look_at, divergent_sum,
        )
        self.random_blink()
        self.play(*it.chain(*[
            [pi.change_mode, "confused", pi.look_at, divergent_sum]
            for pi in self.get_pi_creatures()
        ]))
        self.wait()
        self.random_blink()
        divergent_sum[1].set_color(WHITE)
        self.play(Write(divergent_sum[1]))
        self.random_blink()
        self.wait()

        #Ask about continuation
        self.student_says(
            TextMobject("Can you explain \\\\" , "``analytic continuation''?"),
            student_index = 1,
            target_mode = "raise_right_hand"
        )
        self.change_student_modes(
            "raise_left_hand",
            "raise_right_hand",
            "raise_left_hand",
        )
        self.play(
            self.get_teacher().change_mode, "happy",
            self.get_teacher().look_at, student.eyes,
        )
        self.random_blink()
        self.wait(2)
        self.random_blink()
        self.wait()

class ComplexValuedFunctions(ComplexTransformationScene):
    def construct(self):
        title = TextMobject("Complex-valued function")
        title.scale(1.5)
        title.add_background_rectangle()
        title.to_edge(UP)
        self.add(title)

        z_in = Dot(UP+RIGHT, color = YELLOW)
        z_out = Dot(4*RIGHT + 2*UP, color = MAROON_B)
        arrow = Arrow(z_in, z_out, buff = 0.1)
        arrow.set_color(WHITE)
        z = TexMobject("z").next_to(z_in, DOWN+LEFT, buff = SMALL_BUFF)
        z.set_color(z_in.get_color())
        f_z = TexMobject("f(z)").next_to(z_out, UP+RIGHT, buff = SMALL_BUFF)
        f_z.set_color(z_out.get_color())

        self.add(z_in, z)
        self.wait()
        self.play(ShowCreation(arrow))
        self.play(
            ShowCreation(z_out),
            Write(f_z)
        )
        self.wait(2)

class PreviewZetaAndContinuation(ZetaTransformationScene):
    CONFIG = {
        "default_apply_complex_function_kwargs" : {
            "run_time" : 4,
        }
    }
    def construct(self):
        self.add_transformable_plane()
        self.add_extra_plane_lines_for_zeta()
        reflected_plane = self.get_reflected_plane()

        titles = [
            TextMobject(
                "What does", "%s"%s,
                "look like?",
                alignment = "",
            )
            for s in [
                "$\\displaystyle \\sum_{n=1}^\\infty \\frac{1}{n^s}$",
                "analytic continuation"
            ]
        ]
        for mob in titles:
            mob[1].set_color(YELLOW)
            mob.to_corner(UP+LEFT, buff = 0.7)
            mob.add_background_rectangle()

        self.remove(self.plane)
        self.play(Write(titles[0], run_time = 2))
        self.add_foreground_mobjects(titles[0])
        self.play(FadeIn(self.plane))
        self.apply_zeta_function()
        reflected_plane.apply_complex_function(zeta)
        reflected_plane.make_smooth()
        reflected_plane.set_stroke(width = 2)
        self.wait()
        self.play(Transform(*titles))
        self.wait()
        self.play(ShowCreation(
            reflected_plane,
            lag_ratio = 0,
            run_time = 2
        ))
        self.wait()

class AssumeKnowledgeOfComplexNumbers(ComplexTransformationScene):
    def construct(self):
        z = complex(5, 2)
        dot = Dot(z.real*RIGHT + z.imag*UP, color = YELLOW)
        line = Line(ORIGIN, dot.get_center(), color = dot.get_color())
        x_line = Line(ORIGIN, z.real*RIGHT, color = GREEN_B)
        y_line = Line(ORIGIN, z.imag*UP, color = RED)
        y_line.shift(z.real*RIGHT)
        complex_number_label = TexMobject(
            "%d+%di"%(int(z.real), int(z.imag))
        )
        complex_number_label[0].set_color(x_line.get_color())
        complex_number_label[2].set_color(y_line.get_color())
        complex_number_label.next_to(dot, UP)

        text = VGroup(
            TextMobject("Assumed knowledge:"),
            TextMobject("1) What complex numbers are."),
            TextMobject("2) How to work with them."),
            TextMobject("3) Maybe derivatives?"),
        )
        text.arrange(DOWN, aligned_edge = LEFT)
        for words in text:
            words.add_background_rectangle()
        text[0].shift(LEFT)
        text[-1].set_color(PINK)
        text.to_corner(UP+LEFT)

        self.play(Write(text[0]))
        self.wait()
        self.play(FadeIn(text[1]))
        self.play(
            ShowCreation(x_line),
            ShowCreation(y_line),
            ShowCreation(VGroup(line, dot)),
            Write(complex_number_label),
        )
        self.play(Write(text[2]))
        self.wait(2)
        self.play(Write(text[3]))
        self.wait()
        self.play(text[3].fade)

class DefineForRealS(PiCreatureScene):
    def construct(self):
        zeta_def, s_group = self.get_definition("s")

        self.initial_definition(zeta_def)
        self.plug_in_two(zeta_def)
        self.plug_in_three_and_four(zeta_def)
        self.plug_in_negative_values(zeta_def)

    def initial_definition(self, zeta_def):
        zeta_s, sum_terms, brace, sigma = zeta_def

        self.say("Let's define $\\zeta(s)$")
        self.blink()
        pre_zeta_s = VGroup(
            *self.pi_creature.bubble.content.copy()[-4:]
        )
        pre_zeta_s.add(VectorizedPoint(pre_zeta_s.get_right()))
        self.play(
            Transform(pre_zeta_s, zeta_s),
            *self.get_bubble_fade_anims()
        )
        self.remove(pre_zeta_s)
        self.add(zeta_s)
        self.wait()

        for count, term in enumerate(sum_terms):
            self.play(FadeIn(term), run_time = 0.5)
            if count%2 == 0:
                self.wait()
        self.play(
            GrowFromCenter(brace),
            Write(sigma),
            self.pi_creature.change_mode, "pondering"
        )
        self.wait()

    def plug_in_two(self, zeta_def):
        two_def = self.get_definition("2")[0]
        number_line = NumberLine(
            x_min = 0,
            x_max = 3,
            tick_frequency = 0.25,
            numbers_with_elongated_ticks = list(range(4)),
            unit_size = 3,
        )
        number_line.add_numbers()
        number_line.next_to(self.pi_creature, LEFT)
        number_line.to_edge(LEFT)
        self.number_line = number_line

        lines, braces, dots, pi_dot = self.get_sum_lines(2)
        fracs = VGroup(*[
            TexMobject("\\frac{1}{%d}"%((d+1)**2)).scale(0.7)
            for d, brace in enumerate(braces)
        ])
        for frac, brace, line in zip(fracs, braces, lines):
            frac.set_color(line.get_color())
            frac.next_to(brace, UP, buff = SMALL_BUFF)
            if frac is fracs[-1]:
                frac.shift(0.5*RIGHT + 0.2*UP)
                arrow = Arrow(
                    frac.get_bottom(), brace.get_top(),
                    tip_length = 0.1,
                    buff = 0.1
                )
                arrow.set_color(line.get_color())
                frac.add(arrow)

        pi_term = TexMobject("= \\frac{\\pi^2}{6}")
        pi_term.next_to(zeta_def[1], RIGHT)
        pi_arrow = Arrow(
            pi_term[-1].get_bottom(), pi_dot,
            color = pi_dot.get_color()
        )
        approx = TexMobject("\\approx 1.645")
        approx.next_to(pi_term)

        self.play(Transform(zeta_def, two_def))
        self.wait()
        self.play(ShowCreation(number_line))

        for frac, brace, line in zip(fracs, braces, lines):
            self.play(
                Write(frac),
                GrowFromCenter(brace),
                ShowCreation(line),
                run_time = 0.7
            )
            self.wait(0.7)
        self.wait()
        self.play(
            ShowCreation(VGroup(*lines[4:])),
            Write(dots)
        )
        self.wait()
        self.play(
            Write(pi_term),
            ShowCreation(VGroup(pi_arrow, pi_dot)),
            self.pi_creature.change_mode, "hooray"
        )
        self.wait()
        self.play(
            Write(approx),
            self.pi_creature.change_mode, "happy"
        )
        self.wait(3)
        self.play(*list(map(FadeOut, [
            fracs, pi_arrow, pi_dot, approx,
        ])))
        self.lines = lines
        self.braces = braces
        self.dots = dots
        self.final_dot = pi_dot
        self.final_sum = pi_term

    def plug_in_three_and_four(self, zeta_def):
        final_sums = ["1.202\\dots", "\\frac{\\pi^4}{90}"]
        sum_terms, brace, sigma = zeta_def[1:]
        for exponent, final_sum in zip([3, 4], final_sums):
            self.transition_to_new_input(zeta_def, exponent, final_sum)
            self.wait()

        arrow = Arrow(sum_terms.get_left(), sum_terms.get_right())
        arrow.next_to(sum_terms, DOWN)
        smaller_words = TextMobject("Getting smaller")
        smaller_words.next_to(arrow, DOWN)
        self.arrow, self.smaller_words = arrow, smaller_words

        self.wait()
        self.play(
            ShowCreation(arrow),
            Write(smaller_words)
        )
        self.change_mode("happy")
        self.wait(2)

    def plug_in_negative_values(self, zeta_def):
        zeta_s, sum_terms, brace, sigma = zeta_def
        arrow = self.arrow
        smaller_words = self.smaller_words
        bigger_words = TextMobject("Getting \\emph{bigger}?")
        bigger_words.move_to(self.smaller_words)

        #plug in -1
        self.transition_to_new_input(zeta_def, -1, "-\\frac{1}{12}")
        self.play(
            Transform(self.smaller_words, bigger_words),
            self.pi_creature.change_mode, "confused"
        )
        new_sum_terms = TexMobject(
            list("1+2+3+4+") + ["\\cdots"]
        )
        new_sum_terms.move_to(sum_terms, LEFT)
        arrow.target = arrow.copy().next_to(new_sum_terms, DOWN)
        arrow.target.stretch_to_fit_width(new_sum_terms.get_width())
        bigger_words.next_to(arrow.target, DOWN)
        new_brace = Brace(new_sum_terms, UP)
        self.play(
            Transform(sum_terms, new_sum_terms),
            Transform(brace, new_brace),
            sigma.next_to, new_brace, UP,
            MoveToTarget(arrow),
            Transform(smaller_words, bigger_words),
            self.final_sum.next_to, new_sum_terms, RIGHT
        )
        self.wait(3)

        #plug in -2
        new_sum_terms = TexMobject(
            list("1+4+9+16+") + ["\\cdots"]
        )
        new_sum_terms.move_to(sum_terms, LEFT)
        new_zeta_def, ignore = self.get_definition("-2")
        zeta_minus_two, ignore, ignore, new_sigma = new_zeta_def
        new_sigma.next_to(brace, UP)
        new_final_sum = TexMobject("=0")
        new_final_sum.next_to(new_sum_terms)
        lines, braces, dots, final_dot = self.get_sum_lines(-2)

        self.play(
            Transform(zeta_s, zeta_minus_two),
            Transform(sum_terms, new_sum_terms),
            Transform(sigma, new_sigma),
            Transform(self.final_sum, new_final_sum),
            Transform(self.lines, lines),
            Transform(self.braces, braces),
        )
        self.wait()
        self.change_mode("pleading")
        self.wait(2)

    def get_definition(self, input_string, input_color = YELLOW):
        inputs = VGroup()
        num_shown_terms = 4
        n_input_chars = len(input_string)

        zeta_s_eq = TexMobject("\\zeta(%s) = "%input_string)
        zeta_s_eq.to_edge(LEFT, buff = LARGE_BUFF)
        zeta_s_eq.shift(0.5*UP)
        inputs.add(*zeta_s_eq[2:2+n_input_chars])

        sum_terms = TexMobject(*it.chain(*list(zip(
            [
                "\\frac{1}{%d^{%s}}"%(d, input_string)
                for d in range(1, 1+num_shown_terms)
            ],
            it.cycle(["+"])
        ))))
        sum_terms.add(TexMobject("\\cdots").next_to(sum_terms))
        sum_terms.next_to(zeta_s_eq, RIGHT)
        for x in range(num_shown_terms):
            inputs.add(*sum_terms[2*x][-n_input_chars:])


        brace = Brace(sum_terms, UP)
        sigma = TexMobject(
            "\\sum_{n=1}^\\infty \\frac{1}{n^{%s}}"%input_string
        )
        sigma.next_to(brace, UP)
        inputs.add(*sigma[-n_input_chars:])

        inputs.set_color(input_color)
        group = VGroup(zeta_s_eq, sum_terms, brace, sigma)
        return group, inputs

    def get_sum_lines(self, exponent, line_thickness = 6):
        num_lines = 100 if exponent > 0 else 6
        powers = [0] + [x**(-exponent) for x in range(1, num_lines)]
        power_sums = np.cumsum(powers)
        lines = VGroup(*[
            Line(
                self.number_line.number_to_point(s1),
                self.number_line.number_to_point(s2),
            )
            for s1, s2 in zip(power_sums, power_sums[1:])
        ])
        lines.set_stroke(width = line_thickness)
        # VGroup(*lines[:4]).set_color_by_gradient(RED, GREEN_B)
        # VGroup(*lines[4:]).set_color_by_gradient(GREEN_B, MAROON_B)
        VGroup(*lines[::2]).set_color(MAROON_B)
        VGroup(*lines[1::2]).set_color(RED)

        braces = VGroup(*[
            Brace(line, UP)
            for line in lines[:4]
        ])
        dots = TexMobject("...")
        dots.stretch_to_fit_width(
            0.8 * VGroup(*lines[4:]).get_width()
        )
        dots.next_to(braces, RIGHT, buff = SMALL_BUFF)

        final_dot = Dot(
            self.number_line.number_to_point(power_sums[-1]),
            color = GREEN_B
        )

        return lines, braces, dots, final_dot

    def transition_to_new_input(self, zeta_def, exponent, final_sum):
        new_zeta_def = self.get_definition(str(exponent))[0]
        lines, braces, dots, final_dot = self.get_sum_lines(exponent)
        final_sum = TexMobject("=" + final_sum)
        final_sum.next_to(new_zeta_def[1][-1])
        final_sum.shift(SMALL_BUFF*UP)
        self.play(
            Transform(zeta_def, new_zeta_def),
            Transform(self.lines, lines),
            Transform(self.braces, braces),
            Transform(self.dots, dots),
            Transform(self.final_dot, final_dot),
            Transform(self.final_sum, final_sum),
            self.pi_creature.change_mode, "pondering"
        )

class ReadIntoZetaFunction(Scene):
    CONFIG = {
        "statement" : "$\\zeta(-1) = -\\frac{1}{12}$",
        "target_mode" : "frustrated",
    }
    def construct(self):
        randy = Randolph(mode = "pondering")
        randy.shift(3*LEFT+DOWN)
        paper = Rectangle(width = 4, height = 5)
        paper.next_to(randy, RIGHT, aligned_edge = DOWN)
        paper.set_color(WHITE)
        max_width = 0.8*paper.get_width()

        title = TextMobject("$\\zeta(s)$ manual")
        title.next_to(paper.get_top(), DOWN)
        title.set_color(YELLOW)
        paper.add(title)
        paragraph_lines = VGroup(
            Line(LEFT, RIGHT),
            Line(LEFT, RIGHT).shift(0.2*DOWN),
            Line(LEFT, ORIGIN).shift(0.4*DOWN)
        )
        paragraph_lines.set_width(max_width)
        paragraph_lines.next_to(title, DOWN, MED_LARGE_BUFF)
        paper.add(paragraph_lines)
        max_height = 1.5*paragraph_lines.get_height()

        statement = TextMobject(self.statement)
        if statement.get_width() > max_width:
            statement.set_width(max_width)
        if statement.get_height() > max_height:
            statement.set_height(max_height)

        statement.next_to(paragraph_lines, DOWN)
        statement.set_color(GREEN_B)
        paper.add(paragraph_lines.copy().next_to(statement, DOWN, MED_LARGE_BUFF))

        randy.look_at(statement)
        self.add(randy, paper)
        self.play(Write(statement))
        self.play(
            randy.change_mode, self.target_mode,
            randy.look_at, title
        )
        self.play(Blink(randy))
        self.play(randy.look_at, statement)
        self.wait()

class ReadIntoZetaFunctionTrivialZero(ReadIntoZetaFunction):
    CONFIG = {
        "statement" : "$\\zeta(-2n) = 0$"
    }

class ReadIntoZetaFunctionAnalyticContinuation(ReadIntoZetaFunction):
    CONFIG = {
        "statement" : "...analytic \\\\ continuation...",
        "target_mode" : "confused",
    }

class IgnoreNegatives(TeacherStudentsScene):
    def construct(self):
        definition = TexMobject("""
            \\zeta(s) = \\sum_{n=1}^{\\infty} \\frac{1}{n^s}
        """)
        VGroup(definition[2], definition[-1]).set_color(YELLOW)
        definition.to_corner(UP+LEFT)
        self.add(definition)
        brace = Brace(definition, DOWN)
        only_s_gt_1 = brace.get_text("""
            Only defined
            for $s > 1$
        """)
        only_s_gt_1[-3].set_color(YELLOW)


        self.change_student_modes(*["confused"]*3)
        words = TextMobject(
            "Ignore $s \\le 1$ \\dots \\\\",
            "For now."
        )
        words[0][6].set_color(YELLOW)
        words[1].set_color(BLACK)
        self.teacher_says(words)
        self.play(words[1].set_color, WHITE)
        self.change_student_modes(*["happy"]*3)
        self.play(
            GrowFromCenter(brace),
            Write(only_s_gt_1),
            *it.chain(*[
                [pi.look_at, definition]
                for pi in self.get_pi_creatures()
            ])
        )
        self.random_blink(3)

class RiemannFatherOfComplex(ComplexTransformationScene):
    def construct(self):
        name = TextMobject(
            "Bernhard Riemann $\\rightarrow$ Complex analysis"
        )
        name.to_corner(UP+LEFT)
        name.shift(0.25*DOWN)
        name.add_background_rectangle()
        # photo = Square()
        photo = ImageMobject("Riemann", invert = False)
        photo.set_width(5)
        photo.next_to(name, DOWN, aligned_edge = LEFT)


        self.add(photo)
        self.play(Write(name))
        self.wait()

        input_dot = Dot(2*RIGHT+UP, color = YELLOW)
        arc = Arc(-2*np.pi/3)
        arc.rotate(-np.pi)
        arc.add_tip()
        arc.shift(input_dot.get_top()-arc.points[0]+SMALL_BUFF*UP)
        output_dot = Dot(
            arc.points[-1] + SMALL_BUFF*(2*RIGHT+DOWN),
            color = MAROON_B
        )
        for dot, tex in (input_dot, "z"), (output_dot, "f(z)"):
            dot.label = TexMobject(tex)
            dot.label.add_background_rectangle()
            dot.label.next_to(dot, DOWN+RIGHT, buff = SMALL_BUFF)
            dot.label.set_color(dot.get_color())

        self.play(
            ShowCreation(input_dot),
            Write(input_dot.label)
        )
        self.play(ShowCreation(arc))
        self.play(
            ShowCreation(output_dot),
            Write(output_dot.label)
        )
        self.wait()

class FromRealToComplex(ComplexTransformationScene):
    CONFIG = {
        "plane_config" : {
            "space_unit_to_x_unit" : 2,
            "space_unit_to_y_unit" : 2,
        },
        "background_label_scale_val" : 0.7,
        "output_color" : GREEN_B,
        "num_lines_in_spiril_sum" : 1000,
    }
    def construct(self):
        self.handle_background()
        self.show_real_to_real()
        self.transition_to_complex()
        self.single_out_complex_exponent()
        ##Fade to several scenes defined below
        self.show_s_equals_two_lines()
        self.transition_to_spiril_sum()
        self.vary_complex_input()
        self.show_domain_of_convergence()
        self.ask_about_visualizing_all()

    def handle_background(self):
        self.remove(self.background)
        #Oh yeah, this is great practice...
        self.background[-1].remove(*self.background[-1][-3:])

    def show_real_to_real(self):
        zeta = self.get_zeta_definition("2",  "\\frac{\\pi^2}{6}")
        number_line = NumberLine(
            unit_size = 2,
            tick_frequency = 0.5,
            numbers_with_elongated_ticks = list(range(-2, 3))
        )
        number_line.add_numbers()
        input_dot = Dot(number_line.number_to_point(2))
        input_dot.set_color(YELLOW)

        output_dot = Dot(number_line.number_to_point(np.pi**2/6))
        output_dot.set_color(self.output_color)

        arc = Arc(
            2*np.pi/3, start_angle = np.pi/6,
        )
        arc.stretch_to_fit_width(
            (input_dot.get_center()-output_dot.get_center())[0]
        )
        arc.stretch_to_fit_height(0.5)
        arc.next_to(input_dot.get_center(), UP, aligned_edge = RIGHT)
        arc.add_tip()

        two = zeta[1][2].copy()
        sum_term = zeta[-1]
        self.add(number_line, *zeta[:-1])
        self.wait()
        self.play(Transform(two, input_dot))
        self.remove(two)
        self.add(input_dot)
        self.play(ShowCreation(arc))
        self.play(ShowCreation(output_dot))
        self.play(Transform(output_dot.copy(), sum_term))
        self.remove(*self.get_mobjects_from_last_animation())
        self.add(sum_term)
        self.wait(2)
        self.play(
            ShowCreation(
                self.background,
                run_time = 2
            ),
            FadeOut(VGroup(arc, output_dot, number_line)),
            Animation(zeta),
            Animation(input_dot)
        )
        self.wait(2)

        self.zeta = zeta
        self.input_dot = input_dot

    def transition_to_complex(self):
        complex_zeta = self.get_zeta_definition("2+i", "???")
        input_dot = self.input_dot
        input_dot.generate_target()
        input_dot.target.move_to(
            self.background.num_pair_to_point((2, 1))
        )
        input_label = TexMobject("2+i")
        input_label.set_color(YELLOW)
        input_label.next_to(input_dot.target, DOWN+RIGHT, buff = SMALL_BUFF)
        input_label.add_background_rectangle()
        input_label.save_state()
        input_label.replace(VGroup(*complex_zeta[1][2:5]))
        input_label.background_rectangle.scale_in_place(0.01)
        self.input_label = input_label

        self.play(Transform(self.zeta, complex_zeta))
        self.wait()
        self.play(
            input_label.restore,
            MoveToTarget(input_dot)
        )
        self.wait(2)

    def single_out_complex_exponent(self):
        frac_scale_factor = 1.2

        randy = Randolph()
        randy.to_corner()
        bubble = randy.get_bubble(height = 4)
        bubble.set_fill(BLACK, opacity = 1)

        frac = VGroup(
            VectorizedPoint(self.zeta[2][3].get_left()),
            self.zeta[2][3],
            VectorizedPoint(self.zeta[2][3].get_right()),
            self.zeta[2][4],
        ).copy()
        frac.generate_target()
        frac.target.scale(frac_scale_factor)
        bubble.add_content(frac.target)
        new_frac = TexMobject(
            "\\Big(", "\\frac{1}{2}", "\\Big)", "^{2+i}"
        )
        new_frac[-1].set_color(YELLOW)
        new_frac.scale(frac_scale_factor)
        new_frac.move_to(frac.target)
        new_frac.shift(LEFT+0.2*UP)

        words = TextMobject("Not repeated \\\\", " multiplication")
        words.scale(0.8)
        words.set_color(RED)
        words.next_to(new_frac, RIGHT)

        new_words = TextMobject("Not \\emph{super} \\\\", "crucial to know...")
        new_words.replace(words)
        new_words.scale_in_place(1.3)

        self.play(FadeIn(randy))
        self.play(
            randy.change_mode, "confused",
            randy.look_at, bubble,
            ShowCreation(bubble),
            MoveToTarget(frac)
        )
        self.play(Blink(randy))
        self.play(Transform(frac, new_frac))
        self.play(Write(words))
        for x in range(2):
            self.wait(2)
            self.play(Blink(randy))
        self.play(
            Transform(words, new_words),
            randy.change_mode, "maybe"
        )
        self.wait()
        self.play(Blink(randy))
        self.play(randy.change_mode, "happy")
        self.wait()
        self.play(*list(map(FadeOut, [randy, bubble, frac, words])))

    def show_s_equals_two_lines(self):
        self.input_label.save_state()
        zeta = self.get_zeta_definition("2", "\\frac{\\pi^2}{6}")
        lines, output_dot = self.get_sum_lines(2)
        sum_terms = self.zeta[2][:-1:3]
        dots_copy = zeta[2][-1].copy()
        pi_copy = zeta[3].copy()
        def transform_and_replace(m1, m2):
            self.play(Transform(m1, m2))
            self.remove(m1)
            self.add(m2)

        self.play(
            self.input_dot.shift, 2*DOWN,
            self.input_label.fade, 0.7,
        )
        self.play(Transform(self.zeta, zeta))

        for term, line in zip(sum_terms, lines):
            line.save_state()
            line.next_to(term, DOWN)
            term_copy = term.copy()
            transform_and_replace(term_copy, line)
            self.play(line.restore)
        later_lines = VGroup(*lines[4:])
        transform_and_replace(dots_copy, later_lines)
        self.wait()
        transform_and_replace(pi_copy, output_dot)
        self.wait()

        self.lines = lines
        self.output_dot = output_dot

    def transition_to_spiril_sum(self):
        zeta = self.get_zeta_definition("2+i", "1.15 - 0.44i")
        zeta.set_width(FRAME_WIDTH-1)
        zeta.to_corner(UP+LEFT)
        lines, output_dot = self.get_sum_lines(complex(2, 1))

        self.play(
            self.input_dot.shift, 2*UP,
            self.input_label.restore,
        )
        self.wait()
        self.play(Transform(self.zeta, zeta))
        self.wait()
        self.play(
            Transform(self.lines, lines),
            Transform(self.output_dot, output_dot),
            run_time = 2,
            path_arc = -np.pi/6,
        )
        self.wait()

    def vary_complex_input(self):
        zeta = self.get_zeta_definition("s", "")
        zeta[3].set_color(BLACK)
        self.play(Transform(self.zeta, zeta))
        self.play(FadeOut(self.input_label))
        self.wait(2)
        inputs = [
            complex(1.5, 1.8),
            complex(1.5, -1),
            complex(3, -1),
            complex(1.5, 1.8),
            complex(1.5, -1.8),
            complex(1.4, -1.8),
            complex(1.5, 0),
            complex(2, 1),
        ]
        for s in inputs:
            input_point = self.z_to_point(s)
            lines, output_dot = self.get_sum_lines(s)
            self.play(
                self.input_dot.move_to, input_point,
                Transform(self.lines, lines),
                Transform(self.output_dot, output_dot),
                run_time = 2
            )
            self.wait()
        self.wait()

    def show_domain_of_convergence(self, opacity = 0.2):
        domain = Rectangle(
            width = FRAME_X_RADIUS-2,
            height = FRAME_HEIGHT,
            stroke_width = 0,
            fill_color = YELLOW,
            fill_opacity = opacity,
        )
        domain.to_edge(RIGHT, buff = 0)
        anti_domain = Rectangle(
            width = FRAME_X_RADIUS+2,
            height = FRAME_HEIGHT,
            stroke_width = 0,
            fill_color = RED,
            fill_opacity = opacity,
        )
        anti_domain.to_edge(LEFT, buff = 0)

        domain_words = TextMobject("""
            $\\zeta(s)$ happily
            converges and
            makes sense
        """)
        domain_words.to_corner(UP+RIGHT, buff = MED_LARGE_BUFF)

        anti_domain_words = TextMobject("""
            Not so much...
        """)
        anti_domain_words.next_to(ORIGIN, LEFT, buff = LARGE_BUFF)
        anti_domain_words.shift(1.5*DOWN)

        self.play(FadeIn(domain))
        self.play(Write(domain_words))
        self.wait()
        self.play(FadeIn(anti_domain))
        self.play(Write(anti_domain_words))
        self.wait(2)
        self.play(*list(map(FadeOut, [
            anti_domain, anti_domain_words,
        ])))
        self.domain_words = domain_words

    def ask_about_visualizing_all(self):
        morty = Mortimer().flip()
        morty.scale(0.7)
        morty.to_corner(DOWN+LEFT)
        bubble = morty.get_bubble(SpeechBubble, height = 4)
        bubble.set_fill(BLACK, opacity = 0.5)
        bubble.write("""
            How can we visualize
            this for all inputs?
        """)

        self.play(FadeIn(morty))
        self.play(
            morty.change_mode, "speaking",
            ShowCreation(bubble),
            Write(bubble.content)
        )
        self.play(Blink(morty))
        self.wait(3)
        self.play(
            morty.change_mode, "pondering",
            morty.look_at, self.input_dot,
            *list(map(FadeOut, [
                bubble, bubble.content, self.domain_words
            ]))
        )
        arrow = Arrow(self.input_dot, self.output_dot, buff = SMALL_BUFF)
        arrow.set_color(WHITE)
        self.play(ShowCreation(arrow))
        self.play(Blink(morty))
        self.wait()

    def get_zeta_definition(self, input_string, output_string, input_color = YELLOW):
        inputs = VGroup()
        num_shown_terms = 4
        n_input_chars = len(input_string)

        zeta_s_eq = TexMobject("\\zeta(%s) = "%input_string)
        zeta_s_eq.to_edge(LEFT, buff = LARGE_BUFF)
        zeta_s_eq.shift(0.5*UP)
        inputs.add(*zeta_s_eq[2:2+n_input_chars])


        raw_sum_terms = TexMobject(*[
            "\\frac{1}{%d^{%s}} + "%(d, input_string)
            for d in range(1, 1+num_shown_terms)
        ])
        sum_terms = VGroup(*it.chain(*[
            [
                VGroup(*term[:3]),
                VGroup(*term[3:-1]),
                term[-1],
            ]
            for term in raw_sum_terms
        ]))
        sum_terms.add(TexMobject("\\cdots").next_to(sum_terms[-1]))
        sum_terms.next_to(zeta_s_eq, RIGHT)
        for x in range(num_shown_terms):
            inputs.add(*sum_terms[3*x+1])

        output = TexMobject("= \\," + output_string)
        output.next_to(sum_terms, RIGHT)
        output.set_color(self.output_color)

        inputs.set_color(input_color)
        group = VGroup(zeta_s_eq, sum_terms, output)
        group.to_edge(UP)
        group.add_to_back(BackgroundRectangle(group))
        return group

    def get_sum_lines(self, exponent, line_thickness = 6):
        powers = [0] + [
            x**(-exponent)
            for x in range(1, self.num_lines_in_spiril_sum)
        ]
        power_sums = np.cumsum(powers)
        lines = VGroup(*[
            Line(*list(map(self.z_to_point, z_pair)))
            for z_pair in zip(power_sums, power_sums[1:])
        ])
        widths = np.linspace(line_thickness, 0, len(list(lines)))
        for line, width in zip(lines, widths):
            line.set_stroke(width = width)
        VGroup(*lines[::2]).set_color(MAROON_B)
        VGroup(*lines[1::2]).set_color(RED)

        final_dot = Dot(
            # self.z_to_point(power_sums[-1]),
            self.z_to_point(zeta(exponent)),
            color = self.output_color
        )

        return lines, final_dot

class TerritoryOfExponents(ComplexTransformationScene):
    def construct(self):
        self.add_title()
        familiar_territory = TextMobject("Familiar territory")
        familiar_territory.set_color(YELLOW)
        familiar_territory.next_to(ORIGIN, UP+RIGHT)
        familiar_territory.shift(2*UP)
        real_line = Line(LEFT, RIGHT).scale(FRAME_X_RADIUS)
        real_line.set_color(YELLOW)
        arrow1 = Arrow(familiar_territory.get_bottom(), real_line.get_left())
        arrow2 = Arrow(familiar_territory.get_bottom(), real_line.get_right())
        VGroup(arrow1, arrow2).set_color(WHITE)

        extended_realm = TextMobject("Extended realm")
        extended_realm.move_to(familiar_territory)
        full_plane = Rectangle(
            width = FRAME_WIDTH,
            height = FRAME_HEIGHT,
            fill_color = YELLOW,
            fill_opacity = 0.3
        )

        self.add(familiar_territory)
        self.play(ShowCreation(arrow1))
        self.play(
            Transform(arrow1, arrow2),
            ShowCreation(real_line)
        )
        self.play(FadeOut(arrow1))
        self.play(
            FadeIn(full_plane),
            Transform(familiar_territory, extended_realm),
            Animation(real_line)
        )

    def add_title(self):
        exponent = TexMobject(
            "\\left(\\frac{1}{2}\\right)^s"
        )
        exponent[-1].set_color(YELLOW)
        exponent.next_to(ORIGIN, LEFT, MED_LARGE_BUFF).to_edge(UP)
        self.add_foreground_mobjects(exponent)

class ComplexExponentiation(Scene):
    def construct(self):
        self.extract_pure_imaginary_part()
        self.add_on_planes()
        self.show_imaginary_powers()

    def extract_pure_imaginary_part(self):
        original = TexMobject(
            "\\left(\\frac{1}{2}\\right)", "^{2+i}"
        )
        split = TexMobject(
             "\\left(\\frac{1}{2}\\right)", "^{2}",
             "\\left(\\frac{1}{2}\\right)", "^{i}",
        )
        VGroup(original[-1], split[1], split[3]).set_color(YELLOW)
        VGroup(original, split).shift(UP)
        real_part = VGroup(*split[:2])
        imag_part = VGroup(*split[2:])

        brace = Brace(real_part)
        we_understand = brace.get_text(
            "We understand this"
        )
        VGroup(brace, we_understand).set_color(GREEN_B)

        self.add(original)
        self.wait()
        self.play(*[
            Transform(*pair)
            for pair in [
                (original[0], split[0]),
                (original[1][0], split[1]),
                (original[0].copy(), split[2]),
                (VGroup(*original[1][1:]), split[3]),
            ]
        ])
        self.remove(*self.get_mobjects_from_last_animation())
        self.add(real_part, imag_part)
        self.wait()
        self.play(
            GrowFromCenter(brace),
            FadeIn(we_understand),
            real_part.set_color, GREEN_B
        )
        self.wait()
        self.play(
            imag_part.move_to, imag_part.get_left(),
            *list(map(FadeOut, [brace, we_understand, real_part]))
        )
        self.wait()
        self.imag_exponent = imag_part

    def add_on_planes(self):
        left_plane = NumberPlane(x_radius = (FRAME_X_RADIUS-1)/2)
        left_plane.to_edge(LEFT, buff = 0)
        imag_line = Line(DOWN, UP).scale(FRAME_Y_RADIUS)
        imag_line.set_color(YELLOW).fade(0.3)
        imag_line.move_to(left_plane.get_center())
        left_plane.add(imag_line)
        left_title = TextMobject("Input space")
        left_title.add_background_rectangle()
        left_title.set_color(YELLOW)
        left_title.next_to(left_plane.get_top(), DOWN)

        right_plane = NumberPlane(x_radius = (FRAME_X_RADIUS-1)/2)
        right_plane.to_edge(RIGHT, buff = 0)
        unit_circle = Circle()
        unit_circle.set_color(MAROON_B).fade(0.3)
        unit_circle.shift(right_plane.get_center())
        right_plane.add(unit_circle)
        right_title = TextMobject("Output space")
        right_title.add_background_rectangle()
        right_title.set_color(MAROON_B)
        right_title.next_to(right_plane.get_top(), DOWN)

        for plane in left_plane, right_plane:
            labels = VGroup()
            for x in range(-2, 3):
                label = TexMobject(str(x))
                label.move_to(plane.num_pair_to_point((x, 0)))
                labels.add(label)
            for y in range(-3, 4):
                if y == 0:
                    continue
                label = TexMobject(str(y) + "i")
                label.move_to(plane.num_pair_to_point((0, y)))
                labels.add(label)
            for label in labels:
                label.scale_in_place(0.5)
                label.next_to(
                    label.get_center(), DOWN+RIGHT,
                    buff = SMALL_BUFF
                )
            plane.add(labels)

        arrow = Arrow(LEFT, RIGHT)

        self.play(
            ShowCreation(left_plane),
            Write(left_title),
            run_time = 3
        )
        self.play(
            ShowCreation(right_plane),
            Write(right_title),
            run_time = 3
        )
        self.play(ShowCreation(arrow))
        self.wait()
        self.left_plane = left_plane
        self.right_plane = right_plane

    def show_imaginary_powers(self):
        i = complex(0, 1)
        input_dot = Dot(self.z_to_point(i))
        input_dot.set_color(YELLOW)
        output_dot = Dot(self.z_to_point(0.5**(i), is_input = False))
        output_dot.set_color(MAROON_B)

        output_dot.save_state()
        output_dot.move_to(input_dot)
        output_dot.set_color(input_dot.get_color())

        curr_base = 0.5
        def output_dot_update(ouput_dot):
            y = input_dot.get_center()[1]
            output_dot.move_to(self.z_to_point(
                curr_base**complex(0, y), is_input = False
            ))
            return output_dot

        def walk_up_and_down():
            for vect in 3*DOWN, 5*UP, 5*DOWN, 2*UP:
                self.play(
                    input_dot.shift, vect,
                    UpdateFromFunc(output_dot, output_dot_update),
                    run_time = 3
                )

        exp = self.imag_exponent[-1]
        new_exp = TexMobject("ti")
        new_exp.set_color(exp.get_color())
        new_exp.set_height(exp.get_height())
        new_exp.move_to(exp, LEFT)

        nine = TexMobject("9")
        nine.set_color(BLUE)
        denom = self.imag_exponent[0][3]
        denom.save_state()
        nine.replace(denom)

        self.play(Transform(exp, new_exp))
        self.play(input_dot.shift, 2*UP)
        self.play(input_dot.shift, 2*DOWN)
        self.wait()
        self.play(output_dot.restore)
        self.wait()
        walk_up_and_down()
        self.wait()
        curr_base = 1./9
        self.play(Transform(denom, nine))
        walk_up_and_down()
        self.wait()

    def z_to_point(self, z, is_input = True):
        if is_input:
            plane = self.left_plane
        else:
            plane = self.right_plane
        return plane.num_pair_to_point((z.real, z.imag))

class SizeAndRotationBreakdown(Scene):
    def construct(self):
        original = TexMobject(
            "\\left(\\frac{1}{2}\\right)", "^{2+i}"
        )
        split = TexMobject(
             "\\left(\\frac{1}{2}\\right)", "^{2}",
             "\\left(\\frac{1}{2}\\right)", "^{i}",
        )
        VGroup(original[-1], split[1], split[3]).set_color(YELLOW)
        VGroup(original, split).shift(UP)
        real_part = VGroup(*split[:2])
        imag_part = VGroup(*split[2:])

        size_brace = Brace(real_part)
        size = size_brace.get_text("Size")
        rotation_brace = Brace(imag_part, UP)
        rotation = rotation_brace.get_text("Rotation")

        self.add(original)
        self.wait()
        self.play(*[
            Transform(*pair)
            for pair in [
                (original[0], split[0]),
                (original[1][0], split[1]),
                (original[0].copy(), split[2]),
                (VGroup(*original[1][1:]), split[3]),
            ]
        ])
        self.play(
            GrowFromCenter(size_brace),
            Write(size)
        )
        self.play(
            GrowFromCenter(rotation_brace),
            Write(rotation)
        )
        self.wait()

class SeeLinksInDescription(TeacherStudentsScene):
    def construct(self):
        self.teacher_says("""
            See links in the
            description for more.
        """)
        self.play(*it.chain(*[
            [pi.change_mode, "hooray", pi.look, DOWN]
            for pi in self.get_students()
        ]))
        self.random_blink(3)

class ShowMultiplicationOfRealAndImaginaryExponentialParts(FromRealToComplex):
    def construct(self):
        self.break_up_exponent()
        self.show_multiplication()

    def break_up_exponent(self):
        original = TexMobject(
            "\\left(\\frac{1}{2}\\right)", "^{2+i}"
        )
        split = TexMobject(
             "\\left(\\frac{1}{2}\\right)", "^{2}",
             "\\left(\\frac{1}{2}\\right)", "^{i}",
        )
        VGroup(original[-1], split[1], split[3]).set_color(YELLOW)
        VGroup(original, split).to_corner(UP+LEFT)
        rect = BackgroundRectangle(split)
        real_part = VGroup(*split[:2])
        imag_part = VGroup(*split[2:])

        self.add(rect, original)
        self.wait()
        self.play(*[
            Transform(*pair)
            for pair in [
                (original[0], split[0]),
                (original[1][0], split[1]),
                (original[0].copy(), split[2]),
                (VGroup(*original[1][1:]), split[3]),
            ]
        ])
        self.remove(*self.get_mobjects_from_last_animation())
        self.add(real_part, imag_part)
        self.wait()
        self.real_part = real_part
        self.imag_part = imag_part

    def show_multiplication(self):
        real_part = self.real_part.copy()
        imag_part = self.imag_part.copy()
        for part in real_part, imag_part:
            part.add_to_back(BackgroundRectangle(part))

        fourth_point = self.z_to_point(0.25)
        fourth_line = Line(ORIGIN, fourth_point)
        brace = Brace(fourth_line, UP, buff = SMALL_BUFF)
        fourth_dot = Dot(fourth_point)
        fourth_group = VGroup(fourth_line, brace, fourth_dot)
        fourth_group.set_color(RED)

        circle = Circle(radius = 2, color = MAROON_B)
        circle.fade(0.3)
        imag_power_point = self.z_to_point(0.5**complex(0, 1))
        imag_power_dot = Dot(imag_power_point)
        imag_power_line = Line(ORIGIN, imag_power_point)
        VGroup(imag_power_dot, imag_power_line).set_color(MAROON_B)

        full_power_tex = TexMobject(
            "\\left(\\frac{1}{2}\\right)", "^{2+i}"
        )
        full_power_tex[-1].set_color(YELLOW)
        full_power_tex.add_background_rectangle()
        full_power_tex.scale(0.7)
        full_power_tex.next_to(
            0.5*self.z_to_point(0.5**complex(2, 1)),
            UP+RIGHT
        )

        self.play(
            real_part.scale, 0.7,
            real_part.next_to, brace, UP, SMALL_BUFF, LEFT,
            ShowCreation(fourth_dot)
        )
        self.play(
            GrowFromCenter(brace),
            ShowCreation(fourth_line),
        )
        self.wait()
        self.play(
            imag_part.scale, 0.7,
            imag_part.next_to, imag_power_dot, DOWN+RIGHT, SMALL_BUFF,
            ShowCreation(imag_power_dot)
        )
        self.play(ShowCreation(circle), Animation(imag_power_dot))
        self.play(ShowCreation(imag_power_line))
        self.wait(2)
        self.play(
            fourth_group.rotate, imag_power_line.get_angle()
        )
        real_part.generate_target()
        imag_part.generate_target()
        real_part.target.next_to(brace, UP+RIGHT, buff = 0)
        imag_part.target.next_to(real_part.target, buff = 0)
        self.play(*list(map(MoveToTarget, [real_part, imag_part])))
        self.wait()

class ComplexFunctionsAsTransformations(ComplexTransformationScene):
    def construct(self):
        self.add_title()
        input_dots, output_dots, arrows = self.get_dots()

        self.play(FadeIn(
            input_dots,
            run_time = 2,
            lag_ratio = 0.5
        ))
        for in_dot, out_dot, arrow in zip(input_dots, output_dots, arrows):
            self.play(
                Transform(in_dot.copy(), out_dot),
                ShowCreation(arrow)
            )
            self.wait()
        self.wait()


    def add_title(self):
        title = TextMobject("Complex functions as transformations")
        title.add_background_rectangle()
        title.to_edge(UP)
        self.add(title)

    def get_dots(self):
        input_points = [
            RIGHT+2*UP,
            4*RIGHT+DOWN,
            2*LEFT+2*UP,
            LEFT+DOWN,
            6*LEFT+DOWN,
        ]
        output_nudges = [
            DOWN+RIGHT,
            2*UP+RIGHT,
            2*RIGHT+2*DOWN,
            2*RIGHT+DOWN,
            RIGHT+2*UP,
        ]
        input_dots = VGroup(*list(map(Dot, input_points)))
        input_dots.set_color(YELLOW)
        output_dots = VGroup(*[
            Dot(ip + on)
            for ip, on in zip(input_points, output_nudges)
        ])
        output_dots.set_color(MAROON_B)
        arrows = VGroup(*[
            Arrow(in_dot, out_dot, buff = 0.1, color = WHITE)
            for in_dot, out_dot, in zip(input_dots, output_dots)
        ])
        for i, dot in enumerate(input_dots):
            label = TexMobject("s_%d"%i)
            label.set_color(dot.get_color())
            label.next_to(dot, DOWN+LEFT, buff = SMALL_BUFF)
            dot.add(label)
        for i, dot in enumerate(output_dots):
            label = TexMobject("f(s_%d)"%i)
            label.set_color(dot.get_color())
            label.next_to(dot, UP+RIGHT, buff = SMALL_BUFF)
            dot.add(label)
        return input_dots, output_dots, arrows

class VisualizingSSquared(ComplexTransformationScene):
    CONFIG = {
        "num_anchors_to_add_per_line" : 100,
        "horiz_end_color" : GOLD,
        "y_min" : 0,
    }
    def construct(self):
        self.add_title()
        self.plug_in_specific_values()
        self.show_transformation()
        self.comment_on_two_dimensions()

    def add_title(self):
        title = TexMobject("f(", "s", ") = ", "s", "^2")
        title.set_color_by_tex("s", YELLOW)
        title.add_background_rectangle()
        title.scale(1.5)
        title.to_corner(UP+LEFT)
        self.play(Write(title))
        self.add_foreground_mobject(title)
        self.wait()
        self.title = title

    def plug_in_specific_values(self):
        inputs = list(map(complex, [2, -1, complex(0, 1)]))
        input_dots  = VGroup(*[
            Dot(self.z_to_point(z), color = YELLOW)
            for z in inputs
        ])
        output_dots = VGroup(*[
            Dot(self.z_to_point(z**2), color = BLUE)
            for z in inputs
        ])
        arrows = VGroup()
        VGroup(*[
            ParametricFunction(
                lambda t : self.z_to_point(z**(1.1+0.8*t))
            )
            for z in inputs
        ])
        for z, dot in zip(inputs, input_dots):
            path = ParametricFunction(
                lambda t : self.z_to_point(z**(1+t))
            )
            dot.path = path
            arrow = ParametricFunction(
                lambda t : self.z_to_point(z**(1.1+0.8*t))
            )
            stand_in_arrow = Arrow(
                arrow.points[-2], arrow.points[-1],
                tip_length = 0.2
            )
            arrow.add(stand_in_arrow.tip)
            arrows.add(arrow)
        arrows.set_color(WHITE)

        for input_dot, output_dot, arrow in zip(input_dots, output_dots, arrows):
            input_dot.save_state()
            input_dot.move_to(self.title[1][1])
            input_dot.set_fill(opacity = 0)

            self.play(input_dot.restore)
            self.wait()
            self.play(ShowCreation(arrow))
            self.play(ShowCreation(output_dot))
            self.wait()
        self.add_foreground_mobjects(arrows, output_dots, input_dots)
        self.input_dots = input_dots
        self.output_dots = output_dots

    def add_transformable_plane(self, **kwargs):
        ComplexTransformationScene.add_transformable_plane(self, **kwargs)
        self.plane.next_to(ORIGIN, UP, buff = 0.01)
        self.plane.add(self.plane.copy().rotate(np.pi, RIGHT))
        self.plane.add(
            Line(ORIGIN, FRAME_X_RADIUS*RIGHT, color = self.horiz_end_color),
            Line(ORIGIN, FRAME_X_RADIUS*LEFT, color = self.horiz_end_color),
        )
        self.add(self.plane)

    def show_transformation(self):
        self.add_transformable_plane()
        self.play(ShowCreation(self.plane, run_time = 3))

        self.wait()
        self.apply_complex_homotopy(
            lambda z, t : z**(1+t),
            added_anims = [
                MoveAlongPath(dot, dot.path, run_time = 5)
                for dot in self.input_dots
            ],
            run_time = 5
        )
        self.wait(2)


    def comment_on_two_dimensions(self):
        morty = Mortimer().flip()
        morty.scale(0.7)
        morty.to_corner(DOWN+LEFT)
        bubble = morty.get_bubble(SpeechBubble, height = 2, width = 4)
        bubble.set_fill(BLACK, opacity = 0.9)
        bubble.write("""
            It all happens
            in two dimensions!
        """)
        self.foreground_mobjects = []

        self.play(FadeIn(morty))
        self.play(
            morty.change_mode, "hooray",
            ShowCreation(bubble),
            Write(bubble.content),
        )
        self.play(Blink(morty))
        self.wait(2)

class ShowZetaOnHalfPlane(ZetaTransformationScene):
    CONFIG = {
        "x_min" : 1,
        "x_max" : int(FRAME_X_RADIUS+2),
    }
    def construct(self):
        self.add_title()
        self.initial_transformation()
        self.react_to_transformation()
        self.show_cutoff()
        self.set_color_i_line()
        self.show_continuation()
        self.emphsize_sum_doesnt_make_sense()


    def add_title(self):
        zeta = TexMobject(
            "\\zeta(", "s", ")=",
            *[
                "\\frac{1}{%d^s} + "%d
                for d in range(1, 5)
            ] + ["\\cdots"]
        )
        zeta[1].set_color(YELLOW)
        for mob in zeta[3:3+4]:
            mob[-2].set_color(YELLOW)
        zeta.add_background_rectangle()
        zeta.scale(0.8)
        zeta.to_corner(UP+LEFT)
        self.add_foreground_mobjects(zeta)
        self.zeta = zeta

    def initial_transformation(self):
        self.add_transformable_plane()
        self.wait()
        self.add_extra_plane_lines_for_zeta(animate = True)
        self.wait(2)
        self.plane.save_state()
        self.apply_zeta_function()
        self.wait(2)

    def react_to_transformation(self):
        morty = Mortimer().flip()
        morty.to_corner(DOWN+LEFT)
        bubble = morty.get_bubble(SpeechBubble)
        bubble.set_fill(BLACK, 0.5)
        bubble.write("\\emph{Damn}!")
        bubble.resize_to_content()
        bubble.pin_to(morty)

        self.play(FadeIn(morty))
        self.play(
            morty.change_mode, "surprised",
            ShowCreation(bubble),
            Write(bubble.content)
        )
        self.play(Blink(morty))
        self.play(morty.look_at, self.plane.get_top())
        self.wait()
        self.play(
            morty.look_at, self.plane.get_bottom(),
            *list(map(FadeOut, [bubble, bubble.content]))
        )
        self.play(Blink(morty))
        self.play(FadeOut(morty))

    def show_cutoff(self):
        words = TextMobject("Such an abrupt stop...")
        words.add_background_rectangle()
        words.next_to(ORIGIN, UP+LEFT)
        words.shift(LEFT+UP)

        line = Line(*list(map(self.z_to_point, [
            complex(np.euler_gamma, u*FRAME_Y_RADIUS)
            for u in (1, -1)
        ])))
        line.set_color(YELLOW)
        arrows = [
            Arrow(words.get_right(), point)
            for point in line.get_start_and_end()
        ]

        self.play(Write(words, run_time = 2))
        self.play(ShowCreation(arrows[0]))
        self.play(
            Transform(*arrows),
            ShowCreation(line),
            run_time = 2
        )
        self.play(FadeOut(arrows[0]))
        self.wait(2)
        self.play(*list(map(FadeOut, [words, line])))

    def set_color_i_line(self):
        right_i_lines, left_i_lines = [
            VGroup(*[
                Line(
                    vert_vect+RIGHT,
                    vert_vect+(FRAME_X_RADIUS+1)*horiz_vect
                )
                for vert_vect in (UP, DOWN)
            ])
            for horiz_vect in (RIGHT, LEFT)
        ]
        right_i_lines.set_color(YELLOW)
        left_i_lines.set_color(BLUE)
        for lines in right_i_lines, left_i_lines:
            self.prepare_for_transformation(lines)

        self.restore_mobjects(self.plane)
        self.plane.add(*right_i_lines)
        colored_plane = self.plane.copy()
        right_i_lines.set_stroke(width = 0)
        self.play(
            self.plane.set_stroke, GREY, 1,
        )
        right_i_lines.set_stroke(YELLOW, width = 3)
        self.play(ShowCreation(right_i_lines))
        self.plane.save_state()
        self.wait(2)
        self.apply_zeta_function()
        self.wait(2)

        left_i_lines.save_state()
        left_i_lines.apply_complex_function(zeta)
        self.play(ShowCreation(left_i_lines, run_time = 5))
        self.wait()
        self.restore_mobjects(self.plane, left_i_lines)
        self.play(Transform(self.plane, colored_plane))
        self.wait()
        self.left_i_lines = left_i_lines

    def show_continuation(self):
        reflected_plane = self.get_reflected_plane()
        self.play(ShowCreation(reflected_plane, run_time = 2))
        self.plane.add(reflected_plane)
        self.remove(self.left_i_lines)
        self.wait()
        self.apply_zeta_function()
        self.wait(2)
        self.play(ShowCreation(
            reflected_plane,
            run_time = 6,
            rate_func = lambda t : 1-there_and_back(t)
        ))
        self.wait(2)

    def emphsize_sum_doesnt_make_sense(self):
        brace = Brace(VGroup(*self.zeta[1][3:]))
        words = brace.get_text("""
            Still fails to converge
            when Re$(s) < 1$
        """, buff = SMALL_BUFF)
        words.add_background_rectangle()
        words.scale_in_place(0.8)
        divergent_sum = TexMobject("1+2+3+4+\\cdots")
        divergent_sum.next_to(ORIGIN, UP)
        divergent_sum.to_edge(LEFT)
        divergent_sum.add_background_rectangle()

        self.play(
            GrowFromCenter(brace),
            Write(words)
        )
        self.wait(2)
        self.play(Write(divergent_sum))
        self.wait(2)

    def restore_mobjects(self, *mobjects):
        self.play(*it.chain(*[
            [m.restore, m.make_smooth]
            for m in  mobjects
        ]), run_time = 2)
        for m in mobjects:
            self.remove(m)
            m.restore()
            self.add(m)

class ShowConditionalDefinition(Scene):
    def construct(self):
        zeta = TexMobject("\\zeta(s)=")
        zeta[2].set_color(YELLOW)
        sigma = TexMobject("\\sum_{n=1}^\\infty \\frac{1}{n^s}")
        sigma[-1].set_color(YELLOW)
        something_else = TextMobject("Something else...")
        conditions = VGroup(*[
            TextMobject("if Re$(s) %s 1$"%s)
            for s in (">", "\\le")
        ])
        definitions = VGroup(sigma, something_else)
        definitions.arrange(DOWN, buff = MED_LARGE_BUFF, aligned_edge = LEFT)
        conditions.arrange(DOWN, buff = LARGE_BUFF)
        definitions.shift(2*LEFT+2*UP)
        conditions.next_to(definitions, RIGHT, buff = LARGE_BUFF, aligned_edge = DOWN)
        brace = Brace(definitions, LEFT)
        zeta.next_to(brace, LEFT)

        sigma.save_state()
        sigma.next_to(zeta)
        self.add(zeta, sigma)
        self.wait()
        self.play(
            sigma.restore,
            GrowFromCenter(brace),
            FadeIn(something_else)
        )
        self.play(Write(conditions))
        self.wait()

        underbrace = Brace(something_else)
        question = underbrace.get_text("""
            What to put here?
        """)
        VGroup(underbrace, question).set_color(GREEN_B)

        self.play(
            GrowFromCenter(underbrace),
            Write(question),
            something_else.set_color, GREEN_B
        )
        self.wait(2)

class SquiggleOnExtensions(ZetaTransformationScene):
    CONFIG = {
        "x_min" : 1,
        "x_max" : int(FRAME_X_RADIUS+2),
    }
    def construct(self):
        self.show_negative_one()
        self.cycle_through_options()
        self.lock_into_place()

    def show_negative_one(self):
        self.add_transformable_plane()
        thin_plane = self.plane.copy()
        thin_plane.add(self.get_reflected_plane())
        self.remove(self.plane)
        self.add_extra_plane_lines_for_zeta()
        reflected_plane = self.get_reflected_plane()
        self.plane.add(reflected_plane)
        self.remove(self.plane)
        self.add(thin_plane)

        dot = self.note_point(-1, "-1")
        self.play(
            ShowCreation(self.plane, run_time = 2),
            Animation(dot),
            run_time = 2
        )
        self.remove(thin_plane)
        self.apply_zeta_function(added_anims = [
            ApplyMethod(
                dot.move_to, self.z_to_point(-1./12),
                run_time = 5
            )
        ])
        dot_to_remove = self.note_point(-1./12, "-\\frac{1}{12}")
        self.remove(dot_to_remove)
        self.left_plane = reflected_plane
        self.dot = dot

    def note_point(self, z, label_tex):
        dot = Dot(self.z_to_point(z))
        dot.set_color(YELLOW)
        label = TexMobject(label_tex)
        label.add_background_rectangle()
        label.next_to(dot, UP+LEFT, buff = SMALL_BUFF)
        label.shift(LEFT)
        arrow = Arrow(label.get_right(), dot, buff = SMALL_BUFF)

        self.play(Write(label, run_time = 1))
        self.play(*list(map(ShowCreation, [arrow, dot])))
        self.wait()
        self.play(*list(map(FadeOut, [arrow, label])))
        return dot

    def cycle_through_options(self):
        gamma = np.euler_gamma
        def shear(point):
            x, y, z = point
            return np.array([
                x,
                y+0.25*(1-x)**2,
                0
            ])
        def mixed_scalar_func(point):
            x, y, z = point
            scalar = 1 + (gamma-x)/(gamma+FRAME_X_RADIUS)
            return np.array([
                (scalar**2)*x,
                (scalar**3)*y,
                0
            ])
        def alt_mixed_scalar_func(point):
            x, y, z = point
            scalar = 1 + (gamma-x)/(gamma+FRAME_X_RADIUS)
            return np.array([
                (scalar**5)*x,
                (scalar**2)*y,
                0
            ])
        def sinusoidal_func(point):
            x, y, z = point
            freq = np.pi/gamma
            return np.array([
                x-0.2*np.sin(x*freq)*np.sin(y),
                y-0.2*np.sin(x*freq)*np.sin(y),
                0
            ])
        funcs = [
            shear,
            mixed_scalar_func,
            alt_mixed_scalar_func,
            sinusoidal_func,
        ]
        for mob in self.left_plane.family_members_with_points():
            if np.all(np.abs(mob.points[:,1]) < 0.1):
                self.left_plane.remove(mob)

        new_left_planes = [
            self.left_plane.copy().apply_function(func)
            for func in funcs
        ]
        new_dots = [
            self.dot.copy().move_to(func(self.dot.get_center()))
            for func in funcs
        ]
        self.left_plane.save_state()
        for plane, dot in zip(new_left_planes, new_dots):
            self.play(
                Transform(self.left_plane, plane),
                Transform(self.dot, dot),
                run_time = 3
            )
            self.wait()
        self.play(FadeOut(self.dot))

        #Squiggle on example
        self.wait()
        self.play(FadeOut(self.left_plane))
        self.play(ShowCreation(
            self.left_plane,
            run_time = 5,
            rate_func=linear
        ))
        self.wait()

    def lock_into_place(self):
        words = TextMobject(
            """Only one extension
            has a """,
            "\\emph{derivative}",
            "everywhere",
            alignment = ""
        )
        words.to_corner(UP+LEFT)
        words.set_color_by_tex("\\emph{derivative}", YELLOW)
        words.add_background_rectangle()

        self.play(Write(words))
        self.add_foreground_mobjects(words)
        self.play(self.left_plane.restore)
        self.wait()

class DontKnowDerivatives(TeacherStudentsScene):
    def construct(self):
        self.student_says(
            """
            You said we don't
            need derivatives!
            """,
            target_mode = "pleading"
        )
        self.random_blink(2)
        self.student_says(
            """
            I get $\\frac{df}{dx}$, just not
            for complex functions
            """,
            target_mode = "confused",
            student_index = 2
        )
        self.random_blink(2)
        self.teacher_says(
            """
            Luckily, there's a purely
            geometric intuition here.
            """,
            target_mode = "hooray"
        )
        self.change_student_modes(*["happy"]*3)
        self.random_blink(3)

class IntroduceAnglePreservation(VisualizingSSquared):
    CONFIG = {
        "num_anchors_to_add_per_line" : 50,
        "use_homotopy" : True,
    }
    def construct(self):
        self.add_title()
        self.show_initial_transformation()
        self.talk_about_derivative()
        self.cycle_through_line_pairs()
        self.note_grid_lines()
        self.name_analytic()

    def add_title(self):
        title = TexMobject("f(", "s", ")=", "s", "^2")
        title.set_color_by_tex("s", YELLOW)
        title.scale(1.5)
        title.to_corner(UP+LEFT)
        title.add_background_rectangle()
        self.title = title

        self.add_transformable_plane()
        self.play(Write(title))
        self.add_foreground_mobjects(title)
        self.wait()

    def show_initial_transformation(self):
        self.apply_function()
        self.wait(2)
        self.reset()

    def talk_about_derivative(self):
        randy = Randolph().scale(0.8)
        randy.to_corner(DOWN+LEFT)
        morty = Mortimer()
        morty.to_corner(DOWN+RIGHT)
        randy.make_eye_contact(morty)
        for pi, words in (randy, "$f'(s) = 2s$"), (morty, "Here's some \\\\ related geometry..."):
            pi.bubble = pi.get_bubble(SpeechBubble)
            pi.bubble.set_fill(BLACK, opacity = 0.7)
            pi.bubble.write(words)
            pi.bubble.resize_to_content()
            pi.bubble.pin_to(pi)
        for index in 3, 7:
            randy.bubble.content[index].set_color(YELLOW)

        self.play(*list(map(FadeIn, [randy, morty])))
        self.play(
            randy.change_mode, "speaking",
            ShowCreation(randy.bubble),
            Write(randy.bubble.content)
        )
        self.play(Blink(morty))
        self.wait()
        self.play(
            morty.change_mode, "speaking",
            randy.change_mode, "pondering",
            ShowCreation(morty.bubble),
            Write(morty.bubble.content),
        )
        self.play(Blink(randy))
        self.wait()
        self.play(*list(map(FadeOut, [
            randy, morty,
            randy.bubble, randy.bubble.content,
            morty.bubble, morty.bubble.content,
        ])))


    def cycle_through_line_pairs(self):
        line_pairs = [
            (
                Line(3*DOWN+3*RIGHT, 2*UP),
                Line(DOWN+RIGHT, 3*UP+4*RIGHT)
            ),
            (
                Line(RIGHT+3.5*DOWN, RIGHT+2.5*UP),
                Line(3*LEFT+0.5*UP, 3*RIGHT+0.5*UP),
            ),
            (
                Line(4*RIGHT+4*DOWN, RIGHT+2*UP),
                Line(4*DOWN+RIGHT, 2*UP+2*RIGHT)
            ),
        ]
        for lines in line_pairs:
            self.show_angle_preservation_between_lines(*lines)
            self.reset()

    def note_grid_lines(self):
        intersection_inputs = [
            complex(x, y)
            for x in np.arange(-5, 5, 0.5)
            for y in np.arange(0, 3, 0.5)
            if not (x <= 0 and y == 0)
        ]
        brackets = VGroup(*list(map(
            self.get_right_angle_bracket,
            intersection_inputs
        )))
        self.apply_function()
        self.wait()
        self.play(
            ShowCreation(brackets, run_time = 5),
            Animation(self.plane)
        )
        self.wait()

    def name_analytic(self):
        equiv = TextMobject("``Analytic'' $\\Leftrightarrow$ Angle-preserving")
        kind_of = TextMobject("...kind of")
        for text in equiv, kind_of:
            text.scale(1.2)
            text.add_background_rectangle()
        equiv.set_color(YELLOW)
        kind_of.set_color(RED)
        kind_of.next_to(equiv, RIGHT)
        VGroup(equiv, kind_of).next_to(ORIGIN, UP, buff = 1)

        self.play(Write(equiv))
        self.wait(2)
        self.play(Write(kind_of, run_time = 1))
        self.wait(2)

    def reset(self, faded = True):
        self.play(FadeOut(self.plane))
        self.add_transformable_plane()
        if faded:
            self.plane.fade()
        self.play(FadeIn(self.plane))

    def apply_function(self, **kwargs):
        if self.use_homotopy:
            self.apply_complex_homotopy(
                lambda z, t : z**(1+t),
                run_time = 5,
                **kwargs
            )
        else:
            self.apply_complex_function(
                lambda z : z**2,
                **kwargs
            )

    def show_angle_preservation_between_lines(self, *lines):
        R2_endpoints = [
            [l.get_start()[:2], l.get_end()[:2]]
            for l in lines
        ]
        R2_intersection_point = intersection(*R2_endpoints)
        intersection_point = np.array(list(R2_intersection_point) + [0])

        angle1, angle2 = [l.get_angle() for l in lines]
        arc = Arc(
            start_angle = angle1,
            angle = angle2-angle1,
            radius = 0.4,
            color = YELLOW
        )
        arc.shift(intersection_point)
        arc.insert_n_curves(10)
        arc.generate_target()
        input_z = complex(*arc.get_center()[:2])
        scale_factor = abs(2*input_z)
        arc.target.scale_about_point(1./scale_factor, intersection_point)
        arc.target.apply_complex_function(lambda z : z**2)

        angle_tex = TexMobject(
            "%d^\\circ"%abs(int((angle2-angle1)*180/np.pi))
        )
        angle_tex.set_color(arc.get_color())
        angle_tex.add_background_rectangle()
        self.put_angle_tex_next_to_arc(angle_tex, arc)
        angle_arrow = Arrow(
            angle_tex, arc,
            color = arc.get_color(),
            buff = 0.1,
        )
        angle_group = VGroup(angle_tex, angle_arrow)


        self.play(*list(map(ShowCreation, lines)))
        self.play(
            Write(angle_tex),
            ShowCreation(angle_arrow),
            ShowCreation(arc)
        )
        self.wait()

        self.play(FadeOut(angle_group))
        self.plane.add(*lines)
        self.apply_function(added_anims = [
            MoveToTarget(arc, run_time = 5)
        ])
        self.put_angle_tex_next_to_arc(angle_tex, arc)
        arrow = Arrow(angle_tex, arc, buff = 0.1)
        arrow.set_color(arc.get_color())
        self.play(
            Write(angle_tex),
            ShowCreation(arrow)
        )
        self.wait(2)
        self.play(*list(map(FadeOut, [arc, angle_tex, arrow])))

    def put_angle_tex_next_to_arc(self, angle_tex, arc):
        vect = arc.point_from_proportion(0.5)-interpolate(
            arc.points[0], arc.points[-1], 0.5
        )
        unit_vect = vect/get_norm(vect)
        angle_tex.move_to(arc.get_center() + 1.7*unit_vect)

    def get_right_angle_bracket(self, input_z):
        output_z = input_z**2
        derivative = 2*input_z
        rotation = np.log(derivative).imag

        brackets = VGroup(
            Line(RIGHT, RIGHT+UP),
            Line(RIGHT+UP, UP)
        )
        brackets.scale(0.15)
        brackets.set_stroke(width = 2)
        brackets.set_color(YELLOW)
        brackets.shift(0.02*UP) ##Why???
        brackets.rotate(rotation, about_point = ORIGIN)
        brackets.shift(self.z_to_point(output_z))
        return brackets

class AngleAtZeroDerivativePoints(IntroduceAnglePreservation):
    CONFIG = {
        "use_homotopy" : True
    }
    def construct(self):
        self.add_title()
        self.is_before_transformation = True
        self.add_transformable_plane()
        self.plane.fade()
        line = Line(3*LEFT+0.5*UP, 3*RIGHT+0.5*DOWN)
        self.show_angle_preservation_between_lines(
            line, line.copy().rotate(np.pi/5)
        )
        self.wait()

    def add_title(self):
        title = TexMobject("f(", "s", ")=", "s", "^2")
        title.set_color_by_tex("s", YELLOW)
        title.scale(1.5)
        title.to_corner(UP+LEFT)
        title.add_background_rectangle()
        derivative = TexMobject("f'(0) = 0")
        derivative.set_color(RED)
        derivative.scale(1.2)
        derivative.add_background_rectangle()
        derivative.next_to(title, DOWN)

        self.add_foreground_mobjects(title, derivative)


    def put_angle_tex_next_to_arc(self, angle_tex, arc):
        IntroduceAnglePreservation.put_angle_tex_next_to_arc(
            self, angle_tex, arc
        )
        if not self.is_before_transformation:
            two_dot = TexMobject("2 \\times ")
            two_dot.set_color(angle_tex.get_color())
            two_dot.next_to(angle_tex, LEFT, buff = SMALL_BUFF)
            two_dot.add_background_rectangle()
            center = angle_tex.get_center()
            angle_tex.add_to_back(two_dot)
            angle_tex.move_to(center)
        else:
            self.is_before_transformation = False

class AnglePreservationAtAnyPairOfPoints(IntroduceAnglePreservation):
    def construct(self):
        self.add_transformable_plane()
        self.plane.fade()
        line_pairs = self.get_line_pairs()
        line_pair = line_pairs[0]
        for target_pair in line_pairs[1:]:
            self.play(Transform(
                line_pair, target_pair,
                run_time = 2,
                path_arc = np.pi
            ))
            self.wait()
        self.show_angle_preservation_between_lines(*line_pair)
        self.show_example_analytic_functions()

    def get_line_pairs(self):
        return list(it.starmap(VGroup, [
            (
                Line(3*DOWN, 3*LEFT+2*UP),
                Line(2*LEFT+DOWN, 3*UP+RIGHT)
            ),
            (
                Line(2*RIGHT+DOWN, 3*LEFT+2*UP),
                Line(LEFT+3*DOWN, 4*RIGHT+3*UP),
            ),
            (
                Line(LEFT+3*DOWN, LEFT+3*UP),
                Line(5*LEFT+UP, 3*RIGHT+UP)
            ),
            (
                Line(4*RIGHT+3*DOWN, RIGHT+2*UP),
                Line(3*DOWN+RIGHT, 2*UP+2*RIGHT)
            ),
        ]))

    def show_example_analytic_functions(self):
        words = TextMobject("Examples of analytic functions:")
        words.shift(2*UP)
        words.set_color(YELLOW)
        words.add_background_rectangle()
        words.next_to(UP, UP).to_edge(LEFT)
        functions = TextMobject(
            "$e^x$, ",
            "$\\sin(x)$, ",
            "any polynomial, "
            "$\\log(x)$, ",
            "\\dots",
        )
        functions.next_to(ORIGIN, UP).to_edge(LEFT)
        for function in functions:
            function.add_to_back(BackgroundRectangle(function))

        self.play(Write(words))
        for function in functions:
            self.play(FadeIn(function))
        self.wait()

class NoteZetaFunctionAnalyticOnRightHalf(ZetaTransformationScene):
    CONFIG = {
        "anchor_density" : 35,
    }
    def construct(self):
        self.add_title()
        self.add_transformable_plane(animate = False)
        self.add_extra_plane_lines_for_zeta(animate = True)
        self.apply_zeta_function()
        self.note_right_angles()

    def add_title(self):
        title = TexMobject(
            "\\zeta(s) = \\sum_{n=1}^\\infty \\frac{1}{n^s}"
        )
        title[2].set_color(YELLOW)
        title[-1].set_color(YELLOW)
        title.add_background_rectangle()
        title.to_corner(UP+LEFT)
        self.add_foreground_mobjects(title)

    def note_right_angles(self):
        intersection_inputs = [
            complex(x, y)
            for x in np.arange(1+2./16, 1.4, 1./16)
            for y in np.arange(-0.5, 0.5, 1./16)
            if abs(y) > 1./16
        ]
        brackets = VGroup(*list(map(
            self.get_right_angle_bracket,
            intersection_inputs
        )))
        self.play(ShowCreation(brackets, run_time = 3))
        self.wait()

    def get_right_angle_bracket(self, input_z):
        output_z = zeta(input_z)
        derivative = d_zeta(input_z)
        rotation = np.log(derivative).imag

        brackets = VGroup(
            Line(RIGHT, RIGHT+UP),
            Line(RIGHT+UP, UP)
        )
        brackets.scale(0.1)
        brackets.set_stroke(width = 2)
        brackets.set_color(YELLOW)
        brackets.rotate(rotation, about_point = ORIGIN)
        brackets.shift(self.z_to_point(output_z))
        return brackets

class InfiniteContinuousJigsawPuzzle(ZetaTransformationScene):
    CONFIG = {
        "anchor_density" : 35,
    }
    def construct(self):
        self.set_stage()
        self.add_title()
        self.show_jigsaw()
        self.name_analytic_continuation()

    def set_stage(self):
        self.plane = self.get_dense_grid()
        left_plane = self.get_reflected_plane()
        self.plane.add(left_plane)
        self.apply_zeta_function(run_time = 0)
        self.remove(left_plane)
        lines_per_piece = 5
        pieces = [
            VGroup(*left_plane[lines_per_piece*i:lines_per_piece*(i+1)])
            for i in range(len(list(left_plane))/lines_per_piece)
        ]
        random.shuffle(pieces)
        self.pieces = pieces

    def add_title(self):
        title = TextMobject("Infinite ", "continuous ", "jigsaw puzzle")
        title.scale(1.5)
        title.to_edge(UP)
        for word in title:
            word.add_to_back(BackgroundRectangle(word))
            self.play(FadeIn(word))
        self.wait()
        self.add_foreground_mobjects(title)
        self.title = title

    def show_jigsaw(self):
        for piece in self.pieces:
            self.play(FadeIn(piece, run_time = 0.5))
        self.wait()

    def name_analytic_continuation(self):
        words = TextMobject("``Analytic continuation''")
        words.set_color(YELLOW)
        words.scale(1.5)
        words.next_to(self.title, DOWN, buff = LARGE_BUFF)
        words.add_background_rectangle()
        self.play(Write(words))
        self.wait()

class ThatsHowZetaIsDefined(TeacherStudentsScene):
    def construct(self):
        self.add_zeta_definition()
        self.teacher_says("""
            So that's how
            $\\zeta(s)$ is defined
        """)
        self.change_student_modes(*["hooray"]*3)
        self.random_blink(2)

    def add_zeta_definition(self):
        zeta = TexMobject(
            "\\zeta(s) = \\sum_{n=1}^\\infty \\frac{1}{n^s}"
        )
        VGroup(zeta[2], zeta[-1]).set_color(YELLOW)
        zeta.to_corner(UP+LEFT)
        self.add(zeta)

class ManyIntersectingLinesPreZeta(ZetaTransformationScene):
    CONFIG = {
        "apply_zeta" : False,
        "lines_center" : RIGHT,
        "nudge_size" : 0.9,
        "function" : zeta,
        "angle" : np.pi/5,
        "arc_scale_factor" : 0.3,
        "shift_directions" : [LEFT, RIGHT],
    }
    def construct(self):
        self.establish_plane()
        self.add_title()

        line = Line(DOWN+2*LEFT, UP+2*RIGHT)
        lines = VGroup(line, line.copy().rotate(self.angle))
        arc = Arc(start_angle = line.get_angle(), angle = self.angle)
        arc.scale(self.arc_scale_factor)
        arc.set_color(YELLOW)
        lines.add(arc)
        # lines.set_stroke(WHITE, width = 5)
        lines.shift(self.lines_center + self.nudge_size*RIGHT)

        if self.apply_zeta:
            self.apply_zeta_function(run_time = 0)
            lines.set_stroke(width = 0)

        added_anims = self.get_modified_line_anims(lines)
        for vect in self.shift_directions:
            self.play(
                ApplyMethod(lines.shift, 2*self.nudge_size*vect, path_arc = np.pi),
                *added_anims,
                run_time = 3
            )

    def establish_plane(self):
        self.add_transformable_plane()
        self.add_extra_plane_lines_for_zeta()
        self.add_reflected_plane()
        self.plane.fade()


    def add_title(self):
        if self.apply_zeta:
            title = TextMobject("After \\\\ transformation")
        else:
            title = TextMobject("Before \\\\ transformation")
        title.add_background_rectangle()
        title.to_edge(UP)
        self.add_foreground_mobjects(title)

    def get_modified_line_anims(self, lines):
        return []

class ManyIntersectingLinesPostZeta(ManyIntersectingLinesPreZeta):
    CONFIG = {
        "apply_zeta" : True,
        # "anchor_density" : 5
    }
    def get_modified_line_anims(self, lines):
        n_inserted_points = 30
        new_lines = lines.copy()
        new_lines.set_stroke(width = 5)
        def update_new_lines(lines_to_update):
            transformed = lines.copy()
            self.prepare_for_transformation(transformed)
            transformed.apply_complex_function(self.function)
            transformed.make_smooth()
            transformed.set_stroke(width = 5)
            for start, end in zip(lines_to_update, transformed):
                if start.get_num_points() > 0:
                    start.points = np.array(end.points)
        return [UpdateFromFunc(new_lines, update_new_lines)]

class ManyIntersectingLinesPreSSquared(ManyIntersectingLinesPreZeta):
    CONFIG = {
        "x_min" : -int(FRAME_X_RADIUS),
        "apply_zeta" : False,
        "lines_center" : ORIGIN,
        "nudge_size" : 0.9,
        "function" : lambda z : z**2,
        "shift_directions" : [LEFT, RIGHT, UP, DOWN, DOWN+LEFT, UP+RIGHT],
    }
    def establish_plane(self):
        self.add_transformable_plane()
        self.plane.fade()

    def apply_zeta_function(self, **kwargs):
        self.apply_complex_function(self.function, **kwargs)

class ManyIntersectingLinesPostSSquared(ManyIntersectingLinesPreSSquared):
    CONFIG = {
        "apply_zeta" : True,
    }
    def get_modified_line_anims(self, lines):
        n_inserted_points = 30
        new_lines = lines.copy()
        new_lines.set_stroke(width = 5)
        def update_new_lines(lines_to_update):
            transformed = lines.copy()
            self.prepare_for_transformation(transformed)
            transformed.apply_complex_function(self.function)
            transformed.make_smooth()
            transformed.set_stroke(width = 5)
            for start, end in zip(lines_to_update, transformed):
                if start.get_num_points() > 0:
                    start.points = np.array(end.points)
        return [UpdateFromFunc(new_lines, update_new_lines)]

class ButWhatIsTheExensions(TeacherStudentsScene):
    def construct(self):
        self.student_says(
            """
            But what exactly \\emph{is}
            that continuation?
            """,
            target_mode = "sassy"
        )
        self.change_student_modes("confused", "sassy", "confused")
        self.random_blink(2)
        self.teacher_says("""
            You're $\\$1{,}000{,}000$ richer
            if you can answer
            that fully
        """, target_mode = "shruggie")
        self.change_student_modes(*["pondering"]*3)
        self.random_blink(3)

class MathematiciansLookingAtFunctionEquation(Scene):
    def construct(self):
        equation = TexMobject(
            "\\zeta(s)",
            "= 2^s \\pi ^{s-1}",
            "\\sin\\left(\\frac{\\pi s}{2}\\right)",
            "\\Gamma(1-s)",
            "\\zeta(1-s)",
        )
        equation.shift(UP)

        mathy = Mathematician().to_corner(DOWN+LEFT)
        mathys = VGroup(mathy)
        for x in range(2):
            mathys.add(Mathematician().next_to(mathys))
        for mathy in mathys:
            mathy.change_mode("pondering")
            mathy.look_at(equation)

        self.add(mathys)
        self.play(Write(VGroup(*equation[:-1])))
        self.play(Transform(
            equation[0].copy(),
            equation[-1],
            path_arc = -np.pi/3,
            run_time = 2
        ))
        for mathy in mathys:
            self.play(Blink(mathy))
        self.wait()

class DiscussZeros(ZetaTransformationScene):
    def construct(self):
        self.establish_plane()
        self.ask_about_zeros()
        self.show_trivial_zeros()
        self.show_critical_strip()
        self.transform_bit_of_critical_line()
        self.extend_transformed_critical_line()

    def establish_plane(self):
        self.add_transformable_plane()
        self.add_extra_plane_lines_for_zeta()
        self.add_reflected_plane()
        self.plane.fade()

    def ask_about_zeros(self):
        dots = VGroup(*[
            Dot(
                (2+np.sin(12*alpha))*\
                rotate_vector(RIGHT, alpha+nudge)
            )
            for alpha in np.arange(3*np.pi/20, 2*np.pi, 2*np.pi/5)
            for nudge in [random.random()*np.pi/6]
        ])
        dots.set_color(YELLOW)
        q_marks = VGroup(*[
            TexMobject("?").next_to(dot, UP)
            for dot in dots
        ])
        arrows = VGroup(*[
            Arrow(dot, ORIGIN, buff = 0.2, tip_length = 0.1)
            for dot in dots
        ])
        question = TextMobject("Which numbers go to $0$?")
        question.add_background_rectangle()
        question.to_edge(UP)

        for mob in dots, arrows, q_marks:
            self.play(ShowCreation(mob))
        self.play(Write(question))
        self.wait(2)
        dots.generate_target()
        for i, dot in enumerate(dots.target):
            dot.move_to(2*(i+1)*LEFT)
        self.play(
            FadeOut(arrows),
            FadeOut(q_marks),
            FadeOut(question),
            MoveToTarget(dots),
        )
        self.wait()
        self.dots = dots

    def show_trivial_zeros(self):
        trivial_zero_words = TextMobject("``Trivial'' zeros")
        trivial_zero_words.next_to(ORIGIN, UP)
        trivial_zero_words.to_edge(LEFT)

        randy = Randolph().flip()
        randy.to_corner(DOWN+RIGHT)
        bubble = randy.get_bubble()
        bubble.set_fill(BLACK, opacity = 0.8)
        bubble.write("$1^1 + 2^2 + 3^2 + \\cdots = 0$")
        bubble.resize_to_content()
        bubble.pin_to(randy)

        self.plane.save_state()
        self.dots.save_state()
        for dot in self.dots.target:
            dot.move_to(ORIGIN)
        self.apply_zeta_function(
            added_anims = [MoveToTarget(self.dots, run_time = 3)],
            run_time = 3
        )
        self.wait(3)
        self.play(
            self.plane.restore,
            self.plane.make_smooth,
            self.dots.restore,
            run_time = 2
        )
        self.remove(*self.get_mobjects_from_last_animation())
        self.plane.restore()
        self.dots.restore()
        self.add(self.plane, self.dots)

        self.play(Write(trivial_zero_words))
        self.wait()
        self.play(FadeIn(randy))
        self.play(
            randy.change_mode, "confused",
            ShowCreation(bubble),
            Write(bubble.content)
        )
        self.play(Blink(randy))
        self.wait()
        self.play(Blink(randy))
        self.play(*list(map(FadeOut, [
            randy, bubble, bubble.content, trivial_zero_words
        ])))

    def show_critical_strip(self):
        strip = Rectangle(
            height = FRAME_HEIGHT,
            width = 1
        )
        strip.next_to(ORIGIN, RIGHT, buff = 0)
        strip.set_stroke(width = 0)
        strip.set_fill(YELLOW, opacity = 0.3)
        name = TextMobject("Critical strip")
        name.add_background_rectangle()
        name.next_to(ORIGIN, LEFT)
        name.to_edge(UP)
        arrow = Arrow(name.get_bottom(), 0.5*RIGHT+UP)
        primes = TexMobject("2, 3, 5, 7, 11, 13, 17, \\dots")
        primes.to_corner(UP+RIGHT)
        # photo = Square()
        photo = ImageMobject("Riemann", invert = False)
        photo.set_width(5)
        photo.to_corner(UP+LEFT)
        new_dots = VGroup(*[
            Dot(0.5*RIGHT + y*UP)
            for y in np.linspace(-2.5, 3.2, 5)
        ])
        new_dots.set_color(YELLOW)
        critical_line = Line(
            0.5*RIGHT+FRAME_Y_RADIUS*DOWN,
            0.5*RIGHT+FRAME_Y_RADIUS*UP,
            color = YELLOW
        )

        self.give_dots_wandering_anims()

        self.play(FadeIn(strip), *self.get_dot_wandering_anims())
        self.play(
            Write(name, run_time = 1),
            ShowCreation(arrow),
            *self.get_dot_wandering_anims()
        )
        self.play(*self.get_dot_wandering_anims())
        self.play(
            FadeIn(primes),
            *self.get_dot_wandering_anims()
        )
        for x in range(7):
            self.play(*self.get_dot_wandering_anims())
        self.play(
            GrowFromCenter(photo),
            FadeOut(name),
            FadeOut(arrow),
            *self.get_dot_wandering_anims()
        )
        self.play(Transform(self.dots, new_dots))
        self.play(ShowCreation(critical_line))
        self.wait(3)
        self.play(
            photo.shift, 7*LEFT,
            *list(map(FadeOut, [
            primes, self.dots, strip
            ]))
        )
        self.remove(photo)
        self.critical_line = critical_line

    def give_dots_wandering_anims(self):
        def func(t):
            result = (np.sin(6*2*np.pi*t) + 1)*RIGHT/2
            result += 3*np.cos(2*2*np.pi*t)*UP
            return result

        self.wandering_path = ParametricFunction(func)
        for i, dot in enumerate(self.dots):
            dot.target = dot.copy()
            q_mark = TexMobject("?")
            q_mark.next_to(dot.target, UP)
            dot.target.add(q_mark)
            dot.target.move_to(self.wandering_path.point_from_proportion(
                (float(2+2*i)/(4*len(list(self.dots))))%1
            ))
        self.dot_anim_count = 0

    def get_dot_wandering_anims(self):
        self.dot_anim_count += 1
        if self.dot_anim_count == 1:
            return list(map(MoveToTarget, self.dots))
        denom = 4*(len(list(self.dots)))
        def get_rate_func(index):
            return lambda t : (float(self.dot_anim_count + 2*index + t)/denom)%1
        return [
            MoveAlongPath(
                dot, self.wandering_path,
                rate_func = get_rate_func(i)
            )
            for i, dot in enumerate(self.dots)
        ]

    def transform_bit_of_critical_line(self):
        self.play(
            self.plane.scale, 0.8,
            self.critical_line.scale, 0.8,
            rate_func = there_and_back,
            run_time = 2
        )
        self.wait()
        self.play(
            self.plane.set_stroke, GREY, 1,
            Animation(self.critical_line)
        )
        self.plane.add(self.critical_line)
        self.apply_zeta_function()
        self.wait(2)
        self.play(
            self.plane.fade,
            Animation(self.critical_line)
        )

    def extend_transformed_critical_line(self):
        def func(t):
            z = zeta(complex(0.5, t))
            return z.real*RIGHT + z.imag*UP
        full_line = VGroup(*[
            ParametricFunction(func, t_min = t0, t_max = t0+1)
            for t0 in range(100)
        ])
        full_line.set_color_by_gradient(
            YELLOW, BLUE, GREEN, RED, YELLOW, BLUE, GREEN, RED,
        )
        self.play(ShowCreation(full_line, run_time = 20, rate_func=linear))
        self.wait()

class AskAboutRelationToPrimes(TeacherStudentsScene):
    def construct(self):
        self.student_says("""
            Whoa!  Where the heck
            do primes come in here?
        """, target_mode = "confused")
        self.random_blink(3)
        self.teacher_says("""
            Perhaps in a
            different video.
        """, target_mode = "hesitant")
        self.random_blink(3)

class HighlightCriticalLineAgain(DiscussZeros):
    def construct(self):
        self.establish_plane()
        title = TexMobject("\\zeta(", "s", ") = 0")
        title.set_color_by_tex("s", YELLOW)
        title.add_background_rectangle()
        title.to_corner(UP+LEFT)
        self.add(title)

        strip = Rectangle(
            height = FRAME_HEIGHT,
            width = 1
        )
        strip.next_to(ORIGIN, RIGHT, buff = 0)
        strip.set_stroke(width = 0)
        strip.set_fill(YELLOW, opacity = 0.3)
        line = Line(
            0.5*RIGHT+FRAME_Y_RADIUS*UP,
            0.5*RIGHT+FRAME_Y_RADIUS*DOWN,
            color = YELLOW
        )
        randy = Randolph().to_corner(DOWN+LEFT)
        million = TexMobject("\\$1{,}000{,}000")
        million.set_color(GREEN_B)
        million.next_to(ORIGIN, UP+LEFT)
        million.shift(2*LEFT)
        arrow1 = Arrow(million.get_right(), line.get_top())
        arrow2 = Arrow(million.get_right(), line.get_bottom())

        self.add(randy, strip)
        self.play(Write(million))
        self.play(
            randy.change_mode, "pondering",
            randy.look_at, line.get_top(),
            ShowCreation(arrow1),
            run_time = 3
        )
        self.play(
            randy.look_at, line.get_bottom(),
            ShowCreation(line),
            Transform(arrow1, arrow2)
        )
        self.play(FadeOut(arrow1))
        self.play(Blink(randy))
        self.wait()
        self.play(randy.look_at, line.get_center())
        self.play(randy.change_mode, "confused")
        self.play(Blink(randy))
        self.wait()
        self.play(randy.change_mode, "pondering")
        self.wait()

class DiscussSumOfNaturals(Scene):
    def construct(self):
        title = TexMobject(
            "\\zeta(s) = \\sum_{n=1}^\\infty \\frac{1}{n^s}"
        )
        VGroup(title[2], title[-1]).set_color(YELLOW)
        title.to_corner(UP+LEFT)

        neg_twelfth, eq, zeta_neg_1, sum_naturals = equation = TexMobject(
            "-\\frac{1}{12}",
            "=",
            "\\zeta(-1)",
            "= 1 + 2 + 3 + 4 + \\cdots"
        )
        neg_twelfth.set_color(GREEN_B)
        VGroup(*zeta_neg_1[2:4]).set_color(YELLOW)
        q_mark = TexMobject("?").next_to(sum_naturals[0], UP)
        q_mark.set_color(RED)
        randy = Randolph()
        randy.to_corner(DOWN+LEFT)
        analytic_continuation = TextMobject("Analytic continuation")
        analytic_continuation.next_to(title, RIGHT, 3*LARGE_BUFF)

        sum_to_zeta = Arrow(title.get_corner(DOWN+RIGHT), zeta_neg_1)
        sum_to_ac = Arrow(title.get_right(), analytic_continuation)
        ac_to_zeta = Arrow(analytic_continuation.get_bottom(), zeta_neg_1.get_top())
        cross = TexMobject("\\times")
        cross.scale(2)
        cross.set_color(RED)
        cross.rotate(np.pi/6)
        cross.move_to(sum_to_zeta.get_center())

        brace = Brace(VGroup(zeta_neg_1, sum_naturals))
        words = TextMobject(
            "If not equal, at least connected",
            "\\\\(see links in description)"
        )
        words.next_to(brace, DOWN)

        self.add(neg_twelfth, eq, zeta_neg_1, randy, title)
        self.wait()
        self.play(
            Write(sum_naturals),
            Write(q_mark),
            randy.change_mode, "confused"
        )
        self.play(Blink(randy))
        self.wait()
        self.play(randy.change_mode, "angry")
        self.play(
            ShowCreation(sum_to_zeta),
            Write(cross)
        )
        self.play(Blink(randy))
        self.wait()
        self.play(
            Transform(sum_to_zeta, sum_to_ac),
            FadeOut(cross),
            Write(analytic_continuation),
            randy.change_mode, "pondering",
            randy.look_at, analytic_continuation,
        )
        self.play(ShowCreation(ac_to_zeta))
        self.play(Blink(randy))
        self.wait()
        self.play(
            GrowFromCenter(brace),
            Write(words[0]),
            randy.look_at, words[0],
        )
        self.wait()
        self.play(FadeIn(words[1]))
        self.play(Blink(randy))
        self.wait()

class InventingMathPreview(Scene):
    def construct(self):
        rect = Rectangle(height = 9, width = 16)
        rect.set_height(4)
        title = TextMobject("What does it feel like to invent math?")
        title.next_to(rect, UP)
        sum_tex = TexMobject("1+2+4+8+\\cdots = -1")
        sum_tex.set_width(rect.get_width()-1)

        self.play(
            ShowCreation(rect),
            Write(title)
        )
        self.play(Write(sum_tex))
        self.wait()

class FinalAnimationTease(Scene):
    def construct(self):
        morty = Mortimer().shift(2*(DOWN+RIGHT))
        bubble = morty.get_bubble(SpeechBubble)
        bubble.write("""
            Want to know what
            $\\zeta'(s)$ looks like?
        """)

        self.add(morty)
        self.play(
            morty.change_mode, "hooray",
            morty.look_at, bubble.content,
            ShowCreation(bubble),
            Write(bubble.content)
        )
        self.play(Blink(morty))
        self.wait()

class PatreonThanks(Scene):
    CONFIG = {
        "specific_patrons" : [
            "CrypticSwarm",
            "Ali Yahya",
            "Damion Kistler",
            "Juan Batiz-Benet",
            "Yu Jun",
            "Othman Alikhan",
            "Markus Persson",
            "Joseph John Cox",
            "Luc Ritchie",
            "Shimin Kuang",
            "Einar Johansen",
            "Rish Kundalia",
            "Achille Brighton",
            "Kirk Werklund",
            "Ripta Pasay",
            "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(3*UP)
        patreon_logo = ImageMobject("patreon", invert = False)
        patreon_logo.set_height(1.5)
        patreon_logo.next_to(special_thanks, DOWN)

        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.add(patreon_logo)
        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/3blue1brown")
        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")
        for x in range(4):
            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
        )
        for x in range(10):
            self.play(Blink(morty))
            self.wait()
            self.play(Blink(brother))
            self.wait()

class FinalAnimation(ZetaTransformationScene):
    CONFIG = {
        "min_added_anchors" : 100,
    }
    def construct(self):
        self.add_transformable_plane()
        self.add_extra_plane_lines_for_zeta()
        self.add_reflected_plane()
        title = TexMobject("s", "\\to \\frac{d\\zeta}{ds}(", "s", ")")
        title.set_color_by_tex("s", YELLOW)
        title.add_background_rectangle()
        title.scale(1.5)
        title.to_corner(UP+LEFT)

        self.play(Write(title))
        self.add_foreground_mobjects(title)
        self.wait()
        self.apply_complex_function(d_zeta, run_time = 8)
        self.wait()

class Thumbnail(ZetaTransformationScene):
    CONFIG = {
        "anchor_density" : 35
    }
    def construct(self):
        self.y_min = -4
        self.y_max = 4
        self.x_min = 1
        self.x_max = int(FRAME_X_RADIUS+2)
        self.add_transformable_plane()
        self.add_extra_plane_lines_for_zeta()
        self.add_reflected_plane()
        # self.apply_zeta_function()
        self.plane.set_stroke(width = 4)

        div_sum = TexMobject("-\\frac{1}{12} = ", "1+2+3+4+\\cdots")
        div_sum.set_width(FRAME_WIDTH-1)
        div_sum.to_edge(DOWN)
        div_sum.set_color(YELLOW)
        div_sum.set_background_stroke(width=8)
        # for mob in div_sum.submobjects:
        #     mob.add_to_back(BackgroundRectangle(mob))

        zeta = TexMobject("\\zeta(s)")
        zeta.set_height(FRAME_Y_RADIUS-1)
        zeta.to_corner(UP+LEFT)

        million = TexMobject("\\$1{,}000{,}000")
        million.set_width(FRAME_X_RADIUS+1)
        million.to_edge(UP+RIGHT)
        million.set_color(GREEN_B)
        million.set_background_stroke(width=8)

        self.add(div_sum, million, zeta)


class ZetaPartialSums(ZetaTransformationScene):
    CONFIG = {
        "anchor_density" : 35,
        "num_partial_sums" : 12,
    }
    def construct(self):
        self.add_transformable_plane()
        self.add_extra_plane_lines_for_zeta()
        self.prepare_for_transformation(self.plane)

        N_list = [2**k for k in range(self.num_partial_sums)]
        sigma = TexMobject(
            "\\sum_{n = 1}^N \\frac{1}{n^s}"
        )
        sigmas = []
        for N in N_list + ["\\infty"]:
            tex = TexMobject(str(N))
            tex.set_color(YELLOW)
            new_sigma = sigma.copy()
            top = new_sigma[0]
            tex.move_to(top, DOWN)
            new_sigma.remove(top)
            new_sigma.add(tex)
            new_sigma.to_corner(UP+LEFT)
            sigmas.append(new_sigma)

        def get_partial_sum_func(n_terms):
            return lambda s : sum([1./(n**s) for n in range(1, n_terms+1)])
        interim_planes = [
            self.plane.copy().apply_complex_function(
                get_partial_sum_func(N)
            )
            for N in N_list
        ]
        interim_planes.append(self.plane.copy().apply_complex_function(zeta))
        symbol = VGroup(TexMobject("s"))
        symbol.scale(2)
        symbol.set_color(YELLOW)
        symbol.to_corner(UP+LEFT)
        for plane, sigma in zip(interim_planes, sigmas):
            self.play(
                Transform(self.plane, plane),
                Transform(symbol, sigma)
            )
            self.wait()
