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


from big_ol_pile_of_manim_imports import *
from once_useful_constructs.light import AmbientLight
from once_useful_constructs.light import Lighthouse
from once_useful_constructs.light import SwitchOn
from functools import reduce
# from once_useful_constructs.light import LightSource

PRODUCT_COLOR = BLUE
DEFAULT_OPACITY_FUNCTION = inverse_power_law(1, 1.5, 1, 4)
CHEAP_AMBIENT_LIGHT_CONFIG = {
    "num_levels": 5,
    "radius": 0.25,
    "opacity_function": DEFAULT_OPACITY_FUNCTION,
}
HIGHT_QUALITY_AMBIENT_LIGHT_CONFIG = {
    "opacity_function": DEFAULT_OPACITY_FUNCTION,
    "num_levels": 100,
    "radius": 5,
    "max_opacity": 0.8,
    "color": PRODUCT_COLOR,
}


def get_chord_f_label(chord, arg="f", direction=DOWN):
    chord_f = TextMobject("Chord(", "$%s$" % arg, ")", arg_separator="")
    chord_f.set_color_by_tex("$%s$" % arg, YELLOW)
    chord_f.add_background_rectangle()
    chord_f.next_to(chord.get_center(), direction, SMALL_BUFF)
    angle = ((chord.get_angle() + TAU / 2) % TAU) - TAU / 2
    if np.abs(angle) > TAU / 4:
        angle += TAU / 2
    chord_f.rotate(angle, about_point=chord.get_center())
    chord_f.angle = angle
    return chord_f


class WallisNumeratorDenominatorGenerator(object):
    def __init__(self):
        self.n = 0

    def __iter__(self):
        return self

    def __next__(self):
        return next(self)

    def __next__(self):
        n = self.n
        self.n += 1
        if n % 2 == 0:
            return (n + 2, n + 1)
        else:
            return (n + 1, n + 2)


def get_wallis_product(n_terms=6, show_result=True):
    tex_mob_args = []
    nd_generator = WallisNumeratorDenominatorGenerator()
    for x in range(n_terms):
        numerator, denominator = next(nd_generator)
        tex_mob_args += [
            "{%d" % numerator, "\\over", "%d}" % denominator, "\\cdot"
        ]
    tex_mob_args[-1] = "\\cdots"
    if show_result:
        tex_mob_args += ["=", "{\\pi", "\\over", "2}"]

    result = TexMobject(*tex_mob_args)
    return result


def get_wallis_product_numerical_terms(n_terms=20):
    result = []
    nd_generator = WallisNumeratorDenominatorGenerator()
    for x in range(n_terms):
        n, d = next(nd_generator)
        result.append(float(n) / d)
    return result


# Scenes

class Introduction(Scene):
    def construct(self):
        n_terms = 10

        number_line = NumberLine(
            x_min=0,
            x_max=2,
            unit_size=5,
            tick_frequency=0.25,
            numbers_with_elongated_ticks=[0, 1, 2],
            color=LIGHT_GREY,
        )
        number_line.add_numbers()
        number_line.move_to(DOWN)

        numerical_terms = get_wallis_product_numerical_terms(400)
        partial_products = np.cumprod(numerical_terms)
        curr_product = partial_products[0]

        arrow = Vector(DOWN, color=YELLOW)

        def get_arrow_update():
            return ApplyFunction(
                lambda mob: mob.next_to(
                    number_line.number_to_point(curr_product),
                    UP, SMALL_BUFF
                ),
                arrow,
            )
        get_arrow_update().update(1)
        decimal = DecimalNumber(
            curr_product, num_decimal_places=5, show_ellipsis=True)
        decimal.next_to(arrow, UP, SMALL_BUFF, submobject_to_align=decimal[:5])
        decimal_anim = ChangingDecimal(
            decimal,
            lambda a: number_line.point_to_number(arrow.get_center()),
            tracked_mobject=arrow
        )

        product_mob = get_wallis_product(n_terms)
        product_mob.to_edge(UP)

        rects = VGroup(*[
            SurroundingRectangle(product_mob[:n])
            for n in list(range(3, 4 * n_terms, 4)) + [4 * n_terms]
        ])
        rect = rects[0].copy()

        pi_halves_arrow = Vector(UP, color=BLUE)
        pi_halves_arrow.next_to(
            number_line.number_to_point(np.pi / 2), DOWN, SMALL_BUFF
        )
        pi_halves_term = TexMobject("\\pi / 2")
        pi_halves_term.next_to(pi_halves_arrow, DOWN)

        self.add(product_mob, number_line, rect, arrow, decimal)
        self.add(pi_halves_arrow, pi_halves_term)
        for n in range(1, len(rects)):
            curr_product = partial_products[n]
            self.play(
                get_arrow_update(),
                decimal_anim,
                Transform(rect, rects[n]),
                run_time=0.5
            )
            self.wait(0.5)

        for n in range(len(rects), len(numerical_terms), 31):
            curr_product = partial_products[n]
            self.play(
                get_arrow_update(),
                decimal_anim,
                run_time=0.25
            )
        curr_product = np.pi / 2
        self.play(
            get_arrow_update(),
            decimal_anim,
            run_time=0.5
        )
        self.wait()


class TableOfContents(Scene):
    def construct(self):
        topics = VGroup(
            TextMobject("The setup"),
            TextMobject("Circle geometry with complex polynomials"),
            TextMobject("Proof of the Wallis product"),
            TextMobject("Formalities not discussed"),
            TextMobject(
                "Generalizing this proof to get \\\\ the product formula for sine"),
        )
        for topic in topics:
            dot = Dot(color=BLUE)
            dot.next_to(topic, LEFT)
            topic.add(dot)
        topics.arrange(
            DOWN, aligned_edge=LEFT, buff=LARGE_BUFF
        )
        self.add(topics)
        self.wait()
        for i in range(len(topics)):
            self.play(
                topics[i + 1:].set_fill, {"opacity": 0.25},
                topics[:i].set_fill, {"opacity": 0.25},
                topics[i].set_fill, {"opacity": 1},
            )
            self.wait(2)


class SourcesOfOriginality(TeacherStudentsScene):
    def construct(self):
        self.mention_excitement()
        self.break_down_value_of_math_presentations()
        self.where_we_fit_in()

    def mention_excitement(self):
        self.teacher_says(
            "This one came about \\\\ a bit differently...",
            target_mode="speaking",
            run_time=1
        )
        self.change_student_modes("happy", "confused", "erm")
        self.wait(2)

    def break_down_value_of_math_presentations(self):
        title = TextMobject("The value of a", "math", "presentation")
        title.to_edge(UP, buff=MED_SMALL_BUFF)
        value_of, math, presentation = title

        MATH_COLOR = YELLOW
        COMMUNICATION_COLOR = BLUE

        big_rect = self.big_rect = Rectangle(
            width=title.get_width() + 2 * MED_LARGE_BUFF,
            height=3.5,
            color=WHITE
        )
        big_rect.next_to(title, DOWN)

        left_rect, right_rect = self.left_rect, self.right_rect = [
            Rectangle(
                height=big_rect.get_height() - 2 * SMALL_BUFF,
                width=0.5 * big_rect.get_width() - 2 * SMALL_BUFF,
                color=color
            )
            for color in (MATH_COLOR, COMMUNICATION_COLOR)
        ]
        right_rect.flip()
        left_rect.next_to(big_rect.get_left(), RIGHT, SMALL_BUFF)
        right_rect.next_to(big_rect.get_right(), LEFT, SMALL_BUFF)

        underlying_math = TextMobject("Underlying", "math")
        underlying_math.set_color(MATH_COLOR)
        communication = TextMobject("Communication")
        communication.set_color(COMMUNICATION_COLOR)
        VGroup(underlying_math, communication).scale(0.75)
        underlying_math.next_to(left_rect.get_top(), DOWN, SMALL_BUFF)
        communication.next_to(right_rect.get_top(), DOWN, SMALL_BUFF)

        formula = TexMobject(
            "\\sum_{n = 1}^\\infty \\frac{1}{n^2} = \\frac{\\pi^2}{2}",
        )
        formula.scale(0.75)
        formula.next_to(underlying_math, DOWN)

        based_on_wastlund = TextMobject(
            "Previous video based on\\\\",
            "a paper by Johan W\\\"{a}stlund"
        )
        based_on_wastlund.set_width(
            left_rect.get_width() - MED_SMALL_BUFF)
        based_on_wastlund.next_to(formula, DOWN, MED_LARGE_BUFF)

        communication_parts = TextMobject("Visuals, narrative, etc.")
        communication_parts.scale(0.75)
        communication_parts.next_to(communication, DOWN, MED_LARGE_BUFF)
        lighthouse = Lighthouse(height=0.5)
        lighthouse.next_to(communication_parts, DOWN, LARGE_BUFF)
        ambient_light = AmbientLight(
            num_levels=200,
            radius=5,
            opacity_function=DEFAULT_OPACITY_FUNCTION,
        )
        ambient_light.move_source_to(lighthouse.get_top())

        big_rect.save_state()
        big_rect.stretch(0, 1)
        big_rect.stretch(0.5, 0)
        big_rect.move_to(title)

        self.play(
            FadeInFromDown(title),
            RemovePiCreatureBubble(
                self.teacher,
                target_mode="raise_right_hand",
                look_at_arg=title,
            ),
            self.get_student_changes(
                *["pondering"] * 3,
                look_at_arg=title
            )
        )
        self.play(big_rect.restore)
        self.play(*list(map(ShowCreation, [left_rect, right_rect])))
        self.wait()
        self.play(
            math.match_color, left_rect,
            ReplacementTransform(VGroup(math.copy()), underlying_math)
        )
        self.play(FadeIn(formula))
        self.play(
            presentation.match_color, right_rect,
            ReplacementTransform(presentation.copy(), communication)
        )
        self.play(
            FadeIn(communication_parts),
            FadeIn(lighthouse),
            SwitchOn(ambient_light)
        )
        self.play(self.teacher.change, "tease")
        self.wait()

        self.play(
            FadeIn(based_on_wastlund),
            self.get_student_changes(
                "sassy", "erm", "plain",
                look_at_arg=based_on_wastlund
            ),
        )
        self.wait()

        self.math_content = VGroup(formula, based_on_wastlund)

    def where_we_fit_in(self):
        right_rect = self.right_rect
        left_rect = self.left_rect

        points = [
            right_rect.get_left() + SMALL_BUFF * RIGHT,
            right_rect.get_corner(UL),
            right_rect.get_corner(UR),
            right_rect.get_right() + SMALL_BUFF * LEFT,
            right_rect.get_corner(DR),
            right_rect.get_bottom() + SMALL_BUFF * UP,
            right_rect.get_corner(DL),
        ]
        added_points = [
            left_rect.get_bottom(),
            left_rect.get_corner(DL),
            left_rect.get_corner(DL) + 1.25 * UP,
            left_rect.get_bottom() + 1.25 * UP,
        ]

        blob1, blob2 = VMobject(), VMobject()
        blob1.set_points_smoothly(points + [points[0]])
        blob1.append_points(3 * len(added_points) * [points[0]])
        blob2.set_points_smoothly(points + added_points + [points[0]])
        for blob in blob1, blob2:
            blob.set_stroke(width=0)
            blob.set_fill(BLUE, opacity=0.5)

        our_contribution = TextMobject("Our target \\\\ contribution")
        our_contribution.scale(0.75)
        our_contribution.to_corner(UR)
        arrow = Arrow(
            our_contribution.get_bottom(),
            right_rect.get_right() + MED_LARGE_BUFF * LEFT,
            color=BLUE
        )

        wallis_product = get_wallis_product(n_terms=4)
        wallis_product.set_width(
            left_rect.get_width() - 2 * MED_LARGE_BUFF)
        wallis_product.move_to(self.math_content, UP)
        wallis_product_name = TextMobject("``Wallis product''")
        wallis_product_name.scale(0.75)
        wallis_product_name.next_to(wallis_product, DOWN, MED_SMALL_BUFF)

        new_proof = TextMobject("New proof")
        new_proof.next_to(wallis_product_name, DOWN, MED_LARGE_BUFF)

        self.play(
            DrawBorderThenFill(blob1),
            Write(our_contribution),
            GrowArrow(arrow),
        )
        self.wait(2)
        self.play(FadeOut(self.math_content))
        self.play(
            FadeIn(wallis_product),
            Write(wallis_product_name, run_time=1)
        )
        self.wait(2)
        self.play(
            Transform(blob1, blob2, path_arc=-90 * DEGREES),
            FadeIn(new_proof),
            self.teacher.change, "hooray",
        )
        self.change_all_student_modes("hooray", look_at_arg=new_proof)
        self.wait(5)


class Six(Scene):
    def construct(self):
        six = TexMobject("6")
        six.add_background_rectangle(opacity = 1)
        six.background_rectangle.stretch(1.5, 0)
        six.set_height(7)
        self.add(six)



class SridharWatchingScene(PiCreatureScene):
    CONFIG = {
        "default_pi_creature_kwargs": {
            "color": YELLOW_E,
            "flip_at_start": False,
        },
    }

    def construct(self):
        laptop = Laptop()
        laptop.scale(1.8)
        laptop.to_corner(DR)
        sridhar = self.pi_creature
        sridhar.next_to(laptop, LEFT, SMALL_BUFF, DOWN)
        bubble = ThoughtBubble()
        bubble.flip()
        bubble.pin_to(sridhar)

        basel = TexMobject(
            "{1", "\\over", "1^2}", "+"
            "{1", "\\over", "2^2}", "+"
            "{1", "\\over", "3^2}", "+", "\\cdots",
            "= \\frac{\\pi^2}{6}"
        )
        wallis = get_wallis_product(n_terms=4)
        VGroup(basel, wallis).scale(0.7)
        basel.move_to(bubble.get_bubble_center())
        basel.to_edge(UP, buff=MED_SMALL_BUFF)
        wallis.next_to(basel, DOWN, buff=0.75)
        arrow = TexMobject("\\updownarrow")
        arrow.move_to(VGroup(basel, wallis))
        basel.set_color(YELLOW)
        wallis.set_color(BLUE)

        self.play(LaggedStartMap(DrawBorderThenFill, laptop))
        self.play(sridhar.change, "pondering", laptop.screen)
        self.wait()
        self.play(ShowCreation(bubble))
        self.play(LaggedStartMap(FadeIn, basel))
        self.play(
            ReplacementTransform(basel.copy(), wallis),
            GrowFromPoint(arrow, arrow.get_top())
        )
        self.wait(4)
        self.play(sridhar.change, "thinking", wallis)
        self.wait(4)
        self.play(LaggedStartMap(
            ApplyFunction,
            VGroup(*list(laptop) + [bubble, basel, arrow, wallis, sridhar]),
            lambda mob: (lambda m: m.set_color(BLACK).fade(1).scale(0.8), mob),
            run_time=3,
        ))


class ShowProduct(Scene):
    def construct(self):
        self.setup_axes()
        self.setup_wallis_product()
        self.show_larger_terms()
        self.show_smaller_terms()
        self.interleave_terms()
        self.show_answer()

    def setup_axes(self):
        axes = self.axes = self.get_axes(unit_size=0.75)
        self.add(axes)

    def setup_wallis_product(self):
        full_wallis_product = get_wallis_product(n_terms=16, show_result=False)
        wallis_product_parts = VGroup(*[
            full_wallis_product[i:i + 4]
            for i in range(0, len(full_wallis_product), 4)
        ])

        larger_parts = self.larger_parts = wallis_product_parts[::2]
        larger_parts.set_color(YELLOW)
        dots = TexMobject("\\cdots")
        dots.move_to(larger_parts[-1][-1], LEFT)
        larger_parts[-1][-1].submobjects = dots.submobjects

        smaller_parts = self.smaller_parts = wallis_product_parts[1::2]
        smaller_parts.set_color(BLUE)

        for parts in larger_parts, smaller_parts:
            parts.arrange(RIGHT, buff=2 * SMALL_BUFF)
            # Move around the dots
            for part1, part2 in zip(parts, parts[1:]):
                dot = part1.submobjects.pop(-1)
                part2.add_to_back(dot)

        larger_parts.to_edge(UP)
        smaller_parts.next_to(larger_parts, DOWN, LARGE_BUFF)

        self.wallis_product_terms = get_wallis_product_numerical_terms(40)

    def show_larger_terms(self):
        axes = self.axes
        parts = self.larger_parts
        terms = self.wallis_product_terms[::2]
        partial_products = np.cumprod(terms)

        dots = VGroup(*[
            Dot(axes.coords_to_point(n + 1, prod))
            for n, prod in enumerate(partial_products)
        ])
        dots.match_color(parts)
        lines = VGroup(*[
            Line(d1.get_center(), d2.get_center())
            for d1, d2 in zip(dots, dots[1:])
        ])

        braces = VGroup(*[
            Brace(parts[:n + 1], DOWN)
            for n in range(len(parts))
        ])

        brace = braces[0].copy()
        decimal = DecimalNumber(partial_products[0], num_decimal_places=4)
        decimal.next_to(brace, DOWN)

        self.add(brace, decimal, dots[0], parts[0])
        tuples = list(zip(parts[1:], lines, dots[1:], partial_products[1:], braces[1:]))
        for part, line, dot, prod, new_brace in tuples:
            self.play(
                FadeIn(part),
                Transform(brace, new_brace),
                ChangeDecimalToValue(
                    decimal, prod,
                    position_update_func=lambda m: m.next_to(brace, DOWN)
                ),
                ShowCreation(line),
                GrowFromCenter(dot, rate_func=squish_rate_func(smooth, 0.5, 1)),
                run_time=0.5,
            )
            self.wait(0.5)
        N = len(parts)
        self.play(
            LaggedStartMap(ShowCreation, lines[N - 1:], lag_ratio=0.2),
            LaggedStartMap(FadeIn, dots[N:], lag_ratio=0.2),
            brace.stretch, 1.2, 0, {"about_edge": LEFT},
            ChangeDecimalToValue(
                decimal, partial_products[-1],
                position_update_func=lambda m: m.next_to(brace, DOWN)
            ),
            run_time=4,
            rate_func=linear,
        )
        self.play(
            FadeOut(brace),
            ChangeDecimalToValue(
                decimal, partial_products[-1] + 2,
                position_update_func=lambda m: m.next_to(brace, DOWN)
            ),
            UpdateFromFunc(
                decimal, lambda d: d.shift(self.frame_duration * RIGHT)
            ),
            UpdateFromAlphaFunc(
                decimal, lambda d, a: d.set_fill(opacity=1 - a)
            ),
        )
        self.remove(decimal)

        self.graph_to_remove = VGroup(dots, lines)

    def show_smaller_terms(self):
        larger_parts = self.larger_parts
        larger_parts.save_state()
        larger_parts_mover = larger_parts.copy()
        larger_parts.fade(0.5)

        smaller_parts = self.smaller_parts
        for parts in larger_parts_mover, smaller_parts:
            parts.denominators = VGroup(
                parts[0][2],
                *[part[3] for part in parts[1:]]
            )
        vect = op.sub(
            smaller_parts.denominators[1].get_left(),
            smaller_parts.denominators[0].get_left(),
        )
        smaller_parts.denominators.shift(vect)

        self.play(
            larger_parts_mover.move_to, smaller_parts, LEFT,
            FadeOut(self.graph_to_remove)
        )
        self.play(
            larger_parts_mover.denominators.shift, -vect,
            smaller_parts.denominators.shift, -vect,
            UpdateFromAlphaFunc(
                larger_parts_mover,
                lambda m, a: m.set_fill(opacity=1 - a),
                remover=True
            ),
            UpdateFromAlphaFunc(
                smaller_parts,
                lambda m, a: m.set_fill(opacity=a)
            ),
        )

        # Rescale axes
        new_axes = self.get_axes(unit_size=1.5)
        self.play(ReplacementTransform(self.axes, new_axes))
        axes = self.axes = new_axes

        # Show graph
        terms = self.wallis_product_terms[1::2]
        partial_products = np.cumprod(terms)[:15]

        dots = VGroup(*[
            Dot(axes.coords_to_point(n + 1, prod))
            for n, prod in enumerate(partial_products)
        ])
        dots.match_color(smaller_parts)
        lines = VGroup(*[
            Line(d1.get_center(), d2.get_center())
            for d1, d2 in zip(dots, dots[1:])
        ])

        self.play(
            ShowCreation(lines),
            LaggedStartMap(FadeIn, dots, lag_ratio=0.1),
            run_time=3,
            rate_func=linear,
        )
        self.wait(2)
        self.play(FadeOut(VGroup(dots, lines)))

    def interleave_terms(self):
        larger_parts = self.larger_parts
        smaller_parts = self.smaller_parts
        index = 6
        larger_parts.restore()
        for parts in larger_parts, smaller_parts:
            parts.prefix = parts[:index]
            parts.suffix = parts[index:]
            parts.prefix.generate_target()
        larger_parts.fade(0.5)
        full_product = VGroup(*it.chain(
            *list(zip(larger_parts.prefix.target, smaller_parts.prefix.target))
        ))
        for i, tex, vect in (0, "\\cdot", LEFT), (-1, "\\cdots", RIGHT):
            part = smaller_parts.prefix.target[i]
            dot = TexMobject(tex)
            dot.match_color(part)
            dot.next_to(part, vect, buff=2 * SMALL_BUFF)
            part.add(dot)
        full_product.arrange(RIGHT, buff=2 * SMALL_BUFF)
        full_product.to_edge(UP)

        for parts in larger_parts, smaller_parts:
            self.play(
                MoveToTarget(parts.prefix),
                FadeOut(parts.suffix)
            )
        self.wait()

        # Dots and lines
        # In poor form, this is modified copy-pasted from show_larger_terms
        axes = self.axes
        parts = full_product
        terms = self.wallis_product_terms
        partial_products = np.cumprod(terms)
        partial_products_iter = iter(partial_products)
        print(partial_products)

        dots = VGroup(*[
            Dot(axes.coords_to_point(n + 1, prod))
            for n, prod in enumerate(partial_products)
        ])
        dots.set_color(GREEN)
        lines = VGroup(*[
            Line(d1.get_center(), d2.get_center())
            for d1, d2 in zip(dots, dots[1:])
        ])

        braces = VGroup(*[
            Brace(parts[:n + 1], DOWN)
            for n in range(len(parts))
        ])

        brace = braces[0].copy()
        decimal = DecimalNumber(next(partial_products_iter), num_decimal_places=4)
        decimal.next_to(brace, DOWN)

        self.play(*list(map(FadeIn, [brace, decimal, dots[0]])))
        tuples = list(zip(lines, dots[1:], braces[1:]))
        for line, dot, new_brace in tuples:
            self.play(
                Transform(brace, new_brace),
                ChangeDecimalToValue(
                    decimal, next(partial_products_iter),
                    position_update_func=lambda m: m.next_to(brace, DOWN)
                ),
                ShowCreation(line),
                GrowFromCenter(dot, rate_func=squish_rate_func(smooth, 0.5, 1)),
                run_time=0.5,
            )
            self.wait(0.5)

        def get_decimal_anim():
            return ChangeDecimalToValue(
                decimal, next(partial_products_iter),
                run_time=1,
                rate_func=squish_rate_func(smooth, 0, 0.5),
            )

        self.play(
            FadeIn(lines[len(parts) - 1:]),
            FadeIn(dots[len(parts):]),
            get_decimal_anim()
        )
        for x in range(3):
            self.play(get_decimal_anim())

        self.partial_product_decimal = decimal
        self.get_decimal_anim = get_decimal_anim

    def show_answer(self):
        decimal = self.partial_product_decimal
        axes = self.axes

        pi_halves = TexMobject("{\\pi", "\\over", "2}")
        pi_halves.scale(1.5)
        pi_halves.move_to(decimal, UP)

        randy = Randolph(height=1.7)
        randy.next_to(decimal, DL)
        randy.change("confused")
        randy.save_state()
        randy.change("plain")
        randy.fade(1)

        h_line = DashedLine(
            axes.coords_to_point(0, np.pi / 2),
            axes.coords_to_point(20, np.pi / 2),
            color=RED
        )

        self.play(
            ShowCreation(h_line),
            randy.restore,
            self.get_decimal_anim()
        )
        self.play(Blink(randy), self.get_decimal_anim())
        self.play(self.get_decimal_anim())
        self.play(
            self.get_decimal_anim(),
            UpdateFromAlphaFunc(
                decimal,
                lambda m, a: m.set_fill(opacity=1 - a)
            ),
            ReplacementTransform(randy, pi_halves[0]),
            Write(pi_halves[1:]),
        )
        self.remove(decimal)
        self.wait()

    # Helpers

    def get_axes(self, unit_size):
        y_max = 7
        axes = Axes(
            x_min=-1,
            x_max=12.5,
            y_min=-0.5,
            y_max=y_max + 0.25,
            y_axis_config={
                "unit_size": unit_size,
                "numbers_with_elongated_ticks": list(range(1, y_max + 1)),
                "tick_size": 0.05,
            },
        )
        axes.shift(6 * LEFT + 3 * DOWN - axes.coords_to_point(0, 0))

        axes.y_axis.label_direction = LEFT
        axes.y_axis.add_numbers(*list(range(1, y_max + 1)))
        return axes


