From afbdb94bb37069d658a57a2df6b3cbaaac55c0a1 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 28 Aug 2019 14:23:00 -0700 Subject: [PATCH 01/21] Set basic functionality to show the hyperdarts game --- active_projects/hyperdarts.py | 355 ++++++++++++++++++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 active_projects/hyperdarts.py diff --git a/active_projects/hyperdarts.py b/active_projects/hyperdarts.py new file mode 100644 index 00000000..798e70b9 --- /dev/null +++ b/active_projects/hyperdarts.py @@ -0,0 +1,355 @@ +from manimlib.imports import * + + +OUTPUT_DIRECTORY = "hyperdarts" + + +class HyperdartScene(Scene): + CONFIG = { + "square_width": 6, + "square_style": { + "stroke_width": 2, + "fill_color": BLUE, + "fill_opacity": 0.5, + }, + "circle_style": { + "fill_color": RED_E, + "fill_opacity": 1, + "stroke_width": 0, + }, + "default_line_style": { + "stroke_width": 2, + "stroke_color": WHITE, + }, + "default_dot_config": { + "fill_color": WHITE, + "background_stroke_width": 1, + "background_stroke_color": BLACK, + }, + } + + def setup(self): + self.square = self.get_square() + self.circle = self.get_circle() + self.circle_center_dot = self.get_circle_center_dot() + + self.add(self.square) + self.add(self.circle) + self.add(self.circle_center_dot) + + def get_square(self): + return Square( + side_length=self.square_width, + **self.square_style + ) + + def get_circle(self, square=None): + square = square or self.square + circle = Circle(**self.circle_style) + circle.replace(square) + return circle + + def get_circle_center_dot(self, circle=None): + circle = circle or self.circle + dot = Dot(circle.get_center()) + dot.set_width(0.02) + dot.set_color(GREY) + return dot + + def get_number_plane(self): + square = self.square + unit_size = square.get_width() / 2 + plane = NumberPlane( + axis_config={ + "unit_size": unit_size, + } + ) + plane.add_coordinates() + plane.shift(square.get_center() - plane.c2p(0, 0)) + return plane + + def get_random_points(self, n): + square = self.square + points = np.random.uniform(-1, 1, 3 * n).reshape((n, 3)) + + points[:, 0] *= square.get_width() / 2 + points[:, 1] *= square.get_height() / 2 + points[:, 2] = 0 + points += square.get_center() + return points + + def get_random_point(self): + return self.get_random_points(1)[0] + + def get_dot(self, point): + return Dot(point, **self.default_dot_config) + + # Hit transform rules + def is_inside(self, point, circle=None): + circle = circle or self.circle + return get_norm(point - circle.get_center()) <= circle.get_width() / 2 + + def get_new_radius(self, point, circle=None): + circle = circle or self.circle + center = circle.get_center() + radius = circle.get_width() / 2 + p_dist = get_norm(point - center) + return np.sqrt(radius**2 - p_dist**2) + + def get_hit_distance_line(self, point, circle=None): + circle = circle or self.circle + + line = Line( + circle.get_center(), point, + **self.default_line_style + ) + return line + + def get_chord(self, point, circle=None): + circle = circle or self.circle + + center = circle.get_center() + p_angle = angle_of_vector(point - center) + chord = Line(DOWN, UP) + new_radius = self.get_new_radius(point, circle) + chord.scale(new_radius) + chord.rotate(p_angle) + chord.move_to(point) + chord.set_style(**self.default_line_style) + return chord + + def get_radii_to_chord(self, chord, circle=None): + circle = circle or self.circle + + center = circle.get_center() + radii = VGroup(*[ + DashedLine(center, point) + for point in chord.get_start_and_end() + ]) + radii.set_style(**self.default_line_style) + return radii + + def get_all_hit_lines(self, point, circle=None): + h_line = self.get_hit_distance_line(point, circle) + chord = self.get_chord(point, circle) + radii = self.get_radii_to_chord(chord, circle) + return VGroup(h_line, chord, radii) + + # New circle + def get_new_circle_from_point(self, point, circle=None): + return self.get_new_circle( + self.get_new_radius(point, circle), + circle, + ) + + def get_new_circle_from_chord(self, chord, circle=None): + return self.get_new_circle( + chord.get_length() / 2, + circle, + ) + + def get_new_circle(self, new_radius, circle=None): + circle = circle or self.circle + new_circle = self.get_circle() + new_circle.set_width(2 * new_radius) + new_circle.move_to(circle) + return new_circle + + # Animations + + def show_hit(self, point, pace="slow", added_anims=None): + assert(pace in ["slow", "fast"]) + if added_anims is None: + added_anims = [] + + dot = self.get_dot(point) + if pace == "slow": + self.play( + FadeInFromLarge(dot, rate_func=rush_into), + *added_anims, + run_time=0.5, + ) + self.wait(0.5) + elif pace == "fast": + self.add(dot) + return dot + + def show_full_hit_process(self, point, pace="slow"): + assert(pace in ["slow", "fast"]) + + lines = self.get_all_hit_lines(point, self.circle) + h_line, chord, radii = lines + + dot = self.show_hit(point) + if pace == "slow": + self.play( + ShowCreation(h_line), + GrowFromCenter(chord), + ) + self.play(*map(ShowCreation, radii)) + elif pace == "fast": + self.add(dot) + self.play( + ShowCreation(h_line), + GrowFromCenter(chord), + *map(ShowCreation, radii), + run_time=0.5 + ) + + new_chord = self.show_circle_shrink(chord, pace=pace) + self.play( + FadeOut(lines), + FadeOut(new_chord), + FadeOut(dot), + run_time=(1 if pace == "slow" else 0.5) + ) + + def show_circle_shrink(self, chord, pace="slow"): + circle = self.circle + chord_copy = chord.copy() + new_circle = self.get_new_circle_from_chord(chord) + + outline = VGroup(*[ + VMobject().pointwise_become_partial(new_circle, a, b) + for (a, b) in [(0, 0.5), (0.5, 1)] + ]) + outline.rotate(chord.get_angle()) + outline.set_fill(opacity=0) + outline.set_stroke(YELLOW, 2) + + assert(pace in ["slow", "fast"]) + + if pace == "slow": + self.play( + chord_copy.move_to, circle.get_center(), + circle.set_opacity, 0.5, + ) + self.play( + Rotating( + chord_copy, + radians=PI, + ), + ShowCreation( + outline, + lag_ratio=0 + ), + run_time=1, + rate_func=smooth, + ) + self.play( + Transform(circle, new_circle), + FadeOut(outline), + ) + elif pace == "fast": + outline = new_circle.copy() + outline.set_fill(opacity=0) + outline.set_stroke(YELLOW, 2) + outline.move_to(chord) + outline.generate_target() + outline.target.move_to(circle) + self.play( + chord_copy.move_to, circle, + Transform(circle, new_circle), + # MoveToTarget( + # outline, + # remover=True + # ) + ) + # circle.become(new_circle) + # circle.become(new_circle) + # self.remove(new_circle) + + return chord_copy + + def show_miss(self, point): + square = self.square + miss = TextMobject("Miss!") + miss.next_to(point, DOWN) + + dot = self.show_hit( + point, + added_anims=[ + ApplyMethod( + square.set_color, YELLOW, + rate_func=there_and_back, + ), + GrowFromCenter(miss) + ] + ) + return VGroup(dot, miss) + + +# Problem statement + +class IntroduceGame(HyperdartScene): + CONFIG = { + "random_seed": 1, + "square_width": 5, + # "num_darts_in_initial_flurry": 1000, + "num_darts_in_initial_flurry": 100, + } + + def construct(self): + self.add_real_dartboard() + self.show_flurry_of_points() + self.show_board_dimensions() + self.introduce_bullseye() + self.show_shrink_rule() + + def add_real_dartboard(self): + pass + + def show_flurry_of_points(self): + title = TextMobject("Hyperdarts") + title.scale(1.5) + title.to_edge(UP) + + n = self.num_darts_in_initial_flurry + points = np.random.normal(size=n * 3).reshape((n, 3)) + points[:, 2] = 0 + points *= 0.75 + dots = VGroup(*[ + Dot(point, radius=0.01) + for point in points + ]) + + self.remove(self.circle) + self.play( + FadeInFromDown(title), + LaggedStartMap(FadeInFromLarge, dots) + ) + self.wait() + + self.flurry_dots = dots + + def show_board_dimensions(self): + square = self.square + + labels = VGroup(*[ + TextMobject("2ft").next_to( + square.get_edge_center(vect), -vect, SMALL_BUFF + ) + for vect in [UP, RIGHT] + ]) + labels.set_color(YELLOW) + + h_line, v_line = lines = VGroup(*[ + DashedLine( + square.get_edge_center(v1), + square.get_edge_center(-v1), + ).next_to(label, -v2, buff=SMALL_BUFF) + for label, v1, v2 in zip(labels, [LEFT, UP], [UP, RIGHT]) + ]) + lines.match_color(labels) + + self.play( + LaggedStartMap(ShowCreation, lines), + LaggedStartMap(FadeInFromDown, labels), + lag_ratio=0.5 + ) + + def introduce_bullseye(self): + pass + + def show_shrink_rule(self): + pass From f54b2ac81a76aac5914a3e1eb30e79e20afc6bfa Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 10 Sep 2019 13:26:30 -0700 Subject: [PATCH 02/21] Don't add sound while skipping animations --- manimlib/scene/scene.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index c9ad7187..014ea528 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -543,6 +543,8 @@ class Scene(Container): self.file_writer.write_frame(frame) def add_sound(self, sound_file, time_offset=0, gain=None, **kwargs): + if self.skip_animations: + return time = self.get_time() + time_offset self.file_writer.add_sound(sound_file, time, gain, **kwargs) From f34a110d5b430b613d6bcb079781641eca9e0c56 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 10 Sep 2019 13:26:40 -0700 Subject: [PATCH 03/21] More hyperdarts scenes --- active_projects/hyperdarts.py | 639 +++++++++++++++++++++++++++++++--- 1 file changed, 587 insertions(+), 52 deletions(-) diff --git a/active_projects/hyperdarts.py b/active_projects/hyperdarts.py index 798e70b9..4e69b1ce 100644 --- a/active_projects/hyperdarts.py +++ b/active_projects/hyperdarts.py @@ -17,6 +17,7 @@ class HyperdartScene(Scene): "fill_opacity": 1, "stroke_width": 0, }, + "circle_center_dot_radius": 0.025, "default_line_style": { "stroke_width": 2, "stroke_color": WHITE, @@ -25,7 +26,10 @@ class HyperdartScene(Scene): "fill_color": WHITE, "background_stroke_width": 1, "background_stroke_color": BLACK, + "radius": 0.5 * DEFAULT_DOT_RADIUS, }, + "dart_sound": "dart_low", + "default_bullseye_shadow_opacity": 0.35, } def setup(self): @@ -51,10 +55,11 @@ class HyperdartScene(Scene): def get_circle_center_dot(self, circle=None): circle = circle or self.circle - dot = Dot(circle.get_center()) - dot.set_width(0.02) - dot.set_color(GREY) - return dot + return Dot( + circle.get_center(), + radius=self.circle_center_dot_radius, + fill_color=BLACK, + ) def get_number_plane(self): square = self.square @@ -135,6 +140,18 @@ class HyperdartScene(Scene): radii = self.get_radii_to_chord(chord, circle) return VGroup(h_line, chord, radii) + def get_dart(self, length=1.5): + dart = SVGMobject(file_name="dart") + dart.rotate(135 * DEGREES) + dart.set_width(length) + dart.rotate(45 * DEGREES, UP) + dart.rotate(-10 * DEGREES) + + dart.set_fill(GREY) + dart.set_sheen(2, UL) + dart.set_stroke(BLACK, 0.5, background=True) + return dart + # New circle def get_new_circle_from_point(self, point, circle=None): return self.get_new_circle( @@ -155,7 +172,89 @@ class HyperdartScene(Scene): new_circle.move_to(circle) return new_circle + # Sound + def add_dart_sound(self, time_offset=0, gain=-20, **kwargs): + self.add_sound( + self.dart_sound, + time_offset=time_offset, + gain=-20, + **kwargs, + ) + # Animations + def show_full_hit_process(self, point, pace="slow", with_dart=True): + assert(pace in ["slow", "fast"]) + + to_fade = VGroup() + + if with_dart: + dart, dot = self.show_hit_with_dart(point) + to_fade.add(dart, dot) + else: + dot = self.show_hit(point) + to_fade.add(dot) + + if pace == "slow": + self.wait(0.5) + + # TODO, automatically act based on hit or miss? + + lines = self.show_geometry(point, pace) + chord_and_shadow = self.show_circle_shrink(lines[1], pace=pace) + + to_fade.add_to_back(chord_and_shadow, lines) + + self.play( + FadeOut(to_fade), + run_time=(1 if pace == "slow" else 0.5) + ) + + def show_hits_with_darts(self, points, run_time=0.5, added_anims=None): + if added_anims is None: + added_anims = [] + + darts = VGroup(*[ + self.get_dart().move_to(point, DR) + for point in points + ]) + dots = VGroup(*[ + self.get_dot(point) + for point in points + ]) + + for dart in darts: + dart.save_state() + dart.set_x(-(FRAME_WIDTH + dart.get_width()) / 2) + dart.rotate(20 * DEGREES) + + n_points = len(points) + self.play( + ShowIncreasingSubsets( + dots, + rate_func=squish_rate_func(linear, 0.5, 1), + ), + LaggedStart(*[ + Restore( + dart, + path_arc=-20 * DEGREES, + rate_func=linear, + run_time=run_time, + ) + for dart in darts + ], lag_ratio=(1 / n_points)), + *added_anims, + run_time=run_time + ) + for n in range(n_points): + self.add_dart_sound( + time_offset=(-n / (2 * n_points)) + ) + + return darts, dots + + def show_hit_with_dart(self, point, run_time=0.25, **kwargs): + darts, dots = self.show_hits_with_darts([point], run_time, **kwargs) + return darts[0], dots[0] def show_hit(self, point, pace="slow", added_anims=None): assert(pace in ["slow", "fast"]) @@ -169,18 +268,17 @@ class HyperdartScene(Scene): *added_anims, run_time=0.5, ) - self.wait(0.5) elif pace == "fast": self.add(dot) + # self.add_dart_sound() return dot - def show_full_hit_process(self, point, pace="slow"): + def show_geometry(self, point, pace="slow"): assert(pace in ["slow", "fast"]) lines = self.get_all_hit_lines(point, self.circle) h_line, chord, radii = lines - dot = self.show_hit(point) if pace == "slow": self.play( ShowCreation(h_line), @@ -188,26 +286,31 @@ class HyperdartScene(Scene): ) self.play(*map(ShowCreation, radii)) elif pace == "fast": - self.add(dot) self.play( ShowCreation(h_line), GrowFromCenter(chord), *map(ShowCreation, radii), run_time=0.5 ) + return lines - new_chord = self.show_circle_shrink(chord, pace=pace) - self.play( - FadeOut(lines), - FadeOut(new_chord), - FadeOut(dot), - run_time=(1 if pace == "slow" else 0.5) - ) - - def show_circle_shrink(self, chord, pace="slow"): + def show_circle_shrink(self, chord, pace="slow", shadow_opacity=None): circle = self.circle chord_copy = chord.copy() new_circle = self.get_new_circle_from_chord(chord) + to_fade = VGroup(chord_copy) + + if shadow_opacity is None: + shadow_opacity = self.default_bullseye_shadow_opacity + if shadow_opacity > 0: + shadow = circle.copy() + shadow.set_opacity(shadow_opacity) + to_fade.add_to_back(shadow) + if circle in self.mobjects: + index = self.mobjects.index(circle) + self.mobjects.insert(index, shadow) + else: + self.add(shadow, self.circle_center_dot) outline = VGroup(*[ VMobject().pointwise_become_partial(new_circle, a, b) @@ -258,48 +361,107 @@ class HyperdartScene(Scene): # circle.become(new_circle) # circle.become(new_circle) # self.remove(new_circle) + return to_fade - return chord_copy - - def show_miss(self, point): + def show_miss(self, point, with_dart=True): square = self.square miss = TextMobject("Miss!") - miss.next_to(point, DOWN) + miss.next_to(point, UP) + to_fade = VGroup(miss) - dot = self.show_hit( - point, - added_anims=[ - ApplyMethod( - square.set_color, YELLOW, - rate_func=there_and_back, - ), - GrowFromCenter(miss) - ] + if with_dart: + dart, dot = self.show_hit_with_dart(point) + to_fade.add(dart, dot) + else: + dot = self.show_hit(point) + to_fade.add(dot) + self.play( + ApplyMethod( + square.set_color, YELLOW, + rate_func=lambda t: (1 - t), + ), + GrowFromCenter(miss), + run_time=0.25 ) - return VGroup(dot, miss) + return to_fade + + def show_game_over(self): + game_over = TextMobject("GAME OVER") + game_over.set_width(FRAME_WIDTH - 1) + rect = FullScreenFadeRectangle(opacity=0.25) + + self.play( + FadeIn(rect), + FadeInFromLarge(game_over), + ) + return VGroup(rect, game_over) + + +class Dartboard(VGroup): + CONFIG = { + "radius": 3, + "n_sectors": 20, + } + + def __init__(self, **kwargs): + super().__init__(**kwargs) + n_sectors = self.n_sectors + angle = TAU / n_sectors + + segments = VGroup(*[ + VGroup(*[ + AnnularSector( + inner_radius=in_r, + outer_radius=out_r, + start_angle=n * angle, + angle=angle, + color=color, + ) + for n, color in zip( + range(n_sectors), + it.cycle(colors) + ) + ]) + for colors, in_r, out_r in [ + ([LIGHT_GREY, DARKER_GREY], 0, 1), + ([GREEN_E, RED_E], 0.5, 0.55), + ([GREEN_E, RED_E], 0.95, 1), + ] + ]) + segments.rotate(-angle / 2) + bullseyes = VGroup(*[ + Circle(radius=r) + for r in [0.07, 0.035] + ]) + bullseyes.set_fill(opacity=1) + bullseyes.set_stroke(width=0) + bullseyes[0].set_color(GREEN_E) + bullseyes[1].set_color(RED_E) + + self.add(*segments, *bullseyes) + self.scale(self.radius) # Problem statement class IntroduceGame(HyperdartScene): CONFIG = { - "random_seed": 1, + "random_seed": 0, "square_width": 5, - # "num_darts_in_initial_flurry": 1000, - "num_darts_in_initial_flurry": 100, + "num_darts_in_initial_flurry": 5, } def construct(self): - self.add_real_dartboard() self.show_flurry_of_points() self.show_board_dimensions() self.introduce_bullseye() + self.show_miss_example() self.show_shrink_rule() - def add_real_dartboard(self): - pass - def show_flurry_of_points(self): + square = self.square + circle = self.circle + title = TextMobject("Hyperdarts") title.scale(1.5) title.to_edge(UP) @@ -308,28 +470,54 @@ class IntroduceGame(HyperdartScene): points = np.random.normal(size=n * 3).reshape((n, 3)) points[:, 2] = 0 points *= 0.75 - dots = VGroup(*[ - Dot(point, radius=0.01) - for point in points - ]) - self.remove(self.circle) - self.play( - FadeInFromDown(title), - LaggedStartMap(FadeInFromLarge, dots) + board = Dartboard() + board.match_width(square) + board.move_to(square) + + pre_square = Circle(color=WHITE) + pre_square.replace(square) + + self.remove(circle, square) + self.add(board) + + darts, dots = self.show_hits_with_darts( + points, + added_anims=[FadeInFromDown(title)] ) self.wait() + def func(p): + theta = angle_of_vector(p) % (TAU / 4) + if theta > TAU / 8: + theta = TAU / 4 - theta + p *= 1 / np.cos(theta) + return p + + self.play( + *[ + ApplyPointwiseFunction(func, pieces, run_time=1) + for pieces in [*board[:3], *dots] + ], + *[ + MaintainPositionRelativeTo(dart, dot) + for dart, dot in zip(darts, dots) + ] + ) + self.flurry_dots = dots + self.darts = darts + self.title = title + self.board = board def show_board_dimensions(self): square = self.square labels = VGroup(*[ - TextMobject("2ft").next_to( - square.get_edge_center(vect), -vect, SMALL_BUFF + TextMobject("2 ft").next_to( + square.get_edge_center(vect), vect, ) - for vect in [UP, RIGHT] + for vect in [DOWN, RIGHT] ]) labels.set_color(YELLOW) @@ -337,8 +525,8 @@ class IntroduceGame(HyperdartScene): DashedLine( square.get_edge_center(v1), square.get_edge_center(-v1), - ).next_to(label, -v2, buff=SMALL_BUFF) - for label, v1, v2 in zip(labels, [LEFT, UP], [UP, RIGHT]) + ).next_to(label, v2) + for label, v1, v2 in zip(labels, [LEFT, UP], [UP, LEFT]) ]) lines.match_color(labels) @@ -347,9 +535,356 @@ class IntroduceGame(HyperdartScene): LaggedStartMap(FadeInFromDown, labels), lag_ratio=0.5 ) + self.wait() + + self.square_dimensions = VGroup(lines, labels) def introduce_bullseye(self): - pass + square = self.square + circle = self.circle + board = self.board + circle.save_state() + circle.replace(board[-1]) + + label = TextMobject("Bullseye") + label.scale(1.5) + label.next_to(square, LEFT, aligned_edge=UP) + label.set_color(RED) + arrow = Arrow( + label.get_bottom(), + circle.get_corner(DR) + ) + + radius = DashedLine( + square.get_center(), + square.get_left(), + stroke_width=2, + ) + radius_label = TextMobject("1 ft") + radius_label.next_to(radius, DOWN, SMALL_BUFF) + + self.add(circle, self.square_dimensions) + self.play( + FadeInFromLarge(circle), + FadeInFromDown(label), + ShowCreation(arrow), + LaggedStartMap(FadeOut, self.flurry_dots, run_time=1), + LaggedStartMap(FadeOut, self.darts, run_time=1), + ) + self.wait() + self.add(square, board, arrow, circle) + self.play( + Restore(circle), + ApplyMethod( + arrow.scale, 0.4, + {"about_point": arrow.get_start()} + ), + ) + self.add(radius, self.circle_center_dot) + self.play( + ShowCreation(radius), + FadeInFrom(radius_label, RIGHT), + FadeIn(self.circle_center_dot), + ) + self.play( + FadeOut(label), + Uncreate(arrow), + FadeOut(board) + ) + self.wait() + + s_lines, s_labels = self.square_dimensions + self.play( + FadeOut(s_lines), + FadeOut(radius), + FadeOut(radius_label), + FadeOut(self.title), + ) + + self.circle_dimensions = VGroup( + radius, radius_label, + ) + + def show_miss_example(self): + square = self.square + point = square.get_corner(UL) + 0.5 * DR + + miss_word = TextMobject("Miss!") + miss_word.scale(1.5) + miss_word.next_to(point, UP, LARGE_BUFF) + + dart, dot = self.show_hit_with_dart(point) + self.play(FadeInFromDown(miss_word)) + self.wait() + game_over = self.show_game_over() + self.wait() + self.play( + *map(FadeOut, [dart, dot, miss_word, game_over]) + ) def show_shrink_rule(self): + circle = self.circle + point = 0.5 * circle.point_from_proportion(0.2) + + # First example + self.show_full_hit_process(point) + self.wait() + + # Close to border + label = TextMobject("Bad shot $\\Rightarrow$ much shrinkage") + label.scale(1.5) + label.to_edge(UP) + + point = 0.98 * circle.point_from_proportion(3 / 8) + circle.save_state() + self.play(FadeInFromDown(label)) + self.show_full_hit_process(point) + self.wait() + self.play(Restore(circle)) + + # Close to center + new_label = TextMobject("Good shot $\\Rightarrow$ less shrinkage") + new_label.scale(1.5) + new_label.to_edge(UP) + point = 0.2 * circle.point_from_proportion(3 / 8) + self.play( + FadeInFromDown(new_label), + FadeOutAndShift(label, UP), + ) + self.show_full_hit_process(point) + self.wait() + self.play(FadeOut(new_label)) + + # Play on + for x in range(3): + r1, r2 = np.random.random(size=2) + point = r1 * circle.point_from_proportion(r2) + self.show_full_hit_process(point) + point = circle.get_right() + 0.5 * UR + self.show_miss(point) + self.wait() + self.show_game_over() + + +class ShowScoring(HyperdartScene): + def setup(self): + super().setup() + self.add_score_counter() + + def construct(self): + self.comment_on_score() + self.show_several_hits() + + def comment_on_score(self): + score_label = self.score_label + comment = TextMobject("\\# Bullseyes") + # rect = SurroundingRectangle(comment) + # rect.set_stroke(width=1) + # comment.add(rect) + comment.set_color(YELLOW) + comment.next_to(score_label, DOWN, LARGE_BUFF) + comment.set_x(midpoint( + self.square.get_left(), + LEFT_SIDE, + )[0]) + arrow = Arrow( + comment.get_top(), + score_label[1].get_bottom(), + buff=0.2, + ) + arrow.match_color(comment) + + self.play( + FadeInFromDown(comment), + GrowArrow(arrow), + ) + + def show_several_hits(self): + points = [UR, DL, 0.5 * UL, 0.5 * DR] + for point in points: + self.show_full_hit_process(point, pace="fast") + self.show_miss(2 * UR) + self.wait() + + # + def add_score_counter(self): + score = Integer(0) + score_label = VGroup( + TextMobject("Score: "), + score + ) + score_label.arrange(RIGHT, aligned_edge=DOWN) + score_label.scale(1.5) + score_label.to_corner(UL) + + self.add(score_label) + + self.score = score + self.score_label = score_label + + def increment_score(self): + score = self.score + new_score = score.copy() + new_score.increment_value(1) + self.play( + FadeOutAndShift(score, UP), + FadeInFrom(new_score, DOWN), + run_time=1, + ) + self.remove(new_score) + score.increment_value() + score.move_to(new_score) + self.add(score) + + def show_hit(self, point, *args, **kwargs): + result = super().show_hit(point, *args, **kwargs) + if self.is_inside(point): + self.increment_score() + return result + + def show_hit_with_dart(self, point, *args, **kwargs): + result = super().show_hit_with_dart(point, *args, **kwargs) + if self.is_inside(point): + self.increment_score() + return result + + +class ShowSeveralRounds(ShowScoring): + CONFIG = { + "n_rounds": 5, + } + + def construct(self): + for x in range(self.n_rounds): + self.show_single_round() + self.reset_board() + + def show_single_round(self, pace="fast"): + while True: + point = self.get_random_point() + if self.is_inside(point): + self.show_full_hit_process(point, pace=pace) + else: + to_fade = self.show_miss(point) + self.wait(0.5) + self.play( + ShowCreationThenFadeAround(self.score_label), + FadeOut(to_fade) + ) + return + + def reset_board(self): + score = self.score + new_score = score.copy() + new_score.set_value(0) + self.play( + self.circle.match_width, self.square, + FadeOutAndShift(score, UP), + FadeInFrom(new_score, DOWN), + ) + score.set_value(0) + self.add(score) + self.remove(new_score) + + +class ShowSeveralRoundsQuickly(ShowSeveralRounds): + CONFIG = { + "n_rounds": 15, + } + + def show_full_hit_process(self, point, *args, **kwargs): + lines = self.get_all_hit_lines(point) + + dart = self.show_hit_with_dart(point) + self.add(lines) + self.score.increment_value(1) + to_fade = self.show_circle_shrink(lines[1], pace="fast") + to_fade.add(*lines, *dart) + self.play(FadeOut(to_fade), run_time=0.5) + + def increment_score(self): + pass # Handled elsewhere + + +class ShowUniformDistribution(HyperdartScene): + CONFIG = { + "dart_sound": "dart_high", + "n_points": 1000, + } + + def construct(self): + self.add_title() + self.show_random_points() + self.exchange_titles() + self.show_random_points() + + def get_square(self): + return super().get_square().to_edge(DOWN) + + def add_title(self): + # square = self.square + title = TextMobject("All points in the square are equally likely") + title.scale(1.5) + title.to_edge(UP) + + new_title = TextMobject("``Uniform distribution'' on the square") + new_title.scale(1.5) + new_title.to_edge(UP) + + self.play(FadeInFromDown(title)) + + self.title = title + self.new_title = new_title + + def show_random_points(self): + points = self.get_random_points(self.n_points) + dots = VGroup(*[ + Dot(point, radius=0.02) + for point in points + ]) + dots.set_fill(opacity=0.75) + + run_time = 5 + self.play(LaggedStartMap( + FadeInFromLarge, dots, + run_time=run_time, + )) + for x in range(1000): + self.add_dart_sound( + time_offset=-run_time * np.random.random(), + gain=-10, + gain_to_background=-5, + ) + self.wait() + + def exchange_titles(self): + self.play( + FadeInFromDown(self.new_title), + FadeOutAndShift(self.title, UP), + ) + + +class ExpectedScoreEqualsQMark(Scene): + def construct(self): + equation = TextMobject( + "\\textbf{E}[Score] = ???", + tex_to_color_map={ + "???": YELLOW, + } + ) + aka = TextMobject("a.k.a. Long-term average") + aka.next_to(equation, DOWN) + + self.play(Write(equation)) + self.wait(2) + self.play(FadeInFrom(aka, UP)) + self.wait() + + +class ScoreHistogram(Scene): + def construct(self): + pass + + # + def get_random_score(self): pass From 85e5b20ede3fc6cc66bba8d3dd6a0fbc15b76419 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 10 Sep 2019 13:27:18 -0700 Subject: [PATCH 04/21] Small tweak to patron scroll --- manimlib/for_3b1b_videos/common_scenes.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/manimlib/for_3b1b_videos/common_scenes.py b/manimlib/for_3b1b_videos/common_scenes.py index 6c417a98..88996b4b 100644 --- a/manimlib/for_3b1b_videos/common_scenes.py +++ b/manimlib/for_3b1b_videos/common_scenes.py @@ -259,11 +259,15 @@ class PatreonEndScreen(PatreonThanks, PiCreatureScene): self.wait(wait_time) def modify_patron_name(self, name): - if name.lower() == "RedAgent14".lower(): - return "Brian Shepetofsky" - elif name.lower() == "DeathByShrimp".lower(): - return "Henry Bresnahan" - + modification_map = { + "RedAgent14": "Brian Shepetofsky", + "DeathByShrimp": "Henry Bresnahan", + "akostrikov": "Aleksandr Kostrikov", + "Jacob Baxter": "Will Fleshman", + } + for n1, n2 in modification_map.items(): + if name.lower() == n1.lower(): + return n2 return name From 77797741a5efca6b4384934b74b09e3c9dc1a3e1 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 18 Sep 2019 15:37:46 -0700 Subject: [PATCH 05/21] Small updates to hyperdarts scenes --- active_projects/hyperdarts.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/active_projects/hyperdarts.py b/active_projects/hyperdarts.py index 4e69b1ce..e0d972a5 100644 --- a/active_projects/hyperdarts.py +++ b/active_projects/hyperdarts.py @@ -806,6 +806,11 @@ class ShowSeveralRoundsQuickly(ShowSeveralRounds): pass # Handled elsewhere +class ShowSeveralRoundsVeryQuickly(ShowSeveralRoundsQuickly): + def construct(self): + pass + + class ShowUniformDistribution(HyperdartScene): CONFIG = { "dart_sound": "dart_high", @@ -882,9 +887,34 @@ class ExpectedScoreEqualsQMark(Scene): class ScoreHistogram(Scene): + CONFIG = { + "axes_config": { + "" + } + } + def construct(self): + self.add_axes() + self.add_score_label() + self.setup_histogram() + self.show_many_runs() + + def add_axes(self): + axes = Axes(**self.axes_config) + + def add_score_label(self): pass + def setup_histogram(self): + pass + + def show_many_runs(self): + pass + + # + def add_one_run(self, animate=True): + pass + def get_random_score(self): pass From 0f037651f3f53d927a1fefe0762d57130f416bc3 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 18 Sep 2019 15:38:02 -0700 Subject: [PATCH 06/21] Small modifications to some old name animations --- old_projects/name_animation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/old_projects/name_animation.py b/old_projects/name_animation.py index 8315cc6d..7114cabe 100644 --- a/old_projects/name_animation.py +++ b/old_projects/name_animation.py @@ -88,7 +88,9 @@ class NameAnimationScene(Scene): return self.animated_name.replace(" ", "") + "Animation" -names = [] +names = [ + "Happy 18th Birthday\\\\Pranavi Hiremath!" +] if __name__ == "__main__": for name in names: From ffecd03cb2ce9899ed303fd57ad3c284cf8cb443 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 18 Sep 2019 15:38:27 -0700 Subject: [PATCH 07/21] Begining of spirals video, which is my newfound distraction --- active_projects/spirals.py | 528 +++++++++++++++++++++++++++++++++++++ 1 file changed, 528 insertions(+) create mode 100644 active_projects/spirals.py diff --git a/active_projects/spirals.py b/active_projects/spirals.py new file mode 100644 index 00000000..3808a921 --- /dev/null +++ b/active_projects/spirals.py @@ -0,0 +1,528 @@ +from manimlib.imports import * + + +OUTPUT_DIRECTORY = "spirals" + + +def generate_prime_list(*args): + if len(args) == 1: + start, stop = 2, args[0] + elif len(args) == 2: + start, stop = args + start = max(start, 2) + else: + raise TypeError("generate_prime_list takes 1 or 2 arguments") + + result = [] + for n in range(start, stop): + include = True + for k in range(2, int(np.sqrt(n)) + 1): + if n % k == 0: + include = False + break + if include: + result.append(n) + return result + + +class SpiralScene(Scene): + CONFIG = { + "axes_config": { + "number_line_config": { + "stroke_width": 1.5, + } + } + } + + def setup(self): + self.axes = Axes(**self.axes_config) + self.add(self.axes) + + def get_dots_in_spiral(self, sequence, axes=None, dot_radius=None): + if axes is None: + axes = self.axes + if dot_radius is None: + unit = get_norm(axes.c2p(1, 0) - axes.c2p(0, 0)), + dot_radius = max( + 0.1 / (-np.log10(unit) + 1), + 0.01, + ) + + return VGroup(*[ + Dot( + self.get_polar_point(n, n, axes), + radius=dot_radius, + fill_color=TEAL, + ) + for n in sequence + ]) + + def get_polar_point(self, r, theta, axes): + return axes.c2p(r * np.cos(theta), r * np.sin(theta)) + + def set_scale(self, scale, exceptions=None, run_time=3): + axes = self.axes + if exceptions is None: + exceptions = [] + + unit = get_norm(axes.c2p(1, 0) - axes.c2p(0, 0)) + sf = unit / scale + + self.remove(*exceptions) + self.play(*[ + ApplyMethod( + mob.scale, sf, + about_point=ORIGIN, + run_time=run_time, + ) + for mob in self.get_top_level_mobjects() + ]) + + +# Scenes + + +class RefresherOnPolarCoordinates(MovingCameraScene): + CONFIG = { + "x_color": GREEN, + "y_color": RED, + "r_color": YELLOW, + "theta_color": LIGHT_PINK, + } + + def construct(self): + self.show_xy_coordinates() + self.transition_to_polar_grid() + self.show_polar_coordinates() + + self.show_all_nn_tuples() + + def show_xy_coordinates(self): + plane = NumberPlane() + plane.add_coordinates() + + x, y = 3, 2 + point = plane.c2p(x, y) + xp = plane.c2p(x, 0) + origin = plane.c2p(0, 0) + + x_color = self.x_color + y_color = self.y_color + + x_line = Line(origin, xp, color=x_color) + y_line = Line(xp, point, color=y_color) + + dot = Dot(point) + + coord_label = self.get_coord_label(x, y, x_color, y_color) + x_coord = coord_label.x_coord + y_coord = coord_label.y_coord + + coord_label.next_to(dot, UR, SMALL_BUFF) + + x_brace = Brace(x_coord, UP) + y_brace = Brace(y_coord, UP) + x_brace.add(x_brace.get_tex("x").set_color(x_color)) + y_brace.add(y_brace.get_tex("y").set_color(y_color)) + x_brace.add_updater(lambda m: m.next_to(x_coord, UP, SMALL_BUFF)) + y_brace.add_updater(lambda m: m.next_to(y_coord, UP, SMALL_BUFF)) + + self.add(plane) + self.add(dot, coord_label) + self.add(x_brace, y_brace) + + coord_label.add_updater( + lambda m: m.next_to(dot, UR, SMALL_BUFF) + ) + + self.play( + ShowCreation(x_line), + ChangeDecimalToValue(x_coord, x), + UpdateFromFunc( + dot, + lambda d: d.move_to(x_line.get_end()), + ), + run_time=2, + ) + self.play( + ShowCreation(y_line), + ChangeDecimalToValue(y_coord, y), + UpdateFromFunc( + dot, + lambda d: d.move_to(y_line.get_end()), + ), + run_time=2, + ) + self.wait() + + self.xy_coord_mobjects = VGroup( + x_line, y_line, coord_label, + x_brace, y_brace, + ) + self.plane = plane + self.dot = dot + + def transition_to_polar_grid(self): + self.polar_grid = self.get_polar_grid() + self.add(self.polar_grid, self.dot) + self.play( + FadeOut(self.xy_coord_mobjects), + FadeOut(self.plane), + ShowCreation(self.polar_grid, run_time=2), + ) + self.wait() + + def show_polar_coordinates(self): + dot = self.dot + plane = self.plane + origin = plane.c2p(0, 0) + + r_color = self.r_color + theta_color = self.theta_color + + r_line = Line(origin, dot.get_center()) + r_line.set_color(r_color) + r_value = r_line.get_length() + theta_value = r_line.get_angle() + + coord_label = self.get_coord_label(r_value, theta_value, r_color, theta_color) + r_coord = coord_label.x_coord + theta_coord = coord_label.y_coord + + coord_label.add_updater(lambda m: m.next_to(dot, UP, buff=SMALL_BUFF)) + r_coord.add_updater(lambda d: d.set_value( + get_norm(dot.get_center()) + )) + theta_coord.add_background_rectangle() + theta_coord.add_updater(lambda d: d.set_value( + (angle_of_vector(dot.get_center()) % TAU) + )) + coord_label[-1].add_updater( + lambda m: m.next_to(theta_coord, RIGHT, SMALL_BUFF) + ) + + non_coord_parts = VGroup(*[ + part + for part in coord_label + if part not in [r_coord, theta_coord] + ]) + + r_label = TexMobject("r") + r_label.set_color(r_color) + r_label.add_updater(lambda m: m.next_to(r_coord, UP)) + theta_label = TexMobject("\\theta") + theta_label.set_color(theta_color) + theta_label.add_updater(lambda m: m.next_to(theta_coord, UP)) + + r_coord_copy = r_coord.copy() + r_coord_copy.add_updater( + lambda m: m.next_to(r_line.get_center(), UL, buff=0) + ) + + degree_label = DecimalNumber(0, num_decimal_places=1, unit="^\\circ") + arc = Arc(radius=1, angle=theta_value) + arc.set_color(theta_color) + degree_label.set_color(theta_color) + + # Show r + self.play( + ShowCreation(r_line, run_time=2), + ChangeDecimalToValue(r_coord_copy, r_value, run_time=2), + VFadeIn(r_coord_copy, run_time=0.5), + ) + r_coord.set_value(r_value) + self.add(non_coord_parts, r_coord_copy) + self.play( + FadeIn(non_coord_parts), + ReplacementTransform(r_coord_copy, r_coord), + FadeInFromDown(r_label), + ) + self.wait() + + # Show theta + degree_label.next_to(arc.get_start(), UR, SMALL_BUFF) + line = r_line.copy() + line.rotate(-theta_value, about_point=ORIGIN) + line.set_color(theta_color) + self.play( + ShowCreation(arc), + Rotate(line, theta_value, about_point=ORIGIN), + VFadeInThenOut(line), + ChangeDecimalToValue(degree_label, theta_value / DEGREES), + ) + self.play( + degree_label.move_to, theta_coord, + FadeInFromDown(theta_label), + ) + self.wait() + + degree_cross = Cross(degree_label) + radians_word = TextMobject("in radians") + radians_word.scale(0.9) + radians_word.set_color(theta_color) + radians_word.add_background_rectangle() + radians_word.add_updater( + lambda m: m.next_to(theta_label, RIGHT, aligned_edge=DOWN) + ) + + self.play(ShowCreation(degree_cross)) + self.play( + FadeOutAndShift( + VGroup(degree_label, degree_cross), + DOWN + ), + FadeIn(theta_coord) + ) + self.play(FadeIn(radians_word)) + self.wait() + + # Move point around + r_line.add_updater( + lambda l: l.put_start_and_end_on(ORIGIN, dot.get_center()) + ) + arc.add_updater( + lambda m: m.become(Arc( + angle=(r_line.get_angle() % TAU), + color=theta_color, + radius=1, + )) + ) + + self.add(coord_label) + for angle in [PI - theta_value, PI - 0.001, -TAU + 0.002]: + self.play( + Rotate(dot, angle, about_point=ORIGIN), + run_time=3, + ) + self.wait() + self.play( + FadeOut(coord_label), + FadeOut(r_line), + FadeOut(arc), + FadeOut(r_label), + FadeOut(theta_label), + FadeOut(radians_word), + FadeOut(dot), + ) + + def show_all_nn_tuples(self): + self.remove(self.dot) + primes = generate_prime_list(20) + non_primes = list(range(1, 20)) + for prime in primes: + non_primes.remove(prime) + + pp_points = VGroup(*map(self.get_nn_point, primes)) + pp_points[0][1].shift(0.3 * LEFT + SMALL_BUFF * UP) + np_points = VGroup(*map(self.get_nn_point, non_primes)) + pp_points.set_color(TEAL) + np_points.set_color(WHITE) + pp_points.set_stroke(BLACK, 4, background=True) + np_points.set_stroke(BLACK, 4, background=True) + + frame = self.camera_frame + self.play( + ApplyMethod(frame.scale, 2), + LaggedStartMap( + FadeInFromDown, pp_points + ), + run_time=2 + ) + self.wait() + self.play(LaggedStartMap(FadeIn, np_points)) + self.play(frame.scale, 0.5) + self.wait() + + # Talk about 1 + one = np_points[0] + r_line = Line(ORIGIN, one.dot.get_center()) + r_line.set_color(self.r_color) + # pre_arc = Line(RIGHT, UR, color=self.r_color) + theta_tracker = ValueTracker(1) + arc = always_redraw(lambda: self.get_arc(theta_tracker.get_value())) + + one_rect = SurroundingRectangle(one) + one_r_rect = SurroundingRectangle(one.label[1]) + one_theta_rect = SurroundingRectangle(one.label[3]) + one_theta_rect.set_color(self.theta_color) + + self.play(ShowCreation(one_rect)) + self.add(r_line, np_points, pp_points, one_rect) + self.play( + ReplacementTransform(one_rect, one_r_rect), + ShowCreation(r_line) + ) + self.wait() + # self.play(TransformFromCopy(r_line, pre_arc)) + # self.add(pre_arc, one) + self.play( + TransformFromCopy(r_line, arc), + ReplacementTransform(one_r_rect, one_theta_rect) + ) + self.add(arc, one, one_theta_rect) + self.play(FadeOut(one_theta_rect)) + self.wait() + + # Talk about 2 + self.play(theta_tracker.set_value, 2) + self.wait() + self.play(Rotate(r_line, angle=1, about_point=ORIGIN)) + self.play(r_line.scale, 2, {'about_point': ORIGIN}) + self.wait() + + # And now 3 + self.play(theta_tracker.set_value, 3) + self.play( + Transform(arc, self.get_arc(3)), + Rotate(r_line, angle=1, about_point=ORIGIN), + ) + self.wait() + self.play( + r_line.scale, 3 / 2, {"about_point": ORIGIN} + ) + self.wait() + + # Zoom out and show spiral + spiral = ParametricFunction( + lambda t: self.get_polar_point(t, t), + t_min=0, + t_max=25, + stroke_width=1.5, + ) + + self.add(spiral, pp_points, np_points) + + self.polar_grid.generate_target() + for mob in self.polar_grid: + if not isinstance(mob[0], Integer): + mob.set_stroke(width=1) + + self.play( + ApplyMethod( + frame.scale, 3, + run_time=5, + rate_func=lambda t: smooth(t, 2) + ), + ShowCreation( + spiral, + run_time=6, + ), + FadeOut(r_line), + FadeOut(arc), + MoveToTarget(self.polar_grid) + ) + self.wait() + + # + def get_nn_point(self, n): + point = self.get_polar_point(n, n) + dot = Dot(point) + coord_label = self.get_coord_label( + n, n, + include_background_rectangle=False, + num_decimal_places=0 + ) + coord_label.next_to(dot, UR, buff=0) + result = VGroup(dot, coord_label) + result.dot = dot + result.label = coord_label + return result + + def get_polar_grid(self, radius=25): + plane = self.plane + axes = VGroup( + Line(radius * DOWN, radius * UP), + Line(radius * LEFT, radius * RIGHT), + ) + axes.set_stroke(width=2) + circles = VGroup(*[ + Circle(color=BLUE, stroke_width=1, radius=r) + for r in range(1, int(radius)) + ]) + rays = VGroup(*[ + Line( + ORIGIN, radius * RIGHT, + color=BLUE, + stroke_width=1, + ).rotate(angle, about_point=ORIGIN) + for angle in np.arange(0, TAU, TAU / 16) + ]) + labels = VGroup(*[ + Integer(n).scale(0.5).next_to( + plane.c2p(n, 0), DR, SMALL_BUFF + ) + for n in range(1, int(radius)) + ]) + + return VGroup( + circles, rays, labels, axes, + ) + + def get_coord_label(self, + x=0, + y=0, + x_color=WHITE, + y_color=WHITE, + include_background_rectangle=True, + **decimal_kwargs): + x_coord = DecimalNumber(x, **decimal_kwargs) + y_coord = DecimalNumber(y, **decimal_kwargs) + + coord_label = VGroup( + TexMobject("("), x_coord, + TexMobject(","), y_coord, + TexMobject(")") + ) + coord_label.arrange(RIGHT, buff=SMALL_BUFF) + coord_label[2].align_to(coord_label[0], DOWN) + + coord_label.x_coord = x_coord + coord_label.y_coord = y_coord + if include_background_rectangle: + coord_label.add_background_rectangle() + return coord_label + + def get_polar_point(self, r, theta): + plane = self.plane + return plane.c2p(r * np.cos(theta), r * np.sin(theta)) + + def get_arc(self, theta, r=1, color=None): + if color is None: + color = self.theta_color + return Arc( + angle=theta, + radius=r, + stroke_color=color, + ) + + +class SpiralSceneTest(SpiralScene): + def construct(self): + N = 1 * 10**4 + axes = self.axes + + primes = generate_prime_list(N) + prime_dots = self.get_dots_in_spiral(primes) + + scale_factor = TAU / N + + axes.generate_target(use_deepcopy=True) + axes.target.scale(scale_factor, about_point=ORIGIN) + prime_dots.target = self.get_dots_in_spiral(primes, axes.target) + + self.add(prime_dots) + self.wait() + self.set_scale(0.1) + + # self.add(ParametricFunction( + # lambda t: axes.c2p(t * np.cos(t), t * np.sin(t)), + # t_min=0, + # t_max=30, + # )) + + return + + ap_dots = self.get_dots_in_spiral(range(0, N, 666)) + ap_dots.set_color(YELLOW) + self.play(ShowIncreasingSubsets(ap_dots)) + self.wait() From 307ac852a65c21c1b62667ffa7b8ae1ce66f413d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 19 Sep 2019 13:38:03 -0700 Subject: [PATCH 08/21] Added ShowSubmobjectsOneByOne --- manimlib/animation/creation.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/manimlib/animation/creation.py b/manimlib/animation/creation.py index c88a7a4e..dee61497 100644 --- a/manimlib/animation/creation.py +++ b/manimlib/animation/creation.py @@ -1,5 +1,6 @@ from manimlib.animation.animation import Animation from manimlib.mobject.types.vectorized_mobject import VMobject +from manimlib.mobject.mobject import Group from manimlib.utils.bezier import integer_interpolate from manimlib.utils.config_ops import digest_config from manimlib.utils.rate_functions import linear @@ -132,4 +133,20 @@ class ShowIncreasingSubsets(Animation): def interpolate_mobject(self, alpha): n_submobs = len(self.all_submobs) index = int(self.int_func(alpha * n_submobs)) + self.update_submobject_list(index) + + def update_submobject_list(self, index): self.mobject.submobjects = self.all_submobs[:index] + + +class ShowSubmobjectsOneByOne(ShowIncreasingSubsets): + def __init__(self, group, **kwargs): + new_group = Group(*group) + super().__init__(new_group, **kwargs) + + def update_submobject_list(self, index): + # N = len(self.all_submobs) + if index == 0: + self.mobject.submobjects = [] + else: + self.mobject.submobjects = self.all_submobs[index - 1] From 42e0cd07acb0b4bd0b557736712d22f8201ed8db Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 19 Sep 2019 13:38:46 -0700 Subject: [PATCH 09/21] Added support for changing and interpolating stroke_width on PMobjects --- manimlib/mobject/types/point_cloud_mobject.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/manimlib/mobject/types/point_cloud_mobject.py b/manimlib/mobject/types/point_cloud_mobject.py index f7b087ab..44ef3f98 100644 --- a/manimlib/mobject/types/point_cloud_mobject.py +++ b/manimlib/mobject/types/point_cloud_mobject.py @@ -50,6 +50,15 @@ class PMobject(Mobject): self.color = color return self + def get_stroke_width(self): + return self.stroke_width + + def set_stroke_width(self, width, family=True): + mobs = self.family_members_with_points() if family else [self] + for mob in mobs: + mob.stroke_width = width + return self + # def set_color_by_gradient(self, start_color, end_color): def set_color_by_gradient(self, *colors): self.rgbas = np.array(list(map( @@ -158,6 +167,12 @@ class PMobject(Mobject): self.rgbas = interpolate( mobject1.rgbas, mobject2.rgbas, alpha ) + self.set_stroke_width(interpolate( + mobject1.get_stroke_width(), + mobject2.get_stroke_width(), + alpha, + )) + return self def pointwise_become_partial(self, mobject, a, b): lower_index, upper_index = [ From 2bb8afa9e097a2c0eb5d13496a162d3b5604d777 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 19 Sep 2019 13:39:02 -0700 Subject: [PATCH 10/21] Animations for the introduction of the prime spiral video --- active_projects/spirals.py | 421 ++++++++++++++++++++++++++++++++----- 1 file changed, 367 insertions(+), 54 deletions(-) diff --git a/active_projects/spirals.py b/active_projects/spirals.py index 3808a921..cb8b0efd 100644 --- a/active_projects/spirals.py +++ b/active_projects/spirals.py @@ -1,7 +1,10 @@ from manimlib.imports import * +import json OUTPUT_DIRECTORY = "spirals" +INV_113_MOD_710 = 377 # Inverse of 113 mode 710 +INV_7_MOD_44 = 19 def generate_prime_list(*args): @@ -25,58 +28,133 @@ def generate_prime_list(*args): return result -class SpiralScene(Scene): +def get_gcd(x, y): + while y > 0: + x, y = y, x % y + return x + + +def read_in_primes(max_N=None): + with open(os.path.join("assets", "primes.json")) as fp: + primes = np.array(json.load(fp)) + if max_N: + return primes[primes <= max_N] + return primes + + +class SpiralScene(MovingCameraScene): CONFIG = { "axes_config": { "number_line_config": { "stroke_width": 1.5, } - } + }, + "default_dot_color": TEAL, + "p_spiral_width": 6, } def setup(self): + super().setup() self.axes = Axes(**self.axes_config) self.add(self.axes) - def get_dots_in_spiral(self, sequence, axes=None, dot_radius=None): + def get_v_spiral(self, sequence, axes=None, data_point_width=None): if axes is None: axes = self.axes - if dot_radius is None: + if data_point_width is None: unit = get_norm(axes.c2p(1, 0) - axes.c2p(0, 0)), - dot_radius = max( - 0.1 / (-np.log10(unit) + 1), - 0.01, + data_point_width = max( + 0.2 / (-np.log10(unit) + 1), + 0.02, ) return VGroup(*[ - Dot( - self.get_polar_point(n, n, axes), - radius=dot_radius, - fill_color=TEAL, - ) + Square( + side_length=data_point_width, + fill_color=self.default_dot_color, + fill_opacity=1, + stroke_width=0, + ).move_to(self.get_polar_point(n, n, axes)) for n in sequence ]) - def get_polar_point(self, r, theta, axes): + def get_p_spiral(self, sequence, axes=None): + if axes is None: + axes = self.axes + result = PMobject( + color=self.default_dot_color, + stroke_width=self.p_spiral_width, + ) + result.add_points([ + self.get_polar_point(n, n, axes) + for n in sequence + ]) + return result + + def get_prime_v_spiral(self, max_N, **kwargs): + primes = read_in_primes(max_N) + return self.get_v_spiral(primes, **kwargs) + + def get_prime_p_spiral(self, max_N, **kwargs): + primes = read_in_primes(max_N) + return self.get_p_spiral(primes, **kwargs) + + def get_polar_point(self, r, theta, axes=None): + if axes is None: + axes = self.axes return axes.c2p(r * np.cos(theta), r * np.sin(theta)) - def set_scale(self, scale, exceptions=None, run_time=3): - axes = self.axes - if exceptions is None: - exceptions = [] + def set_scale(self, scale, + axes=None, + spiral=None, + to_shrink=None, + min_data_point_width=0.05, + target_p_spiral_width=None, + run_time=3): + if axes is None: + axes = self.axes + sf = self.get_scale_factor(scale, axes) - unit = get_norm(axes.c2p(1, 0) - axes.c2p(0, 0)) - sf = unit / scale + anims = [] + for mob in [axes, spiral, to_shrink]: + if mob is None: + continue + mob.generate_target() + mob.target.scale(sf, about_point=ORIGIN) + if mob is spiral: + if isinstance(mob, VMobject): + old_width = mob[0].get_width() + for submob in mob.target: + submob.set_width(max( + old_width * sf, + min_data_point_width, + )) + elif isinstance(mob, PMobject): + if target_p_spiral_width is not None: + mob.target.set_stroke_width(target_p_spiral_width) + anims.append(MoveToTarget(mob)) - self.remove(*exceptions) - self.play(*[ - ApplyMethod( - mob.scale, sf, - about_point=ORIGIN, + if run_time == 0: + for anim in anims: + anim.begin() + anim.update(1) + anim.finish() + else: + self.play( + *anims, run_time=run_time, + rate_func=lambda t: interpolate( + smooth(t), + smooth(t)**(sf**(0.5)), + t, + ) ) - for mob in self.get_top_level_mobjects() - ]) + + def get_scale_factor(self, target_scale, axes=None): + if axes is None: + axes = self.axes + unit = get_norm(axes.c2p(1, 0) - axes.c2p(0, 0)) + return 1 / (target_scale * unit) # Scenes @@ -101,7 +179,9 @@ class RefresherOnPolarCoordinates(MovingCameraScene): plane = NumberPlane() plane.add_coordinates() - x, y = 3, 2 + x = 3 * np.cos(PI / 6) + y = 3 * np.sin(PI / 6) + point = plane.c2p(x, y) xp = plane.c2p(x, 0) origin = plane.c2p(0, 0) @@ -114,7 +194,7 @@ class RefresherOnPolarCoordinates(MovingCameraScene): dot = Dot(point) - coord_label = self.get_coord_label(x, y, x_color, y_color) + coord_label = self.get_coord_label(0, 0, x_color, y_color) x_coord = coord_label.x_coord y_coord = coord_label.y_coord @@ -251,6 +331,7 @@ class RefresherOnPolarCoordinates(MovingCameraScene): ChangeDecimalToValue(degree_label, theta_value / DEGREES), ) self.play( + degree_label.scale, 0.9, degree_label.move_to, theta_coord, FadeInFromDown(theta_label), ) @@ -371,9 +452,8 @@ class RefresherOnPolarCoordinates(MovingCameraScene): self.wait() # And now 3 - self.play(theta_tracker.set_value, 3) self.play( - Transform(arc, self.get_arc(3)), + theta_tracker.set_value, 3, Rotate(r_line, angle=1, about_point=ORIGIN), ) self.wait() @@ -382,6 +462,17 @@ class RefresherOnPolarCoordinates(MovingCameraScene): ) self.wait() + # Finally 4 + self.play( + theta_tracker.set_value, 4, + Rotate(r_line, angle=1, about_point=ORIGIN), + ) + self.wait() + self.play( + r_line.scale, 4 / 3, {"about_point": ORIGIN} + ) + self.wait() + # Zoom out and show spiral spiral = ParametricFunction( lambda t: self.get_polar_point(t, t), @@ -466,7 +557,9 @@ class RefresherOnPolarCoordinates(MovingCameraScene): include_background_rectangle=True, **decimal_kwargs): x_coord = DecimalNumber(x, **decimal_kwargs) + x_coord.set_color(x_color) y_coord = DecimalNumber(y, **decimal_kwargs) + y_coord.set_color(y_color) coord_label = VGroup( TexMobject("("), x_coord, @@ -496,33 +589,253 @@ class RefresherOnPolarCoordinates(MovingCameraScene): ) -class SpiralSceneTest(SpiralScene): +class IntroducePrimePatterns(SpiralScene): + CONFIG = { + "small_n_primes": 25000, + "big_n_primes": 1000000, + "axes_config": { + "x_min": -25, + "x_max": 25, + "y_min": -25, + "y_max": 25, + }, + "spiral_scale": 3e3, + "ray_scale": 1e5, + } + def construct(self): - N = 1 * 10**4 - axes = self.axes + self.slowly_zoom_out() + self.show_clumps_of_four() - primes = generate_prime_list(N) - prime_dots = self.get_dots_in_spiral(primes) + def slowly_zoom_out(self): + zoom_time = 8 - scale_factor = TAU / N + prime_spiral = self.get_prime_p_spiral(self.small_n_primes) + prime_spiral.set_stroke_width(25) + self.add(prime_spiral) - axes.generate_target(use_deepcopy=True) - axes.target.scale(scale_factor, about_point=ORIGIN) - prime_dots.target = self.get_dots_in_spiral(primes, axes.target) - - self.add(prime_dots) + self.set_scale(3, spiral=prime_spiral) self.wait() - self.set_scale(0.1) - - # self.add(ParametricFunction( - # lambda t: axes.c2p(t * np.cos(t), t * np.sin(t)), - # t_min=0, - # t_max=30, - # )) - - return - - ap_dots = self.get_dots_in_spiral(range(0, N, 666)) - ap_dots.set_color(YELLOW) - self.play(ShowIncreasingSubsets(ap_dots)) + self.set_scale( + self.spiral_scale, + spiral=prime_spiral, + target_p_spiral_width=8, + run_time=zoom_time, + ) self.wait() + + self.remove(prime_spiral) + prime_spiral = self.get_prime_p_spiral(self.big_n_primes) + prime_spiral.set_stroke_width(8) + self.set_scale( + self.ray_scale, + spiral=prime_spiral, + target_p_spiral_width=4, + run_time=zoom_time, + ) + self.wait() + + def show_clumps_of_four(self): + line_groups = VGroup() + for n in range(71): + group = VGroup() + for k in [-3, -1, 1, 3]: + r = ((10 * n + k) * INV_113_MOD_710) % 710 + group.add(self.get_arithmetic_sequence_line( + 710, r, self.big_n_primes + )) + line_groups.add(group) + + line_groups.set_stroke(YELLOW, 2, opacity=0.5) + + self.play(ShowCreation(line_groups[0])) + for g1, g2 in zip(line_groups, line_groups[1:5]): + self.play( + FadeOut(g1), + ShowCreation(g2) + ) + + self.play( + FadeOut(line_groups[4]), + LaggedStartMap( + VFadeInThenOut, + line_groups[4:], + lag_ratio=0.5, + run_time=5, + ) + ) + self.wait() + + def get_arithmetic_sequence_line(self, N, r, max_val, skip_factor=5): + line = VMobject() + line.set_points_smoothly([ + self.get_polar_point(x, x) + for x in range(r, max_val, skip_factor * N) + ]) + return line + + +class AskWhat(TeacherStudentsScene): + def construct(self): + screen = self.screen + self.student_says( + "I'm sory,\\\\what?!?", + target_mode="angry", + look_at_arg=screen, + student_index=2, + added_anims=[ + self.teacher.change, "happy", screen, + self.students[0].change, "confused", screen, + self.students[1].change, "confused", screen, + ] + ) + self.wait(3) + + +class CountSpirals(IntroducePrimePatterns): + CONFIG = { + "count_sound": "pen_click.wav", + } + + def construct(self): + prime_spiral = self.get_prime_p_spiral(self.small_n_primes) + + self.add(prime_spiral) + self.set_scale( + self.spiral_scale, + spiral=prime_spiral, + run_time=0, + ) + + spiral_lines = self.get_all_primative_arithmetic_lines( + 44, self.small_n_primes, INV_7_MOD_44, + ) + spiral_lines.set_stroke(YELLOW, 2, opacity=0.5) + + counts = VGroup() + for n, spiral in zip(it.count(1), spiral_lines): + count = Integer(n) + count.move_to(spiral.point_from_proportion(0.25)) + counts.add(count) + + run_time = 3 + self.play( + ShowIncreasingSubsets(spiral_lines), + ShowSubmobjectsOneByOne(counts), + run_time=run_time, + rate_func=linear, + ) + self.add_count_clicks(len(spiral_lines), run_time) + self.play( + counts[-1].scale, 3, + counts[-1].set_stroke, BLACK, 5, {"background": True}, + ) + self.wait() + + def get_all_primative_arithmetic_lines(self, N, max_val, mult_factor): + lines = VGroup() + for r in range(1, N): + if get_gcd(N, r) == 1: + lines.add( + self.get_arithmetic_sequence_line(N, (mult_factor * r) % N, max_val) + ) + return lines + + def add_count_clicks(self, N, time, rate_func=linear): + alphas = np.arange(0, 1, 1 / N) + if rate_func is linear: + delays = time * alphas + else: + delays = time * np.array([ + binary_search( + rate_func, + alpha, + 0, + 1, + ) + for alpha in alphas + ]) + for delay in delays: + self.add_sound( + self.count_sound, + time_offset=-delay, + gain=-15, + ) + + +class CountRays(CountSpirals): + def construct(self): + prime_spiral = self.get_prime_p_spiral(self.big_n_primes) + + self.add(prime_spiral) + self.set_scale( + self.ray_scale, + spiral=prime_spiral, + run_time=0, + ) + + spiral_lines = self.get_all_primative_arithmetic_lines( + 710, self.big_n_primes, INV_113_MOD_710, + ) + spiral_lines.set_stroke(YELLOW, 2, opacity=0.5) + + counts = VGroup() + for n, spiral in zip(it.count(1), spiral_lines): + count = Integer(n) + count.move_to(spiral.point_from_proportion(0.25)) + counts.add(count) + + run_time = 6 + self.play( + ShowIncreasingSubsets(spiral_lines), + ShowSubmobjectsOneByOne(counts), + run_time=run_time, + rate_func=smooth, + ) + self.add_count_clicks(len(spiral_lines), run_time, rate_func=smooth) + self.play( + counts[-1].scale, 3, + counts[-1].set_stroke, BLACK, 5, {"background": True}, + ) + self.wait() + self.play(FadeOut(spiral_lines)) + self.wait() + + +class AskAboutRelationToPrimes(TeacherStudentsScene): + def construct(self): + numbers = TextMobject("20, 280") + arrow = Arrow(LEFT, RIGHT) + primes = TextMobject("2, 3, 5, 7, 11, \\dots") + q_marks = TextMobject("???") + q_marks.set_color(YELLOW) + + group = VGroup(numbers, arrow, primes) + group.arrange(RIGHT) + q_marks.next_to(arrow, UP) + group.add(q_marks) + group.scale(1.5) + group.next_to(self.pi_creatures, UP, LARGE_BUFF) + + self.play( + self.get_student_changes( + *3 * ["maybe"], + look_at_arg=numbers, + ), + self.teacher.change, "maybe", numbers, + ShowCreation(arrow), + FadeInFrom(numbers, RIGHT) + ) + self.play( + FadeInFrom(primes, LEFT), + ) + self.play( + LaggedStartMap(FadeInFromDown, q_marks[0]), + Blink(self.teacher) + ) + self.wait(3) + + +class ZoomOutOnPrimesWithNumbers(IntroducePrimePatterns): + def construct(self): + pass From 557bbff36c71a972a27b7ef0b422a66ecccf52b9 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 24 Sep 2019 14:40:21 -0700 Subject: [PATCH 11/21] Added PGroup --- manimlib/mobject/types/point_cloud_mobject.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/manimlib/mobject/types/point_cloud_mobject.py b/manimlib/mobject/types/point_cloud_mobject.py index 44ef3f98..f8276382 100644 --- a/manimlib/mobject/types/point_cloud_mobject.py +++ b/manimlib/mobject/types/point_cloud_mobject.py @@ -221,6 +221,14 @@ class Mobject2D(PMobject): Mobject.__init__(self, **kwargs) +class PGroup(PMobject): + def __init__(self, *pmobs, **kwargs): + if not all([isinstance(m, PMobject) for m in pmobs]): + raise Exception("All submobjects must be of type PMobject") + super().__init__(**kwargs) + self.add(*pmobs) + + class PointCloudDot(Mobject1D): CONFIG = { "radius": 0.075, From ff55e768c93843d45b585b474899123841225307 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 24 Sep 2019 14:40:39 -0700 Subject: [PATCH 12/21] Spiral animations up to Label44Spirals --- active_projects/spirals.py | 1161 +++++++++++++++++++++++++++++++++++- 1 file changed, 1143 insertions(+), 18 deletions(-) diff --git a/active_projects/spirals.py b/active_projects/spirals.py index cb8b0efd..3a53672e 100644 --- a/active_projects/spirals.py +++ b/active_projects/spirals.py @@ -1,5 +1,6 @@ from manimlib.imports import * import json +import numbers OUTPUT_DIRECTORY = "spirals" @@ -35,11 +36,19 @@ def get_gcd(x, y): def read_in_primes(max_N=None): - with open(os.path.join("assets", "primes.json")) as fp: + if max_N is None: + max_N = int(1e7) + + if max_N < 1e5: + file = "primes_1e5.json" + elif max_N < 1e6: + file = "primes_1e5.json" + else: + file = "primes_1e7.json" + + with open(os.path.join("assets", file)) as fp: primes = np.array(json.load(fp)) - if max_N: - return primes[primes <= max_N] - return primes + return primes[primes <= max_N] class SpiralScene(MovingCameraScene): @@ -58,19 +67,19 @@ class SpiralScene(MovingCameraScene): self.axes = Axes(**self.axes_config) self.add(self.axes) - def get_v_spiral(self, sequence, axes=None, data_point_width=None): + def get_v_spiral(self, sequence, axes=None, box_width=None): if axes is None: axes = self.axes - if data_point_width is None: + if box_width is None: unit = get_norm(axes.c2p(1, 0) - axes.c2p(0, 0)), - data_point_width = max( + box_width = max( 0.2 / (-np.log10(unit) + 1), 0.02, ) return VGroup(*[ Square( - side_length=data_point_width, + side_length=box_width, fill_color=self.default_dot_color, fill_opacity=1, stroke_width=0, @@ -108,7 +117,7 @@ class SpiralScene(MovingCameraScene): axes=None, spiral=None, to_shrink=None, - min_data_point_width=0.05, + min_box_width=0.05, target_p_spiral_width=None, run_time=3): if axes is None: @@ -127,7 +136,7 @@ class SpiralScene(MovingCameraScene): for submob in mob.target: submob.set_width(max( old_width * sf, - min_data_point_width, + min_box_width, )) elif isinstance(mob, PMobject): if target_p_spiral_width is not None: @@ -156,6 +165,23 @@ class SpiralScene(MovingCameraScene): unit = get_norm(axes.c2p(1, 0) - axes.c2p(0, 0)) return 1 / (target_scale * unit) + def get_labels(self, sequence, scale_func=np.sqrt): + labels = VGroup() + for n in sequence: + label = Integer(n) + label.set_stroke(width=0, background=True) + label.scale(scale_func(n)) + label.next_to( + self.get_polar_point(n, n), UP, + buff=0.5 * label.get_height(), + ) + labels.add(label) + return labels + + def get_prime_labels(self, max_N): + primes = read_in_primes(max_N) + return self.get_labels(primes) + # Scenes @@ -556,9 +582,18 @@ class RefresherOnPolarCoordinates(MovingCameraScene): y_color=WHITE, include_background_rectangle=True, **decimal_kwargs): - x_coord = DecimalNumber(x, **decimal_kwargs) + coords = VGroup() + for n in x, y: + if isinstance(n, numbers.Number): + coord = DecimalNumber(n, **decimal_kwargs) + elif isinstance(n, str): + coord = TexMobject(n) + else: + raise Exception("Invalid type") + coords.add(coord) + + x_coord, y_coord = coords x_coord.set_color(x_color) - y_coord = DecimalNumber(y, **decimal_kwargs) y_coord.set_color(y_color) coord_label = VGroup( @@ -589,6 +624,48 @@ class RefresherOnPolarCoordinates(MovingCameraScene): ) +class ReplacePolarCoordinatesWithPrimes(RefresherOnPolarCoordinates): + def construct(self): + coords, p_coords = [ + self.get_coord_label( + *pair, + x_color=self.r_color, + y_color=self.theta_color, + ).scale(2) + for pair in [("r", "\\theta"), ("p", "p")] + ] + p_coords.x_coord.set_color(LIGHT_GREY) + p_coords.y_coord.set_color(LIGHT_GREY) + + some_prime = TextMobject("Some prime") + some_prime.scale(1.5) + some_prime.next_to(p_coords, UP, buff=1.5) + arrows = VGroup(*[ + Arrow( + some_prime.get_bottom(), coord.get_top(), + stroke_width=5, + tip_length=0.4 + ) + for coord in [p_coords.x_coord, p_coords.y_coord] + ]) + + equals = TexMobject("=") + equals.next_to(p_coords, LEFT) + + self.add(coords) + self.wait() + self.play( + coords.next_to, equals, LEFT, + FadeIn(equals), + FadeIn(p_coords), + ) + self.play( + FadeInFromDown(some_prime), + ShowCreation(arrows), + ) + self.wait() + + class IntroducePrimePatterns(SpiralScene): CONFIG = { "small_n_primes": 25000, @@ -747,14 +824,10 @@ class CountSpirals(IntroducePrimePatterns): delays = time * alphas else: delays = time * np.array([ - binary_search( - rate_func, - alpha, - 0, - 1, - ) + binary_search(rate_func, alpha, 0, 1) for alpha in alphas ]) + for delay in delays: self.add_sound( self.count_sound, @@ -837,5 +910,1057 @@ class AskAboutRelationToPrimes(TeacherStudentsScene): class ZoomOutOnPrimesWithNumbers(IntroducePrimePatterns): + CONFIG = { + "n_labeled_primes": 1000, + "big_n_primes": int(5e6), + "thicknesses": [8, 3, 2], + "thicker_target": False, + } + + def construct(self): + zoom_time = 20 + + prime_spiral = self.get_prime_p_spiral(self.big_n_primes) + prime_spiral.set_stroke_width(25) + + prime_labels = self.get_prime_labels(self.n_labeled_primes) + + self.add(prime_spiral) + self.add(prime_labels) + + scales = [self.spiral_scale, self.ray_scale, 5e5] + thicknesses = self.thicknesses + + for scale, tp in zip(scales, thicknesses): + kwargs = { + "spiral": prime_spiral, + "to_shrink": prime_labels, + "run_time": zoom_time, + "target_p_spiral_width": tp, + } + if self.thicker_target: + kwargs["target_p_spiral_width"] += 1 + self.set_scale(scale, **kwargs) + prime_spiral.set_stroke_width(tp) + self.wait() + self.remove(prime_labels) + + +class ThickZoomOutOnPrimesWithNumbers(ZoomOutOnPrimesWithNumbers): + CONFIG = { + # The only purpose of this scene is for overlay + # with the last one to smooth things out. + "thicker_target": True, + } + + +class ShowSpiralsForWholeNumbers(CountSpirals): + CONFIG = { + "max_prime": 10000, + "scale_44": 1e3, + "scale_6": 10, + "n_labels": 100, + "axes_config": { + "x_min": -50, + "x_max": 50, + "y_min": -50, + "y_max": 50, + }, + } + + def construct(self): + self.zoom_out_with_whole_numbers() + self.count_44_spirals() + self.zoom_back_in_to_6() + + def zoom_out_with_whole_numbers(self): + wholes = self.get_p_spiral(range(self.max_prime)) + primes = self.get_prime_p_spiral(self.max_prime) + + wholes.set_color(YELLOW) + wholes.set_stroke_width(20) + primes.set_stroke_width(20) + spiral = PGroup(wholes, primes) + + labels = self.get_labels(range(1, self.n_labels)) + + self.add(spiral, labels) + self.set_scale( + self.scale_44, + spiral=spiral, + to_shrink=labels, + target_p_spiral_width=6, + run_time=10, + ) + self.wait(2) + + self.spiral = spiral + self.labels = labels + + def count_44_spirals(self): + curr_spiral = self.spiral + + new_spirals = PGroup(*[ + self.get_p_spiral(range( + (INV_7_MOD_44 * k) % 44, self.max_prime, 44 + )) + for k in range(44) + ]) + new_spirals.set_color(YELLOW) + + counts = VGroup() + for n, spiral in zip(it.count(1), new_spirals): + count = Integer(n) + count.scale(2) + count.move_to(spiral.points[50]) + counts.add(count) + + self.remove(curr_spiral) + run_time = 3 + self.play( + ShowIncreasingSubsets(new_spirals), + ShowSubmobjectsOneByOne(counts), + run_time=run_time, + rate_func=linear, + ) + self.add_count_clicks(44, run_time) + self.play( + counts[-1].scale, 2, {"about_edge": DL}, + counts[-1].set_stroke, BLACK, 5, {"background": True}, + ) + self.wait() + self.play( + FadeOut(counts[-1]), + FadeOut(new_spirals), + FadeIn(curr_spiral), + ) + + def zoom_back_in_to_6(self): + spiral = self.spiral + + self.rescale_labels(self.labels) + self.set_scale( + self.scale_6, + spiral=spiral, + to_shrink=self.labels, + target_p_spiral_width=15, + run_time=6, + ) + self.wait() + + def rescale_labels(self, labels): + for i, label in zip(it.count(1), labels): + height = label.get_height() + label.set_height( + 3 * height / (i**0.25), + about_point=label.get_bottom() + 0.5 * label.get_height() * DOWN, + ) + + +class ExplainSixSpirals(ShowSpiralsForWholeNumbers): + CONFIG = { + "max_N": 150, + } + + def construct(self): + self.add_spirals_and_labels() + self.comment_on_arms() + self.talk_though_multiples_of_six() + self.limit_to_primes() + + def add_spirals_and_labels(self): + max_N = self.max_N + + spiral = self.get_v_spiral(range(max_N)) + primes = generate_prime_list(max_N) + spiral.set_color(YELLOW) + for n, box in enumerate(spiral): + if n in primes: + box.set_color(TEAL) + + labels = self.get_labels(range(max_N)) + + self.add(spiral, labels) + self.set_scale( + spiral=spiral, + scale=self.scale_6, + to_shrink=labels, + min_box_width=0.08, + run_time=0, + ) + self.rescale_labels(labels) + + self.spiral = spiral + self.labels = labels + + def comment_on_arms(self): + labels = self.labels + spiral = self.spiral + + label_groups = VGroup(*[labels[k::6] for k in range(6)]) + spiral_groups = VGroup(*[spiral[k::6] for k in range(6)]) + six_groups = VGroup(*[ + VGroup(sg, lg) + for sg, lg in zip(spiral_groups, label_groups) + ]) + rect_groups = VGroup(*[ + VGroup(*[ + SurroundingRectangle(label, stroke_width=2, buff=0.05) + for label in group + ]) + for group in label_groups + ]) + + formula = VGroup( + *TexMobject("6k", "+"), + Integer(1) + ) + formula.arrange(RIGHT, buff=SMALL_BUFF) + formula.scale(2) + formula.set_color(YELLOW) + formula.to_corner(UL) + formula_rect = SurroundingRectangle(formula, buff=MED_LARGE_BUFF - SMALL_BUFF) + formula_rect.set_fill(DARK_GREY, opacity=1) + formula_rect.set_stroke(WHITE, 1) + + # 6k + self.add(six_groups, formula_rect) + self.play( + LaggedStartMap(ShowCreation, rect_groups[0]), + FadeInFromDown(formula_rect), + FadeInFromDown(formula[0]), + *[ + ApplyMethod(group.set_opacity, 0.25) + for group in six_groups[1:] + ], + run_time=2 + ) + self.play( + LaggedStartMap( + FadeOut, rect_groups[0], + run_time=1, + ), + ) + self.wait() + + # 6k + 1 + self.play( + six_groups[0].set_opacity, 0.25, + six_groups[1].set_opacity, 1, + FadeIn(formula[1:]), + ) + self.wait(2) + + # 6k + m + for m in [2, 3, 4, 5]: + self.play( + six_groups[m - 1].set_opacity, 0.25, + six_groups[m].set_opacity, 1, + ChangeDecimalToValue(formula[2], m), + ) + self.wait() + self.play( + six_groups[5].set_opacity, 0.25, + six_groups[0].set_opacity, 1, + formula[1:].set_opacity, 0, + ) + self.wait() + + self.six_groups = six_groups + self.formula = VGroup(formula_rect, *formula) + + def talk_though_multiples_of_six(self): + spiral = self.spiral + labels = self.labels + formula = self.formula + + # Zoom in + self.add(spiral, labels, formula) + self.set_scale( + 4.5, + spiral=spiral, + to_shrink=labels, + run_time=2, + ) + self.wait() + + boxes = VGroup(*[ + VGroup(b.copy(), l.copy()) + for b, l in zip(spiral, labels) + ]) + boxes.set_opacity(1) + + lines = VGroup(*[ + Line(ORIGIN, box[0].get_center()) + for box in boxes + ]) + lines.set_stroke(LIGHT_GREY, width=2) + + arcs = self.get_arcs(range(31)) + + trash = VGroup() + + def show_steps(start, stop, added_anims=None, run_time=2): + if added_anims is None: + added_anims = [] + self.play( + *[ + ShowSubmobjectsOneByOne(group[start:stop + 1]) + for group in [arcs, boxes, lines] + ], + *added_anims, + rate_func=linear, + run_time=run_time, + ) + self.add_count_clicks(N=6, time=run_time) + trash.add(VGroup(arcs[stop], boxes[stop], lines[stop])) + + # Writing next to the 6 + six = boxes[6][1] + rhs = TexMobject( + "\\text{radians}", + "\\approx", + "2\\pi", + "\\text{ radians}" + ) + rhs.next_to(six, RIGHT, 2 * SMALL_BUFF, aligned_edge=DOWN) + rhs.add_background_rectangle() + tau_value = TexMobject("{:.8}\\dots".format(TAU)) + tau_value.next_to(rhs[3], UP, aligned_edge=LEFT) + + # Animations + show_steps(0, 6, run_time=3) + self.wait() + self.play(FadeIn(rhs)) + self.wait() + self.play(FadeInFromDown(tau_value)) + self.wait(2) + + show_steps(6, 12) + self.wait() + + show_steps(12, 18) + self.wait() + + # Zoom out + frame = self.camera_frame + frame.add(formula) + show_steps(18, 24, added_anims=[frame.scale, 2.5]) + self.wait() + show_steps(24, 30) + self.wait(2) + + self.play( + FadeOut(trash), + FadeOut(rhs), + FadeOut(tau_value), + spiral.set_opacity, 1, + labels.set_opacity, 1, + formula[1].set_opacity, 0, + ) + + def limit_to_primes(self): + formula = self.formula + formula_rect, six_k, plus, m_sym = formula + spiral = self.spiral + labels = self.labels + six_groups = self.six_groups + frame = self.camera_frame + + boxes = VGroup(*[ + VGroup(b, l) + for b, l in zip(spiral, labels) + ]) + prime_numbers = read_in_primes(self.max_N) + primes = VGroup() + non_primes = VGroup() + for n, box in enumerate(boxes): + if n in prime_numbers: + primes.add(box) + else: + non_primes.add(box) + + prime_label = TextMobject("Primes") + prime_label.set_color(TEAL) + prime_label.match_width(VGroup(six_k, m_sym)) + prime_label.move_to(six_k, LEFT) + + # Show just primes + self.add(primes, non_primes, formula) + self.play( + FadeIn(prime_label), + non_primes.set_opacity, 0.25, + ) + frame.add(prime_label) + self.play( + frame.scale, 1.5, + run_time=2, + ) + self.wait(2) + + cross_groups = VGroup() + for group in six_groups: + group.save_state() + boxes, labels = group + cross_group = VGroup() + for label in labels: + cross_group.add(Cross(label)) + cross_groups.add(cross_group) + cross_groups.set_stroke(width=3) + cross_groups[2].remove(cross_groups[2][0]) + cross_groups[3].remove(cross_groups[3][0]) + + # Show multiples of 6 + for r in [0, 2, 4, 3]: + arm = six_groups[r] + crosses = cross_groups[r] + self.add(arm, frame) + + anims = [arm.set_opacity, 1] + if r == 0: + anims += [ + prime_label.set_opacity, 0, + six_k.set_opacity, 1 + ] + elif r == 2: + m_sym.set_value(2) + anims += [ + plus.set_opacity, 1, + m_sym.set_opacity, 1, + ] + else: + anims.append(ChangeDecimalToValue(m_sym, r)) + self.play(*anims) + self.add(*crosses, frame) + self.play( + LaggedStartMap(ShowCreation, crosses), + ) + self.wait() + + # Fade forbidden groups + to_fade = VGroup(*[ + VGroup(six_groups[r], cross_groups[r]) + for r in (0, 2, 3, 4) + ]) + self.add(to_fade, frame) + self.play( + to_fade.set_opacity, 0.25, + VGroup(six_k, plus, m_sym).set_opacity, 0, + prime_label.set_opacity, 1, + ) + self.wait() + + # + def arc_func(self, t): + r = 0.25 + 0.02 * t + return r * np.array([np.cos(t), np.sin(t), 0]) + + def get_arc(self, n): + if n == 0: + return VectorizedPoint() + return ParametricFunction( + self.arc_func, + t_min=0, + t_max=n, + step_size=0.1, + stroke_width=2, + stroke_color=PINK, + ) + + def get_arcs(self, sequence): + return VGroup(*map(self.get_arc, sequence)) + + +class IntroduceResidueClassTerminology(Scene): + def construct(self): + self.add_title() + self.add_sequences() + self.add_terms() + self.highlight_example() + self.simple_english() + + def add_title(self): + title = TextMobject("Overly-fancy terminology") + title.scale(1.5) + title.to_edge(UP, buff=MED_SMALL_BUFF) + underline = Line().match_width(title) + underline.next_to(title, DOWN, SMALL_BUFF) + title.add(underline) + self.add(title) + + self.title = title + self.underline = underline + + def add_sequences(self): + sequences = VGroup() + n_terms = 7 + + for r in range(6): + sequence = VGroup(*[ + Integer(6 * k + r) + for k in range(n_terms) + ]) + sequence.arrange(RIGHT, buff=0.4) + sequences.add(sequence) + + sequences.arrange(DOWN, buff=0.7, aligned_edge=LEFT) + for sequence in sequences: + for s1, s2 in zip(sequence[:n_terms], sequences[-1]): + s1.align_to(s2, RIGHT) + commas = VGroup() + for num in sequence[:-1]: + comma = TextMobject(",") + comma.next_to(num.get_corner(DR), RIGHT, SMALL_BUFF) + commas.add(comma) + dots = TexMobject("\\dots") + dots.next_to(sequence.get_corner(DR), RIGHT, SMALL_BUFF) + sequence.numbers = VGroup(*sequence) + sequence.commas = commas + sequence.dots = dots + + sequence.add(*commas) + sequence.add(dots) + sequence.sort(lambda p: p[0]) + + labels = VGroup(*[ + TexMobject("6k + {}:".format(r)) + for r in range(6) + ]) + labels.set_color(YELLOW) + for label, sequence in zip(labels, sequences): + label.next_to(sequence, LEFT, MED_LARGE_BUFF) + + group = VGroup(sequences, labels) + group.to_edge(LEFT).to_edge(DOWN, buff=MED_LARGE_BUFF) + + self.add(labels) + self.play(LaggedStart(*[ + LaggedStartMap( + FadeInFrom, sequence, + lambda m: (m, LEFT), + ) + for sequence in sequences + ], lag_ratio=0.3)) + self.wait() + + self.sequences = sequences + self.sequence_labels = labels + + def add_terms(self): + sequences = self.sequences + + terms = TextMobject( + "``", "Residue\\\\", + "classes\\\\", + "mod ", "6''" + ) + terms.scale(1.5) + terms.set_color(YELLOW) + terms.to_edge(RIGHT) + + res_brace = Brace(terms.get_part_by_tex("Residue"), UP) + remainder = TextMobject("Remainder") + remainder.next_to(res_brace, UP, SMALL_BUFF) + + mod_brace = Brace(terms.get_part_by_tex("mod"), DOWN) + mod_def = TextMobject( + "``where the thing\\\\you divide by is''" + ) + mod_def.next_to(mod_brace, DOWN, SMALL_BUFF) + + arrows = VGroup(*[ + Arrow(terms.get_left(), sequence.get_right()) + for sequence in sequences + ]) + arrows.set_color(YELLOW) + + self.play( + FadeIn(terms), + LaggedStartMap(ShowCreation, arrows), + ) + self.wait() + self.play( + GrowFromCenter(res_brace), + FadeInFromDown(remainder), + ) + self.wait() + self.play(GrowFromCenter(mod_brace)) + self.play(Write(mod_def)) + self.wait() + + self.terminology = VGroup( + terms, + res_brace, remainder, + mod_brace, mod_def, + arrows + ) + + def highlight_example(self): + sequences = self.sequences + labels = self.sequence_labels + + r = 2 + k = 2 + sequence = sequences[r] + label = labels[r] + r_tex = label[0][3] + n_rects = VGroup(*[ + SurroundingRectangle(num) + for num in sequence.numbers + ]) + r_rect = SurroundingRectangle(r_tex) + n_rects.set_color(RED) + r_rect.set_color(RED) + + n_rect = n_rects.submobjects.pop(k) + + self.play(ShowCreation(n_rect)) + self.wait() + self.play( + TransformFromCopy(n_rect, r_rect, path_arc=30 * DEGREES) + ) + self.wait() + self.play(ShowCreation(n_rects)) + self.wait() + self.play( + ShowCreationThenFadeOut( + self.underline.copy().set_color(PINK), + ) + ) + + def simple_english(self): + terminology = self.terminology + sequences = self.sequences + + randy = Randolph() + randy.set_height(2) + randy.flip() + randy.to_corner(DR) + + new_phrase = TextMobject("Everything 2 above\\\\a multiple of 6") + new_phrase.scale(1.2) + new_phrase.set_color(RED_B) + new_phrase.next_to(sequences[2]) + + self.play( + FadeOut(terminology), + FadeIn(new_phrase), + VFadeIn(randy), + randy.change, "sassy" + ) + self.wait() + self.play(randy.change, "angry") + for x in range(2): + self.play(Blink(randy)) + self.wait() + self.play( + FadeOut(new_phrase), + FadeIn(terminology), + FadeOutAndShift(randy, DOWN) + ) + self.wait() + + +class Explain44Spirals(ExplainSixSpirals): + CONFIG = { + "max_N": 3000, + "initial_scale": 10, + "zoom_factor_1": 7, + "zoom_factor_2": 3, + } + + def construct(self): + self.add_spirals_and_labels() + self.show_44_steps() + self.show_top_right_arithmetic() + self.show_pi_approx_arithmetic() + self.count_by_44() + + def add_spirals_and_labels(self): + max_N = self.max_N + + wholes = self.get_p_spiral(range(max_N)) + primes = self.get_prime_p_spiral(max_N) + wholes.set_color(YELLOW) + spiral = PGroup(wholes, primes) + + labels = self.get_labels(range(80)) + + self.add(spiral, labels) + self.set_scale( + spiral=spiral, + scale=self.initial_scale, + to_shrink=labels, + target_p_spiral_width=10, + run_time=0, + ) + self.rescale_labels(labels) + + self.spiral = spiral + self.labels = labels + + def show_44_steps(self): + labels = self.labels + + ns = range(45) + points = [self.get_polar_point(n, n) for n in ns] + lines = VGroup(*[ + Line(ORIGIN, point) + for point in points + ]) + lines.set_stroke(WHITE, 2) + arcs = self.get_arcs(ns) + + opaque_labels = labels.copy() + labels.set_opacity(0.25) + + trash = VGroup() + + def show_steps(start, stop, added_anims=None, run_time=2): + if added_anims is None: + added_anims = [] + + def rate_func(t): + return smooth(t, 2) + + self.play( + *[ + ShowSubmobjectsOneByOne(group[start:stop + 1]) + for group in [arcs, opaque_labels, lines] + ], + *added_anims, + rate_func=rate_func, + run_time=run_time, + ) + self.add_count_clicks( + N=(stop - start), time=run_time, + rate_func=rate_func + ) + trash.add(arcs[stop], opaque_labels[stop], lines[stop]) + + show_steps(0, 6) + self.wait() + show_steps(6, 44, added_anims=[FadeOut(trash)], run_time=4) + self.wait() + + self.spiral_group = trash[-3:] + + def show_top_right_arithmetic(self): + labels = self.labels + ff = labels[44].copy() + ff.generate_target() + + radians = TextMobject("radians") + ff.target.scale(1.5) + ff.target.set_opacity(1) + + unit_conversion = TexMobject( + "/\\,", "\\left(", "2\\pi", + "{\\text{radians}", "\\over", "\\text{rotations}}", + "\\right)" + ) + unit_conversion[1:].scale(0.7, about_edge=LEFT) + + top_line = VGroup(ff.target, radians, unit_conversion) + top_line.arrange(RIGHT) + ff.target.align_to(radians, DOWN) + top_line.to_corner(UR, buff=0.4) + + next_line = TexMobject( + "=", "44", "/", "2\\pi", + "\\text{ rotations}" + ) + next_line.next_to(top_line, DOWN, MED_LARGE_BUFF, aligned_edge=LEFT) + + brace = Brace(next_line[1:4], DOWN, buff=SMALL_BUFF) + value = DecimalNumber(44 / TAU, num_decimal_places=8, show_ellipsis=True) + value.next_to(brace, DOWN) + + rect = SurroundingRectangle(VGroup(top_line, value), buff=MED_SMALL_BUFF) + rect.set_stroke(WHITE, 2) + rect.set_fill(DARKER_GREY, 0.9) + + self.play(MoveToTarget(ff)) + top_line.add(ff) + self.play(FadeInFrom(radians, LEFT)) + self.wait() + self.add(rect, top_line, unit_conversion) + self.play( + FadeIn(rect), + FadeIn(unit_conversion), + ) + self.wait() + self.play( + TransformFromCopy(ff, next_line.get_part_by_tex("44")), + FadeIn(next_line.get_part_by_tex("=")), + TransformFromCopy( + unit_conversion.get_part_by_tex("/"), + next_line.get_part_by_tex("/"), + ), + TransformFromCopy( + unit_conversion.get_part_by_tex("rotations"), + next_line.get_part_by_tex("rotations"), + ), + TransformFromCopy( + unit_conversion.get_part_by_tex("2\\pi"), + next_line.get_part_by_tex("2\\pi"), + ), + ) + self.wait() + self.play( + GrowFromCenter(brace), + Write(value), + ) + self.wait() + + self.right_arithmetic = VGroup( + rect, top_line, next_line, + brace, value + ) + + def show_pi_approx_arithmetic(self): + ra = self.right_arithmetic + ra_rect, ra_l1, ra_l2, ra_brace, ra_value = ra + + lines = VGroup( + TexMobject("{44", "\\over", "2\\pi}", "\\approx", "7"), + TexMobject("\\Leftrightarrow"), + TexMobject("{44", "\\over", "7}", "\\approx", "2\\pi"), + TexMobject("{22", "\\over", "7}", "\\approx", "\\pi"), + ) + lines.arrange(RIGHT, buff=MED_SMALL_BUFF) + lines.to_corner(UL) + lines[3].move_to(lines[2], LEFT) + + rect = SurroundingRectangle(lines, buff=MED_LARGE_BUFF) + rect.match_style(ra_rect) + + self.play( + FadeIn(rect), + LaggedStart( + TransformFromCopy(ra_l2[1:4], lines[0][:3]), + FadeIn(lines[0].get_part_by_tex("approx")), + TransformFromCopy(ra_value[0], lines[0].get_part_by_tex("7")), + run_time=2, + ) + ) + self.wait() + l0_copy = lines[0].copy() + self.play( + l0_copy.move_to, lines[2], + Write(lines[1]), + ) + self.play( + LaggedStart(*[ + ReplacementTransform( + l0_copy.get_part_by_tex(tex), + lines[2].get_part_by_tex(tex), + path_arc=60 * DEGREES, + ) + for tex in ["44", "\\over", "7", "approx", "2\\pi"] + ], lag_ratio=0.1, run_time=2), + ) + self.wait() + self.play(Transform(lines[2], lines[3])) + self.wait() + + left_arithmetic = VGroup(rect, lines[:3]) + self.play( + LaggedStart( + FadeOut(self.spiral_group[0]), + FadeOut(left_arithmetic), + FadeOut(self.right_arithmetic), + ) + ) + + def count_by_44(self): + ff_label, ff_line = self.spiral_group[1:] + faded_labels = self.labels + frame = self.camera_frame + + n_values = 100 + mod = 44 + values = range(mod, n_values * mod, mod) + points = [ + self.get_polar_point(n, n) + for n in values + ] + + p2l_tracker = ValueTracker( + get_norm(ff_label.get_bottom() - points[0]) + ) + get_p2l = p2l_tracker.get_value + l_height_ratio_tracker = ValueTracker( + ff_label.get_height() / frame.get_height() + ) + get_l_height_ratio = l_height_ratio_tracker.get_value + + n_labels = 10 + labels = VGroup(*[Integer(n) for n in values[:n_labels]]) + for label, point in zip(labels, points): + label.point = point + label.add_updater( + lambda l: l.set_height( + frame.get_height() * get_l_height_ratio() + ) + ) + label.add_updater( + lambda l: l.move_to( + l.point + get_p2l() * UP, + DOWN, + ) + ) + labels.set_stroke(BLACK, 2, background=True) + + lines = VGroup(ff_line) + for p1, p2 in zip(points, points[1:]): + lines.add(Line(p1, p2)) + lines.match_style(ff_line) + + self.remove(self.spiral_group) + self.remove(faded_labels[44]) + self.play( + frame.scale, self.zoom_factor_1, + p2l_tracker.set_value, 1, + l_height_ratio_tracker.set_value, 0.025, + FadeOut( + ff_label, + rate_func=squish_rate_func(smooth, 0, 1 / 8), + ), + LaggedStart( + *2 * [Animation(Group())], # Weird and dumb + *map(FadeIn, labels), + lag_ratio=0.5, + ), + LaggedStart( + *2 * [Animation(Group())], + *map(ShowCreation, lines[:len(labels)]), + lag_ratio=1, + ), + run_time=8, + ) + self.play( + frame.scale, self.zoom_factor_2, + l_height_ratio_tracker.set_value, 0.01, + ShowCreation(lines[len(labels):]), + run_time=8, + ) + + self.ff_spiral_lines = lines + self.ff_spiral_labels = labels + + # + def arc_func(self, t): + r = 0.1 * t + return r * np.array([np.cos(t), np.sin(t), 0]) + + +class Label44Spirals(Explain44Spirals): + def construct(self): + self.setup_spirals() + self.enumerate_spirals() + + def setup_spirals(self): + max_N = self.max_N + mod = 44 + primes = read_in_primes(max_N) + spirals = VGroup() + for r in range(mod): + ns = range(r, max_N, mod) + spiral = self.get_v_spiral(ns, box_width=1) + for box, n in zip(spiral, ns): + if n in primes: + box.set_color(TEAL) + else: + box.set_color(YELLOW) + spirals.add(spiral) + + self.add(spirals) + scale = np.prod([ + self.initial_scale, + self.zoom_factor_1, + self.zoom_factor_2, + ]) + self.set_scale( + spiral=VGroup(*it.chain(*spirals)), + scale=scale, + run_time=0 + ) + + self.spirals = spirals + + def enumerate_spirals(self): + spirals = self.spirals + labels = self.get_spiral_arm_labels(spirals) + + self.play( + spirals[1:].set_opacity, 0.25, + FadeIn(labels[0]), + ) + self.wait() + + for n in range(10): + arc = Arc( + start_angle=n + 0.2, + angle=0.9, + radius=1.5, + ) + arc.add_tip() + mid_point = arc.point_from_proportion(0.5) + r_label = TextMobject("1 radian") + # r_label.rotate( + # angle_of_vector(arc.get_end() - arc.get_start()) - PI + + # ) + r_label.next_to(mid_point, normalize(mid_point)) + + self.play( + ShowCreation(arc), + FadeIn(r_label), + spirals[n + 1].set_opacity, 1, + TransformFromCopy(labels[n], labels[n + 1]) + ) + self.play( + FadeOut(arc), + FadeOut(r_label), + spirals[n].set_opacity, 0.25, + FadeOut(labels[n]), + ) + + # + def get_spiral_arm_labels(self, spirals, index=15): + mod = 44 + labels = VGroup(*[ + VGroup( + *TexMobject("44k", "+"), + Integer(n) + ).arrange(RIGHT, buff=SMALL_BUFF) + for n in range(mod) + ]) + labels[0][1:].set_opacity(0) + labels.scale(1.5) + labels.set_color(YELLOW) + + for label, spiral in zip(labels, spirals): + box = spiral[index] + vect = rotate_vector(box.get_center(), 90 * DEGREES) + label.next_to(box, normalize(vect), SMALL_BUFF) + labels[0].shift(UR + 1.25 * RIGHT) + return labels + + +class ResidueClassMod44Label(Scene): + def construct(self): + text = TextMobject( + "``Residue class mod 44''" + ) + text.scale(2) + text.to_corner(UL) + + self.play(Write(text)) + self.wait() + + +class EliminateNonPrimativeResidueClassesOf44(Label44Spirals): def construct(self): pass From 8e15846ec571266e1dd677d031e398af84e753ba Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Sep 2019 11:04:35 -0700 Subject: [PATCH 13/21] Tiny clean to spacing --- manimlib/mobject/geometry.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/manimlib/mobject/geometry.py b/manimlib/mobject/geometry.py index a66b3be9..b24e406d 100644 --- a/manimlib/mobject/geometry.py +++ b/manimlib/mobject/geometry.py @@ -202,8 +202,6 @@ class TipableVMobject(VMobject): return get_norm(start - end) - - class Arc(TipableVMobject): CONFIG = { "radius": 1.0, From 4eb6c6fb5b584c573ffe9b724181a9c1a7f10e0e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Sep 2019 11:04:47 -0700 Subject: [PATCH 14/21] Spiral animations Up to ExplainRays --- active_projects/spirals.py | 560 ++++++++++++++++++++++++++++++++++++- 1 file changed, 558 insertions(+), 2 deletions(-) diff --git a/active_projects/spirals.py b/active_projects/spirals.py index 3a53672e..30d58dcb 100644 --- a/active_projects/spirals.py +++ b/active_projects/spirals.py @@ -42,7 +42,7 @@ def read_in_primes(max_N=None): if max_N < 1e5: file = "primes_1e5.json" elif max_N < 1e6: - file = "primes_1e5.json" + file = "primes_1e6.json" else: file = "primes_1e7.json" @@ -119,9 +119,12 @@ class SpiralScene(MovingCameraScene): to_shrink=None, min_box_width=0.05, target_p_spiral_width=None, + added_anims=[], run_time=3): if axes is None: axes = self.axes + if added_anims is None: + added_anims = [] sf = self.get_scale_factor(scale, axes) anims = [] @@ -142,6 +145,7 @@ class SpiralScene(MovingCameraScene): if target_p_spiral_width is not None: mob.target.set_stroke_width(target_p_spiral_width) anims.append(MoveToTarget(mob)) + anims += added_anims if run_time == 0: for anim in anims: @@ -1567,6 +1571,7 @@ class Explain44Spirals(ExplainSixSpirals): "initial_scale": 10, "zoom_factor_1": 7, "zoom_factor_2": 3, + "n_labels": 80, } def construct(self): @@ -1584,7 +1589,7 @@ class Explain44Spirals(ExplainSixSpirals): wholes.set_color(YELLOW) spiral = PGroup(wholes, primes) - labels = self.get_labels(range(80)) + labels = self.get_labels(range(self.n_labels)) self.add(spiral, labels) self.set_scale( @@ -1869,6 +1874,7 @@ class Label44Spirals(Explain44Spirals): ns = range(r, max_N, mod) spiral = self.get_v_spiral(ns, box_width=1) for box, n in zip(spiral, ns): + box.n = n if n in primes: box.set_color(TEAL) else: @@ -1962,5 +1968,555 @@ class ResidueClassMod44Label(Scene): class EliminateNonPrimativeResidueClassesOf44(Label44Spirals): + CONFIG = { + "max_N": 7000, + } + def construct(self): + self.setup_spirals() + self.eliminate_classes() + self.zoom_out() + self.filter_to_primes() + + def eliminate_classes(self): + spirals = self.spirals + labels = self.get_spiral_arm_labels(spirals) + + # Eliminate factors of 2 + self.play( + spirals[1:].set_opacity, 0.5, + FadeIn(labels[0]), + ) + self.wait() + self.play( + FadeOut(spirals[0]), + FadeOut(labels[0]), + ) + for n in range(2, 8, 2): + self.play( + FadeIn(labels[n]), + spirals[n].set_opacity, 1, + ) + self.play(FadeOut(VGroup(labels[n], spirals[n]))) + + words = TextMobject("All even numbers") + words.scale(1.5) + words.to_corner(UL) + self.play( + LaggedStart(*[ + ApplyMethod(spiral.set_opacity, 1) + for spiral in spirals[8::2] + ], lag_ratio=0.01), + FadeIn(words), + ) + self.play( + FadeOut(words), + FadeOut(spirals[8::2]) + ) + self.wait() + + # Eliminate factors of 11 + for k in [11, 33]: + self.play( + spirals[k].set_opacity, 1, + FadeIn(labels[k]) + ) + self.wait() + self.play( + FadeOut(spirals[k]), + FadeOut(labels[k]), + ) + + admissible_spirals = VGroup(*[ + spiral + for n, spiral in enumerate(spirals) + if n % 2 != 0 and n % 11 != 0 + + ]) + + self.play(admissible_spirals.set_opacity, 1) + + self.admissible_spirals = admissible_spirals + + def zoom_out(self): + frame = self.camera_frame + admissible_spirals = self.admissible_spirals + admissible_spirals.generate_target() + for spiral in admissible_spirals.target: + for box in spiral: + box.scale(3) + + self.play( + frame.scale, 4, + MoveToTarget(admissible_spirals), + run_time=3, + ) + + def filter_to_primes(self): + admissible_spirals = self.admissible_spirals + frame = self.camera_frame + primes = read_in_primes(self.max_N) + + to_fade = VGroup(*[ + box + for spiral in admissible_spirals + for box in spiral + if box.n not in primes + ]) + words = TextMobject("Just the primes") + words.set_height(0.1 * frame.get_height()) + words.next_to(frame.get_corner(UL), DR, LARGE_BUFF) + + self.play( + LaggedStartMap(FadeOut, to_fade, lag_ratio=2 / len(to_fade)), + FadeIn(words), + ) + self.wait() + + +class TwoUnrelatedFacts(Scene): + def construct(self): + self.add_title() + self.show_columns() + + def add_title(self): + title = TextMobject("Two (unrelated) bits of number theory") + title.set_width(FRAME_WIDTH - 1) + title.to_edge(UP) + h_line = Line() + h_line.match_width(title) + h_line.next_to(title, DOWN, SMALL_BUFF) + h_line.set_stroke(LIGHT_GREY) + + self.play( + FadeIn(title), + ShowCreation(h_line), + ) + + self.h_line = h_line + + def show_columns(self): + h_line = self.h_line + v_line = Line( + h_line.get_center() + MED_SMALL_BUFF * DOWN, + FRAME_HEIGHT * DOWN / 2, + ) + v_line.match_style(h_line) + + approx = TexMobject( + "{44 \\over 7} \\approx 2\\pi" + ) + approx.scale(1.5) + approx.next_to( + h_line.point_from_proportion(0.25), + DOWN, MED_LARGE_BUFF, + ) + + mod = 44 + n_terms = 9 + residue_classes = VGroup() + prime_numbers = read_in_primes(1000) + primes = VGroup() + non_primes = VGroup() + for r in range(mod): + if r <= 11 or r == 43: + row = VGroup() + for n in range(r, r + n_terms * mod, mod): + elem = Integer(n) + comma = TexMobject(",") + comma.next_to( + elem.get_corner(DR), + RIGHT, SMALL_BUFF + ) + elem.add(comma) + row.add(elem) + if n in prime_numbers: + primes.add(elem) + else: + non_primes.add(elem) + row.arrange(RIGHT, buff=0.3) + dots = TexMobject("\\dots") + dots.next_to(row.get_corner(DR), RIGHT, SMALL_BUFF) + dots.shift(SMALL_BUFF * UP) + row.add(dots) + row.r = r + if r == 12: + row = TexMobject("\\vdots") + residue_classes.add(row) + + residue_classes.arrange(DOWN) + residue_classes[-2].align_to(residue_classes, LEFT) + residue_classes[-2].shift(MED_SMALL_BUFF * RIGHT) + residue_classes.set_height(6) + residue_classes.next_to(ORIGIN, RIGHT) + residue_classes.to_edge(DOWN, buff=MED_SMALL_BUFF) + + def get_line(row): + return Line( + row.get_left(), row.get_right(), + stroke_color=RED, + stroke_width=4, + ) + + even_lines = VGroup(*[ + get_line(row) + for row in residue_classes[:12:2] + ]) + eleven_line = get_line(residue_classes[11]) + eleven_line.set_color(PINK) + for line in [even_lines[1], eleven_line]: + line.scale(0.93, about_edge=RIGHT) + + self.play(ShowCreation(v_line)) + self.wait() + self.play(FadeInFrom(approx, DOWN)) + self.wait() + self.play(FadeIn(residue_classes)) + self.wait() + self.play( + LaggedStartMap(ShowCreation, even_lines), + ) + self.wait() + self.play(ShowCreation(eleven_line)) + self.wait() + self.play( + primes.set_color, TEAL, + non_primes.set_opacity, 0.25, + even_lines.set_opacity, 0.25, + eleven_line.set_opacity, 0.25, + ) + self.wait() + + +class ExplainRays(Explain44Spirals): + CONFIG = { + "max_N": int(5e5), + "axes_config": { + "x_min": -1000, + "x_max": 1000, + "y_min": -1000, + "y_max": 1000, + "number_line_config": { + "tick_frequency": 50, + }, + }, + } + + def construct(self): + self.add_spirals_and_labels() + self.show_710th_point() + self.show_arithmetic() + self.zoom_and_count() + + def show_710th_point(self): + spiral = self.spiral + axes = self.axes + labels = self.labels + + scale_factor = 12 + + fade_rect = FullScreenFadeRectangle() + fade_rect.scale(scale_factor) + + new_ns = list(range(711)) + bright_boxes = self.get_v_spiral(new_ns) + bright_boxes.set_color(YELLOW) + for n, box in enumerate(bright_boxes): + box.set_height(0.02 * np.sqrt(n)) + + big_labels = self.get_labels(new_ns) + + index_tracker = ValueTracker(44) + + labeled_box = VGroup(Square(), Integer(0)) + + def update_labeled_box(mob): + index = int(index_tracker.get_value()) + labeled_box[0].become(bright_boxes[index]) + labeled_box[1].become(big_labels[index]) + + labeled_box.add_updater(update_labeled_box) + + self.set_scale( + scale=120, + spiral=spiral, + to_shrink=labels, + ) + + box_710 = self.get_v_spiral([710])[0] + box_710.scale(2) + box_710.set_color(YELLOW) + label_710 = Integer(710) + label_710.scale(1.5) + label_710.next_to(box_710, UP) + + arrow = Arrow( + ORIGIN, DOWN, + stroke_width=6, + max_tip_length_to_length_ratio=0.35, + max_stroke_width_to_length_ratio=10, + tip_length=0.35 + ) + arrow.match_color(box_710) + arrow.next_to(box_710, UP, SMALL_BUFF) + label_710.next_to(arrow, UP, SMALL_BUFF) + + self.add(spiral, fade_rect, axes, labels) + self.play( + FadeIn(fade_rect), + FadeOut(labels), + FadeInFromLarge(box_710), + FadeInFrom(label_710, DOWN), + ShowCreation(arrow), + ) + self.wait() + + self.fade_rect = fade_rect + self.box_710 = box_710 + self.label_710 = label_710 + self.arrow = arrow + + def show_arithmetic(self): + label_710 = self.label_710 + + equation = TexMobject( + "710", "\\text{ radians}", "=", + "(710 / 2\\pi)", "\\text{ rotations}", + ) + equation.to_corner(UL) + frac = equation.get_part_by_tex("710 / 2\\pi") + brace = Brace(frac, DOWN, buff=SMALL_BUFF) + value = TextMobject("{:.15}".format(710 / TAU)) + value.next_to(brace, DOWN, SMALL_BUFF) + values = VGroup(*[ + value[0][:n].deepcopy().next_to(brace, DOWN, SMALL_BUFF) + for n in [3, *range(5, 13)] + ]) + + group = VGroup(equation, brace, value) + rect = SurroundingRectangle(group, buff=MED_SMALL_BUFF) + rect.set_stroke(WHITE, 2) + rect.set_fill(DARK_GREY, 1) + + approx = TexMobject( + "{710", "\\over", "113}", + "\\approx", "2\\pi", + ) + approx.next_to(rect, DOWN) + approx.align_to(equation, LEFT) + + approx2 = TexMobject( + "{355", "\\over", "113}", + "\\approx", "\\pi", + ) + approx2.next_to(approx, RIGHT, LARGE_BUFF) + + self.play( + FadeIn(rect), + TransformFromCopy(label_710, equation[0]), + FadeIn(equation[1:3]), + ) + self.play( + FadeInFrom(equation[3:], LEFT) + ) + self.play(GrowFromCenter(brace)) + self.play( + ShowSubmobjectsOneByOne(values), + run_time=3, + rate_func=linear, + ) + self.wait() + self.play( + rect.stretch, 2, 1, {"about_edge": UP}, + LaggedStart( + TransformFromCopy( # 710 + equation[3][1:4], + approx[0], + ), + FadeIn(approx[1][0]), + TransformFromCopy( # 113 + values[-1][:3], + approx[2], + ), + FadeIn(approx[3]), + TransformFromCopy( # 2pi + equation[3][5:7], + approx[4], + ), + run_time=2, + ) + ) + self.wait() + self.play( + TransformFromCopy(approx, approx2), + ) + self.wait() + self.play( + FadeOut(VGroup( + rect, equation, brace, values[-1], + approx, approx2 + )), + self.fade_rect.set_opacity, 0.25, + ) + + def zoom_and_count(self): + label = self.label_710 + arrow = self.arrow + box = self.box_710 + axes = self.axes + spiral = self.spiral + + times = TexMobject("\\times") + times.next_to(label, LEFT, SMALL_BUFF) + k_label = Integer(1) + k_label.match_height(label) + k_label.set_color(YELLOW) + k_label.next_to(times, LEFT) + + boxes = VGroup(*[box.copy() for x in range(150)]) + box_height_tracker = ValueTracker(box.get_height()) + + def get_k(): + max_x = axes.x_axis.p2n(label.get_center()) + return max(1, int(max_x / 710)) + + def get_k_point(k): + return self.get_polar_point(710 * k, 710 * k) + + def update_arrow(arrow): + point = get_k_point(get_k()) + arrow.put_start_and_end_on( + label.get_bottom() + SMALL_BUFF * DOWN, + point + SMALL_BUFF * UP + ) + + def get_unit(): + return get_norm(axes.c2p(1, 0) - axes.c2p(0, 0)) + + def update_boxes(boxes): + box_height = box_height_tracker.get_value() + for k, box in enumerate(boxes): + box.set_height(box_height) + box.move_to(get_k_point(k)) + + arrow.add_updater(update_arrow) + boxes.add_updater(update_boxes) + k_label.add_updater( + lambda d: d.set_value(get_k()).next_to( + times, LEFT, SMALL_BUFF + ) + ) + + self.remove(box) + self.add(times, k_label, boxes) + self.set_scale( + scale=10000, + spiral=self.spiral, + run_time=8, + target_p_spiral_width=2, + added_anims=[ + box_height_tracker.set_value, 0.035, + ] + ) + self.wait() + + # Show other residue classes + new_label = TexMobject( + "710", "k", "+", + tex_to_color_map={"k": YELLOW} + ) + new_label.match_height(label) + new_label.next_to(boxes, UP, SMALL_BUFF) + new_label.to_edge(RIGHT) + new_label[2].set_opacity(0) + + r_label = Integer(1) + r_label.match_height(new_label) + r_label.set_opacity(0) + r_label.add_updater( + lambda m: m.next_to(new_label, RIGHT, SMALL_BUFF) + ) + + k_label.clear_updaters() + self.play( + FadeOut(times), + ReplacementTransform(label, new_label[0]), + ReplacementTransform(k_label, new_label[1]), + FadeOut(arrow) + ) + + boxes.clear_updaters() + for r in range(1, 12): + if r in [3, 6]: + vect = UR + else: + vect = RIGHT + point = rotate_vector(boxes[40].get_center(), 1) + new_boxes = boxes.copy() + new_boxes.rotate(1, about_point=ORIGIN) + for box in new_boxes: + box.rotate(-1) + self.play( + FadeOut(boxes), + LaggedStartMap(FadeIn, new_boxes, lag_ratio=0.01), + new_label.set_opacity, 1, + new_label.next_to, point, vect, + r_label.set_opacity, 1, + ChangeDecimalToValue(r_label, r), + run_time=1, + ) + self.remove(boxes) + boxes = new_boxes + self.add(boxes) + self.wait() + + # Show just the primes + self.play( + FadeOut(boxes), + FadeOut(new_label), + FadeOut(r_label), + FadeOut(self.fade_rect) + ) + self.set_scale( + 30000, + spiral=spiral, + run_time=4, + ) + self.wait() + self.remove(spiral) + self.add(spiral[1]) + self.wait() + + +class CompareTauToApprox(Scene): + def construct(self): + eqs = VGroup( + TexMobject("2\\pi", "=", "{:.10}\\dots".format(TAU)), + TexMobject("\\frac{710}{113}", "=", "{:.10}\\dots".format(710 / 113)), + ) + eqs.arrange(DOWN, buff=LARGE_BUFF) + eqs[1].shift((eqs[0][2].get_left()[0] - eqs[1][2].get_left()[0]) * RIGHT) + + eqs.generate_target() + for eq in eqs.target: + eq[2][:8].set_color(GREEN) + + self.play(LaggedStart( + FadeInFrom(eqs[0], DOWN), + FadeInFrom(eqs[1], UP), + )) + self.play(MoveToTarget(eqs)) + self.wait() + + +class ShowClassesOfPrimeRays(SpiralScene): + def construct(self): + self.setup_rays() + self.show_classes() + + def setup_rays(self): pass + + def show_classes(self): + pass + From c94aef88451cc400bf88091b2ee44e275a4f1515 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 30 Sep 2019 12:06:17 -0700 Subject: [PATCH 15/21] Have AnimationGroup initialize its runtime before begin is called. This way it can be composed --- manimlib/animation/composition.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/animation/composition.py b/manimlib/animation/composition.py index 043765bf..753f5111 100644 --- a/manimlib/animation/composition.py +++ b/manimlib/animation/composition.py @@ -33,6 +33,7 @@ class AnimationGroup(Animation): self.group = Group(*remove_list_redundancies( [anim.mobject for anim in animations] )) + self.init_run_time() Animation.__init__(self, self.group, **kwargs) def get_all_mobjects(self): @@ -41,7 +42,7 @@ class AnimationGroup(Animation): def begin(self): for anim in self.animations: anim.begin() - self.init_run_time() + # self.init_run_time() def finish(self): for anim in self.animations: From 67f3d1584cd1ecd32fccfa8d08ab59e93c0a924d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 30 Sep 2019 12:06:35 -0700 Subject: [PATCH 16/21] A few filler scenes for spirals project --- active_projects/spirals.py | 548 ++++++++++++++++++++++++++++++++----- 1 file changed, 483 insertions(+), 65 deletions(-) diff --git a/active_projects/spirals.py b/active_projects/spirals.py index 30d58dcb..492087c1 100644 --- a/active_projects/spirals.py +++ b/active_projects/spirals.py @@ -8,6 +8,15 @@ INV_113_MOD_710 = 377 # Inverse of 113 mode 710 INV_7_MOD_44 = 19 +def is_prime(n): + if n < 2: + return False + for k in range(2, int(np.sqrt(n)) + 1): + if n % k == 0: + return False + return True + + def generate_prime_list(*args): if len(args) == 1: start, stop = 2, args[0] @@ -17,15 +26,10 @@ def generate_prime_list(*args): else: raise TypeError("generate_prime_list takes 1 or 2 arguments") - result = [] - for n in range(start, stop): - include = True - for k in range(2, int(np.sqrt(n)) + 1): - if n % k == 0: - include = False - break - if include: - result.append(n) + result = [ + n for n in range(start, stop) + if is_prime(n) + ] return result @@ -189,6 +193,77 @@ class SpiralScene(MovingCameraScene): # Scenes +class MathExchange(ExternallyAnimatedScene): + pass + + +class PrimesAndPi(Scene): + def construct(self): + self.show_primes() + self.show_rational_approximations() + + def show_primes(self): + n_rows = 10 + n_cols = 10 + matrix = IntegerMatrix([ + [n_cols * x + y for y in range(n_cols)] + for x in range(n_rows) + ]) + numbers = matrix.get_entries() + primes = VGroup(*filter( + lambda m: is_prime(m.get_value()), + numbers, + )) + non_primes = VGroup(*filter( + lambda m: not is_prime(m.get_value()), + numbers + )) + + self.add(numbers) + + self.play( + LaggedStart(*[ + ApplyFunction( + lambda m: m.set_color(YELLOW).scale(1.2), + prime + ) + for prime in primes + ]), + non_primes.set_opacity, 0.25, + run_time=2, + ) + self.wait() + + self.numbers = numbers + + def show_rational_approximations(self): + numbers = self.numbers + + approxs = TexMobject( + "{22 \\over 7} &=", "{:.12}\\dots\\\\".format(22 / 7), + "{355 \\over 113} &=", "{:.12}\\dots\\\\".format(355 / 113), + "\\pi &=", "{:.12}\\dots\\\\".format(PI), + ) + approxs[:2].shift(MED_LARGE_BUFF * UP) + approxs[-2:].shift(MED_LARGE_BUFF * DOWN) + approxs[-2:].set_color(YELLOW) + approxs[1][:4].set_color(YELLOW) + approxs[3][:8].set_color(YELLOW) + approxs.scale(1.5) + + randy = Randolph(color=YELLOW, height=1) + randy.move_to(approxs[-2][0], RIGHT) + approxs[-2][0].set_opacity(0) + + self.play( + LaggedStartMap(FadeOutAndShiftDown, numbers), + LaggedStartMap(FadeIn, approxs), + FadeIn(randy) + ) + self.play(Blink(randy)) + self.play(randy.change, "pondering", UR) + self.wait() + class RefresherOnPolarCoordinates(MovingCameraScene): CONFIG = { @@ -391,12 +466,15 @@ class RefresherOnPolarCoordinates(MovingCameraScene): r_line.add_updater( lambda l: l.put_start_and_end_on(ORIGIN, dot.get_center()) ) + theta_tracker = ValueTracker(0) + theta_tracker.add_updater( + lambda m: m.set_value(r_line.get_angle() % TAU) + ) + self.add(theta_tracker) arc.add_updater( - lambda m: m.become(Arc( - angle=(r_line.get_angle() % TAU), - color=theta_color, - radius=1, - )) + lambda m: m.become( + self.get_arc(theta_tracker.get_value()) + ) ) self.add(coord_label) @@ -408,16 +486,25 @@ class RefresherOnPolarCoordinates(MovingCameraScene): self.wait() self.play( FadeOut(coord_label), - FadeOut(r_line), - FadeOut(arc), FadeOut(r_label), FadeOut(theta_label), FadeOut(radians_word), + FadeOut(r_line), + FadeOut(arc), FadeOut(dot), ) + self.dot = dot + self.r_line = r_line + self.arc = arc + self.theta_tracker = theta_tracker + def show_all_nn_tuples(self): - self.remove(self.dot) + dot = self.dot + arc = self.arc + r_line = self.r_line + theta_tracker = self.theta_tracker + primes = generate_prime_list(20) non_primes = list(range(1, 20)) for prime in primes: @@ -446,11 +533,15 @@ class RefresherOnPolarCoordinates(MovingCameraScene): # Talk about 1 one = np_points[0] - r_line = Line(ORIGIN, one.dot.get_center()) - r_line.set_color(self.r_color) + dot.move_to(self.get_polar_point(1, 1)) + self.add(dot) + theta_tracker.clear_updaters() + theta_tracker.set_value(1) + # r_line = Line(ORIGIN, one.dot.get_center()) + # r_line.set_color(self.r_color) # pre_arc = Line(RIGHT, UR, color=self.r_color) - theta_tracker = ValueTracker(1) - arc = always_redraw(lambda: self.get_arc(theta_tracker.get_value())) + # theta_tracker = ValueTracker(1) + # arc = always_redraw(lambda: self.get_arc(theta_tracker.get_value())) one_rect = SurroundingRectangle(one) one_r_rect = SurroundingRectangle(one.label[1]) @@ -467,43 +558,37 @@ class RefresherOnPolarCoordinates(MovingCameraScene): # self.play(TransformFromCopy(r_line, pre_arc)) # self.add(pre_arc, one) self.play( - TransformFromCopy(r_line, arc), + ReplacementTransform( + Line(*r_line.get_start_and_end()), arc + ), ReplacementTransform(one_r_rect, one_theta_rect) ) self.add(arc, one, one_theta_rect) self.play(FadeOut(one_theta_rect)) self.wait() - # Talk about 2 - self.play(theta_tracker.set_value, 2) - self.wait() - self.play(Rotate(r_line, angle=1, about_point=ORIGIN)) - self.play(r_line.scale, 2, {'about_point': ORIGIN}) - self.wait() - - # And now 3 - self.play( - theta_tracker.set_value, 3, - Rotate(r_line, angle=1, about_point=ORIGIN), - ) - self.wait() - self.play( - r_line.scale, 3 / 2, {"about_point": ORIGIN} - ) - self.wait() - - # Finally 4 - self.play( - theta_tracker.set_value, 4, - Rotate(r_line, angle=1, about_point=ORIGIN), - ) - self.wait() - self.play( - r_line.scale, 4 / 3, {"about_point": ORIGIN} - ) - self.wait() + # Talk about 2, 3 then 4 + for n in [2, 3, 4]: + self.play( + Rotate(dot, 1, about_point=ORIGIN), + theta_tracker.set_value, n, + ) + self.wait() + self.play(dot.move_to, self.get_polar_point(n, n)) + self.wait() # Zoom out and show spiral + big_anim = Succession(*3 * [Animation(Mobject())], *it.chain(*[ + [ + AnimationGroup( + Rotate(dot, 1, about_point=ORIGIN), + ApplyMethod(theta_tracker.set_value, n), + ), + ApplyMethod(dot.move_to, self.get_polar_point(n, n)) + ] + for n in [5, 6, 7, 8, 9] + ])) + spiral = ParametricFunction( lambda t: self.get_polar_point(t, t), t_min=0, @@ -511,7 +596,7 @@ class RefresherOnPolarCoordinates(MovingCameraScene): stroke_width=1.5, ) - self.add(spiral, pp_points, np_points) + # self.add(spiral, pp_points, np_points) self.polar_grid.generate_target() for mob in self.polar_grid: @@ -519,18 +604,24 @@ class RefresherOnPolarCoordinates(MovingCameraScene): mob.set_stroke(width=1) self.play( - ApplyMethod( - frame.scale, 3, - run_time=5, - rate_func=lambda t: smooth(t, 2) - ), + frame.scale, 3, + big_anim, + run_time=10, + ) + self.play( + # ApplyMethod( + # frame.scale, 1.5, + # run_time=2, + # rate_func=lambda t: smooth(t, 2) + # ), ShowCreation( spiral, - run_time=6, + run_time=4, ), FadeOut(r_line), FadeOut(arc), - MoveToTarget(self.polar_grid) + FadeOut(dot), + # MoveToTarget(self.polar_grid) ) self.wait() @@ -621,11 +712,19 @@ class RefresherOnPolarCoordinates(MovingCameraScene): def get_arc(self, theta, r=1, color=None): if color is None: color = self.theta_color - return Arc( - angle=theta, - radius=r, - stroke_color=color, + return ParametricFunction( + lambda t: self.get_polar_point(1 + 0.025 * t, t), + t_min=0, + t_max=theta, + dt=0.25, + color=color, + stroke_width=3, ) + # return Arc( + # angle=theta, + # radius=r, + # stroke_color=color, + # ) class ReplacePolarCoordinatesWithPrimes(RefresherOnPolarCoordinates): @@ -887,7 +986,7 @@ class AskAboutRelationToPrimes(TeacherStudentsScene): q_marks = TextMobject("???") q_marks.set_color(YELLOW) - group = VGroup(numbers, arrow, primes) + group = VGroup(primes, arrow, numbers) group.arrange(RIGHT) q_marks.next_to(arrow, UP) group.add(q_marks) @@ -958,6 +1057,94 @@ class ThickZoomOutOnPrimesWithNumbers(ZoomOutOnPrimesWithNumbers): } +class HighlightGapsInSpirals(IntroducePrimePatterns): + def construct(self): + self.setup_spiral() + + max_n_tracker = ValueTracker(0) + get_max_n = max_n_tracker.get_value + gaps = always_redraw(lambda: VGroup(*[ + self.get_highlighted_gap(n - 1, n + 1, get_max_n()) + for n in [11, 33] + ])) + + self.add(gaps) + self.play(max_n_tracker.set_value, 25000, run_time=5) + gaps.clear_updaters() + self.play(FadeOut(gaps)) + + def setup_spiral(self): + p_spiral = self.get_p_spiral(read_in_primes(self.small_n_primes)) + self.add(p_spiral) + self.set_scale( + scale=self.spiral_scale, + spiral=p_spiral, + target_p_spiral_width=5, + run_time=0, + ) + + def get_highlighted_gap(self, n1, n2, max_n): + l1, l2 = [ + [ + self.get_polar_point(k, k) + for k in range(INV_7_MOD_44 * n, int(max_n), 5 * 44) + ] + for n in (n1, n2) + ] + + if len(l1) == 0 or len(l2) == 0: + return VectorizedPoint() + + result = VMobject() + result.set_points_as_corners( + [*l1, *reversed(l2)] + ) + result.make_smooth() + + result.set_stroke(GREY, width=0) + result.set_fill(DARK_GREY, 1) + + return result + + +class ImagineYouFoundIt(TeacherStudentsScene): + def construct(self): + you = self.students[1] + others = VGroup( + self.students[0], + self.students[2], + self.teacher, + ) + bubble = you.get_bubble(direction=LEFT) + + you_label = TextMobject("You") + arrow = Vector(DOWN) + arrow.next_to(you, UP) + you_label.next_to(arrow, UP) + + self.play( + you.change, "hesitant", you_label, + FadeInFromDown(you_label), + GrowArrow(arrow), + others.set_opacity, 0.25, + ) + self.play(Blink(you)) + self.play( + ShowCreation(bubble), + FadeOut(you_label), + FadeOut(arrow), + you.change, "pondering", + ) + self.play(you.look_at, bubble.get_corner(UR)) + self.play(Blink(you)) + self.wait(3) + self.play(you.change, "hooray") + self.play(Blink(you)) + self.wait(2) + self.play(you.change, "sassy", bubble.get_top()) + self.wait(3) + + class ShowSpiralsForWholeNumbers(CountSpirals): CONFIG = { "max_prime": 10000, @@ -1061,6 +1248,50 @@ class ShowSpiralsForWholeNumbers(CountSpirals): ) +class SeparateIntoTwoQuestions(Scene): + def construct(self): + top_q = TextMobject("Why do", " primes", " cause", " spirals", "?") + top_q.scale(2) + top_q.to_edge(UP) + + q1 = TextMobject("Where do the\\\\", "spirals", " come from?") + q2 = TextMobject("What happens when\\\\", "filtering to", " primes", "?") + for q in q1, q2: + q.scale(1.3) + q.next_to(top_q, DOWN, LARGE_BUFF) + q1.to_edge(LEFT) + q1.set_color(YELLOW) + q2.to_edge(RIGHT) + q2.set_color(TEAL) + + v_line = DashedLine( + top_q.get_bottom() + MED_SMALL_BUFF * DOWN, + FRAME_HEIGHT * DOWN / 2, + ) + + self.add(top_q) + self.wait() + + for q, text in [(q1, "spirals"), (q2, "primes")]: + self.play( + top_q.get_part_by_tex(text).set_color, q.get_color(), + TransformFromCopy( + top_q.get_part_by_tex(text), + q.get_part_by_tex(text), + ), + LaggedStartMap( + FadeIn, + filter( + lambda m: m is not q.get_part_by_tex(text), + q, + ) + ), + ) + self.wait() + self.play(ShowCreation(v_line)) + self.wait() + + class ExplainSixSpirals(ShowSpiralsForWholeNumbers): CONFIG = { "max_N": 150, @@ -2510,13 +2741,200 @@ class CompareTauToApprox(Scene): class ShowClassesOfPrimeRays(SpiralScene): + CONFIG = { + "max_N": int(1e6), + "scale": 1e5 + } + def construct(self): self.setup_rays() self.show_classes() def setup_rays(self): - pass + spiral = self.get_prime_p_spiral(self.max_N) + self.add(spiral) + self.set_scale( + scale=self.scale, + spiral=spiral, + target_p_spiral_width=3, + run_time=0 + ) def show_classes(self): - pass + max_N = self.max_N + mod = 710 + primes = read_in_primes(max_N) + rect = FullScreenFadeRectangle() + rect.set_opacity(0) + self.add(rect) + + last_ray = PMobject() + last_label = VGroup(*[VectorizedPoint() for x in range(3)]) + for i in range(40): + if get_gcd(i, mod) != 1: + continue + + r = (INV_113_MOD_710 * i) % mod + + sequence = filter( + lambda x: x in primes, + range(r, max_N, mod) + ) + ray = self.get_v_spiral(sequence, box_width=0.03) + ray.set_color(GREEN) + ray.set_opacity(0.9) + + label = VGroup( + *TexMobject("710k", "+"), + Integer(r) + ) + label.arrange(RIGHT, buff=SMALL_BUFF) + label.next_to(ray[100], UL, SMALL_BUFF) + + label[2].save_state() + label[2].set_opacity(0) + label[2].move_to(last_label, RIGHT) + + self.play( + rect.set_opacity, 0.5, + ShowCreation(ray), + LaggedStartMap(FadeOut, last_ray), + ReplacementTransform(last_label[:2], label[:2]), + Restore(label[2]), + last_label[2].move_to, label[2].saved_state, + last_label[2].set_opacity, 0, + ) + self.remove(last_label) + self.add(label) + self.wait() + + last_ray = ray + last_label = label + + +class ShowFactorsOf710(Scene): + def construct(self): + equation = TexMobject( + "710", "=", + "71", "\\cdot", + "5", "\\cdot", + "2", + ) + equation.scale(1.5) + equation.to_corner(UL) + ten = TexMobject("10") + ten.match_height(equation) + ten.move_to(equation.get_part_by_tex("5"), LEFT) + + self.add(equation[0]) + self.wait() + self.play( + TransformFromCopy( + equation[0][:2], + equation[2], + ), + FadeIn(equation[1]), + FadeIn(equation[3]), + TransformFromCopy( + equation[0][2:], + ten, + ) + ) + self.wait() + self.remove(ten) + self.play(*[ + TransformFromCopy(ten, mob) + for mob in equation[4:] + ]) + self.wait() + + # Circle factors + colors = [RED, BLUE, PINK] + for factor, color in zip(equation[:-6:-2], colors): + rect = SurroundingRectangle(factor) + rect.set_color(color) + self.play(ShowCreation(rect)) + self.wait() + self.play(FadeOut(rect)) + + +class LookAtRemainderMod710(Scene): + def construct(self): + t2c = { + "n": YELLOW, + "r": GREEN + } + equation = TexMobject( + "n", "=", "710", "\\cdot", "q", "+", "r", + tex_to_color_map=t2c, + ) + equation.scale(1.5) + + n_arrow = Vector(UP).next_to(equation.get_part_by_tex("n"), DOWN) + r_arrow = Vector(UP).next_to(equation.get_part_by_tex("r"), DOWN) + n_arrow.set_color(t2c["n"]) + r_arrow.set_color(t2c["r"]) + + n_label = TextMobject("Some\\\\number") + r_label = TextMobject("Remainder") + VGroup(n_label, r_label).scale(1.5) + n_label.next_to(n_arrow, DOWN) + n_label.match_color(n_arrow) + r_label.next_to(r_arrow, DOWN) + r_label.match_color(r_arrow) + + self.add(equation) + self.play( + FadeInFrom(n_label, UP), + ShowCreation(n_arrow), + ) + self.wait() + self.play( + FadeInFrom(r_label, DOWN), + ShowCreation(r_arrow), + ) + self.wait() + + +class EliminateNonPrimative710Residues(ShowClassesOfPrimeRays): + def construct(self): + self.setup_rays() + self.eliminate_classes() + + def setup_rays(self): + mod = 710 + rays = PGroup(*[ + self.get_p_spiral(range(r, self.max_N, mod)) + for r in range(mod) + ]) + rays.set_color(YELLOW) + self.add(rays) + self.set_scale( + scale=self.scale, + spiral=rays, + target_p_spiral_width=1, + run_time=0, + ) + self.wait() + + self.rays = rays + + def eliminate_classes(self): + rays = self.rays + rect = FullScreenFadeRectangle() + rect.set_opacity(0) + + mod = 710 + mult2 = PGroup(*[rays[i] for i in range(0, mod, 2)]) + mult5 = PGroup(*[rays[i] for i in range(0, mod, 5) if i % 2 != 0]) + mult71 = PGroup(*[rays[i] for i in range(0, mod, 71) if (i % 2 != 0 and i % 5 != 0)]) + groups = Group(mult2, mult5, mult71) + colors = [RED, BLUE, PINK] + + for group, color in zip(groups, colors): + group.set_stroke_width(2) + self.add(rect, group) + self.wait() + self.remove(rect, group) + self.wait() From f66d6e4f972852980645933087e1a11f72a06a01 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 3 Oct 2019 13:58:00 -0700 Subject: [PATCH 17/21] Added AddTextWordByWord, but it's not yet working, presumably because of some bug in Succession. --- manimlib/animation/creation.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/manimlib/animation/creation.py b/manimlib/animation/creation.py index dee61497..a7382653 100644 --- a/manimlib/animation/creation.py +++ b/manimlib/animation/creation.py @@ -1,4 +1,5 @@ from manimlib.animation.animation import Animation +from manimlib.animation.composition import Succession from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.mobject.mobject import Group from manimlib.utils.bezier import integer_interpolate @@ -8,6 +9,7 @@ from manimlib.utils.rate_functions import double_smooth from manimlib.utils.rate_functions import smooth import numpy as np +import itertools as it class ShowPartial(Animation): @@ -150,3 +152,25 @@ class ShowSubmobjectsOneByOne(ShowIncreasingSubsets): self.mobject.submobjects = [] else: self.mobject.submobjects = self.all_submobs[index - 1] + + +# TODO, this is broken... +class AddTextWordByWord(Succession): + CONFIG = { + # If given a value for run_time, it will + # override the time_per_char + "run_time": None, + "time_per_char": 0.06, + } + + def __init__(self, text_mobject, **kwargs): + digest_config(self, kwargs) + tpc = self.time_per_char + anims = it.chain(*[ + [ + ShowIncreasingSubsets(word, run_time=tpc * len(word)), + Animation(word, run_time=0.005 * len(word)**1.5), + ] + for word in text_mobject + ]) + super().__init__(*anims, **kwargs) From ccc51664f4c0536ef85568433dac51aac09b4d41 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 3 Oct 2019 13:58:13 -0700 Subject: [PATCH 18/21] Still being dumb --- stage_scenes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stage_scenes.py b/stage_scenes.py index b58ea342..4eb90ccd 100644 --- a/stage_scenes.py +++ b/stage_scenes.py @@ -45,7 +45,7 @@ def stage_scenes(module_name): animation_dir = os.path.join( os.path.expanduser('~'), "Dropbox (3Blue1Brown)/3Blue1Brown Team Folder/videos", - "windmill", "1440p60" + "spirals", "1440p60" ) # files = os.listdir(animation_dir) From d80af647989c2e54a0491907830f01d0205f3b0a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 3 Oct 2019 13:58:27 -0700 Subject: [PATCH 19/21] Many more scenes from the prime spirals video --- active_projects/spirals.py | 620 +++++++++++++++++++++++++++++++++++-- 1 file changed, 601 insertions(+), 19 deletions(-) diff --git a/active_projects/spirals.py b/active_projects/spirals.py index 492087c1..6815a79c 100644 --- a/active_projects/spirals.py +++ b/active_projects/spirals.py @@ -193,6 +193,64 @@ class SpiralScene(MovingCameraScene): # Scenes +class AltTitle(Scene): + def construct(self): + title_text = """ + How pretty but pointless patterns\\\\ + in polar plots of primes\\\\ + prompt pretty important ponderings\\\\ + on properties of those primes. + """ + words = [w + " " for w in title_text.split(" ") if w] + title = TextMobject(*words) + title.set_width(FRAME_WIDTH - 1) + + title[2:5].set_color(TEAL) + title[12:15].set_color(YELLOW) + title.set_stroke(BLACK, 5, background=True) + + image = ImageMobject("PrimeSpiral") + image.set_height(FRAME_HEIGHT) + rect = FullScreenFadeRectangle(fill_opacity=0.25) + + self.add(image, rect) + + for word in title: + self.play( + FadeIn( + word, run_time=0.05 * len(word), + lag_ratio=0.4, + ) + ) + self.wait() + + +class HoldUpMathExchange(TeacherStudentsScene): + def construct(self): + title = TextMobject("Mathematics Stack Exchange") + title.scale(1.5) + title.to_edge(UP) + + self.add(title) + self.play(self.teacher.change, "raise_right_hand", ORIGIN), + self.change_all_student_modes("thinking", look_at_arg=ORIGIN) + self.wait(3) + self.change_all_student_modes("confused", look_at_arg=ORIGIN) + self.wait(3) + + +class MathExchangeNames(Scene): + def construct(self): + names = VGroup( + TextMobject("dwymark"), + TextMobject("Greg Martin"), + ) + names.arrange(DOWN, buff=1) + for name in names: + self.play(FadeInFrom(name, RIGHT)) + self.wait() + + class MathExchange(ExternallyAnimatedScene): pass @@ -224,7 +282,7 @@ class PrimesAndPi(Scene): self.play( LaggedStart(*[ ApplyFunction( - lambda m: m.set_color(YELLOW).scale(1.2), + lambda m: m.set_color(TEAL).scale(1.2), prime ) for prime in primes @@ -727,6 +785,22 @@ class RefresherOnPolarCoordinates(MovingCameraScene): # ) +class IntroducePolarPlot(RefresherOnPolarCoordinates): + def construct(self): + self.plane = NumberPlane() + grid = self.get_polar_grid() + title = TextMobject("Polar coordinates") + title.scale(3) + title.set_stroke(BLACK, 10, background=True) + title.to_edge(UP) + + self.add(grid, title) + self.play( + ShowCreation(grid, lag_ratio=0.1), + run_time=3, + ) + + class ReplacePolarCoordinatesWithPrimes(RefresherOnPolarCoordinates): def construct(self): coords, p_coords = [ @@ -742,10 +816,10 @@ class ReplacePolarCoordinatesWithPrimes(RefresherOnPolarCoordinates): some_prime = TextMobject("Some prime") some_prime.scale(1.5) - some_prime.next_to(p_coords, UP, buff=1.5) + some_prime.next_to(p_coords.get_left(), DOWN, buff=1.5) arrows = VGroup(*[ Arrow( - some_prime.get_bottom(), coord.get_top(), + some_prime.get_top(), coord.get_bottom(), stroke_width=5, tip_length=0.4 ) @@ -859,7 +933,7 @@ class AskWhat(TeacherStudentsScene): def construct(self): screen = self.screen self.student_says( - "I'm sory,\\\\what?!?", + "I'm sorry,\\\\what?!?", target_mode="angry", look_at_arg=screen, student_index=2, @@ -1079,7 +1153,7 @@ class HighlightGapsInSpirals(IntroducePrimePatterns): self.set_scale( scale=self.spiral_scale, spiral=p_spiral, - target_p_spiral_width=5, + target_p_spiral_width=8, run_time=0, ) @@ -1107,6 +1181,58 @@ class HighlightGapsInSpirals(IntroducePrimePatterns): return result +class QuestionIsMisleading(TeacherStudentsScene): + def construct(self): + self.student_says( + "Whoa, is this some\\\\divine hidden structure\\\\in the primes?", + target_mode="surprised", + student_index=0, + added_anims=[ + self.students[1].change, "pondering", + self.students[2].change, "pondering", + ] + ) + self.wait(2) + + self.students[0].bubble = None + self.teacher_says( + "Er...not exactly", + bubble_kwargs={"width": 3, "height": 2}, + target_mode="guilty" + ) + self.wait(3) + + +class JustPrimesLabel(Scene): + def construct(self): + text = TextMobject("Just the primes") + text.scale(2) + text.to_corner(UL) + self.play(Write(text)) + self.wait(3) + self.play(FadeOutAndShift(text, DOWN)) + + +class DirichletComingUp(Scene): + def construct(self): + image = ImageMobject("Dirichlet") + image.set_height(3) + words = TextMobject( + "Coming up: \\\\", "Dirichlet's theorem", + alignment="", + ) + words.set_color_by_tex("Dirichlet's", YELLOW) + words.scale(1.5) + words.next_to(image, RIGHT) + Group(words, image).center() + + self.play( + FadeInFrom(image, RIGHT), + FadeInFrom(words, LEFT), + ) + self.wait() + + class ImagineYouFoundIt(TeacherStudentsScene): def construct(self): you = self.students[1] @@ -1116,6 +1242,7 @@ class ImagineYouFoundIt(TeacherStudentsScene): self.teacher, ) bubble = you.get_bubble(direction=LEFT) + bubble[-1].set_fill(GREEN_SCREEN, 1) you_label = TextMobject("You") arrow = Vector(DOWN) @@ -1130,19 +1257,19 @@ class ImagineYouFoundIt(TeacherStudentsScene): ) self.play(Blink(you)) self.play( - ShowCreation(bubble), + FadeIn(bubble), FadeOut(you_label), FadeOut(arrow), you.change, "pondering", ) self.play(you.look_at, bubble.get_corner(UR)) self.play(Blink(you)) - self.wait(3) + self.wait() self.play(you.change, "hooray") self.play(Blink(you)) - self.wait(2) + self.wait() self.play(you.change, "sassy", bubble.get_top()) - self.wait(3) + self.wait(6) class ShowSpiralsForWholeNumbers(CountSpirals): @@ -1248,6 +1375,18 @@ class ShowSpiralsForWholeNumbers(CountSpirals): ) +class PrimeSpiralsAtScale1000(SpiralScene): + def construct(self): + spiral = self.get_prime_p_spiral(10000) + self.add(spiral) + self.set_scale( + scale=1000, + spiral=spiral, + target_p_spiral_width=15, + run_time=0, + ) + + class SeparateIntoTwoQuestions(Scene): def construct(self): top_q = TextMobject("Why do", " primes", " cause", " spirals", "?") @@ -1615,11 +1754,30 @@ class IntroduceResidueClassTerminology(Scene): self.simple_english() def add_title(self): - title = TextMobject("Overly-fancy terminology") + title = TextMobject("Overly-fancy ", "terminology") title.scale(1.5) title.to_edge(UP, buff=MED_SMALL_BUFF) underline = Line().match_width(title) underline.next_to(title, DOWN, SMALL_BUFF) + + pre_title = TextMobject("Terminology") + pre_title.replace(title, dim_to_match=1) + + self.play(FadeInFromDown(pre_title)) + self.wait() + title[0].set_color(BLUE) + underline.set_color(BLUE) + self.play( + ReplacementTransform(pre_title[0], title[1]), + FadeInFrom(title[0], RIGHT), + GrowFromCenter(underline) + ) + self.play( + title[0].set_color, WHITE, + underline.set_color, WHITE, + ) + self.wait() + title.add(underline) self.add(title) @@ -1796,6 +1954,63 @@ class IntroduceResidueClassTerminology(Scene): self.wait() +class SimpleLongDivision(MovingCameraScene): + CONFIG = { + "camera_config": { + "background_color": DARKER_GREY + } + } + + def construct(self): + divisor = Integer(6) + num = Integer(20) + quotient = Integer(num.get_value() // divisor.get_value()) + to_subtract = Integer(-1 * quotient.get_value() * divisor.get_value()) + remainder = Integer(num.get_value() + to_subtract.get_value()) + + div_sym = VMobject() + div_sym.set_points_as_corners([0.2 * UP, UP, UP + 3 * RIGHT]) + + divisor.next_to(div_sym, LEFT, MED_SMALL_BUFF) + num.next_to(divisor, RIGHT, MED_LARGE_BUFF) + to_subtract.next_to(num, DOWN, buff=MED_LARGE_BUFF, aligned_edge=RIGHT) + h_line = Line(LEFT, RIGHT) + h_line.next_to(to_subtract, DOWN, buff=MED_SMALL_BUFF) + remainder.next_to(to_subtract, DOWN, buff=MED_LARGE_BUFF, aligned_edge=RIGHT) + quotient.next_to(num, UP, buff=MED_LARGE_BUFF, aligned_edge=RIGHT) + + remainder_rect = SurroundingRectangle(remainder) + remainder_rect.set_color(RED) + + frame = self.camera_frame + frame.scale(0.7, about_point=ORIGIN) + + divisor.set_color(YELLOW) + num.set_color(RED) + + self.add(divisor) + self.add(div_sym) + self.add(num) + self.play(FadeInFromDown(quotient)) + self.play( + TransformFromCopy(divisor, to_subtract.copy()), + TransformFromCopy(quotient, to_subtract), + ) + self.play(ShowCreation(h_line)) + self.play(Write(remainder)) + self.play(ShowCreation(remainder_rect)) + self.wait() + self.play(FadeOut(remainder_rect)) + + +class ZoomOutWords(Scene): + def construct(self): + words = TextMobject("Zoom out!") + words.scale(3) + self.play(FadeInFromLarge(words)) + self.wait() + + class Explain44Spirals(ExplainSixSpirals): CONFIG = { "max_N": 3000, @@ -2305,6 +2520,247 @@ class EliminateNonPrimativeResidueClassesOf44(Label44Spirals): self.wait() +class IntroduceTotientJargon(TeacherStudentsScene): + def construct(self): + self.add_title() + self.eliminate_non_coprimes() + + def add_title(self): + self.teacher_says( + "More jargon!", + target_mode="hooray", + ) + self.change_all_student_modes("erm") + words = self.teacher.bubble.content + + words.generate_target() + words.target.scale(1.5) + words.target.center().to_edge(UP, buff=MED_SMALL_BUFF) + words.target.set_color(BLUE) + underline = Line(LEFT, RIGHT) + underline.match_width(words.target) + underline.next_to(words.target, DOWN, SMALL_BUFF) + underline.scale(1.2) + + self.play( + MoveToTarget(words), + FadeOut(self.teacher.bubble), + LaggedStart(*[ + FadeOutAndShift(pi, 4 * DOWN) + for pi in self.pi_creatures + ]), + ShowCreation(underline) + ) + + def eliminate_non_coprimes(self): + number_grid = VGroup(*[ + VGroup(*[ + Integer(n) for n in range(11 * k, 11 * (k + 1)) + ]).arrange(DOWN) + for k in range(4) + ]).arrange(RIGHT, buff=1) + numbers = VGroup(*it.chain(*number_grid)) + numbers.set_height(6) + numbers.move_to(4 * LEFT) + numbers.to_edge(DOWN) + + evens = VGroup(*filter( + lambda nm: nm.get_value() % 2 == 0, + numbers + )) + div11 = VGroup(*filter( + lambda nm: nm.get_value() % 11 == 0, + numbers + )) + coprimes = VGroup(*filter( + lambda nm: nm not in evens and nm not in div11, + numbers + )) + + words = TextMobject( + "Which ones ", "don't\\\\", + "share any factors\\\\", + "with ", "44", + alignment="" + ) + words.scale(1.5) + words.next_to(ORIGIN, RIGHT) + + ff = words.get_part_by_tex("44") + ff.set_color(YELLOW) + ff.generate_target() + + # Show coprimes + self.play( + ShowIncreasingSubsets(numbers, run_time=3), + FadeInFrom(words, LEFT) + ) + self.wait() + for group in evens, div11: + rects = VGroup(*[ + SurroundingRectangle(number, color=RED) + for number in group + ]) + self.play(LaggedStartMap(ShowCreation, rects, run_time=1)) + self.play( + LaggedStart(*[ + ApplyMethod(number.set_opacity, 0.2) + for number in group + ]), + LaggedStartMap(FadeOut, rects), + run_time=1 + ) + self.wait() + + # Rearrange words + dsf = words[1:3] + dsf.generate_target() + dsf.target.arrange(RIGHT) + dsf.target[0].align_to(dsf.target[1][0], DOWN) + + example = numbers[35].copy() + example.generate_target() + example.target.match_height(ff) + num_pair = VGroup( + ff.target, + TextMobject("and").scale(1.5), + example.target, + ) + num_pair.arrange(RIGHT) + num_pair.move_to(words.get_top(), DOWN) + dsf.target.next_to(num_pair, DOWN, MED_LARGE_BUFF) + + phrase1 = TextMobject("are ", "``relatively prime''") + phrase2 = TextMobject("are ", "``coprime''") + for phrase in phrase1, phrase2: + phrase.scale(1.5) + phrase.move_to(dsf.target) + phrase[1].set_color(BLUE) + phrase.arrow = TexMobject("\\Updownarrow") + phrase.arrow.scale(1.5) + phrase.arrow.next_to(phrase, DOWN, 2 * SMALL_BUFF) + phrase.rect = SurroundingRectangle(phrase[1]) + phrase.rect.set_stroke(BLUE) + + self.play( + FadeOut(words[0]), + FadeOut(words[3]), + MoveToTarget(dsf), + MoveToTarget(ff), + GrowFromCenter(num_pair[1]), + ) + self.play( + MoveToTarget(example, path_arc=30 * DEGREES), + ) + self.wait() + self.play( + dsf.next_to, phrase1.arrow, DOWN, SMALL_BUFF, + GrowFromEdge(phrase1.arrow, UP), + GrowFromCenter(phrase1), + ShowCreation(phrase1.rect) + ) + self.play(FadeOut(phrase1.rect)) + self.wait() + self.play( + VGroup(dsf, phrase1, phrase1.arrow).next_to, + phrase2.arrow, DOWN, SMALL_BUFF, + GrowFromEdge(phrase2.arrow, UP), + GrowFromCenter(phrase2), + ShowCreation(phrase2.rect) + ) + self.play(FadeOut(phrase2.rect)) + self.wait() + + # Count through coprimes + coprime_rects = VGroup(*map(SurroundingRectangle, coprimes)) + coprime_rects.set_stroke(BLUE, 2) + example_anim = UpdateFromFunc( + example, lambda m: m.set_value(coprimes[len(coprime_rects) - 1].get_value()) + ) + self.play( + ShowIncreasingSubsets(coprime_rects, int_func=np.ceil), + example_anim, + run_time=3, + rate_func=linear, + ) + self.wait() + + # Show totient function + words_to_keep = VGroup(ff, num_pair[1], example, phrase2) + to_fade = VGroup(phrase2.arrow, phrase1, phrase1.arrow, dsf) + + totient = TexMobject("\\phi", "(", "44", ")", "=", "20") + totient.set_color_by_tex("44", YELLOW) + totient.scale(1.5) + totient.move_to(num_pair, UP) + phi = totient.get_part_by_tex("phi") + rhs = Integer(20) + rhs.replace(totient[-1], dim_to_match=1) + totient.submobjects[-1] = rhs + + self.play( + words_to_keep.to_edge, DOWN, + MaintainPositionRelativeTo(to_fade, words_to_keep), + VFadeOut(to_fade), + ) + self.play(FadeIn(totient)) + self.wait() + + # Label totient + brace = Brace(phi, DOWN) + etf = TextMobject("Euler's totient function") + etf.next_to(brace, DOWN) + etf.shift(RIGHT) + + self.play( + GrowFromCenter(brace), + FadeInFrom(etf, UP) + ) + self.wait() + self.play( + ShowIncreasingSubsets(coprime_rects), + UpdateFromFunc( + rhs, lambda m: m.set_value(len(coprime_rects)), + ), + example_anim, + rate_func=linear, + run_time=3, + ) + self.wait() + + # Show totatives + totient_group = VGroup(totient, brace, etf) + for cp, rect in zip(coprimes, coprime_rects): + cp.add(rect) + + self.play( + coprimes.arrange, RIGHT, {"buff": SMALL_BUFF}, + coprimes.set_width, FRAME_WIDTH - 1, + coprimes.move_to, 2 * UP, + FadeOut(evens), + FadeOut(div11[1::2]), + FadeOutAndShiftDown(words_to_keep), + totient_group.center, + totient_group.to_edge, DOWN, + ) + + totatives = TextMobject("``Totatives''") + totatives.scale(2) + totatives.set_color(BLUE) + totatives.move_to(ORIGIN) + arrows = VGroup(*[ + Arrow(totatives.get_top(), coprime.get_bottom()) + for coprime in coprimes + ]) + arrows.set_color(WHITE) + + self.play( + FadeIn(totatives), + LaggedStartMap(VFadeInThenOut, arrows, run_time=4, lag_ratio=0.05) + ) + self.wait(2) + + class TwoUnrelatedFacts(Scene): def construct(self): self.add_title() @@ -2730,7 +3186,8 @@ class CompareTauToApprox(Scene): eqs.generate_target() for eq in eqs.target: - eq[2][:8].set_color(GREEN) + eq[2][:8].set_color(RED) + eq.set_stroke(BLACK, 8, background=True) self.play(LaggedStart( FadeInFrom(eqs[0], DOWN), @@ -2740,6 +3197,25 @@ class CompareTauToApprox(Scene): self.wait() +class RecommendedMathologerVideo(Scene): + def construct(self): + full_rect = FullScreenFadeRectangle() + full_rect.set_fill(DARK_GREY, 1) + self.add(full_rect) + + title = TextMobject("Recommended Mathologer video") + title.set_width(FRAME_WIDTH - 1) + title.to_edge(UP) + screen_rect = SurroundingRectangle(ScreenRectangle(height=5.9), buff=SMALL_BUFF) + screen_rect.next_to(title, DOWN) + screen_rect.set_fill(BLACK, 1) + screen_rect.set_stroke(WHITE, 3) + + self.add(screen_rect) + self.play(Write(title)) + self.wait() + + class ShowClassesOfPrimeRays(SpiralScene): CONFIG = { "max_N": int(1e6), @@ -2866,7 +3342,7 @@ class LookAtRemainderMod710(Scene): "r": GREEN } equation = TexMobject( - "n", "=", "710", "\\cdot", "q", "+", "r", + "n", "=", "710", "k", "+", "r", tex_to_color_map=t2c, ) equation.scale(1.5) @@ -2898,6 +3374,11 @@ class LookAtRemainderMod710(Scene): class EliminateNonPrimative710Residues(ShowClassesOfPrimeRays): + CONFIG = { + "max_N": int(5e5), + "scale": 5e4, + } + def construct(self): self.setup_rays() self.eliminate_classes() @@ -2916,7 +3397,6 @@ class EliminateNonPrimative710Residues(ShowClassesOfPrimeRays): target_p_spiral_width=1, run_time=0, ) - self.wait() self.rays = rays @@ -2925,16 +3405,118 @@ class EliminateNonPrimative710Residues(ShowClassesOfPrimeRays): rect = FullScreenFadeRectangle() rect.set_opacity(0) + for r, ray in enumerate(rays): + ray.r = r + mod = 710 - mult2 = PGroup(*[rays[i] for i in range(0, mod, 2)]) + odds = PGroup(*[rays[i] for i in range(1, mod, 2)]) mult5 = PGroup(*[rays[i] for i in range(0, mod, 5) if i % 2 != 0]) mult71 = PGroup(*[rays[i] for i in range(0, mod, 71) if (i % 2 != 0 and i % 5 != 0)]) - groups = Group(mult2, mult5, mult71) colors = [RED, BLUE, PINK] - for group, color in zip(groups, colors): - group.set_stroke_width(2) - self.add(rect, group) + pre_label, r_label = label = VGroup( + TexMobject("710k + "), + Integer(100) + ) + label.scale(1.5) + label.arrange(RIGHT, buff=SMALL_BUFF) + label.set_stroke(BLACK, 5, background=True, family=True) + label.next_to(ORIGIN, DOWN) + + r_label.group = odds + r_label.add_updater( + lambda m: m.set_value(m.group[-1].r if len(m.group) > 0 else 1), + ) + + self.remove(rays) + # Odds + odds.set_stroke_width(3) + self.add(odds, label) + self.play( + ShowIncreasingSubsets(odds, int_func=np.ceil), + run_time=10, + ) + self.play(FadeOut(label)) + self.remove(odds) + self.add(*odds) + self.wait() + + # Multiples of 5 then 71 + for i, group in [(1, mult5), (2, mult71)]: + group_copy = group.copy() + group_copy.set_color(colors[i]) + group_copy.set_stroke_width(4) + r_label.group = group_copy + self.add(group_copy, label) + self.play( + ShowIncreasingSubsets(group_copy, int_func=np.ceil, run_time=10), + ) + self.play(FadeOut(label)) self.wait() - self.remove(rect, group) + self.remove(group_copy, *group) self.wait() + + +class Show280Computation(Scene): + def construct(self): + equation = TexMobject( + "\\phi(710) = ", + "710", + "\\left({1 \\over 2}\\right)", + "\\left({4 \\over 5}\\right)", + "\\left({70 \\over 71}\\right)", + "=", + "280", + ) + equation.set_width(FRAME_WIDTH - 1) + equation.move_to(UP) + words = VGroup( + TextMobject("Filter out\\\\evens"), + TextMobject("Filter out\\\\multiples of 5"), + TextMobject("Filter out\\\\multiples of 71"), + ) + vects = [DOWN, UP, DOWN] + colors = [RED, BLUE, LIGHT_PINK] + for part, word, vect, color in zip(equation[2:5], words, vects, colors): + brace = Brace(part, vect) + brace.stretch(0.8, 0) + word.brace = brace + word.next_to(brace, vect) + part.set_color(color) + word.set_color(color) + word.set_stroke(BLACK, 5, background=True) + equation.set_stroke(BLACK, 5, background=True) + + rect = FullScreenFadeRectangle(fill_opacity=0.9) + self.play( + FadeIn(rect), + FadeInFromDown(equation), + ) + self.wait() + for word in words: + self.play( + FadeIn(word), + GrowFromCenter(word.brace), + ) + self.wait() + + +class TeacherHoldUp(TeacherStudentsScene): + def construct(self): + self.change_all_student_modes( + "pondering", look_at_arg=2 * UP, + added_anims=[ + self.teacher.change, "raise_right_hand" + ] + ) + self.wait(4) + + +class DiscussPrimesMod10(Scene): + def construct(self): + pass + + +class BucketPrimesByLastDigit(Scene): + def construct(self): + pass From 7674a9eaee85f6f58c92f64cb18bd599c170bdc2 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 9 Oct 2019 18:09:05 -0700 Subject: [PATCH 20/21] Changed patreon end screen --- manimlib/for_3b1b_videos/common_scenes.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/manimlib/for_3b1b_videos/common_scenes.py b/manimlib/for_3b1b_videos/common_scenes.py index 88996b4b..baa5a2a5 100644 --- a/manimlib/for_3b1b_videos/common_scenes.py +++ b/manimlib/for_3b1b_videos/common_scenes.py @@ -154,8 +154,7 @@ class PatreonEndScreen(PatreonThanks, PiCreatureScene): "randomize_order": True, "capitalize": True, "name_y_spacing": 0.7, - # "thanks_words": "Funded by the community, with special thanks to:", - "thanks_words": "Early access, name in credits and more at 3b1b.org/support", + "thanks_words": "My thanks to all the patrons among you", } def construct(self): @@ -245,7 +244,7 @@ class PatreonEndScreen(PatreonThanks, PiCreatureScene): columns.next_to(underline, DOWN, buff=2) columns.generate_target() - columns.target.to_edge(DOWN, buff=2) + columns.target.to_edge(DOWN, buff=4) vect = columns.target.get_center() - columns.get_center() distance = get_norm(vect) wait_time = 20 From c4aefe9396f8e245336536055b9cfebd06fba50c Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 9 Oct 2019 18:09:15 -0700 Subject: [PATCH 21/21] End of Prime Spiral video --- active_projects/spirals.py | 1412 +++++++++++++++++++++++++++++++++++- 1 file changed, 1408 insertions(+), 4 deletions(-) diff --git a/active_projects/spirals.py b/active_projects/spirals.py index 6815a79c..758f02dc 100644 --- a/active_projects/spirals.py +++ b/active_projects/spirals.py @@ -1224,6 +1224,7 @@ class DirichletComingUp(Scene): words.set_color_by_tex("Dirichlet's", YELLOW) words.scale(1.5) words.next_to(image, RIGHT) + words.set_stroke(BLACK, 8, background=True) Group(words, image).center() self.play( @@ -1431,6 +1432,17 @@ class SeparateIntoTwoQuestions(Scene): self.wait() +class TopQuestionCross(Scene): + def construct(self): + top_q = TextMobject("Why do", " primes", " cause", " spirals", "?") + top_q.scale(2) + top_q.to_edge(UP) + cross = Cross(top_q) + + self.play(ShowCreation(cross)) + self.wait() + + class ExplainSixSpirals(ShowSpiralsForWholeNumbers): CONFIG = { "max_N": 150, @@ -1893,7 +1905,7 @@ class IntroduceResidueClassTerminology(Scene): labels = self.sequence_labels r = 2 - k = 2 + k = 3 sequence = sequences[r] label = labels[r] r_tex = label[0][3] @@ -1952,6 +1964,7 @@ class IntroduceResidueClassTerminology(Scene): FadeOutAndShift(randy, DOWN) ) self.wait() + self.wait(6) class SimpleLongDivision(MovingCameraScene): @@ -2362,9 +2375,10 @@ class Label44Spirals(Explain44Spirals): r_label = TextMobject("1 radian") # r_label.rotate( # angle_of_vector(arc.get_end() - arc.get_start()) - PI - # ) r_label.next_to(mid_point, normalize(mid_point)) + if n > 2: + r_label.set_opacity(0) self.play( ShowCreation(arc), @@ -3487,10 +3501,18 @@ class Show280Computation(Scene): word.set_stroke(BLACK, 5, background=True) equation.set_stroke(BLACK, 5, background=True) + etf_label = TextMobject("Euler's totient function") + etf_label.to_corner(UL) + arrow = Arrow(etf_label.get_bottom(), equation[0][0].get_top()) + equation[0][0].set_color(YELLOW) + etf_label.set_color(YELLOW) + rect = FullScreenFadeRectangle(fill_opacity=0.9) self.play( FadeIn(rect), FadeInFromDown(equation), + FadeIn(etf_label), + GrowArrow(arrow), ) self.wait() for word in words: @@ -3509,14 +3531,1396 @@ class TeacherHoldUp(TeacherStudentsScene): self.teacher.change, "raise_right_hand" ] ) - self.wait(4) + self.wait(8) class DiscussPrimesMod10(Scene): def construct(self): - pass + labels = VGroup(*[ + TextMobject(str(n), " mod 10:") + for n in range(10) + ]) + labels.arrange(DOWN, buff=0.35, aligned_edge=LEFT) + labels.to_edge(LEFT) + # digits = VGroup(*[l[0] for l in labels]) + labels.set_submobject_colors_by_gradient(YELLOW, BLUE) + + sequences = VGroup(*[ + VGroup(*[ + Integer(n).shift((n // 10) * RIGHT) + for n in range(r, 100 + r, 10) + ]) + for r in range(10) + ]) + for sequence, label in zip(sequences, labels): + sequence.next_to(label, RIGHT, buff=MED_LARGE_BUFF) + for item in sequence: + if item is sequence[-1]: + punc = TexMobject("\\dots") + else: + punc = TextMobject(",") + punc.next_to(item.get_corner(DR), RIGHT, SMALL_BUFF) + item.add(punc) + + # Introduce everything + self.play(LaggedStart(*[ + FadeInFrom(label, UP) + for label in labels + ])) + self.wait() + self.play( + LaggedStart(*[ + LaggedStart(*[ + FadeInFrom(item, LEFT) + for item in sequence + ]) + for sequence in sequences + ]) + ) + self.wait() + + # Highlight 0's then 1's + for sequence in sequences[:2]: + lds = VGroup(*[item[-2] for item in sequence]) + rects = VGroup(*[ + SurroundingRectangle(ld, buff=0.05) + for ld in lds + ]) + rects.set_color(YELLOW) + self.play( + LaggedStartMap( + ShowCreationThenFadeOut, rects + ) + ) + self.wait() + + # Eliminate certain residues + two = sequences[2][0] + five = sequences[5][0] + evens = VGroup(*it.chain(*sequences[::2])) + evens.remove(two) + div5 = sequences[5][1:] + prime_numbers = read_in_primes(100) + + primes = VGroup(*[ + item + for seq in sequences + for item in seq + if int(item.get_value()) in prime_numbers + ]) + non_primes = VGroup(*[ + item + for seq in sequences + for item in seq + if reduce(op.and_, [ + int(item.get_value()) not in prime_numbers, + item.get_value() % 2 != 0, + item.get_value() % 5 != 0, + ]) + ]) + + for prime, group in [(two, evens), (five, div5)]: + self.play(ShowCreationThenFadeAround(prime)) + self.play(LaggedStart(*[ + ApplyMethod(item.set_opacity, 0.2) + for item in group + ])) + self.wait() + + # Highlight primes + self.play( + LaggedStart(*[ + ApplyFunction( + lambda m: m.scale(1.2).set_color(TEAL), + prime + ) + for prime in primes + ]), + LaggedStart(*[ + ApplyFunction( + lambda m: m.scale(0.8).set_opacity(0.8), + non_prime + ) + for non_prime in non_primes + ]), + ) + self.wait() + + # Highlight coprime residue classes + rects = VGroup(*[ + SurroundingRectangle(VGroup(labels[r], sequences[r])) + for r in [1, 3, 7, 9] + ]) + for rect in rects: + rect.reverse_points() + + fade_rect = FullScreenFadeRectangle() + fade_rect.scale(1.1) + new_fade_rect = fade_rect.copy() + fade_rect.append_vectorized_mobject(rects[0]) + for rect in rects: + new_fade_rect.append_vectorized_mobject(rect) + + self.play(DrawBorderThenFill(fade_rect)) + self.wait() + self.play( + FadeOut(fade_rect), + FadeIn(new_fade_rect), + ) + self.wait() class BucketPrimesByLastDigit(Scene): + CONFIG = { + "bar_colors": [YELLOW, BLUE], + "mod": 10, + "max_n": 10000, + "n_to_animate": 20, + "n_to_show": 1000, + "x_label_scale_factor": 1, + "x_axis_label": "Last digit", + "bar_width": 0.5, + } + def construct(self): + self.add_axes() + self.add_bars() + self.bucket_primes() + + def add_axes(self): + mod = self.mod + + axes = Axes( + x_min=0, + x_max=mod + 0.5, + x_axis_config={ + "unit_size": 10 / mod, + "include_tip": False, + }, + y_min=0, + y_max=100, + y_axis_config={ + "unit_size": 0.055, + "tick_frequency": 12.5, + "include_tip": False, + }, + ) + + x_labels = VGroup() + for x in range(mod): + digit = Integer(x) + digit.scale(self.x_label_scale_factor) + digit.next_to(axes.x_axis.n2p(x + 1), DOWN, MED_SMALL_BUFF) + x_labels.add(digit) + self.modify_x_labels(x_labels) + x_labels.set_submobject_colors_by_gradient(*self.bar_colors) + axes.add(x_labels) + axes.x_labels = x_labels + + y_labels = VGroup() + for y in range(25, 125, 25): + label = Integer(y, unit="\\%") + label.next_to(axes.y_axis.n2p(y), LEFT, MED_SMALL_BUFF) + y_labels.add(label) + axes.add(y_labels) + + x_axis_label = TextMobject(self.x_axis_label) + x_axis_label.next_to(axes.x_axis.get_end(), RIGHT, buff=MED_LARGE_BUFF) + axes.add(x_axis_label) + + y_axis_label = TextMobject("Proportion") + y_axis_label.next_to(axes.y_axis.get_end(), UP, buff=MED_LARGE_BUFF) + # y_axis_label.set_color(self.bar_colors[0]) + axes.add(y_axis_label) + + axes.center() + axes.set_width(FRAME_WIDTH - 1) + axes.to_edge(DOWN) + + self.axes = axes + self.add(axes) + + def add_bars(self): + axes = self.axes + mod = self.mod + + count_trackers = Group(*[ + ValueTracker(0) + for x in range(mod) + ]) + + bars = VGroup() + for x in range(mod): + bar = Rectangle( + height=1, + width=self.bar_width, + fill_opacity=1, + ) + bar.bottom = axes.x_axis.n2p(x + 1) + bars.add(bar) + bars.set_submobject_colors_by_gradient(*self.bar_colors) + bars.set_stroke(WHITE, 1) + + def update_bars(bars): + values = [ct.get_value() for ct in count_trackers] + total = sum(values) + if total == 0: + props = [0 for x in range(mod)] + elif total < 1: + props = values + else: + props = [value / total for value in values] + + for bar, prop in zip(bars, props): + bar.set_height( + max( + 1e-5, + 100 * prop * axes.y_axis.unit_size, + ), + stretch=True + ) + # bar.set_height(1) + bar.move_to(bar.bottom, DOWN) + + bars.add_updater(update_bars) + + self.add(count_trackers) + self.add(bars) + + self.bars = bars + self.count_trackers = count_trackers + + def bucket_primes(self): + bars = self.bars + count_trackers = self.count_trackers + + max_n = self.max_n + n_to_animate = self.n_to_animate + n_to_show = self.n_to_show + mod = self.mod + + primes = VGroup(*[ + Integer(prime).scale(2).to_edge(UP, buff=LARGE_BUFF) + for prime in read_in_primes(max_n) + ]) + + arrow = Arrow(ORIGIN, DOWN) + x_labels = self.axes.x_labels + rects = VGroup(*map(SurroundingRectangle, x_labels)) + rects.set_color(RED) + + self.play(FadeIn(primes[0])) + for i, p, np in zip(it.count(), primes[:n_to_show], primes[1:]): + d = int(p.get_value()) % mod + self.add(rects[d]) + if i < n_to_animate: + self.play( + p.scale, 0.5, + p.move_to, bars[d].get_top(), + p.set_opacity, 0, + FadeIn(np), + count_trackers[d].increment_value, 1, + ) + self.remove(p) + else: + arrow.next_to(bars[d], UP) + self.add(arrow) + self.add(p) + count_trackers[d].increment_value(1) + self.wait(0.1) + self.remove(p) + self.remove(rects[d]) + + # + def modify_x_labels(self, labels): pass + + +class PhraseDirichletsTheoremFor10(TeacherStudentsScene): + def construct(self): + expression = TexMobject( + "\\lim_{x \\to \\infty}", + "\\left(", + "{\\text{\\# of primes $p$ where $p \\le x$} \\text{ and $p \\equiv 1$ mod 10}", + "\\over", + "\\text{\\# of primes $p$ where $p \\le x$}}", + "\\right)", + "=", + "\\frac{1}{4}", + ) + lim, lp, num, over, denom, rp, eq, fourth = expression + expression.shift(UP) + + denom.save_state() + denom.move_to(self.hold_up_spot, DOWN) + denom.shift_onto_screen() + + num[len(denom):].set_color(YELLOW) + + x_example = VGroup( + TextMobject("Think, for example, $x = $"), + Integer(int(1e6)), + ) + x_example.arrange(RIGHT) + x_example.scale(1.5) + x_example.to_edge(UP) + + # + teacher = self.teacher + students = self.students + self.play( + FadeInFromDown(denom), + teacher.change, "raise_right_hand", + self.get_student_changes(*["pondering"] * 3), + ) + self.wait() + self.play(FadeInFromDown(x_example)) + self.wait() + self.play( + Restore(denom), + teacher.change, "thinking", + ) + self.play( + TransformFromCopy(denom, num[:len(denom)]), + Write(over), + ) + self.play( + Write(num[len(denom):]), + students[0].change, "confused", + students[2].change, "erm", + ) + self.wait(2) + self.play( + Write(lp), + Write(rp), + Write(eq), + ) + self.play(FadeInFrom(fourth, LEFT)) + self.play(FadeInFrom(lim, RIGHT)) + self.play( + ChangeDecimalToValue( + x_example[1], int(1e7), + run_time=8, + rate_func=linear, + ), + VFadeOut(x_example, run_time=8), + self.get_student_changes(*["thinking"] * 3), + Blink( + teacher, + run_time=4, + rate_func=squish_rate_func(there_and_back, 0.65, 0.7) + ), + ) + + +class InsertNewResidueClasses(Scene): + def construct(self): + nums = VGroup(*map(Integer, [3, 7, 9])) + colors = [GREEN, TEAL, BLUE] + for num, color in zip(nums, colors): + num.set_color(color) + num.add_background_rectangle(buff=SMALL_BUFF, opacity=1) + self.play(FadeInFrom(num, UP)) + self.wait() + + +class BucketPrimesBy44(BucketPrimesByLastDigit): + CONFIG = { + "mod": 44, + "n_to_animate": 5, + "x_label_scale_factor": 0.5, + "x_axis_label": "r mod 44", + "bar_width": 0.1, + } + + def modify_x_labels(self, labels): + labels[::2].set_opacity(0) + + +class BucketPrimesBy9(BucketPrimesByLastDigit): + CONFIG = { + "mod": 9, + "n_to_animate": 5, + "x_label_scale_factor": 1, + "x_axis_label": "r mod 9", + "bar_width": 1, + } + + def modify_x_labels(self, labels): + pass + + +class DirichletIn1837(MovingCameraScene): + def construct(self): + # Add timeline + dates = list(range(1780, 2030, 10)) + timeline = NumberLine( + x_min=1700, + x_max=2020, + tick_frequency=1, + numbers_with_elongated_ticks=dates, + unit_size=0.2, + stroke_color=GREY, + stroke_width=2, + ) + timeline.add_numbers( + *dates, + number_config={"group_with_commas": False}, + ) + timeline.numbers.shift(SMALL_BUFF * DOWN) + timeline.to_edge(RIGHT) + + # Special dates + d_arrow, rh_arrow, pnt_arrow = arrows = VGroup(*[ + Vector(DOWN).next_to(timeline.n2p(date), UP) + for date in [1837, 1859, 1896] + ]) + d_label, rh_label, pnt_label = labels = VGroup(*[ + TextMobject(text).next_to(arrow, UP) + for arrow, text in zip(arrows, [ + "Dirichlet's\\\\theorem\\\\1837", + "Riemann\\\\hypothesis\\\\1859", + "Prime number\\\\theorem\\\\1896", + ]) + ]) + + # Back in time + frame = self.camera_frame + self.add(timeline, arrows, labels) + self.play( + frame.move_to, timeline.n2p(1837), + run_time=4, + ) + self.wait() + + # Show picture + image = ImageMobject("Dirichlet") + image.set_height(3) + image.next_to(d_label, LEFT) + self.play(FadeInFrom(image, RIGHT)) + self.wait() + + # Flash + self.play( + Flash( + d_label.get_center(), + num_lines=12, + line_length=0.25, + flash_radius=1.5, + line_stroke_width=3, + lag_ratio=0.05, + ) + ) + self.wait() + + # Transition title + title, underline = self.get_title_and_underline() + n = len(title[0]) + self.play( + ReplacementTransform(d_label[0][:n], title[0][:n]), + FadeOut(d_label[0][n:]), + LaggedStartMap( + FadeOutAndShiftDown, Group( + image, d_arrow, + rh_label, rh_arrow, + pnt_label, pnt_arrow, + *timeline, + ) + ), + ) + self.play(ShowCreation(underline)) + self.wait() + + def get_title_and_underline(self): + frame = self.camera_frame + title = TextMobject("Dirichlet's theorem") + title.scale(1.5) + title.next_to(frame.get_top(), DOWN, buff=MED_LARGE_BUFF) + underline = Line() + underline.match_width(title) + underline.next_to(title, DOWN, SMALL_BUFF) + return title, underline + + +class PhraseDirichletsTheorem(DirichletIn1837): + def construct(self): + self.add(*self.get_title_and_underline()) + + # Copy-pasted, which isn't great + expression = TexMobject( + "\\lim_{x \\to \\infty}", + "\\left(", + "{\\text{\\# of primes $p$ where $p \\le x$} \\text{ and $p \\equiv 1$ mod 10}", + "\\over", + "\\text{\\# of primes $p$ where $p \\le x$}}", + "\\right)", + "=", + "\\frac{1}{4}", + ) + lim, lp, num, over, denom, rp, eq, fourth = expression + expression.shift(1.5 * UP) + expression.to_edge(LEFT, MED_SMALL_BUFF) + num[len(denom):].set_color(YELLOW) + # + + # Terms and labels + ten = num[-2:] + one = num[-6] + four = fourth[-1] + + N = TexMobject("N") + r = TexMobject("r") + one_over_phi_N = TexMobject("1", "\\over", "\\phi(", "N", ")") + + N.set_color(MAROON_B) + r.set_color(BLUE) + one_over_phi_N.set_color_by_tex("N", N.get_color()) + + N.move_to(ten, DL) + r.move_to(one, DOWN) + one_over_phi_N.move_to(fourth, LEFT) + + N_label = TextMobject("$N$", " is any number") + N_label.set_color_by_tex("N", N.get_color()) + N_label.next_to(expression, DOWN, LARGE_BUFF) + + r_label = TextMobject("$r$", " is coprime to ", "$N$") + r_label[0].set_color(r.get_color()) + r_label[2].set_color(N.get_color()) + r_label.next_to(N_label, DOWN, MED_LARGE_BUFF) + + phi_N_label = TexMobject( + "\\phi({10}) = ", + "\\#\\{1, 3, 7, 9\\} = 4", + tex_to_color_map={ + "{10}": N.get_color(), + } + ) + phi_N_label[-1][2:9:2].set_color(r.get_color()) + phi_N_label.next_to(r_label, DOWN, MED_LARGE_BUFF) + # + + self.play( + LaggedStart(*[ + FadeIn(denom), + ShowCreation(over), + FadeIn(num), + Write(VGroup(lp, rp)), + FadeIn(lim), + Write(VGroup(eq, fourth)), + ]), + run_time=3, + lag_ratio=0.7, + ) + self.wait() + for mob in denom, num: + self.play(ShowCreationThenFadeAround(mob)) + self.wait() + self.play( + FadeInFrom(r, DOWN), + FadeOutAndShift(one, UP), + ) + self.play( + FadeInFrom(N, DOWN), + FadeOutAndShift(ten, UP), + ) + self.wait() + self.play( + TransformFromCopy(N, N_label[0]), + FadeIn(N_label[1:], DOWN) + ) + self.wait() + self.play( + FadeIn(r_label[1:-1], DOWN), + TransformFromCopy(r, r_label[0]), + ) + self.play( + TransformFromCopy(N_label[0], r_label[-1]), + ) + self.wait() + + self.play( + ShowCreationThenFadeAround(fourth), + ) + self.play( + FadeInFrom(one_over_phi_N[2:], LEFT), + FadeOutAndShift(four, RIGHT), + ReplacementTransform(fourth[0], one_over_phi_N[0][0]), + ReplacementTransform(fourth[1], one_over_phi_N[1][0]), + ) + self.play( + FadeInFrom(phi_N_label, DOWN) + ) + self.wait() + + # Fancier version + new_expression = TexMobject( + "\\lim_{x \\to \\infty}", + "\\left(", + "{\\pi(x; {N}, {r})", + "\\over", + "\\pi(x)}", + "\\right)", + "=", + "\\frac{1}{\\phi({N})}", + tex_to_color_map={ + "{N}": N.get_color(), + "{r}": r.get_color(), + "\\pi": WHITE, + } + ) + pis = new_expression.get_parts_by_tex("\\pi") + + randy = Randolph(height=2) + randy.next_to(new_expression, LEFT, buff=LARGE_BUFF) + randy.shift(0.75 * DOWN) + + new_expression.next_to(expression, DOWN, LARGE_BUFF) + ne_rect = SurroundingRectangle(new_expression, color=BLUE) + + label_group = VGroup(N_label, r_label) + label_group.generate_target() + label_group.target.arrange(RIGHT, buff=LARGE_BUFF) + label_group.target.next_to(new_expression, DOWN, buff=LARGE_BUFF) + + self.play( + FadeIn(randy), + ) + self.play( + randy.change, "hooray", expression + ) + self.play(Blink(randy)) + self.wait() + self.play( + FadeIn(new_expression), + MoveToTarget(label_group), + phi_N_label.to_edge, DOWN, MED_LARGE_BUFF, + randy.change, "horrified", new_expression, + ) + self.play(ShowCreation(ne_rect)) + self.play(randy.change, "confused") + self.play(Blink(randy)) + self.wait() + self.play( + LaggedStartMap( + ShowCreationThenFadeAround, pis, + ), + randy.change, "angry", new_expression + ) + self.wait() + self.play(Blink(randy)) + self.wait() + + +class MoreModestDirichlet(Scene): + def construct(self): + ed = TextMobject( + "Each (coprime) residue class ", + "is equally dense with ", + "primes." + ) + inf = TextMobject( + "Each (coprime) residue class ", + "has infinitely many ", + "primes." + ) + ed[1].set_color(BLUE) + inf[1].set_color(GREEN) + + for mob in [*ed, *inf]: + mob.save_state() + + cross = Cross(ed[1]) + c_group = VGroup(ed[1], cross) + + self.add(ed) + self.wait() + self.play(ShowCreation(cross)) + self.play( + c_group.shift, DOWN, + c_group.set_opacity, 0.5, + ReplacementTransform(ed[::2], inf[::2]), + FadeIn(inf[1]) + ) + self.wait() + self.remove(*inf) + self.play( + inf[1].shift, UP, + Restore(ed[0]), + Restore(ed[1]), + Restore(ed[2]), + FadeOut(cross), + ) + self.wait() + + +class TalkAboutProof(TeacherStudentsScene): + def construct(self): + teacher = self.teacher + students = self.students + + # Ask question + self.student_says( + "So how'd he\\\\prove it?", + student_index=0, + ) + bubble = students[0].bubble + students[0].bubble = None + self.play( + teacher.change, "hesitant", + students[1].change, "happy", + students[2].change, "happy", + ) + self.wait() + self.teacher_says( + "...er...it's a\\\\bit complicated", + target_mode="guilty", + ) + self.change_all_student_modes( + "tired", + look_at_arg=teacher.bubble, + lag_ratio=0.1, + ) + self.play( + FadeOut(bubble), + FadeOut(bubble.content), + ) + self.wait(3) + + # Bring up complex analysis + ca = TextMobject("Complex ", "Analysis") + ca.move_to(self.hold_up_spot, DOWN) + + self.play( + teacher.change, "raise_right_hand", + FadeInFromDown(ca), + FadeOut(teacher.bubble), + FadeOut(teacher.bubble.content), + self.get_student_changes(*["pondering"] * 3), + ) + self.wait() + self.play( + ca.scale, 2, + ca.center, + ca.to_edge, UP, + teacher.change, "happy", + ) + self.play(ca[1].set_color, GREEN) + self.wait(2) + self.play(ca[0].set_color, YELLOW) + self.wait(2) + self.change_all_student_modes( + "confused", look_at_arg=ca, + ) + self.wait(4) + + +class HighlightTwinPrimes(Scene): + def construct(self): + self.add_paper_titles() + self.show_twin_primes() + + def add_paper_titles(self): + gpy = TextMobject( + "Goldston, Pintz, Yildirim\\\\", + "2005", + ) + zhang = TextMobject("Zhang\\\\2014") + + gpy.move_to(FRAME_WIDTH * LEFT / 4) + gpy.to_edge(UP) + zhang.move_to(FRAME_WIDTH * RIGHT / 4) + zhang.to_edge(UP) + + self.play(LaggedStartMap( + FadeInFromDown, VGroup(gpy, zhang), + lag_ratio=0.3, + )) + + def show_twin_primes(self): + max_x = 300 + line = NumberLine( + x_min=0, + x_max=max_x, + unit_size=0.5, + numbers_with_elongated_ticks=range(10, max_x, 10), + ) + line.move_to(2.5 * DOWN + 7 * LEFT, LEFT) + line.add_numbers(*range(10, max_x, 10)) + + primes = read_in_primes(max_x) + prime_mobs = VGroup(*[ + Integer(p).next_to(line.n2p(p), UP) + for p in primes + ]) + dots = VGroup(*[Dot(line.n2p(p)) for p in primes]) + + arcs = VGroup() + for pm, npm in zip(prime_mobs, prime_mobs[1:]): + p = pm.get_value() + np = npm.get_value() + if np - p == 2: + angle = 30 * DEGREES + arc = Arc( + start_angle=angle, + angle=PI - 2 * angle, + color=RED, + ) + arc.set_width( + get_norm(npm.get_center() - pm.get_center()) + ) + arc.next_to(VGroup(pm, npm), UP, SMALL_BUFF) + arcs.add(arc) + + dots.set_color(TEAL) + prime_mobs.set_color(TEAL) + + line.add(dots) + + self.play( + FadeIn(line, lag_ratio=0.9), + LaggedStartMap(FadeInFromDown, prime_mobs), + run_time=2, + ) + line.add(prime_mobs) + self.wait() + + self.play(FadeIn(arcs)) + self.play( + line.shift, 100 * LEFT, + arcs.shift, 100 * LEFT, + run_time=20, + rate_func=lambda t: smooth(t, 5) + ) + self.wait() + + +class RandomToImportant(PiCreatureScene): + CONFIG = { + "default_pi_creature_kwargs": { + "color": GREY_BROWN, + }, + "camera_config": { + "background_color": DARKER_GREY, + } + } + + def construct(self): + morty = self.pi_creature + morty.center().to_edge(DOWN) + + left_comment = TextMobject("Arbitrary question") + left_comment.to_edge(UP) + left_comment.shift(3.5 * LEFT) + + right_comment = TextMobject("Deep fact") + right_comment.to_edge(UP) + right_comment.shift(3.5 * RIGHT) + + arrow = Arrow( + left_comment.get_right(), + right_comment.get_left(), + buff=0.5, + ) + + self.play( + morty.change, "raise_left_hand", left_comment, + FadeInFromDown(left_comment) + ) + self.wait(2) + self.play( + morty.change, "raise_right_hand", right_comment, + FadeInFromDown(right_comment) + ) + self.play( + ShowCreation(arrow), + morty.look_at, right_comment, + ) + self.wait(2) + + +class RandomWalkOfTopics(Scene): + CONFIG = { + "n_dots": 30, + "n_edge_range": [2, 4], + "super_dot_n_edges": 20, + "isolated_threashold": 0.5, + } + + def construct(self): + self.setup_network() + self.define_important() + self.perform_walk() + + def setup_network(self): + n_dots = self.n_dots + dots = VGroup() + + while len(dots) < n_dots: + point = np.random.uniform(-1, 1, size=3) + point[2] = 0 + point[0] *= 7 + point[1] *= 3 + isolated = True + for dot in dots: + if get_norm(dot.get_center() - point) < self.isolated_threashold: + isolated = False + if not isolated: + continue + dot = Dot(point) + dot.edges = VGroup() + dot.neighbors = VGroup() + dots.add(dot) + super_dot = dots[len(dots) // 2] + + all_edges = VGroup() + + def add_edge(d1, d2): + if d1 is d2: + return + edge = Line( + d1.get_center(), d2.get_center(), + buff=d1.get_width() / 2 + ) + d1.edges.add(edge) + d2.edges.add(edge) + d1.neighbors.add(d2) + d2.neighbors.add(d1) + all_edges.add(edge) + + for dot in dots: + # others = list(dots[i + 1:]) + others = [d for d in dots if d is not dot] + others.sort(key=lambda d: get_norm(d.get_center() - dot.get_center())) + n_edges = np.random.randint(*self.n_edge_range) + for other in others[:n_edges]: + if dot in other.neighbors: + continue + add_edge(dot, other) + + for dot in dots: + if len(super_dot.neighbors) > self.super_dot_n_edges: + break + elif dot in super_dot.neighbors: + continue + add_edge(super_dot, dot) + + dots.sort(lambda p: p[0]) + + all_edges.set_stroke(WHITE, 2) + dots.set_fill(LIGHT_GREY, 1) + + VGroup(dots, all_edges).to_edge(DOWN, buff=MED_SMALL_BUFF) + + self.dots = dots + self.edges = all_edges + self.super_dot = super_dot + + def define_important(self): + sd = self.super_dot + dots = self.dots + edges = self.edges + + sd.set_color(RED) + for mob in [*dots, *edges]: + mob.save_state() + mob.set_opacity(0) + + sd.set_opacity(1) + sd.edges.set_opacity(1) + sd.neighbors.set_opacity(1) + + # angles = np.arange(0, TAU, TAU / len(sd.neighbors)) + # center = 0.5 * DOWN + # sd.move_to(center) + # for dot, edge, angle in zip(sd.neighbors, sd.edges, angles): + # dot.move_to(center + rotate_vector(2.5 * RIGHT, angle)) + # if edge.get_length() > 0: + # edge.put_start_and_end_on( + # sd.get_center(), + # dot.get_center() + # ) + # rad = dot.get_width() / 2 + # llen = edge.get_length() + # edge.scale((llen - 2 * rad) / llen) + + title = VGroup( + TextMobject("Important"), + TexMobject("\\Leftrightarrow"), + TextMobject("Many connections"), + ) + title.scale(1.5) + title.arrange(RIGHT, buff=MED_LARGE_BUFF) + title.to_edge(UP) + + arrow_words = TextMobject("(in my view)") + arrow_words.set_width(2 * title[1].get_width()) + arrow_words.next_to(title[1], UP, SMALL_BUFF) + + title[0].save_state() + title[0].set_x(0) + + self.add(title[0]) + self.play( + FadeInFromLarge(sd), + title[0].set_color, RED, + ) + title[0].saved_state.set_color(RED) + self.play( + Restore(title[0]), + GrowFromCenter(title[1]), + FadeIn(arrow_words), + FadeInFrom(title[2], LEFT), + LaggedStartMap( + ShowCreation, sd.edges, + run_time=3, + ), + LaggedStartMap( + GrowFromPoint, sd.neighbors, + lambda m: (m, sd.get_center()), + run_time=3, + ), + ) + self.wait() + self.play(*map(Restore, [*dots, *edges])) + self.wait() + + def perform_walk(self): + # dots = self.dots + # edges = self.edges + sd = self.super_dot + + path = VGroup(sd) + random.seed(1) + for x in range(3): + new_choice = None + while new_choice is None or new_choice in path: + new_choice = random.choice(path[0].neighbors) + path.add_to_back(new_choice) + + for d1, d2 in zip(path, path[1:]): + self.play(Flash(d1.get_center())) + self.play( + ShowCreationThenDestruction( + Line( + d1.get_center(), + d2.get_center(), + color=YELLOW, + ) + ) + ) + + self.play(Flash(sd)) + self.play(LaggedStart(*[ + ApplyMethod( + edge.set_stroke, YELLOW, 5, + rate_func=there_and_back, + ) + for edge in sd.edges + ])) + self.wait() + + +class DeadEnds(RandomWalkOfTopics): + CONFIG = { + "n_dots": 20, + "n_edge_range": [2, 4], + "super_dot_n_edges": 2, + "random_seed": 1, + } + + def construct(self): + self.setup_network() + dots = self.dots + edges = self.edges + + self.add(dots, edges) + + VGroup( + edges[3], + edges[4], + edges[7], + edges[10], + edges[12], + edges[15], + edges[27], + edges[30], + edges[33], + ).set_opacity(0) + + # for i, edge in enumerate(edges): + # self.add(Integer(i).move_to(edge)) + + +class Rediscovery(Scene): + def construct(self): + randy = Randolph() + randy.to_edge(DOWN) + randy.shift(2 * RIGHT) + + lightbulb = Lightbulb() + lightbulb.set_stroke(width=4) + lightbulb.scale(1.5) + lightbulb.next_to(randy, UP) + + rings = self.get_rings( + lightbulb.get_center(), + max_radius=10.0, + delta_r=0.1, + ) + + bubble = ThoughtBubble() + bubble.pin_to(randy) + # bubble[-1].set_fill(GREEN_SCREEN, 0.5) + self.play( + randy.change, "pondering", + ShowCreation(bubble), + FadeInFromDown(lightbulb), + ) + self.add(rings, bubble) + self.play( + randy.change, "thinking", + LaggedStartMap( + VFadeInThenOut, + rings, + lag_ratio=0.002, + run_time=3, + ) + ) + self.wait(4) + + def get_rings(self, center, max_radius, delta_r): + radii = np.arange(0, max_radius, delta_r) + rings = VGroup(*[ + Annulus( + inner_radius=r1, + outer_radius=r2, + fill_opacity=0.75 * (1 - fdiv(r1, max_radius)), + fill_color=YELLOW + ) + for r1, r2 in zip(radii, radii[1:]) + ]) + rings.move_to(center) + return rings + + +class BePlayful(TeacherStudentsScene): + def construct(self): + self.teacher_says( + "So be playful!", + target_mode="hooray", + ) + self.change_student_modes("thinking", "hooray", "happy") + self.wait(3) + + +class SpiralsPatronThanks(PatreonEndScreen): + CONFIG = { + "specific_patrons": [ + "Juan Benet", + "Kurt Dicus", + "Vassili Philippov", + "Burt Humburg", + "Matt Russell", + "Scott Gray", + "soekul", + "Tihan Seale", + "D. Sivakumar", + "Ali Yahya", + "Arthur Zey", + "dave nicponski", + "Joseph Kelly", + "Kaustuv DeBiswas", + "kkm", + "Lambda AI Hardware", + "Lukas Biewald", + "Mark Heising", + "Nicholas Cahill", + "Peter Mcinerney", + "Quantopian", + "Scott Walter, Ph.D.", + "Tauba Auerbach", + "Yana Chernobilsky", + "Yu Jun", + "Jordan Scales", + "Lukas -krtek.net- Novy", + "Andrew Weir", + "Britt Selvitelle", + "Britton Finley", + "David Gow", + "J", + "Jonathan Wilson", + "Joseph John Cox", + "Magnus Dahlström", + "Mattéo Delabre", + "Randy C. Will", + "Ryan Atallah", + "Luc Ritchie", + "1stViewMaths", + "Aidan Shenkman", + "Alex Mijalis", + "Alexis Olson", + "Andreas Benjamin Brössel", + "Andrew Busey", + "Andrew R. Whalley", + "Ankalagon", + "Anthony Turvey", + "Antoine Bruguier", + "Antonio Juarez", + "Arjun Chakroborty", + "Art Ianuzzi", + "Austin Goodman", + "Avi Finkel", + "Awoo", + "Azeem Ansar", + "AZsorcerer", + "Barry Fam", + "Bernd Sing", + "Boris Veselinovich", + "Bradley Pirtle", + "Brian Staroselsky", + "Calvin Lin", + "Charles Southerland", + "Charlie N", + "Chris Connett", + "Christian Kaiser", + "Clark Gaebel", + "Danger Dai", + "Daniel Herrera C", + "Daniel Pang", + "Dave B", + "Dave Kester", + "David B. Hill", + "David Clark", + "DeathByShrimp", + "Delton Ding", + "Dominik Wagner", + "eaglle", + "emptymachine", + "Eric Younge", + "Ero Carrera", + "Eryq Ouithaqueue", + "Federico Lebron", + "Fernando Via Canel", + "Frank R. Brown, Jr.", + "Giovanni Filippi", + "Hal Hildebrand", + "Hause Lin", + "Hitoshi Yamauchi", + "Ivan Sorokin", + "j eduardo perez", + "Jacob Baxter", + "Jacob Harmon", + "Jacob Hartmann", + "Jacob Magnuson", + "Jameel Syed", + "James Stevenson", + "Jason Hise", + "Jeff Linse", + "Jeff Straathof", + "John C. Vesey", + "John Griffith", + "John Haley", + "John V Wertheim", + "Jonathan Eppele", + "Josh Kinnear", + "Joshua Claeys", + "Kai-Siang Ang", + "Kanan Gill", + "Kartik Cating-Subramanian", + "L0j1k", + "Lee Redden", + "Linh Tran", + "Ludwig Schubert", + "Magister Mugit", + "Mark B Bahu", + "Mark Mann", + "Martin Price", + "Mathias Jansson", + "Matt Langford", + "Matt Roveto", + "Matthew Bouchard", + "Matthew Cocke", + "Michael Faust", + "Michael Hardel", + "Michele Donadoni", + "Mirik Gogri", + "Mustafa Mahdi", + "Márton Vaitkus", + "Nero Li", + "Nikita Lesnikov", + "Omar Zrien", + "Owen Campbell-Moore", + "Patrick Lucas", + "Pedro Igor Salomão Budib", + "Peter Ehrnstrom", + "RedAgent14", + "rehmi post", + "Rex Godby", + "Richard Barthel", + "Ripta Pasay", + "Rish Kundalia", + "Roman Sergeychik", + "Roobie", + "Ryan Williams", + "Sebastian Garcia", + "Solara570", + "Steven Siddals", + "Stevie Metke", + "Suthen Thomas", + "Tal Einav", + "Ted Suzman", + "The Responsible One", + "Thomas Tarler", + "Tianyu Ge", + "Tom Fleming", + "Tyler VanValkenburg", + "Valeriy Skobelev", + "Veritasium", + "Vinicius Reis", + "Xuanji Li", + "Yavor Ivanov", + "YinYangBalance.Asia", + ] + } + + +class Thumbnail(SpiralScene): + CONFIG = { + "max_N": 8000, + "just_show": True, + } + + def construct(self): + self.add_dots() + if not self.just_show: + pass + + def add_dots(self): + self.set_scale(scale=1e3) + + p_spiral = self.get_prime_p_spiral(self.max_N) + dots = VGroup(*[ + Dot( + point, + radius=interpolate(0.01, 0.07, min(0.5 * get_norm(point), 1)), + fill_color=TEAL, + # fill_opacity=interpolate(0.5, 1, min(get_norm(point), 1)) + ) + for point in p_spiral.points + ]) + dots.set_fill([TEAL_E, TEAL_A]) + dots.set_stroke(BLACK, 1) + + label = VGroup( + TextMobject("$(p, p)$ for all primes $p$, in polar"), + ) + label.scale(2) + label.set_stroke(BLACK, 10, background=True) + label.add_background_rectangle() + label.to_corner(DL) + + self.add(dots) + self.add(label) + + self.dots = dots