class TeacherShowing(TeacherStudentsScene):
    def construct(self):
        screen = self.screen
        screen.set_height(4)
        screen.next_to(self.students, UP, MED_LARGE_BUFF, RIGHT)
        self.play(
            ShowCreation(screen),
            self.teacher.change, "raise_right_hand", screen,
            self.get_student_changes(
                *["pondering"] * 3,
                look_at_arg=screen
            )
        )
        self.wait(5)


class DistanceProductScene(MovingCameraScene):
    CONFIG = {
        "ambient_light_config": HIGHT_QUALITY_AMBIENT_LIGHT_CONFIG,
        "circle_color": BLUE,
        "circle_radius": 3,
        "num_lighthouses": 6,
        "lighthouse_height": 0.5,
        "ignored_lighthouse_indices": [],
        "observer_config": {
            "color": MAROON_B,
            "mode": "pondering",
            "height": 0.25,
            "flip_at_start": True,
        },
        "observer_fraction": 1.0 / 3,
        "d_label_height": 0.35,
        "numeric_distance_label_height": 0.25,
        "default_product_column_top": FRAME_WIDTH * RIGHT / 4 + 1.5 * UP,
        "include_lighthouses": True,
        "include_distance_labels_background_rectangle": True,
    }

    def setup(self):
        super(DistanceProductScene, self).setup()
        self.circle = Circle(
            color=self.circle_color,
            radius=self.circle_radius,
        )

    def get_circle_point_at_proportion(self, alpha):
        radius = self.get_radius()
        center = self.circle.get_center()
        angle = alpha * TAU
        unit_circle_point = np.cos(angle) * RIGHT + np.sin(angle) * UP
        return radius * unit_circle_point + center

    def get_lh_points(self):
        return np.array([
            self.get_circle_point_at_proportion(fdiv(i, self.num_lighthouses))
            for i in range(self.num_lighthouses)
            if i not in self.ignored_lighthouse_indices
        ])

    def get_observer_point(self, fraction=None):
        if fraction is None:
            fraction = self.observer_fraction
        return self.get_circle_point_at_proportion(fraction / self.num_lighthouses)

    def get_observer(self):
        observer = self.observer = PiCreature(**self.observer_config)
        observer.next_to(self.get_observer_point(), RIGHT, buff=SMALL_BUFF)
        return observer

    def get_observer_dot(self):
        self.observer_dot = Dot(
            self.get_observer_point(),
            color=self.observer_config["color"]
        )
        return self.observer_dot

    def get_lighthouses(self):
        self.lighthouses = VGroup()
        for point in self.get_lh_points():
            lighthouse = Lighthouse()
            lighthouse.set_height(self.lighthouse_height)
            lighthouse.move_to(point)
            self.lighthouses.add(lighthouse)
        return self.lighthouses

    def get_lights(self):
        self.lights = VGroup()
        for point in self.get_lh_points():
            light = AmbientLight(
                source_point=VectorizedPoint(point),
                **self.ambient_light_config
            )
            self.lights.add(light)
        return self.lights

    def get_distance_lines(self, start_point=None, line_class=Line):
        if start_point is None:
            start_point = self.get_observer_point()
        lines = VGroup(*[
            line_class(start_point, point)
            for point in self.get_lh_points()
        ])
        lines.set_stroke(width=2)
        self.distance_lines = lines
        return self.distance_lines

    def get_symbolic_distance_labels(self):
        if not hasattr(self, "distance_lines"):
            self.get_distance_lines()
        self.d_labels = VGroup()
        for i, line in enumerate(self.distance_lines):
            d_label = TexMobject("d_%d" % i)
            d_label.set_height(self.d_label_height)
            vect = rotate_vector(line.get_vector(), 90 * DEGREES)
            vect *= 2.5 * SMALL_BUFF / get_norm(vect)
            d_label.move_to(line.get_center() + vect)
            self.d_labels.add(d_label)
        return self.d_labels

    def get_numeric_distance_labels(self, lines=None, num_decimal_places=3, show_ellipsis=True):
        radius = self.circle.get_width() / 2
        if lines is None:
            if not hasattr(self, "distance_lines"):
                self.get_distance_lines()
            lines = self.distance_lines
        labels = self.numeric_distance_labels = VGroup()
        for line in lines:
            label = DecimalNumber(
                line.get_length() / radius,
                num_decimal_places=num_decimal_places,
                show_ellipsis=show_ellipsis,
                include_background_rectangle=self.include_distance_labels_background_rectangle,
            )
            label.set_height(self.numeric_distance_label_height)
            max_width = 0.5 * max(line.get_length(), 0.1)
            if label.get_width() > max_width:
                label.set_width(max_width)
            angle = (line.get_angle() % TAU) - TAU / 2
            if np.abs(angle) > TAU / 4:
                angle += np.sign(angle) * np.pi
            label.angle = angle
            label.next_to(line.get_center(), UP, SMALL_BUFF)
            label.rotate(angle, about_point=line.get_center())
            labels.add(label)
        return labels

    def get_distance_product_column(self, column_top=None, labels=None, fraction=None):
        if column_top is None:
            column_top = self.default_product_column_top
        if labels is None:
            if not hasattr(self, "numeric_distance_labels"):
                self.get_numeric_distance_labels()
            labels = self.numeric_distance_labels
        stacked_labels = labels.copy()
        for label in stacked_labels:
            label.rotate(-label.angle)
            label.set_height(self.numeric_distance_label_height)
        stacked_labels.arrange(DOWN)
        stacked_labels.move_to(column_top, UP)

        h_line = Line(LEFT, RIGHT)
        h_line.set_width(1.5 * stacked_labels.get_width())
        h_line.next_to(stacked_labels, DOWN, aligned_edge=RIGHT)
        times = TexMobject("\\times")
        times.next_to(h_line, UP, SMALL_BUFF, aligned_edge=LEFT)

        product_decimal = DecimalNumber(
            self.get_distance_product(fraction),
            num_decimal_places=3,
            show_ellipsis=True,
            include_background_rectangle=self.include_distance_labels_background_rectangle,
        )
        product_decimal.set_height(self.numeric_distance_label_height)
        product_decimal.next_to(h_line, DOWN)
        product_decimal.align_to(stacked_labels, RIGHT)
        product_decimal[1].set_color(BLUE)
        self.distance_product_column = VGroup(
            stacked_labels, h_line, times, product_decimal
        )
        return self.distance_product_column

    def get_fractional_arc(self, fraction, start_fraction=0):
        arc = Arc(
            angle=fraction * TAU,
            start_angle=start_fraction * TAU,
            radius=self.get_radius(),
        )
        arc.shift(self.circle.get_center())
        return arc

    def get_halfway_indication_arcs(self):
        fraction = 0.5 / self.num_lighthouses
        arcs = VGroup(
            self.get_fractional_arc(fraction),
            self.get_fractional_arc(-fraction, start_fraction=2 * fraction),
        )
        arcs.set_stroke(YELLOW, 4)
        return arcs

    def get_circle_group(self):
        group = VGroup(self.circle)
        if not hasattr(self, "observer_dot"):
            self.get_observer_dot()
        if not hasattr(self, "observer"):
            self.get_observer()
        if not hasattr(self, "lighthouses"):
            self.get_lighthouses()
        if not hasattr(self, "lights"):
            self.get_lights()
        group.add(self.observer_dot, self.observer)
        if self.include_lighthouses:
            group.add(self.lighthouses)
        group.add(self.lights)
        return group

    def setup_lighthouses_and_observer(self):
        self.add(*self.get_circle_group())

    # Numerical results

    def get_radius(self):
        return self.circle.get_width() / 2.0

    def get_distance_product(self, fraction=None):
        radius = self.get_radius()
        observer_point = self.get_observer_point(fraction)
        distances = [
            get_norm(point - observer_point) / radius
            for point in self.get_lh_points()
        ]
        return reduce(op.mul, distances, 1.0)

    # Animating methods

    def add_numeric_distance_labels(self, show_line_creation=True):
        anims = []
        if not hasattr(self, "distance_lines"):
            self.get_distance_lines()
        if not hasattr(self, "numeric_distance_labels"):
            self.get_numeric_distance_labels()
        if show_line_creation:
            anims.append(LaggedStartMap(ShowCreation, self.distance_lines))
        anims.append(LaggedStartMap(FadeIn, self.numeric_distance_labels))

        self.play(*anims)

    def show_distance_product_in_column(self, **kwargs):
        group = self.get_distance_product_column(**kwargs)
        stacked_labels, h_line, times, product_decimal = group
        labels = self.numeric_distance_labels

        self.play(ReplacementTransform(labels.copy(), stacked_labels))
        self.play(
            ShowCreation(h_line),
            Write(times)
        )
        self.play(
            ReplacementTransform(
                stacked_labels.copy(),
                VGroup(product_decimal)
            )
        )


class IntroduceDistanceProduct(DistanceProductScene):
    CONFIG = {
        "ambient_light_config": {"color": YELLOW},
    }

    def construct(self):
        self.draw_circle_with_points()
        self.turn_into_lighthouses_and_observer()
        self.show_sum_of_inverse_squares()
        self.transition_to_lemma_1()

    def draw_circle_with_points(self):
        circle = self.circle

        lh_dots = self.lh_dots = VGroup(*[
            Dot(point) for point in self.get_lh_points()
        ])
        lh_dot_arrows = VGroup(*[
            Arrow(*[
                interpolate(circle.get_center(), dot.get_center(), a)
                for a in (0.6, 0.9)
            ], buff=0)
            for dot in lh_dots
        ])
        evenly_space_dots_label = TextMobject("Evenly-spaced \\\\ dots")
        evenly_space_dots_label.set_width(0.5 * circle.get_width())
        evenly_space_dots_label.move_to(circle)

        special_dot = self.special_dot = self.get_observer_dot()
        special_dot_arrow = Vector(DL)
        special_dot_arrow.next_to(special_dot, UR, SMALL_BUFF)
        special_dot_arrow.match_color(special_dot)
        special_dot_label = TextMobject("Special dot")
        special_dot_label.next_to(
            special_dot_arrow.get_start(), UP, SMALL_BUFF)
        special_dot_label.match_color(special_dot)
        special_dot.save_state()
        special_dot.next_to(special_dot_arrow, UR)
        special_dot.set_fill(opacity=0)

        self.play(ShowCreation(circle))
        self.play(
            LaggedStartMap(ShowCreation, lh_dots),
            LaggedStartMap(GrowArrow, lh_dot_arrows),
            Write(evenly_space_dots_label)
        )
        self.wait()
        self.play(
            special_dot.restore,
            GrowArrow(special_dot_arrow),
            Write(special_dot_label, run_time=1),
            FadeOut(VGroup(lh_dot_arrows, evenly_space_dots_label))
        )
        self.wait()
        self.play(FadeOut(VGroup(special_dot_arrow, special_dot_label)))

    def turn_into_lighthouses_and_observer(self):
        lighthouses = self.get_lighthouses()
        lights = self.get_lights()

        observer = self.get_observer()
        observer.save_state()
        observer.set_height(2)
        observer.change_mode("happy")
        observer.to_edge(RIGHT)

        self.play(
            LaggedStartMap(FadeOut, self.lh_dots),
            LaggedStartMap(FadeIn, lighthouses),
            LaggedStartMap(SwitchOn, lights),
        )
        self.wait()
        self.play(FadeIn(observer))
        self.play(observer.restore)
        self.wait()

    def show_sum_of_inverse_squares(self):
        lines = self.get_distance_lines()
        labels = self.get_symbolic_distance_labels()

        sum_of_inverse_squares = TexMobject(*it.chain(*[
            ["{1", "\\over", "(", "d_%d" % i, ")", "^2}", "+"]
            for i in range(len(lines))
        ]))
        sum_of_inverse_squares.submobjects.pop(-1)
        sum_of_inverse_squares.to_edge(UP)
        d_terms = sum_of_inverse_squares.get_parts_by_tex("d_")
        d_terms.set_color(YELLOW)
        plusses = sum_of_inverse_squares.get_parts_by_tex("+")
        last_term = sum_of_inverse_squares[-6:]
        non_d_terms = VGroup(*[m for m in sum_of_inverse_squares if m not in d_terms and m not in last_term])

        brace = Brace(sum_of_inverse_squares, DOWN)
        brace_text = brace.get_text("Total intensity of light")

        arrow = Vector(DOWN, color=WHITE).next_to(brace, DOWN)
        basel_sum = TexMobject(
            "{1 \\over 1^2} + ",
            "{1 \\over 2^2} + ",
            "{1 \\over 3^2} + ",
            "{1 \\over 4^2} + ",
            "\\cdots",
        )
        basel_sum.next_to(arrow, DOWN)
        basel_cross = Cross(basel_sum)
        useful_for = TextMobject("Useful for")
        useful_for.next_to(arrow, RIGHT)

        wallis_product = TexMobject(
            "{2 \\over 1} \\cdot", "{2 \\over 3} \\cdot",
            "{4 \\over 3} \\cdot", "{4 \\over 5} \\cdot",
            "{6 \\over 5} \\cdot", "{6 \\over 7} \\cdot",
            "\\cdots"
        )
        wallis_product.move_to(basel_sum)

        light_rings = VGroup(*it.chain(*self.lights))

        self.play(
            LaggedStartMap(ShowCreation, lines),
            LaggedStartMap(Write, labels),
        )
        circle_group = VGroup(*self.get_top_level_mobjects())
        self.wait()
        self.play(
            ReplacementTransform(labels[-1].copy(), last_term[3]),
            Write(VGroup(*it.chain(last_term[:3], last_term[4:])))
        )
        self.remove(last_term)
        self.add(last_term)
        self.wait()
        self.play(
            Write(non_d_terms),
            ReplacementTransform(
                labels[:-1].copy(),
                d_terms[:-1],
            ),
            circle_group.scale, 0.8, {"about_point": FRAME_Y_RADIUS * DOWN}
        )
        self.wait()
        self.play(LaggedStartMap(
            ApplyMethod, light_rings,
            lambda m: (m.set_fill, {"opacity": 2 * m.get_fill_opacity()}),
            rate_func=there_and_back,
            run_time=3,
        ))
        self.wait()

        # Mention useful just to basel problem
        circle_group.save_state()
        v_point = VectorizedPoint(
            FRAME_X_RADIUS * LEFT + FRAME_Y_RADIUS * DOWN)
        self.play(
            circle_group.next_to, v_point, UR, {
                "submobject_to_align": self.circle},
            circle_group.scale, 0.5, {"about_point": v_point.get_center()},
        )
        self.play(
            GrowFromCenter(brace),
            Write(brace_text)
        )
        self.wait()
        self.play(
            FadeOut(brace_text),
            GrowArrow(arrow),
            FadeIn(useful_for),
            FadeIn(basel_sum),
        )
        self.wait()
        self.play(
            ShowCreation(basel_cross),
            FadeOut(VGroup(arrow, useful_for, brace))
        )
        basel_group = VGroup(basel_sum, basel_cross)
        self.play(
            basel_group.scale, 0.5,
            basel_group.to_corner, DR,
        )
        self.play(Write(wallis_product))
        self.wait()

        # Transition to distance product
        self.play(
            circle_group.restore,
            wallis_product.match_width, basel_sum,
            wallis_product.next_to, basel_sum, UP, {"aligned_edge": RIGHT},
        )
        self.play(
            d_terms.shift, 0.75 * d_terms.get_height() * UP,
            d_terms.set_color, PRODUCT_COLOR,
            light_rings.set_fill, PRODUCT_COLOR,
            *[
                FadeOut(mob)
                for mob in sum_of_inverse_squares
                if mob not in d_terms and mob not in plusses
            ]
        )
        self.wait()
        self.play(
            FadeOut(plusses),
            d_terms.arrange, RIGHT, 0.25 * SMALL_BUFF,
            d_terms.move_to, sum_of_inverse_squares, DOWN,
        )
        self.wait()

        # Label distance product
        brace = Brace(d_terms, UP, buff=SMALL_BUFF)
        distance_product_label = brace.get_text("``Distance product''")

        self.play(
            GrowFromCenter(brace),
            Write(distance_product_label)
        )
        line_copies = lines.copy().set_color(RED)
        self.play(LaggedStartMap(ShowCreationThenDestruction, line_copies))
        self.wait()
        self.play(LaggedStartMap(
            ApplyFunction, light_rings,
            lambda mob: (
                lambda m: m.shift(
                    MED_SMALL_BUFF * UP).set_fill(opacity=2 * m.get_fill_opacity()),
                mob
            ),
            rate_func=wiggle,
            run_time=6,
        ))
        self.wait()

    def transition_to_lemma_1(self):
        self.lighthouse_height = Lemma1.CONFIG["lighthouse_height"]
        self.circle_radius = Lemma1.CONFIG["circle_radius"]
        self.observer_fraction = Lemma1.CONFIG["observer_fraction"]

        self.ambient_light_config["color"] = BLUE

        circle = self.circle
        lighthouses = self.lighthouses
        lights = self.lights

        circle.generate_target()
        circle.target.set_width(2 * self.circle_radius)
        circle.target.to_corner(DL)
        self.circle = circle.target

        new_lighthouses = self.get_lighthouses()
        new_lights = self.get_lights()

        self.clear()
        self.play(
            MoveToTarget(circle),
            Transform(lighthouses, new_lighthouses),
            Transform(lights, new_lights),
            ApplyMethod(
                self.observer_dot.move_to,
                self.get_circle_point_at_proportion(
                    self.observer_fraction / self.num_lighthouses
                )
            ),
            MaintainPositionRelativeTo(self.observer, self.observer_dot),
        )


class Lemma1(DistanceProductScene):
    CONFIG = {
        "circle_radius": 2.5,
        "observer_fraction": 0.5,
        "lighthouse_height": 0.25,
        "lemma_text": "distance product = 2",
        "include_distance_labels_background_rectangle": False,
        # "ambient_light_config": CHEAP_AMBIENT_LIGHT_CONFIG,
    }

    def construct(self):
        self.add_title()
        self.add_circle_group()
        self.state_lemma_premise()
        self.show_product()
        self.wiggle_observer()

    def add_title(self):
        title = self.title = TextMobject("Two lemmas:")
        title.set_color(YELLOW)
        title.to_edge(UP, buff=MED_SMALL_BUFF)
        self.add(title)

    def add_circle_group(self):
        self.circle.to_corner(DL)
        circle_group = self.get_circle_group()
        self.play(LaggedStartMap(FadeIn, VGroup(
            *circle_group.family_members_with_points())))

    def state_lemma_premise(self):
        premise = TextMobject(
            "Lemma 1: If observer is halfway between lighthouses,")
        self.premise = premise
        premise.next_to(self.title, DOWN)

        frac = 1.0 / self.num_lighthouses
        arc1, arc2 = arcs = VGroup(VMobject(), VMobject())
        arc1.pointwise_become_partial(self.circle, 0, frac / 2)
        arc2.pointwise_become_partial(self.circle, frac / 2, frac)
        arc1.reverse_points()
        arcs.set_stroke(YELLOW, 5)
        show_arcs = ShowCreationThenDestruction(
            arcs,
            lag_ratio=0,
            run_time=2,
        )

        self.play(Write(premise), show_arcs, run_time=2)
        self.wait()
        self.play(show_arcs)
        self.wait()

    def show_product(self):
        lemma = TextMobject(self.lemma_text)
        lemma.set_color(BLUE)
        lemma.next_to(self.premise, DOWN)
        self.add_numeric_distance_labels()
        self.play(Write(lemma, run_time=1))
        self.show_distance_product_in_column()
        self.wait()

    def wiggle_observer(self):
        # Overwriting existing method
        self.get_observer_point = lambda dummy=None: self.observer_dot.get_center()

        center = self.circle.get_center()
        observer_angle = angle_of_vector(self.get_observer_point() - center)
        observer_angle_tracker = ValueTracker(observer_angle)

        def update_dot(dot):
            dot.move_to(self.get_circle_point_at_proportion(
                observer_angle_tracker.get_value() / TAU
            ))

        def update_distance_lines(lines):
            new_lines = self.get_distance_lines(start_point=self.get_observer_point())
            lines.submobjects = new_lines.submobjects

        def update_numeric_distance_labels(labels):
            new_labels = self.get_numeric_distance_labels(self.distance_lines)
            labels.submobjects = new_labels.submobjects

        def update_distance_product_column(column):
            new_column = self.get_distance_product_column()
            column.submobjects = new_column.submobjects

        self.remove(*VGroup(
            self.observer, self.observer_dot,
            self.distance_lines,
            self.numeric_distance_labels,
            self.distance_product_column,
        ).get_family())
        self.play(
            ApplyMethod(
                observer_angle_tracker.set_value, observer_angle + 0.05 * TAU,
                rate_func=wiggle
            ),
            UpdateFromFunc(self.observer_dot, update_dot),
            MaintainPositionRelativeTo(self.observer, self.observer_dot),
            UpdateFromFunc(self.distance_lines, update_distance_lines),
            UpdateFromFunc(self.numeric_distance_labels, update_numeric_distance_labels),
            UpdateFromFunc(self.distance_product_column, update_distance_product_column),
            run_time=5
        )
        self.distance_product_column[-1].set_color(BLUE).scale_in_place(1.05)
        self.wait()


class Lemma1With7Lighthouses(Lemma1):
    CONFIG = {
        "num_lighthouses": 7,
    }


class Lemma1With8Lighthouses(Lemma1):
    CONFIG = {
        "num_lighthouses": 8,
    }


class Lemma1With9Lighthouses(Lemma1):
    CONFIG = {
        "num_lighthouses": 9,
    }


class Lemma2(Lemma1):
    CONFIG = {
        # "ambient_light_config": CHEAP_AMBIENT_LIGHT_CONFIG,
        "lemma_text": "distance product = \\# Initial lighthouses"
    }

    def construct(self):
        self.add_title()
        self.add_circle_group()
        self.state_lemma_premise()
        self.replace_first_lighthouse()
        self.show_product()
        self.wiggle_observer()

    def state_lemma_premise(self):
        premise = self.premise = TextMobject(
            "Lemma 2: If the observer replaces a lighthouse,"
        )
        premise.next_to(self.title, DOWN)

        self.play(Write(premise, run_time=1))

    def replace_first_lighthouse(self):
        dot = self.observer_dot
        observer_anim = MaintainPositionRelativeTo(self.observer, dot)
        lighthouse_group = VGroup(self.lighthouses[0], self.lights[0])
        point = self.get_lh_points()[0]

        self.play(
            lighthouse_group.shift, 5 * RIGHT,
            lighthouse_group.fade, 1,
            run_time=1.5,
            rate_func=running_start,
            remover=True,
        )
        self.play(
            dot.move_to, point,
            observer_anim,
            path_arc=(-120 * DEGREES),
        )
        self.wait()

        self.ignored_lighthouse_indices = [0]
        self.observer_fraction = 0
        for group in self.lighthouses, self.lights:
            self.lighthouses.submobjects.pop(0)


class Lemma2With7Lighthouses(Lemma2):
    CONFIG = {
        "num_lighthouses": 7,
    }


class Lemma2With8Lighthouses(Lemma2):
    CONFIG = {
        "num_lighthouses": 8,
    }


class Lemma2With9Lighthouses(Lemma2):
    CONFIG = {
        "num_lighthouses": 9,
    }


class ConfusedPiCreature(Scene):
    def construct(self):
        randy = Randolph(color=GREY_BROWN)
        self.add(randy)
        self.play(Blink(randy))
        self.play(randy.change, "confused")
        self.play(Blink(randy))
        self.wait()


class FromGeometryToAlgebra(DistanceProductScene):
    CONFIG = {
        "num_lighthouses": 7,
        # "ambient_light_config": CHEAP_AMBIENT_LIGHT_CONFIG,
    }

    def construct(self):
        self.setup_lights()
        self.point_out_evenly_spaced()
        self.transition_to_complex_plane()
        self.discuss_powers()
        self.raise_everything_to_the_nth()

    def setup_lights(self):
        circle = self.circle
        circle.set_height(5, about_edge=DOWN)
        lights = self.get_lights()
        dots = VGroup(*[Dot(point) for point in self.get_lh_points()])
        for dot, light in zip(dots, lights):
            light.add_to_back(dot)

        self.add(circle, lights)

    def point_out_evenly_spaced(self):
        circle = self.circle
        step = 1.0 / self.num_lighthouses / 2
        alpha_range = np.arange(0, 1 + step, step)
        arcs = VGroup(*[
            VMobject().pointwise_become_partial(circle, a1, a2)
            for a1, a2 in zip(alpha_range, alpha_range[1:])
        ])
        arcs.set_stroke(YELLOW, 5)

        for arc in arcs[::2]:
            arc.reverse_points()

        arcs_anim = ShowCreationThenDestruction(
            arcs, lag_ratio=0, run_time=2
        )

        spacing_words = self.spacing_words = TextMobject("Evenly-spaced")
        spacing_words.set_width(self.get_radius())
        spacing_words.move_to(circle)

        arrows = self.get_arrows()

        geometric_words = self.geometric_words = TextMobject(
            "Geometric property")
        geometric_words.to_edge(UP)
        geometric_words.add_background_rectangle()

        self.add(geometric_words)
        self.play(
            FadeIn(spacing_words),
            arcs_anim,
            *list(map(GrowArrow, arrows))
        )
        self.play(FadeOut(arrows), arcs_anim)
        self.wait()

    def transition_to_complex_plane(self):
        plane = self.complex_plane = ComplexPlane(
            unit_size=2, y_radius=6, x_radius=9,
        )
        plane.shift(1.5 * RIGHT)
        plane.add_coordinates()
        origin = plane.number_to_point(0)
        h_line = Line(plane.number_to_point(-1), plane.number_to_point(1))

        circle = self.circle
        circle_group = VGroup(circle, self.lights)
        circle_group.generate_target()
        circle_group.target.scale(h_line.get_width() / circle.get_width())
        circle_group.target.shift(
            origin - circle_group.target[0].get_center()
        )
        circle_group.target[0].set_stroke(RED)

        geometric_words = self.geometric_words
        geometric_words.generate_target()
        arrow = TexMobject("\\rightarrow")
        arrow.add_background_rectangle()
        algebraic_words = TextMobject("Algebraic property")
        algebraic_words.add_background_rectangle()
        word_group = VGroup(geometric_words.target, arrow, algebraic_words)
        word_group.arrange(RIGHT)
        word_group.move_to(origin)
        word_group.to_edge(UP)

        unit_circle_words = TextMobject("Unit circle", "")
        unit_circle_words.match_color(circle_group.target[0])
        for part in unit_circle_words:
            part.add_background_rectangle()
        unit_circle_words.next_to(origin, UP)

        complex_plane_words = TextMobject("Complex Plane")
        self.complex_plane_words = complex_plane_words
        complex_plane_words.move_to(word_group)
        complex_plane_words.add_background_rectangle()

        roots_of_unity_words = TextMobject("Roots of\\\\", "unity")
        roots_of_unity_words.move_to(origin)
        roots_of_unity_words.set_color(YELLOW)
        for part in roots_of_unity_words:
            part.add_background_rectangle()

        self.play(
            Write(plane),
            MoveToTarget(circle_group),
            FadeOut(self.spacing_words),
            MoveToTarget(geometric_words),
            FadeIn(arrow),
            FadeIn(algebraic_words),
        )
        word_group.submobjects[0] = geometric_words
        self.play(Write(unit_circle_words, run_time=1))

        # Show complex values
        outer_arrows = self.outer_arrows = self.get_arrows()
        for arrow, point in zip(outer_arrows, self.get_lh_points()):
            arrow.rotate(np.pi, about_point=point)
        outer_arrow = self.outer_arrow = outer_arrows[3].copy()

        values = list(map(plane.point_to_number, self.get_lh_points()))
        complex_decimal = self.complex_decimal = DecimalNumber(
            values[3],
            num_decimal_places=3,
            include_background_rectangle=True
        )
        complex_decimal.next_to(outer_arrow.get_start(), LEFT, SMALL_BUFF)
        complex_decimal_rect = SurroundingRectangle(complex_decimal)
        complex_decimal_rect.fade(1)

        self.play(
            FadeIn(complex_plane_words),
            FadeOut(word_group),
            FadeIn(complex_decimal),
            FadeIn(outer_arrow)
        )
        self.wait(2)
        self.play(
            ChangeDecimalToValue(
                complex_decimal, values[1],
                tracked_mobject=complex_decimal_rect
            ),
            complex_decimal_rect.next_to, outer_arrows[1].get_start(
            ), UP, SMALL_BUFF,
            Transform(outer_arrow, outer_arrows[1]),
            run_time=1.5
        )
        self.wait()

        arrows = self.get_arrows()
        arrows.set_color(YELLOW)
        self.play(
            ReplacementTransform(unit_circle_words, roots_of_unity_words),
            LaggedStartMap(GrowArrow, arrows)
        )
        self.wait()
        self.play(
            complex_plane_words.move_to, word_group,
            LaggedStartMap(FadeOut, VGroup(*it.chain(
                arrows, roots_of_unity_words
            )))
        )

        # Turn decimal into z
        x_term = self.x_term = TexMobject("x")
        x_term.add_background_rectangle()
        x_term.move_to(complex_decimal, DOWN)
        x_term.shift(0.5 * SMALL_BUFF * (DR))
        self.play(ReplacementTransform(complex_decimal, x_term))

    def discuss_powers(self):
        x_term = self.x_term
        outer_arrows = self.outer_arrows
        outer_arrows.add(outer_arrows[0].copy())
        plane = self.complex_plane
        origin = plane.number_to_point(0)

        question = TextMobject("What is $x^2$")
        question.next_to(x_term, RIGHT, LARGE_BUFF)
        question.set_color(YELLOW)

        lh_points = list(self.get_lh_points())
        lh_points.append(lh_points[0])
        lines = VGroup(*[
            Line(origin, point)
            for point in lh_points
        ])
        lines.set_color(GREEN)
        step = 1.0 / self.num_lighthouses
        angle_arcs = VGroup(*[
            Arc(angle=alpha * TAU, radius=0.35).shift(origin)
            for alpha in np.arange(0, 1 + step, step)
        ])
        angle_labels = VGroup()
        for i, arc in enumerate(angle_arcs):
            label = TexMobject("(%d / %d)\\tau" % (i, self.num_lighthouses))
            label.scale(0.5)
            label.add_background_rectangle()
            point = arc.point_from_proportion(0.5)
            point += 1.2 * (point - origin)
            label.move_to(point)
            angle_labels.add(label)
            if i == 0:
                label.shift(0.75 * label.get_height() * DOWN)

        line = self.angle_line = lines[1].copy()
        line_ghost = DashedLine(line.get_start(), line.get_end())
        self.ghost_angle_line = line_ghost
        line_ghost.set_stroke(line.get_color(), 2)
        angle_arc = angle_arcs[1].copy()
        angle_label = angle_labels[1].copy()
        angle_label.shift(0.25 * SMALL_BUFF * DR)

        magnitude_label = TexMobject("1")
        magnitude_label.next_to(line.get_center(), UL, buff=SMALL_BUFF)

        power_labels = VGroup()
        for i, arrow in enumerate(outer_arrows[:-1]):
            label = TexMobject("x^%d" % i)
            label.next_to(
                arrow.get_start(), -arrow.get_vector(),
                submobject_to_align=label[0]
            )
            label.add_background_rectangle()
            power_labels.add(label)
        power_labels[0].next_to(outer_arrows[-1].get_start(), UR, SMALL_BUFF)
        power_labels.submobjects[1] = x_term

        L_labels = self.L_labels = VGroup(*[
            TexMobject("L_%d" % i).move_to(power_label, DOWN).add_background_rectangle(
                opacity=1
            )
            for i, power_label in enumerate(power_labels)
        ])

        # Ask about squaring
        self.play(Write(question))
        self.wait()
        self.play(
            ShowCreation(line),
            Write(magnitude_label)
        )
        self.wait()
        self.play(
            ShowCreation(angle_arc),
            Write(angle_label)
        )
        self.wait()
        self.add(line_ghost)
        for i in list(range(2, self.num_lighthouses)) + [0]:
            anims = [
                Transform(angle_arc, angle_arcs[i]),
                Transform(angle_label, angle_labels[i]),
                Transform(line, lines[i], path_arc=TAU / self.num_lighthouses),
            ]
            if i == 2:
                anims.append(FadeOut(magnitude_label))
            if i == 3:
                anims.append(FadeOut(question))
            self.play(*anims)
            new_anims = [
                GrowArrow(outer_arrows[i]),
                Write(power_labels[i]),
            ]
            if i == 2:
                new_anims.append(FadeOut(self.complex_plane_words))
            self.play(*new_anims)
            self.wait()
        self.play(ReplacementTransform(power_labels, L_labels))
        self.wait()
        self.play(
            Rotate(self.lights, TAU / self.num_lighthouses / 2),
            rate_func=wiggle
        )
        self.wait()
        self.play(
            FadeOut(angle_arc),
            FadeOut(angle_label),
            *list(map(ShowCreationThenDestruction, lines))
        )
        self.wait()

    def raise_everything_to_the_nth(self):
        func_label = TexMobject("L \\rightarrow L^7")
        func_label.set_color(YELLOW)
        func_label.to_corner(UL, buff=LARGE_BUFF)
        func_label.add_background_rectangle()

        polynomial_scale_factor = 0.8

        polynomial = TexMobject("x^%d - 1" % self.num_lighthouses, "=", "0")
        polynomial.scale(polynomial_scale_factor)
        polynomial.next_to(func_label, UP)
        polynomial.to_edge(LEFT)

        factored_polynomial = TexMobject(
            "(x-L_0)(x-L_1)\\cdots(x-L_{%d - 1})" % self.num_lighthouses, "=", "0"
        )
        factored_polynomial.scale(polynomial_scale_factor)
        factored_polynomial.next_to(polynomial, DOWN, aligned_edge=LEFT)
        for group in polynomial, factored_polynomial:
            for part in group:
                part.add_background_rectangle()

        origin = self.complex_plane.number_to_point(0)

        lights = self.lights
        lights.save_state()
        rotations = []
        for i, light in enumerate(lights):
            rotations.append(Rotating(
                light,
                radians=(i * TAU - i * TAU / self.num_lighthouses),
                about_point=origin,
                rate_func=bezier([0, 0, 1, 1]),
            ))

        self.play(Write(func_label, run_time=1))
        for i, rotation in enumerate(rotations[:4]):
            if i == 3:
                rect = SurroundingRectangle(polynomial)
                rect.set_color(YELLOW)
                self.play(
                    FadeIn(polynomial),
                    ShowCreationThenDestruction(rect)
                )
            self.play(
                rotation,
                run_time=np.sqrt(i + 1)
            )
        self.play(*rotations[4:], run_time=3)
        self.wait()

        self.play(lights.restore)
        self.play(
            FadeOut(func_label),
            FadeIn(factored_polynomial)
        )
        self.wait(3)
        self.play(
            factored_polynomial[0].next_to, polynomial[1], RIGHT, 1.5 * SMALL_BUFF,
            FadeOut(polynomial[2]),
            FadeOut(factored_polynomial[1:]),
        )

        # Comment on formula
        formula = VGroup(polynomial[0], polynomial[1], factored_polynomial[0])
        rect = SurroundingRectangle(formula)

        brace = Brace(factored_polynomial[0], DOWN)
        brace2 = Brace(polynomial[0], DOWN)

        morty = PiCreature(color=GREY_BROWN)
        morty.scale(0.5)
        morty.next_to(brace.get_center(), DL, buff=LARGE_BUFF)

        L1_rhs = TexMobject("= \\cos(\\tau / 7) + \\\\", "\\sin(\\tau / 7)i")
        L1_rhs.next_to(self.L_labels[1], RIGHT, aligned_edge=UP)
        for part in L1_rhs:
            part.add_background_rectangle()

        self.play(ShowCreation(rect))
        self.play(FadeOut(rect))
        self.wait()
        self.play(GrowFromCenter(brace))
        self.play(FadeIn(morty))
        self.play(morty.change, "horrified", brace)
        self.play(Blink(morty))
        self.wait()
        self.play(
            Write(L1_rhs),
            morty.change, "confused", L1_rhs
        )
        self.play(Blink(morty))
        self.wait()
        self.play(
            Transform(brace, brace2),
            morty.change, "hooray", brace2
        )
        self.play(Blink(morty))
        self.wait()

        # Nothing special about 7
        new_lights = self.lights.copy()
        new_lights.rotate(
            TAU / self.num_lighthouses / 2,
            about_point=origin
        )
        sevens = VGroup(polynomial[0][1][1], factored_polynomial[0][1][-4])
        n_terms = VGroup()
        for seven in sevens:
            n_term = TexMobject("N")
            n_term.replace(seven, dim_to_match=1)
            n_term.scale(0.9)
            n_term.shift(0.25 * SMALL_BUFF * DR)
            n_terms.add(n_term)

        self.play(LaggedStartMap(FadeOut, VGroup(*it.chain(
            L1_rhs, self.outer_arrows, self.L_labels, self.outer_arrow,
            self.angle_line, self.ghost_angle_line
        ))))
        self.play(LaggedStartMap(SwitchOn, new_lights), morty.look_at, new_lights)
        self.play(Transform(sevens, n_terms))
        self.wait()
        self.play(Blink(morty))
        self.wait()
    #

    def get_arrows(self):
        return VGroup(*[
            Arrow(
                interpolate(self.circle.get_center(), point, 0.6),
                interpolate(self.circle.get_center(), point, 0.9),
                buff=0
            )
            for point in self.get_lh_points()
        ])


class PlugObserverIntoPolynomial(DistanceProductScene):
    CONFIG = {
        # "ambient_light_config": CHEAP_AMBIENT_LIGHT_CONFIG,
        "num_lighthouses": 7,
        # This makes it look slightly better, but renders much slower
        "add_lights_in_foreground": True,
    }

    def construct(self):
        self.add_plane()
        self.add_circle_group()
        self.label_roots()
        self.add_polynomial()
        self.point_out_rhs()
        self.introduce_observer()
        self.raise_observer_to_the_N()

    def add_plane(self):
        plane = self.plane = ComplexPlane(
            unit_size=2,
            y_radius=5,
        )
        plane.shift(DOWN)
        plane.add_coordinates()
        plane.coordinate_labels.submobjects.pop(-4)
        self.origin = plane.number_to_point(0)

        self.add(plane)

    def add_circle_group(self):
        self.circle.set_color(RED)
        self.circle.set_width(
            2 * get_norm(self.plane.number_to_point(1) - self.origin)
        )
        self.circle.move_to(self.origin)

        lights = self.lights = self.get_lights()
        dots = VGroup(*[
            Dot(point) for point in self.get_lh_points()
        ])
        for dot, light in zip(dots, lights):
            light.add_to_back(dot)

        self.add(self.circle, lights)
        if self.add_lights_in_foreground:
            self.add_foreground_mobject(lights)

    def label_roots(self):
        origin = self.origin
        labels = VGroup(*[
            TexMobject("L_%d" % d)
            for d in range(self.num_lighthouses)
        ])
        self.root_labels = labels
        points = self.get_lh_points()
        for label, point in zip(labels, points):
            label.move_to(interpolate(origin, point, 1.2))
        labels[0].align_to(origin, UP)
        labels[0].shift(SMALL_BUFF * DOWN)
        self.add(labels)

    def add_polynomial(self, arg="x"):
        self.polynomial = self.get_polynomial_equation(arg)
        self.add(self.polynomial)

    def point_out_rhs(self):
        rhs = self.get_polynomial_rhs(self.polynomial)
        brace = Brace(rhs, DOWN, buff=SMALL_BUFF)
        brace_text = brace.get_text(
            "Useful for distance product", buff=SMALL_BUFF)
        brace_text.set_color(YELLOW)
        brace_text.add_background_rectangle()

        self.play(
            GrowFromCenter(brace),
            Write(brace_text)
        )
        self.wait()
        self.play(FadeOut(VGroup(brace, brace_text)))

    def introduce_observer(self):
        dot = self.observer_dot = Dot()
        dot.move_to(self.plane.coords_to_point(1.6, 0.8))
        observer = PiCreature(**self.observer_config)
        observer.move_to(dot)
        dot.match_color(observer)

        vect = 2 * DOWN + LEFT
        vect /= get_norm(vect)
        arrow = self.arrow = Vector(0.5 * vect)
        arrow.next_to(observer, -vect, buff=SMALL_BUFF)
        arrow.set_color(WHITE)

        full_name = TextMobject("Observer")
        var_name = self.var_name = TexMobject("O")
        for mob in full_name, var_name:
            mob.match_color(observer)
            mob.next_to(arrow.get_start(), UP, SMALL_BUFF)
            mob.add_background_rectangle()

        complex_decimal = DecimalNumber(0, include_background_rectangle=True)
        equals = TexMobject("=")
        complex_decimal_animation = ChangingDecimal(
            complex_decimal,
            lambda a: self.plane.point_to_number(dot.get_center()),
            position_update_func=lambda m: m.next_to(equals, RIGHT, SMALL_BUFF)
        )
        complex_decimal_animation.update(0)
        equals_decimal = VGroup(equals, complex_decimal)
        equals_decimal.next_to(var_name, RIGHT)

        new_polynomial = self.get_polynomial_equation("O")
        O_terms = new_polynomial.get_parts_by_tex("O")

        lhs, poly_eq, rhs = self.get_polynomial_split(new_polynomial)
        lhs_rect = SurroundingRectangle(lhs, color=YELLOW)
        rhs_rect = SurroundingRectangle(rhs, color=YELLOW)
        self.lhs, self.rhs = lhs, rhs
        self.lhs_rect, self.rhs_rect = lhs_rect, rhs_rect

        lines = self.lines = self.get_lines()
        lines_update = self.lines_update = UpdateFromFunc(
            lines, lambda l: Transform(l, self.get_lines()).update(1)
        )

        anims_for_dot_movement = self.anims_for_dot_movement = [
            MaintainPositionRelativeTo(arrow, dot),
            MaintainPositionRelativeTo(var_name, arrow),
            MaintainPositionRelativeTo(equals, var_name),
            complex_decimal_animation,
            lines_update,
        ]

        self.play(
            FadeInFrom(observer, direction=-vect),
            GrowArrow(arrow)
        )
        self.play(Write(full_name))
        self.wait()
        self.play(
            ReplacementTransform(full_name[0], var_name[0]),
            ReplacementTransform(full_name[1][0], var_name[1][0]),
            FadeOut(full_name[1][1:]),
            ReplacementTransform(observer, dot),
            FadeIn(equals_decimal)
        )
        self.add_foreground_mobject(dot)

        # Substitute
        self.wait()
        self.play(
            ReplacementTransform(var_name.copy(), O_terms),
            ReplacementTransform(self.polynomial, new_polynomial)
        )
        self.polynomial = new_polynomial
        self.wait()

        # Show distances
        self.play(ShowCreation(rhs_rect))
        self.play(
            LaggedStartMap(ShowCreation, lines),
            Animation(dot)
        )

        self.play(
            Rotating(
                dot,
                radians=TAU,
                rate_func=smooth,
                about_point=dot.get_center() + MED_LARGE_BUFF * LEFT,
                run_time=4
            ),
            *anims_for_dot_movement
        )
        self.wait()

        self.remove(rhs_rect)
        self.play(ReplacementTransform(rhs_rect.copy(), lhs_rect))
        self.wait()

        # Move onto circle
        angle = self.observer_angle = TAU / self.num_lighthouses / 3.0
        target_point = self.plane.number_to_point(
            np.exp(complex(0, angle))
        )
        self.play(
            dot.move_to, target_point,
            *anims_for_dot_movement
        )
        self.play(FadeOut(VGroup(
            equals, complex_decimal,
            var_name, arrow,
        )))

    def raise_observer_to_the_N(self):
        dot = self.observer_dot
        origin = self.origin
        radius = self.get_radius()

        text_scale_val = 0.8

        question = TextMobject(
            "What fraction \\\\", "between $L_0$ and $L_1$", "?",
            arg_separator=""
        )
        question.scale(text_scale_val)
        question.next_to(dot, RIGHT)
        question.add_background_rectangle_to_submobjects()

        f_words = TextMobject("$f$", "of the way")
        third_words = TextMobject("$\\frac{1}{3}$", "of the way")
        for words in f_words, third_words:
            words.scale(text_scale_val)
            words.move_to(question[0])
            words[0].set_color(YELLOW)
            words.add_background_rectangle()

        obs_angle = self.observer_angle
        full_angle = TAU / self.num_lighthouses

        def get_arc(angle):
            result = Arc(angle=angle, radius=radius,
                         color=YELLOW, stroke_width=4)
            result.shift(origin)
            return result

        arc = get_arc(obs_angle)
        O_to_N_arc = get_arc(obs_angle * self.num_lighthouses)

        O_to_N_dot = dot.copy().move_to(O_to_N_arc.point_from_proportion(1))
        O_to_N_arrow = Vector(0.5 * DR).next_to(O_to_N_dot, UL, SMALL_BUFF)
        O_to_N_arrow.set_color(WHITE)
        O_to_N_label = TexMobject("O", "^N")
        O_to_N_label.set_color_by_tex("O", dot.get_color())
        O_to_N_label.next_to(O_to_N_arrow.get_start(), UP, SMALL_BUFF)
        O_to_N_label.shift(SMALL_BUFF * RIGHT)
        O_to_N_group = VGroup(O_to_N_arc, O_to_N_arrow, O_to_N_label)

        around_circle_words = TextMobject("around the circle")
        around_circle_words.scale(text_scale_val)
        around_circle_words.add_background_rectangle()
        around_circle_words.next_to(self.circle.get_top(), UR)

        chord = Line(O_to_N_dot.get_center(), self.circle.get_right())
        chord.set_stroke(GREEN)

        chord_halves = VGroup(
            Line(chord.get_center(), chord.get_start()),
            Line(chord.get_center(), chord.get_end()),
        )
        chord_halves.set_stroke(WHITE, 5)

        chord_label = TexMobject("|", "O", "^N", "-", "1", "|")
        chord_label.set_color_by_tex("O", MAROON_B)
        chord_label.add_background_rectangle()
        chord_label.next_to(chord.get_center(), DOWN, SMALL_BUFF)
        chord_label.rotate(
            chord.get_angle(), about_point=chord.get_center()
        )

        numeric_chord_label = DecimalNumber(
            np.sqrt(3),
            num_decimal_places=4,
            include_background_rectangle=True,
            show_ellipsis=True,
        )
        numeric_chord_label.rotate(chord.get_angle())
        numeric_chord_label.move_to(chord_label)

        self.play(
            FadeIn(question),
            ShowCreation(arc),
        )
        for angle in [full_angle - obs_angle, -full_angle, obs_angle]:
            last_angle = angle_of_vector(dot.get_center() - origin)
            self.play(
                self.lines_update,
                UpdateFromAlphaFunc(
                    arc, lambda arc, a: Transform(
                        arc, get_arc(last_angle + a * angle)
                    ).update(1)
                ),
                Rotate(dot, angle, about_point=origin),
                run_time=2
            )
        self.play(
            FadeOut(question[0]),
            FadeOut(question[2]),
            FadeIn(f_words),
        )
        self.wait()
        self.play(
            FadeOut(self.lines),
            FadeOut(self.root_labels),
        )
        self.play(
            ReplacementTransform(dot.copy(), O_to_N_dot),
            ReplacementTransform(arc, O_to_N_arc),
            path_arc=O_to_N_arc.angle - arc.angle,
        )
        self.add_foreground_mobject(O_to_N_dot)
        self.play(
            FadeIn(O_to_N_label),
            GrowArrow(O_to_N_arrow),
        )
        self.wait()
        self.play(
            FadeOut(question[1]),
            f_words.next_to, around_circle_words, UP, SMALL_BUFF,
            FadeIn(around_circle_words)
        )
        self.wait()
        self.play(
            FadeIn(chord_label[0]),
            ReplacementTransform(self.lhs.copy(), chord_label[1]),
            ShowCreation(chord)
        )
        self.wait()

        # Talk through current example
        light_rings = VGroup(*it.chain(self.lights))
        self.play(LaggedStartMap(
            ApplyMethod, light_rings,
            lambda m: (m.shift, MED_SMALL_BUFF * UP),
            rate_func=wiggle
        ))
        self.play(
            FadeOut(around_circle_words),
            FadeIn(question[1]),
            ReplacementTransform(f_words, third_words)
        )
        self.play(
            Rotate(dot, 0.05 * TAU, about_point=origin, rate_func=wiggle)
        )
        self.wait(2)
        self.play(ReplacementTransform(
            dot.copy(), O_to_N_dot, path_arc=TAU / 3))
        self.play(
            third_words.next_to, around_circle_words, UP, SMALL_BUFF,
            FadeIn(around_circle_words),
            FadeOut(question[1])
        )
        self.wait()
        self.play(Indicate(self.lhs))
        for x in range(2):
            self.play(ShowCreationThenDestruction(chord_halves))
        self.play(
            FadeOut(chord_label),
            FadeIn(numeric_chord_label)
        )
        self.wait()
        self.remove(self.lhs_rect)
        self.play(
            FadeOut(chord),
            FadeOut(numeric_chord_label),
            FadeOut(O_to_N_group),
            FadeIn(self.lines),
            ReplacementTransform(self.lhs_rect.copy(), self.rhs_rect)
        )
        self.wait()

        # Add new lights
        for light in self.lights:
            light[1:].fade(0.5)
        added_lights = self.lights.copy()
        added_lights.rotate(full_angle / 2, about_point=origin)
        new_lights = VGroup(*it.chain(*list(zip(self.lights, added_lights))))
        self.num_lighthouses *= 2
        dot.generate_target()
        dot.target.move_to(self.get_circle_point_at_proportion(
            obs_angle / TAU / 2
        ))
        dot.save_state()
        dot.move_to(dot.target)
        new_lines = self.get_lines()
        dot.restore()

        self.play(Transform(self.lights, new_lights))
        self.play(
            MoveToTarget(dot),
            Transform(self.lines, new_lines)
        )
        self.wait()
        self.play(
            third_words.next_to, question[1], UP, SMALL_BUFF,
            FadeOut(around_circle_words),
            FadeIn(question[1]),
        )
        self.wait()

        chord_group = VGroup(chord, numeric_chord_label[1])
        chord_group.set_color(YELLOW)
        self.add_foreground_mobjects(*chord_group)
        self.play(
            FadeIn(chord),
            FadeIn(numeric_chord_label),
        )
        self.wait()

    # Helpers

    def get_polynomial_equation(self, var="x", color=None):
        if color is None:
            color = self.observer_config["color"]
        equation = TexMobject(
            "\\left(", var, "^N", "-", "1", "\\right)", "=",
            "\\left(", var, "-", "L_0", "\\right)",
            "\\left(", var, "-", "L_1", "\\right)",
            "\\cdots",
            "\\left(", var, "-", "L_{N-1}", "\\right)",
        )
        equation.set_color_by_tex(var, color)
        equation.to_edge(UP)
        equation.add_background_rectangle()
        return equation

    def get_polynomial_rhs(self, polynomial):
        return self.get_polynomial_split(polynomial)[2]

    def get_polynomial_lhs(self, polynomial):
        return self.get_polynomial_split(polynomial)[0]

    def get_polynomial_split(self, polynomial):
        eq = polynomial.get_part_by_tex("=")
        i = polynomial[1].submobjects.index(eq)
        return polynomial[1][:i], polynomial[1][i], polynomial[1][i + 1:]

    def get_lines(self, start_point=None):
        return self.get_distance_lines(
            start_point=start_point,
            line_class=DashedLine
        )

    def get_observer_point(self, dummy_arg=None):
        return self.observer_dot.get_center()


class PlugObserverIntoPolynomial5Lighthouses(PlugObserverIntoPolynomial):
    CONFIG = {
        "num_lighthouses": 5,
    }


class PlugObserverIntoPolynomial3Lighthouses(PlugObserverIntoPolynomial):
    CONFIG = {
        "num_lighthouses": 3,
    }


class PlugObserverIntoPolynomial2Lighthouses(PlugObserverIntoPolynomial):
    CONFIG = {
        "num_lighthouses": 2,
    }


class DefineChordF(Scene):
    def construct(self):
        radius = 2.5

        full_chord_f = TextMobject(
            "``", "Chord(", "$f$", ")", "''", arg_separator="")
        full_chord_f.set_color_by_tex("$f$", YELLOW)
        full_chord_f.to_edge(UP)
        chord_f = full_chord_f[1:-1]
        chord_f.generate_target()

        circle = Circle(radius=2.5)
        circle.set_color(RED)
        radius_line = Line(circle.get_center(), circle.get_right())
        one_label = TexMobject("1")
        one_label.next_to(radius_line, DOWN, SMALL_BUFF)

        chord = Line(*[circle.point_from_proportion(f) for f in [0, 1. / 3]])
        chord.set_color(YELLOW)
        chord_third = TextMobject("Chord(", "$1/3$", ")", arg_separator="")
        chord_third.set_color_by_tex("1/3", YELLOW)
        for term in chord_third, chord_f.target:
            term.next_to(chord.get_center(), UP, SMALL_BUFF)
            chord_angle = chord.get_angle() + np.pi
            term.rotate(chord_angle, about_point=chord.get_center())

        brace = Brace(Line(ORIGIN, TAU * UP / 3), RIGHT, buff=0)
        brace.generate_target()
        brace.target.stretch(0.5, 0)
        brace.target.apply_complex_function(np.exp)
        VGroup(brace, brace.target).scale(radius)
        brace.next_to(circle.get_right(), RIGHT, SMALL_BUFF, DOWN)
        brace.scale(0.5, about_edge=DOWN)
        brace.target.move_to(brace, DR)
        brace.target.shift(2 * SMALL_BUFF * LEFT)

        f_label = TexMobject("f")
        f_label.set_color(YELLOW)
        point = circle.point_from_proportion(1.0 / 6)
        f_label.move_to(point + 0.4 * (point - circle.get_center()))

        third_label = TexMobject("\\frac{1}{3}")
        third_label.scale(0.7)
        third_label.move_to(f_label)
        third_label.match_color(f_label)

        alphas = np.linspace(0, 1, 4)
        third_arcs = VGroup(*[
            VMobject().pointwise_become_partial(circle, a1, a2)
            for a1, a2 in zip(alphas, alphas[1:])
        ])
        third_arcs.set_color_by_gradient(BLUE, PINK, GREEN)

        # Terms for sine formula
        origin = circle.get_center()
        height = DashedLine(origin, chord.get_center())
        half_chords = VGroup(
            Line(chord.get_start(), chord.get_center()),
            Line(chord.get_end(), chord.get_center()),
        )
        half_chords.set_color_by_gradient(BLUE, PINK)
        alt_radius_line = Line(origin, chord.get_end())
        alt_radius_line.set_color(WHITE)
        angle_arc = Arc(
            radius=0.3,
            angle=TAU / 6,
        )
        angle_arc.shift(origin)
        angle_label = TexMobject("\\frac{f}{2}", "2\\pi")
        angle_label[0][0].set_color(YELLOW)
        angle_label.scale(0.6)
        angle_label.next_to(angle_arc, RIGHT, SMALL_BUFF, DOWN)
        angle_label.shift(SMALL_BUFF * UR)

        circle_group = VGroup(
            circle, chord, radius_line, one_label,
            brace, f_label, chord_f,
            half_chords, height,
            angle_arc, angle_label,
        )

        formula = TexMobject(
            "= 2 \\cdot \\sin\\left(\\frac{f}{2} 2\\pi \\right)",
            "= 2 \\cdot \\sin\\left(f \\pi \\right)",
        )
        for part in formula:
            part[7].set_color(YELLOW)

        # Draw circle and chord
        self.add(radius_line, circle, one_label)
        self.play(Write(full_chord_f))
        self.play(ShowCreation(chord))
        self.play(
            MoveToTarget(chord_f),
            FadeOut(VGroup(full_chord_f[0], full_chord_f[-1]))
        )
        self.play(GrowFromEdge(brace, DOWN))
        self.play(MoveToTarget(brace, path_arc=TAU / 3))
        self.play(Write(f_label))
        self.wait(2)

        # Show third
        self.remove(chord_f, f_label)
        self.play(
            ReplacementTransform(chord_f.copy(), chord_third),
            ReplacementTransform(f_label.copy(), third_label),
        )
        chord_copies = VGroup()
        last_chord = chord
        for color in PINK, BLUE:
            chord_copy = last_chord.copy()
            old_color = chord_copy.get_color()
            self.play(
                Rotate(chord_copy, -TAU / 6, about_point=last_chord.get_end()),
                UpdateFromAlphaFunc(
                    chord_copy,
                    lambda m, a: m.set_stroke(
                        interpolate_color(old_color, color, a))
                )
            )
            chord_copy.reverse_points()
            last_chord = chord_copy
            chord_copies.add(chord_copy)
        self.wait()
        self.play(
            FadeOut(chord_copies),
            ReplacementTransform(chord_third, chord_f),
            ReplacementTransform(third_label, f_label),
        )

        # Show sine formula
        top_chord_f = chord_f.copy()
        top_chord_f.generate_target()
        top_chord_f.target.rotate(-chord_angle)
        top_chord_f.target.center().to_edge(UP, buff=LARGE_BUFF)
        top_chord_f.target.shift(3 * LEFT)
        formula.next_to(top_chord_f.target, RIGHT)

        self.play(
            ShowCreation(height),
            FadeIn(half_chords),
            ShowCreation(angle_arc),
            Write(angle_label)
        )
        self.wait()
        self.play(
            MoveToTarget(top_chord_f),
            circle_group.shift, 1.5 * DOWN,
        )
        self.play(Write(formula[0], run_time=1))
        self.wait()
        self.play(ReplacementTransform(
            formula[0].copy(), formula[1],
            path_arc=45 * DEGREES
        ))
        self.wait()


class DistanceProductIsChordF(PlugObserverIntoPolynomial):
    CONFIG = {
        "include_lighthouses": False,
        "num_lighthouses": 8,
        # "ambient_light_config": CHEAP_AMBIENT_LIGHT_CONFIG,
        # "add_lights_in_foreground": False,
    }

    def construct(self):
        self.add_plane()
        self.add_circle_group()
        self.add_polynomial("O")
        self.show_all_animations()

    def show_all_animations(self):
        fraction = self.observer_fraction = 0.3
        circle = self.circle

        O_dot = self.observer_dot = Dot()
        O_dot.set_color(self.observer_config["color"])
        O_to_N_dot = O_dot.copy()
        O_dot.move_to(self.get_circle_point_at_proportion(
            fraction / self.num_lighthouses))
        O_to_N_dot.move_to(self.get_circle_point_at_proportion(fraction))

        for dot, vect, tex in [(O_dot, DL, "O"), (O_to_N_dot, DR, "O^N")]:
            arrow = Vector(0.5 * vect, color=WHITE)
            arrow.next_to(dot.get_center(), -vect, SMALL_BUFF)
            label = TexMobject(tex)
            O_part = label[0]
            O_part.match_color(dot)
            label.add_background_rectangle()
            label.next_to(arrow.get_start(), -vect, buff=0,
                          submobject_to_align=O_part)
            dot.arrow = arrow
            dot.label = label
            self.add_foreground_mobject(dot)
            self.add(arrow, label)
            # For the transition to f = 1 / 2
            dot.generate_target()

        fraction_words = VGroup(
            TextMobject("$f$", "of the way"),
            TextMobject("between lighthouses")
        )
        fraction_words.scale(0.8)
        fraction_words[0][0].set_color(YELLOW)
        fraction_words.arrange(DOWN, SMALL_BUFF, aligned_edge=LEFT)
        fraction_words.next_to(O_dot.label, RIGHT)
        list(map(TexMobject.add_background_rectangle, fraction_words))

        f_arc, new_arc = [
            Arc(
                angle=(TAU * f / self.num_lighthouses),
                radius=self.get_radius(),
                color=YELLOW,
            ).shift(circle.get_center())
            for f in (fraction, 0.5)
        ]
        self.add(f_arc)

        lines = self.lines = self.get_lines()
        labels = self.get_numeric_distance_labels()

        black_rect = Rectangle(height=6, width=3.5)
        black_rect.set_stroke(width=0)
        black_rect.set_fill(BLACK, 0.8)
        black_rect.to_corner(DL, buff=0)
        colum_group = self.get_distance_product_column(
            column_top=black_rect.get_top() + MED_SMALL_BUFF * DOWN
        )
        stacked_labels, h_line, times, product_decimal = colum_group

        chord = Line(*[
            self.get_circle_point_at_proportion(f)
            for f in (0, fraction)
        ])
        chord.set_stroke(YELLOW)
        chord_f = get_chord_f_label(chord)
        chord_f_as_product = chord_f.copy()
        chord_f_as_product.generate_target()
        chord_f_as_product.target.rotate(-chord_f_as_product.angle)
        chord_f_as_product.target.scale(0.8)
        chord_f_as_product.target.move_to(product_decimal, RIGHT)

        # Constructs for the case f = 1 / 2
        new_chord = Line(circle.get_right(), circle.get_left())
        new_chord.match_style(chord)
        chord_half = get_chord_f_label(new_chord, "1/2")

        f_terms = VGroup(fraction_words[0][1][0], chord_f_as_product[1][1])
        half_terms = VGroup(*[
            TexMobject("\\frac{1}{2}").scale(0.6).set_color(YELLOW).move_to(f)
            for f in f_terms
        ])
        half_terms[1].move_to(chord_f_as_product.target[1][1])

        O_dot.target.move_to(self.get_circle_point_at_proportion(
            0.5 / self.num_lighthouses))
        O_to_N_dot .target.move_to(circle.get_left())
        self.observer_dot = O_dot.target
        new_lines = self.get_lines()

        changing_decimals = []
        radius = self.get_radius()
        for decimal, line in zip(stacked_labels, new_lines):
            changing_decimals.append(
                ChangeDecimalToValue(decimal, line.get_length() / radius)
            )

        equals_two_terms = VGroup(*[
            TexMobject("=2").next_to(mob, DOWN, SMALL_BUFF)
            for mob in (chord_half, chord_f_as_product.target)
        ])

        # Animations

        self.play(Write(fraction_words))
        self.wait()
        self.play(
            LaggedStartMap(ShowCreation, lines),
            LaggedStartMap(FadeIn, labels),
        )
        self.play(
            FadeIn(black_rect),
            ReplacementTransform(labels.copy(), stacked_labels),
            ShowCreation(h_line),
            Write(times),
        )
        self.wait(2)
        self.add_foreground_mobjects(
            chord_f[1], chord, O_dot, O_to_N_dot
        )
        self.play(
            FadeOut(labels),
            ShowCreation(chord),
            FadeIn(chord_f),
        )
        self.play(MoveToTarget(chord_f_as_product))
        self.wait(2)

        # Transition to f = 1 / 2
        self.play(
            Transform(lines, new_lines),
            Transform(f_arc, new_arc),
            Transform(chord, new_chord),
            chord_f.rotate, -chord_f.angle,
            chord_f.move_to, chord_half,
            MoveToTarget(O_dot),
            MoveToTarget(O_to_N_dot),
            MaintainPositionRelativeTo(O_dot.arrow, O_dot),
            MaintainPositionRelativeTo(O_dot.label, O_dot),
            MaintainPositionRelativeTo(O_to_N_dot.arrow, O_to_N_dot),
            MaintainPositionRelativeTo(O_to_N_dot.label, O_to_N_dot),
            *changing_decimals,
            path_arc=(45 * DEGREES),
            run_time=2
        )
        self.play(
            Transform(chord_f, chord_half),
            Transform(f_terms, half_terms),
        )
        self.wait()
        for term in equals_two_terms:
            term.add_background_rectangle()
            self.add_foreground_mobject(term[1])
        self.play(
            Write(equals_two_terms)
        )
        self.wait()


class ProveLemma2(PlugObserverIntoPolynomial):
    CONFIG = {
        "include_lighthouses": False,
        "num_lighthouses": 8,
        # "ambient_light_config": CHEAP_AMBIENT_LIGHT_CONFIG,
        # "add_lights_in_foreground": False,
    }

    def construct(self):
        self.add_plane()
        self.add_circle_group()
        self.add_polynomial("O")

        self.replace_first_lighthouse()
        self.rearrange_polynomial()
        self.plug_in_one()

    def replace_first_lighthouse(self):
        light_to_remove = self.lights[0]
        dot = self.observer_dot = Dot(color=self.observer_config["color"])
        dot.move_to(self.get_circle_point_at_proportion(
            0.5 / self.num_lighthouses))
        arrow = Vector(0.5 * DL, color=WHITE)
        arrow.next_to(dot, UR, SMALL_BUFF)
        O_label = self.O_dot_label = TexMobject("O")
        O_label.match_color(dot)
        O_label.add_background_rectangle()
        O_label.next_to(arrow, UR, SMALL_BUFF)

        # First, move the lighthouse
        self.add_foreground_mobject(dot)
        self.play(
            dot.move_to, light_to_remove,
            MaintainPositionRelativeTo(arrow, dot),
            MaintainPositionRelativeTo(O_label, dot),
            path_arc=-TAU / 2
        )

        black_rect = Rectangle(
            height=6, width=3.5,
            stroke_width=0,
            fill_color=BLACK,
            fill_opacity=1,
        )
        black_rect.to_corner(DL, buff=0)
        lines = self.get_lines(self.circle.get_right())
        labels = self.get_numeric_distance_labels()
        column_group = self.get_distance_product_column(
            black_rect.get_top() + MED_SMALL_BUFF * DOWN
        )
        stacked_labels, h_line, times, product_decimal = column_group
        q_marks = self.q_marks = TextMobject("???")
        q_marks.move_to(product_decimal, LEFT)
        q_marks.match_color(product_decimal)

        zero_rects = VGroup(
            *list(map(SurroundingRectangle, [dot, stacked_labels[0]])))

        self.play(
            LaggedStartMap(ShowCreation, lines),
            LaggedStartMap(FadeIn, labels),
        )
        self.play(
            FadeIn(black_rect),
            ShowCreation(h_line),
            Write(times),
            ReplacementTransform(labels.copy(), stacked_labels)
        )
        self.wait()
        self.play(ReplacementTransform(
            stacked_labels.copy(),
            VGroup(product_decimal)
        ))
        self.wait()
        self.add_foreground_mobject(zero_rects)
        self.play(*list(map(ShowCreation, zero_rects)))
        self.wait(2)
        self.play(
            VGroup(light_to_remove, zero_rects[0]
                   ).shift, FRAME_WIDTH * RIGHT / 2,
            path_arc=-60 * DEGREES,
            rate_func=running_start,
            remover=True
        )
        self.play(
            VGroup(stacked_labels[0], zero_rects[1]).shift, 4 * LEFT,
            rate_func=running_start,
            remover=True,
        )
        self.remove_foreground_mobjects(zero_rects)
        self.play(
            FadeOut(product_decimal),
            FadeIn(q_marks)
        )
        self.play(FadeOut(labels))
        self.wait()

    def rearrange_polynomial(self):
        dot = self.observer_dot
        lhs, equals, rhs = self.get_polynomial_split(self.polynomial)
        polynomial_background = self.polynomial[0]
        first_factor = rhs[:5]
        remaining_factors = rhs[5:]
        equals_remaining_factors = VGroup(equals, remaining_factors)

        # first_factor_rect = SurroundingRectangle(first_factor)
        lhs_rect = SurroundingRectangle(lhs)

        frac_line = Line(LEFT, RIGHT, color=WHITE)
        frac_line.match_width(lhs, stretch=True)
        frac_line.next_to(lhs, DOWN, SMALL_BUFF)
        O_minus_1 = TexMobject("\\left(", "O", "-", "1", "\\right)")
        O_minus_1.next_to(frac_line, DOWN, SMALL_BUFF)
        new_lhs_background = BackgroundRectangle(
            VGroup(lhs, O_minus_1), buff=SMALL_BUFF)
        new_lhs_rect = SurroundingRectangle(VGroup(lhs, O_minus_1))

        roots_of_unity_circle = VGroup(*[
            Circle(radius=0.2, color=YELLOW).move_to(point)
            for point in self.get_lh_points()
        ])
        for circle in roots_of_unity_circle:
            circle.save_state()
            circle.scale(4)
            circle.fade(1)

        self.play(ShowCreation(lhs_rect))
        self.add_foreground_mobject(roots_of_unity_circle)
        self.play(LaggedStartMap(
            ApplyMethod, roots_of_unity_circle,
            lambda m: (m.restore,)
        ))
        self.wait()
        frac_line_copy = frac_line.copy()
        self.play(
            FadeIn(new_lhs_background),
            polynomial_background.stretch, 0.8, 0,
            polynomial_background.move_to, frac_line_copy, LEFT,
            equals_remaining_factors.arrange, RIGHT, SMALL_BUFF,
            equals_remaining_factors.next_to, frac_line_copy, RIGHT, MED_SMALL_BUFF,
            ReplacementTransform(first_factor, O_minus_1,
                                 path_arc=-90 * DEGREES),
            ShowCreation(frac_line),
            Animation(lhs),
            ReplacementTransform(lhs_rect, new_lhs_rect),
        )
        self.play(
            roots_of_unity_circle[0].shift, FRAME_WIDTH * RIGHT / 2,
            path_arc=(-60 * DEGREES),
            rate_func=running_start,
            remover=True
        )

        # Expand rhs
        expanded_rhs = self.expanded_rhs = TexMobject(
            "=", "1", "+",
            "O", "+",
            "O", "^2", "+",
            "\\cdots",
            "O", "^{N-1}"
        )
        expanded_rhs.next_to(frac_line, RIGHT)
        expanded_rhs.shift(LEFT)
        expanded_rhs.scale(0.9)
        expanded_rhs.set_color_by_tex("O", dot.get_color())

        self.play(
            polynomial_background.stretch, 1.8, 0, {"about_edge": LEFT},
            FadeIn(expanded_rhs),
            equals_remaining_factors.scale, 0.9,
            equals_remaining_factors.next_to, expanded_rhs,
            VGroup(
                new_lhs_background, lhs, frac_line, O_minus_1,
                new_lhs_rect,
            ).shift, LEFT,
        )
        self.wait()

    def plug_in_one(self):
        expanded_rhs = self.expanded_rhs
        O_terms = expanded_rhs.get_parts_by_tex("O")
        ones = VGroup(*[
            TexMobject("1").move_to(O_term, RIGHT)
            for O_term in O_terms
        ])
        ones.match_color(O_terms[0])

        equals_1 = TexMobject("= 1")
        equals_1.next_to(self.O_dot_label, RIGHT, SMALL_BUFF)
        brace = Brace(expanded_rhs[1:], DOWN)
        N_term = brace.get_text("N")

        product = DecimalNumber(
            self.num_lighthouses,
            num_decimal_places=3,
            show_ellipsis=True
        )
        product.move_to(self.q_marks, LEFT)

        self.play(Write(equals_1))
        self.play(
            FocusOn(brace),
            GrowFromCenter(brace)
        )
        self.wait(2)
        self.play(ReplacementTransform(O_terms, ones))
        self.wait()
        self.play(Write(N_term))
        self.play(FocusOn(product))
        self.play(
            FadeOut(self.q_marks),
            FadeIn(product)
        )
        self.wait()


class LocalMathematician(PiCreatureScene):
    def construct(self):
        randy, mathy = self.pi_creatures
        screen = ScreenRectangle(height=2)
        screen.to_corner(UL)
        screen.fade(1)

        mathy_name = TextMobject("Local \\\\ mathematician")
        mathy_name.next_to(mathy, LEFT, LARGE_BUFF)
        arrow = Arrow(mathy_name, mathy)

        self.play(
            Animation(screen),
            mathy.change, "pondering",
            PiCreatureSays(
                randy, "Check these \\\\ out!",
                target_mode="surprised",
                bubble_kwargs={"height": 3, "width": 4},
                look_at_arg=screen,
            ),
        )
        self.play(
            Animation(screen),
            RemovePiCreatureBubble(
                randy, 
                target_mode="raise_right_hand",
                look_at_arg=screen,
            )
        )
        self.wait(2)
        self.play(
            PiCreatureSays(
                mathy, "Ah yes, consider \\\\ $x^n - 1$ over $\\mathds{C}$...",
                look_at_arg=randy.eyes
            ),
            randy.change, "happy", mathy.eyes
        )
        self.wait(3)

    def create_pi_creatures(self):
        randy = Randolph().flip()
        mathy = Mathematician()
        randy.scale(0.9)
        randy.to_edge(DOWN).shift(4 * RIGHT)
        mathy.to_edge(DOWN).shift(4 * LEFT)
        return randy, mathy


class ArmedWithTwoKeyFacts(TeacherStudentsScene, DistanceProductScene):
    CONFIG = {
        "num_lighthouses": 6,
        "ambient_light_config": {
            "opacity_function": inverse_power_law(1, 1, 1, 6),
            "radius": 1,
            "num_levels": 100,
            "max_opacity": 1,
        },
    }

    def setup(self):
        TeacherStudentsScene.setup(self)
        DistanceProductScene.setup(self)

    def construct(self):
        circle1 = self.circle
        circle1.set_height(1.5)
        circle1.to_corner(UL)
        circle2 = circle1.copy()
        circle2.next_to(circle1, DOWN, MED_LARGE_BUFF)

        wallis_product = get_wallis_product(n_terms=8)

        N = self.num_lighthouses
        labels = VGroup()
        for circle, f, dp in (circle1, 0.5, "2"), (circle2, 0, "N"):
            self.circle = circle
            lights = self.get_lights()
            if f == 0:
                lights.submobjects.pop(0)
            observer = Dot(color=MAROON_B)
            frac = f / N
            point = self.get_circle_point_at_proportion(frac)
            observer.move_to(point)
            lines = self.get_distance_lines(point, line_class=DashedLine)

            label = TextMobject("Distance product = %s" % dp)
            label.scale(0.7)
            label.next_to(circle, RIGHT)
            labels.add(label)

            group = VGroup(lines, observer, label)
            self.play(
                FadeIn(circle),
                LaggedStartMap(FadeIn, VGroup(*it.chain(lights))),
                LaggedStartMap(
                    FadeIn, VGroup(
                        *it.chain(group.family_members_with_points()))
                ),
                self.teacher.change, "raise_right_hand",
                self.get_student_changes(*["pondering"] * 3)
            )
        wallis_product.move_to(labels).to_edge(RIGHT)
        self.play(
            LaggedStartMap(FadeIn, wallis_product),
            self.teacher.change_mode, "hooray",
            self.get_student_changes(
                *["thinking"] * 3, look_at_arg=wallis_product)
        )
        self.wait(2)


class Sailor(PiCreature):
    CONFIG = {
        "flip_at_start": True,
        "color": YELLOW_D,
        "hat_height_factor": 1.0 / 6,
    }

    def __init__(self, *args, **kwargs):
        PiCreature.__init__(self, *args, **kwargs)
        height = self.get_height() * self.hat_height_factor
        sailor_hat = SVGMobject(file_name="sailor_hat", height=height)
        # Rhombus is a horrible hack...
        rhombus = Polygon(
            UP, UP + 2 * RIGHT,
            1.75 * RIGHT + 0.5 * UP, 0.5 * RIGHT + 0.1 * DOWN,
            1.25 * LEFT + 0.15 * DOWN,
        )
        rhombus.set_fill(BLACK, opacity=1)
        rhombus.set_stroke(width=0)
        rhombus.set_height(sailor_hat.get_height() / 3)
        rhombus.rotate(5 * DEGREES)
        rhombus.move_to(sailor_hat, DR)
        rhombus.shift(0.05 * sailor_hat.get_width() * LEFT)
        sailor_hat.add_to_back(rhombus)
        sailor_hat.rotate(-15 * DEGREES)
        sailor_hat.move_to(self.eyes.get_center(), DOWN)
        sailor_hat.shift(
            0.1 * self.eyes.get_width() * RIGHT,
            0.1 * self.eyes.get_height() * UP,
        )
        self.add(sailor_hat)


class KeeperAndSailor(DistanceProductScene, PiCreatureScene):
    CONFIG = {
        "num_lighthouses": 9,
        "circle_radius": 2.75,
        # "ambient_light_config": CHEAP_AMBIENT_LIGHT_CONFIG,
        "add_lights_in_foreground": False,  # Keep this way
        "text_scale_val": 0.7,
        "observer_fraction": 0.5,
        "keeper_color": BLUE,
        "sailor_color": YELLOW_D,
        "include_distance_labels_background_rectangle": False,
        "big_circle_center": FRAME_WIDTH * LEFT / 2 + SMALL_BUFF * RIGHT,
    }

    def setup(self):
        DistanceProductScene.setup(self)
        PiCreatureScene.setup(self)
        self.remove(*self.pi_creatures)

    def construct(self):
        self.place_lighthouses()
        self.introduce_observers()
        self.write_distance_product_fraction()
        self.break_down_distance_product_by_parts()
        self.grow_circle_and_N()
        self.show_limit_for_each_fraction()
        self.show_limit_of_lhs()

    def place_lighthouses(self):
        circle = self.circle
        circle.to_corner(DL)
        circle.shift(MED_SMALL_BUFF * UR)
        circle.set_color(RED)

        lighthouses = self.get_lighthouses()
        lights = self.get_lights()
        for light in lights:
            dot = Dot(radius=0.06).move_to(light)
            dot.match_color(light)
            light.add_to_back(dot)
        origin = circle.get_center()
        arrows = VGroup(*[
            Arrow(0.6 * (p - origin), 0.9 * (p - origin), buff=0).shift(origin)
            for p in self.get_lh_points()
        ])
        arrows.set_color(WHITE)

        words = TextMobject("N evenly-spaced \\\\ lighthouses")
        words.scale(0.8)
        words.move_to(origin)

        self.add(circle)
        if self.add_lights_in_foreground:
            self.add_foreground_mobject(lights)
        self.add_foreground_mobject(words)
        self.play(
            LaggedStartMap(FadeIn, VGroup(*it.chain(lights))),
            LaggedStartMap(FadeIn, lighthouses),
            LaggedStartMap(GrowArrow, arrows),
        )
        self.remove_foreground_mobjects(words)
        self.play(FadeOut(words), FadeOut(arrows))
        self.wait()

    def introduce_observers(self):
        keeper, sailor = observers = self.observers
        keeper.target_point = self.get_keeper_point()
        sailor.target_point = self.get_sailor_point()

        for pi, text in (keeper, "Keeper"), (sailor, "Sailor"):
            pi.title = TextMobject(text)
            pi.title.next_to(pi, DOWN)
            pi.dot = Dot()
            pi.dot.match_color(pi)
            pi.dot.next_to(pi, LEFT)
            pi.dot.set_fill(opacity=0)

        self.play(LaggedStartMap(
            Succession, observers,
            lambda m: (FadeIn, m, ApplyMethod, m.change, "wave_1")
        ))
        for pi in observers:
            self.play(
                FadeIn(pi.title),
                pi.change, "plain"
            )
        self.wait()
        if self.add_lights_in_foreground:
            self.add_foreground_mobjects(keeper, keeper.dot, keeper.title)
        for pi in observers:
            self.play(
                pi.set_height, 0.5,
                pi.next_to, pi.target_point, RIGHT, SMALL_BUFF,
                pi.dot.move_to, pi.target_point,
                pi.dot.set_fill, {"opacity": 1},
                pi.title.scale, self.text_scale_val,
                pi.title.next_to, pi.target_point, RIGHT, {"buff": 0.6},
            )
            if pi is sailor:
                arcs = self.get_halfway_indication_arcs()
                self.play(*list(map(ShowCreationThenDestruction, arcs)))
            self.wait()

    def write_distance_product_fraction(self):
        fraction = self.distance_product_fraction = TexMobject(
            "{\\text{Keeper's distance product}", "\\over",
            "\\text{Sailor's distance product}}"
        )
        fraction.scale(self.text_scale_val)
        fraction.to_corner(UR)

        keeper_lines = self.get_distance_lines(
            self.get_keeper_point(),
            line_class=DashedLine
        )
        sailor_lines = self.get_distance_lines(
            self.get_sailor_point(),
            line_class=DashedLine
        )
        sailor_line_lengths = self.get_numeric_distance_labels(sailor_lines)
        keeper_line_lengths = self.get_numeric_distance_labels(keeper_lines)
        sailor_dp_column, keeper_dp_column = [
            self.get_distance_product_column(
                4 * RIGHT + 1.5 * UP, labels, frac
            )
            for labels, frac in [
                (sailor_line_lengths, 0.5),
                (keeper_line_lengths, 0),
            ]
        ]
        sailor_dp_decimal = sailor_dp_column[-1]
        sailor_dp_decimal_rect = SurroundingRectangle(sailor_dp_decimal)
        keeper_dp_decimal = keeper_dp_column[-1]
        keeper_dp_decimal_rect = SurroundingRectangle(keeper_dp_decimal)
        keeper_top_zero_rect = SurroundingRectangle(keeper_dp_column[0][0])

        # stacked_labels, h_line, times, product_decimal = column

        # Define result fraction
        equals = self.distance_product_equals = TexMobject("=")
        result_fraction = self.result_fraction = TexMobject(
            "{N", "{\\text{distance} \\choose \\text{between obs.}}", "\\over", "2}"
        )
        N, dist, frac_line, two = result_fraction
        result_fraction.to_corner(UR)
        equals.next_to(frac_line, LEFT)
        for part in result_fraction:
            part.save_state()
            part.generate_target()
        div = TexMobject("/")
        first_denom = VGroup(two.target, div, dist)
        first_denom.arrange(RIGHT, buff=SMALL_BUFF)
        first_denom.move_to(two, UP)
        N.next_to(frac_line, UP, SMALL_BUFF)

        # Define terms to be removed
        first_light_group = VGroup(self.lights[0], self.lighthouses[0])
        keeper_top_zero_group = VGroup(
            keeper_dp_column[0][0], keeper_top_zero_rect)

        new_keeper_dp_decimal = DecimalNumber(
            self.num_lighthouses,
            num_decimal_places=3,
        )
        new_keeper_dp_decimal.replace(keeper_dp_decimal, dim_to_match=1)
        new_keeper_dp_decimal.set_color(YELLOW)

        self.play(*list(map(ShowCreation, keeper_lines)))
        self.play(ReplacementTransform(
            keeper_lines.copy(), VGroup(fraction[0])
        ))
        self.play(FadeOut(keeper_lines))
        self.play(*list(map(ShowCreation, sailor_lines)))
        self.play(
            ReplacementTransform(
                sailor_lines.copy(),
                VGroup(fraction[2])
            ),
            ShowCreation(fraction[1])
        )
        self.wait()
        self.play(LaggedStartMap(FadeIn, sailor_line_lengths))
        self.play(ReplacementTransform(
            sailor_line_lengths.copy(), sailor_dp_column[0]
        ))
        self.play(FadeIn(sailor_dp_column[1:]))
        self.play(ShowCreation(sailor_dp_decimal_rect))
        self.play(
            fraction.next_to, equals, LEFT,
            FadeIn(equals),
            ShowCreation(frac_line),
            ReplacementTransform(sailor_dp_decimal.copy(), two),
            FadeOut(sailor_dp_decimal_rect)
        )
        self.wait()

        # Note, sailor_lines and sailor_line_lengths get changed here
        self.remove(*list(sailor_lines) + list(sailor_line_lengths))
        self.play(
            FadeOut(sailor_dp_column),
            ReplacementTransform(sailor_lines.deepcopy(), keeper_lines),
            ReplacementTransform(
                sailor_line_lengths.deepcopy(), keeper_line_lengths),
        )
        self.play(ReplacementTransform(
            keeper_line_lengths.copy(), keeper_dp_column[0]
        ))
        self.play(FadeIn(keeper_dp_column[1:]))
        self.wait()
        self.play(
            ShowCreation(keeper_dp_decimal_rect),
            ShowCreation(keeper_top_zero_rect)
        )
        self.wait(2)

        # Remove first lighthouse
        self.play(
            first_light_group.shift, 0.6 * FRAME_WIDTH * RIGHT,
            keeper_top_zero_group.shift, 0.4 * FRAME_WIDTH * RIGHT,
            FadeOut(keeper_dp_decimal),
            FadeOut(keeper_dp_decimal_rect),
            path_arc=-30 * DEGREES,
            rate_func=running_start,
        )
        self.remove(first_light_group, keeper_top_zero_group)
        self.wait()
        self.play(ReplacementTransform(
            keeper_dp_column[0][1:].copy(),
            VGroup(new_keeper_dp_decimal),
        ))
        self.wait()
        self.play(ReplacementTransform(new_keeper_dp_decimal.copy(), N,))
        self.wait(2)

        sailor_lines[0].set_color(RED)
        sailor_line_lengths[0].set_color(RED)
        sailor_line_lengths[0].set_stroke(RED, 1)
        self.remove(*list(keeper_lines) + list(keeper_line_lengths))
        self.play(
            ReplacementTransform(keeper_lines.copy(), sailor_lines),
            ReplacementTransform(
                keeper_line_lengths.copy(), sailor_line_lengths),
            FadeOut(keeper_dp_column[:-1]),
            FadeOut(new_keeper_dp_decimal),
        )
        self.play(
            Rotate(sailor_line_lengths[0], 30 * DEGREES, rate_func=wiggle)
        )
        self.wait()
        self.play(
            ReplacementTransform(sailor_lines[0].copy(), dist),
            FadeIn(div),
            MoveToTarget(two),
        )
        self.wait()
        self.play(
            two.restore,
            FadeOut(div),
            dist.restore,
            N.restore,
        )
        self.play(
            FadeOut(sailor_lines),
            FadeOut(sailor_line_lengths),
        )
        self.wait()

    def break_down_distance_product_by_parts(self):
        result_fraction = self.result_fraction
        result_fraction_rect = SurroundingRectangle(result_fraction)

        product_parts = self.product_parts = TexMobject(
            "{|L_1 - K|", "\\over", "|L_1 - S|}", "\\cdot",
            "{|L_2 - K|", "\\over", "|L_2 - S|}", "\\cdot",
            "{|L_3 - K|", "\\over", "|L_3 - S|}", "\\cdots",
        )
        product_parts.set_color_by_tex_to_color_map({
            "K": BLUE,
            "S": YELLOW,
        })
        product_parts.set_width(0.4 * FRAME_WIDTH)
        product_parts.next_to(result_fraction, DOWN, LARGE_BUFF, RIGHT)
        product_parts.shift(MED_SMALL_BUFF * RIGHT)

        sailor_lines = self.get_sailor_lines()
        sailor_lines.save_state()
        keeper_lines = self.get_keeper_lines()
        keeper_lines.save_state()

        sailor_length_braces = VGroup(VMobject())  # Add fluff first object
        keeper_length_braces = VGroup(VMobject())  # Add fluff first object
        triplets = [
            ("S", sailor_length_braces, DOWN),
            ("K", keeper_length_braces, UP),
        ]
        for char, brace_group, vect in triplets:
            for part in product_parts.get_parts_by_tex(char):
                brace = Brace(part, vect, buff=SMALL_BUFF)
                brace.match_color(part)
                brace_group.add(brace)

        # Animations
        self.replace_lighthouses_with_labels()
        self.play(
            LaggedStartMap(FadeIn, product_parts),
            LaggedStartMap(FadeIn, sailor_lines,
                        rate_func=there_and_back, remover=True),
            LaggedStartMap(FadeIn, keeper_lines,
                        rate_func=there_and_back, remover=True),
        )
        sailor_lines.restore()
        keeper_lines.restore()
        self.wait()

        keeper_line = self.keeper_line = keeper_lines[1].copy()
        sailor_line = self.sailor_line = sailor_lines[1].copy()
        keeper_brace = keeper_length_braces[1].copy()
        sailor_brace = sailor_length_braces[1].copy()
        self.play(
            ShowCreation(keeper_line),
            GrowFromCenter(keeper_brace),
        )
        self.wait()
        self.play(
            ShowCreation(sailor_line),
            GrowFromCenter(sailor_brace),
        )
        self.wait()
        for i in range(2, 4):
            self.play(
                Transform(keeper_line, keeper_lines[i]),
                Transform(keeper_brace, keeper_length_braces[i]),
            )
            self.play(
                Transform(sailor_line, sailor_lines[i]),
                Transform(sailor_brace, sailor_length_braces[i]),
            )
            self.wait()
        for i in range(4, self.num_lighthouses):
            anims = [
                Transform(keeper_line, keeper_lines[i]),
                Transform(sailor_line, sailor_lines[i]),
            ]
            if i == 4:
                anims += [
                    FadeOut(sailor_brace),
                    FadeOut(keeper_brace),
                ]
            self.play(*anims)
        self.play(FocusOn(result_fraction))
        self.play(ShowPassingFlash(result_fraction_rect))
        self.wait(3)

    def grow_circle_and_N(self, circle_scale_factor=2, N_multiple=3, added_anims=None):
        if added_anims is None:
            added_anims = []
        circle = self.circle
        lights = self.lights
        labels = self.lighthouse_labels
        keeper = self.keeper
        sailor = self.sailor
        half_N = self.num_lighthouses / 2

        anims = []

        circle.generate_target()
        for pi in keeper, sailor:
            for mob in pi, pi.dot, pi.title:
                mob.generate_target()

        circle.target.scale(circle_scale_factor)
        circle.target.move_to(self.big_circle_center)
        self.circle = circle.target
        anims.append(MoveToTarget(circle))

        self.num_lighthouses = int(N_multiple * self.num_lighthouses)
        new_lights = self.get_lights()
        for light in new_lights:
            light.scale(1.0 / circle_scale_factor)
        new_labels = self.get_light_labels()
        anims.append(ReplacementTransform(labels[1:], new_labels[1:]))

        if hasattr(self, "keeper_line"):
            keeper_line = self.keeper_line
            sailor_line = self.sailor_line
            self.keeper_lines = self.get_keeper_lines()
            self.sailor_lines = self.get_sailor_lines()
            anims += [
                Transform(keeper_line, self.keeper_lines[-1]),
                Transform(sailor_line, self.sailor_lines[-1]),
            ]

        for group in lights, labels, new_lights, new_labels:
            group[0].fade(1)

        for mob in lights, labels:
            for x in range(len(new_lights) - len(mob)):
                mob.submobjects.insert(
                    half_N + 1, VectorizedPoint(circle.get_left()))
        anims.append(ReplacementTransform(lights, new_lights))

        keeper.dot.target.move_to(self.get_keeper_point())
        sailor.dot.target.move_to(self.get_sailor_point())
        for pi in keeper, sailor:
            pi.target.scale(0)
            pi.target.move_to(pi.dot.target)
            pi.title.target.scale(0.85)
            pi.title.target.next_to(pi.dot.target, RIGHT, SMALL_BUFF)
        anims += [
            MoveToTarget(part)
            for pi in self.observers
            for part in [pi, pi.dot, pi.title]
        ]

        anims += added_anims

        self.circle = circle

        self.play(*anims, run_time=2)
        if self.add_lights_in_foreground:
            self.remove_foreground_mobjects(*self.lights)
            self.remove_foreground_mobjects(*self.lighthouse_labels)
            self.add_foreground_mobjects(new_lights, new_labels)
        self.wait()
        self.lights = new_lights
        self.lighthouse_labels = new_labels

    def show_limit_for_each_fraction(self):
        product_parts = self.product_parts
        keeper_line = self.keeper_line
        keeper_lines = self.keeper_lines
        sailor_line = self.sailor_line
        sailor_lines = self.sailor_lines
        labels = self.lighthouse_labels

        center = self.circle.get_center()
        center_dot = Dot(center)
        lh_points = self.get_lh_points()
        sailor_point = self.get_sailor_point()
        keeper_point = self.get_keeper_point()

        def get_angle_mob(p1, p2):
            angle1 = angle_of_vector(p1 - center)
            angle2 = angle_of_vector(p2 - center)
            arc = Arc(start_angle=angle1, angle=(angle2 - angle1), radius=1)
            arc.shift(center)
            return VGroup(
                center_dot,
                Line(center, p1),
                Line(center, p2),
                arc,
            )

        angle_mob = get_angle_mob(lh_points[1], keeper_point)

        ratios = VGroup(*[
            product_parts[i:i + 3]
            for i in [0, 4, 8]
        ])
        term_rects = self.get_term_rects(ratios)

        limit_fractions = VGroup(
            TexMobject("{2", "\\over", "1}"),
            TexMobject("{4", "\\over", "3}"),
            TexMobject("{6", "\\over", "5}"),
        )
        limit_arrows = VGroup()
        for rect, fraction in zip(term_rects, limit_fractions):
            fraction.next_to(rect, DOWN, LARGE_BUFF)
            arrow = Arrow(rect, fraction, color=WHITE)
            limit_arrows.add(arrow)

        approx = TexMobject("\\approx")
        approx.scale(1.5)
        approx.rotate(90 * DEGREES)
        approx.move_to(limit_arrows[0])

        braces = self.get_all_circle_braces()

        # Show first lighthouse
        term_rect = term_rects[0].copy()
        self.play(
            Transform(keeper_line, keeper_lines[1]),
            Transform(sailor_line, sailor_lines[1]),
            FadeIn(term_rect),
            path_arc=-180 * DEGREES
        )
        self.wait(2)
        self.play(
            FadeOut(VGroup(keeper_line, sailor_line)),
            FadeIn(braces[:2]),
            FadeIn(angle_mob)
        )
        self.wait()
        self.play(Transform(angle_mob, get_angle_mob(
            lh_points[1], sailor_point)))
        self.wait(2)
        self.play(
            Write(approx),
            ReplacementTransform(ratios[0].copy(), limit_fractions[0]),
            FadeOut(angle_mob)
        )
        self.wait()
        self.play(ReplacementTransform(approx, limit_arrows[0]))
        self.let_N_approach_infinity(braces[:2])

        # Show second lighthouse
        self.play(
            Transform(term_rect, term_rects[1]),
            ReplacementTransform(limit_arrows[0].copy(), limit_arrows[1]),
            FadeIn(braces[2:4])
        )
        for group, color in (braces[:4], self.keeper_color), (braces[1:4], self.sailor_color):
            self.play(
                group.scale, 0.95, {"about_point": center},
                group.set_color, color,
                rate_func=there_and_back
            )
            self.wait(0.5)
        self.play(
            ReplacementTransform(ratios[1].copy(), limit_fractions[1])
        )
        self.wait()

        # Show third lighthouse
        braces[4:6].set_color(YELLOW)
        self.play(
            Transform(term_rect, term_rects[2]),
            ReplacementTransform(limit_arrows[1].copy(), limit_arrows[2]),
            FadeIn(braces[4:6]),
            braces[1:4].set_color, YELLOW,
            ReplacementTransform(limit_fractions[1].copy(), limit_fractions[2])
        )
        self.let_N_approach_infinity(braces[:6])
        self.wait()

        # Set up for lighthouse "before" keeper
        ccw_product_group = VGroup(
            product_parts, limit_arrows, limit_fractions)
        cw_product_parts = TexMobject(
            "\\cdots", "{|L_{-3} - K|", "\\over", "|L_{-3} - S|}",
            "\\cdot", "{|L_{-2} - K|", "\\over", "|L_{-2} - S|}",
            "\\cdot", "{|L_{-1} - K|", "\\over", "|L_{-1} - S|}",
        )
        cw_product_parts.match_height(product_parts)
        cw_product_parts.set_color_by_tex_to_color_map({
            "K": BLUE,
            "S": YELLOW,
        })
        cw_product_parts.move_to(ratios, RIGHT)
        cw_ratios = VGroup(*[cw_product_parts[i:i + 3] for i in (9, 5, 1)])
        cw_term_rects = self.get_term_rects(cw_ratios)
        cw_limit_fractions = VGroup(
            TexMobject("{2", "\\over", "3}"),
            TexMobject("{4", "\\over", "5}"),
            TexMobject("{6", "\\over", "7}"),
        )
        cw_limit_arrows = VGroup()
        for rect, fraction in zip(cw_term_rects, cw_limit_fractions):
            fraction.next_to(rect, DOWN, LARGE_BUFF)
            arrow = Arrow(rect, fraction, color=WHITE)
            cw_limit_arrows.add(arrow)

        cw_product_parts.save_state()
        cw_product_parts.next_to(product_parts, RIGHT, LARGE_BUFF)

        cw_label_rects = self.get_term_rects(labels[-1:-5:-1])
        cw_label_rects.set_color(RED)

        braces[-8:].set_color(BLUE)
        braces[0].set_color(YELLOW)

        def show_braces(n):
            cw_group = braces[-2 * n:]
            for group in cw_group, VGroup(braces[0], *cw_group):
                self.play(
                    group.scale, 0.95, {"about_point": center},
                    rate_func=there_and_back
                )
                self.wait(0.5)

        # Animated clockwise-from-keeper terms
        self.play(
            ccw_product_group.scale, 0.5, {"about_edge": UL},
            ccw_product_group.to_corner, UL,
            FadeOut(term_rect),
            FadeOut(braces[:6]),
            cw_product_parts.restore,
        )
        term_rect = cw_term_rects[0].copy()
        self.play(LaggedStartMap(ShowCreationThenDestruction, cw_label_rects))
        self.wait()
        self.play(
            FadeIn(term_rect),
            FadeIn(braces[-2:]),
            FadeIn(braces[0]),
        )
        show_braces(1)
        self.play(
            GrowArrow(cw_limit_arrows[0]),
            FadeIn(cw_limit_fractions[0])
        )
        self.wait()

        # Second and third lighthouse before
        self.play(
            Transform(term_rect, cw_term_rects[1]),
            ReplacementTransform(
                cw_limit_arrows[0].copy(), cw_limit_arrows[1]),
            FadeIn(braces[-4:-2]),
            Write(cw_limit_fractions[1])
        )
        show_braces(2)
        self.wait()
        self.play(
            Transform(term_rect, cw_term_rects[2]),
            ReplacementTransform(
                cw_limit_arrows[1].copy(), cw_limit_arrows[2]),
            FadeIn(braces[-6:-4]),
            Write(cw_limit_fractions[2])
        )
        show_braces(3)
        self.let_N_approach_infinity(VGroup(braces[0], *braces[-6:]))
        self.wait()

        # Organize fractions
        fractions = VGroup(*it.chain(*list(zip(
            limit_fractions, cw_limit_fractions,
        ))))
        fractions.generate_target()
        wallis_product = VGroup()
        dots = VGroup()
        for fraction in fractions.target:
            fraction.match_height(cw_limit_fractions[0])
            wallis_product.add(fraction)
            dot = TexMobject("\\cdot")
            wallis_product.add(dot)
            dots.add(dot)
        final_dot = TexMobject("\\cdots")
        for group in wallis_product, dots:
            group.submobjects[-1] = final_dot
        wallis_product.arrange(RIGHT, buff=MED_SMALL_BUFF)
        wallis_product.to_edge(RIGHT)

        self.play(
            FadeOut(limit_arrows),
            FadeOut(cw_limit_arrows),
            FadeOut(braces[-6:]),
            FadeOut(braces[0]),
            FadeOut(term_rect),
        )
        self.play(
            cw_product_parts.scale, 0.5,
            cw_product_parts.next_to, product_parts, DOWN, {
                "aligned_edge": LEFT},
            MoveToTarget(fractions),
            Write(dots),
            run_time=2,
            path_arc=90 * DEGREES
        )
        self.wait()

        self.wallis_product = VGroup(dots, fractions)
        self.observers_brace = braces[0]

    def show_limit_of_lhs(self):
        brace = self.observers_brace
        wallis_product = self.wallis_product
        result_fraction = self.result_fraction
        N, dist, over, two = result_fraction
        distance_product_equals = self.distance_product_equals

        result_rect = SurroundingRectangle(result_fraction)
        result_rect.set_color(WHITE)

        equals = TexMobject("=")
        equals.next_to(brace, LEFT, SMALL_BUFF)
        approx1, approx2, approx3 = [TexMobject("\\approx") for x in range(3)]
        approx1.next_to(brace, LEFT, SMALL_BUFF)
        half_two_pi_over_N = TexMobject(
            "{1", "\\over", "2}", "{2", "\\pi", "\\over", "N}",
        )
        pi = half_two_pi_over_N.get_part_by_tex("\\pi")
        half_two_pi_over_N.next_to(approx1, LEFT)
        approx2.next_to(half_two_pi_over_N, LEFT, SMALL_BUFF)

        approx3.move_to(distance_product_equals)

        pi_over_N = TexMobject("(", "\\pi", "/", "N", ")")
        pi_over_N.next_to(N, RIGHT)
        N_shift = MED_LARGE_BUFF * RIGHT
        pi_over_N.shift(N_shift)

        pi_halves = TexMobject("{\\pi", "\\over", "2}")
        pi_halves.next_to(result_rect, DOWN, LARGE_BUFF)
        pi_halves.shift(RIGHT)
        pi_halves_arrow = Arrow(
            result_rect.get_bottom(),
            pi_halves.get_top(),
            color=WHITE,
            buff=SMALL_BUFF
        )

        last_equals = TexMobject("=")
        last_equals.next_to(pi_halves, LEFT)

        self.play(ShowCreation(result_rect))
        self.wait()
        self.play(
            dist.next_to, equals, LEFT,
            FadeIn(equals),
            GrowFromCenter(brace),
        )
        self.wait()
        approx2.next_to(dist, LEFT, SMALL_BUFF)
        half_two_pi_over_N.next_to(approx2, LEFT)
        self.play(
            Write(half_two_pi_over_N),
            FadeIn(approx2)
        )
        self.wait()
        self.play(
            FadeOut(half_two_pi_over_N[:4]),
            pi.shift, SMALL_BUFF * LEFT,
        )
        self.wait()
        self.play(
            ReplacementTransform(
                half_two_pi_over_N[-3:].copy(),
                pi_over_N[1:4]
            ),
            FadeIn(pi_over_N[0]),
            FadeIn(pi_over_N[-1]),
            N.shift, N_shift * RIGHT,
            ReplacementTransform(distance_product_equals, approx3)
        )
        self.wait()
        self.play(
            GrowArrow(pi_halves_arrow),
            wallis_product.shift, DOWN,
        )
        self.play(Write(pi_halves))
        self.wait(2)
        self.play(
            wallis_product.next_to, last_equals, LEFT, 2 * SMALL_BUFF,
            FadeIn(last_equals)
        )
        final_rect = SurroundingRectangle(
            VGroup(wallis_product, pi_halves),
            buff=MED_SMALL_BUFF
        )
        final_rect.set_color(YELLOW)
        self.play(ShowCreation(final_rect))
        self.wait(2)

    #

    def let_N_approach_infinity(self, braces=None, factor=4, run_time=5, zoom_in_after=False):
        lights = self.lights
        labels = self.lighthouse_labels
        keeper, sailor = self.observers
        circle = self.circle

        if braces is None:
            braces = VGroup()

        start_fraction = 1.0 / self.num_lighthouses
        target_fraction = start_fraction / factor
        half_N = self.num_lighthouses / 2

        fraction_tracker = ValueTracker(start_fraction)

        def get_fraction():
            return fraction_tracker.get_value()

        def get_ks_distance():
            return get_norm(keeper.dot.get_center() - sailor.dot.get_center())

        def update_title_heights(*titles):
            for title in titles:
                if not hasattr(title, "original_height"):
                    title.original_height = title.get_height()
                title.set_height(min(
                    title.original_height,
                    0.8 * get_ks_distance(),
                ))
            if len(titles) > 1:
                return titles
            else:
                return titles[0]

        initial_light_width = lights[0].get_width()

        def update_lights(lights):
            for k in range(-half_N, half_N + 1):
                if k == 0:
                    continue
                light = lights[k]
                light = light.set_width(
                    (get_fraction() / start_fraction) * initial_light_width
                )
                point = self.get_circle_point_at_proportion(k * get_fraction())
                light.move_source_to(point)
            return lights

        def update_braces(braces):
            for brace in braces:
                f1 = brace.fraction1 * (get_fraction() / start_fraction)
                f2 = brace.fraction2 * (get_fraction() / start_fraction)
                new_brace = self.get_circle_brace(f1, f2)
                new_brace.match_style(brace)
                Transform(brace, new_brace).update(1)
            return braces

        light_update_anim = UpdateFromFunc(lights, update_lights)
        label_update_anim = UpdateFromFunc(
            labels,
            lambda ls: self.position_labels_outside_lights(
                update_title_heights(*ls)),
        )
        sailor_dot_anim = UpdateFromFunc(
            sailor.dot,
            lambda d: d.move_to(
                self.get_circle_point_at_proportion(get_fraction() / 2))
        )
        sailor_title_anim = UpdateFromFunc(
            sailor.title,
            lambda m: update_title_heights(m).next_to(
                sailor.dot, RIGHT, SMALL_BUFF)
        )
        keeper_title_anim = UpdateFromFunc(
            keeper.title,
            lambda m: update_title_heights(m).next_to(
                keeper.dot, RIGHT, SMALL_BUFF)
        )
        braces_update_anim = UpdateFromFunc(braces, update_braces)

        lights[0].fade(1)
        labels[0].fade(1)

        all_updates = [
            light_update_anim,
            label_update_anim,
            sailor_dot_anim,
            sailor_title_anim,
            keeper_title_anim,
            braces_update_anim,
        ]

        self.play(
            fraction_tracker.set_value, target_fraction,
            *all_updates,
            run_time=run_time
        )
        if zoom_in_after:
            self.play(
                circle.scale, factor, {"about_point": circle.get_right()},
                *all_updates,
                run_time=1
            )
            self.wait()
            self.play(
                circle.scale, 1.0 /
                factor, {"about_point": circle.get_right()},
                *all_updates,
                run_time=1
            )
        self.wait()
        self.play(
            fraction_tracker.set_value, start_fraction,
            *all_updates,
            run_time=run_time / 2
        )

    def get_keeper_point(self):
        return self.get_circle_point_at_proportion(0)

    def get_sailor_point(self):
        return self.get_circle_point_at_proportion(0.5 / self.num_lighthouses)

    def create_pi_creatures(self):
        keeper = self.keeper = PiCreature(color=self.keeper_color).flip()
        sailor = self.sailor = Sailor()
        observers = self.observers = VGroup(keeper, sailor)
        observers.set_height(3)
        keeper.shift(4 * RIGHT + 2 * DOWN)
        sailor.shift(4 * RIGHT + 2 * UP)
        return VGroup(keeper, sailor)

    def replace_lighthouses_with_labels(self):
        lighthouse_labels = self.get_light_labels()
        self.lighthouse_labels = lighthouse_labels

        self.remove(self.lights[0], self.lighthouses[0])
        self.play(
            FadeOut(self.lighthouses[1:]),
            FadeIn(lighthouse_labels[1:]),
        )

    def get_light_labels(self):
        labels = VGroup()
        for count, point in enumerate(self.get_lh_points()):
            if count > self.num_lighthouses / 2:
                count -= self.num_lighthouses
            label = TexMobject("L_{%d}" % count)
            label.scale(0.8)
            labels.add(label)
        self.position_labels_outside_lights(labels)
        return labels

    def position_labels_outside_lights(self, labels):
        center = self.circle.get_center()
        for light, label in zip(self.lights, labels):
            point = light[0].get_center()
            vect = (point - center)
            norm = get_norm(vect)
            buff = label.get_height()
            vect *= (norm + buff) / norm
            label.move_to(center + vect)
        return labels

    def get_keeper_lines(self, line_class=Line):
        lines = self.get_distance_lines(self.get_keeper_point())
        lines.set_stroke(self.keeper_color, 3)
        return lines

    def get_sailor_lines(self, line_class=Line):
        lines = self.get_distance_lines(self.get_sailor_point())
        lines.set_stroke(self.sailor_color, 3)
        return lines

    def get_term_rects(self, terms):
        return VGroup(*[
            SurroundingRectangle(term, color=WHITE)
            for term in terms
        ])

    def get_circle_brace(self, f1, f2):
        line = Line(
            self.get_circle_point_at_proportion(f1),
            self.get_circle_point_at_proportion(f2),
        )
        angle = (line.get_angle() + TAU / 2) % TAU
        scale_factor = 1.5
        line.rotate(-angle, about_point=ORIGIN)
        line.scale(scale_factor, about_point=ORIGIN)
        brace = Brace(line, DOWN, buff=SMALL_BUFF)
        group = VGroup(line, brace)
        group.scale(1.0 / scale_factor, about_point=ORIGIN)
        group.rotate(angle, about_point=ORIGIN)

        # Keep track of a fraction between -0.5 and 0.5
        if f1 > 0.5:
            f1 -= 1
        if f2 > 0.5:
            f2 -= 1
        brace.fraction1 = f1
        brace.fraction2 = f2
        return brace

    def get_all_circle_braces(self):
        fractions = np.linspace(0, 1, 2 * self.num_lighthouses + 1)
        return VGroup(*[
            self.get_circle_brace(f1, f2)
            for f1, f2 in zip(fractions, fractions[1:])
        ])


class MentionJohnWallis(Scene):
    def construct(self):
        product = get_wallis_product(10)
        product.to_edge(UP)

        name = TextMobject("``Wallis product''")
        name.scale(1.5)
        name.set_color(BLUE)
        name.next_to(product, DOWN, MED_LARGE_BUFF)

        image = ImageMobject("John_Wallis")
        image.set_height(3)
        image.next_to(name, DOWN)
        image_name = TextMobject("John Wallis")
        image_name.next_to(image, DOWN)

        infinity = TexMobject("\\infty")
        infinity.set_height(1.5)
        infinity.next_to(image, RIGHT, MED_LARGE_BUFF)

        self.add(product)
        self.wait()
        self.play(Write(name))
        self.play(GrowFromEdge(image, UP))
        self.play(Write(image_name))
        self.wait(2)
        self.play(Write(infinity, run_time=3))
        self.wait(2)


class HowThisArgumentRequiresCommunitingLimits(PiCreatureScene):
    def construct(self):
        mathy, morty = self.pi_creatures

        scale_val = 0.7
        factors = TexMobject(
            "{|L_1 - K|", "\\over", "|L_1 - S|}", "\\cdot",
            "{|L_{-1} - K|", "\\over", "|L_{-1} - S|}", "\\cdot",
            "{|L_2 - K|", "\\over", "|L_2 - S|}", "\\cdot",
            "{|L_{-2} - K|", "\\over", "|L_{-2} - S|}", "\\cdot",
            "{|L_3 - K|", "\\over", "|L_3 - S|}", "\\cdot",
            "{|L_{-3} - K|", "\\over", "|L_{-3} - S|}", "\\cdots",
        )
        factors.set_color_by_tex_to_color_map({
            "K": BLUE,
            "S": YELLOW,
        })
        equals = TexMobject("=")
        result = TexMobject(
            "{N", "{\\text{distance} \\choose \\text{between obs.}}",
            "\\over", "2}"
        )

        top_line = VGroup(factors, equals, result)
        top_line.arrange(RIGHT, buff=SMALL_BUFF)
        result.shift(SMALL_BUFF * UP)
        top_line.scale(scale_val)
        top_line.to_edge(UP)

        fractions = VGroup(*[
            factors[i:i + 3]
            for i in range(0, len(factors), 4)
        ])
        fraction_limit_arrows = VGroup(*[
            Vector(0.5 * DOWN).next_to(fraction, DOWN)
            for fraction in fractions
        ])
        fraction_limit_arrows.set_color(WHITE)

        wallis_product = get_wallis_product(6)
        fraction_limits = VGroup(*[
            wallis_product[i:i + 3]
            for i in range(0, 4 * 6, 4)
        ])
        for lf, arrow in zip(fraction_limits, fraction_limit_arrows):
            lf.next_to(arrow, DOWN, MED_SMALL_BUFF)

        result_limit_arrow = fraction_limit_arrows[0].copy()
        result_limit_arrow.next_to(result, DOWN)
        result_limit_arrow.align_to(fraction_limit_arrows[0])

        result_limit = wallis_product[-3:]
        result_limit.next_to(result_limit_arrow, DOWN, MED_SMALL_BUFF)

        lower_equals = TexMobject("=")
        lower_equals.next_to(result_limit, LEFT)

        mult_signs = VGroup()
        for f1, f2 in zip(fraction_limits, fraction_limits[1:]):
            mult_sign = TexMobject("\\times")
            mult_sign.move_to(VGroup(f1, f2))
            mult_signs.add(mult_sign)
        cdots = TexMobject("\\cdots")
        cdots.move_to(VGroup(fraction_limits[-1], lower_equals))
        mult_signs.add(cdots)

        # Pi creatures react
        self.play(
            PiCreatureSays(
                mathy,
                "Whoa whoa whoa \\\\ there buddy",
                look_at_arg=morty.eyes,
                target_mode="sassy",
            ),
            morty.change, "guilty", mathy.eyes,
        )
        self.wait(2)

        # Write out commutative diagram
        self.play(
            RemovePiCreatureBubble(
                mathy,
                target_mode="raise_right_hand",
                look_at_arg=factors,
            ),
            morty.change, "pondering", factors,
            LaggedStartMap(FadeIn, factors),
        )
        self.wait()
        self.play(
            FadeIn(equals),
            Write(result)
        )
        self.wait()
        self.play(
            LaggedStartMap(GrowArrow, fraction_limit_arrows),
            LaggedStartMap(
                FadeInFrom, fraction_limits,
                direction=UP
            ),
            run_time=4,
            lag_ratio=0.25,
        )
        self.wait()
        self.play(
            LaggedStartMap(FadeIn, mult_signs),
            FadeIn(lower_equals),
            mathy.change, "sassy",
        )
        self.play(
            GrowArrow(result_limit_arrow),
            FadeInFrom(result_limit, direction=UP),
            morty.change, "confused",
        )
        self.wait(2)

        # Write general limit rule
        limit_rule = TexMobject(
            "\\left( \\lim_{N \\to \\infty} a_N^{(1)} \\right)",
            "\\left( \\lim_{N \\to \\infty} a_N^{(2)} \\right)",
            "\\cdots", "=",
            "\\lim_{N \\to \\infty} \\left( a_N^{(1)} a_N^{(2)} \\cdots \\right)"
        )
        limit_rule.next_to(self.pi_creatures, UP)
        q_marks = TexMobject("???")
        q_marks.set_color(YELLOW)
        limit_equals = limit_rule.get_part_by_tex("=")
        q_marks.next_to(limit_equals, UP, SMALL_BUFF)

        index_of_equals = limit_rule.index_of_part(limit_equals)
        lhs_brace = Brace(limit_rule[:index_of_equals], UP)
        rhs_brace = Brace(limit_rule[index_of_equals + 1:], UP)

        self.play(
            FadeInFromDown(limit_rule),
            mathy.change, "angry",
            morty.change, "erm",
        )
        self.play(GrowFromCenter(lhs_brace))
        self.wait()
        self.play(ReplacementTransform(lhs_brace, rhs_brace))
        self.wait(2)
        self.play(FadeOut(rhs_brace), Write(q_marks))
        limit_rule.add(q_marks)
        self.wait(2)
        self.play(morty.change, "pondering")
        self.play(mathy.change, "tease")
        self.wait(3)
        self.play(
            Animation(limit_rule),
            morty.change, "pondering",
            mathy.change, "pondering",
        )
        self.wait(3)

        # Write dominated convergence
        mover = VGroup(
            top_line, fraction_limits, fraction_limit_arrows,
            mult_signs, lower_equals,
            result_limit, result_limit_arrow,
        )
        self.play(
            mover.next_to, FRAME_HEIGHT * UP / 2, UP,
            limit_rule.to_edge, UP,
        )
        dominated_convergence = TextMobject("``Dominated convergence''")
        dominated_convergence.set_color(BLUE)
        dominated_convergence.next_to(limit_rule, DOWN, LARGE_BUFF)

        see_blog_post = TextMobject("(See supplementary blog post)")
        see_blog_post.next_to(dominated_convergence, DOWN)

        self.play(
            FadeInFromDown(dominated_convergence),
            mathy.change, "raise_right_hand",
        )
        self.play(morty.change, "thinking")
        self.wait()
        self.play(Write(see_blog_post))
        self.wait(4)

    def create_pi_creatures(self):
        group = VGroup(PiCreature(color=GREY), Mortimer())
        group.set_height(2)
        group.arrange(RIGHT, buff=4)
        group.to_edge(DOWN)
        return group


class NonCommunitingLimitsExample(Scene):
    CONFIG = {
        "num_terms_per_row": 6,
        "num_rows": 5,
        "x_spacing": 0.75,
        "y_spacing": 1,
    }

    def construct(self):
        rows = VGroup(*[
            self.get_row(seven_index)
            for seven_index in range(self.num_rows)
        ])
        rows.add(self.get_v_dot_row())
        for n, row in enumerate(rows):
            row.move_to(n * self.y_spacing * DOWN)
        rows.center().to_edge(UP)
        rows[-1].shift(MED_SMALL_BUFF * UP)

        row_rects = VGroup(*list(map(SurroundingRectangle, rows[:-1])))
        row_rects.set_color(YELLOW)

        columns = VGroup(*[
            VGroup(*[row[0][i] for row in rows])
            for i in range(len(rows[0][0]))
        ])
        column_rects = VGroup(*list(map(SurroundingRectangle, columns)))
        column_rects.set_color(BLUE)

        row_arrows = VGroup(*[
            TexMobject("\\rightarrow").next_to(row, RIGHT)
            for row in rows[:-1]
        ])
        row_products = VGroup(*[
            Integer(7).next_to(arrow)
            for arrow in row_arrows
        ])
        row_products.set_color(YELLOW)

        row_product_limit_dots = TexMobject("\\vdots")
        row_product_limit_dots.next_to(row_products, DOWN)
        row_product_limit = Integer(7)
        row_product_limit.set_color(YELLOW)
        row_product_limit.next_to(row_product_limit_dots, DOWN)

        column_arrows = VGroup(*[
            TexMobject("\\downarrow").next_to(part, DOWN, SMALL_BUFF)
            for part in rows[-1][0]
        ])
        column_limits = VGroup(*[
            Integer(1).next_to(arrow, DOWN)
            for arrow in column_arrows
        ])
        column_limits.set_color(BLUE)
        column_limit_dots = self.get_row_dots(column_limits)

        column_limits_arrow = TexMobject("\\rightarrow").next_to(
            column_limit_dots, RIGHT
        )
        product_of_limits = Integer(1).next_to(column_limits_arrow, RIGHT)
        product_of_limits.set_color(BLUE)

        self.add(rows)
        self.wait()
        self.play(LaggedStartMap(ShowCreation, row_rects))
        self.wait(2)
        row_products_iter = iter(row_products)
        self.play(
            LaggedStartMap(Write, row_arrows),
            LaggedStartMap(
                ReplacementTransform, rows[:-1].copy(),
                lambda r: (r, next(row_products_iter))
            )
        )
        self.wait()
        self.play(Write(row_product_limit_dots))
        self.play(Write(row_product_limit))
        self.wait()

        self.play(LaggedStartMap(FadeOut, row_rects))
        self.play(LaggedStartMap(FadeIn, column_rects))
        self.wait()
        column_limit_iter = iter(column_limits)
        self.play(
            LaggedStartMap(Write, column_arrows),
            LaggedStartMap(
                ReplacementTransform, columns.copy(),
                lambda c: (c, next(column_limit_iter))
            )
        )
        self.wait()
        self.play(Write(column_limits_arrow))
        self.play(
            Write(product_of_limits),
            FadeOut(row_product_limit)
        )
        self.wait()


    def get_row(self, seven_index):
        terms = [1] * (self.num_terms_per_row)
        if seven_index < len(terms):
            terms[seven_index] = 7
        row = VGroup(*list(map(Integer, terms)))
        self.arrange_row(row)
        dots = self.get_row_dots(row)

        return VGroup(row, dots)

    def get_v_dot_row(self):
        row = VGroup(*[
            TexMobject("\\vdots")
            for x in range(self.num_terms_per_row)
        ])
        self.arrange_row(row)
        dots = self.get_row_dots(row)
        dots.fade(1)
        return VGroup(row, dots)

    def arrange_row(self, row):
        for n, part in enumerate(row):
            part.move_to(n * self.x_spacing * RIGHT)

    def get_row_dots(self, row):
        dots = VGroup()
        for p1, p2 in zip(row, row[1:]):
            dots.add(TexMobject("\\cdot").move_to(VGroup(p1, p2)))
        dots.add(TexMobject("\\cdots").next_to(row, RIGHT))
        return dots


class DelicacyInIntermixingSeries(Scene):
    def construct(self):
        n_terms = 6

        top_product, bottom_product = products = VGroup(*[
            TexMobject(*it.chain(*[
                ["{%d" % (2 * x), "\\over", "%d}" % (2 * x + u), "\\cdot"]
                for x in range(1, n_terms + 1)
            ]))
            for u in (-1, 1)
        ])
        top_product.set_color(GREEN)
        bottom_product.set_color(BLUE)
        top_product.to_edge(UP)
        bottom_product.next_to(top_product, DOWN, LARGE_BUFF)

        infinity = TexMobject("\\infty")
        top_product.limit = infinity
        zero = TexMobject("0")
        bottom_product.limit = zero

        for product in products:
            cdots = TexMobject("\\cdots")
            cdots.move_to(product[-1], LEFT)
            cdots.match_color(product)
            product.submobjects[-1] = cdots
            product.parts = VGroup(*[
                product[i:i + 4]
                for i in range(0, len(product), 4)
            ])
            arrow = Vector(0.75 * RIGHT)
            arrow.set_color(WHITE)
            arrow.next_to(product, RIGHT)
            product.arrow = arrow
            product.limit.next_to(arrow, RIGHT)
            product.limit.match_color(product)

        group = VGroup(products, infinity)
        h_line = Line(LEFT, RIGHT)
        h_line.stretch_to_fit_width(group.get_width() + LARGE_BUFF)
        h_line.next_to(group, DOWN, aligned_edge=RIGHT)
        times = TexMobject("\\times")
        times.next_to(h_line, UP, aligned_edge=LEFT)
        q_marks = TexMobject("?????")
        q_marks.set_color_by_gradient(BLUE, YELLOW)
        q_marks.scale(2)
        q_marks.next_to(h_line, DOWN)

        # Show initial products
        self.play(
            LaggedStartMap(FadeIn, top_product),
            LaggedStartMap(FadeIn, bottom_product),
        )
        self.wait()
        for product in products:
            self.play(
                GrowArrow(product.arrow),
                FadeInFrom(product.limit, direction=LEFT)
            )
            self.wait()
        self.play(
            Write(times),
            ShowCreation(h_line)
        )
        self.play(Write(q_marks, run_time=3))
        self.wait(2)

        # Show alternate interweaving
        top_parts_iter = iter(top_product.parts)
        bottom_parts_iter = iter(bottom_product.parts)
        movers1 = VGroup()
        while True:
            try:
                new_terms = [
                    next(bottom_parts_iter),
                    next(top_parts_iter),
                    next(top_parts_iter),
                ]
                movers1.add(*new_terms)
            except StopIteration:
                break
        new_product = VGroup()
        movers1.save_state()
        for mover in movers1:
            mover.generate_target()
            new_product.add(mover.target)
        new_product.arrange(RIGHT, buff=SMALL_BUFF)
        new_product.next_to(h_line, DOWN, LARGE_BUFF, aligned_edge=LEFT)

        new_arrow = top_product.arrow.copy()
        new_arrow.next_to(new_product, RIGHT)

        ghost_top = top_product.copy().fade()
        ghost_bottom = bottom_product.copy().fade()
        self.add(ghost_top, top_product)
        self.add(ghost_bottom, bottom_product)

        new_limit = TexMobject("\\frac{\\pi}{2}", "\\sqrt{2}")
        new_limit.next_to(new_arrow, RIGHT)

        randy = Randolph(height=1.5)
        randy.flip()
        randy.to_corner(DR)

        movers2 = VGroup(*it.chain(*list(zip(
            top_product.parts, bottom_product.parts
        ))))
        final_product = VGroup()
        for mover in movers2:
            mover.final_position = mover.copy()
            if mover is movers2[-2]:
                # Excessive ellipses
                final_dot = mover.final_position[-1][0]
                mover.final_position.submobjects[-1] = final_dot
            final_product.add(mover.final_position)
        final_product.arrange(RIGHT, buff=SMALL_BUFF)
        final_product.move_to(new_product, RIGHT)

        self.play(
            FadeOut(q_marks),
            LaggedStartMap(
                MoveToTarget, movers1,
                run_time=5,
                lag_ratio=0.2,
            )
        )
        self.play(
            GrowArrow(new_arrow),
            FadeInFrom(new_limit, LEFT),
            bottom_product.parts[3:].fade, 1,
        )
        self.play(FadeIn(randy))
        self.play(randy.change, "confused", new_limit)
        self.wait()
        self.play(Blink(randy))
        self.wait()
        self.play(LaggedStartMap(
            Transform, movers2,
            lambda m: (m, m.final_position),
            run_time=3,
            path_arc=TAU / 4,
        ))
        self.play(
            FadeOut(new_limit[1]),
            randy.change, "pondering", new_limit
        )
        self.wait(2)
        self.play(Blink(randy))
        self.wait(2)


class JustTechnicalities(TeacherStudentsScene):
    def construct(self):
        self.teacher_says(
            "These are just \\\\ technicalities"
        )
        self.change_all_student_modes("happy")
        self.play(RemovePiCreatureBubble(
            self.teacher, target_mode="raise_right_hand",
        ))
        self.look_at(self.screen)
        self.wait(4)


class KeeperAndSailorForSineProduct(KeeperAndSailor):
    CONFIG = {
        # "ambient_light_config": CHEAP_AMBIENT_LIGHT_CONFIG,
        "new_sailor_fraction": 0.7,
        "big_circle_center": FRAME_WIDTH * LEFT / 2 + 2.6 * LEFT,
    }

    def construct(self):
        # Rerun old animation (probably to be skipped)g
        self.force_skipping()
        self.place_lighthouses()
        self.introduce_observers()
        self.write_distance_product_fraction()
        self.revert_to_original_skipping_status()

        # New animations
        self.replace_lighthouses_with_labels()
        self.move_sailor_to_new_spot()
        self.show_new_distance_product()
        self.show_new_limit_of_ratio()
        self.show_new_infinite_product()

    def move_sailor_to_new_spot(self):
        sailor = self.sailor
        fraction = self.new_sailor_fraction
        self.get_sailor_point = lambda: self.get_circle_point_at_proportion(
            fraction / self.num_lighthouses
        )
        target_point = self.get_sailor_point()

        brace1 = self.get_circle_brace(0, 0.5 / self.num_lighthouses)
        brace2 = self.get_circle_brace(0, fraction / self.num_lighthouses)

        center = self.circle.get_center()
        radius = self.get_radius()

        def warp_func(point):
            vect = point - center
            norm = get_norm(vect)
            new_norm = norm + 0.5 * (radius - norm)
            return center + new_norm * vect / norm
        brace1.apply_function(warp_func)
        brace2.apply_function(warp_func)

        scale_val = 0.7
        words1 = TextMobject("Instead of", "$\\frac{1}{2}$")
        words1.set_color_by_tex("$", YELLOW)
        words1.scale(scale_val)
        words1.next_to(brace1.get_center(), LEFT)
        words2 = TextMobject("Some other fraction", "$f$")
        words2.set_color_by_tex("$", GREEN)
        words2.scale(scale_val)
        words2.next_to(brace2.get_center(), LEFT)

        self.play(
            GrowFromCenter(brace1),
            Write(words1, run_time=1)
        )
        self.wait()
        self.play(
            sailor.dot.move_to, target_point,
            sailor.dot.set_color, GREEN,
            sailor.set_color, GREEN,
            MaintainPositionRelativeTo(sailor, sailor.dot),
            MaintainPositionRelativeTo(sailor.title, sailor),
            ReplacementTransform(brace1, brace2),
            ReplacementTransform(words1, words2),
            run_time=1.5
        )

        self.fraction_label_group = VGroup(brace2, words2)

    def show_new_distance_product(self):
        result_fraction = self.result_fraction
        N, dist, over, two = result_fraction
        sailor_dp = self.distance_product_fraction[-1]
        sailor_dp_rect = SurroundingRectangle(sailor_dp)
        sailor_dp_rect.set_color(GREEN)

        sailor_lines = self.get_distance_lines(
            self.sailor.dot.get_center(), line_class=DashedLine
        )
        chord_f = TextMobject("Chord(", "$f$", ")", arg_separator="")
        chord_f.set_color_by_tex("$f$", GREEN)
        chord_f.move_to(two, UP)

        two_cross = Cross(SurroundingRectangle(two))
        two_group = VGroup(two, two_cross)

        self.play(
            ShowCreation(sailor_dp_rect),
            LaggedStartMap(ShowCreation, sailor_lines),
        )
        self.wait()
        self.play(ShowCreation(two_cross))
        self.play(
            two_group.next_to, chord_f, DOWN,
            ReplacementTransform(
                VGroup(
                    sailor_dp[:-6].copy(),
                    sailor_dp[-6:-4].copy(),
                    sailor_dp[-4:-2].copy(),
                    sailor_dp[-2:].copy(),
                ),
                chord_f
            )
        )
        self.wait()
        self.play(LaggedStartMap(FadeOut, VGroup(
            sailor_lines, sailor_dp_rect, two_group
        )))

        result_fraction.submobjects[-1] = chord_f

    def show_new_limit_of_ratio(self):
        fraction_label_group = self.fraction_label_group
        fraction_brace, fraction_words = fraction_label_group

        frac1 = TexMobject(
            "{N", "(", "f", " \\cdot 2\\pi / N)", "\\over",
            "\\text{Chord}(", "f", ")}"
        )
        frac2 = TexMobject(
            "{f", "\\cdot", "2", "\\pi", "\\over",
            "\\text{Chord}(", "f", ")}"
        )
        for frac in frac1, frac2:
            frac.set_color_by_tex("f", GREEN, substring=False)
        arrow1, arrow2 = [
            Vector(0.75 * DOWN, color=WHITE)
            for x in range(2)
        ]

        group = VGroup(arrow1, frac1, arrow2, frac2)
        group.arrange(DOWN)
        group.next_to(self.result_fraction, DOWN)
        big_group = VGroup(self.result_fraction, *group)

        arrow = TexMobject("\\rightarrow")
        arrow.move_to(self.distance_product_equals)

        fraction_brace.generate_target()
        fraction_brace.target.rotate(-10 * DEGREES)
        fraction_brace.target.scale(0.65)
        fraction_brace.target.align_to(ORIGIN, DOWN)
        fraction_brace.target.shift(3.3 * LEFT)
        fraction_words.generate_target()
        fraction_words.target[0][:9].next_to(
            VGroup(fraction_words.target[0][9:], fraction_words.target[1]),
            UP, SMALL_BUFF
        )
        fraction_words.target.next_to(fraction_brace.target, LEFT, SMALL_BUFF)

        self.play(LaggedStartMap(FadeIn, group))
        self.grow_circle_and_N(
            added_anims=[
                MoveToTarget(fraction_brace),
                MoveToTarget(fraction_words),
            ]
        )
        self.wait()
        self.play(
            big_group.next_to, self.distance_product_equals, RIGHT,
            {"submobject_to_align": frac2},
            UpdateFromAlphaFunc(
                big_group[:-1],
                lambda m, a: m.set_fill(opacity=1 - a),
            ),
            Transform(self.distance_product_equals, arrow)
        )
        self.wait()

        self.result_fraction = frac2

    def show_new_infinite_product(self):
        scale_val = 0.7
        fractions = TexMobject(
            "\\cdots",
            "{|L_{-2} - K|", "\\over", "|L_{-2} - S|}", "\\cdot",
            "{|L_{-1} - K|", "\\over", "|L_{-1} - S|}", "\\cdot",
            "{|L_1 - K|", "\\over", "|L_1 - S|}", "\\cdot",
            "{|L_2 - K|", "\\over", "|L_2 - S|}", "\\cdot",
            "{|L_3 - K|", "\\over", "|L_3 - S|}", "\\cdots",
        )
        fractions.scale(scale_val)
        fractions.move_to(self.observers)
        fractions.to_edge(RIGHT)
        fractions.set_color_by_tex_to_color_map({
            "K": BLUE,
            "S": GREEN,
        })

        keeper_lines = self.get_keeper_lines()
        sailor_lines = self.get_sailor_lines()
        sailor_lines.set_color(GREEN)

        ratios = VGroup(*[
            fractions[i:i + 3]
            for i in range(1, len(fractions), 4)
        ])
        limit_arrows = VGroup(*[
            Vector(0.5 * DOWN, color=WHITE).next_to(ratio, DOWN, SMALL_BUFF)
            for ratio in ratios
        ])
        limits = VGroup(*[
            TexMobject("{%d" % k, "\\over", "%d" % k, "-", "f}")
            for k in (-2, -1, 1, 2, 3)
        ])
        for limit, arrow in zip(limits, limit_arrows):
            limit.set_color_by_tex("f", GREEN)
            limit.scale(scale_val)
            limit.next_to(arrow, DOWN, SMALL_BUFF)
        dots = VGroup()
        dots.add(TexMobject("\\cdots").next_to(limits, LEFT))
        for l1, l2 in zip(limits, limits[1:]):
            dots.add(TexMobject("\\cdot").move_to(VGroup(l1, l2)))
        dots.add(TexMobject("\\cdots").next_to(limits, RIGHT))
        full_limits_group = VGroup(*list(limits) + list(dots))

        # brace = Brace(limits, DOWN)
        product = TexMobject(
            "\\prod_{k \\ne 0}", "{k", "\\over", "k", "-", "f}"
        )
        product.next_to(limits, DOWN, LARGE_BUFF)
        product_lines = VGroup(
            DashedLine(full_limits_group.get_corner(DL), product.get_corner(UL)),
            DashedLine(full_limits_group.get_corner(DR), product.get_corner(UR)),
        )
        product_lines.set_color(YELLOW)

        self.play(
            LaggedStartMap(FadeIn, fractions),
            *[
                LaggedStartMap(
                    FadeIn, VGroup(*list(lines[-10:]) + list(lines[1:10])),
                    rate_func=there_and_back,
                    remover=True,
                    run_time=3,
                    lag_ratio=0.1
                )
                for lines in (keeper_lines, sailor_lines)
            ]
        )
        self.wait()
        self.play(
            LaggedStartMap(GrowArrow, limit_arrows),
            LaggedStartMap(
                FadeInFrom, limits,
                lambda m: (m, UP),
            ),
            LaggedStartMap(FadeIn, dots)
        )
        self.wait()
        self.play(
            # GrowFromCenter(brace),
            ShowCreation(product_lines, lag_ratio=0),
            FadeIn(product)
        )
        self.wait()

        # Shift everything
        result_fraction = self.result_fraction
        big_group = VGroup(
            fractions, limit_arrows, full_limits_group,
            # brace,
            product_lines,
            product,
        )
        big_group.generate_target()
        big_group.target.to_edge(UP)
        equals = TexMobject("=")
        equals.next_to(big_group.target[-1], LEFT)
        result_fraction.generate_target()
        result_fraction.target.next_to(equals, LEFT)

        self.play(
            MoveToTarget(big_group),
            FadeIn(equals),
            MoveToTarget(result_fraction),
            FadeOut(self.distance_product_fraction),
            FadeOut(self.distance_product_equals),
        )
        self.wait()

        # Replace chord with sine
        chord_f = result_fraction[-3:]
        f_pi = VGroup(result_fraction[0], result_fraction[3])
        over = result_fraction.get_part_by_tex("\\over")
        dot_two = result_fraction[1:3]

        two_sine_f_pi = TexMobject("2", "\\sin(", "f", "\\pi", ")")
        sine_f_pi = two_sine_f_pi[1:]
        two_sine_f_pi.set_color_by_tex("f", GREEN)
        two_sine_f_pi.move_to(chord_f)

        self.play(
            FadeIn(two_sine_f_pi),
            chord_f.shift, DOWN
        )
        self.wait()
        self.play(FadeOut(chord_f))
        self.wait()
        self.play(
            f_pi.arrange, RIGHT, {"buff": SMALL_BUFF},
            f_pi.next_to, over, UP, SMALL_BUFF,
            FadeOut(dot_two),
            FadeOut(two_sine_f_pi[0]),
            sine_f_pi.shift, SMALL_BUFF * LEFT,
        )
        self.wait()

        # Reciprocate
        pairs = VGroup()
        for num, denom in zip(fractions[1::4], fractions[3::4]):
            pairs.add(VGroup(num, denom))
        for limit in limits:
            pairs.add(VGroup(limit[0], limit[2:]))
        pairs.add(
            VGroup(f_pi, sine_f_pi),
            VGroup(product[1], product[3:]),
        )

        for pair in pairs:
            pair.generate_target()
            pair.target[0].move_to(pair[1], UP)
            pair.target[1].move_to(pair[0], DOWN)

        self.play(
            LaggedStartMap(
                MoveToTarget, pairs,
                path_arc=180 * DEGREES,
                run_time=3,
            ),
            product_lines[1].scale, 0.9, {"about_point": product_lines[1].get_start()},
            product_lines[1].shift, SMALL_BUFF * UP
        )
        self.wait()

        # Rearrange
        one_minus_f_over_k = TexMobject(
            "\\left(", "1", "-", "{f", "\\over", "k}", "\\right)"
        )
        # 0 1 2 3 4
        # k / k - f
        one_minus_f_over_k.set_color_by_tex("{f", GREEN)
        one_minus_f_over_k.next_to(product[0], RIGHT, buff=SMALL_BUFF)
        one_minus_f_over_k.shift(SMALL_BUFF * UP)

        self.play(
            FadeIn(one_minus_f_over_k[0]),
            FadeIn(one_minus_f_over_k[-1]),
            FadeOut(product_lines),
            *[
                ReplacementTransform(product[i], one_minus_f_over_k[j])
                for i, j in [(3, 1), (4, 2), (5, 3), (2, 4), (1, 5)]
            ]
        )
        self.wait()

        product = VGroup(product[0], *one_minus_f_over_k)
        product.generate_target()
        f_pi.generate_target()
        f_pi.target.next_to(equals, RIGHT, SMALL_BUFF)
        product.target.next_to(f_pi.target, RIGHT, SMALL_BUFF)
        product.target.shift(SMALL_BUFF * DOWN)

        self.play(
            sine_f_pi.next_to, equals, LEFT, SMALL_BUFF,
            FadeOut(over),
            MoveToTarget(f_pi),
            MoveToTarget(product),
        )
        self.wait()

        # Show final result
        rect = SurroundingRectangle(VGroup(sine_f_pi, product))
        rect.set_color(BLUE)
        pi_creatures = VGroup(
            PiCreature(color=BLUE_E),
            PiCreature(color=BLUE_C),
            PiCreature(color=BLUE_D),
            Mortimer()
        )
        pi_creatures.arrange(RIGHT, LARGE_BUFF)
        pi_creatures.set_height(1)
        pi_creatures.next_to(rect, DOWN)
        for pi in pi_creatures:
            pi.change("hooray", rect)
            pi.save_state()
            pi.change("plain")
            pi.fade(1)

        self.play(
            ShowCreation(rect),
            LaggedStartMap(ApplyMethod, pi_creatures, lambda m: (m.restore,))
        )
        for x in range(4):
            self.play(Blink(random.choice(pi_creatures)))
            self.wait()


class Conclusion(TeacherStudentsScene):
    CONFIG = {
        "camera_config": {"background_opacity": 1},
    }

    def construct(self):
        wallis_product = get_wallis_product(6)
        wallis_product.move_to(self.hold_up_spot, DOWN)
        wallis_product.shift_onto_screen()
        for i in range(0, len(wallis_product) - 5, 4):
            color = GREEN if (i / 4) % 2 == 0 else BLUE
            wallis_product[i:i + 3].set_color(color)

        sine_formula = TexMobject(
            "\\sin(", "f", "\\pi", ")", "=", "f", "\\pi",
            "\\prod_{k \\ne 0}", "\\left(", "1", "-",
            "{f", "\\over", "k}", "\\right)"
        )
        sine_formula.set_color_by_tex("f", GREEN)
        sine_formula.set_color_by_tex("left", WHITE)
        sine_formula.move_to(self.hold_up_spot, DOWN)
        sine_formula.shift_onto_screen()

        euler = ImageMobject("Euler")
        euler.set_height(2.5)
        basel_problem = TexMobject(
            "\\frac{1}{1^2} + ",
            "\\frac{1}{2^2} + ",
            "\\frac{1}{3^2} + ",
            "\\cdots",
            "\\frac{\\pi^2}{6}"
        )
        basel_problem.move_to(self.hold_up_spot, DOWN)
        implication = TexMobject("\\Rightarrow")
        implication.next_to(basel_problem[0][1], LEFT)

        self.play(
            self.teacher.change, "raise_right_hand",
            FadeInFromDown(wallis_product),
            self.get_student_changes("thinking", "hooray", "surprised")
        )
        self.wait()
        self.play(
            self.teacher.change, "hooray",
            FadeInFromDown(sine_formula),
            wallis_product.to_edge, UP,
            self.get_student_changes("pondering", "thinking", "erm")
        )
        self.wait(3)
        self.play(
            sine_formula.next_to, implication, LEFT,
            {"submobject_to_align": sine_formula[-1]},
            FadeIn(implication),
        )
        euler.next_to(sine_formula, UP)
        self.play(
            FadeIn(euler),
            LaggedStartMap(FadeIn, basel_problem),
            self.teacher.change, "happy",
            self.get_student_changes("sassy", "confused", "hesitant")
        )
        self.wait(2)

        wallis_rect = SurroundingRectangle(wallis_product)
        wallis_rect.set_color(BLUE)
        basel_rect = SurroundingRectangle(basel_problem)
        basel_rect.set_color(YELLOW)

        self.play(
            ShowCreation(wallis_rect),
            FadeOut(implication),
            FadeOut(euler),
            FadeOut(sine_formula),
        )
        self.play(
            ShowCreation(basel_rect),
            self.teacher.change, "surprised",
            self.get_student_changes(*["happy"] * 3)
        )
        self.wait(5)


class ByLine(Scene):
    def construct(self):
        self.add(TextMobject("""
            Written and animated by \\\\
            \\quad \\\\
            Sridhar Ramesh \\\\
            Grant Sanderson
        """).shift(2 * UP))


class SponsorUnderlay(PiCreatureScene):
    CONFIG = {
        "default_pi_creature_start_corner": DR,
    }

    def construct(self):
        url = TextMobject("https://udacity.com/3b1b/")
        url.to_corner(UL)

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

        url.next_to(self.pi_creature.get_corner(UL), UP)

        logo = SVGMobject(file_name="udacity")
        logo.set_fill("#02b3e4")
        logo.to_corner(UR)

        self.add(logo)

        self.play(
            Write(url),
            self.pi_creature.change, "raise_right_hand"
        )
        self.play(
            url.restore,
            ShowCreation(rect),
            path_arc=90 * DEGREES,
        )
        self.wait(2)
        self.change_mode("thinking")
        self.wait()
        self.look_at(url)
        self.wait(10)
        self.change_mode("happy")
        self.wait(10)
        self.change_mode("raise_right_hand")
        self.wait(10)

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


class EndScreen(PatreonEndScreen):
    CONFIG = {
        "specific_patrons": [
            "Juan Benet",
            "Keith Smith",
            "Chloe Zhou",
            "Ross Garber",
            "Desmos",
            "Burt Humburg",
            "CrypticSwarm",
            "Hoang Tung Lam",
            "Sergei",
            "Devin Scott",
            "George John",
            "Akash Kumar",
            "Felix Tripier",
            "Arthur Zey",
            "David Kedmey",
            "Ali Yahya",
            "Mayank M. Mehrotra",
            "Lukas Biewald",
            "Yana Chernobilsky",
            "Kaustuv DeBiswas",
            "Yu Jun",
            "dave nicponski",
            "Damion Kistler",
            "Jordan Scales",
            "Markus Persson",
            "Fred Ehrsam",
            "Britt Selvitelle",
            "Jonathan Wilson",
            "Ryan Atallah",
            "Joseph John Cox",
            "Luc Ritchie",
            "Cooper Jones",
            "James Hughes",
            "John V Wertheim",
            "Chris Giddings",
            "Song Gao",
            "William Fritzon",
            "Alexander Feldman",
            # "孟子易",
            "Mengzi yi",
            "zheng zhang",
            "Matt Langford",
            "Max Mitchell",
            "Richard Burgmann",
            "John Griffith",
            "Chris Connett",
            "Steven Tomlinson",
            "Jameel Syed",
            "Bong Choung",
            "Ignacio Freiberg",
            "Zhilong Yang",
            "Dan Esposito (Guardion)",
            "Giovanni Filippi",
            "Eric Younge",
            "Prasant Jagannath",
            "Cody Brocious",
            "James H. Park",
            "Norton Wang",
            "Kevin Le",
            "Tianyu Ge",
            "David MacCumber",
            "Oliver Steele",
            "Yaw Etse",
            "Dave B",
            "Waleed Hamied",
            "George Chiesa",
            "supershabam",
            "Delton Ding",
            "Thomas Tarler",
            "Jonathan Eppele",
            "Isak Hietala",
            "1stViewMaths",
            "Jacob Magnuson",
            "Mark Govea",
            "Clark Gaebel",
            "Mathias Jansson",
            "David Clark",
            "Michael Gardner",
            "Mads Elvheim",
            "Awoo",
            "Dr  David G. Stork",
            "Ted Suzman",
            "Linh Tran",
            "Andrew Busey",
            "John Haley",
            "Ankalagon",
            "Eric Lavault",
            "Boris Veselinovich",
            "Julian Pulgarin",
            "Jeff Linse",
            "Ryan Dahl",
            "Robert Teed",
            "Jason Hise",
            "Meshal Alshammari",
            "Bernd Sing",
            "James Thornton",
            "Mustafa Mahdi",
            "Mathew Bramson",
            "Jerry Ling",
            # "世珉 匡",
            "Shi min kuang",
            "Rish Kundalia",
            "Achille Brighton",
            "Ripta Pasay",
        ]
    }


class Thumbnail(DistanceProductScene):
    def construct(self):
        product = get_wallis_product()
        product.scale(1.5)
        product.move_to(2.5 * UP)
        frac_lines = product.get_parts_by_tex("\\over")
        frac_indices = list(map(product.index_of_part, frac_lines))
        parts = VGroup(*[
            product[i-1:i+2]
            for i in frac_indices
        ])
        parts[::2].set_color(WHITE)
        parts[1::2].set_color(WHITE)
        parts[-1].set_color(WHITE)
        parts[-1].set_background_stroke(color=YELLOW, width=3)
        parts[-1].scale(1.5, about_edge=LEFT)
        product[-4:].next_to(product[:-4], DOWN, MED_LARGE_BUFF)
        product.scale(1.2).center().to_edge(UP)

        # new_proof = TextMobject("New proof")
        # new_proof.scale(2.5)
        # new_proof.set_color(YELLOW)
        # new_proof.set_stroke(RED, 0.75)
        # new_proof.next_to(product, DOWN, MED_LARGE_BUFF)

        circle = self.circle = Circle(radius=8, color=YELLOW)
        circle.move_to(3 * DOWN, DOWN)
        bottom_dot = Dot(color=BLUE)
        bottom_dot.move_to(circle.get_bottom())
        observer = PiCreature(mode="pondering")
        observer.set_height(0.5)
        observer.next_to(bottom_dot, DOWN)

        lights = VGroup()
        lines = VGroup()
        light_config = dict(HIGHT_QUALITY_AMBIENT_LIGHT_CONFIG)
        # light_config = dict(CHEAP_AMBIENT_LIGHT_CONFIG)
        light_config["max_opacity"] = 1
        step = 0.03
        for frac in np.arange(step, 0.2, step):
            for u in -1, 1:
                light = AmbientLight(**light_config)
                dot = Dot(color=BLUE)
                dot.move_to(light)
                light.add_to_back(dot)
                light.move_to(self.get_circle_point_at_proportion(u * frac - 0.25))
                lights.add(light)
                line = DashedLine(dot.get_center(), circle.get_bottom())
                lines.add(line)


        self.add(circle)
        self.add(lights)
        self.add(product)
        # self.add(new_proof)
        self.add(bottom_dot, observer)









































