diff --git a/docs/source/animation.rst b/docs/source/animation.rst index e38db6333f..930dd3de1b 100644 --- a/docs/source/animation.rst +++ b/docs/source/animation.rst @@ -61,7 +61,7 @@ Fade .. code-block:: python - class AnimationFadeInFrom(Scene): + class AnimationFadeIn(Scene): def construct(self): square = Square() for label, edge in zip( @@ -71,7 +71,7 @@ Fade anno.shift(2 * DOWN) self.add(anno) - self.play(FadeInFrom(square, edge)) + self.play(FadeIn(square, edge)) self.remove(anno, square) @@ -84,7 +84,7 @@ Fade .. code-block:: python - class AnimationFadeOutAndShift(Scene): + class AnimationFadeOut(Scene): def construct(self): square = Square() for label, edge in zip( @@ -94,7 +94,7 @@ Fade anno.shift(2 * DOWN) self.add(anno) - self.play(FadeOutAndShift(square, edge)) + self.play(FadeOut(square, edge)) self.remove(anno, square) @@ -203,7 +203,7 @@ You can combine cardinal directions to form diagonal animations def construct(self): square = Square() for diag in [UP + LEFT, UP + RIGHT, DOWN + LEFT, DOWN + RIGHT]: - self.play(FadeInFrom(square, diag)) + self.play(FadeIn(square, diag)) .. note:: You can also use the abbreviated forms like ``UL, UR, DL, DR``. diff --git a/example_scenes.py b/example_scenes.py index bc15c55776..bbe0c41e32 100644 --- a/example_scenes.py +++ b/example_scenes.py @@ -25,7 +25,7 @@ def construct(self): VGroup(title, basel).arrange(DOWN) self.play( Write(title), - FadeInFrom(basel, UP), + FadeIn(basel, UP), ) self.wait() diff --git a/from_3b1b/active/antipode.py b/from_3b1b/active/antipode.py new file mode 100644 index 0000000000..f8ce68a612 --- /dev/null +++ b/from_3b1b/active/antipode.py @@ -0,0 +1,6 @@ +from manimlib.imports import * + + +class NewSceneName(ThreeDScene): + def construct(self): + pass diff --git a/from_3b1b/active/bayes/beta1.py b/from_3b1b/active/bayes/beta1.py new file mode 100644 index 0000000000..4588e7bc97 --- /dev/null +++ b/from_3b1b/active/bayes/beta1.py @@ -0,0 +1,3819 @@ +from manimlib.imports import * +from from_3b1b.active.bayes.beta_helpers import * + +import scipy.stats + +OUTPUT_DIRECTORY = "bayes/beta1" + + +# Scenes +class BarChartTest(Scene): + def construct(self): + bar_chart = BarChart() + bar_chart.to_edge(DOWN) + self.add(bar_chart) + + +class Thumbnail1(Scene): + def construct(self): + p1 = "$96\\%$" + p2 = "$93\\%$" + n1 = "50" + n2 = "200" + t2c = { + p1: BLUE, + p2: YELLOW, + n1: BLUE_C, + n2: YELLOW, + } + kw = {"tex_to_color_map": t2c} + text = VGroup( + TextMobject(f"{p1} with {n1} reviews", **kw), + TextMobject("vs.", **kw), + TextMobject(f"{p2} with {n2} reviews", **kw), + ) + fix_percent(text[0].get_part_by_tex(p1)[-1]) + fix_percent(text[2].get_part_by_tex(p2)[-1]) + text.scale(2) + text.arrange(DOWN, buff=LARGE_BUFF) + text.set_width(FRAME_WIDTH - 1) + self.add(text) + + +class AltThumbnail1(Scene): + def construct(self): + N = 20 + n_trials = 10000 + p = 0.7 + outcomes = (np.random.random((N, n_trials)) < p).sum(0) + counts = [] + for k in range(N + 1): + counts.append((outcomes == k).sum()) + + hist = Histogram( + counts, + y_max=0.3, + y_tick_freq=0.05, + y_axis_numbers_to_show=[10, 20, 30], + x_label_freq=10, + ) + hist.set_width(FRAME_WIDTH - 1) + hist.bars.set_submobject_colors_by_gradient(YELLOW, YELLOW, GREEN, BLUE) + hist.bars.set_stroke(WHITE, 2) + + title = TextMobject("Binomial distribution") + title.set_width(12) + title.to_corner(UR, buff=0.8) + title.add_background_rectangle() + + self.add(hist) + self.add(title) + + +class Thumbnail2(Scene): + def construct(self): + axes = self.get_axes() + graph = get_beta_graph(axes, 2, 2) + # sub_graph = axes.get_graph( + # lambda x: (1 - x) * graph.underlying_function(x) + # ) + # sub_graph.add_line_to(axes.c2p(1, 0)) + # sub_graph.add_line_to(axes.c2p(0, 0)) + # sub_graph.set_stroke(YELLOW, 4) + # sub_graph.set_fill(YELLOW_D, 1) + + new_graph = get_beta_graph(axes, 9, 2) + new_graph.set_stroke(GREEN, 4) + new_graph.set_fill(GREEN, 0.5) + + self.add(axes) + self.add(graph) + self.add(new_graph) + + arrow = Arrow( + axes.input_to_graph_point(0.5, graph), + axes.input_to_graph_point(0.8, new_graph), + path_arc=-90 * DEGREES, + buff=0.3 + ) + self.add(arrow) + + formula = TexMobject( + "P(H|D) = {P(H)P(D|H) \\over P(D)}", + tex_to_color_map={ + "H": YELLOW, + "D": GREEN, + } + ) + formula.next_to(axes.c2p(0, 3), RIGHT, LARGE_BUFF) + formula.set_height(1.5) + formula.to_edge(LEFT) + formula.to_edge(UP, LARGE_BUFF) + formula.add_to_back(BackgroundRectangle(formula[:4], buff=0.25)) + + self.add(formula) + + def get_axes(self, y_max=3, y_height=4.5, y_unit=0.5): + axes = get_beta_dist_axes(y_max=y_max, y_unit=y_unit) + axes.y_axis.set_height(y_height, about_point=axes.c2p(0, 0)) + axes.to_edge(DOWN) + axes.scale(0.9) + return axes + + +class Thumbnail3(Thumbnail2): + def construct(self): + axes = self.get_axes(y_max=4, y_height=6) + axes.set_height(7) + graph = get_beta_graph(axes, 9, 2) + + self.add(axes) + self.add(graph) + + label = TexMobject( + "\\text{Beta}(10, 3)", + tex_to_color_map={ + "10": GREEN, + "3": RED, + } + ) + label = get_beta_label(9, 2) + label.set_height(1.25) + label.next_to(axes.c2p(0, 3), RIGHT, LARGE_BUFF) + + self.add(label) + + +class HighlightReviewParts(Scene): + CONFIG = { + "reverse_order": False, + } + + def construct(self): + # Setup up rectangles + rects = VGroup(*[Rectangle() for x in range(3)]) + rects.set_stroke(width=0) + rects.set_fill(GREY, 0.5) + + rects.set_height(1.35, stretch=True) + rects.set_width(9.75, stretch=True) + + rects[0].move_to([-0.2, 0.5, 0]) + rects[1].next_to(rects[0], DOWN, buff=0) + rects[2].next_to(rects[1], DOWN, buff=0) + + rects[2].set_height(1, stretch=True, about_edge=UP) + + inv_rects = VGroup() + for rect in rects: + fsr = FullScreenFadeRectangle() + fsr.append_points(rect.points[::-1]) + inv_rects.add(fsr) + + inv_rects.set_fill(BLACK, 0.85) + + # Set up labels + ratings = [100, 96, 93] + n_reviews = [10, 50, 200] + colors = [PINK, BLUE, YELLOW] + + review_labels = VGroup() + for rect, rating, nr, color in zip(rects, ratings, n_reviews, colors): + label = TexMobject( + f"{nr}", "\\text{ reviews }", + f"{rating}", "\\%", + ) + label[2:].set_color(color) + label.set_height(1) + label.next_to(rect, UP, aligned_edge=RIGHT) + label.set_stroke(BLACK, 4, background=True) + fix_percent(label[3][0]) + review_labels.add(label) + + # Animations + curr_fsr = inv_rects[0] + curr_label = None + + tuples = list(zip(inv_rects, review_labels)) + if self.reverse_order: + tuples = reversed(tuples) + curr_fsr = inv_rects[-1] + + for fsr, label in tuples: + if curr_fsr is fsr: + self.play(VFadeIn(fsr)) + else: + self.play( + Transform(curr_fsr, fsr), + MoveToTarget(curr_label), + ) + + first, second = label[2:], label[:2] + if self.reverse_order: + first, second = second, first + + self.add(first) + self.wait(2) + self.add(second) + self.wait(2) + + label.generate_target() + label.target.scale(0.3) + if curr_label is None: + label.target.to_corner(UR) + label.target.shift(MED_LARGE_BUFF * LEFT) + else: + label.target.next_to(curr_label, DOWN) + + curr_label = label + self.play(MoveToTarget(curr_label)) + self.wait() + + br = BackgroundRectangle(review_labels, buff=0.25) + br.set_fill(BLACK, 0.85) + br.set_width(FRAME_WIDTH) + br.set_height(FRAME_HEIGHT, stretch=True) + br.center() + self.add(br, review_labels) + self.play( + FadeOut(curr_fsr), + FadeIn(br), + ) + self.wait() + + +class ShowThreeCases(Scene): + def construct(self): + titles = self.get_titles() + reviews = self.get_reviews(titles) + for review in reviews: + review.match_x(reviews[2]) + + # Introduce everything + self.play(LaggedStartMap( + FadeInFrom, titles, + lambda m: (m, DOWN), + lag_ratio=0.2 + )) + self.play(LaggedStart(*[ + LaggedStartMap( + FadeInFromLarge, review, + lag_ratio=0.1 + ) + for review in reviews + ], lag_ratio=0.1)) + self.add(reviews) + self.wait() + + self.play(ShowCreationThenFadeAround(reviews[2])) + self.wait() + + # Suspicious of 100% + randy = Randolph() + randy.flip() + randy.set_height(2) + randy.next_to( + reviews[0], RIGHT, LARGE_BUFF, + aligned_edge=UP, + ) + randy.look_at(reviews[0]) + self.play(FadeIn(randy)) + self.play(randy.change, "sassy") + self.play(Blink(randy)) + self.wait() + self.play(FadeOut(randy)) + + # Low number means it could be a fluke. + review = reviews[0] + + review.generate_target() + review.target.scale(2) + review.target.arrange(RIGHT) + review.target.move_to(review) + + self.play(MoveToTarget(review)) + + alt_negs = [1, 2, 1, 0] + alt_reviews = VGroup() + for k in alt_negs: + alt_reviews.add(self.get_plusses_and_minuses(titles[0], 1, 10, k)) + for ar in alt_reviews: + for m1, m2 in zip(ar, review): + m1.replace(m2) + + alt_percents = VGroup(*[ + TexMobject(str(10 * (10 - k)) + "\\%") + for k in alt_negs + ]) + hundo = titles[0][0] + for ap in alt_percents: + fix_percent(ap.family_members_with_points()[-1]) + ap.match_style(hundo) + ap.match_height(hundo) + ap.move_to(hundo, RIGHT) + + last_review = review + last_percent = hundo + for ar, ap in zip(alt_reviews, alt_percents): + self.play( + FadeIn(ar, 0.5 * DOWN, lag_ratio=0.2), + FadeOut(last_review), + FadeIn(ap, 0.5 * DOWN), + FadeOut(last_percent, 0.5 * UP), + run_time=1.5 + ) + last_review = ar + last_percent = ap + self.remove(last_review, last_percent) + self.add(titles, *reviews) + + # How do you think about the tradeoff? + p1 = titles[1][0] + p2 = titles[2][0] + nums = VGroup(p1, p2) + lower_reviews = reviews[1:] + lower_reviews.generate_target() + lower_reviews.target.arrange(LEFT, buff=1.5) + lower_reviews.target.center() + nums.generate_target() + for nt, review in zip(nums.target, lower_reviews.target): + nt.next_to(review, UP, MED_LARGE_BUFF) + + nums.target[0].match_y(nums.target[1]) + + self.clear() + self.play( + MoveToTarget(lower_reviews), + MoveToTarget(nums), + FadeOut(titles[1][1:]), + FadeOut(titles[2][1:]), + FadeOut(titles[0]), + FadeOut(reviews[0]), + run_time=2, + ) + + greater_than = TexMobject(">") + greater_than.scale(2) + greater_than.move_to(midpoint( + reviews[2].get_right(), + reviews[1].get_left(), + )) + less_than = greater_than.copy().flip() + less_than.match_height(nums[0][0]) + less_than.match_y(nums, DOWN) + + nums.generate_target() + nums.target[1].next_to(less_than, LEFT, MED_LARGE_BUFF) + nums.target[0].next_to(less_than, RIGHT, MED_LARGE_BUFF) + + squares = VGroup(*[ + SurroundingRectangle( + submob, buff=0.01, + stroke_color=LIGHT_GREY, + stroke_width=1, + ) + for submob in reviews[2] + ]) + squares.shuffle() + self.play( + LaggedStartMap( + ShowCreationThenFadeOut, squares, + lag_ratio=0.5 / len(squares), + run_time=3, + ), + Write(greater_than), + ) + self.wait() + self.play( + MoveToTarget(nums), + TransformFromCopy( + greater_than, less_than, + ) + ) + self.wait() + + def get_titles(self): + titles = VGroup( + TextMobject( + "$100\\%$ \\\\", + "10 reviews" + ), + TextMobject( + "$96\\%$ \\\\", + "50 reviews" + ), + TextMobject( + "$93\\%$ \\\\", + "200 reviews" + ), + ) + colors = [PINK, BLUE, YELLOW] + for title, color in zip(titles, colors): + fix_percent(title[0][-1]) + title[0].set_color(color) + + titles.scale(1.25) + titles.arrange(DOWN, buff=1.5) + titles.to_corner(UL) + return titles + + def get_reviews(self, titles): + return VGroup( + self.get_plusses_and_minuses( + titles[0], 5, 2, 0, + ), + self.get_plusses_and_minuses( + titles[1], 5, 10, 2, + ), + self.get_plusses_and_minuses( + titles[2], 8, 25, 14, + ), + ) + + def get_plusses_and_minuses(self, title, n_rows, n_cols, n_minus): + check = TexMobject(CMARK_TEX, color=GREEN) + cross = TexMobject(XMARK_TEX, color=RED) + checks = VGroup(*[ + check.copy() for x in range(n_rows * n_cols) + ]) + checks.arrange_in_grid(n_rows=n_rows, n_cols=n_cols) + checks.scale(0.5) + # if checks.get_height() > title.get_height(): + # checks.match_height(title) + checks.next_to(title, RIGHT, LARGE_BUFF) + + for check in random.sample(list(checks), n_minus): + mob = cross.copy() + mob.replace(check, dim_to_match=0) + check.become(mob) + + return checks + + +class PreviewThreeVideos(Scene): + def construct(self): + # Write equations + equations = VGroup( + TexMobject("{10", "\\over", "10}", "=", "100\\%"), + TexMobject("{48", "\\over", "50}", "=", "96\\%"), + TexMobject("{186", "\\over", "200}", "=", "93\\%"), + ) + equations.arrange(RIGHT, buff=3) + equations.to_edge(UP) + + colors = [PINK, BLUE, YELLOW] + for eq, color in zip(equations, colors): + eq[-1].set_color(color) + fix_percent(eq[-1][-1]) + + vs_labels = VGroup(*[TextMobject("vs.") for x in range(2)]) + for eq1, eq2, vs in zip(equations, equations[1:], vs_labels): + vs.move_to(midpoint(eq1.get_right(), eq2.get_left())) + + self.add(equations) + self.add(vs_labels) + + # Show topics + title = TextMobject("To be explained:") + title.set_height(0.7) + title.next_to(equations, DOWN, LARGE_BUFF) + title.to_edge(LEFT) + title.add(Underline(title)) + + topics = VGroup( + TextMobject("Binomial distributions"), + TextMobject("Bayesian updating"), + TextMobject("Probability density functions"), + TextMobject("Beta distribution"), + ) + topics.arrange(DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT) + topics.next_to(title, DOWN, MED_LARGE_BUFF) + topics.to_edge(LEFT, buff=LARGE_BUFF) + + bullets = VGroup() + for topic in topics: + bullet = Dot() + bullet.next_to(topic, LEFT) + bullets.add(bullet) + + self.play( + Write(title), + Write(bullets), + run_time=1, + ) + self.play(LaggedStart(*[ + FadeIn(topic, lag_ratio=0.1) + for topic in topics + ], run_time=3, lag_ratio=0.3)) + self.wait() + + # Show videos + images = [ + ImageMobject(os.path.join( + consts.VIDEO_DIR, + OUTPUT_DIRECTORY, + "images", + name + )) + for name in ["Thumbnail1", "Thumbnail2", "Thumbnail3"] + ] + thumbnails = Group() + for image in images: + image.set_width(FRAME_WIDTH / 3 - 1) + rect = SurroundingRectangle(image, buff=0) + rect.set_stroke(WHITE, 3) + rect.set_fill(BLACK, 1) + thumbnails.add(Group(rect, image)) + + thumbnails.arrange(RIGHT, buff=LARGE_BUFF) + + for topic, i in zip(topics, [0, 1, 1, 2]): + thumbnail = thumbnails[i] + topic.generate_target() + topic.target.scale(0.6) + topic.target.next_to(thumbnail, DOWN, aligned_edge=LEFT) + topics[2].target.next_to( + topics[1].target, DOWN, + aligned_edge=LEFT, + ) + + self.play( + FadeOut(title, LEFT), + FadeOut(bullets, LEFT), + LaggedStartMap(MoveToTarget, topics), + LaggedStartMap(FadeIn, thumbnails), + ) + self.wait() + + tn_groups = Group( + Group(thumbnails[0], topics[0]), + Group(thumbnails[1], topics[1], topics[2]), + Group(thumbnails[2], topics[3]), + ) + + setup_words = TextMobject("Set up the model") + analysis_words = TextMobject("Analysis") + for words in [setup_words, analysis_words]: + words.scale(topics[0][0].get_height() / words[0][0].get_height()) + words.set_color(YELLOW) + setup_words.move_to(topics[0], UL) + analysis_words.next_to(topics[3], DOWN, aligned_edge=LEFT) + + def set_opacity(mob, alpha): + for sm in mob.family_members_with_points(): + sm.set_opacity(alpha) + return mob + + self.play(ApplyFunction(lambda m: set_opacity(m, 0.2), tn_groups[1:])) + self.play( + FadeIn(setup_words, lag_ratio=0.1), + topics[0].next_to, setup_words, DOWN, {"aligned_edge": LEFT}, + ) + tn_groups[0].add(setup_words) + self.wait(2) + for i in 0, 1: + self.play( + ApplyFunction(lambda m: set_opacity(m, 0.2), tn_groups[i]), + ApplyFunction(lambda m: set_opacity(m, 1), tn_groups[i + 1]), + ) + self.wait(2) + self.play(FadeIn(analysis_words, 0.25 * UP)) + tn_groups[2].add(analysis_words) + self.wait(2) + + self.play( + FadeOut(setup_words), + FadeOut(topics[0]), + FadeOut(tn_groups[1]), + FadeOut(tn_groups[2]), + FadeOut(vs_labels, UP), + FadeOut(equations, UP), + ApplyFunction(lambda m: set_opacity(m, 1), thumbnails[0]), + ) + thumbnails[0].generate_target() + # thumbnails[0].target.set_width(FRAME_WIDTH) + # thumbnails[0].target.center() + thumbnails[0].target.to_edge(UP) + self.play(MoveToTarget(thumbnails[0], run_time=4)) + self.wait() + + +class LetsLookAtOneAnswer(TeacherStudentsScene): + def construct(self): + self.remove(self.background) + self.teacher_says( + "Let me show you\\\\one answer.", + added_anims=[ + self.get_student_changes("pondering", "thinking", "pondering") + ] + ) + self.look_at(self.screen) + self.change_all_student_modes("thinking", look_at_arg=self.screen) + self.wait(4) + + +class LaplacesRuleOfSuccession(Scene): + def construct(self): + # Setup + title = TextMobject("How to read a rating") + title.set_height(0.75) + title.to_edge(UP) + underline = Underline(title) + underline.scale(1.2) + self.add(title, underline) + + data = get_checks_and_crosses(11 * [True] + [False], width=10) + data.shift(DOWN) + underlines = get_underlines(data) + + real_data = data[:10] + fake_data = data[10:] + + def get_review_label(num, denom): + result = VGroup( + Integer(num, color=GREEN), + TextMobject("out of"), + Integer(denom), + ) + result.arrange(RIGHT) + result.set_height(0.6) + return result + + review_label = get_review_label(10, 10) + review_label.next_to(data[:10], UP, MED_LARGE_BUFF) + + # Show initial review + self.add(review_label) + self.add(underlines[:10]) + + self.play( + ShowIncreasingSubsets(real_data, int_func=np.ceil), + CountInFrom(review_label[0], 0), + rate_func=lambda t: smooth(t, 3), + ) + self.wait() + + # Fake data + fd_rect = SurroundingRectangle(VGroup(fake_data, underlines[10:])) + fd_rect.set_stroke(WHITE, 2) + fd_rect.set_fill(GREY_E, 1) + + fd_label = TextMobject("Pretend you see\\\\two more") + fd_label.next_to(fd_rect, DOWN) + fd_label.shift_onto_screen() + + self.play( + FadeIn(fd_label, UP), + DrawBorderThenFill(fd_rect), + ShowCreation(underlines[10:]) + ) + self.wait() + for mark in data[10:]: + self.play(Write(mark)) + self.wait() + + # Update rating + review_center = VectorizedPoint(review_label.get_center()) + pretend_label = TextMobject("Pretend that it's") + pretend_label.match_width(review_label) + pretend_label.next_to(review_label, UP, MED_LARGE_BUFF) + pretend_label.match_x(data) + pretend_label.set_color(BLUE_D) + + old_review_label = VGroup(Integer(0), TextMobject("out of"), Integer(0)) + old_review_label.become(review_label) + + self.add(old_review_label, review_label) + self.play( + review_center.set_x, data.get_center()[0], + MaintainPositionRelativeTo(review_label, review_center), + UpdateFromAlphaFunc( + review_label[0], + lambda m, a: m.set_value(int(interpolate(10, 11, a))) + ), + UpdateFromAlphaFunc( + review_label[2], + lambda m, a: m.set_value(int(interpolate(10, 12, a))) + ), + FadeIn(pretend_label, LEFT), + old_review_label.scale, 0.5, + old_review_label.set_opacity, 0.5, + old_review_label.to_edge, LEFT, + ) + self.wait() + + # Show fraction + eq = TexMobject( + "{11", "\\over", "12}", + "\\approx", "91.7\\%" + ) + fix_percent(eq[-1][-1]) + eq.set_color_by_tex("11", GREEN) + + eq.next_to(pretend_label, RIGHT) + eq.to_edge(RIGHT, buff=MED_LARGE_BUFF) + + self.play(Write(eq)) + self.wait() + self.play(ShowCreationThenFadeAround(eq)) + self.wait() + + # Remove clutter + old_review_label.generate_target() + old_review_label.target.next_to(title, DOWN, LARGE_BUFF) + old_review_label.target.to_edge(LEFT) + old_review_label.target.set_opacity(1) + arrow = Vector(0.5 * RIGHT) + arrow.next_to(old_review_label.target, RIGHT) + + self.play( + MoveToTarget(old_review_label), + FadeIn(arrow), + eq.next_to, arrow, RIGHT, + FadeOut( + VGroup( + fake_data, + underlines, + pretend_label, + review_label, + fd_rect, fd_label, + ), + DOWN, + lag_ratio=0.01, + ), + real_data.match_width, old_review_label.target, + real_data.next_to, old_review_label.target, DOWN, + ) + self.wait() + + # Show 48 of 50 case + # Largely copied from above...not great + data = get_checks_and_crosses( + 48 * [True] + 2 * [False] + [True, False], + width=FRAME_WIDTH - 1, + ) + data.shift(DOWN) + underlines = get_underlines(data) + + review_label = get_review_label(48, 50) + review_label.next_to(data, UP, MED_LARGE_BUFF) + + true_data = data[:-2] + fake_data = data[-2:] + + fd_rect.replace(fake_data, stretch=True) + fd_rect.stretch(1.2, 0) + fd_rect.stretch(2.2, 1) + fd_rect.shift(0.025 * DOWN) + fd_label.next_to(fd_rect, DOWN, LARGE_BUFF) + fd_label.shift_onto_screen() + fd_arrow = Arrow(fd_label.get_top(), fd_rect.get_corner(DL)) + + self.play( + FadeIn(underlines[:-2]), + ShowIncreasingSubsets(true_data, int_func=np.ceil), + CountInFrom(review_label[0], 0), + UpdateFromAlphaFunc( + review_label, + lambda m, a: m.set_opacity(a), + ), + ) + self.wait() + self.play( + FadeIn(fd_label), + GrowArrow(fd_arrow), + FadeIn(fd_rect), + Write(fake_data), + Write(underlines[-2:]), + ) + self.wait() + + # Pretend it's 49 / 52 + old_review_label = VGroup(Integer(0), TextMobject("out of"), Integer(0)) + old_review_label.become(review_label) + review_center = VectorizedPoint(review_label.get_center()) + + self.play( + review_center.set_x, data.get_center()[0] + 3, + MaintainPositionRelativeTo(review_label, review_center), + UpdateFromAlphaFunc( + review_label[0], + lambda m, a: m.set_value(int(interpolate(48, 49, a))) + ), + UpdateFromAlphaFunc( + review_label[2], + lambda m, a: m.set_value(int(interpolate(50, 52, a))) + ), + old_review_label.scale, 0.5, + old_review_label.to_edge, LEFT, + ) + self.wait() + + arrow2 = Vector(0.5 * RIGHT) + arrow2.next_to(old_review_label, RIGHT) + + eq2 = TexMobject( + "{49", "\\over", "52}", + "\\approx", "94.2\\%" + ) + fix_percent(eq2[-1][-1]) + eq2[0].set_color(GREEN) + eq2.next_to(arrow2, RIGHT) + eq2.save_state() + eq2[1].set_opacity(0) + eq2[3:].set_opacity(0) + eq2[0].replace(review_label[0]) + eq2[2].replace(review_label[2]) + + self.play( + Restore(eq2, run_time=1.5), + FadeIn(arrow2), + ) + self.wait() + + faders = VGroup( + fd_rect, fd_arrow, fd_label, + fake_data, underlines, + review_label, + ) + self.play( + FadeOut(faders), + true_data.match_width, old_review_label, + true_data.next_to, old_review_label, DOWN, + ) + + # 200 review case + final_review_label = get_review_label(186, 200) + final_review_label.match_height(old_review_label) + final_review_label.move_to(old_review_label, LEFT) + final_review_label.shift( + arrow2.get_center() - + arrow.get_center() + ) + + data = get_checks_and_crosses([True] * 186 + [False] * 14 + [True, False]) + data[:200].arrange_in_grid(10, 20, buff=0) + data[-2:].next_to(data[:200], DOWN, buff=0) + data.set_width(FRAME_WIDTH / 2 - 1) + data.to_edge(RIGHT, buff=MED_SMALL_BUFF) + data.to_edge(DOWN) + for mark in data: + mark.scale(0.5) + + true_data = data[:-2] + fake_data = data[-2:] + + self.play( + UpdateFromAlphaFunc( + final_review_label, + lambda m, a: m.set_opacity(a), + ), + CountInFrom(final_review_label[0], 0), + ShowIncreasingSubsets(true_data), + ) + self.wait() + + arrow3 = Vector(0.5 * RIGHT) + arrow3.next_to(final_review_label, RIGHT) + + eq3 = TexMobject( + "{187", "\\over", "202}", + "\\approx", "92.6\\%" + ) + fix_percent(eq3[-1][-1]) + eq3[0].set_color(GREEN) + eq3.next_to(arrow3, RIGHT) + + self.play( + GrowArrow(arrow3), + FadeIn(eq3), + Write(fake_data) + ) + self.wait() + self.play( + true_data.match_width, final_review_label, + true_data.next_to, final_review_label, DOWN, + FadeOut(fake_data) + ) + self.wait() + + # Make a selection + rect = SurroundingRectangle(VGroup(eq2, old_review_label)) + rect.set_stroke(YELLOW, 2) + + self.play( + ShowCreation(rect), + eq2[-1].set_color, YELLOW, + ) + self.wait() + + # Retitle + name = TextMobject("Laplace's rule of succession") + name.match_height(title) + name.move_to(title) + name.set_color(TEAL) + + self.play( + FadeInFromDown(name), + FadeOut(title, UP), + underline.match_width, name, + ) + self.wait() + + +class AskWhy(TeacherStudentsScene): + def construct(self): + self.student_says( + "Wait...why?", + look_at_arg=self.screen, + ) + self.play( + self.students[0].change, "confused", self.screen, + self.students[1].change, "confused", self.screen, + self.teacher.change, "tease", self.students[2].eyes, + ) + self.wait(3) + + self.students[2].bubble.content.unlock_triangulation() + self.student_says( + "Is that really\\\\the answer?", + target_mode="raise_right_hand", + added_anims=[self.teacher.change, "thinking"], + ) + self.wait(2) + self.teacher_says("Let's dive in!", target_mode="hooray") + self.change_all_student_modes("hooray") + self.wait(3) + + +class BinomialName(Scene): + def construct(self): + text = TextMobject("Probabilities of probabilities\\\\", "Part 1") + text.set_width(FRAME_WIDTH - 1) + text[0].set_color(BLUE) + self.add(text[0]) + self.play(Write(text[1], run_time=2)) + self.wait(2) + + +class WhatsTheModel(Scene): + CONFIG = { + "random_seed": 5, + } + + def construct(self): + self.add_questions() + self.introduce_buyer_and_seller() + + for x in range(3): + self.play(*self.experience_animations(self.seller, self.buyer)) + self.wait() + + self.add_probability_label() + self.bring_up_goal() + + def add_questions(self): + questions = VGroup( + TextMobject("What's the model?"), + TextMobject("What are you optimizing?"), + ) + for question, vect in zip(questions, [LEFT, RIGHT]): + question.move_to(vect * FRAME_WIDTH / 4) + questions.arrange(DOWN, buff=LARGE_BUFF) + questions.scale(1.5) + + # Intro questions + self.play(FadeIn(questions[0])) + self.play(FadeIn(questions[1], UP)) + self.wait() + questions[1].save_state() + + self.questions = questions + + def introduce_buyer_and_seller(self): + if hasattr(self, "questions"): + questions = self.questions + added_anims = [ + questions[0].to_edge, UP, + questions[1].set_opacity, 0.5, + questions[1].scale, 0.25, + questions[1].to_corner, UR, + ] + else: + added_anims = [] + + seller = Randolph(mode="coin_flip_1") + seller.to_edge(LEFT) + seller.label = TextMobject("Seller") + + buyer = Mortimer() + buyer.to_edge(RIGHT) + buyer.label = TextMobject("Buyer") + + VGroup(buyer, seller).shift(DOWN) + + labels = VGroup() + for pi in seller, buyer: + pi.set_height(2) + pi.label.scale(1.5) + pi.label.next_to(pi, DOWN, MED_LARGE_BUFF) + labels.add(pi.label) + buyer.make_eye_contact(seller) + + self.play( + LaggedStartMap( + FadeInFromDown, VGroup(seller, buyer, *labels), + lag_ratio=0.2 + ), + *added_anims + ) + self.wait() + + self.buyer = buyer + self.seller = seller + + def add_probability_label(self): + seller = self.seller + buyer = self.buyer + + label = get_prob_positive_experience_label() + label.add(TexMobject("=").next_to(label, RIGHT)) + rhs = DecimalNumber(0.75) + rhs.next_to(label, RIGHT) + rhs.align_to(label[0], DOWN) + label.add(rhs) + label.scale(1.5) + label.next_to(seller, UP, MED_LARGE_BUFF, aligned_edge=LEFT) + + rhs.set_color(YELLOW) + brace = Brace(rhs, UP) + success_rate = brace.get_text("Success rate")[0] + s_sym = brace.get_tex("s").scale(1.5, about_edge=DOWN) + success_rate.match_color(rhs) + s_sym.match_color(rhs) + + self.add(label) + + self.play( + GrowFromCenter(brace), + FadeIn(success_rate, 0.5 * DOWN) + ) + self.wait() + self.play( + TransformFromCopy(success_rate[0], s_sym), + FadeOut(success_rate, 0.1 * RIGHT, lag_ratio=0.1), + ) + for x in range(2): + self.play(*self.experience_animations(seller, buyer, arc=30 * DEGREES)) + self.wait() + + grey_box = SurroundingRectangle(rhs, buff=SMALL_BUFF) + grey_box.set_stroke(GREY_E, 0.5) + grey_box.set_fill(GREY_D, 1) + lil_q_marks = TexMobject("???") + lil_q_marks.scale(0.5) + lil_q_marks.next_to(buyer, UP) + + self.play( + FadeOut(rhs, 0.5 * DOWN), + FadeIn(grey_box, 0.5 * UP), + FadeIn(lil_q_marks, DOWN), + buyer.change, "confused", grey_box, + ) + rhs.set_opacity(0) + for x in range(2): + self.play(*self.experience_animations(seller, buyer, arc=30 * DEGREES)) + self.play(buyer.change, "confused", lil_q_marks) + self.play(Blink(buyer)) + + self.prob_group = VGroup( + label, grey_box, brace, s_sym, + ) + self.buyer_q_marks = lil_q_marks + + def bring_up_goal(self): + prob_group = self.prob_group + questions = self.questions + questions.generate_target() + questions.target[1].replace(questions[0], dim_to_match=1) + questions.target[1].match_style(questions[0]) + questions.target[0].replace(questions[1], dim_to_match=1) + questions.target[0].match_style(questions[1]) + + prob_group.generate_target() + prob_group.target.scale(0.5) + prob_group.target.next_to(self.seller, RIGHT) + + self.play( + FadeOut(self.buyer_q_marks), + self.buyer.change, "pondering", questions[0], + self.seller.change, "pondering", questions[0], + MoveToTarget(prob_group), + MoveToTarget(questions), + ) + self.play(self.seller.change, "coin_flip_1") + for x in range(3): + self.play(*self.experience_animations(self.seller, self.buyer)) + self.wait() + + # + def experience_animations(self, seller, buyer, arc=-30 * DEGREES, p=0.75): + positive = (random.random() < p) + words = TextMobject( + "Positive\\\\experience" + if positive else + "Negative\\\\experience" + ) + words.set_color(GREEN if positive else RED) + if positive: + new_mode = random.choice([ + "hooray", + "coin_flip_1", + ]) + else: + new_mode = random.choice([ + "tired", + "angry", + "sad", + ]) + + words.move_to(seller.get_corner(UR)) + result = [ + ApplyMethod( + words.move_to, buyer.get_corner(UL), + path_arc=arc, + run_time=2 + ), + VFadeInThenOut(words, run_time=2), + ApplyMethod( + buyer.change, new_mode, seller.eyes, + run_time=2, + rate_func=squish_rate_func(smooth, 0.5, 1), + ), + ApplyMethod( + seller.change, "coin_flip_2", buyer.eyes, + rate_func=there_and_back, + ), + ] + return result + + +class IsSellerOne100(Scene): + def construct(self): + self.add_review() + self.show_probability() + self.show_simulated_reviews() + + def add_review(self): + reviews = VGroup(*[TexMobject(CMARK_TEX) for x in range(10)]) + reviews.arrange(RIGHT) + reviews.scale(2) + reviews.set_color(GREEN) + reviews.next_to(ORIGIN, UP) + + blanks = VGroup(*[ + Line(LEFT, RIGHT).match_width(rev).next_to(rev, DOWN, SMALL_BUFF) + for rev in reviews + ]) + blanks.shift(0.25 * reviews[0].get_width() * LEFT) + + label = TextMobject( + " out of ", + ) + tens = VGroup(*[Integer(10) for x in range(2)]) + tens[0].next_to(label, LEFT) + tens[1].next_to(label, RIGHT) + tens.set_color(GREEN) + label.add(tens) + label.scale(2) + label.next_to(reviews, DOWN, LARGE_BUFF) + + self.add(label) + self.add(blanks) + tens[0].to_count = reviews + self.play( + ShowIncreasingSubsets(reviews, int_func=np.ceil), + UpdateFromAlphaFunc( + tens[0], + lambda m, a: m.set_color( + interpolate_color(RED, GREEN, a) + ).set_value(len(m.to_count)) + ), + run_time=2, + rate_func=bezier([0, 0, 1, 1]), + ) + self.wait() + + self.review_group = VGroup(reviews, blanks, label) + + def show_probability(self): + review_group = self.review_group + + prob_label = get_prob_positive_experience_label() + prob_label.add(TexMobject("=").next_to(prob_label, RIGHT)) + rhs = DecimalNumber(1) + rhs.next_to(prob_label, RIGHT) + rhs.set_color(YELLOW) + prob_label.add(rhs) + prob_label.scale(2) + prob_label.to_corner(UL) + + q_mark = TexMobject("?") + q_mark.set_color(YELLOW) + q_mark.match_height(rhs) + q_mark.reference = rhs + q_mark.add_updater(lambda m: m.next_to(m.reference, RIGHT)) + + rhs_rect = SurroundingRectangle(rhs, buff=0.2) + rhs_rect.set_color(RED) + + not_necessarily = TextMobject("Not necessarily!") + not_necessarily.set_color(RED) + not_necessarily.scale(1.5) + not_necessarily.next_to(prob_label, DOWN, 1.5) + arrow = Arrow( + not_necessarily.get_top(), + rhs_rect.get_corner(DL), + buff=MED_SMALL_BUFF, + ) + arrow.set_color(RED) + + rhs.set_value(0) + self.play( + ChangeDecimalToValue(rhs, 1), + UpdateFromAlphaFunc( + prob_label, + lambda m, a: m.set_opacity(a), + ), + FadeIn(q_mark), + ) + self.wait() + self.play( + ShowCreation(rhs_rect), + Write(not_necessarily), + ShowCreation(arrow), + review_group.to_edge, DOWN, + run_time=1, + ) + self.wait() + self.play( + ChangeDecimalToValue(rhs, 0.95), + FadeOut(rhs_rect), + FadeOut(arrow), + FadeOut(not_necessarily), + ) + self.wait() + + self.prob_label_group = VGroup( + prob_label, rhs, q_mark, + ) + + def show_simulated_reviews(self): + prob_label_group = self.prob_label_group + review_group = self.review_group + + # Set up decimals + random.seed(2) + decimals = VGroup() + for x in range(10): + dec = DecimalNumber() + decimals.add(dec) + + def randomize_decimals(decimals): + for dec in decimals: + value = random.random() + dec.set_value(value) + if value > 0.95: + dec.set_color(RED) + else: + dec.set_color(WHITE) + + randomize_decimals(decimals) + + decimals.set_height(0.3) + decimals.arrange(RIGHT, buff=MED_LARGE_BUFF) + decimals.next_to(ORIGIN, DOWN) + decimals[0].set_value(0.42) + decimals[0].set_color(WHITE) + decimals[1].set_value(0.97) + decimals[1].set_color(RED) + + random_label = TextMobject("Random number\\\\in [0, 1]") + random_label.scale(0.7) + random_label.next_to(decimals[0], DOWN) + random_label.set_color(GREY_B) + + arrows = VGroup() + for dec in decimals: + arrow = Vector(0.4 * UP) + arrow.next_to(dec, UP) + arrows.add(arrow) + + # Set up marks + def get_marks(decs, arrows): + marks = VGroup() + for dec, arrow in zip(decs, arrows): + if dec.get_value() < 0.95: + mark = TexMobject(CMARK_TEX) + mark.set_color(GREEN) + else: + mark = TexMobject(XMARK_TEX) + mark.set_color(RED) + mark.set_height(0.5) + mark.next_to(arrow, UP) + marks.add(mark) + return marks + + marks = get_marks(decimals, arrows) + + lt_p95 = TexMobject("< 0.95") + gte_p95 = TexMobject("\\ge 0.95") + for label in lt_p95, gte_p95: + label.match_height(decimals[0]) + + lt_p95.next_to(decimals[0], RIGHT, MED_SMALL_BUFF) + gte_p95.next_to(decimals[1], RIGHT, MED_SMALL_BUFF) + lt_p95.set_color(GREEN) + gte_p95.set_color(RED) + + # Introduce simulation + review_group.save_state() + self.play( + review_group.scale, 0.25, + review_group.to_corner, UR, + Write(random_label), + CountInFrom(decimals[0], 0), + ) + self.wait() + self.play(FadeIn(lt_p95, LEFT)) + self.play( + GrowArrow(arrows[0]), + FadeIn(marks[0], DOWN) + ) + self.wait() + self.play( + FadeOut(lt_p95, 0.5 * RIGHT), + FadeIn(gte_p95, 0.5 * LEFT), + ) + self.play( + random_label.match_x, decimals[1], + CountInFrom(decimals[1], 0), + UpdateFromAlphaFunc( + decimals[1], + lambda m, a: m.set_opacity(a), + ), + ) + self.play( + GrowArrow(arrows[1]), + FadeIn(marks[1], DOWN), + ) + self.wait() + self.play( + LaggedStartMap( + CountInFrom, decimals[2:], + ), + UpdateFromAlphaFunc( + decimals[2:], + lambda m, a: m.set_opacity(a), + ), + FadeOut(gte_p95), + run_time=1, + ) + self.add(decimals) + self.play( + LaggedStartMap(GrowArrow, arrows[2:]), + LaggedStartMap(FadeInFromDown, marks[2:]), + run_time=1 + ) + self.add(arrows, marks) + self.wait() + + # Add new rows + decimals.arrows = arrows + decimals.add_updater(lambda d: d.next_to(d.arrows, DOWN)) + added_anims = [FadeOut(random_label)] + rows = VGroup(marks) + for x in range(3): + self.play( + arrows.shift, DOWN, + UpdateFromFunc(decimals, randomize_decimals), + *added_anims, + ) + added_anims = [] + new_marks = get_marks(decimals, arrows) + self.play(LaggedStartMap(FadeInFromDown, new_marks)) + self.wait() + rows.add(new_marks) + + # Create a stockpile of new rows + added_rows = VGroup() + decimals.clear_updaters() + decimals.save_state() + for x in range(100): + randomize_decimals(decimals) + added_rows.add(get_marks(decimals, arrows)) + decimals.restore() + + # Compress rows + rows.generate_target() + for group in rows.target, added_rows: + group.scale(0.3) + for row in group: + row.arrange(RIGHT, buff=SMALL_BUFF) + group.arrange(DOWN, buff=0.2) + rows.target.next_to(prob_label_group, DOWN, MED_LARGE_BUFF) + rows.target.set_x(-3.5) + + nr = 15 + added_rows[:nr].move_to(rows.target, UP) + added_rows[nr:2 * nr].move_to(rows.target, UP) + added_rows[nr:2 * nr].shift(3.5 * RIGHT) + added_rows[2 * nr:3 * nr].move_to(rows.target, UP) + added_rows[2 * nr:3 * nr].shift(7 * RIGHT) + added_rows = added_rows[4:3 * nr] + + self.play( + MoveToTarget(rows), + FadeOut(decimals), + FadeOut(arrows), + ) + self.play(ShowIncreasingSubsets(added_rows), run_time=3) + + # Show scores + all_rows = VGroup(*rows, *added_rows) + scores = VGroup() + ten_rects = VGroup() + for row in all_rows: + score = Integer(sum([ + mark.get_color() == Color(GREEN) + for mark in row + ])) + score.match_height(row) + score.next_to(row, RIGHT) + if score.get_value() == 10: + score.set_color(TEAL) + ten_rects.add(SurroundingRectangle(score)) + scores.add(score) + + ten_rects.set_stroke(YELLOW, 2) + + self.play(FadeIn(scores)) + self.wait() + self.play(LaggedStartMap(ShowCreation, ten_rects)) + self.play(LaggedStartMap(FadeOut, ten_rects)) + self.wait(2) + + # Show alternate possibilities + prob = DecimalNumber(0.95) + prob.set_color(YELLOW) + template = prob_label_group[0][-1] + prob.match_height(template) + prob.move_to(template, LEFT) + rect = BackgroundRectangle(template, buff=SMALL_BUFF) + rect.set_fill(BLACK, 1) + self.add(rect) + self.add(prob) + self.play( + LaggedStartMap(FadeOutAndShift, all_rows, lag_ratio=0.01), + LaggedStartMap(FadeOutAndShift, scores, lag_ratio=0.01), + Restore(review_group), + ) + for value in [0.9, 0.99, 0.8, 0.95]: + self.play(ChangeDecimalToValue(prob, value)) + self.wait() + + # No longer used + def show_random_numbers(self): + prob_label_group = self.prob_label_group + + random.seed(2) + rows = VGroup(*[ + VGroup(*[ + Integer( + random.randint(0, 99) + ).move_to(0.85 * x * RIGHT) + for x in range(10) + ]) + for y in range(10 * 2) + ]) + rows.arrange_in_grid(n_cols=2, buff=MED_LARGE_BUFF) + rows[:10].shift(LEFT) + rows.set_height(5.5) + rows.center().to_edge(DOWN) + + lt_95 = VGroup(*[ + mob + for row in rows + for mob in row + if mob.get_value() < 95 + ]) + + square = Square() + square.set_stroke(width=0) + square.set_fill(YELLOW, 0.5) + square.set_width(1.5 * rows[0][0].get_height()) + # highlights = VGroup(*[ + # square.copy().move_to(mob) + # for row in rows + # for mob in row + # if mob.get_value() < 95 + # ]) + + row_rects = VGroup(*[ + SurroundingRectangle(row) + for row in rows + if all([m.get_value() < 95 for m in row]) + ]) + row_rects.set_stroke(GREEN, 2) + + self.play( + LaggedStartMap( + ShowIncreasingSubsets, rows, + run_time=3, + lag_ratio=0.25, + ), + FadeOut(self.review_group, DOWN), + prob_label_group.set_height, 0.75, + prob_label_group.to_corner, UL, + ) + self.wait() + # self.add(highlights, rows) + self.play( + # FadeIn(highlights) + lt_95.set_fill, BLUE, + lt_95.set_stroke, BLUE, 2, {"background": True}, + ) + self.wait() + self.play(LaggedStartMap(ShowCreation, row_rects)) + self.wait() + + +class LookAtAllPossibleSuccessRates(Scene): + def construct(self): + axes = get_beta_dist_axes(y_max=6, y_unit=1) + dist = scipy.stats.beta(10, 2) + graph = axes.get_graph(dist.pdf) + graph.set_stroke(BLUE, 3) + flat_graph = graph.copy() + flat_graph.points[:, 1] = axes.c2p(0, 0)[1] + flat_graph.set_stroke(YELLOW, 3) + + x_labels = axes.x_axis.numbers + x_labels.set_opacity(0) + + sellers = VGroup(*[ + self.get_example_seller(label.get_value()) + for label in x_labels + ]) + sellers.arrange(RIGHT, buff=LARGE_BUFF) + sellers.set_width(FRAME_WIDTH - 1) + sellers.to_edge(UP, buff=LARGE_BUFF) + + sellers.generate_target() + for seller, label in zip(sellers.target, x_labels): + seller.next_to(label, DOWN) + seller[0].set_opacity(0) + seller[1].set_opacity(0) + seller[2].replace(label, dim_to_match=1) + + x_label = TextMobject("All possible success rates") + x_label.next_to(axes.c2p(0.5, 0), UP) + x_label.shift(2 * LEFT) + + y_axis_label = TextMobject( + "A kind of probability\\\\", + "of probabilities" + ) + y_axis_label.scale(0.75) + y_axis_label.next_to(axes.y_axis, RIGHT) + y_axis_label.to_edge(UP) + y_axis_label[1].set_color(YELLOW) + + graph_label = TextMobject( + "Some notion of likelihood\\\\", + "for each one" + ) + graph_label[1].align_to(graph_label[0], LEFT) + graph_label.next_to(graph.get_boundary_point(UP), UP) + graph_label.shift(0.5 * DOWN) + graph_label.to_edge(RIGHT) + + x_axis_line = Line(axes.c2p(0, 0), axes.c2p(1, 0)) + x_axis_line.set_stroke(YELLOW, 3) + + shuffled_sellers = VGroup(*sellers) + shuffled_sellers.shuffle() + self.play(GrowFromCenter(shuffled_sellers[0])) + self.play(LaggedStartMap( + FadeInFromPoint, shuffled_sellers[1:], + lambda m: (m, sellers.get_center()) + )) + self.wait() + self.play( + MoveToTarget(sellers), + FadeIn(axes), + run_time=2, + ) + + self.play( + x_label.shift, 4 * RIGHT, + UpdateFromAlphaFunc( + x_label, + lambda m, a: m.set_opacity(a), + rate_func=there_and_back, + ), + ShowCreation(x_axis_line), + run_time=3, + ) + self.play(FadeOut(x_axis_line)) + self.wait() + self.play( + FadeInFromDown(graph_label), + ReplacementTransform(flat_graph, graph), + ) + self.wait() + self.play(FadeInFromDown(y_axis_label)) + + # Show probabilities + x_tracker = ValueTracker(0.5) + + prob_label = get_prob_positive_experience_label(True, True, True) + prob_label.next_to(axes.c2p(0, 2), RIGHT, MED_LARGE_BUFF) + prob_label.decimal.tracker = x_tracker + prob_label.decimal.add_updater( + lambda m: m.set_value(m.tracker.get_value()) + ) + + v_line = Line(DOWN, UP) + v_line.set_stroke(YELLOW, 2) + v_line.tracker = x_tracker + v_line.graph = graph + v_line.axes = axes + v_line.add_updater( + lambda m: m.put_start_and_end_on( + m.axes.x_axis.n2p(m.tracker.get_value()), + m.axes.input_to_graph_point(m.tracker.get_value(), m.graph), + ) + ) + + self.add(v_line) + for x in [0.95, 0.8, 0.9]: + self.play( + x_tracker.set_value, x, + run_time=4, + ) + self.wait() + + def get_example_seller(self, success_rate): + randy = Randolph(mode="coin_flip_1", height=1) + label = TexMobject("s = ") + decimal = DecimalNumber(success_rate) + decimal.match_height(label) + decimal.next_to(label[-1], RIGHT) + label.set_color(YELLOW) + decimal.set_color(YELLOW) + VGroup(label, decimal).next_to(randy, DOWN) + result = VGroup(randy, label, decimal) + result.randy = randy + result.label = label + result.decimal = decimal + return result + + +class AskAboutUnknownProbabilities(Scene): + def construct(self): + # Setup + unknown_title, prob_title = titles = self.get_titles() + + v_line = Line(UP, DOWN) + v_line.set_height(FRAME_HEIGHT) + v_line.set_stroke([WHITE, LIGHT_GREY], 3) + h_line = Line(LEFT, RIGHT) + h_line.set_width(FRAME_WIDTH) + h_line.next_to(titles, DOWN) + + processes = VGroup( + get_random_coin(shuffle_time=1), + get_random_die(shuffle_time=1.5), + get_random_card(shuffle_time=2), + ) + processes.arrange(DOWN, buff=0.7) + processes.next_to(unknown_title, DOWN, LARGE_BUFF) + processes_rect = BackgroundRectangle(processes) + processes_rect.set_fill(BLACK, 1) + + prob_labels = VGroup( + TexMobject("P(", "00", ")", "=", "1 / 2"), + TexMobject("P(", "00", ")", "=", "1 / 6}"), + TexMobject("P(", "00", ")", "=", "1 / 52}"), + ) + prob_labels.scale(1.5) + prob_labels.arrange(DOWN, aligned_edge=LEFT) + prob_labels.match_x(prob_title) + for pl, pr in zip(prob_labels, processes): + pl.match_y(pr) + content = pr[1].copy() + content.replace(pl[1], dim_to_match=0) + pl.replace_submobject(1, content) + + # Putting numbers to the unknown + number_rects = VGroup(*[ + SurroundingRectangle(pl[-1]) + for pl in prob_labels + ]) + number_rects.set_stroke(YELLOW, 2) + + for pl in prob_labels: + pl.save_state() + pl[:3].match_x(prob_title) + pl[3:].match_x(prob_title) + pl.set_opacity(0) + + self.add(processes) + self.play( + LaggedStartMap(FadeInFromDown, titles), + LaggedStart( + ShowCreation(v_line), + ShowCreation(h_line), + lag_ratio=0.1, + ), + LaggedStartMap(Restore, prob_labels), + run_time=1 + ) + self.wait(10) + # self.play( + # LaggedStartMap( + # ShowCreationThenFadeOut, + # number_rects, + # run_time=3, + # ) + # ) + # self.wait(2) + + # Highlight coin flip + fade_rects = VGroup(*[ + VGroup( + BackgroundRectangle(pl, buff=MED_SMALL_BUFF), + BackgroundRectangle(pr, buff=MED_SMALL_BUFF), + ) + for pl, pr in zip(prob_labels, processes) + ]) + fade_rects.set_fill(BLACK, 0.8) + + prob_half = prob_labels[0] + half = prob_half[-1] + half_underline = Line(LEFT, RIGHT) + half_underline.set_width(half.get_width() + MED_SMALL_BUFF) + half_underline.next_to(half, DOWN, buff=SMALL_BUFF) + half_underline.set_stroke(YELLOW, 3) + + self.play( + FadeIn(fade_rects[1]), + FadeIn(fade_rects[2]), + ) + self.wait(2) + self.play( + FadeIn(fade_rects[0]), + FadeOut(fade_rects[1]), + ) + self.wait(3) + self.play( + FadeOut(fade_rects[0]), + FadeOut(fade_rects[2]), + ) + self.wait(4) + + # Transition to question + processes.suspend_updating() + self.play( + LaggedStart( + FadeOut(unknown_title, UP), + FadeOut(prob_title, UP), + lag_ratio=0.2, + ), + FadeOut(h_line, UP, lag_ratio=0.1), + FadeOut(processes, LEFT, lag_ratio=0.1), + FadeOut(prob_labels[1]), + FadeOut(prob_labels[2]), + v_line.rotate, 90 * DEGREES, + v_line.shift, 0.6 * FRAME_HEIGHT * UP, + prob_half.center, + prob_half.to_edge, UP, + run_time=2, + ) + self.clear() + self.add(prob_half) + + arrow = Vector(UP) + arrow.next_to(half, DOWN) + question = TextMobject("What exactly does\\\\this mean?") + question.next_to(arrow, DOWN) + + self.play( + GrowArrow(arrow), + FadeIn(question, UP), + ) + self.wait(2) + self.play( + FadeOut(question, RIGHT), + Rotate(arrow, 90 * DEGREES), + VFadeOut(arrow), + ) + + # Show long run averages + self.show_many_coins(20, 50) + self.show_many_coins(40, 100) + + # Make probability itself unknown + q_marks = TexMobject("???") + q_marks.set_color(YELLOW) + q_marks.replace(half, dim_to_match=0) + + randy = Randolph(mode="confused") + randy.center() + randy.look_at(prob_half) + + self.play( + FadeOut(half, UP), + FadeIn(q_marks, DOWN), + ) + self.play(FadeIn(randy)) + self.play(Blink(randy)) + self.wait() + + # self.embed() + + def get_titles(self): + unknown_label = TextMobject("Random process") + prob_label = TextMobject("Long-run frequency") + titles = VGroup(unknown_label, prob_label) + titles.scale(1.25) + + unknown_label.move_to(FRAME_WIDTH * LEFT / 4) + prob_label.move_to(FRAME_WIDTH * RIGHT / 4) + titles.to_edge(UP, buff=MED_SMALL_BUFF) + titles.set_color(BLUE) + return titles + + def show_many_coins(self, n_rows, n_cols): + coin_choices = VGroup( + get_coin("H"), + get_coin("T"), + ) + coin_choices.set_stroke(width=0) + coins = VGroup(*[ + random.choice(coin_choices).copy() + for x in range(n_rows * n_cols) + ]) + + def organize_coins(coin_group): + coin_group.scale(1 / coin_group[0].get_height()) + coin_group.arrange_in_grid(n_rows=n_rows) + coin_group.set_width(FRAME_WIDTH - 1) + coin_group.to_edge(DOWN, MED_LARGE_BUFF) + + organize_coins(coins) + + sorted_coins = VGroup() + for coin in coins: + coin.generate_target() + sorted_coins.add(coin.target) + sorted_coins.submobjects.sort(key=lambda m: m.symbol) + organize_coins(sorted_coins) + + self.play(LaggedStartMap( + FadeInFrom, coins, + lambda m: (m, 0.2 * DOWN), + run_time=3, + rate_func=linear + )) + self.wait() + self.play(LaggedStartMap( + MoveToTarget, coins, + path_arc=30 * DEGREES, + run_time=2, + lag_ratio=1 / len(coins), + )) + self.wait() + self.play(FadeOut(coins)) + + +class AskProbabilityOfCoins(Scene): + def construct(self): + condition = VGroup( + TextMobject("If you've seen"), + Integer(80, color=BLUE_C), + get_coin("H").set_height(0.5), + TextMobject("and"), + Integer(20, color=RED_C), + get_coin("T").set_height(0.5), + ) + condition.arrange(RIGHT) + condition.to_edge(UP) + self.add(condition) + + question = TexMobject( + "\\text{What is }", + "P(", "00", ")", "?" + ) + coin = get_coin("H") + coin.replace(question.get_part_by_tex("00")) + question.replace_submobject( + question.index_of_part_by_tex("00"), + coin + ) + question.next_to(condition, DOWN) + self.add(question) + + values = ["H"] * 80 + ["T"] * 20 + random.shuffle(values) + + coins = VGroup(*[ + get_coin(symbol) + for symbol in values + ]) + coins.arrange_in_grid(10, 10, buff=MED_SMALL_BUFF) + coins.set_width(5) + coins.next_to(question, DOWN, MED_LARGE_BUFF) + + self.play( + ShowIncreasingSubsets(coins), + run_time=8, + rate_func=bezier([0, 0, 1, 1]) + ) + self.wait() + + self.embed() + + +class RunCarFactory(Scene): + def construct(self): + # Factory + factory = SVGMobject(file_name="factory") + factory.set_fill(GREY_D) + factory.set_stroke(width=0) + factory.flip() + factory.set_height(6) + factory.to_edge(LEFT) + + self.add(factory) + + # Dumb hack + l1 = Line( + factory[0].points[-200], + factory[0].points[-216], + ) + l2 = Line( + factory[0].points[-300], + factory[0].points[-318], + ) + for line in l1, l2: + square = Square() + square.set_fill(BLACK, 1) + square.set_stroke(width=0) + square.replace(line) + factory.add(square) + + rect = Rectangle() + rect.match_style(factory) + rect.set_height(1.1) + rect.set_width(6.75, stretch=True) + rect.move_to(factory, DL) + + # Get cars + car = Car(color=interpolate_color(BLUE_E, GREY_C, 0.5)) + car.set_height(0.9) + for tire in car.get_tires(): + tire.set_fill(GREY_C) + tire.set_stroke(BLACK) + car.randy.set_opacity(0) + car.move_to(rect.get_corner(DR)) + + cars = VGroup() + n_cars = 20 + for x in range(n_cars): + cars.add(car.copy()) + + for car in cars[4], cars[6]: + scratch = VMobject() + scratch.start_new_path(UP) + scratch.add_line_to(0.25 * DL) + scratch.add_line_to(0.25 * UR) + scratch.add_line_to(DOWN) + scratch.set_stroke([RED_A, RED_C], [0.1, 2, 2, 0.1]) + scratch.set_height(0.25) + scratch.move_to(car) + scratch.shift(0.1 * DOWN) + car.add(scratch) + + self.add(cars, rect) + self.play(LaggedStartMap( + MoveCar, cars, + lambda m: (m, m.get_corner(DR) + 10 * RIGHT), + lag_ratio=0.3, + rate_func=linear, + run_time=1.5 * n_cars, + )) + self.remove(cars) + + +class CarFactoryNumbers(Scene): + def construct(self): + # Test words + denom_words = TextMobject( + "in a test of 100 cars", + tex_to_color_map={"100": BLUE}, + ) + denom_words.to_corner(UR) + + numer_words = TextMobject( + "2 defects found", + tex_to_color_map={"2": RED} + ) + numer_words.move_to(denom_words, LEFT) + + self.play(Write(denom_words, run_time=1)) + self.wait() + self.play( + denom_words.next_to, numer_words, DOWN, {"aligned_edge": LEFT}, + FadeIn(numer_words), + ) + self.wait() + + # Question words + question = VGroup( + TextMobject("How do you plan"), + TextMobject("for"), + Integer(int(1e6), color=BLUE), + TextMobject("cars?") + ) + question[1:].arrange(RIGHT, aligned_edge=DOWN) + question[2].shift( + (question[2][1].get_bottom()[1] - question[2][0].get_bottom()[1]) * UP + ) + question[1:].next_to(question[0], DOWN, aligned_edge=LEFT) + question.next_to(denom_words, DOWN, LARGE_BUFF, aligned_edge=LEFT) + + self.play( + UpdateFromAlphaFunc( + question, + lambda m, a: m.set_opacity(a), + ), + CountInFrom(question[2], 0, run_time=1.5) + ) + self.wait() + + +class ComplainAboutSimplisticModel(TeacherStudentsScene): + def construct(self): + axes = self.get_experience_graph() + + self.add(axes) + self.play( + self.teacher.change, "raise_right_hand", axes, + self.get_student_changes( + "pondering", "erm", "sassy", + look_at_arg=axes, + ), + ShowCreation( + axes.graph, + run_time=3, + rate_func=linear, + ), + ) + self.wait(2) + + student = self.students[2] + bubble = SpeechBubble( + direction=LEFT, + height=3, + width=5, + ) + bubble.pin_to(student) + bubble.write("What about something\\\\like this?") + + self.play( + axes.next_to, student, UL, + VFadeOut(axes.graph), + FadeIn(bubble), + Write(bubble.content, run_time=1), + student.change, "raise_left_hand", + self.students[0].change, "thinking", axes, + self.students[1].change, "thinking", axes, + self.teacher.change, "happy", + ) + + new_graph = VMobject() + new_graph.set_points_as_corners([ + axes.c2p(0, 0.75), + axes.c2p(2, 0.9), + axes.c2p(4, 0.5), + axes.c2p(6, 0.75), + axes.c2p(8, 0.55), + axes.c2p(10, 0.95), + ]) + new_graph.make_smooth() + new_graph.set_stroke([YELLOW, RED, GREEN], 2) + + self.play( + ShowCreation(new_graph), + *[ + ApplyMethod(pi.look_at, new_graph) + for pi in self.pi_creatures + ] + ) + self.wait(3) + + def get_experience_graph(self): + axes = Axes( + x_min=-1, + x_max=10, + y_min=0, + y_max=1.25, + y_axis_config={ + "unit_size": 5, + "tick_frequency": 0.25, + "include_tip": False, + } + ) + axes.set_stroke(LIGHT_GREY, 1) + axes.set_height(3) + y_label = TextMobject("Experience quality") + y_label.scale(0.5) + y_label.next_to(axes.y_axis.get_top(), RIGHT, SMALL_BUFF) + axes.add(y_label) + + lines = VGroup() + for x in range(10): + lines.add( + Line(axes.c2p(x, 0), axes.c2p(x + 0.9, 0)) + ) + lines.set_stroke(RED, 3) + for line in lines: + if random.random() < 0.5: + line.set_y(axes.c2p(0, 1)[1]) + line.set_stroke(GREEN) + + axes.add(lines) + axes.graph = lines + + rect = BackgroundRectangle(axes, buff=0.25) + rect.set_stroke(WHITE, 1) + rect.set_fill(BLACK, 1) + + axes.add_to_back(rect) + axes.to_corner(UR) + + return axes + + +class ComingUpWrapper(Scene): + def construct(self): + background = FullScreenFadeRectangle() + background.set_fill(GREY_E, 1) + + title = TextMobject("What's coming...") + title.scale(1.5) + title.to_edge(UP) + + rect = ScreenRectangle() + rect.set_height(6) + rect.set_stroke(WHITE) + rect.set_fill(BLACK, 1) + rect.next_to(title, DOWN) + + self.add(background, rect) + self.play(FadeInFromDown(title)) + self.wait() + + +class PreviewBeta(Scene): + def construct(self): + axes = get_beta_dist_axes(label_y=True) + axes.y_axis.remove(axes.y_axis.numbers) + marks = get_plusses_and_minuses(p=0.75) + marks.next_to(axes.y_axis.get_top(), DR, buff=0.75) + + beta_label = get_beta_label(0, 0) + beta_label.next_to(marks, UR, buff=LARGE_BUFF) + beta_label.to_edge(UP) + bl_left = beta_label.get_left() + + beta_container = VGroup() + graph_container = VGroup() + n_graphs = 2 + for x in range(n_graphs): + graph_container.add(VMobject()) + + def get_counts(marks): + is_plusses = [m.is_plus for m in marks] + p = sum(is_plusses) + n = len(is_plusses) - p + return p, n + + def update_beta(container): + counts = get_counts(marks) + new_label = get_beta_label(*counts) + new_label.move_to(bl_left, LEFT) + container.set_submobjects([new_label]) + return container + + def update_graph(container): + counts = get_counts(marks) + new_graph = get_beta_graph(axes, *counts) + new_graphs = [*container[1:], new_graph] + for g, a in zip(new_graphs, np.linspace(0.2, 1, n_graphs)): + g.set_opacity(a) + + container.set_submobjects(new_graphs) + return container + + self.add(axes) + self.play( + ShowIncreasingSubsets(marks), + UpdateFromFunc( + beta_container, + update_beta, + ), + UpdateFromFunc( + graph_container, + update_graph, + ), + run_time=15, + rate_func=bezier([0, 0, 1, 1]), + ) + self.wait() + + +class AskInverseQuestion(WhatsTheModel): + def construct(self): + self.force_skipping() + self.introduce_buyer_and_seller() + self.bs_group = VGroup( + self.buyer, + self.seller, + self.buyer.label, + self.seller.label, + ) + self.bs_group.to_edge(DOWN) + self.revert_to_original_skipping_status() + + self.add_probability_label() + self.show_many_review_animations() + self.ask_question() + + def add_probability_label(self): + label = get_prob_positive_experience_label(True, True, False) + label.decimal.set_value(0.95) + label.next_to(self.seller, UP, aligned_edge=LEFT, buff=MED_LARGE_BUFF) + + self.add(label) + self.probability_label = label + + def show_many_review_animations(self): + for x in range(7): + self.play(*self.experience_animations( + self.seller, + self.buyer, + arc=30 * DEGREES, + p=0.95, + )) + + def ask_question(self): + pis = [self.buyer, self.seller] + labels = VGroup( + self.get_prob_review_label(10, 0), + self.get_prob_review_label(48, 2), + self.get_prob_review_label(186, 14), + ) + labels.arrange(DOWN) + labels.to_edge(UP) + + labels[0].save_state() + labels[0].set_opacity(0) + words = labels[0][-3:-1] + words.set_opacity(1) + words.scale(1.5) + words.center().to_edge(UP) + + self.play( + FadeInFromDown(words), + ) + self.wait() + self.play( + Restore(labels[0]), + *[ + ApplyMethod(pi.change, 'pondering', labels) + for pi in pis + ] + ) + self.play(Blink(pis[0])) + self.play(Blink(pis[1])) + self.play(LaggedStartMap(FadeInFromDown, labels[1:])) + self.wait(2) + + # Succinct + short_label = TexMobject( + "P(\\text{data} | s)", + tex_to_color_map={ + "\\text{data}": LIGHT_GREY, + "s": YELLOW + } + ) + short_label.scale(2) + short_label.next_to(labels, DOWN, LARGE_BUFF), + rect = SurroundingRectangle(short_label, buff=MED_SMALL_BUFF) + bs_group = self.bs_group + bs_group.add(self.probability_label) + + self.play( + FadeIn(short_label, UP), + bs_group.scale, 0.5, {"about_edge": DOWN}, + ) + self.play(ShowCreation(rect)) + self.wait() + + def get_prob_review_label(self, n_positive, n_negative): + label = TexMobject( + "P(", + f"{n_positive}\\,{CMARK_TEX}", ",\\,", + f"{n_negative}\\,{XMARK_TEX}", + "\\,\\text{ Given that }", + "s = 0.95", + ")", + ) + label.set_color_by_tex_to_color_map({ + CMARK_TEX: GREEN, + XMARK_TEX: RED, + "0.95": YELLOW, + }) + return label + + +class SimulationsOf10Reviews(Scene): + CONFIG = { + "s": 0.95, + "histogram_height": 5, + "histogram_width": 10, + } + + def construct(self): + # Add s label + s_label = TexMobject("s = 0.95") + s_label.set_height(0.3) + s_label.to_corner(UL, buff=MED_SMALL_BUFF) + s_label.set_color(YELLOW) + self.add(s_label) + self.camera.frame.shift(LEFT) + s_label.shift(LEFT) + + # Add random row + np.random.seed(0) + row = get_random_num_row(self.s) + count = self.get_count(row) + count.add_updater( + lambda m: m.set_value( + sum([s.positive for s in row.syms]) + ) + ) + + def update_nums(nums): + for num in nums: + num.set_value(np.random.random()) + + row.nums.save_state() + row.nums.set_color(WHITE) + self.play( + UpdateFromFunc(row.nums, update_nums), + run_time=2, + ) + row.nums.restore() + self.wait() + + self.add(count) + self.play( + ShowIncreasingSubsets(row.syms), + run_time=2, + rate_func=linear, + ) + count.clear_updaters() + self.wait() + + # Histogram + data = np.zeros(11) + histogram = self.get_histogram(data) + + stacks = VGroup() + for bar in histogram.bars: + stacks.add(VGroup(bar.copy())) + + def put_into_histogram(row_count_group): + row, count = row_count_group + count.clear_updaters() + index = int(count.get_value()) + stack = stacks[index] + + row.set_width(stack.get_width() - SMALL_BUFF) + row.next_to(stack, UP, SMALL_BUFF) + count.replace(histogram.axes.x_labels[index]) + stack.add(row) + return row_count_group + + # Random samples in histogram + self.play( + FadeIn(histogram), + ApplyFunction( + put_into_histogram, + VGroup(row, count), + ) + ) + self.wait() + for x in range(2): + row = get_random_num_row(self.s) + count = self.get_count(row) + group = VGroup(row, count) + self.play(FadeIn(group, lag_ratio=0.2)) + self.wait(0.5) + self.play( + ApplyFunction( + put_into_histogram, + VGroup(row, count), + ) + ) + + # More! + for x in range(40): + row = get_random_num_row(self.s) + count = self.get_count(row) + lower_group = VGroup(row, count).copy() + put_into_histogram(lower_group) + self.add(row, count, lower_group) + self.wait(0.1) + self.remove(row, count) + + data = np.array([len(stack) - 1 for stack in stacks]) + self.add(row, count) + self.play( + FadeOut(stacks), + FadeOut(count), + histogram.bars.become, histogram.get_bars(data), + histogram.axes.y_labels.set_opacity, 1, + histogram.axes.h_lines.set_opacity, 1, + histogram.axes.y_axis.set_opacity, 1, + ) + self.remove(stacks) + + arrow = Vector(0.5 * DOWN) + arrow.set_stroke(width=5) + arrow.set_color(YELLOW) + arrow.next_to(histogram.bars[10], UP, SMALL_BUFF) + + def update(dummy): + new_row = get_random_num_row(self.s) + row.become(new_row) + count = sum([m.positive for m in new_row.nums]) + data[count] += 1 + histogram.bars.become(histogram.get_bars(data)) + arrow.next_to(histogram.bars[count], UP, SMALL_BUFF) + + self.add(arrow) + self.play( + UpdateFromFunc(Group(row, arrow, histogram.bars), update), + run_time=10, + ) + + # + def get_histogram(self, data): + histogram = Histogram( + data, + bar_colors=[RED, RED, BLUE, GREEN], + height=self.histogram_height, + width=self.histogram_width, + ) + histogram.to_edge(DOWN) + + histogram.axes.y_labels.set_opacity(0) + histogram.axes.h_lines.set_opacity(0) + return histogram + + def get_count(self, row): + count = Integer() + count.set_height(0.75) + count.next_to(row, DOWN, buff=0.65) + count.set_value(sum([s.positive for s in row.syms])) + return count + + +class SimulationsOf10ReviewsSquished(SimulationsOf10Reviews): + CONFIG = { + "histogram_height": 2, + "histogram_width": 11, + } + + def get_histogram(self, data): + hist = super().get_histogram(data) + hist.to_edge(UP, buff=1.5) + return hist + + +class SimulationsOf50Reviews(Scene): + CONFIG = { + "s": 0.95, + "histogram_config": { + "x_label_freq": 5, + "y_axis_numbers_to_show": range(10, 70, 10), + "y_max": 0.6, + "y_tick_freq": 0.1, + "height": 5, + "bar_colors": [BLUE], + }, + "random_seed": 1, + } + + def construct(self): + self.add_s_label() + + data = np.zeros(51) + histogram = self.get_histogram(data) + + row = self.get_row() + count = self.get_count(row) + original_count = count.get_value() + count.set_value(0) + + self.add(histogram) + self.play( + ShowIncreasingSubsets(row), + ChangeDecimalToValue(count, original_count) + ) + + # Run many samples + arrow = Vector(0.5 * DOWN) + arrow.set_stroke(width=5) + arrow.set_color(YELLOW) + arrow.next_to(histogram.bars[10], UP, SMALL_BUFF) + + total_data_label = VGroup( + TextMobject("Total samples: "), + Integer(1), + ) + total_data_label.arrange(RIGHT) + total_data_label.next_to(row, DOWN) + total_data_label.add_updater( + lambda m: m[1].set_value(data.sum()) + ) + + def update(dummy, n_added_data_points=0): + new_row = self.get_row() + row.become(new_row) + num_positive = sum([m.positive for m in new_row]) + count.set_value(num_positive) + data[num_positive] += 1 + if n_added_data_points: + values = np.random.random((n_added_data_points, 50)) + counts = (values < self.s).sum(1) + for i in range(len(data)): + data[i] += (counts == i).sum() + histogram.bars.become(histogram.get_bars(data)) + histogram.bars.set_fill(GREY_C) + histogram.bars[48].set_fill(GREEN) + arrow.next_to(histogram.bars[num_positive], UP, SMALL_BUFF) + + self.add(arrow, total_data_label) + group = VGroup(histogram.bars, row, count, arrow) + self.play( + UpdateFromFunc(group, update), + run_time=4 + ) + self.play( + UpdateFromFunc( + group, + lambda m: update(m, 1000) + ), + run_time=4 + ) + random.seed(0) + np.random.seed(0) + update(group) + self.wait() + + # Show 48 bar + axes = histogram.axes + y = choose(50, 48) * (self.s)**48 * (1 - self.s)**2 + line = DashedLine( + axes.c2p(0, y), + axes.c2p(51, y), + ) + label = TexMobject("{:.1f}\\%".format(100 * y)) + fix_percent(label.family_members_with_points()[-1]) + label.next_to(line, RIGHT) + + self.play( + ShowCreation(line), + FadeInFromPoint(label, line.get_start()) + ) + + def add_s_label(self): + s_label = TexMobject("s = 0.95") + s_label.set_height(0.3) + s_label.to_corner(UL, buff=MED_SMALL_BUFF) + s_label.shift(0.8 * DOWN) + s_label.set_color(YELLOW) + self.add(s_label) + + def get_histogram(self, data): + histogram = Histogram( + data, **self.histogram_config + ) + histogram.to_edge(DOWN) + return histogram + + def get_row(self, n=50): + row = get_random_checks_and_crosses(n, self.s) + row.move_to(3.5 * UP) + return row + + def get_count(self, row): + count = Integer(sum([m.positive for m in row])) + count.set_height(0.3) + count.next_to(row, RIGHT) + return count + + +class ShowBinomialFormula(SimulationsOf50Reviews): + CONFIG = { + "histogram_config": { + "x_label_freq": 5, + "y_axis_numbers_to_show": range(10, 40, 10), + "y_max": 0.3, + "y_tick_freq": 0.1, + "height": 2.5, + "bar_colors": [BLUE], + }, + "random_seed": 0, + } + + def construct(self): + # Add histogram + dist = scipy.stats.binom(50, self.s) + data = np.array([ + dist.pmf(x) + for x in range(0, 51) + ]) + histogram = self.get_histogram(data) + histogram.bars.set_fill(GREY_C) + histogram.bars[48].set_fill(GREEN) + self.add(histogram) + + row = self.get_row() + self.add(row) + + # Formula + prob_label = get_prob_review_label(48, 2) + eq = TexMobject("=") + formula = get_binomial_formula(50, 48, self.s) + + equation = VGroup( + prob_label, + eq, + formula, + ) + equation.arrange(RIGHT) + equation.next_to(histogram, UP, LARGE_BUFF) + equation.to_edge(RIGHT) + + prob_label.save_state() + arrow = Vector(DOWN) + arrow.next_to(histogram.bars[48], UP, SMALL_BUFF) + prob_label.next_to(arrow, UP) + + self.play( + FadeIn(prob_label), + GrowArrow(arrow), + ) + for mob in prob_label[1::2]: + line = Underline(mob) + line.match_color(mob) + self.play(ShowCreationThenDestruction(line)) + self.wait(0.5) + self.play( + Restore(prob_label), + FadeIn(equation[1:], lag_ratio=0.1), + ) + self.wait() + + self.explain_n_choose_k(row, formula) + + # Circle formula parts + rect1 = SurroundingRectangle(formula[4:8]) + rect2 = SurroundingRectangle(formula[8:]) + rect1.set_stroke(GREEN, 2) + rect2.set_stroke(RED, 2) + + for rect in rect1, rect2: + self.play(ShowCreation(rect)) + self.wait() + self.play(FadeOut(rect)) + + # Show numerical answer + eq2 = TexMobject("=") + value = DecimalNumber(dist.pmf(48), num_decimal_places=5) + rhs = VGroup(eq2, value) + rhs.arrange(RIGHT) + rhs.match_y(eq) + rhs.to_edge(RIGHT, buff=MED_SMALL_BUFF) + self.play( + FadeIn(value, LEFT), + FadeIn(eq2), + equation.next_to, eq2, LEFT, + ) + self.wait() + + # Show alternate values of k + n = 50 + for k in it.chain(range(47, 42, -1), range(43, 51), [49, 48]): + new_prob_label = get_prob_review_label(k, n - k) + new_prob_label.replace(prob_label) + prob_label.become(new_prob_label) + new_formula = get_binomial_formula(n, k, self.s) + new_formula.replace(formula) + formula.set_submobjects(new_formula) + + value.set_value(dist.pmf(k)) + histogram.bars.set_fill(LIGHT_GREY) + histogram.bars[k].set_fill(GREEN) + arrow.next_to(histogram.bars[k], UP, SMALL_BUFF) + + new_row = get_checks_and_crosses((n - k) * [False] + k * [True]) + new_row.replace(row) + row.become(new_row) + self.wait(0.5) + + # Name it as the Binomial distribution + long_equation = VGroup(prob_label, eq, formula, eq2, value) + bin_name = TextMobject("Binomial", " Distribution") + bin_name.scale(1.5) + bin_name.next_to(histogram, UP, MED_LARGE_BUFF) + + underline = Underline(bin_name[0]) + underline.set_stroke(PINK, 2) + nck_rect = SurroundingRectangle(formula[:4]) + nck_rect.set_stroke(PINK, 2) + + self.play( + long_equation.next_to, self.slots, DOWN, MED_LARGE_BUFF, + long_equation.to_edge, RIGHT, + FadeIn(bin_name, DOWN), + ) + self.wait() + self.play(ShowCreationThenDestruction(underline)) + self.wait() + bools = [True] * 50 + bools[random.randint(0, 49)] = False + bools[random.randint(0, 49)] = False + row.become(get_checks_and_crosses(bools).replace(row)) + self.play(ShowIncreasingSubsets(row, run_time=4)) + self.wait() + + # Show likelihood and posterior labels + likelihood_label = TexMobject( + "P(", + "\\text{data}", "\\,|\\,", + "\\text{success rate}", + ")", + ) + posterior_label = TexMobject( + "P(", + "\\text{success rate}", + "\\,|\\,", + "\\text{data}", + ")", + ) + for label in (likelihood_label, posterior_label): + label.set_color_by_tex_to_color_map({ + "data": GREEN, + "success": YELLOW, + }) + + likelihood_label.next_to( + prob_label, DOWN, LARGE_BUFF, aligned_edge=LEFT + ) + + right_arrow = Vector(RIGHT) + right_arrow.next_to(likelihood_label, RIGHT) + ra_label = TextMobject("But we want") + ra_label.match_width(right_arrow) + ra_label.next_to(right_arrow, UP, SMALL_BUFF) + posterior_label.next_to(right_arrow, RIGHT) + + self.play( + FadeIn(likelihood_label, UP), + bin_name.set_height, 0.4, + bin_name.set_y, histogram.axes.c2p(0, .25)[1] + ) + self.wait() + self.play( + GrowArrow(right_arrow), + FadeIn(ra_label, 0.5 * LEFT), + ) + anims = [] + for i, j in enumerate([0, 3, 2, 1, 4]): + anims.append( + TransformFromCopy( + likelihood_label[i], + posterior_label[j], + path_arc=-45 * DEGREES, + run_time=2, + ) + ) + self.play(*anims) + self.add(posterior_label) + self.wait() + + # Prepare for new plot + histogram.add(bin_name) + always(arrow.next_to, histogram.bars[48], UP, SMALL_BUFF) + self.play( + FadeOut(likelihood_label), + FadeOut(posterior_label), + FadeOut(right_arrow), + FadeOut(ra_label), + FadeOut(row, UP), + FadeOut(self.slots, UP), + histogram.scale, 0.7, + histogram.to_edge, UP, + arrow.scale, 0.5, + arrow.set_stroke, None, 4, + long_equation.center, + run_time=1.5, + ) + self.add(arrow) + + # x_labels = histogram.axes.x_labels + # underline = Underline(x_labels) + # underline.set_stroke(GREEN, 3) + # self.play( + # LaggedStartMap( + # ApplyFunction, x_labels, + # lambda mob: ( + # lambda m: m.scale(1.5).set_color(GREEN), + # mob, + # ), + # rate_func=there_and_back, + # ), + # ShowCreationThenDestruction(underline), + # ) + # num_checks = TexMobject("\\# " + CMARK_TEX) + # num_checks.set_color(GREEN) + # num_checks.next_to( + # x_labels, RIGHT, + # MED_LARGE_BUFF, + # aligned_edge=DOWN, + # ) + # self.play(Write(num_checks)) + # self.wait() + + low_axes = get_beta_dist_axes(y_max=0.3, y_unit=0.1, label_y=False) + low_axes.y_axis.set_height( + 2, + about_point=low_axes.c2p(0, 0), + stretch=True, + ) + low_axes.to_edge(DOWN) + low_axes.x_axis.numbers.set_color(YELLOW) + y_label_copies = histogram.axes.y_labels.copy() + y_label_copies.set_height(0.6 * low_axes.get_height()) + y_label_copies.next_to(low_axes, LEFT, 0, aligned_edge=UP) + y_label_copies.shift(SMALL_BUFF * UP) + low_axes.y_axis.add(y_label_copies) + low_axes.y_axis.set_opacity(0) + + # Show alternate values of s + s_tracker = ValueTracker(self.s) + + s_tip = ArrowTip(start_angle=-90 * DEGREES) + s_tip.set_color(YELLOW) + s_tip.axis = low_axes.x_axis + s_tip.st = s_tracker + s_tip.add_updater( + lambda m: m.next_to(m.axis.n2p(m.st.get_value()), UP, buff=0) + ) + + pl_decimal = DecimalNumber(self.s) + pl_decimal.set_color(YELLOW) + pl_decimal.replace(prob_label[-2][2:]) + prob_label[-2][2:].set_opacity(0) + + s_label = VGroup(prob_label[-2][:2], pl_decimal).copy() + sl_rect = SurroundingRectangle(s_label) + sl_rect.set_stroke(YELLOW, 2) + + self.add(pl_decimal) + self.play( + ShowCreation(sl_rect), + Write(low_axes), + ) + self.play( + s_label.next_to, s_tip, UP, 0.2, ORIGIN, s_label[1], + ReplacementTransform(sl_rect, s_tip) + ) + always(s_label.next_to, s_tip, UP, 0.2, ORIGIN, s_label[1]) + + decimals = VGroup(pl_decimal, s_label[1], formula[5], formula[9]) + decimals.s_tracker = s_tracker + + histogram.s_tracker = s_tracker + histogram.n = n + histogram.rhs_value = value + + def update_decimals(decs): + for dec in decs: + dec.set_value(decs.s_tracker.get_value()) + + def update_histogram(hist): + new_dist = scipy.stats.binom(hist.n, hist.s_tracker.get_value()) + new_data = np.array([ + new_dist.pmf(x) + for x in range(0, 51) + ]) + new_bars = hist.get_bars(new_data) + new_bars.match_style(hist.bars) + hist.bars.become(new_bars) + hist.rhs_value.set_value(new_dist.pmf(48)) + + bar_copy = histogram.bars[48].copy() + value.initial_config["num_decimal_places"] = 3 + value.set_value(value.get_value()) + bar_copy.next_to(value, RIGHT, aligned_edge=DOWN) + bar_copy.add_updater( + lambda m: m.set_height( + max( + histogram.bars[48].get_height() * 0.75, + 1e-6, + ), + stretch=True, + about_edge=DOWN, + ) + ) + self.add(bar_copy) + + self.add(histogram) + self.add(decimals) + for s in [0.95, 0.5, 0.99, 0.9]: + self.play( + s_tracker.set_value, s, + UpdateFromFunc(decimals, update_decimals), + UpdateFromFunc(histogram, update_histogram), + UpdateFromFunc(value, lambda m: m), + UpdateFromFunc(s_label, lambda m: m.update), + run_time=5, + ) + self.wait() + + # Plot + def func(x): + return scipy.stats.binom(50, x).pmf(48) + 1e-5 + graph = low_axes.get_graph(func, step_size=0.05) + graph.set_stroke(BLUE, 3) + + v_line = Line(DOWN, UP) + v_line.axes = low_axes + v_line.st = s_tracker + v_line.graph = graph + v_line.add_updater( + lambda m: m.put_start_and_end_on( + m.axes.c2p(m.st.get_value(), 0), + m.axes.input_to_graph_point( + m.st.get_value(), + m.graph, + ), + ) + ) + v_line.set_stroke(GREEN, 2) + dot = Dot() + dot.line = v_line + dot.set_height(0.05) + dot.add_updater(lambda m: m.move_to(m.line.get_end())) + + self.play( + ApplyMethod( + histogram.bars[48].stretch, 2, 1, {"about_edge": DOWN}, + rate_func=there_and_back, + run_time=2, + ), + ) + self.wait() + self.play(low_axes.y_axis.set_opacity, 1) + self.play( + FadeIn(graph), + FadeOut(s_label), + FadeOut(s_tip), + ) + self.play( + TransformFromCopy(histogram.bars[48], v_line), + FadeIn(dot), + ) + + self.add(histogram) + decimals.remove(decimals[1]) + for s in [0.9, 0.96, 1, 0.8, 0.96]: + self.play( + s_tracker.set_value, s, + UpdateFromFunc(decimals, update_decimals), + UpdateFromFunc(histogram, update_histogram), + UpdateFromFunc(value, lambda m: m), + run_time=5, + ) + self.wait() + + # Write formula + clean_form = TexMobject( + "P(", "\\text{data}", "\\,|\\,", "{s}", ")", "=", + "c", "\\cdot", + "{s}", "^{\\#" + CMARK_TEX + "}", + "(1 - ", "{s}", ")", "^{\\#" + XMARK_TEX + "}", + tex_to_color_map={ + "{s}": YELLOW, + "\\#" + CMARK_TEX: GREEN, + "\\#" + XMARK_TEX: RED, + } + ) + clean_form.next_to(formula, DOWN, MED_LARGE_BUFF) + clean_form.save_state() + clean_form[:6].align_to(equation[1], RIGHT) + clean_form[6].match_x(formula[2]) + clean_form[7].set_opacity(0) + clean_form[7].next_to(clean_form[6], RIGHT, SMALL_BUFF) + clean_form[8:11].match_x(formula[4:8]) + clean_form[11:].match_x(formula[8:]) + clean_form.saved_state.move_to(clean_form, LEFT) + + fade_rects = VGroup( + BackgroundRectangle(equation[:2]), + BackgroundRectangle(formula), + BackgroundRectangle(VGroup(eq2, bar_copy)), + ) + fade_rects.set_fill(BLACK, 0.8) + fade_rects[1].set_fill(opacity=0) + + pre_c = formula[:4].copy() + pre_s = formula[4:8].copy() + pre_1ms = formula[8:].copy() + + self.play( + FadeIn(fade_rects), + FadeIn(clean_form[:6]) + ) + self.play(ShowCreationThenFadeAround(clean_form[3])) + self.wait() + for cf, pre in (clean_form[6], pre_c), (clean_form[8:11], pre_s), (clean_form[11:], pre_1ms): + self.play( + GrowFromPoint(cf, pre.get_center()), + pre.move_to, cf, + pre.scale, 0, + ) + self.remove(pre) + self.wait() + + self.wait() + self.play(Restore(clean_form)) + + # Show with 480 and 20 + top_fade_rect = BackgroundRectangle(histogram) + top_fade_rect.shift(SMALL_BUFF * DOWN) + top_fade_rect.scale(1.5, about_edge=DOWN) + top_fade_rect.set_fill(BLACK, 0) + + new_formula = get_binomial_formula(500, 480, 0.96) + new_formula.move_to(formula) + + def func500(x): + return scipy.stats.binom(500, x).pmf(480) + 1e-5 + + graph500 = low_axes.get_graph(func500, step_size=0.05) + graph500.set_stroke(TEAL, 3) + + self.play( + top_fade_rect.set_opacity, 1, + fade_rects.set_opacity, 1, + FadeIn(new_formula) + ) + + self.clear() + self.add(new_formula, clean_form, low_axes, graph, v_line, dot) + self.add(low_axes.y_axis) + + self.play(TransformFromCopy(graph, graph500)) + self.wait() + + y_axis = low_axes.y_axis + y_axis.save_state() + sf = 3 + y_axis.stretch(sf, 1, about_point=low_axes.c2p(0, 0)) + for label in y_label_copies: + label.stretch(1 / sf, 1) + + v_line.suspend_updating() + v_line.graph = graph500 + self.play( + Restore(y_axis, rate_func=reverse_smooth), + graph.stretch, sf, 1, {"about_edge": DOWN}, + graph500.stretch, sf, 1, {"about_edge": DOWN}, + ) + v_line.resume_updating() + self.add(v_line, dot) + + sub_decimals = VGroup(new_formula[5], new_formula[9]) + sub_decimals.s_tracker = s_tracker + + for s in [0.94, 0.98, 0.96]: + self.play( + s_tracker.set_value, s, + UpdateFromFunc(sub_decimals, update_decimals), + run_time=5, + ) + self.wait() + + def explain_n_choose_k(self, row, formula): + row.add_updater(lambda m: m) + + brace = Brace(formula[:4], UP, buff=SMALL_BUFF) + words = brace.get_text("``50 choose 48''") + + slots = self.slots = VGroup() + for sym in row: + line = Underline(sym) + line.scale(0.9) + slots.add(line) + for slot in slots: + slot.match_y(slots[0]) + + formula[1].counted = slots + k_rect = SurroundingRectangle(formula[2]) + k_rect.set_stroke(GREEN, 2) + + checks = VGroup() + for sym in row: + if sym.positive: + checks.add(sym) + + self.play( + GrowFromCenter(brace), + FadeInFromDown(words), + ) + self.wait() + self.play(FadeOut(words)) + formula.save_state() + self.play( + ShowIncreasingSubsets(slots), + UpdateFromFunc( + formula[1], + lambda m: m.set_value(len(m.counted)) + ), + run_time=2, + ) + formula.restore() + self.add(formula) + self.wait() + self.play( + LaggedStartMap( + ApplyMethod, checks, + lambda m: (m.shift, 0.3 * DOWN), + rate_func=there_and_back, + lag_ratio=0.05, + ), + ShowCreationThenFadeOut(k_rect), + run_time=2, + ) + self.remove(checks) + self.add(row) + self.wait() + + # Example orderings + row_target = VGroup() + for sym in row: + sym.generate_target() + row_target.add(sym.target) + + row_target.sort(submob_func=lambda m: -int(m.positive)) + row_target.arrange( + RIGHT, buff=get_norm(row[0].get_right() - row[1].get_left()) + ) + row_target.move_to(row) + self.play( + LaggedStartMap( + MoveToTarget, row, + path_arc=30 * DEGREES, + lag_ratio=0, + ), + ) + self.wait() + row.sort() + self.play(Swap(*row[-3:-1])) + self.add(row) + self.wait() + + # All orderings + nck_count = Integer(2) + nck_count.next_to(brace, UP) + nck_top = nck_count.get_top() + always(nck_count.move_to, nck_top, UP) + + combs = list(it.combinations(range(50), 48)) + bool_lists = [ + [i in comb for i in range(50)] + for comb in combs + ] + row.counter = nck_count + row.bool_lists = bool_lists + + def update_row(r): + i = r.counter.get_value() - 1 + new_row = get_checks_and_crosses(r.bool_lists[i]) + new_row.replace(r, dim_to_match=0) + r.set_submobjects(new_row) + + row.add_updater(update_row) + self.add(row) + self.play( + ChangeDecimalToValue(nck_count, choose(50, 48)), + run_time=10, + ) + row.clear_updaters() + self.wait() + self.play( + FadeOut(nck_count), + FadeOut(brace), + ) + + +class StateIndependence(Scene): + def construct(self): + row = get_random_checks_and_crosses() + row.to_edge(UP) + # self.add(row) + + arrows = VGroup() + for m1, m2 in zip(row, row[1:]): + arrow = Arrow( + m1.get_bottom() + 0.025 * DOWN, + m2.get_bottom(), + path_arc=145 * DEGREES, + max_stroke_width_to_length_ratio=10, + max_tip_length_to_length_ratio=0.5, + ) + arrow.tip.rotate(-10 * DEGREES) + arrow.shift(SMALL_BUFF * DOWN) + arrow.set_color(YELLOW) + arrows.add(arrow) + + words = TextMobject("No influence") + words.set_height(0.25) + words.next_to(arrows[0], DOWN) + + self.play( + ShowCreation(arrows[0]), + FadeIn(words) + ) + for i in range(10): + self.play( + words.next_to, arrows[i + 1], DOWN, + FadeOut(arrows[i]), + ShowCreation(arrows[i + 1]) + ) + last_arrow = arrows[i + 1] + + self.play( + FadeOut(words), + FadeOut(last_arrow), + ) + + +class IllustrateBinomialSetupWithCoins(Scene): + def construct(self): + coins = [ + get_coin("H"), + get_coin("T"), + ] + + coin_row = VGroup() + for x in range(12): + coin_row.add(random.choice(coins).copy()) + + coin_row.arrange(RIGHT) + coin_row.to_edge(UP) + + first_coin = get_random_coin(shuffle_time=2, total_time=2) + first_coin.move_to(coin_row[0]) + + brace = Brace(coin_row, UP) + brace_label = brace.get_text("$N$ times") + + prob_label = TexMobject( + "P(\\# 00 = k)", + tex_to_color_map={ + "00": WHITE, + "k": GREEN, + } + ) + heads = get_coin("H") + template = prob_label.get_part_by_tex("00") + heads.replace(template) + prob_label.replace_submobject( + prob_label.index_of_part(template), + heads, + ) + prob_label.set_height(1) + prob_label.next_to(coin_row, DOWN, LARGE_BUFF) + + self.camera.frame.set_height(1.5 * FRAME_HEIGHT) + + self.add(first_coin) + for x in range(4): + self.wait() + first_coin.suspend_updating() + self.wait() + first_coin.resume_updating() + + self.remove(first_coin) + self.play( + ShowIncreasingSubsets(coin_row, int_func=np.ceil), + GrowFromPoint(brace, brace.get_left()), + FadeIn(brace_label, 3 * LEFT) + ) + self.wait() + self.play(FadeIn(prob_label, lag_ratio=0.1)) + self.wait() + + +class WriteLikelihoodFunction(Scene): + def construct(self): + formula = TexMobject( + "f({s}) = (\\text{const.})", + "{s}^{\\#" + CMARK_TEX + "}", + "(1 - {s})^{\\#" + XMARK_TEX, "}", + tex_to_color_map={ + "{s}": YELLOW, + "\\#" + CMARK_TEX: GREEN, + "\\#" + XMARK_TEX: RED, + } + ) + formula.scale(2) + + rect1 = SurroundingRectangle(formula[3:6]) + rect2 = SurroundingRectangle(formula[6:]) + + self.play(FadeInFromDown(formula)) + self.wait() + self.play(ShowCreationThenFadeOut(rect1)) + self.wait() + self.play(ShowCreationThenFadeOut(rect2)) + self.wait() + + self.add(formula) + self.embed() + + +class Guess96Percent(Scene): + def construct(self): + randy = Randolph() + randy.set_height(1) + + bubble = SpeechBubble(height=2, width=3) + bubble.pin_to(randy) + words = TextMobject("96$\\%$, right?") + fix_percent(words[0][2]) + bubble.add_content(words) + + arrow = Vector(2 * RIGHT + DOWN) + arrow.next_to(randy, RIGHT) + arrow.shift(2 * UP) + + self.play( + FadeIn(randy), + ShowCreation(bubble), + Write(words), + ) + self.play(randy.change, "shruggie", randy.get_right() + RIGHT) + self.play(ShowCreation(arrow)) + for x in range(2): + self.play(Blink(randy)) + self.wait() + + self.embed() + + +class LikelihoodGraphFor10of10(ShowBinomialFormula): + CONFIG = { + "histogram_config": { + "x_label_freq": 2, + "y_axis_numbers_to_show": range(25, 125, 25), + "y_max": 1, + "y_tick_freq": 0.25, + "height": 2, + "bar_colors": [BLUE], + }, + } + + def construct(self): + # Add histogram + dist = scipy.stats.binom(10, self.s) + data = np.array([ + dist.pmf(x) + for x in range(0, 11) + ]) + histogram = self.get_histogram(data) + histogram.bars.set_fill(GREY_C) + histogram.bars[10].set_fill(GREEN) + histogram.to_edge(UP) + + x_label = TexMobject("\\#" + CMARK_TEX) + x_label.set_color(GREEN) + x_label.next_to(histogram.axes.x_axis.get_end(), RIGHT) + histogram.add(x_label) + self.add(histogram) + + arrow = Vector(DOWN) + arrow.next_to(histogram.bars[10], UP, SMALL_BUFF) + self.add(arrow) + + # Add formula + prob_label = get_prob_review_label(10, 0) + eq = TexMobject("=") + formula = get_binomial_formula(10, 10, self.s) + eq2 = TexMobject("=") + value = DecimalNumber(dist.pmf(10), num_decimal_places=2) + + equation = VGroup(prob_label, eq, formula, eq2, value) + equation.arrange(RIGHT) + equation.next_to(histogram, DOWN, MED_LARGE_BUFF) + + # Add lower axes + low_axes = get_beta_dist_axes(y_max=1, y_unit=0.25, label_y=False) + low_axes.y_axis.set_height( + 2, + about_point=low_axes.c2p(0, 0), + stretch=True, + ) + low_axes.to_edge(DOWN) + low_axes.x_axis.numbers.set_color(YELLOW) + y_label_copies = histogram.axes.y_labels.copy() + y_label_copies.set_height(0.7 * low_axes.get_height()) + y_label_copies.next_to(low_axes, LEFT, 0, aligned_edge=UP) + y_label_copies.shift(SMALL_BUFF * UP) + low_axes.y_axis.add(y_label_copies) + + # Add lower plot + s_tracker = ValueTracker(self.s) + + def func(x): + return x**10 + graph = low_axes.get_graph(func, step_size=0.05) + graph.set_stroke(BLUE, 3) + + v_line = Line(DOWN, UP) + v_line.axes = low_axes + v_line.st = s_tracker + v_line.graph = graph + v_line.add_updater( + lambda m: m.put_start_and_end_on( + m.axes.c2p(m.st.get_value(), 0), + m.axes.input_to_graph_point( + m.st.get_value(), + m.graph, + ), + ) + ) + v_line.set_stroke(GREEN, 2) + dot = Dot() + dot.line = v_line + dot.set_height(0.05) + dot.add_updater(lambda m: m.move_to(m.line.get_end())) + + # Show simpler formula + brace = Brace(formula, DOWN, buff=SMALL_BUFF) + simpler_formula = TexMobject("s", "^{10}") + simpler_formula.set_color_by_tex("s", YELLOW) + simpler_formula.set_color_by_tex("10", GREEN) + simpler_formula.next_to(brace, DOWN) + + rects = VGroup( + BackgroundRectangle(formula[:4]), + BackgroundRectangle(formula[8:]), + ) + rects.set_opacity(0.75) + + self.wait() + self.play(FadeIn(equation)) + + self.wait() + self.play( + FadeIn(rects), + GrowFromCenter(brace), + FadeIn(simpler_formula, UP) + ) + self.wait() + + # Show various values of s + pl_decimal = DecimalNumber(self.s) + pl_decimal.set_color(YELLOW) + pl_decimal.replace(prob_label[-2][2:]) + prob_label[-2][2:].set_opacity(0) + + decimals = VGroup(pl_decimal, formula[5], formula[9]) + decimals.s_tracker = s_tracker + + histogram.s_tracker = s_tracker + histogram.n = 10 + histogram.rhs_value = value + + def update_decimals(decs): + for dec in decs: + dec.set_value(decs.s_tracker.get_value()) + + def update_histogram(hist): + new_dist = scipy.stats.binom(hist.n, hist.s_tracker.get_value()) + new_data = np.array([ + new_dist.pmf(x) + for x in range(0, 11) + ]) + new_bars = hist.get_bars(new_data) + new_bars.match_style(hist.bars) + hist.bars.become(new_bars) + hist.rhs_value.set_value(new_dist.pmf(10)) + + self.add(histogram) + self.add(decimals, rects) + self.play( + FadeIn(low_axes), + ) + self.play( + ShowCreation(v_line), + FadeIn(dot), + ) + self.add(graph, v_line, dot) + self.play(ShowCreation(graph)) + self.wait() + + always(arrow.next_to, histogram.bars[10], UP, SMALL_BUFF) + for s in [0.8, 1]: + self.play( + s_tracker.set_value, s, + UpdateFromFunc(decimals, update_decimals), + UpdateFromFunc(histogram, update_histogram), + UpdateFromFunc(value, lambda m: m), + run_time=5, + ) + self.wait() + + +class StateNeedForBayesRule(TeacherStudentsScene): + def construct(self): + axes = get_beta_dist_axes(y_max=1, y_unit=0.25, label_y=False) + axes.y_axis.set_height( + 2, + about_point=axes.c2p(0, 0), + stretch=True, + ) + axes.set_width(5) + graph = axes.get_graph(lambda x: x**10) + graph.set_stroke(BLUE, 3) + alt_graph = graph.copy() + alt_graph.add_line_to(axes.c2p(1, 0)) + alt_graph.add_line_to(axes.c2p(0, 0)) + alt_graph.set_stroke(width=0) + alt_graph.set_fill(BLUE_E, 1) + + plot = VGroup(axes, alt_graph, graph) + + student0, student1, student2 = self.students + plot.next_to(student2.get_corner(UL), UP, MED_LARGE_BUFF) + plot.shift(LEFT) + + v_lines = VGroup( + DashedLine(axes.c2p(0.8, 0), axes.c2p(0.8, 1)), + DashedLine(axes.c2p(1, 0), axes.c2p(1, 1)), + ) + v_lines.set_stroke(YELLOW, 2) + + self.play( + LaggedStart( + ApplyMethod(student0.change, "pondering", plot), + ApplyMethod(student1.change, "pondering", plot), + ApplyMethod(student2.change, "raise_left_hand", plot), + ), + FadeIn(plot, DOWN), + run_time=1.5 + ) + self.play(*map(ShowCreation, v_lines)) + self.play( + self.teacher.change, "tease", + *[ + ApplyMethod( + v_line.move_to, + axes.c2p(0.9, 0), + DOWN, + ) + for v_line in v_lines + ] + ) + self.change_student_modes( + "thinking", "thinking", "pondering", + look_at_arg=v_lines, + ) + self.wait(2) + + self.teacher_says( + "But first...", + added_anims=[ + FadeOut(plot, LEFT), + FadeOut(v_lines, LEFT), + self.get_student_changes( + "erm", "erm", "erm", + look_at_arg=self.teacher.eyes, + ) + ] + ) + self.wait(5) + + +class Part1EndScreen(PatreonEndScreen): + CONFIG = { + "specific_patrons": [ + "1stViewMaths", + "Adam Dřínek", + "Aidan Shenkman", + "Alan Stein", + "Alex Mijalis", + "Alexis Olson", + "Ali Yahya", + "Andrew Busey", + "Andrew Cary", + "Andrew R. Whalley", + "Aravind C V", + "Arjun Chakroborty", + "Arthur Zey", + "Ashwin Siddarth", + "Austin Goodman", + "Avi Finkel", + "Awoo", + "Axel Ericsson", + "Ayan Doss", + "AZsorcerer", + "Barry Fam", + "Bernd Sing", + "Boris Veselinovich", + "Bradley Pirtle", + "Brandon Huang", + "Brian Staroselsky", + "Britt Selvitelle", + "Britton Finley", + "Burt Humburg", + "Calvin Lin", + "Charles Southerland", + "Charlie N", + "Chenna Kautilya", + "Chris Connett", + "Christian Kaiser", + "cinterloper", + "Clark Gaebel", + "Colwyn Fritze-Moor", + "Cooper Jones", + "Corey Ogburn", + "D. Sivakumar", + "Dan Herbatschek", + "Daniel Herrera C", + "Dave B", + "Dave Kester", + "dave nicponski", + "David B. Hill", + "David Clark", + "David Gow", + "Delton Ding", + "Dominik Wagner", + "Douglas Cantrell", + "emptymachine", + "Eric Younge", + "Eryq Ouithaqueue", + "Farzaneh Sarafraz", + "Federico Lebron", + "Frank R. Brown, Jr.", + "Giovanni Filippi", + "Hal Hildebrand", + "Hitoshi Yamauchi", + "Ivan Sorokin", + "Jacob Baxter", + "Jacob Harmon", + "Jacob Hartmann", + "Jacob Magnuson", + "Jake Vartuli - Schonberg", + "Jalex Stark", + "Jameel Syed", + "Jason Hise", + "Jayne Gabriele", + "Jean-Manuel Izaret", + "Jeff Linse", + "Jeff Straathof", + "Jimmy Yang", + "John C. Vesey", + "John Haley", + "John Le", + "John V Wertheim", + "Jonathan Heckerman", + "Jonathan Wilson", + "Joseph John Cox", + "Joseph Kelly", + "Josh Kinnear", + "Joshua Claeys", + "Juan Benet", + "Kai-Siang Ang", + "Kanan Gill", + "Karl Niu", + "Kartik Cating-Subramanian", + "Kaustuv DeBiswas", + "Killian McGuinness", + "Kros Dai", + "L0j1k", + "LAI Oscar", + "Lambda GPU Workstations", + "Lee Redden", + "Linh Tran", + "Luc Ritchie", + "Ludwig Schubert", + "Lukas Biewald", + "Magister Mugit", + "Magnus Dahlström", + "Manoj Rewatkar - RITEK SOLUTIONS", + "Mark B Bahu", + "Mark Heising", + "Mark Mann", + "Martin Price", + "Mathias Jansson", + "Matt Godbolt", + "Matt Langford", + "Matt Roveto", + "Matt Russell", + "Matteo Delabre", + "Matthew Bouchard", + "Matthew Cocke", + "Mia Parent", + "Michael Hardel", + "Michael W White", + "Mirik Gogri", + "Mustafa Mahdi", + "Márton Vaitkus", + "Nicholas Cahill", + "Nikita Lesnikov", + "Oleg Leonov", + "Oliver Steele", + "Omar Zrien", + "Owen Campbell-Moore", + "Patrick Lucas", + "Pavel Dubov", + "Peter Ehrnstrom", + "Peter Mcinerney", + "Pierre Lancien", + "Quantopian", + "Randy C. Will", + "rehmi post", + "Rex Godby", + "Ripta Pasay", + "Rish Kundalia", + "Roman Sergeychik", + "Roobie", + "Ryan Atallah", + "Samuel Judge", + "SansWord Huang", + "Scott Gray", + "Scott Walter, Ph.D.", + "soekul", + "Solara570", + "Steve Huynh", + "Steve Sperandeo", + "Steven Braun", + "Steven Siddals", + "Stevie Metke", + "supershabam", + "Suteerth Vishnu", + "Suthen Thomas", + "Tal Einav", + "Taras Bobrovytsky", + "Tauba Auerbach", + "Ted Suzman", + "Thomas J Sargent", + "Thomas Tarler", + "Tianyu Ge", + "Tihan Seale", + "Tyler VanValkenburg", + "Vassili Philippov", + "Veritasium", + "Vignesh Ganapathi Subramanian", + "Vinicius Reis", + "Xuanji Li", + "Yana Chernobilsky", + "Yavor Ivanov", + "YinYangBalance.Asia", + "Yu Jun", + "Yurii Monastyrshyn", + ], + } diff --git a/from_3b1b/active/bayes/beta2.py b/from_3b1b/active/bayes/beta2.py new file mode 100644 index 0000000000..8dbf830961 --- /dev/null +++ b/from_3b1b/active/bayes/beta2.py @@ -0,0 +1,2338 @@ +from manimlib.imports import * +from from_3b1b.active.bayes.beta_helpers import * +from from_3b1b.active.bayes.beta1 import * +from from_3b1b.old.hyperdarts import Dartboard + +import scipy.stats + +OUTPUT_DIRECTORY = "bayes/beta2" + + +class WeightedCoin(Scene): + def construct(self): + # Coin grid + bools = 50 * [True] + 50 * [False] + random.shuffle(bools) + grid = get_coin_grid(bools) + + sorted_grid = VGroup(*grid) + sorted_grid.submobjects.sort(key=lambda m: m.symbol) + + # Prob label + p_label = get_prob_coin_label() + p_label.set_height(0.7) + p_label.to_edge(UP) + + rhs = p_label[-1] + rhs_box = SurroundingRectangle(rhs, color=RED) + rhs_label = TextMobject("Not necessarily") + rhs_label.next_to(rhs_box, DOWN, LARGE_BUFF) + rhs_label.to_edge(RIGHT) + rhs_label.match_color(rhs_box) + + rhs_arrow = Arrow( + rhs_label.get_top(), + rhs_box.get_right(), + buff=SMALL_BUFF, + path_arc=60 * DEGREES, + color=rhs_box.get_color() + ) + + # Introduce coin + self.play(FadeIn( + grid, + run_time=2, + rate_func=linear, + lag_ratio=3 / len(grid), + )) + self.wait() + self.play( + grid.set_height, 5, + grid.to_edge, DOWN, + FadeInFromDown(p_label) + ) + + for coin in grid: + coin.generate_target() + sorted_coins = list(grid) + sorted_coins.sort(key=lambda m: m.symbol) + for c1, c2 in zip(sorted_coins, grid): + c1.target.move_to(c2) + + self.play( + FadeIn(rhs_label, lag_ratio=0.1), + ShowCreation(rhs_arrow), + ShowCreation(rhs_box), + LaggedStartMap( + MoveToTarget, grid, + path_arc=30 * DEGREES, + lag_ratio=0.01, + ), + ) + + # Alternate weightings + old_grid = VGroup(*sorted_coins) + rhs_junk_on_screen = True + for value in [0.2, 0.9, 0.0, 0.31]: + n = int(100 * value) + new_grid = get_coin_grid([True] * n + [False] * (100 - n)) + new_grid.replace(grid) + + anims = [] + if rhs_junk_on_screen: + anims += [ + FadeOut(rhs_box), + FadeOut(rhs_label), + FadeOut(rhs_arrow), + ] + rhs_junk_on_screen = False + + self.wait() + self.play( + FadeOut( + old_grid, + 0.1 * DOWN, + lag_ratio=0.01, + run_time=1.5 + ), + FadeIn(new_grid, lag_ratio=0.01, run_time=1.5), + ChangeDecimalToValue(rhs, value), + *anims, + ) + old_grid = new_grid + + long_rhs = DecimalNumber( + 0.31415926, + num_decimal_places=8, + show_ellipsis=True, + ) + long_rhs.match_height(rhs) + long_rhs.move_to(rhs, DL) + + self.play(ShowIncreasingSubsets(long_rhs, rate_func=linear)) + self.wait() + + # You just don't know + box = get_q_box(rhs) + + self.remove(rhs) + self.play( + FadeOut(old_grid, lag_ratio=0.1), + FadeOut(long_rhs, 0.1 * RIGHT, lag_ratio=0.1), + Write(box), + ) + p_label.add(box) + self.wait() + + # 7/10 heads + bools = [True] * 7 + [False] * 3 + random.shuffle(bools) + coins = VGroup(*[ + get_coin("H" if heads else "T") + for heads in bools + ]) + coins.arrange(RIGHT) + coins.set_height(0.7) + coins.next_to(p_label, DOWN, buff=LARGE_BUFF) + + heads_arrows = VGroup(*[ + Vector( + 0.5 * UP, + max_stroke_width_to_length_ratio=15, + max_tip_length_to_length_ratio=0.4, + ).next_to(coin, DOWN) + for coin in coins + if coin.symbol == "H" + ]) + numbers = VGroup(*[ + Integer(i + 1).next_to(arrow, DOWN, SMALL_BUFF) + for i, arrow in enumerate(heads_arrows) + ]) + + for coin in coins: + coin.save_state() + coin.stretch(0, 0) + coin.set_opacity(0) + + self.play(LaggedStartMap(Restore, coins), run_time=1) + self.play( + ShowIncreasingSubsets(heads_arrows), + ShowIncreasingSubsets(numbers), + rate_func=linear, + ) + self.wait() + + # Plot + axes = scaled_pdf_axes() + axes.to_edge(DOWN, buff=MED_SMALL_BUFF) + axes.y_axis.numbers.set_opacity(0) + axes.y_axis_label.set_opacity(0) + + x_axis_label = p_label[:4].copy() + x_axis_label.set_height(0.4) + x_axis_label.next_to(axes.c2p(1, 0), UR, buff=SMALL_BUFF) + axes.x_axis.add(x_axis_label) + + n_heads = 7 + n_tails = 3 + graph = get_beta_graph(axes, n_heads, n_tails) + dist = scipy.stats.beta(n_heads + 1, n_tails + 1) + true_graph = axes.get_graph(dist.pdf) + + v_line = Line( + axes.c2p(0.7, 0), + axes.input_to_graph_point(0.7, true_graph), + ) + v_line.set_stroke(YELLOW, 4) + + region = get_region_under_curve(axes, true_graph, 0.6, 0.8) + region.set_fill(GREY, 0.85) + region.set_stroke(YELLOW, 1) + + eq_label = VGroup( + p_label[:4].copy(), + TexMobject("= 0.7"), + ) + for mob in eq_label: + mob.set_height(0.4) + eq_label.arrange(RIGHT, buff=SMALL_BUFF) + pp_label = VGroup( + TexMobject("P("), + eq_label, + TexMobject(")"), + ) + for mob in pp_label[::2]: + mob.set_height(0.7) + mob.set_color(YELLOW) + pp_label.arrange(RIGHT, buff=SMALL_BUFF) + pp_label.move_to(axes.c2p(0.3, 3)) + + self.play( + FadeOut(heads_arrows), + FadeOut(numbers), + Write(axes), + DrawBorderThenFill(graph), + ) + self.play( + FadeIn(pp_label[::2]), + ShowCreation(v_line), + ) + self.wait() + self.play(TransformFromCopy(p_label[:4], eq_label[0])) + self.play( + GrowFromPoint(eq_label[1], v_line.get_center()) + ) + self.wait() + + # Look confused + randy = Randolph() + randy.set_height(1.5) + randy.next_to(axes.c2p(0, 0), UR, MED_LARGE_BUFF) + + self.play(FadeIn(randy)) + self.play(randy.change, "confused", pp_label.get_top()) + self.play(Blink(randy)) + self.wait() + self.play(FadeOut(randy)) + + # Remind what the title is + title = TextMobject( + "Probabilities", "of", "Probabilities" + ) + title.arrange(DOWN, aligned_edge=LEFT) + title.next_to(axes.c2p(0, 0), UR, buff=MED_LARGE_BUFF) + title.align_to(pp_label, LEFT) + + self.play(ShowIncreasingSubsets(title, rate_func=linear)) + self.wait() + self.play(FadeOut(title)) + + # Continuous values + v_line.tracker = ValueTracker(0.7) + v_line.axes = axes + v_line.graph = true_graph + v_line.add_updater( + lambda m: m.put_start_and_end_on( + m.axes.c2p(m.tracker.get_value(), 0), + m.axes.input_to_graph_point(m.tracker.get_value(), m.graph), + ) + ) + + for value in [0.4, 0.9, 0.7]: + self.play( + v_line.tracker.set_value, value, + run_time=3, + ) + + # Label h + brace = Brace(rhs_box, DOWN, buff=SMALL_BUFF) + h_label = TexMobject("h", buff=SMALL_BUFF) + h_label.set_color(YELLOW) + h_label.next_to(brace, DOWN) + + self.play( + LaggedStartMap(FadeOutAndShift, coins, lambda m: (m, DOWN)), + GrowFromCenter(brace), + Write(h_label), + ) + self.wait() + + # End + self.embed() + + +class Eq70(Scene): + def construct(self): + label = TexMobject("=", "70", "\\%", "?") + fix_percent(label.get_part_by_tex("\\%")[0]) + self.play(FadeIn(label)) + self.wait() + + +class ShowInfiniteContinuum(Scene): + def construct(self): + # Axes + axes = scaled_pdf_axes() + axes.to_edge(DOWN, buff=MED_SMALL_BUFF) + axes.y_axis.numbers.set_opacity(0) + axes.y_axis_label.set_opacity(0) + self.add(axes) + + # Label + p_label = get_prob_coin_label() + p_label.set_height(0.7) + p_label.to_edge(UP) + box = get_q_box(p_label[-1]) + p_label.add(box) + + brace = Brace(box, DOWN, buff=SMALL_BUFF) + h_label = TexMobject("h") + h_label.next_to(brace, DOWN) + h_label.set_color(YELLOW) + eq = TexMobject("=") + eq.next_to(h_label, RIGHT) + value = DecimalNumber(0, num_decimal_places=4) + value.match_height(h_label) + value.next_to(eq, RIGHT) + value.set_color(YELLOW) + + self.add(p_label) + self.add(brace) + self.add(h_label) + + # Moving h + h_part = h_label.copy() + x_line = Line(axes.c2p(0, 0), axes.c2p(1, 0)) + x_line.set_stroke(YELLOW, 3) + + self.play( + h_part.next_to, x_line.get_start(), UR, SMALL_BUFF, + Write(eq), + FadeInFromPoint(value, h_part.get_center()), + ) + + # Scan continuum + h_part.tracked = x_line + value.tracked = x_line + value.x_axis = axes.x_axis + self.play( + ShowCreation(x_line), + UpdateFromFunc( + h_part, + lambda m: m.next_to(m.tracked.get_end(), UR, SMALL_BUFF) + ), + UpdateFromFunc( + value, + lambda m: m.set_value( + m.x_axis.p2n(m.tracked.get_end()) + ) + ), + run_time=3, + ) + self.wait() + self.play( + FadeOut(eq), + FadeOut(value), + ) + + # Arrows + arrows = VGroup() + arrow_template = Vector(DOWN) + arrow_template.lock_triangulation() + + def get_arrow(s, denom, arrow_template=arrow_template, axes=axes): + arrow = arrow_template.copy() + arrow.set_height(4 / denom) + arrow.move_to(axes.c2p(s, 0), DOWN) + arrow.set_color(interpolate_color( + GREY_A, GREY_C, random.random() + )) + return arrow + + for k in range(2, 50): + for n in range(1, k): + if np.gcd(n, k) != 1: + continue + s = n / k + arrows.add(get_arrow(s, k)) + for k in range(50, 1000): + arrows.add(get_arrow(1 / k, k)) + arrows.add(get_arrow(1 - 1 / k, k)) + + kw = { + "lag_ratio": 0.05, + "run_time": 5, + "rate_func": lambda t: t**5, + } + arrows.save_state() + for arrow in arrows: + arrow.stretch(0, 0) + arrow.set_stroke(width=0) + arrow.set_opacity(0) + self.play(Restore(arrows, **kw)) + self.play(LaggedStartMap( + ApplyMethod, arrows, + lambda m: (m.scale, 0, {"about_edge": DOWN}), + lag_ratio=10 / len(arrows), + rate_func=smooth, + run_time=3, + )) + self.remove(arrows) + self.wait() + + +class TitleCard(Scene): + def construct(self): + text = TextMobject("A beginner's guide to\\\\probability density") + text.scale(2) + text.to_edge(UP, buff=1.5) + + subtext = TextMobject("Probabilities of probabilities, ", "part 2") + subtext.set_width(FRAME_WIDTH - 3) + subtext[0].set_color(BLUE) + subtext.next_to(text, DOWN, LARGE_BUFF) + + self.add(text) + self.play(FadeIn(subtext, lag_ratio=0.1, run_time=2)) + self.wait(2) + + +class NamePdfs(Scene): + def construct(self): + label = TextMobject("Probability density\\\\function") + self.play(Write(label)) + self.wait() + + +class LabelH(Scene): + def construct(self): + p_label = get_prob_coin_label() + p_label.scale(1.5) + brace = Brace(p_label, DOWN) + h = TexMobject("h") + h.scale(2) + h.next_to(brace, DOWN) + + self.add(p_label) + self.play(ShowCreationThenFadeAround(p_label)) + self.play( + GrowFromCenter(brace), + FadeIn(h, UP), + ) + self.wait() + + +class DrawUnderline(Scene): + def construct(self): + line = Line(2 * LEFT, 2 * RIGHT) + line.set_stroke(PINK, 5) + self.play(ShowCreation(line)) + self.wait() + line.reverse_points() + self.play(Uncreate(line)) + + +class TryAssigningProbabilitiesToSpecificValues(Scene): + def construct(self): + # To get "P(s = .7000001) = ???" type labels + def get_p_label(value): + result = TexMobject( + # "P(", "{s}", "=", value, "\\%", ")", + "P(", "{h}", "=", value, ")", + ) + # fix_percent(result.get_part_by_tex("\\%")[0]) + result.set_color_by_tex("{h}", YELLOW) + return result + + labels = VGroup( + get_p_label("0.70000000"), + get_p_label("0.70000001"), + get_p_label("0.70314159"), + get_p_label("0.70271828"), + get_p_label("0.70466920"), + get_p_label("0.70161803"), + ) + labels.arrange(DOWN, buff=0.35, aligned_edge=LEFT) + labels.set_height(4.5) + labels.to_edge(DOWN, buff=LARGE_BUFF) + + q_marks = VGroup() + gt_zero = VGroup() + eq_zero = VGroup() + for label in labels: + qm = TexMobject("=", "\\,???") + qm.next_to(label, RIGHT) + qm[1].set_color(TEAL) + q_marks.add(qm) + + gt = TexMobject("> 0") + gt.next_to(label, RIGHT) + gt_zero.add(gt) + + eqz = TexMobject("= 0") + eqz.next_to(label, RIGHT) + eq_zero.add(eqz) + + v_dots = TexMobject("\\vdots") + v_dots.next_to(q_marks[-1][0], DOWN, MED_LARGE_BUFF) + + # Animations + self.play(FadeInFromDown(labels[0])) + self.play(FadeIn(q_marks[0], LEFT)) + self.wait() + self.play(*[ + TransformFromCopy(m1, m2) + for m1, m2 in [ + (q_marks[0], q_marks[1]), + (labels[0][:3], labels[1][:3]), + (labels[0][-1], labels[1][-1]), + ] + ]) + self.play(ShowIncreasingSubsets( + labels[1][3], + run_time=3, + int_func=np.ceil, + rate_func=linear, + )) + self.add(labels[1]) + self.wait() + self.play( + LaggedStartMap( + FadeInFrom, labels[2:], + lambda m: (m, UP), + ), + LaggedStartMap( + FadeInFrom, q_marks[2:], + lambda m: (m, UP), + ), + Write(v_dots, rate_func=squish_rate_func(smooth, 0.5, 1)) + ) + self.add(labels, q_marks) + self.wait() + + q_marks.unlock_triangulation() + self.play( + ReplacementTransform(q_marks, gt_zero, lag_ratio=0.05), + run_time=2, + ) + self.wait() + + # Show sum + group = VGroup(labels, gt_zero, v_dots) + sum_label = TexMobject( + "\\sum_{0 \\le {h} \\le 1}", "P(", "{h}", ")", "=", + tex_to_color_map={"{h}": YELLOW}, + ) + # sum_label.set_color_by_tex("{s}", YELLOW) + sum_label[0].set_color(WHITE) + sum_label.scale(1.75) + sum_label.next_to(ORIGIN, RIGHT, buff=1) + sum_label.shift(LEFT) + + morty = Mortimer() + morty.set_height(2) + morty.to_corner(DR) + + self.play(group.to_corner, DL) + self.play( + Write(sum_label), + VFadeIn(morty), + morty.change, "confused", sum_label, + ) + + infty = TexMobject("\\infty") + zero = TexMobject("0") + for mob in [infty, zero]: + mob.scale(2) + mob.next_to(sum_label[-1], RIGHT) + zero.set_color(RED) + zero.shift(SMALL_BUFF * RIGHT) + + self.play( + Write(infty), + morty.change, "horrified", infty, + ) + self.play(Blink(morty)) + self.wait() + + # If equal to zero + eq_zero.move_to(gt_zero) + eq_zero.set_color(RED) + gt_zero.unlock_triangulation() + self.play( + ReplacementTransform( + gt_zero, eq_zero, + lag_ratio=0.05, + run_time=2, + path_arc=30 * DEGREES, + ), + morty.change, "pondering", eq_zero, + ) + self.wait() + self.play( + FadeIn(zero, DOWN), + FadeOut(infty, UP), + morty.change, "sad", zero + ) + self.play(Blink(morty)) + self.wait() + + +class WanderingArrow(Scene): + def construct(self): + arrow = Vector(0.8 * DOWN) + arrow.move_to(4 * LEFT, DOWN) + for u in [1, -1, 1, -1, 1]: + self.play( + arrow.shift, u * 8 * RIGHT, + run_time=3 + ) + + +class ProbabilityToContinuousValuesSupplement(Scene): + def construct(self): + nl = UnitInterval() + nl.set_width(10) + nl.add_numbers( + *np.arange(0, 1.1, 0.1), + buff=0.3, + ) + nl.to_edge(LEFT) + self.add(nl) + + def f(x): + return -100 * (x - 0.6) * (x - 0.8) + + values = np.linspace(0.65, 0.75, 100) + lines = VGroup() + for x, color in zip(values, it.cycle([BLUE_E, BLUE_C])): + line = Line(ORIGIN, UP) + line.set_height(f(x)) + line.set_stroke(color, 1) + line.move_to(nl.n2p(x), DOWN) + lines.add(line) + + self.play(ShowCreation(lines, lag_ratio=0.9, run_time=5)) + + lines_row = lines.copy() + lines_row.generate_target() + for lt in lines_row.target: + lt.rotate(90 * DEGREES) + lines_row.target.arrange(RIGHT, buff=0) + lines_row.target.set_stroke(width=4) + lines_row.target.next_to(nl, UP, LARGE_BUFF) + lines_row.target.align_to(nl.n2p(0), LEFT) + + self.play( + MoveToTarget( + lines_row, + lag_ratio=0.1, + rate_func=rush_into, + run_time=4, + ) + ) + self.wait() + self.play( + lines.set_height, 0.01, {"about_edge": DOWN, "stretch": True}, + ApplyMethod( + lines_row.set_width, 0.01, {"about_edge": LEFT}, + rate_func=rush_into, + ), + run_time=6, + ) + self.wait() + + +class CarFactoryNumbers(Scene): + def construct(self): + # Test words + denom_words = TextMobject( + "in a test of 100 cars", + tex_to_color_map={"100": BLUE}, + ) + denom_words.to_corner(UR) + + numer_words = TextMobject( + "2 defects found", + tex_to_color_map={"2": RED} + ) + numer_words.move_to(denom_words, LEFT) + + self.play(Write(denom_words, run_time=1)) + self.wait() + self.play( + denom_words.next_to, numer_words, DOWN, {"aligned_edge": LEFT}, + FadeIn(numer_words), + ) + self.wait() + + # Question words + question = VGroup( + TextMobject("What can you say"), + TexMobject( + "\\text{about } P(\\text{defect})?", + tex_to_color_map={"\\text{defect}": RED} + ) + ) + + question.arrange(DOWN, aligned_edge=LEFT) + question.next_to(denom_words, DOWN, buff=1.5, aligned_edge=LEFT) + + self.play(FadeIn(question)) + self.wait() + + +class TeacherHoldingValue(TeacherStudentsScene): + def construct(self): + self.play(self.teacher.change, "raise_right_hand", self.screen) + self.change_all_student_modes( + "pondering", + look_at_arg=self.screen, + ) + self.wait(8) + + +class ShowLimitToPdf(Scene): + def construct(self): + # Init + axes = self.get_axes() + alpha = 4 + beta = 2 + dist = scipy.stats.beta(alpha, beta) + bars = self.get_bars(axes, dist, 0.05) + + axis_prob_label = TextMobject("Probability") + axis_prob_label.next_to(axes.y_axis, UP) + axis_prob_label.to_edge(LEFT) + + self.add(axes) + self.add(axis_prob_label) + + # From individual to ranges + kw = {"tex_to_color_map": {"h": YELLOW}} + eq_label = TexMobject("P(h = 0.8)", **kw) + ineq_label = TexMobject("P(0.8 < h < 0.85)", **kw) + + arrows = VGroup(Vector(DOWN), Vector(DOWN)) + for arrow, x in zip(arrows, [0.8, 0.85]): + arrow.move_to(axes.c2p(x, 0), DOWN) + brace = Brace( + Line(arrows[0].get_start(), arrows[1].get_start()), + UP, buff=SMALL_BUFF + ) + eq_label.next_to(arrows[0], UP) + ineq_label.next_to(brace, UP) + + self.play( + FadeIn(eq_label, 0.2 * DOWN), + GrowArrow(arrows[0]), + ) + self.wait() + vect = eq_label.get_center() - ineq_label.get_center() + self.play( + FadeOut(eq_label, -vect), + FadeIn(ineq_label, vect), + TransformFromCopy(*arrows), + GrowFromPoint(brace, brace.get_left()), + ) + self.wait() + + # Bars + arrow = arrows[0] + arrow.generate_target() + arrow.target.next_to(bars[16], UP, SMALL_BUFF) + highlighted_bar_color = RED_E + bars[16].set_color(highlighted_bar_color) + + for bar in bars: + bar.save_state() + bar.stretch(0, 1, about_edge=DOWN) + + kw = { + "run_time": 2, + "rate_func": squish_rate_func(smooth, 0.3, 0.9), + } + self.play( + MoveToTarget(arrow, **kw), + ApplyMethod(ineq_label.next_to, arrows[0].target, UP, **kw), + FadeOut(arrows[1]), + FadeOut(brace), + LaggedStartMap(Restore, bars, run_time=2, lag_ratio=0.025), + ) + self.wait() + + # Focus on area, not height + lines = VGroup() + new_bars = VGroup() + for bar in bars: + line = Line( + bar.get_corner(DL), + bar.get_corner(DR), + ) + line.set_stroke(YELLOW, 0) + line.generate_target() + line.target.set_stroke(YELLOW, 3) + line.target.move_to(bar.get_top()) + lines.add(line) + + new_bar = bar.copy() + new_bar.match_style(line) + new_bar.set_fill(YELLOW, 0.5) + new_bar.generate_target() + new_bar.stretch(0, 1, about_edge=UP) + new_bars.add(new_bar) + + prob_label = TextMobject( + "Height", + "$\\rightarrow$", + "Probability", + ) + prob_label.space_out_submobjects(1.1) + prob_label.next_to(bars[10], UL, LARGE_BUFF) + height_word = prob_label[0] + height_cross = Cross(height_word) + area_word = TextMobject("Area") + area_word.move_to(height_word, UR) + area_word.set_color(YELLOW) + + self.play( + LaggedStartMap( + MoveToTarget, lines, + lag_ratio=0.01, + ), + FadeInFromDown(prob_label), + ) + self.add(height_word) + self.play( + ShowCreation(height_cross), + FadeOut(axis_prob_label, LEFT) + ) + self.wait() + self.play( + FadeOut(height_word, UP), + FadeOut(height_cross, UP), + FadeInFromDown(area_word), + ) + self.play( + FadeOut(lines), + LaggedStartMap( + MoveToTarget, new_bars, + lag_ratio=0.01, + ) + ) + self.play( + FadeOut(new_bars), + area_word.set_color, BLUE, + ) + + prob_label = VGroup(area_word, *prob_label[1:]) + self.add(prob_label) + + # Ask about where values come from + randy = Randolph(height=1) + randy.next_to(prob_label, UP, aligned_edge=LEFT) + + bubble = SpeechBubble( + height=2, + width=4, + ) + bubble.move_to(randy.get_corner(UR), DL) + bubble.write("Where do these\\\\probabilities come from?") + + self.play( + FadeIn(randy), + ShowCreation(bubble), + ) + self.play( + randy.change, "confused", + FadeIn(bubble.content, lag_ratio=0.1) + ) + self.play(Blink(randy)) + + bars.generate_target() + bars.save_state() + bars.target.arrange(RIGHT, buff=SMALL_BUFF, aligned_edge=DOWN) + bars.target.next_to(bars.get_bottom(), UP) + + self.play(MoveToTarget(bars)) + self.play(LaggedStartMap(Indicate, bars, scale_factor=1.05), run_time=1) + self.play(Restore(bars)) + self.play(Blink(randy)) + self.play( + FadeOut(randy), + FadeOut(bubble), + FadeOut(bubble.content), + ) + + # Refine + last_ineq_label = ineq_label + last_bars = bars + all_ineq_labels = VGroup(ineq_label) + for step_size in [0.025, 0.01, 0.005, 0.001]: + new_bars = self.get_bars(axes, dist, step_size) + new_ineq_label = TexMobject( + "P(0.8 < h < {:.3})".format(0.8 + step_size), + tex_to_color_map={"h": YELLOW}, + ) + + if step_size <= 0.005: + new_bars.set_stroke(width=0) + + arrow.generate_target() + bar = new_bars[int(0.8 * len(new_bars))] + bar.set_color(highlighted_bar_color) + arrow.target.next_to(bar, UP, SMALL_BUFF) + new_ineq_label.next_to(arrow.target, UP) + + vect = new_ineq_label.get_center() - last_ineq_label.get_center() + + self.wait() + self.play( + ReplacementTransform( + last_bars, new_bars, + lag_ratio=step_size, + ), + MoveToTarget(arrow), + FadeOut(last_ineq_label, vect), + FadeIn(new_ineq_label, -vect), + run_time=2, + ) + last_ineq_label = new_ineq_label + last_bars = new_bars + all_ineq_labels.add(new_ineq_label) + + # Show continuous graph + graph = get_beta_graph(axes, alpha - 1, beta - 1) + graph_curve = axes.get_graph(dist.pdf) + graph_curve.set_stroke([YELLOW, GREEN]) + + limit_words = TextMobject("In the limit...") + limit_words.next_to( + axes.input_to_graph_point(0.75, graph_curve), + UP, MED_LARGE_BUFF, + ) + + self.play( + FadeIn(graph), + FadeOut(last_ineq_label), + FadeOut(arrow), + FadeOut(last_bars), + ) + self.play( + ShowCreation(graph_curve), + Write(limit_words, run_time=1) + ) + self.play(FadeOut(graph_curve)) + self.wait() + + # Show individual probabilities goes to zero + all_ineq_labels.arrange(DOWN, aligned_edge=LEFT) + all_ineq_labels.move_to(prob_label, LEFT) + all_ineq_labels.to_edge(UP) + + prob_label.generate_target() + prob_label.target.next_to( + all_ineq_labels, DOWN, + buff=MED_LARGE_BUFF, + aligned_edge=LEFT + ) + + rhss = VGroup() + step_sizes = [0.05, 0.025, 0.01, 0.005, 0.001] + for label, step in zip(all_ineq_labels, step_sizes): + eq = TexMobject("=") + decimal = DecimalNumber( + dist.cdf(0.8 + step) - dist.cdf(0.8), + num_decimal_places=3, + ) + eq.next_to(label, RIGHT) + decimal.next_to(eq, RIGHT) + decimal.set_stroke(BLACK, 3, background=True) + rhss.add(VGroup(eq, decimal)) + + for rhs in rhss: + rhs.align_to(rhss[1], LEFT) + + VGroup(all_ineq_labels, rhss).set_height(3, about_edge=UL) + + arrow = Arrow(rhss.get_top(), rhss.get_bottom(), buff=0) + arrow.next_to(rhss, RIGHT) + arrow.set_color(YELLOW) + to_zero_words = TextMobject("Individual probabilites\\\\", "go to zero") + to_zero_words[1].align_to(to_zero_words[0], LEFT) + to_zero_words.next_to(arrow, RIGHT, aligned_edge=UP) + + self.play( + LaggedStartMap( + FadeInFrom, all_ineq_labels, + lambda m: (m, UP), + ), + LaggedStartMap( + FadeInFrom, rhss, + lambda m: (m, UP), + ), + MoveToTarget(prob_label) + ) + self.play( + GrowArrow(arrow), + FadeIn(to_zero_words), + ) + self.play( + LaggedStartMap( + Indicate, rhss, + scale_factor=1.05, + ) + ) + self.wait(2) + + # What if it was heights + bars.restore() + height_word.move_to(area_word, RIGHT) + height_word.set_color(PINK) + step = 0.05 + new_y_numbers = VGroup(*[ + DecimalNumber(x) for x in np.arange(step, 5 * step, step) + ]) + for n1, n2 in zip(axes.y_axis.numbers, new_y_numbers): + n2.match_height(n1) + n2.add_background_rectangle( + opacity=1, + buff=SMALL_BUFF, + ) + n2.move_to(n1, RIGHT) + + self.play( + FadeOut(limit_words), + FadeOut(graph), + FadeIn(bars), + FadeOut(area_word, UP), + FadeIn(height_word, DOWN), + FadeIn(new_y_numbers, 0.5 * RIGHT), + ) + + # Height refine + rect = SurroundingRectangle(rhss[0][1]) + rect.set_stroke(RED, 3) + self.play(FadeIn(rect)) + + last_bars = bars + for step_size, rhs in zip(step_sizes[1:], rhss[1:]): + new_bars = self.get_bars(axes, dist, step_size) + bar = new_bars[int(0.8 * len(new_bars))] + bar.set_color(highlighted_bar_color) + new_bars.stretch( + step_size / 0.05, 1, + about_edge=DOWN, + ) + if step_size <= 0.05: + new_bars.set_stroke(width=0) + self.remove(last_bars) + self.play( + TransformFromCopy(last_bars, new_bars, lag_ratio=step_size), + rect.move_to, rhs[1], + ) + last_bars = new_bars + self.play( + FadeOut(last_bars), + FadeOutAndShiftDown(rect), + ) + self.wait() + + # Back to area + self.play( + FadeIn(graph), + FadeIn(area_word, 0.5 * DOWN), + FadeOut(height_word, 0.5 * UP), + FadeOut(new_y_numbers, lag_ratio=0.2), + ) + self.play( + arrow.scale, 0, {"about_edge": DOWN}, + FadeOut(to_zero_words, DOWN), + LaggedStartMap(FadeOutAndShiftDown, all_ineq_labels), + LaggedStartMap(FadeOutAndShiftDown, rhss), + ) + self.wait() + + # Ask about y_axis units + arrow = Arrow( + axes.y_axis.get_top() + 3 * RIGHT, + axes.y_axis.get_top(), + path_arc=90 * DEGREES, + ) + question = TextMobject("What are the\\\\units here?") + question.next_to(arrow.get_start(), DOWN) + + self.play( + FadeIn(question, lag_ratio=0.1), + ShowCreation(arrow), + ) + self.wait() + + # Bring back bars + bars = self.get_bars(axes, dist, 0.05) + self.play( + FadeOut(graph), + FadeIn(bars), + ) + bars.generate_target() + bars.save_state() + bars.target.set_opacity(0.2) + bar_index = int(0.8 * len(bars)) + bars.target[bar_index].set_opacity(0.8) + bar = bars[bar_index] + + prob_word = TextMobject("Probability") + prob_word.rotate(90 * DEGREES) + prob_word.set_height(0.8 * bar.get_height()) + prob_word.move_to(bar) + + self.play( + MoveToTarget(bars), + Write(prob_word, run_time=1), + ) + self.wait() + + # Show dimensions of bar + top_brace = Brace(bar, UP) + side_brace = Brace(bar, LEFT) + top_label = top_brace.get_tex("\\Delta x") + side_label = side_brace.get_tex( + "{\\text{Prob.} \\over \\Delta x}" + ) + + self.play( + GrowFromCenter(top_brace), + FadeIn(top_label), + ) + self.play(GrowFromCenter(side_brace)) + self.wait() + self.play(Write(side_label)) + self.wait() + + y_label = TextMobject("Probability density") + y_label.next_to(axes.y_axis, UP, aligned_edge=LEFT) + + self.play( + Uncreate(arrow), + FadeOutAndShiftDown(question), + Write(y_label), + ) + self.wait(2) + self.play( + Restore(bars), + FadeOut(top_brace), + FadeOut(side_brace), + FadeOut(top_label), + FadeOut(side_label), + FadeOut(prob_word), + ) + + # Point out total area is 1 + total_label = TextMobject("Total area = 1") + total_label.set_height(0.5) + total_label.next_to(bars, UP, LARGE_BUFF) + + self.play(FadeIn(total_label, DOWN)) + bars.save_state() + self.play( + bars.arrange, RIGHT, {"aligned_edge": DOWN, "buff": SMALL_BUFF}, + bars.move_to, bars.get_bottom() + 0.5 * UP, DOWN, + ) + self.play(LaggedStartMap(Indicate, bars, scale_factor=1.05)) + self.play(Restore(bars)) + + # Refine again + for step_size in step_sizes[1:]: + new_bars = self.get_bars(axes, dist, step_size) + if step_size <= 0.05: + new_bars.set_stroke(width=0) + self.play( + ReplacementTransform( + bars, new_bars, lag_ratio=step_size + ), + run_time=3, + ) + self.wait() + bars = new_bars + self.add(graph, total_label) + self.play( + FadeIn(graph), + FadeOut(bars), + total_label.move_to, axes.c2p(0.7, 0.8) + ) + self.wait() + + # Name pdf + func_name = TextMobject("Probability ", "Density ", "Function") + initials = TextMobject("P", "D", "F") + for mob in func_name, initials: + mob.set_color(YELLOW) + mob.next_to(axes.input_to_graph_point(0.75, graph_curve), UP) + + self.play( + ShowCreation(graph_curve), + Write(func_name, run_time=1), + ) + self.wait() + func_name_copy = func_name.copy() + self.play( + func_name.next_to, initials, UP, + *[ + ReplacementTransform(np[0], ip[0]) + for np, ip in zip(func_name_copy, initials) + ], + *[ + FadeOut(np[1:]) + for np in func_name_copy + ] + ) + self.add(initials) + self.wait() + self.play( + FadeOut(func_name), + FadeOut(total_label), + FadeOut(graph_curve), + initials.next_to, axes.input_to_graph_point(0.95, graph_curve), UR, + ) + + # Look at bounded area + min_x = 0.6 + max_x = 0.8 + region = get_region_under_curve(axes, graph_curve, min_x, max_x) + area_label = DecimalNumber( + dist.cdf(max_x) - dist.cdf(min_x), + num_decimal_places=3, + ) + area_label.move_to(region) + + v_lines = VGroup() + for x in [min_x, max_x]: + v_lines.add( + DashedLine( + axes.c2p(x, 0), + axes.c2p(x, 2.5), + ) + ) + v_lines.set_stroke(YELLOW, 2) + + p_label = VGroup( + TexMobject("P("), + DecimalNumber(min_x), + TexMobject("\\le"), + TexMobject("h", color=YELLOW), + TexMobject("\\le"), + DecimalNumber(max_x), + TexMobject(")") + ) + p_label.arrange(RIGHT, buff=0.25) + VGroup(p_label[0], p_label[-1]).space_out_submobjects(0.92) + p_label.next_to(v_lines, UP) + + rhs = VGroup( + TexMobject("="), + area_label.copy() + ) + rhs.arrange(RIGHT) + rhs.next_to(p_label, RIGHT) + + self.play( + FadeIn(p_label, 2 * DOWN), + *map(ShowCreation, v_lines), + ) + self.wait() + region.func = get_region_under_curve + self.play( + UpdateFromAlphaFunc( + region, + lambda m, a: m.become( + m.func( + m.axes, m.graph, + m.min_x, + interpolate(m.min_x, m.max_x, a) + ) + ) + ), + CountInFrom(area_label), + UpdateFromAlphaFunc( + area_label, + lambda m, a: m.set_opacity(a), + ), + ) + self.wait() + self.play( + TransformFromCopy(area_label, rhs[1]), + Write(rhs[0]), + ) + self.wait() + + # Change range + new_x = np.mean([min_x, max_x]) + area_label.original_width = area_label.get_width() + region.new_x = new_x + # Squish to area 1 + self.play( + ChangeDecimalToValue(p_label[1], new_x), + ChangeDecimalToValue(p_label[5], new_x), + ChangeDecimalToValue(area_label, 0), + UpdateFromAlphaFunc( + area_label, + lambda m, a: m.set_width( + interpolate(m.original_width, 1e-6, a) + ) + ), + ChangeDecimalToValue(rhs[1], 0), + v_lines[0].move_to, axes.c2p(new_x, 0), DOWN, + v_lines[1].move_to, axes.c2p(new_x, 0), DOWN, + UpdateFromAlphaFunc( + region, + lambda m, a: m.become(m.func( + m.axes, m.graph, + interpolate(m.min_x, m.new_x, a), + interpolate(m.max_x, m.new_x, a), + )) + ), + run_time=2, + ) + self.wait() + + # Stretch to area 1 + self.play( + ChangeDecimalToValue(p_label[1], 0), + ChangeDecimalToValue(p_label[5], 1), + ChangeDecimalToValue(area_label, 1), + UpdateFromAlphaFunc( + area_label, + lambda m, a: m.set_width( + interpolate(1e-6, m.original_width, clip(5 * a, 0, 1)) + ) + ), + ChangeDecimalToValue(rhs[1], 1), + v_lines[0].move_to, axes.c2p(0, 0), DOWN, + v_lines[1].move_to, axes.c2p(1, 0), DOWN, + UpdateFromAlphaFunc( + region, + lambda m, a: m.become(m.func( + m.axes, m.graph, + interpolate(m.new_x, 0, a), + interpolate(m.new_x, 1, a), + )) + ), + run_time=5, + ) + self.wait() + + def get_axes(self): + axes = Axes( + x_min=0, + x_max=1, + x_axis_config={ + "tick_frequency": 0.05, + "unit_size": 12, + "include_tip": False, + }, + y_min=0, + y_max=4, + y_axis_config={ + "tick_frequency": 1, + "unit_size": 1.25, + "include_tip": False, + } + ) + axes.center() + + h_label = TexMobject("h") + h_label.set_color(YELLOW) + h_label.next_to(axes.x_axis.n2p(1), UR, buff=0.2) + axes.x_axis.add(h_label) + axes.x_axis.label = h_label + + axes.x_axis.add_numbers( + *np.arange(0.2, 1.2, 0.2), + number_config={"num_decimal_places": 1} + ) + axes.y_axis.add_numbers(*range(1, 5)) + return axes + + def get_bars(self, axes, dist, step_size): + bars = VGroup() + for x in np.arange(0, 1, step_size): + bar = Rectangle() + bar.set_stroke(BLUE, 2) + bar.set_fill(BLUE, 0.5) + h_line = Line( + axes.c2p(x, 0), + axes.c2p(x + step_size, 0), + ) + v_line = Line( + axes.c2p(0, 0), + axes.c2p(0, dist.pdf(x)), + ) + bar.match_width(h_line, stretch=True) + bar.match_height(v_line, stretch=True) + bar.move_to(h_line, DOWN) + bars.add(bar) + return bars + + +class FiniteVsContinuum(Scene): + def construct(self): + # Title + f_title = TextMobject("Discrete context") + f_title.set_height(0.5) + f_title.to_edge(UP) + f_underline = Underline(f_title) + f_underline.scale(1.3) + f_title.add(f_underline) + self.add(f_title) + + # Equations + dice = get_die_faces()[::2] + cards = [PlayingCard(letter + "H") for letter in "A35"] + + eqs = VGroup( + self.get_union_equation(dice), + self.get_union_equation(cards), + ) + for eq in eqs: + eq.set_width(FRAME_WIDTH - 1) + eqs.arrange(DOWN, buff=LARGE_BUFF) + eqs.next_to(f_underline, DOWN, LARGE_BUFF) + + anims = [] + for eq in eqs: + movers = eq.mob_copies1.copy() + for m1, m2 in zip(movers, eq.mob_copies2): + m1.generate_target() + m1.target.replace(m2) + eq.mob_copies2.set_opacity(0) + eq.add(movers) + + self.play(FadeIn(eq[0])) + + anims.append(FadeIn(eq[1:])) + anims.append(LaggedStartMap( + MoveToTarget, movers, + path_arc=30 * DEGREES, + lag_ratio=0.1, + )) + self.wait() + for anim in anims: + self.play(anim) + + # Continuum label + c_title = TextMobject("Continuous context") + c_title.match_height(f_title) + c_underline = Underline(c_title) + c_underline.scale(1.25) + + self.play( + Write(c_title, run_time=1), + ShowCreation(c_underline), + eqs[0].shift, 0.5 * UP, + eqs[1].shift, UP, + ) + + # Range sum + c_eq = TexMobject( + "P\\big(", "x \\in [0.65, 0.75]", "\\big)", + "=", + "\\sum_{x \\in [0.65, 0.75]}", + "P(", "x", ")", + ) + c_eq.set_color_by_tex("P", YELLOW) + c_eq.set_color_by_tex(")", YELLOW) + c_eq.next_to(c_underline, DOWN, LARGE_BUFF) + c_eq.to_edge(LEFT) + + equals = c_eq.get_part_by_tex("=") + equals.shift(SMALL_BUFF * RIGHT) + e_cross = Line(DL, UR) + e_cross.replace(equals, dim_to_match=0) + e_cross.set_stroke(RED, 5) + + self.play(FadeIn(c_eq)) + self.wait(2) + self.play(ShowCreation(e_cross)) + self.wait() + + def get_union_equation(self, mobs): + mob_copies1 = VGroup() + mob_copies2 = VGroup() + p_color = YELLOW + + # Create mob_set + brackets = TexMobject("\\big\\{\\big\\}")[0] + mob_set = VGroup(brackets[0]) + commas = VGroup() + for mob in mobs: + mc = mob.copy() + mc.match_height(mob_set[0]) + mob_copies1.add(mc) + comma = TexMobject(",") + commas.add(comma) + mob_set.add(mc) + mob_set.add(comma) + + mob_set.remove(commas[-1]) + commas.remove(commas[-1]) + mob_set.add(brackets[1]) + mob_set.arrange(RIGHT, buff=0.15) + commas.set_y(mob_set[1].get_bottom()[1]) + + mob_set.scale(0.8) + + # Create individual probabilities + probs = VGroup() + for mob in mobs: + prob = TexMobject("P(", "x = ", "00", ")") + index = prob.index_of_part_by_tex("00") + mc = mob.copy() + mc.replace(prob[index]) + mc.scale(0.8, about_edge=LEFT) + mc.match_y(prob[-1]) + mob_copies2.add(mc) + prob.replace_submobject(index, mc) + prob[0].set_color(p_color) + prob[1].match_y(mc) + prob[-1].set_color(p_color) + probs.add(prob) + + # Result + lhs = VGroup( + TexMobject("P\\big(", color=p_color), + TexMobject("x \\in"), + mob_set, + TexMobject("\\big)", color=p_color), + ) + lhs.arrange(RIGHT, buff=SMALL_BUFF) + group = VGroup(lhs, TexMobject("=")) + for prob in probs: + group.add(prob) + group.add(TexMobject("+")) + group.remove(group[-1]) + + group.arrange(RIGHT, buff=0.2) + group.mob_copies1 = mob_copies1 + group.mob_copies2 = mob_copies2 + + return group + + +class ComplainAboutRuleChange(TeacherStudentsScene): + def construct(self): + self.student_says( + "Wait, the rules\\\\changed?", + target_mode="sassy", + added_anims=[self.teacher.change, "tease"] + ) + self.change_student_modes("erm", "confused") + self.wait(4) + self.teacher_says("You may enjoy\\\\``Measure theory''") + self.change_all_student_modes( + "pondering", + look_at_arg=self.teacher.bubble + ) + self.wait(8) + + +class HalfFiniteHalfContinuous(Scene): + def construct(self): + # Basic symbols + box = Rectangle(width=3, height=1.2) + box.set_stroke(WHITE, 2) + box.set_fill(GREY_E, 1) + box.move_to(2.5 * LEFT, RIGHT) + + arrows = VGroup() + arrow_labels = VGroup() + for vect in [UP, DOWN]: + arrow = Arrow( + box.get_corner(vect + RIGHT), + box.get_corner(vect + RIGHT) + 3 * RIGHT + 1.5 * vect, + buff=MED_SMALL_BUFF, + ) + label = TexMobject("50\\%") + fix_percent(label[0][-1]) + label.set_color(YELLOW) + label.next_to( + arrow.get_center(), + vect + LEFT, + buff=SMALL_BUFF, + ) + + arrow_labels.add(label) + arrows.add(arrow) + + zero = Integer(0) + zero.set_height(0.5) + zero.next_to(arrows[0].get_end(), RIGHT) + + # Half Gaussian + axes = Axes( + x_min=0, + x_max=6.5, + y_min=0, + y_max=0.25, + y_axis_config={ + "tick_frequency": 1 / 16, + "unit_size": 10, + "include_tip": False, + } + ) + axes.next_to(arrows[1].get_end(), RIGHT) + + dist = scipy.stats.norm(0, 2) + graph = axes.get_graph(dist.pdf) + graph_fill = graph.copy() + close_off_graph(axes, graph_fill) + graph.set_stroke(BLUE, 3) + graph_fill.set_fill(BLUE_E, 1) + graph_fill.set_stroke(BLUE_E, 0) + + half_gauss = Group( + graph, graph_fill, axes, + ) + + # Random Decimal + number = DecimalNumber(num_decimal_places=4) + number.set_height(0.6) + number.move_to(box) + + number.time = 0 + number.last_change = 0 + number.change_freq = 0.2 + + def update_number(number, dt, dist=dist): + number.time += dt + + if (number.time - number.last_change) < number.change_freq: + return + + number.last_change = number.time + rand_val = random.random() + if rand_val < 0.5: + number.set_value(0) + else: + number.set_value(dist.ppf(rand_val)) + + number.add_updater(update_number) + + v_line = SurroundingRectangle(zero) + v_line.save_state() + v_line.set_stroke(YELLOW, 3) + + def update_v_line(v_line, number=number, axes=axes, graph=graph): + x = number.get_value() + if x < 0.5: + v_line.restore() + else: + v_line.set_width(1e-6) + p1 = axes.c2p(x, 0) + p2 = axes.input_to_graph_point(x, graph) + v_line.set_height(get_norm(p2 - p1), stretch=True) + v_line.move_to(p1, DOWN) + + v_line.add_updater(update_v_line) + + # Add everything + self.add(box) + self.add(number) + self.wait(4) + self.play( + GrowArrow(arrows[0]), + FadeIn(arrow_labels[0]), + GrowFromPoint(zero, box.get_corner(UR)) + ) + self.wait(2) + self.play( + GrowArrow(arrows[1]), + FadeIn(arrow_labels[1]), + FadeIn(half_gauss), + ) + self.add(v_line) + + self.wait(30) + + +class SumToIntegral(Scene): + def construct(self): + # Titles + titles = VGroup( + TextMobject("Discrete context"), + TextMobject("Continuous context"), + ) + titles.set_height(0.5) + for title, vect in zip(titles, [LEFT, RIGHT]): + title.move_to(vect * FRAME_WIDTH / 4) + title.to_edge(UP, buff=MED_SMALL_BUFF) + + v_line = Line(UP, DOWN).set_height(FRAME_HEIGHT) + h_line = Line(LEFT, RIGHT).set_width(FRAME_WIDTH) + h_line.next_to(titles, DOWN) + h_line.set_x(0) + v_line.center() + + self.play( + ShowCreation(VGroup(h_line, v_line)), + LaggedStartMap( + FadeInFrom, titles, + lambda m: (m, -0.2 * m.get_center()[0] * RIGHT), + run_time=1, + lag_ratio=0.1, + ), + ) + self.wait() + + # Sum and int + kw = {"tex_to_color_map": {"S": BLUE}} + s_sym = TexMobject("\\sum", "_{x \\in S} P(x)", **kw) + i_sym = TexMobject("\\int_{S} p(x)", "\\text{d}x", **kw) + syms = VGroup(s_sym, i_sym) + syms.scale(2) + for sym, title in zip(syms, titles): + sym.shift(-sym[-1].get_center()) + sym.match_x(title) + + arrow = Arrow( + s_sym[0].get_corner(UP), + i_sym[0].get_corner(UP), + path_arc=-90 * DEGREES, + ) + arrow.set_color(YELLOW) + + self.play(Write(s_sym, run_time=1)) + anims = [ShowCreation(arrow)] + for i, j in [(0, 0), (2, 1), (3, 2)]: + source = s_sym[i].deepcopy() + target = i_sym[j] + target.save_state() + source.generate_target() + target.replace(source, stretch=True) + source.target.replace(target, stretch=True) + target.set_opacity(0) + source.target.set_opacity(0) + anims += [ + Restore(target, path_arc=-60 * DEGREES), + MoveToTarget(source, path_arc=-60 * DEGREES), + ] + self.play(LaggedStart(*anims)) + self.play(FadeInFromDown(i_sym[3])) + self.add(i_sym) + self.wait() + self.play( + FadeOut(arrow, UP), + syms.next_to, h_line, DOWN, {"buff": MED_LARGE_BUFF}, + syms.match_x, syms, + ) + + # Add curve area in editing + # Add bar chart + axes = Axes( + x_min=0, + x_max=10, + y_min=0, + y_max=7, + y_axis_config={ + "unit_size": 0.75, + } + ) + axes.set_width(0.5 * FRAME_WIDTH - 1) + axes.next_to(s_sym, DOWN) + axes.y_axis.add_numbers(2, 4, 6) + + bars = VGroup() + for x, y in [(1, 1), (4, 3), (7, 2)]: + bar = Rectangle() + bar.set_stroke(WHITE, 1) + bar.set_fill(BLUE_D, 1) + line = Line(axes.c2p(x, 0), axes.c2p(x + 2, y)) + bar.replace(line, stretch=True) + bars.add(bar) + + addition_formula = TexMobject(*"1+3+2") + addition_formula.space_out_submobjects(2.1) + addition_formula.next_to(bars, UP) + + for bar in bars: + bar.save_state() + bar.stretch(0, 1, about_edge=DOWN) + + self.play( + Write(axes), + LaggedStartMap(Restore, bars), + LaggedStartMap(FadeInFromDown, addition_formula), + ) + self.wait() + + # Confusion + morty = Mortimer() + morty.to_corner(DR) + morty.look_at(i_sym) + self.play( + *map(FadeOut, [axes, bars, addition_formula]), + FadeIn(morty) + ) + self.play(morty.change, "maybe") + self.play(Blink(morty)) + self.play(morty.change, "confused", i_sym.get_right()) + self.play(Blink(morty)) + self.wait() + + # Focus on integral + self.play( + Uncreate(VGroup(v_line, h_line)), + FadeOut(titles, UP), + FadeOut(morty, RIGHT), + FadeOut(s_sym, LEFT), + i_sym.center, + i_sym.to_edge, LEFT + ) + + arrows = VGroup() + for vect in [UP, DOWN]: + corner = i_sym[-1].get_corner(RIGHT + vect) + arrows.add(Arrow( + corner, + corner + 2 * RIGHT + 2 * vect, + path_arc=-np.sign(vect[1]) * 60 * DEGREES, + )) + + self.play(*map(ShowCreation, arrows)) + + # Types of integration + dist = scipy.stats.beta(7 + 1, 3 + 1) + axes_pair = VGroup() + graph_pair = VGroup() + for arrow in arrows: + axes = get_beta_dist_axes(y_max=5, y_unit=1) + axes.set_width(4) + axes.next_to(arrow.get_end(), RIGHT) + graph = axes.get_graph(dist.pdf) + graph.set_stroke(BLUE, 2) + graph.set_fill(BLUE_E, 0) + graph.make_smooth() + axes_pair.add(axes) + graph_pair.add(graph) + + r_axes, l_axes = axes_pair + r_graph, l_graph = graph_pair + r_name = TextMobject("Riemann\\\\Integration") + r_name.next_to(r_axes, RIGHT) + l_name = TextMobject("Lebesgue\\\\Integration$^*$") + l_name.next_to(l_axes, RIGHT) + footnote = TextMobject("*a bit more complicated than\\\\these bars make it look") + footnote.match_width(l_name) + footnote.next_to(l_name, DOWN) + + self.play(LaggedStart( + FadeIn(r_axes), + FadeIn(r_graph), + FadeIn(r_name), + FadeIn(l_axes), + FadeIn(l_graph), + FadeIn(l_name), + run_time=1, + )) + + # Approximation bars + def get_riemann_rects(dx, axes=r_axes, func=dist.pdf): + bars = VGroup() + for x in np.arange(0, 1, dx): + bar = Rectangle() + line = Line( + axes.c2p(x, 0), + axes.c2p(x + dx, func(x)), + ) + bar.replace(line, stretch=True) + bar.set_stroke(BLUE_E, width=10 * dx, opacity=1) + bar.set_fill(BLUE, 0.5) + bars.add(bar) + return bars + + def get_lebesgue_bars(dy, axes=l_axes, func=dist.pdf, mx=0.7, y_max=dist.pdf(0.7)): + bars = VGroup() + for y in np.arange(dy, y_max + dy, dy): + x0 = binary_search(func, y, 0, mx) or mx + x1 = binary_search(func, y, mx, 1) or mx + line = Line(axes.c2p(x0, y - dy), axes.c2p(x1, y)) + bar = Rectangle() + bar.set_stroke(RED_E, 0) + bar.set_fill(RED_E, 0.5) + bar.replace(line, stretch=True) + bars.add(bar) + return bars + + r_bar_groups = [] + l_bar_groups = [] + Ns = [10, 20, 40, 80, 160] + Ms = [2, 4, 8, 16, 32] + for N, M in zip(Ns, Ms): + r_bar_groups.append(get_riemann_rects(dx=1 / N)) + l_bar_groups.append(get_lebesgue_bars(dy=1 / M)) + self.play( + FadeIn(r_bar_groups[0], lag_ratio=0.1), + FadeIn(l_bar_groups[0], lag_ratio=0.1), + FadeIn(footnote), + ) + self.wait() + for rbg0, rbg1, lbg0, lbg1 in zip(r_bar_groups, r_bar_groups[1:], l_bar_groups, l_bar_groups[1:]): + self.play( + ReplacementTransform( + rbg0, rbg1, + lag_ratio=1 / len(rbg0), + run_time=2, + ), + ReplacementTransform( + lbg0, lbg1, + lag_ratio=1 / len(lbg0), + run_time=2, + ), + ) + self.wait() + self.play( + FadeOut(r_bar_groups[-1]), + FadeOut(l_bar_groups[-1]), + r_graph.set_fill, BLUE_E, 1, + l_graph.set_fill, RED_E, 1, + ) + + +class MeasureTheoryLeadsTo(Scene): + def construct(self): + words = TextMobject("Measure Theory") + words.set_color(RED) + arrow = Vector(DOWN) + arrow.next_to(words, DOWN, buff=SMALL_BUFF) + arrow.set_stroke(width=7) + arrow.rotate(45 * DEGREES, about_point=arrow.get_start()) + self.play( + FadeIn(words, DOWN), + GrowArrow(arrow), + UpdateFromAlphaFunc(arrow, lambda m, a: m.set_opacity(a)), + ) + self.wait() + + +class WhenIWasFirstLearning(TeacherStudentsScene): + def construct(self): + self.teacher.change_mode("raise_right_hand") + self.play( + self.get_student_changes("pondering", "thinking", "tease"), + self.teacher.change, "thinking", + ) + + younger = BabyPiCreature(color=GREY_BROWN) + younger.set_height(2) + younger.move_to(self.students, DL) + + self.look_at(self.screen) + self.wait() + self.play( + ReplacementTransform(self.teacher, younger), + LaggedStartMap( + FadeOutAndShift, self.students, + lambda m: (m, DOWN), + ) + ) + + # Bubble + bubble = ThoughtBubble() + bubble[-1].set_fill(GREEN_SCREEN, 1) + bubble.move_to(younger.get_corner(UR), DL) + + self.play( + Write(bubble), + younger.change, "maybe", bubble.get_bubble_center(), + ) + self.play(Blink(younger)) + for mode in ["confused", "angry", "pondering", "maybe"]: + self.play(younger.change, mode) + for x in range(2): + self.wait() + if random.random() < 0.5: + self.play(Blink(younger)) + + +class PossibleYetProbabilityZero(Scene): + def construct(self): + poss = TextMobject("Possible") + prob = TextMobject("Probability = 0") + total = TextMobject("P(dart hits somewhere) = 1") + # total[1].next_to(total[0][0], RIGHT) + words = VGroup(poss, prob, total) + words.scale(1.5) + words.arrange(DOWN, aligned_edge=LEFT, buff=MED_LARGE_BUFF) + + self.play(Write(poss, run_time=0.5)) + self.wait() + self.play(FadeIn(prob, UP)) + self.wait() + self.play(FadeIn(total, UP)) + self.wait() + + +class TiePossibleToDensity(Scene): + def construct(self): + poss = TextMobject("Possibility") + prob = TextMobject("Probability", " $>$ 0") + dens = TextMobject("Probability \\emph{density}", " $>$ 0") + dens[0].set_color(BLUE) + implies = TexMobject("\\Rightarrow") + implies2 = implies.copy() + + poss.next_to(implies, LEFT) + prob.next_to(implies, RIGHT) + dens.next_to(implies, RIGHT) + cross = Cross(implies) + + self.camera.frame.scale(0.7, about_point=dens.get_center()) + + self.add(poss) + self.play( + FadeIn(prob, LEFT), + Write(implies, run_time=1) + ) + self.wait() + self.play(ShowCreation(cross)) + self.wait() + + self.play( + VGroup(implies, cross, prob).shift, UP, + FadeIn(implies2), + FadeIn(dens), + ) + self.wait() + + self.embed() + + +class DrawBigRect(Scene): + def construct(self): + rect = Rectangle(width=7, height=2.5) + rect.set_stroke(RED, 5) + rect.to_edge(RIGHT) + + words = TextMobject("Not how to\\\\think about it") + words.set_color(RED) + words.align_to(rect, LEFT) + words.to_edge(UP) + + arrow = Arrow( + words.get_bottom(), + rect.get_top(), + buff=0.25, + color=RED, + ) + + self.play(ShowCreation(rect)) + self.play( + FadeInFromDown(words), + GrowArrow(arrow), + ) + self.wait() + + +class Thumbnail(Scene): + def construct(self): + dartboard = Dartboard() + axes = NumberPlane( + x_min=-1.25, + x_max=1.25, + y_min=-1.25, + y_max=1.25, + axis_config={ + "unit_size": 0.5 * dartboard.get_width(), + "tick_frequency": 0.25, + }, + x_line_frequency=1.0, + y_line_frequency=1.0, + ) + group = VGroup(dartboard, axes) + group.to_edge(LEFT, buff=0) + + # Arrow + arrow = Vector(DR, max_stroke_width_to_length_ratio=np.inf) + arrow.move_to(axes.c2p(PI / 10, np.exp(1) / 10), DR) + arrow.scale(1.5, about_edge=DR) + arrow.set_stroke(WHITE, 10) + + black_arrow = arrow.copy() + black_arrow.set_color(BLACK) + black_arrow.set_stroke(width=20) + + arrow.points[0] += 0.025 * DR + + # Coords + coords = TexMobject("(x, y) = (0.31415\\dots, 0.27182\\dots)") + coords.set_width(5.5) + coords.set_stroke(BLACK, 10, background=True) + coords.next_to(axes.get_bottom(), UP, buff=0) + + # Words + words = VGroup( + TextMobject("Probability = 0"), + TextMobject("$\\dots$but still possible"), + ) + for word in words: + word.set_width(6) + words.arrange(DOWN, buff=MED_LARGE_BUFF) + words.next_to(axes, RIGHT) + words.to_edge(UP, buff=LARGE_BUFF) + + # Pi + morty = Mortimer() + morty.to_corner(DR) + morty.change("confused", words) + + self.add(group) + self.add(black_arrow) + self.add(arrow) + self.add(coords) + self.add(words) + self.add(morty) + + self.embed() + + +class Part2EndScreen(PatreonEndScreen): + CONFIG = { + "scroll_time": 30, + "specific_patrons": [ + "1stViewMaths", + "Adam Dřínek", + "Aidan Shenkman", + "Alan Stein", + "Albin Egasse", + "Alex Mijalis", + "Alexander Mai", + "Alexis Olson", + "Ali Yahya", + "Andrew Busey", + "Andrew Cary", + "Andrew R. Whalley", + "Anthony Losego", + "Aravind C V", + "Arjun Chakroborty", + "Arthur Zey", + "Ashwin Siddarth", + "Augustine Lim", + "Austin Goodman", + "Avi Finkel", + "Awoo", + "Axel Ericsson", + "Ayan Doss", + "AZsorcerer", + "Barry Fam", + "Ben Delo", + "Bernd Sing", + "Bill Gatliff", + "Bob Sanderson", + "Boris Veselinovich", + "Bradley Pirtle", + "Brandon Huang", + "Brian Staroselsky", + "Britt Selvitelle", + "Britton Finley", + "Burt Humburg", + "Calvin Lin", + "Charles Southerland", + "Charlie N", + "Chenna Kautilya", + "Chris Connett", + "Chris Druta", + "Christian Kaiser", + "cinterloper", + "Clark Gaebel", + "Colwyn Fritze-Moor", + "Cooper Jones", + "Corey Ogburn", + "D. Sivakumar", + "Dan Herbatschek", + "Daniel Brown", + "Daniel Herrera C", + "Darrell Thomas", + "Dave B", + "Dave Kester", + "dave nicponski", + "David B. Hill", + "David Clark", + "David Gow", + "Delton Ding", + "Dominik Wagner", + "Eddie Landesberg", + "Eduardo Rodriguez", + "emptymachine", + "Eric Younge", + "Eryq Ouithaqueue", + "Federico Lebron", + "Fernando Via Canel", + "Frank R. Brown, Jr.", + "Gavin", + "Giovanni Filippi", + "Goodwine", + "Hal Hildebrand", + "Hitoshi Yamauchi", + "Ivan Sorokin", + "Jacob Baxter", + "Jacob Harmon", + "Jacob Hartmann", + "Jacob Magnuson", + "Jalex Stark", + "Jameel Syed", + "James Beall", + "Jason Hise", + "Jayne Gabriele", + "Jean-Manuel Izaret", + "Jeff Dodds", + "Jeff Linse", + "Jeff Straathof", + "Jimmy Yang", + "John C. Vesey", + "John Camp", + "John Haley", + "John Le", + "John Luttig", + "John Rizzo", + "John V Wertheim", + "Jonathan Heckerman", + "Jonathan Wilson", + "Joseph John Cox", + "Joseph Kelly", + "Josh Kinnear", + "Joshua Claeys", + "Joshua Ouellette", + "Juan Benet", + "Kai-Siang Ang", + "Kanan Gill", + "Karl Niu", + "Kartik Cating-Subramanian", + "Kaustuv DeBiswas", + "Killian McGuinness", + "Klaas Moerman", + "Kros Dai", + "L0j1k", + "Lael S Costa", + "LAI Oscar", + "Lambda GPU Workstations", + "Laura Gast", + "Lee Redden", + "Linh Tran", + "Luc Ritchie", + "Ludwig Schubert", + "Lukas Biewald", + "Lukas Zenick", + "Magister Mugit", + "Magnus Dahlström", + "Magnus Hiie", + "Manoj Rewatkar - RITEK SOLUTIONS", + "Mark B Bahu", + "Mark Heising", + "Mark Mann", + "Martin Price", + "Mathias Jansson", + "Matt Godbolt", + "Matt Langford", + "Matt Roveto", + "Matt Russell", + "Matteo Delabre", + "Matthew Bouchard", + "Matthew Cocke", + "Maxim Nitsche", + "Michael Bos", + "Michael Day", + "Michael Hardel", + "Michael W White", + "Mihran Vardanyan", + "Mirik Gogri", + "Molly Mackinlay", + "Mustafa Mahdi", + "Márton Vaitkus", + "Nate Heckmann", + "Nicholas Cahill", + "Nikita Lesnikov", + "Oleg Leonov", + "Omar Zrien", + "Owen Campbell-Moore", + "Patrick Lucas", + "Pavel Dubov", + "Pesho Ivanov", + "Petar Veličković", + "Peter Ehrnstrom", + "Peter Francis", + "Peter Mcinerney", + "Pierre Lancien", + "Pradeep Gollakota", + "Rafael Bove Barrios", + "Randy C. Will", + "rehmi post", + "Rex Godby", + "Ripta Pasay", + "Rish Kundalia", + "Roman Sergeychik", + "Roobie", + "Ryan Atallah", + "Ryan Prayogo", + "Samuel Judge", + "SansWord Huang", + "Scott Gray", + "Scott Walter, Ph.D.", + "soekul", + "Solara570", + "Steve Huynh", + "Steve Muench", + "Steve Sperandeo", + "Steven Siddals", + "Stevie Metke", + "Sunil Nagaraj", + "supershabam", + "Susanne Fenja Mehr-Koks", + "Suteerth Vishnu", + "Suthen Thomas", + "Tal Einav", + "Taras Bobrovytsky", + "Tauba Auerbach", + "Ted Suzman", + "THIS IS THE point OF NO RE tUUurRrhghgGHhhnnn", + "Thomas J Sargent", + "Thomas Tarler", + "Tianyu Ge", + "Tihan Seale", + "Tyler Herrmann", + "Tyler McAtee", + "Tyler VanValkenburg", + "Tyler Veness", + "Vassili Philippov", + "Vasu Dubey", + "Veritasium", + "Vignesh Ganapathi Subramanian", + "Vinicius Reis", + "Vladimir Solomatin", + "Wooyong Ee", + "Xuanji Li", + "Yana Chernobilsky", + "YinYangBalance.Asia", + "Yorick Lesecque", + "Yu Jun", + "Yurii Monastyrshyn", + ], + } diff --git a/from_3b1b/active/bayes/beta3.py b/from_3b1b/active/bayes/beta3.py new file mode 100644 index 0000000000..c094ff19c3 --- /dev/null +++ b/from_3b1b/active/bayes/beta3.py @@ -0,0 +1,2150 @@ +from manimlib.imports import * +from from_3b1b.active.bayes.beta_helpers import * +from from_3b1b.active.bayes.beta1 import * +from from_3b1b.active.bayes.beta2 import ShowLimitToPdf + +import scipy.stats + +OUTPUT_DIRECTORY = "bayes/beta3" + + +class RemindOfWeightedCoin(Scene): + def construct(self): + # Largely copied from beta2 + + # Prob label + p_label = get_prob_coin_label() + p_label.set_height(0.7) + p_label.to_edge(UP) + + rhs = p_label[-1] + q_box = get_q_box(rhs) + p_label.add(q_box) + + self.add(p_label) + + # Coin grid + def get_random_coin_grid(p): + bools = np.random.random(100) < p + grid = get_coin_grid(bools) + return grid + + grid = get_random_coin_grid(0.5) + grid.next_to(p_label, DOWN, MED_LARGE_BUFF) + + self.play(LaggedStartMap( + FadeIn, grid, + lag_ratio=2 / len(grid), + run_time=3, + )) + self.wait() + + # Label as h + brace = Brace(q_box, DOWN, buff=SMALL_BUFF) + h_label = TexMobject("h") + h_label.next_to(brace, DOWN) + eq = TexMobject("=") + eq.next_to(h_label, RIGHT) + h_decimal = DecimalNumber(0.5) + h_decimal.next_to(eq, RIGHT) + + self.play( + GrowFromCenter(brace), + FadeIn(h_label, UP), + grid.scale, 0.8, {"about_edge": DOWN}, + ) + self.wait() + + # Alternate weightings + tail_grid = get_random_coin_grid(0) + head_grid = get_random_coin_grid(1) + grid70 = get_random_coin_grid(0.7) + alt_grids = [tail_grid, head_grid, grid70] + for ag in alt_grids: + ag.replace(grid) + + for coins in [grid, *alt_grids]: + for coin in coins: + coin.generate_target() + coin.target.rotate(90 * DEGREES, axis=UP) + coin.target.set_opacity(0) + + def get_grid_swap_anims(g1, g2): + return [ + LaggedStartMap(MoveToTarget, g1, lag_ratio=0.02, run_time=1.5, remover=True), + LaggedStartMap(MoveToTarget, g2, lag_ratio=0.02, run_time=1.5, rate_func=reverse_smooth), + ] + + self.play( + FadeIn(eq), + UpdateFromAlphaFunc(h_decimal, lambda m, a: m.set_opacity(a)), + ChangeDecimalToValue(h_decimal, 0, run_time=2), + *get_grid_swap_anims(grid, tail_grid) + ) + self.wait() + self.play( + ChangeDecimalToValue(h_decimal, 1, run_time=1.5), + *get_grid_swap_anims(tail_grid, head_grid) + ) + self.wait() + self.play( + ChangeDecimalToValue(h_decimal, 0.7, run_time=1.5), + *get_grid_swap_anims(head_grid, grid70) + ) + self.wait() + + # Graph + axes = scaled_pdf_axes() + axes.to_edge(DOWN, buff=MED_SMALL_BUFF) + axes.y_axis.numbers.set_opacity(0) + axes.y_axis_label.set_opacity(0) + + h_lines = VGroup() + for y in range(15): + h_line = Line(axes.c2p(0, y), axes.c2p(1, y)) + h_lines.add(h_line) + h_lines.set_stroke(WHITE, 0.5, opacity=0.5) + axes.add(h_lines) + + x_axis_label = p_label[:4].copy() + x_axis_label.set_height(0.4) + x_axis_label.next_to(axes.c2p(1, 0), UR, buff=SMALL_BUFF) + axes.x_axis.add(x_axis_label) + + n_heads_tracker = ValueTracker(3) + n_tails_tracker = ValueTracker(3) + + def get_graph(axes=axes, nht=n_heads_tracker, ntt=n_tails_tracker): + dist = scipy.stats.beta(nht.get_value() + 1, ntt.get_value() + 1) + graph = axes.get_graph(dist.pdf, step_size=0.05) + graph.set_stroke(BLUE, 3) + graph.set_fill(BLUE_E, 1) + return graph + + graph = always_redraw(get_graph) + + area_label = TextMobject("Area = 1") + area_label.set_height(0.5) + area_label.move_to(axes.c2p(0.5, 1)) + + # pdf label + pdf_label = TextMobject("probability ", "density ", "function") + pdf_label.next_to(axes.input_to_graph_point(0.5, graph), UP) + pdf_target_template = TextMobject("p", "d", "f") + pdf_target_template.next_to(axes.input_to_graph_point(0.7, graph), UR) + pdf_label.generate_target() + for part, letter2 in zip(pdf_label.target, pdf_target_template): + for letter1 in part: + letter1.move_to(letter2) + part[1:].set_opacity(0) + + # Add plot + self.add(axes, *self.mobjects) + self.play( + FadeOut(eq), + FadeOut(h_decimal), + LaggedStartMap(MoveToTarget, grid70, run_time=1, remover=True), + FadeIn(axes), + ) + self.play( + DrawBorderThenFill(graph), + FadeIn(area_label, rate_func=squish_rate_func(smooth, 0.5, 1), run_time=2), + Write(pdf_label, run_time=1), + ) + self.wait() + + # Region + lh_tracker = ValueTracker(0.7) + rh_tracker = ValueTracker(0.7) + + def get_region(axes=axes, graph=graph, lh_tracker=lh_tracker, rh_tracker=rh_tracker): + lh = lh_tracker.get_value() + rh = rh_tracker.get_value() + region = get_region_under_curve(axes, graph, lh, rh) + region.set_fill(GREY, 0.85) + region.set_stroke(YELLOW, 1) + return region + + region = always_redraw(get_region) + + region_area_label = DecimalNumber(num_decimal_places=3) + region_area_label.next_to(axes.c2p(0.7, 0), UP, MED_LARGE_BUFF) + + def update_ra_label(label, nht=n_heads_tracker, ntt=n_tails_tracker, lht=lh_tracker, rht=rh_tracker): + dist = scipy.stats.beta(nht.get_value() + 1, ntt.get_value() + 1) + area = dist.cdf(rht.get_value()) - dist.cdf(lht.get_value()) + label.set_value(area) + + region_area_label.add_updater(update_ra_label) + + range_label = VGroup( + TexMobject("0.6 \\le"), + p_label[:4].copy(), + TexMobject("\\le 0.8"), + ) + for mob in range_label: + mob.set_height(0.4) + range_label.arrange(RIGHT, buff=SMALL_BUFF) + pp_label = VGroup( + TexMobject("P("), + range_label, + TexMobject(")"), + ) + for mob in pp_label[::2]: + mob.set_height(0.7) + mob.set_color(YELLOW) + pp_label.arrange(RIGHT, buff=SMALL_BUFF) + pp_label.move_to(axes.c2p(0.3, 3)) + + self.play( + FadeIn(pp_label[::2]), + MoveToTarget(pdf_label), + FadeOut(area_label), + ) + self.wait() + self.play(TransformFromCopy(p_label[:4], range_label[1])) + self.wait() + self.play(TransformFromCopy(axes.x_axis.numbers[2], range_label[0])) + self.play(TransformFromCopy(axes.x_axis.numbers[3], range_label[2])) + self.wait() + + self.add(region) + self.play( + lh_tracker.set_value, 0.6, + rh_tracker.set_value, 0.8, + UpdateFromAlphaFunc( + region_area_label, + lambda m, a: m.set_opacity(a), + rate_func=squish_rate_func(smooth, 0.25, 1) + ), + run_time=3, + ) + self.wait() + + # 7/10 heads + bools = [True] * 7 + [False] * 3 + random.shuffle(bools) + coins = VGroup(*[ + get_coin("H" if heads else "T") + for heads in bools + ]) + coins.arrange(RIGHT) + coins.set_height(0.7) + coins.next_to(h_label, DOWN, buff=MED_LARGE_BUFF) + + heads = [c for c in coins if c.symbol == "H"] + numbers = VGroup(*[ + Integer(i + 1).set_height(0.2).next_to(coin, DOWN, SMALL_BUFF) + for i, coin in enumerate(heads) + ]) + + for coin in coins: + coin.save_state() + coin.rotate(90 * DEGREES, UP) + coin.set_opacity(0) + + pp_label.generate_target() + pp_label.target.set_height(0.5) + pp_label.target.next_to(axes.c2p(0, 2), RIGHT, MED_LARGE_BUFF) + + self.play( + LaggedStartMap(Restore, coins), + MoveToTarget(pp_label), + run_time=1, + ) + self.play(ShowIncreasingSubsets(numbers)) + self.wait() + + # Move plot + self.play( + n_heads_tracker.set_value, 7, + n_tails_tracker.set_value, 3, + FadeOut(pdf_label, rate_func=squish_rate_func(smooth, 0, 0.5)), + run_time=2 + ) + self.wait() + + # How does the answer change with more data + new_bools = [True] * 63 + [False] * 27 + random.shuffle(new_bools) + bools = [c.symbol == "H" for c in coins] + new_bools + grid = get_coin_grid(bools) + grid.set_height(3.5) + grid.next_to(axes.c2p(0, 3), RIGHT, MED_LARGE_BUFF) + + self.play( + FadeOut(numbers), + ReplacementTransform(coins, grid[:10]), + ) + self.play( + FadeIn(grid[10:], lag_ratio=0.1, rate_func=linear), + pp_label.next_to, grid, DOWN, + ) + self.wait() + self.add(graph, region, region_area_label, p_label, q_box, brace, h_label) + self.play( + n_heads_tracker.set_value, 70, + n_tails_tracker.set_value, 30, + ) + self.wait() + origin = axes.c2p(0, 0) + self.play( + axes.y_axis.stretch, 0.5, 1, {"about_point": origin}, + h_lines.stretch, 0.5, 1, {"about_point": origin}, + ) + self.wait() + + # Shift the shape around + pairs = [ + (70 * 3, 30 * 3), + (35, 15), + (35 + 20, 15 + 20), + (7, 3), + (70, 30), + ] + for nh, nt in pairs: + self.play( + n_heads_tracker.set_value, nh, + n_tails_tracker.set_value, nt, + run_time=2, + ) + self.wait() + + # End + self.embed() + + +class LastTimeWrapper(Scene): + def construct(self): + fs_rect = FullScreenFadeRectangle(fill_opacity=1, fill_color=GREY_E) + self.add(fs_rect) + + title = TextMobject("Last Time") + title.scale(1.5) + title.to_edge(UP) + + rect = ScreenRectangle() + rect.set_height(6) + rect.set_fill(BLACK, 1) + rect.next_to(title, DOWN) + + self.play( + DrawBorderThenFill(rect), + FadeInFromDown(title), + ) + self.wait() + + +class ComplainAboutSimplisticModel(ExternallyAnimatedScene): + pass + + +class BayesianFrequentistDivide(Scene): + def construct(self): + # Setup Bayesian vs. Frequentist divide + b_label = TextMobject("Bayesian") + f_label = TextMobject("Frequentist") + labels = VGroup(b_label, f_label) + for label, vect in zip(labels, [LEFT, RIGHT]): + label.set_height(0.7) + label.move_to(vect * FRAME_WIDTH / 4) + label.to_edge(UP, buff=0.35) + + h_line = Line(LEFT, RIGHT) + h_line.set_width(FRAME_WIDTH) + h_line.next_to(labels, DOWN) + v_line = Line(UP, DOWN) + v_line.set_height(FRAME_HEIGHT) + v_line.center() + + for label in labels: + label.save_state() + label.set_y(0) + self.play( + FadeIn(label, -normalize(label.get_center())), + ) + self.wait() + self.play( + ShowCreation(VGroup(v_line, h_line)), + *map(Restore, labels), + ) + self.wait() + + # Overlay ShowBayesianUpdating in editing + # Frequentist list (ignore?) + kw = { + "tex_to_color_map": { + "$p$-value": YELLOW, + "$H_0$": PINK, + "$\\alpha$": BLUE, + }, + "alignment": "", + } + freq_list = VGroup( + TextMobject("1. State a null hypothesis $H_0$", **kw), + TextMobject("2. Choose a test statistic,\\\\", "$\\qquad$ compute its value", **kw), + TextMobject("3. Calculate a $p$-value", **kw), + TextMobject("4. Choose a significance value $\\alpha$", **kw), + TextMobject("5. Reject $H_0$ if $p$-value\\\\", "$\\qquad$ is less than $\\alpha$", **kw), + ) + + freq_list.set_width(0.5 * FRAME_WIDTH - 1) + freq_list.arrange(DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT) + freq_list.move_to(FRAME_WIDTH * RIGHT / 4) + freq_list.to_edge(DOWN, buff=LARGE_BUFF) + + # Frequentist icon + axes = get_beta_dist_axes(y_max=5, y_unit=1) + axes.set_width(0.5 * FRAME_WIDTH - 1) + axes.move_to(FRAME_WIDTH * RIGHT / 4 + DOWN) + + dist = scipy.stats.norm(0.5, 0.1) + graph = axes.get_graph(dist.pdf) + graphs = VGroup() + for x_min, x_max in [(0, 0.3), (0.3, 0.7), (0.7, 1.0)]: + graph = axes.get_graph(dist.pdf, x_min=x_min, x_max=x_max) + graph.add_line_to(axes.c2p(x_max, 0)) + graph.add_line_to(axes.c2p(x_min, 0)) + graph.add_line_to(graph.get_start()) + graphs.add(graph) + + graphs.set_stroke(width=0) + graphs.set_fill(RED, 1) + graphs[1].set_fill(GREY_D, 1) + + H_words = VGroup(*[TextMobject("Reject\\\\$H_0$") for x in range(2)]) + for H_word, graph, vect in zip(H_words, graphs[::2], [RIGHT, LEFT]): + H_word.next_to(graph, UP, MED_LARGE_BUFF) + arrow = Arrow( + H_word.get_bottom(), + graph.get_center() + 0.75 * vect, + buff=SMALL_BUFF + ) + H_word.add(arrow) + + H_words.set_color(RED) + self.add(H_words) + + self.add(axes) + self.add(graphs) + + self.embed() + + # Transition to 2x2 + # Go back to prior + # Label uniform prior + # Talk about real coin prior + # Update ad infinitum + + +class ArgumentBetweenBayesianAndFrequentist(Scene): + def construct(self): + pass + + +# From version 1 +class ShowBayesianUpdating(Scene): + CONFIG = { + "true_p": 0.72, + "random_seed": 4, + "initial_axis_scale_factor": 3.5 + } + + def construct(self): + # Axes + axes = scaled_pdf_axes(self.initial_axis_scale_factor) + self.add(axes) + + # Graph + n_heads = 0 + n_tails = 0 + graph = get_beta_graph(axes, n_heads, n_tails) + self.add(graph) + + # Get coins + true_p = self.true_p + bool_values = np.random.random(100) < true_p + bool_values[1] = True + coins = self.get_coins(bool_values) + coins.next_to(axes.y_axis, RIGHT, MED_LARGE_BUFF) + coins.to_edge(UP, LARGE_BUFF) + + # Probability label + p_label, prob, prob_box = self.get_probability_label() + self.add(p_label) + self.add(prob_box) + + # Slow animations + def head_likelihood(x): + return x + + def tail_likelihood(x): + return 1 - x + + n_previews = 10 + n_slow_previews = 5 + for x in range(n_previews): + coin = coins[x] + is_heads = bool_values[x] + + new_data_label = TextMobject("New data") + new_data_label.set_height(0.3) + arrow = Vector(0.5 * UP) + arrow.next_to(coin, DOWN, SMALL_BUFF) + new_data_label.next_to(arrow, DOWN, SMALL_BUFF) + new_data_label.shift(MED_SMALL_BUFF * RIGHT) + + if is_heads: + line = axes.get_graph(lambda x: x) + label = TexMobject("\\text{Scale by } x") + likelihood = head_likelihood + n_heads += 1 + else: + line = axes.get_graph(lambda x: 1 - x) + label = TexMobject("\\text{Scale by } (1 - x)") + likelihood = tail_likelihood + n_tails += 1 + label.next_to(graph, UP) + label.set_stroke(BLACK, 3, background=True) + line.set_stroke(YELLOW, 3) + + graph_copy = graph.copy() + graph_copy.unlock_triangulation() + scaled_graph = graph.copy() + scaled_graph.apply_function( + lambda p: axes.c2p( + axes.x_axis.p2n(p), + axes.y_axis.p2n(p) * likelihood(axes.x_axis.p2n(p)) + ) + ) + scaled_graph.set_color(GREEN) + + renorm_label = TextMobject("Renormalize") + renorm_label.move_to(label) + + new_graph = get_beta_graph(axes, n_heads, n_tails) + + renormalized_graph = scaled_graph.copy() + renormalized_graph.match_style(graph) + renormalized_graph.match_height(new_graph, stretch=True, about_edge=DOWN) + + if x < n_slow_previews: + self.play( + FadeInFromDown(coin), + FadeIn(new_data_label), + GrowArrow(arrow), + ) + self.play( + FadeOut(new_data_label), + FadeOut(arrow), + ShowCreation(line), + FadeIn(label), + ) + self.add(graph_copy, line, label) + self.play(Transform(graph_copy, scaled_graph)) + self.play( + FadeOut(line), + FadeOut(label), + FadeIn(renorm_label), + ) + self.play( + Transform(graph_copy, renormalized_graph), + FadeOut(graph), + ) + self.play(FadeOut(renorm_label)) + else: + self.add(coin) + graph_copy.become(scaled_graph) + self.add(graph_copy) + self.play( + Transform(graph_copy, renormalized_graph), + FadeOut(graph), + ) + graph = new_graph + self.remove(graph_copy) + self.add(new_graph) + + # Rescale y axis + axes.save_state() + sf = self.initial_axis_scale_factor + axes.y_axis.stretch(1 / sf, 1, about_point=axes.c2p(0, 0)) + for number in axes.y_axis.numbers: + number.stretch(sf, 1) + axes.y_axis.numbers[:4].set_opacity(0) + + self.play( + Restore(axes, rate_func=lambda t: smooth(1 - t)), + graph.stretch, 1 / sf, 1, {"about_edge": DOWN}, + run_time=2, + ) + + # Fast animations + for x in range(n_previews, len(coins)): + coin = coins[x] + is_heads = bool_values[x] + + if is_heads: + n_heads += 1 + else: + n_tails += 1 + new_graph = get_beta_graph(axes, n_heads, n_tails) + + self.add(coins[:x + 1]) + self.add(new_graph) + self.remove(graph) + self.wait(0.25) + # self.play( + # FadeIn(new_graph), + # run_time=0.25, + # ) + # self.play( + # FadeOut(graph), + # run_time=0.25, + # ) + graph = new_graph + + # Show confidence interval + dist = scipy.stats.beta(n_heads + 1, n_tails + 1) + v_lines = VGroup() + labels = VGroup() + x_bounds = dist.interval(0.95) + for x in x_bounds: + line = DashedLine( + axes.c2p(x, 0), + axes.c2p(x, 12), + ) + line.set_color(YELLOW) + v_lines.add(line) + label = DecimalNumber(x) + label.set_height(0.25) + label.next_to(line, UP) + label.match_color(line) + labels.add(label) + + true_graph = axes.get_graph(dist.pdf) + region = get_region_under_curve(axes, true_graph, *x_bounds) + region.set_fill(GREY_BROWN, 0.85) + region.set_stroke(YELLOW, 1) + + label95 = TexMobject("95\\%") + fix_percent(label95.family_members_with_points()[-1]) + label95.move_to(region, DOWN) + label95.shift(0.5 * UP) + + self.play(*map(ShowCreation, v_lines)) + self.play( + FadeIn(region), + Write(label95) + ) + self.wait() + for label in labels: + self.play(FadeInFromDown(label)) + self.wait() + + # Show true value + self.wait() + self.play(FadeOut(prob_box)) + self.play(ShowCreationThenFadeAround(prob)) + self.wait() + + # Much more data + many_bools = np.hstack([ + bool_values, + (np.random.random(1000) < true_p) + ]) + N_tracker = ValueTracker(100) + graph.N_tracker = N_tracker + graph.bools = many_bools + graph.axes = axes + graph.v_lines = v_lines + graph.labels = labels + graph.region = region + graph.label95 = label95 + + label95.width_ratio = label95.get_width() / region.get_width() + + def update_graph(graph): + N = int(graph.N_tracker.get_value()) + nh = sum(graph.bools[:N]) + nt = len(graph.bools[:N]) - nh + new_graph = get_beta_graph(graph.axes, nh, nt, step_size=0.05) + graph.become(new_graph) + + dist = scipy.stats.beta(nh + 1, nt + 1) + x_bounds = dist.interval(0.95) + for x, line, label in zip(x_bounds, graph.v_lines, graph.labels): + line.set_x(graph.axes.c2p(x, 0)[0]) + label.set_x(graph.axes.c2p(x, 0)[0]) + label.set_value(x) + + graph.labels[0].shift(MED_SMALL_BUFF * LEFT) + graph.labels[1].shift(MED_SMALL_BUFF * RIGHT) + + new_simple_graph = graph.axes.get_graph(dist.pdf) + new_region = get_region_under_curve(graph.axes, new_simple_graph, *x_bounds) + new_region.match_style(graph.region) + graph.region.become(new_region) + + graph.label95.set_width(graph.label95.width_ratio * graph.region.get_width()) + graph.label95.match_x(graph.region) + + self.add(graph, region, label95, p_label) + self.play( + N_tracker.set_value, 1000, + UpdateFromFunc(graph, update_graph), + Animation(v_lines), + Animation(labels), + Animation(graph.region), + Animation(graph.label95), + run_time=5, + ) + self.wait() + + # + + def get_coins(self, bool_values): + coins = VGroup(*[ + get_coin("H" if heads else "T") + for heads in bool_values + ]) + coins.arrange_in_grid(n_rows=10, buff=MED_LARGE_BUFF) + coins.set_height(5) + return coins + + def get_probability_label(self): + head = get_coin("H") + p_label = TexMobject( + "P(00) = ", + tex_to_color_map={"00": WHITE} + ) + template = p_label.get_part_by_tex("00") + head.replace(template) + p_label.replace_submobject( + p_label.index_of_part(template), + head, + ) + prob = DecimalNumber(self.true_p) + prob.next_to(p_label, RIGHT) + p_label.add(prob) + p_label.set_height(0.75) + p_label.to_corner(UR) + + prob_box = SurroundingRectangle(prob, buff=SMALL_BUFF) + prob_box.set_fill(GREY_D, 1) + prob_box.set_stroke(WHITE, 2) + + q_marks = TexMobject("???") + q_marks.move_to(prob_box) + prob_box.add(q_marks) + + return p_label, prob, prob_box + + +class HighlightReviewPartsReversed(HighlightReviewParts): + CONFIG = { + "reverse_order": True, + } + + +class Grey(Scene): + def construct(self): + self.add(FullScreenFadeRectangle(fill_color=GREY_D, fill_opacity=1)) + + +class ShowBayesRule(Scene): + def construct(self): + hyp = "\\text{Hypothesis}" + data = "\\text{Data}" + bayes = TexMobject( + f"P({hyp} \\,|\\, {data})", "=", "{", + f"P({data} \\,|\\, {hyp})", f"P({hyp})", + "\\over", f"P({data})", + tex_to_color_map={ + hyp: YELLOW, + data: GREEN, + } + ) + + title = TextMobject("Bayes' rule") + title.scale(2) + title.to_edge(UP) + + self.add(title) + self.add(*bayes[:5]) + self.wait() + self.play( + *[ + TransformFromCopy(bayes[i], bayes[j], path_arc=30 * DEGREES) + for i, j in [ + (0, 7), + (1, 10), + (2, 9), + (3, 8), + (4, 11), + ] + ], + FadeIn(bayes[5]), + run_time=1.5 + ) + self.wait() + self.play( + *[ + TransformFromCopy(bayes[i], bayes[j], path_arc=30 * DEGREES) + for i, j in [ + (0, 12), + (1, 13), + (4, 14), + (0, 16), + (3, 17), + (4, 18), + ] + ], + FadeIn(bayes[15]), + run_time=1.5 + ) + self.add(bayes) + self.wait() + + hyp_word = bayes.get_part_by_tex(hyp) + example_hyp = TextMobject( + "For example,\\\\", + "$0.9 < s < 0.99$", + ) + example_hyp[1].set_color(YELLOW) + example_hyp.next_to(hyp_word, DOWN, buff=1.5) + + data_word = bayes.get_part_by_tex(data) + example_data = TexMobject( + "48\\,", CMARK_TEX, + "\\,2\\,", XMARK_TEX, + ) + example_data.set_color_by_tex(CMARK_TEX, GREEN) + example_data.set_color_by_tex(XMARK_TEX, RED) + example_data.scale(1.5) + example_data.next_to(example_hyp, RIGHT, buff=1.5) + + hyp_arrow = Arrow( + hyp_word.get_bottom(), + example_hyp.get_top(), + ) + data_arrow = Arrow( + data_word.get_bottom(), + example_data.get_top(), + ) + + self.play( + GrowArrow(hyp_arrow), + FadeInFromPoint(example_hyp, hyp_word.get_center()), + ) + self.wait() + self.play( + GrowArrow(data_arrow), + FadeInFromPoint(example_data, data_word.get_center()), + ) + self.wait() + + +class VisualizeBayesRule(Scene): + def construct(self): + self.show_continuum() + self.show_arrows() + self.show_discrete_probabilities() + self.show_bayes_formula() + self.parallel_universes() + self.update_from_data() + + def show_continuum(self): + axes = get_beta_dist_axes(y_max=1, y_unit=0.1) + axes.y_axis.add_numbers( + *np.arange(0.2, 1.2, 0.2), + number_config={ + "num_decimal_places": 1, + } + ) + + p_label = TexMobject( + "P(s \\,|\\, \\text{data})", + tex_to_color_map={ + "s": YELLOW, + "\\text{data}": GREEN, + } + ) + p_label.scale(1.5) + p_label.to_edge(UP, LARGE_BUFF) + + s_part = p_label.get_part_by_tex("s").copy() + x_line = Line(axes.c2p(0, 0), axes.c2p(1, 0)) + x_line.set_stroke(YELLOW, 3) + + arrow = Vector(DOWN) + arrow.next_to(s_part, DOWN, SMALL_BUFF) + value = DecimalNumber(0, num_decimal_places=4) + value.set_color(YELLOW) + value.next_to(arrow, DOWN) + + self.add(axes) + self.add(p_label) + self.play( + s_part.next_to, x_line.get_start(), UR, SMALL_BUFF, + GrowArrow(arrow), + FadeInFromPoint(value, s_part.get_center()), + ) + + s_part.tracked = x_line + value.tracked = x_line + value.x_axis = axes.x_axis + self.play( + ShowCreation(x_line), + UpdateFromFunc( + s_part, + lambda m: m.next_to(m.tracked.get_end(), UR, SMALL_BUFF) + ), + UpdateFromFunc( + value, + lambda m: m.set_value( + m.x_axis.p2n(m.tracked.get_end()) + ) + ), + run_time=3, + ) + self.wait() + self.play( + FadeOut(arrow), + FadeOut(value), + ) + + self.p_label = p_label + self.s_part = s_part + self.value = value + self.x_line = x_line + self.axes = axes + + def show_arrows(self): + axes = self.axes + + arrows = VGroup() + arrow_template = Vector(DOWN) + arrow_template.lock_triangulation() + + def get_arrow(s, denom): + arrow = arrow_template.copy() + arrow.set_height(4 / denom) + arrow.move_to(axes.c2p(s, 0), DOWN) + arrow.set_color(interpolate_color( + GREY_A, GREY_C, random.random() + )) + return arrow + + for k in range(2, 50): + for n in range(1, k): + if np.gcd(n, k) != 1: + continue + s = n / k + arrows.add(get_arrow(s, k)) + for k in range(50, 1000): + arrows.add(get_arrow(1 / k, k)) + arrows.add(get_arrow(1 - 1 / k, k)) + + kw = { + "lag_ratio": 0.5, + "run_time": 5, + "rate_func": lambda t: t**4, + } + arrows.save_state() + for arrow in arrows: + arrow.stretch(0, 0) + arrow.set_stroke(width=0) + arrow.set_opacity(0) + self.play(Restore(arrows, **kw)) + self.play(LaggedStartMap( + ApplyMethod, arrows, + lambda m: (m.scale, 0, {"about_edge": DOWN}), + **kw + )) + self.remove(arrows) + self.wait() + + def show_discrete_probabilities(self): + axes = self.axes + + x_lines = VGroup() + dx = 0.01 + for x in np.arange(0, 1, dx): + line = Line( + axes.c2p(x, 0), + axes.c2p(x + dx, 0), + ) + line.set_stroke(BLUE, 3) + line.generate_target() + line.target.rotate( + 90 * DEGREES, + about_point=line.get_start() + ) + x_lines.add(line) + + self.add(x_lines) + self.play( + FadeOut(self.x_line), + LaggedStartMap( + MoveToTarget, x_lines, + ) + ) + + label = Integer(0) + label.set_height(0.5) + label.next_to(self.p_label[1], DOWN, LARGE_BUFF) + unit = TexMobject("\\%") + unit.match_height(label) + fix_percent(unit.family_members_with_points()[0]) + always(unit.next_to, label, RIGHT, SMALL_BUFF) + + arrow = Arrow() + arrow.max_stroke_width_to_length_ratio = 1 + arrow.axes = axes + arrow.label = label + arrow.add_updater(lambda m: m.put_start_and_end_on( + m.label.get_bottom() + MED_SMALL_BUFF * DOWN, + m.axes.c2p(0.01 * m.label.get_value(), 0.03), + )) + + self.add(label, unit, arrow) + self.play( + ChangeDecimalToValue(label, 99), + run_time=5, + ) + self.wait() + self.play(*map(FadeOut, [label, unit, arrow])) + + # Show prior label + p_label = self.p_label + given_data = p_label[2:4] + prior_label = TexMobject("P(s)", tex_to_color_map={"s": YELLOW}) + prior_label.match_height(p_label) + prior_label.move_to(p_label, DOWN, LARGE_BUFF) + + p_label.save_state() + self.play( + given_data.scale, 0.5, + given_data.set_opacity, 0.5, + given_data.to_corner, UR, + Transform(p_label[:2], prior_label[:2]), + Transform(p_label[-1], prior_label[-1]), + ) + self.wait() + + # Zoom in on the y-values + new_ticks = VGroup() + new_labels = VGroup() + dy = 0.01 + for y in np.arange(dy, 5 * dy, dy): + height = get_norm(axes.c2p(0, dy) - axes.c2p(0, 0)) + tick = axes.y_axis.get_tick(y, SMALL_BUFF) + label = DecimalNumber(y) + label.match_height(axes.y_axis.numbers[0]) + always(label.next_to, tick, LEFT, SMALL_BUFF) + + new_ticks.add(tick) + new_labels.add(label) + + for num in axes.y_axis.numbers: + height = num.get_height() + always(num.set_height, height, stretch=True) + + bars = VGroup() + dx = 0.01 + origin = axes.c2p(0, 0) + for x in np.arange(0, 1, dx): + rect = Rectangle( + width=get_norm(axes.c2p(dx, 0) - origin), + height=get_norm(axes.c2p(0, dy) - origin), + ) + rect.x = x + rect.set_stroke(BLUE, 1) + rect.set_fill(BLUE, 0.5) + rect.move_to(axes.c2p(x, 0), DL) + bars.add(rect) + + stretch_group = VGroup( + axes.y_axis, + bars, + new_ticks, + x_lines, + ) + x_lines.set_height( + bars.get_height(), + about_edge=DOWN, + stretch=True, + ) + + self.play( + stretch_group.stretch, 25, 1, {"about_point": axes.c2p(0, 0)}, + VFadeIn(bars), + VFadeIn(new_ticks), + VFadeIn(new_labels), + VFadeOut(x_lines), + run_time=4, + ) + + highlighted_bars = bars.copy() + highlighted_bars.set_color(YELLOW) + self.play( + LaggedStartMap( + FadeIn, highlighted_bars, + lag_ratio=0.5, + rate_func=there_and_back, + ), + ShowCreationThenFadeAround(new_labels[0]), + run_time=3, + ) + self.remove(highlighted_bars) + + # Nmae as prior + prior_name = TextMobject("Prior", " distribution") + prior_name.set_height(0.6) + prior_name.next_to(prior_label, DOWN, LARGE_BUFF) + + self.play(FadeInFromDown(prior_name)) + self.wait() + + # Show alternate distribution + bars.save_state() + for a, b in [(5, 2), (1, 6)]: + dist = scipy.stats.beta(a, b) + for bar, saved in zip(bars, bars.saved_state): + bar.target = saved.copy() + height = get_norm(axes.c2p(0.1 * dist.pdf(bar.x)) - axes.c2p(0, 0)) + bar.target.set_height(height, about_edge=DOWN, stretch=True) + + self.play(LaggedStartMap(MoveToTarget, bars, lag_ratio=0.00)) + self.wait() + self.play(Restore(bars)) + self.wait() + + uniform_name = TextMobject("Uniform") + uniform_name.match_height(prior_name) + uniform_name.move_to(prior_name, DL) + uniform_name.shift(RIGHT) + uniform_name.set_y(bars.get_top()[1] + MED_SMALL_BUFF, DOWN) + self.play( + prior_name[0].next_to, uniform_name, RIGHT, MED_SMALL_BUFF, DOWN, + FadeOut(prior_name[1], RIGHT), + FadeIn(uniform_name, LEFT) + ) + self.wait() + + self.bars = bars + self.uniform_label = VGroup(uniform_name, prior_name[0]) + + def show_bayes_formula(self): + uniform_label = self.uniform_label + p_label = self.p_label + bars = self.bars + + prior_label = VGroup( + p_label[0].deepcopy(), + p_label[1].deepcopy(), + p_label[4].deepcopy(), + ) + eq = TexMobject("=") + likelihood_label = TexMobject( + "P(", "\\text{data}", "|", "s", ")", + ) + likelihood_label.set_color_by_tex("data", GREEN) + likelihood_label.set_color_by_tex("s", YELLOW) + over = Line(LEFT, RIGHT) + p_data_label = TextMobject("P(", "\\text{data}", ")") + p_data_label.set_color_by_tex("data", GREEN) + + for mob in [eq, likelihood_label, over, p_data_label]: + mob.scale(1.5) + mob.set_opacity(0.1) + + eq.move_to(prior_label, LEFT) + over.set_width( + prior_label.get_width() + + likelihood_label.get_width() + + MED_SMALL_BUFF + ) + over.next_to(eq, RIGHT, MED_SMALL_BUFF) + p_data_label.next_to(over, DOWN, MED_SMALL_BUFF) + likelihood_label.next_to(over, UP, MED_SMALL_BUFF, RIGHT) + + self.play( + p_label.restore, + p_label.next_to, eq, LEFT, MED_SMALL_BUFF, + prior_label.next_to, over, UP, MED_SMALL_BUFF, LEFT, + FadeIn(eq), + FadeIn(likelihood_label), + FadeIn(over), + FadeIn(p_data_label), + FadeOut(uniform_label), + ) + + # Show new distribution + post_bars = bars.copy() + total_prob = 0 + for bar, p in zip(post_bars, np.arange(0, 1, 0.01)): + prob = scipy.stats.binom(50, p).pmf(48) + bar.stretch(prob, 1, about_edge=DOWN) + total_prob += 0.01 * prob + post_bars.stretch(1 / total_prob, 1, about_edge=DOWN) + post_bars.stretch(0.25, 1, about_edge=DOWN) # Lie to fit on screen... + post_bars.set_color(MAROON_D) + post_bars.set_fill(opacity=0.8) + + brace = Brace(p_label, DOWN) + post_word = brace.get_text("Posterior") + post_word.scale(1.25, about_edge=UP) + post_word.set_color(MAROON_D) + + self.play( + ReplacementTransform( + bars.copy().set_opacity(0), + post_bars, + ), + GrowFromCenter(brace), + FadeIn(post_word, 0.25 * UP) + ) + self.wait() + self.play( + eq.set_opacity, 1, + likelihood_label.set_opacity, 1, + ) + self.wait() + + data = get_check_count_label(48, 2) + data.scale(1.5) + data.next_to(likelihood_label, DOWN, buff=2, aligned_edge=LEFT) + data_arrow = Arrow( + likelihood_label[1].get_bottom(), + data.get_top() + ) + data_arrow.set_color(GREEN) + + self.play( + GrowArrow(data_arrow), + GrowFromPoint(data, data_arrow.get_start()), + ) + self.wait() + self.play(FadeOut(data_arrow)) + self.play( + over.set_opacity, 1, + p_data_label.set_opacity, 1, + ) + self.wait() + + self.play( + FadeOut(brace), + FadeOut(post_word), + FadeOut(post_bars), + FadeOut(data), + p_label.set_opacity, 0.1, + eq.set_opacity, 0.1, + likelihood_label.set_opacity, 0.1, + over.set_opacity, 0.1, + p_data_label.set_opacity, 0.1, + ) + + self.bayes = VGroup( + p_label, eq, + prior_label, likelihood_label, + over, p_data_label + ) + self.data = data + + def parallel_universes(self): + bars = self.bars + + cols = VGroup() + squares = VGroup() + sample_colors = color_gradient( + [GREEN_C, GREEN_D, GREEN_E], + 100 + ) + for bar in bars: + n_rows = 12 + col = VGroup() + for x in range(n_rows): + square = Rectangle( + width=bar.get_width(), + height=bar.get_height() / n_rows, + ) + square.set_stroke(width=0) + square.set_fill(opacity=1) + square.set_color(random.choice(sample_colors)) + col.add(square) + squares.add(square) + col.arrange(DOWN, buff=0) + col.move_to(bar) + cols.add(col) + squares.shuffle() + + self.play( + LaggedStartMap( + VFadeInThenOut, squares, + lag_ratio=0.005, + run_time=3 + ) + ) + self.remove(squares) + squares.set_opacity(1) + self.wait() + + example_col = cols[95] + + self.play( + bars.set_opacity, 0.25, + FadeIn(example_col, lag_ratio=0.1), + ) + self.wait() + + dist = scipy.stats.binom(50, 0.95) + for x in range(12): + square = random.choice(example_col).copy() + square.set_fill(opacity=0) + square.set_stroke(YELLOW, 2) + self.add(square) + nc = dist.ppf(random.random()) + data = get_check_count_label(nc, 50 - nc) + data.next_to(example_col, UP) + + self.add(square, data) + self.wait(0.5) + self.remove(square, data) + self.wait() + + self.data.set_opacity(1) + self.play( + FadeIn(self.data), + FadeOut(example_col), + self.bayes[3].set_opacity, 1, + ) + self.wait() + + def update_from_data(self): + bars = self.bars + data = self.data + bayes = self.bayes + + new_bars = bars.copy() + new_bars.set_stroke(opacity=1) + new_bars.set_fill(opacity=0.8) + for bar, p in zip(new_bars, np.arange(0, 1, 0.01)): + dist = scipy.stats.binom(50, p) + scalar = dist.pmf(48) + bar.stretch(scalar, 1, about_edge=DOWN) + + self.play( + ReplacementTransform( + bars.copy().set_opacity(0), + new_bars + ), + bars.set_fill, {"opacity": 0.1}, + bars.set_stroke, {"opacity": 0.1}, + run_time=2, + ) + + # Show example bar + bar95 = VGroup( + bars[95].copy(), + new_bars[95].copy() + ) + bar95.save_state() + bar95.generate_target() + bar95.target.scale(2) + bar95.target.next_to(bar95, UP, LARGE_BUFF) + bar95.target.set_stroke(BLUE, 3) + + ex_label = TexMobject("s", "=", "0.95") + ex_label.set_color(YELLOW) + ex_label.next_to(bar95.target, DOWN, submobject_to_align=ex_label[-1]) + + highlight = SurroundingRectangle(bar95, buff=0) + highlight.set_stroke(YELLOW, 2) + + self.play(FadeIn(highlight)) + self.play( + MoveToTarget(bar95), + FadeInFromDown(ex_label), + data.shift, LEFT, + ) + self.wait() + + side_brace = Brace(bar95[1], RIGHT, buff=SMALL_BUFF) + side_label = side_brace.get_text("0.26", buff=SMALL_BUFF) + self.play( + GrowFromCenter(side_brace), + FadeIn(side_label) + ) + self.wait() + self.play( + FadeOut(side_brace), + FadeOut(side_label), + FadeOut(ex_label), + ) + self.play( + bar95.restore, + bar95.set_opacity, 0, + ) + + for bar in bars[94:80:-1]: + highlight.move_to(bar) + self.wait(0.5) + self.play(FadeOut(highlight)) + self.wait() + + # Emphasize formula terms + tops = VGroup() + for bar, new_bar in zip(bars, new_bars): + top = Line(bar.get_corner(UL), bar.get_corner(UR)) + top.set_stroke(YELLOW, 2) + top.generate_target() + top.target.move_to(new_bar, UP) + tops.add(top) + + rect = SurroundingRectangle(bayes[2]) + rect.set_stroke(YELLOW, 1) + rect.target = SurroundingRectangle(bayes[3]) + rect.target.match_style(rect) + self.play( + ShowCreation(rect), + ShowCreation(tops), + ) + self.wait() + self.play( + LaggedStartMap( + MoveToTarget, tops, + run_time=2, + lag_ratio=0.02, + ), + MoveToTarget(rect), + ) + self.play(FadeOut(tops)) + self.wait() + + # Show alternate priors + axes = self.axes + bar_groups = VGroup() + for bar, new_bar in zip(bars, new_bars): + bar_groups.add(VGroup(bar, new_bar)) + + bar_groups.save_state() + for a, b in [(5, 2), (7, 1)]: + dist = scipy.stats.beta(a, b) + for bar, saved in zip(bar_groups, bar_groups.saved_state): + bar.target = saved.copy() + height = get_norm(axes.c2p(0.1 * dist.pdf(bar[0].x)) - axes.c2p(0, 0)) + height = max(height, 1e-6) + bar.target.set_height(height, about_edge=DOWN, stretch=True) + + self.play(LaggedStartMap(MoveToTarget, bar_groups, lag_ratio=0)) + self.wait() + self.play(Restore(bar_groups)) + self.wait() + + # Rescale + ex_p_label = TexMobject( + "P(s = 0.95 | 00000000) = ", + tex_to_color_map={ + "s = 0.95": YELLOW, + "00000000": WHITE, + } + ) + ex_p_label.scale(1.5) + ex_p_label.next_to(bars, UP, LARGE_BUFF) + ex_p_label.align_to(bayes, LEFT) + template = ex_p_label.get_part_by_tex("00000000") + template.set_opacity(0) + + highlight = SurroundingRectangle(new_bars[95], buff=0) + highlight.set_stroke(YELLOW, 1) + + self.remove(data) + self.play( + FadeIn(ex_p_label), + VFadeOut(data[0]), + data[1:].move_to, template, + FadeIn(highlight) + ) + self.wait() + + numer = new_bars[95].copy() + numer.set_stroke(YELLOW, 1) + denom = new_bars[80:].copy() + h_line = Line(LEFT, RIGHT) + h_line.set_width(3) + h_line.set_stroke(width=2) + h_line.next_to(ex_p_label, RIGHT) + + self.play( + numer.next_to, h_line, UP, + denom.next_to, h_line, DOWN, + ShowCreation(h_line), + ) + self.wait() + self.play( + denom.space_out_submobjects, + rate_func=there_and_back + ) + self.play( + bayes[4].set_opacity, 1, + bayes[5].set_opacity, 1, + FadeOut(rect), + ) + self.wait() + + # Rescale + self.play( + FadeOut(highlight), + FadeOut(ex_p_label), + FadeOut(data), + FadeOut(h_line), + FadeOut(numer), + FadeOut(denom), + bayes.set_opacity, 1, + ) + + new_bars.unlock_shader_data() + self.remove(new_bars, *new_bars) + self.play( + new_bars.set_height, 5, {"about_edge": DOWN, "stretch": True}, + new_bars.set_color, MAROON_D, + ) + self.wait() + + +class UniverseOf95Percent(WhatsTheModel): + CONFIG = {"s": 0.95} + + def construct(self): + self.introduce_buyer_and_seller() + for m, v in [(self.seller, RIGHT), (self.buyer, LEFT)]: + m.shift(v) + m.label.shift(v) + + pis = VGroup(self.seller, self.buyer) + label = get_prob_positive_experience_label(True, True) + label[-1].set_value(self.s) + label.set_height(1) + label.next_to(pis, UP, LARGE_BUFF) + self.add(label) + + for x in range(4): + self.play(*self.experience_animations( + self.seller, self.buyer, arc=30 * DEGREES, p=self.s + )) + + self.embed() + + +class UniverseOf50Percent(UniverseOf95Percent): + CONFIG = {"s": 0.5} + + +class OpenAndCloseAsideOnPdfs(Scene): + def construct(self): + labels = VGroup( + TextMobject("$\\langle$", "Aside on", " pdfs", "$\\rangle$"), + TextMobject("$\\langle$/", "Aside on", " pdfs", "$\\rangle$"), + ) + labels.set_width(FRAME_WIDTH / 2) + for label in labels: + label.set_color_by_tex("pdfs", YELLOW) + + self.play(FadeInFromDown(labels[0])) + self.wait() + self.play(Transform(*labels)) + self.wait() + + +class BayesRuleWithPdf(ShowLimitToPdf): + def construct(self): + # Axes + axes = self.get_axes() + sf = 1.5 + axes.y_axis.stretch(sf, 1, about_point=axes.c2p(0, 0)) + for number in axes.y_axis.numbers: + number.stretch(1 / sf, 1) + self.add(axes) + + # Formula + bayes = self.get_formula() + + post = bayes[:5] + eq = bayes[5] + prior = bayes[6:9] + likelihood = bayes[9:14] + over = bayes[14] + p_data = bayes[15:] + + self.play(FadeInFromDown(bayes)) + self.wait() + + # Prior + prior_graph = get_beta_graph(axes, 0, 0) + prior_graph_top = Line( + prior_graph.get_corner(UL), + prior_graph.get_corner(UR), + ) + prior_graph_top.set_stroke(YELLOW, 3) + + bayes.save_state() + bayes.set_opacity(0.2) + prior.set_opacity(1) + + self.play( + Restore(bayes, rate_func=reverse_smooth), + FadeIn(prior_graph), + ShowCreation(prior_graph_top), + ) + self.play(FadeOut(prior_graph_top)) + self.wait() + + # Scale Down + nh = 1 + nt = 2 + + scaled_graph = axes.get_graph( + lambda x: scipy.stats.binom(3, x).pmf(1) + 1e-6 + ) + scaled_graph.set_stroke(GREEN) + scaled_region = get_region_under_curve(axes, scaled_graph, 0, 1) + + def to_uniform(p, axes=axes): + return axes.c2p( + axes.x_axis.p2n(p), + int(axes.y_axis.p2n(p) != 0), + ) + + scaled_region.set_fill(opacity=0.75) + scaled_region.save_state() + scaled_region.apply_function(to_uniform) + + self.play( + Restore(scaled_region), + UpdateFromAlphaFunc( + scaled_region, + lambda m, a: m.set_opacity(a * 0.75), + ), + likelihood.set_opacity, 1, + ) + self.wait() + + # Rescale + new_graph = get_beta_graph(axes, nh, nt) + self.play( + ApplyMethod( + scaled_region.set_height, new_graph.get_height(), + {"about_edge": DOWN, "stretch": True}, + run_time=2, + ), + over.set_opacity, 1, + p_data.set_opacity, 1, + ) + self.wait() + self.play( + post.set_opacity, 1, + eq.set_opacity, 1, + ) + self.wait() + + # Use lower case + new_bayes = self.get_formula(lowercase=True) + new_bayes.replace(bayes, dim_to_match=0) + rects = VGroup( + SurroundingRectangle(new_bayes[0][0]), + SurroundingRectangle(new_bayes[6][0]), + ) + rects.set_stroke(YELLOW, 3) + + self.remove(bayes) + bayes = self.get_formula() + bayes.unlock_triangulation() + self.add(bayes) + self.play(Transform(bayes, new_bayes)) + self.play(ShowCreationThenFadeOut(rects)) + + def get_formula(self, lowercase=False): + p_sym = "p" if lowercase else "P" + bayes = TexMobject( + p_sym + "({s} \\,|\\, \\text{data})", "=", + "{" + p_sym + "({s})", + "P(\\text{data} \\,|\\, {s})", + "\\over", + "P(\\text{data})", + tex_to_color_map={ + "{s}": YELLOW, + "\\text{data}": GREEN, + } + ) + bayes.set_height(1.5) + bayes.to_edge(UP) + return bayes + + +class TalkThroughCoinExample(ShowBayesianUpdating): + def construct(self): + # Setup + axes = self.get_axes() + x_label = TexMobject("x") + x_label.next_to(axes.x_axis.get_end(), UR, MED_SMALL_BUFF) + axes.add(x_label) + + p_label, prob, prob_box = self.get_probability_label() + prob_box_x = x_label.copy().move_to(prob_box) + + self.add(axes) + self.add(p_label) + self.add(prob_box) + + self.wait() + q_marks = prob_box[1] + prob_box.remove(q_marks) + self.play( + FadeOut(q_marks), + TransformFromCopy(x_label, prob_box_x) + ) + prob_box.add(prob_box_x) + + # Setup coins + bool_values = (np.random.random(100) < self.true_p) + bool_values[:5] = [True, False, True, True, False] + coins = self.get_coins(bool_values) + coins.next_to(axes.y_axis, RIGHT, MED_LARGE_BUFF) + coins.to_edge(UP) + + # Random coin + rows = VGroup() + for x in range(5): + row = self.get_coins(np.random.random(10) < self.true_p) + row.arrange(RIGHT, buff=MED_LARGE_BUFF) + row.set_width(6) + row.move_to(UP) + rows.add(row) + + last_row = VMobject() + for row in rows: + self.play( + FadeOut(last_row, DOWN), + FadeIn(row, lag_ratio=0.1) + ) + last_row = row + self.play(FadeOut(last_row, DOWN)) + + # Uniform pdf + region = get_beta_graph(axes, 0, 0) + graph = Line( + region.get_corner(UL), + region.get_corner(UR), + ) + func_label = TexMobject("f(x) =", "1") + func_label.next_to(graph, UP) + + self.play( + FadeIn(func_label, lag_ratio=0.1), + ShowCreation(graph), + ) + self.add(region, graph) + self.play(FadeIn(region)) + self.wait() + + # First flip + coin = coins[0] + arrow = Vector(0.5 * UP) + arrow.next_to(coin, DOWN, SMALL_BUFF) + data_label = TextMobject("New data") + data_label.set_height(0.25) + data_label.next_to(arrow, DOWN) + data_label.shift(0.5 * RIGHT) + + self.play( + FadeIn(coin, DOWN), + GrowArrow(arrow), + Write(data_label, run_time=1) + ) + self.wait() + + # Show Bayes rule + bayes = TexMobject( + "p({x} | \\text{data})", "=", + "p({x})", + "{P(\\text{data} | {x})", + "\\over", + "P(\\text{data})", + tex_to_color_map={ + "{x}": WHITE, + "\\text{data}": GREEN, + } + ) + bayes.next_to(func_label, UP, LARGE_BUFF, LEFT) + + likelihood = bayes[9:14] + p_data = bayes[15:] + likelihood_rect = SurroundingRectangle(likelihood, buff=0.05) + likelihood_rect.save_state() + p_data_rect = SurroundingRectangle(p_data, buff=0.05) + + likelihood_x_label = TexMobject("x") + likelihood_x_label.next_to(likelihood_rect, UP) + + self.play(FadeInFromDown(bayes)) + self.wait() + self.play(ShowCreation(likelihood_rect)) + self.wait() + + self.play(TransformFromCopy(likelihood[-2], likelihood_x_label)) + self.wait() + + # Scale by x + times_x = TexMobject("\\cdot \\, x") + times_x.next_to(func_label, RIGHT, buff=0.2) + + new_graph = axes.get_graph(lambda x: x) + sub_region = get_region_under_curve(axes, new_graph, 0, 1) + + self.play( + Write(times_x), + Transform(graph, new_graph), + ) + self.play( + region.set_opacity, 0.5, + FadeIn(sub_region), + ) + self.wait() + + # Show example scalings + low_x = 0.1 + high_x = 0.9 + lines = VGroup() + for x in [low_x, high_x]: + lines.add(Line(axes.c2p(x, 0), axes.c2p(x, 1))) + + lines.set_stroke(YELLOW, 3) + + for x, line in zip([low_x, high_x], lines): + self.play(FadeIn(line)) + self.play(line.scale, x, {"about_edge": DOWN}) + self.wait() + self.play(FadeOut(lines)) + + # Renormalize + self.play( + FadeOut(likelihood_x_label), + ReplacementTransform(likelihood_rect, p_data_rect), + ) + self.wait() + + one = func_label[1] + two = TexMobject("2") + two.move_to(one, LEFT) + + self.play( + FadeOut(region), + sub_region.stretch, 2, 1, {"about_edge": DOWN}, + sub_region.set_color, BLUE, + graph.stretch, 2, 1, {"about_edge": DOWN}, + FadeInFromDown(two), + FadeOut(one, UP), + ) + region = sub_region + func_label = VGroup(func_label[0], two, times_x) + self.add(func_label) + + self.play(func_label.shift, 0.5 * UP) + self.wait() + + const = TexMobject("C") + const.scale(0.9) + const.move_to(two, DR) + const.shift(0.07 * RIGHT) + self.play( + FadeOut(two, UP), + FadeIn(const, DOWN) + ) + self.remove(func_label) + func_label = VGroup(func_label[0], const, times_x) + self.add(func_label) + self.play(FadeOut(p_data_rect)) + self.wait() + + # Show tails + coin = coins[1] + self.play( + arrow.next_to, coin, DOWN, SMALL_BUFF, + MaintainPositionRelativeTo(data_label, arrow), + FadeInFromDown(coin), + ) + self.wait() + + to_prior_arrow = Arrow( + func_label[0][3], + bayes[6], + max_tip_length_to_length_ratio=0.15, + stroke_width=3, + ) + to_prior_arrow.set_color(RED) + + self.play(Indicate(func_label, scale_factor=1.2, color=RED)) + self.play(ShowCreation(to_prior_arrow)) + self.wait() + self.play(FadeOut(to_prior_arrow)) + + # Scale by (1 - x) + eq_1mx = TexMobject("(1 - x)") + dot = TexMobject("\\cdot") + rhs_part = VGroup(dot, eq_1mx) + rhs_part.arrange(RIGHT, buff=0.2) + rhs_part.move_to(func_label, RIGHT) + + l_1mx = eq_1mx.copy() + likelihood_rect.restore() + l_1mx.next_to(likelihood_rect, UP, SMALL_BUFF) + + self.play( + ShowCreation(likelihood_rect), + FadeIn(l_1mx, 0.5 * DOWN), + ) + self.wait() + self.play(ShowCreationThenFadeOut(Underline(p_label))) + self.play(Indicate(coins[1])) + self.wait() + self.play( + TransformFromCopy(l_1mx, eq_1mx), + FadeIn(dot, RIGHT), + func_label.next_to, dot, LEFT, 0.2, + ) + + scaled_graph = axes.get_graph(lambda x: 2 * x * (1 - x)) + scaled_region = get_region_under_curve(axes, scaled_graph, 0, 1) + + self.play(Transform(graph, scaled_graph)) + self.play(FadeIn(scaled_region)) + self.wait() + + # Renormalize + self.remove(likelihood_rect) + self.play( + TransformFromCopy(likelihood_rect, p_data_rect), + FadeOut(l_1mx) + ) + new_graph = get_beta_graph(axes, 1, 1) + group = VGroup(graph, scaled_region) + self.play( + group.set_height, + new_graph.get_height(), {"about_edge": DOWN, "stretch": True}, + group.set_color, BLUE, + FadeOut(region), + ) + region = scaled_region + self.play(FadeOut(p_data_rect)) + self.wait() + self.play(ShowCreationThenFadeAround(const)) + + # Repeat + exp1 = Integer(1) + exp1.set_height(0.2) + exp1.move_to(func_label[2].get_corner(UR), DL) + exp1.shift(0.02 * DOWN + 0.07 * RIGHT) + + exp2 = exp1.copy() + exp2.move_to(eq_1mx.get_corner(UR), DL) + exp2.shift(0.1 * RIGHT) + exp2.align_to(exp1, DOWN) + + shift_vect = UP + 0.5 * LEFT + VGroup(exp1, exp2).shift(shift_vect) + + self.play( + FadeIn(exp1, DOWN), + FadeIn(exp2, DOWN), + VGroup(func_label, dot, eq_1mx).shift, shift_vect, + bayes.scale, 0.5, + bayes.next_to, p_label, DOWN, LARGE_BUFF, {"aligned_edge": RIGHT}, + ) + nh = 1 + nt = 1 + for coin, is_heads in zip(coins[2:10], bool_values[2:10]): + self.play( + arrow.next_to, coin, DOWN, SMALL_BUFF, + MaintainPositionRelativeTo(data_label, arrow), + FadeIn(coin, DOWN), + ) + if is_heads: + nh += 1 + old_exp = exp1 + else: + nt += 1 + old_exp = exp2 + + new_exp = old_exp.copy() + new_exp.increment_value(1) + + dist = scipy.stats.beta(nh + 1, nt + 1) + new_graph = axes.get_graph(dist.pdf) + new_region = get_region_under_curve(axes, new_graph, 0, 1) + new_region.match_style(region) + + self.play( + FadeOut(graph), + FadeOut(region), + FadeIn(new_graph), + FadeIn(new_region), + FadeOut(old_exp, MED_SMALL_BUFF * UP), + FadeIn(new_exp, MED_SMALL_BUFF * DOWN), + ) + graph = new_graph + region = new_region + self.remove(new_exp) + self.add(old_exp) + old_exp.increment_value() + self.wait() + + if coin is coins[4]: + area_label = TextMobject("Area = 1") + area_label.move_to(axes.c2p(0.6, 0.8)) + self.play(GrowFromPoint( + area_label, const.get_center() + )) + + +class PDefectEqualsQmark(Scene): + def construct(self): + label = TexMobject( + "P(\\text{Defect}) = ???", + tex_to_color_map={ + "\\text{Defect}": RED, + } + ) + self.play(FadeIn(label, DOWN)) + self.wait() + + +class UpdateOnceWithBinomial(TalkThroughCoinExample): + def construct(self): + # Fair bit of copy-pasting from above. If there's + # time, refactor this properly + # Setup + axes = self.get_axes() + x_label = TexMobject("x") + x_label.next_to(axes.x_axis.get_end(), UR, MED_SMALL_BUFF) + axes.add(x_label) + + p_label, prob, prob_box = self.get_probability_label() + prob_box_x = x_label.copy().move_to(prob_box) + + q_marks = prob_box[1] + prob_box.remove(q_marks) + prob_box.add(prob_box_x) + + self.add(axes) + self.add(p_label) + self.add(prob_box) + + # Coins + bool_values = (np.random.random(100) < self.true_p) + bool_values[:5] = [True, False, True, True, False] + coins = self.get_coins(bool_values) + coins.next_to(axes.y_axis, RIGHT, MED_LARGE_BUFF) + coins.to_edge(UP) + self.add(coins[:10]) + + # Uniform pdf + region = get_beta_graph(axes, 0, 0) + graph = axes.get_graph( + lambda x: 1, + min_samples=30, + ) + self.add(region, graph) + + # Show Bayes rule + bayes = TexMobject( + "p({x} | \\text{data})", "=", + "p({x})", + "{P(\\text{data} | {x})", + "\\over", + "P(\\text{data})", + tex_to_color_map={ + "{x}": WHITE, + "\\text{data}": GREEN, + } + ) + bayes.move_to(axes.c2p(0, 2.5)) + bayes.align_to(coins, LEFT) + + likelihood = bayes[9:14] + # likelihood_rect = SurroundingRectangle(likelihood, buff=0.05) + + self.add(bayes) + + # All data at once + brace = Brace(coins[:10], DOWN) + all_data_label = brace.get_text("One update from all data") + + self.wait() + self.play( + GrowFromCenter(brace), + FadeIn(all_data_label, 0.2 * UP), + ) + self.wait() + + # Binomial formula + nh = sum(bool_values[:10]) + nt = sum(~bool_values[:10]) + + likelihood_brace = Brace(likelihood, UP) + t2c = { + str(nh): BLUE, + str(nt): RED, + } + binom_formula = TexMobject( + "{10 \\choose ", str(nh), "}", + "x^{", str(nh), "}", + "(1-x)^{" + str(nt) + "}", + tex_to_color_map=t2c, + ) + binom_formula[0][-1].set_color(BLUE) + binom_formula[1].set_color(WHITE) + binom_formula.set_width(likelihood_brace.get_width() + 0.5) + binom_formula.next_to(likelihood_brace, UP) + + self.play( + TransformFromCopy(brace, likelihood_brace), + FadeOut(all_data_label), + FadeIn(binom_formula) + ) + self.wait() + + # New plot + rhs = TexMobject( + "C \\cdot", + "x^{", str(nh), "}", + "(1-x)^{", str(nt), "}", + tex_to_color_map=t2c + ) + rhs.next_to(bayes[:5], DOWN, LARGE_BUFF, aligned_edge=LEFT) + eq = TexMobject("=") + eq.rotate(90 * DEGREES) + eq.next_to(bayes[:5], DOWN, buff=0.35) + + dist = scipy.stats.beta(nh + 1, nt + 1) + new_graph = axes.get_graph(dist.pdf) + new_graph.shift(1e-6 * UP) + new_graph.set_stroke(WHITE, 1, opacity=0.5) + new_region = get_region_under_curve(axes, new_graph, 0, 1) + new_region.match_style(region) + new_region.set_opacity(0.75) + + self.add(new_region, new_graph, bayes) + region.unlock_triangulation() + self.play( + FadeOut(graph), + FadeOut(region), + FadeIn(new_graph), + FadeIn(new_region), + run_time=1, + ) + self.play( + Write(eq), + FadeIn(rhs, UP) + ) + self.wait() diff --git a/from_3b1b/active/bayes/beta_helpers.py b/from_3b1b/active/bayes/beta_helpers.py new file mode 100644 index 0000000000..26f9ecee64 --- /dev/null +++ b/from_3b1b/active/bayes/beta_helpers.py @@ -0,0 +1,635 @@ +from manimlib.imports import * +import scipy.stats + + +CMARK_TEX = "\\text{\\ding{51}}" +XMARK_TEX = "\\text{\\ding{55}}" + +COIN_COLOR_MAP = { + "H": BLUE_E, + "T": RED_E, +} + + +class Histogram(Group): + CONFIG = { + "height": 5, + "width": 10, + "y_max": 1, + "y_axis_numbers_to_show": range(20, 120, 20), + "y_axis_label_height": 0.25, + "y_tick_freq": 0.2, + "x_label_freq": 1, + "include_h_lines": True, + "h_line_style": { + "stroke_width": 1, + "stroke_color": LIGHT_GREY, + # "draw_stroke_behind_fill": True, + }, + "bar_style": { + "stroke_width": 1, + "stroke_color": WHITE, + "fill_opacity": 1, + }, + "bar_colors": [BLUE, GREEN] + } + + def __init__(self, data, **kwargs): + super().__init__(**kwargs) + self.data = data + + self.add_axes() + if self.include_h_lines: + self.add_h_lines() + self.add_bars(data) + self.add_x_axis_labels() + self.add_y_axis_labels() + + def add_axes(self): + n_bars = len(self.data) + axes_config = { + "x_min": 0, + "x_max": n_bars, + "x_axis_config": { + "unit_size": self.width / n_bars, + "include_tip": False, + }, + "y_min": 0, + "y_max": self.y_max, + "y_axis_config": { + "unit_size": self.height / self.y_max, + "include_tip": False, + "tick_frequency": self.y_tick_freq, + }, + } + axes = Axes(**axes_config) + axes.center() + self.axes = axes + self.add(axes) + + def add_h_lines(self): + axes = self.axes + axes.h_lines = VGroup() + for tick in axes.y_axis.tick_marks: + line = Line(**self.h_line_style) + line.match_width(axes.x_axis) + line.move_to(tick.get_center(), LEFT) + axes.h_lines.add(line) + axes.add(axes.h_lines) + + def add_bars(self, data): + self.bars = self.get_bars(data) + self.add(self.bars) + + def add_x_axis_labels(self): + axes = self.axes + axes.x_labels = VGroup() + for x, bar in list(enumerate(self.bars))[::self.x_label_freq]: + label = Integer(x) + label.set_height(0.25) + label.next_to(bar, DOWN) + axes.x_labels.add(label) + axes.add(axes.x_labels) + + def add_y_axis_labels(self): + axes = self.axes + labels = VGroup() + for value in self.y_axis_numbers_to_show: + label = Integer(value, unit="\\%") + fix_percent(label[-1][0]) + label.set_height(self.y_axis_label_height) + label.next_to(axes.y_axis.n2p(0.01 * value), LEFT) + labels.add(label) + axes.y_labels = labels + axes.y_axis.add(labels) + + # Bar manipulations + def get_bars(self, data): + portions = np.array(data).astype(float) + total = portions.sum() + if total == 0: + portions[:] = 0 + else: + portions /= total + bars = VGroup() + for x, prop in enumerate(portions): + bar = Rectangle() + width = get_norm(self.axes.c2p(1, 0) - self.axes.c2p(0, 0)) + height = get_norm(self.axes.c2p(0, 1) - self.axes.c2p(0, 0)) + bar.set_width(width) + bar.set_height(height * prop, stretch=True) + bar.move_to(self.axes.c2p(x, 0), DL) + bars.add(bar) + + bars.set_submobject_colors_by_gradient(*self.bar_colors) + bars.set_style(**self.bar_style) + return bars + + +# Images of randomness + +def fix_percent(sym): + # Really need to make this unneeded... + new_sym = sym.copy() + path_lengths = [len(path) for path in sym.get_subpaths()] + n = sum(path_lengths[:2]) + p1 = sym.points[:n] + p2 = sym.points[n:] + sym.points = p1 + new_sym.points = p2 + sym.add(new_sym) + sym.lock_triangulation() + + +def get_random_process(choices, shuffle_time=2, total_time=3, change_rate=0.05, + h_buff=0.1, v_buff=0.1): + content = choices[0] + + container = Square() + container.set_opacity(0) + container.set_width(content.get_width() + 2 * h_buff, stretch=True) + container.set_height(content.get_height() + 2 * v_buff, stretch=True) + container.move_to(content) + container.add(content) + container.time = 0 + container.last_change_time = 0 + + def update(container, dt): + container.time += dt + + t = container.time + change = all([ + (t % total_time) < shuffle_time, + container.time - container.last_change_time > change_rate + ]) + if change: + mob = container.submobjects[0] + new_mob = random.choice(choices) + new_mob.match_height(mob) + new_mob.move_to(container, DL) + new_mob.shift(2 * np.random.random() * h_buff * RIGHT) + new_mob.shift(2 * np.random.random() * v_buff * UP) + container.set_submobjects([new_mob]) + container.last_change_time = container.time + + container.add_updater(update) + return container + + +def get_die_faces(): + dot = Dot() + dot.set_width(0.15) + dot.set_color(BLUE_B) + + square = Square() + square.round_corners(0.25) + square.set_stroke(WHITE, 2) + square.set_fill(DARKER_GREY, 1) + square.set_width(0.6) + + edge_groups = [ + (ORIGIN,), + (UL, DR), + (UL, ORIGIN, DR), + (UL, UR, DL, DR), + (UL, UR, ORIGIN, DL, DR), + (UL, UR, LEFT, RIGHT, DL, DR), + ] + + arrangements = VGroup(*[ + VGroup(*[ + dot.copy().move_to(square.get_bounding_box_point(ec)) + for ec in edge_group + ]) + for edge_group in edge_groups + ]) + square.set_width(1) + + faces = VGroup(*[ + VGroup(square.copy(), arrangement) + for arrangement in arrangements + ]) + faces.arrange(RIGHT) + + return faces + + +def get_random_die(**kwargs): + return get_random_process(get_die_faces(), **kwargs) + + +def get_random_card(height=1, **kwargs): + cards = DeckOfCards() + cards.set_height(height) + return get_random_process(cards, **kwargs) + + +# Coins +def get_coin(symbol, color=None): + if color is None: + color = COIN_COLOR_MAP.get(symbol, GREY_E) + coin = VGroup() + circ = Circle() + circ.set_fill(color, 1) + circ.set_stroke(WHITE, 1) + circ.set_height(1) + label = TextMobject(symbol) + label.set_height(0.5 * circ.get_height()) + label.move_to(circ) + coin.add(circ, label) + coin.symbol = symbol + coin.lock_triangulation() + return coin + + +def get_random_coin(**kwargs): + return get_random_process([get_coin("H"), get_coin("T")], **kwargs) + + +def get_prob_coin_label(symbol="H", color=None, p=0.5, num_decimal_places=2): + label = TexMobject("P", "(", "00", ")", "=",) + coin = get_coin(symbol, color) + template = label.get_part_by_tex("00") + coin.replace(template) + label.replace_submobject(label.index_of_part(template), coin) + rhs = DecimalNumber(p, num_decimal_places=num_decimal_places) + rhs.next_to(label, RIGHT, buff=MED_SMALL_BUFF) + label.add(rhs) + return label + + +def get_q_box(mob): + box = SurroundingRectangle(mob) + box.set_stroke(WHITE, 1) + box.set_fill(GREY_E, 1) + q_marks = TexMobject("???") + max_width = 0.8 * box.get_width() + max_height = 0.8 * box.get_height() + + if q_marks.get_width() > max_width: + q_marks.set_width(max_width) + + if q_marks.get_height() > max_height: + q_marks.set_height(max_height) + + q_marks.move_to(box) + box.add(q_marks) + return box + + +def get_coin_grid(bools, height=6): + coins = VGroup(*[ + get_coin("H" if heads else "T") + for heads in bools + ]) + coins.arrange_in_grid() + coins.set_height(height) + return coins + + +def get_prob_positive_experience_label(include_equals=False, + include_decimal=False, + include_q_mark=False): + label = TexMobject( + "P", "(", "00000", ")", + ) + + pe = TextMobject("Positive\\\\experience") + pe.set_color(GREEN) + pe.replace(label[2], dim_to_match=0) + label.replace_submobject(2, pe) + VGroup(label[1], label[3]).match_height( + pe, stretch=True, about_edge=DOWN, + ) + if include_equals: + eq = TexMobject("=").next_to(label, RIGHT) + label.add(eq) + if include_decimal: + decimal = DecimalNumber(0.95) + decimal.next_to(label, RIGHT) + decimal.set_color(YELLOW) + label.decimal = decimal + label.add(decimal) + if include_q_mark: + q_mark = TexMobject("?") + q_mark.relative_mob = label[-1] + q_mark.add_updater( + lambda m: m.next_to(m.relative_mob, RIGHT, SMALL_BUFF) + ) + label.add(q_mark) + + return label + + +def get_beta_dist_axes(y_max=20, y_unit=2, label_y=False, **kwargs): + config = { + "x_min": 0, + "x_max": 1, + "x_axis_config": { + "unit_size": 0.1, + "tick_frequency": 0.1, + "include_tip": False, + }, + "y_min": 0, + "y_max": y_max, + "y_axis_config": { + "unit_size": 1, + "tick_frequency": y_unit, + "include_tip": False, + }, + } + result = Axes(**config) + origin = result.c2p(0, 0) + kw = { + "about_point": origin, + "stretch": True, + } + result.x_axis.set_width(11, **kw) + result.y_axis.set_height(6, **kw) + + x_vals = np.arange(0, 1, 0.2) + 0.2 + result.x_axis.add_numbers( + *x_vals, + number_config={"num_decimal_places": 1} + ) + + if label_y: + result.y_axis.add_numbers( + *np.arange(y_unit, y_max, y_unit) + ) + label = TextMobject("Probability density") + label.scale(0.5) + label.next_to(result.y_axis.get_top(), UR, SMALL_BUFF) + label.next_to(result.y_axis, UP, SMALL_BUFF) + label.align_to(result.y_axis.numbers, LEFT) + result.add(label) + result.y_axis_label = label + + result.to_corner(DR, LARGE_BUFF) + + return result + + +def scaled_pdf_axes(scale_factor=3.5): + axes = get_beta_dist_axes( + label_y=True, + y_unit=1, + ) + axes.y_axis.numbers.set_submobjects([ + *axes.y_axis.numbers[:5], + *axes.y_axis.numbers[4::5] + ]) + sf = scale_factor + axes.y_axis.stretch(sf, 1, about_point=axes.c2p(0, 0)) + for number in axes.y_axis.numbers: + number.stretch(1 / sf, 1) + axes.y_axis_label.to_edge(LEFT) + axes.y_axis_label.add_background_rectangle(opacity=1) + axes.set_stroke(background=True) + return axes + + +def close_off_graph(axes, graph): + x_max = axes.x_axis.p2n(graph.get_end()) + graph.add_line_to(axes.c2p(x_max, 0)) + graph.add_line_to(axes.c2p(0, 0)) + graph.lock_triangulation() + return graph + + +def get_beta_graph(axes, n_plus, n_minus, **kwargs): + dist = scipy.stats.beta(n_plus + 1, n_minus + 1) + graph = axes.get_graph(dist.pdf, **kwargs) + close_off_graph(axes, graph) + graph.set_stroke(BLUE, 2) + graph.set_fill(BLUE_E, 1) + graph.lock_triangulation() + return graph + + +def get_beta_label(n_plus, n_minus, point=ORIGIN): + template = TextMobject("Beta(", "00", ",", "00", ")") + template.scale(1.5) + a_label = Integer(n_plus + 1) + a_label.set_color(GREEN) + b_label = Integer(n_minus + 1) + b_label.set_color(RED) + + for i, label in (1, a_label), (3, b_label): + label.match_height(template[i]) + label.move_to(template[i], DOWN) + template.replace_submobject(i, label) + template.save_state() + template.arrange(RIGHT, buff=0.15) + for t1, t2 in zip(template, template.saved_state): + t1.align_to(t2, DOWN) + + return template + + +def get_plusses_and_minuses(n_rows=15, n_cols=20, p=0.95): + result = VGroup() + for x in range(n_rows * n_cols): + if random.random() < p: + mob = TexMobject(CMARK_TEX) + mob.set_color(GREEN) + mob.is_plus = True + else: + mob = TexMobject(XMARK_TEX) + mob.set_color(RED) + mob.is_plus = False + mob.set_width(1) + result.add(mob) + + result.arrange_in_grid(n_rows, n_cols) + result.set_width(5.5) + return result + + +def get_checks_and_crosses(bools, width=12): + result = VGroup() + for positive in bools: + if positive: + mob = TexMobject(CMARK_TEX) + mob.set_color(GREEN) + else: + mob = TexMobject(XMARK_TEX) + mob.set_color(RED) + mob.positive = positive + mob.set_width(0.5) + result.add(mob) + result.arrange(RIGHT, buff=MED_SMALL_BUFF) + result.set_width(width) + return result + + +def get_underlines(marks): + underlines = VGroup() + for mark in marks: + underlines.add(Underline(mark)) + for line in underlines: + line.align_to(underlines[-1], DOWN) + return underlines + + +def get_random_checks_and_crosses(n=50, s=0.95, width=12): + return get_checks_and_crosses( + bools=(np.random.random(n) < s), + width=width + ) + + +def get_random_num_row(s, n=10): + values = np.random.random(n) + nums = VGroup() + syms = VGroup() + for x, value in enumerate(values): + num = DecimalNumber(value) + num.set_height(0.25) + num.move_to(x * RIGHT) + num.positive = (num.get_value() < s) + if num.positive: + num.set_color(GREEN) + sym = TexMobject(CMARK_TEX) + else: + num.set_color(RED) + sym = TexMobject(XMARK_TEX) + sym.match_color(num) + sym.match_height(num) + sym.positive = num.positive + sym.next_to(num, UP) + + nums.add(num) + syms.add(sym) + + row = VGroup(nums, syms) + row.nums = nums + row.syms = syms + row.n_positive = sum([m.positive for m in nums]) + + row.set_width(10) + row.center().to_edge(UP) + return row + + +def get_prob_review_label(n_positive, n_negative, s=0.95): + label = TexMobject( + "P(", + f"{n_positive}\\,{CMARK_TEX}", ",\\,", + f"{n_negative}\\,{XMARK_TEX}", + "\\,|\\,", + "s = {:.2f}".format(s), + ")", + ) + label.set_color_by_tex_to_color_map({ + CMARK_TEX: GREEN, + XMARK_TEX: RED, + "0.95": YELLOW, + }) + return label + + +def get_binomial_formula(n, k, p): + n_mob = Integer(n, color=WHITE) + k_mob = Integer(k, color=GREEN) + nmk_mob = Integer(n - k, color=RED) + p_mob = DecimalNumber(p, color=YELLOW) + + n_str = "N" * len(n_mob) + k_str = "K" * len(k_mob) + p_str = "P" * len(k_mob) + nmk_str = "M" * len(nmk_mob) + + formula = TexMobject( + "\\left(", + "{" + n_str, + "\\over", + k_str + "}", + "\\right)", + "(", p_str, ")", + "^{" + k_str + "}", + "(1 - ", p_str, ")", + "^{" + nmk_str + "}", + ) + parens = VGroup(formula[0], formula[4]) + parens.space_out_submobjects(0.7) + formula.remove(formula.get_part_by_tex("\\over")) + pairs = ( + (n_mob, n_str), + (k_mob, k_str), + (nmk_mob, nmk_str), + (p_mob, p_str), + ) + for mob, tex in pairs: + parts = formula.get_parts_by_tex(tex) + for part in parts: + mob_copy = mob.copy() + i = formula.index_of_part_by_tex(tex) + mob_copy.match_height(part) + mob_copy.move_to(part, DOWN) + formula.replace_submobject(i, mob_copy) + + terms = VGroup( + formula[:4], + formula[4:7], + formula[7], + formula[8:11], + formula[11], + ) + ys = [term.get_y() for term in terms] + terms.arrange(RIGHT, buff=SMALL_BUFF) + terms[0].shift(SMALL_BUFF * LEFT) + for term, y in zip(terms, ys): + term.set_y(y) + + return formula + + +def get_check_count_label(nc, nx, include_rect=True): + result = VGroup( + Integer(nc), + TexMobject(CMARK_TEX, color=GREEN), + Integer(nx), + TexMobject(XMARK_TEX, color=RED), + ) + result.arrange(RIGHT, buff=SMALL_BUFF) + result[2:].shift(SMALL_BUFF * RIGHT) + + if include_rect: + rect = SurroundingRectangle(result) + rect.set_stroke(WHITE, 1) + rect.set_fill(GREY_E, 1) + result.add_to_back(rect) + + return result + + +def reverse_smooth(t): + return smooth(1 - t) + + +def get_region_under_curve(axes, graph, min_x, max_x): + props = [ + binary_search( + function=lambda a: axes.x_axis.p2n(graph.pfp(a)), + target=x, + lower_bound=axes.x_min, + upper_bound=axes.x_max, + ) + for x in [min_x, max_x] + ] + region = graph.copy() + region.pointwise_become_partial(graph, *props) + region.add_line_to(axes.c2p(max_x, 0)) + region.add_line_to(axes.c2p(min_x, 0)) + region.add_line_to(region.get_start()) + + region.set_stroke(GREEN, 2) + region.set_fill(GREEN, 0.5) + + region.axes = axes + region.graph = graph + region.min_x = min_x + region.max_x = max_x + + return region diff --git a/from_3b1b/active/bayes/footnote.py b/from_3b1b/active/bayes/footnote.py index 82449f2a4c..60223d8114 100644 --- a/from_3b1b/active/bayes/footnote.py +++ b/from_3b1b/active/bayes/footnote.py @@ -116,8 +116,8 @@ def construct(self): self.play( MoveToTarget(full_equation), - FadeOutAndShift(image_group, 2 * LEFT), - FadeOutAndShift(asterisk, 2 * LEFT), + FadeOut(image_group, 2 * LEFT), + FadeOut(asterisk, 2 * LEFT), self.teacher.look_at, 4 * UP, self.get_student_changes( "thinking", "erm", "confused", @@ -933,7 +933,7 @@ def construct(self): ) self.play( FadeIn(real_rect), - FadeInFrom(check, RIGHT), + FadeIn(check, RIGHT), ) self.wait() @@ -1070,7 +1070,7 @@ def construct(self): ) self.play( GrowArrow(positive_arrow), - FadeInFrom(positive_words, UP), + FadeIn(positive_words, UP), ) self.wait(2) self.play( diff --git a/from_3b1b/active/bayes/part1.py b/from_3b1b/active/bayes/part1.py index 1a8c6c062e..4a62b58baa 100644 --- a/from_3b1b/active/bayes/part1.py +++ b/from_3b1b/active/bayes/part1.py @@ -471,9 +471,9 @@ def get_formula_slice(*indices): # self.add(get_submobject_index_labels(formula)) # return self.play( - FadeInFrom(hyp_label, DOWN), + FadeIn(hyp_label, DOWN), GrowArrow(hyp_arrow), - FadeInFrom(evid_label, UP), + FadeIn(evid_label, UP), GrowArrow(evid_arrow), ) self.wait() @@ -560,7 +560,7 @@ def construct(self): self.add(line, now_label) self.add(you) self.play( - FadeInFrom(you_label, LEFT), + FadeIn(you_label, LEFT), GrowArrow(you_arrow), you.change, "pondering", ) @@ -622,7 +622,7 @@ def construct(self): )), ) self.play( - FadeInFrom(gold, LEFT), + FadeIn(gold, LEFT), you.change, "erm", gold, ) self.play(Blink(you)) @@ -643,11 +643,11 @@ def construct(self): words.next_to(ship, RIGHT) self.play( - FadeInFrom(words[0], LEFT), + FadeIn(words[0], LEFT), you.change, "tease", words, FadeOut(icons[:2]), ) - self.play(FadeInFrom(words[1], UP)) + self.play(FadeIn(words[1], UP)) self.wait() self.add(ship, gold) @@ -1053,18 +1053,18 @@ def construct(self): books.to_edge(RIGHT, buff=MED_LARGE_BUFF) self.play( - FadeInFrom(danny, DOWN), - FadeInFrom(danny.name, LEFT), + FadeIn(danny, DOWN), + FadeIn(danny.name, LEFT), ) self.play( - FadeInFrom(amos, UP), - FadeInFrom(amos.name, LEFT), + FadeIn(amos, UP), + FadeIn(amos.name, LEFT), ) self.wait() self.play(FadeInFromLarge(prize)) self.wait() for book in books: - self.play(FadeInFrom(book, LEFT)) + self.play(FadeIn(book, LEFT)) self.wait() # Show them thinking @@ -1124,7 +1124,7 @@ def construct(self): ) self.play( DrawBorderThenFill(bubble), - FadeInFrom( + FadeIn( randy, UR, rate_func=squish_rate_func(smooth, 0.5, 1), run_time=2, @@ -1263,8 +1263,8 @@ def construct(self): description, lambda m: (m, LEFT) ), - FadeOutAndShift(randy, LEFT), - FadeOutAndShift(bar, LEFT), + FadeOut(randy, LEFT), + FadeOut(bar, LEFT), ) @@ -1303,7 +1303,7 @@ def construct(self): word.next_to(arrow, vect) self.play( GrowArrow(arrow), - FadeInFrom(word, UP), + FadeIn(word, UP), ) self.wait() @@ -1472,7 +1472,7 @@ def construct(self): self.add(sa_words) self.wait() - self.play(FadeInFrom(formula_group, UP)) + self.play(FadeIn(formula_group, UP)) self.wait() @@ -1813,7 +1813,7 @@ def construct(self): VGroup(equation_rect, equation).shift, prior_rect.get_height() * DOWN, FadeIn(prior_rect), FadeIn(prior_equation), - FadeInFrom(prior_label, RIGHT), + FadeIn(prior_label, RIGHT), GrowArrow(prior_arrow), ) self.wait() @@ -1845,7 +1845,7 @@ def construct(self): self.add(evid) self.play( GrowArrow(arrow), - FadeInFrom(librarian, LEFT) + FadeIn(librarian, LEFT) ) self.play(ShowCreation(cross)) self.wait() @@ -2030,7 +2030,7 @@ def construct(self): arrow.set_stroke(width=5) self.play( - FadeInFrom(words, DOWN), + FadeIn(words, DOWN), GrowArrow(arrow), FadeOut(prob), title.to_edge, LEFT @@ -2140,9 +2140,9 @@ def construct(self): self.play(FadeInFromDown(all_words[0])) self.play( LaggedStart( - FadeInFrom(hypothesis_icon[0], DOWN), + FadeIn(hypothesis_icon[0], DOWN), Write(hypothesis_icon[1]), - FadeInFrom(hypothesis_icon[2], UP), + FadeIn(hypothesis_icon[2], UP), run_time=1, ) ) @@ -2286,7 +2286,7 @@ def construct(self): ) self.wait() self.play( - FadeInFrom(prior_word, RIGHT), + FadeIn(prior_word, RIGHT), GrowArrow(prior_arrow) ) self.wait() @@ -2339,8 +2339,8 @@ def construct(self): diagram.hne_rect.set_opacity, 1, MoveToTarget(hne_people), GrowFromCenter(diagram.he_brace), - FadeInFrom(like_label, RIGHT), - FadeInFrom(like_example, RIGHT), + FadeIn(like_label, RIGHT), + FadeIn(like_example, RIGHT), ) self.wait() self.play( @@ -2361,7 +2361,7 @@ def construct(self): self.play( diagram.people[10:].set_opacity, 0.2, diagram.nh_rect.set_opacity, 0.2, - FadeInFrom(limit_word, DOWN), + FadeIn(limit_word, DOWN), GrowArrow(limit_arrow), rate_func=there_and_back_with_pause, run_time=6, @@ -2401,8 +2401,8 @@ def construct(self): Restore(diagram.nhe_rect), GrowFromCenter(diagram.nhe_brace), MoveToTarget(nhne_people), - FadeInFrom(anti_label, LEFT), - FadeInFrom(anti_example, LEFT), + FadeIn(anti_label, LEFT), + FadeIn(anti_example, LEFT), ) diagram.nhne_rect.set_opacity(1) self.wait() @@ -2739,7 +2739,7 @@ def construct(self): # Name posterior self.play( GrowArrow(post_arrow), - FadeInFrom(post_word, RIGHT), + FadeIn(post_word, RIGHT), FadeOut(formula_rect), FadeOut(bayes_words), ) @@ -3224,7 +3224,7 @@ def construct(self): # Add people for person in [scientist, programmer]: - self.play(FadeInFrom(person, DOWN)) + self.play(FadeIn(person, DOWN)) rhs_copy = rhs.copy() rhs_copy.add_to_back( SurroundingRectangle( @@ -3286,7 +3286,7 @@ def construct(self): self.play( self.teacher.change, "raise_right_hand", - FadeInFrom(words, DOWN), + FadeIn(words, DOWN), self.get_student_changes("erm", "pondering", "confused") ) self.wait(2) @@ -3328,8 +3328,8 @@ def construct(self): steve_words.next_to(steve, UP, LARGE_BUFF) self.play(LaggedStart( - FadeInFrom(steve, LEFT), - FadeInFrom(steve_words, LEFT), + FadeIn(steve, LEFT), + FadeIn(steve_words, LEFT), )) self.wait() @@ -3342,9 +3342,9 @@ def construct(self): self.play( LaggedStart( - FadeOutAndShift(steve_words, 2 * RIGHT), - FadeOutAndShift(steve, 2 * RIGHT), - FadeInFrom(linda, 2 * LEFT), + FadeOut(steve_words, 2 * RIGHT), + FadeOut(steve, 2 * RIGHT), + FadeIn(linda, 2 * LEFT), lag_ratio=0.15, ) ) @@ -3404,7 +3404,7 @@ def construct(self): MoveToTarget(options), MoveToTarget(rect), VFadeIn(rect), - FadeInFrom(result, LEFT), + FadeIn(result, LEFT), GrowArrow(arrow) ) self.wait() @@ -3642,7 +3642,7 @@ def push_down(phrase): stereotypes.move_to(people) self.play( - FadeInFrom(phrases[0], UP), + FadeIn(phrases[0], UP), randy.change, "pondering", ) self.play( @@ -3652,7 +3652,7 @@ def push_down(phrase): ) self.wait() self.play( - FadeInFrom(phrases[1], UP), + FadeIn(phrases[1], UP), randy.change, "confused", phrases[1], FadeOut(people), ApplyFunction(push_down, phrases[0]), @@ -3661,7 +3661,7 @@ def push_down(phrase): self.play(bar.p_tracker.set_value, 0.4) bar.clear_updaters() self.play( - FadeInFrom(phrases[2], UP), + FadeIn(phrases[2], UP), ApplyFunction(push_down, phrases[:2]), FadeOut(bar.percentages), randy.change, "guilty", @@ -3669,7 +3669,7 @@ def push_down(phrase): self.wait() bar.remove(bar.percentages) self.play( - FadeInFrom(phrases[3], UP), + FadeIn(phrases[3], UP), ApplyFunction(push_down, phrases[:3]), FadeOut(bar), FadeIn(stereotypes), @@ -3861,13 +3861,13 @@ def update_border(border): self.play(FadeInFromDown(prob_word)) self.play( - FadeInFrom(unc_word, LEFT), + FadeIn(unc_word, LEFT), Write(arrows[1]), ) self.add(random_dice) self.wait(9) self.play( - FadeInFrom(prop_word, RIGHT), + FadeIn(prop_word, RIGHT), Write(arrows[0]) ) self.play(FadeIn(diagram)) @@ -3905,7 +3905,7 @@ def get_die_faces(self): arrangements = VGroup(*[ VGroup(*[ - dot.copy().move_to(square.get_critical_point(ec)) + dot.copy().move_to(square.get_bounding_box_point(ec)) for ec in edge_group ]) for edge_group in edge_groups @@ -4089,7 +4089,7 @@ def construct(self): ), FadeIn(denom_rect), ShowCreation(E_arrow), - FadeInFrom(E_words, UP), + FadeIn(E_words, UP), low_diagram_rects.set_stroke, TEAL, 3, ) self.wait() @@ -4099,8 +4099,8 @@ def construct(self): FadeOut(denom_rect), FadeIn(numer_rect), ShowCreation(H_arrow), - FadeInFrom(H_words, DOWN), - FadeOutAndShift(title, UP), + FadeIn(H_words, DOWN), + FadeOut(title, UP), low_diagram_rects.set_stroke, WHITE, 1, top_diagram_rects.set_stroke, YELLOW, 3, ) @@ -4253,7 +4253,7 @@ def construct(self): self.play(randy.change, "sassy") self.wait() self.play( - FadeInFrom(people, RIGHT, lag_ratio=0.01), + FadeIn(people, RIGHT, lag_ratio=0.01), randy.change, "raise_left_hand", people, ) self.wait() @@ -4287,7 +4287,7 @@ def construct(self): MoveToTarget(steve), randy.shift, 2 * LEFT, randy.change, 'erm', kt.target, - FadeOutAndShift(people, 2 * LEFT), + FadeOut(people, 2 * LEFT), ) self.remove(people, cross) self.play(Blink(randy)) @@ -4453,13 +4453,13 @@ def construct(self): brain_outline.set_fill(opacity=0) brain_outline.set_stroke(TEAL, 4) - self.play(FadeInFrom(brain, RIGHT)) + self.play(FadeIn(brain, RIGHT)) self.play( GrowFromCenter(arrow), LaggedStartMap(FadeInFromDown, q_marks[0]), run_time=1 ) - self.play(FadeInFrom(formula, LEFT)) + self.play(FadeIn(formula, LEFT)) self.wait() kw = {"run_time": 1, "lag_ratio": 0.3} @@ -4586,7 +4586,7 @@ def construct(self): programmer.set_height(3) programmer.to_corner(DL) - self.play(FadeInFrom(programmer, DOWN)) + self.play(FadeIn(programmer, DOWN)) self.wait() diff --git a/from_3b1b/active/chess.py b/from_3b1b/active/chess.py new file mode 100644 index 0000000000..fed78df881 --- /dev/null +++ b/from_3b1b/active/chess.py @@ -0,0 +1,4873 @@ +from manimlib.imports import * + + +def boolian_linear_combo(bools): + return reduce(op.xor, [b * n for n, b in enumerate(bools)], 0) + + +def string_to_bools(message): + # For easter eggs on the board + as_int = int.from_bytes(message.encode(), 'big') + bits = "{0:b}".format(as_int) + bits = (len(message) * 8 - len(bits)) * '0' + bits + return [bool(int(b)) for b in bits] + + +def layer_mobject(mobject, nudge=1e-6): + for i, sm in enumerate(mobject.family_members_with_points()): + sm.shift(i * nudge * OUT) + + +def int_to_bit_coords(n, min_dim=3): + bits = "{0:b}".format(n) + bits = (min_dim - len(bits)) * '0' + bits + return np.array(list(map(int, bits))) + + +def bit_coords_to_int(bits): + return sum([(2**n) * b for n, b in enumerate(reversed(bits))]) + + +def get_vertex_sphere(height=0.4, color=GREY, resolution=(21, 21)): + sphere = Sphere(resolution=resolution) + sphere.set_height(height) + sphere.set_color(color) + return sphere + + +def get_bit_string(bit_coords): + result = VGroup(*[Integer(int(b)) for b in bit_coords]) + result.arrange(RIGHT, buff=SMALL_BUFF) + result.set_stroke(BLACK, 4, background=True) + return result + + +class Chessboard(SGroup): + CONFIG = { + "shape": (8, 8), + "height": 7, + "depth": 0.25, + "colors": [LIGHT_GREY, DARKER_GREY], + "gloss": 0.2, + "square_resolution": (3, 3), + "top_square_resolution": (5, 5), + } + + def __init__(self, **kwargs): + super().__init__(**kwargs) + nr, nc = self.shape + cube = Cube(square_resolution=self.square_resolution) + # Replace top square with something slightly higher res + top_square = Square3D(resolution=self.top_square_resolution) + top_square.replace(cube[0]) + cube.replace_submobject(0, top_square) + self.add(*[cube.copy() for x in range(nc * nr)]) + self.arrange_in_grid(nr, nc, buff=0) + self.set_height(self.height) + self.set_depth(self.depth, stretch=True) + for i, j in it.product(range(nr), range(nc)): + color = self.colors[(i + j) % 2] + self[i * nc + j].set_color(color) + self.center() + self.set_gloss(self.gloss) + + +class Coin(Group): + CONFIG = { + "disk_resolution": (4, 51), + "height": 1, + "depth": 0.1, + "color": GOLD_D, + "tails_color": RED, + "include_labels": True, + "numeric_labels": False, + } + + def __init__(self, **kwargs): + super().__init__(**kwargs) + res = self.disk_resolution + self.top = Disk3D(resolution=res, gloss=0.2) + self.bottom = self.top.copy() + self.top.shift(OUT) + self.bottom.shift(IN) + self.edge = Cylinder(height=2, resolution=(res[1], 2)) + self.add(self.top, self.bottom, self.edge) + self.rotate(90 * DEGREES, OUT) + self.set_color(self.color) + self.bottom.set_color(RED) + + if self.include_labels: + chars = "10" if self.numeric_labels else "HT" + labels = VGroup(*[TextMobject(c) for c in chars]) + for label, vect in zip(labels, [OUT, IN]): + label.shift(1.02 * vect) + label.set_height(0.8) + labels[1].rotate(PI, RIGHT) + labels.apply_depth_test() + labels.set_stroke(width=0) + self.add(*labels) + self.labels = labels + + self.set_height(self.height) + self.set_depth(self.depth, stretch=True) + + def is_heads(self): + return self.top.get_center()[2] > self.bottom.get_center()[2] + + def flip(self, axis=RIGHT): + super().flip(axis) + return self + + +class CoinsOnBoard(Group): + CONFIG = { + "proportion_of_square_height": 0.7, + "coin_config": {}, + } + + def __init__(self, chessboard, **kwargs): + super().__init__(**kwargs) + prop = self.proportion_of_square_height + for cube in chessboard: + coin = Coin(**self.coin_config) + coin.set_height(prop * cube.get_height()) + coin.next_to(cube, OUT, buff=0) + self.add(coin) + + def flip_at_random(self, p=0.5): + bools = np.random.random(len(self)) < p + self.flip_by_bools(bools) + return self + + def flip_by_message(self, message): + self.flip_by_bools(string_to_bools(message)) + return self + + def flip_by_bools(self, bools): + for coin, head in zip(self, bools): + if coin.is_heads() ^ head: + coin.flip() + return self + + def get_bools(self): + return [coin.is_heads() for coin in self] + + +class Key(SVGMobject): + CONFIG = { + "file_name": "key", + "fill_color": YELLOW_D, + "fill_opacity": 1, + "stroke_color": YELLOW_D, + "stroke_width": 0, + "gloss": 0.5, + "depth_test": True, + } + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.rotate(PI / 2, OUT) + + +class FlipCoin(Animation): + CONFIG = { + "axis": RIGHT, + "run_time": 1, + "shift_dir": OUT, + } + + def __init__(self, coin, **kwargs): + super().__init__(coin, **kwargs) + self.shift_vect = coin.get_height() * self.shift_dir / 2 + + def interpolate_mobject(self, alpha): + coin = self.mobject + for sm, start_sm in self.families: + sm.points[:] = start_sm.points + coin.rotate(alpha * PI, axis=self.axis) + coin.shift(4 * alpha * (1 - alpha) * self.shift_vect) + return coin + + +# Scenes +class IntroducePuzzle(Scene): + CONFIG = { + "camera_class": ThreeDCamera, + } + + def construct(self): + # Setup + frame = self.camera.frame + + chessboard = Chessboard() + chessboard.move_to(ORIGIN, OUT) + + grid = NumberPlane( + x_range=(0, 8), y_range=(0, 8), + faded_line_ratio=0 + ) + grid.match_height(chessboard) + grid.next_to(chessboard, OUT, 1e-8) + low_grid = grid.copy() + low_grid.next_to(chessboard, IN, 1e-8) + grid.add(low_grid) + grid.set_stroke(GREY, width=2) + grid.set_gloss(0.5) + grid.prepare_for_nonlinear_transform(0) + + coins = CoinsOnBoard(chessboard) + coins.set_gloss(0.2) + coins_to_flip = Group() + head_bools = string_to_bools('3b1b :)') + for coin, heads in zip(coins, head_bools): + if not heads: + coins_to_flip.add(coin) + coins_to_flip.shuffle() + + count_label = VGroup( + Integer(0, edge_to_fix=RIGHT), + TextMobject("Coins") + ) + count_label.arrange(RIGHT, aligned_edge=DOWN) + count_label.to_corner(UL) + count_label.fix_in_frame() + + # Draw board and coins + frame.set_rotation(-25 * DEGREES, 70 * DEGREES, 0) + self.play( + FadeIn(chessboard), + ShowCreationThenDestruction(grid, lag_ratio=0.01), + frame.set_theta, 0, + frame.set_phi, 45 * DEGREES, + run_time=3, + ) + self.wait() + + self.add(count_label) + self.play( + ShowIncreasingSubsets(coins), + UpdateFromFunc(count_label[0], lambda m, c=coins: m.set_value(len(c))), + rate_func=bezier([0, 0, 1, 1]), + run_time=2, + ) + self.wait() + self.play(LaggedStartMap(FlipCoin, coins_to_flip, run_time=6, lag_ratio=0.1)) + self.add(coins) + self.wait() + + # Show key + key = Key() + key.rotate(PI / 4, RIGHT) + key.move_to(3 * OUT) + key.scale(0.8) + key.to_edge(LEFT, buff=1) + + k = boolian_linear_combo(head_bools) ^ 63 # To make the flip below the actual solution + key_cube = Cube(square_resolution=(6, 6)) + key_cube.match_color(chessboard[k]) + key_cube.replace(chessboard[k], stretch=True) + chessboard.replace_submobject(k, key_cube) + key_square = key_cube[0] + chessboard.generate_target() + chessboard.save_state() + for i, cube in enumerate(chessboard.target): + if i == k: + cube[0].set_color(MAROON_E) + else: + cube.set_color(interpolate_color(cube.get_color(), BLACK, 0.8)) + + key.generate_target() + key.target.rotate(PI / 4, LEFT) + key.target.set_width(0.7 * key_square.get_width()) + key.target.next_to(key_square, IN, buff=SMALL_BUFF) + + self.play(FadeIn(key, LEFT)) + self.wait() + self.play( + FadeOut(coins, lag_ratio=0.01), + MoveToTarget(chessboard), + ) + ks_top = key_square.get_top() + self.play( + Rotate(key_square, PI / 2, axis=LEFT, about_point=ks_top), + MoveToTarget(key), + frame.set_phi, 60 * DEGREES, + run_time=2, + ) + self.play( + Rotate(key_square, PI / 2, axis=RIGHT, about_point=ks_top), + run_time=2, + ) + chessboard.saved_state[k][0].match_color(key_square) + self.play( + chessboard.restore, + FadeIn(coins), + frame.set_phi, 0 * DEGREES, + frame.move_to, 2 * LEFT, + run_time=3 + ) + + # State goal + goal = TextMobject( + "Communicate where\\\\the key is", + " by turning\\\\over one coin.", + alignment="" + ) + goal.next_to(count_label, DOWN, LARGE_BUFF, LEFT) + goal.fix_in_frame() + goal[1].set_color(YELLOW) + + self.play(FadeIn(goal[0])) + self.wait() + self.play(FadeIn(goal[1])) + self.wait() + + coin = coins[63] + rect = SurroundingRectangle(coin, color=TEAL) + rect.set_opacity(0.5) + rect.save_state() + rect.replace(chessboard) + rect.set_stroke(width=0) + rect.set_fill(opacity=0) + + self.play(Restore(rect, run_time=2)) + self.add(coin, rect) + self.play(FlipCoin(coin), FadeOut(rect)) + + +class PrisonerPuzzleSetting(PiCreatureScene): + CONFIG = { + "camera_config": { + "background_color": GREY_E + } + } + + def create_pi_creatures(self): + p1 = PiCreature(color=BLUE_C, height=2) + p2 = PiCreature(color=RED, height=2) + warden = PiCreature(color=GREY_BROWN, height=2.5) + warden.flip() + result = VGroup(p1, p2, warden) + result.arrange(RIGHT, buff=2, aligned_edge=DOWN) + warden.shift(RIGHT) + result.center().to_edge(DOWN, buff=1.5) + return result + + def construct(self): + pis = self.pi_creatures + p1, p2, warden = self.pi_creatures + + names = VGroup( + TextMobject("Prisoner 1\\\\(you)"), + TextMobject("Prisoner 2"), + TextMobject("Warden"), + ) + for name, pi in zip(names, pis): + name.match_color(pi.body) + name.next_to(pi, DOWN) + + question = TextMobject( + "Why do mathematicians\\\\always set their puzzles\\\\in prisons?", + alignment="" + ) + question.to_corner(UR) + + self.remove(warden) + warden.look_at(p2.eyes) + self.play( + LaggedStartMap(FadeIn, pis[:2], run_time=1.5, lag_ratio=0.3), + LaggedStartMap(FadeIn, names[:2], run_time=1.5, lag_ratio=0.3), + ) + self.play( + p1.change, "sad", + p2.change, "pleading", warden.eyes + ) + self.play( + FadeIn(warden), + FadeIn(names[2]), + ) + self.play(warden.change, "conniving", p2.eyes) + self.wait() + self.play(FadeIn(question, lag_ratio=0.1)) + self.wait(3) + self.play(FadeOut(question)) + self.wait(2) + + +class FromCoinToSquareMaps(ThreeDScene): + CONFIG = { + "messages": [ + "Please, ", + "go watch", + "Stand-up", + "Maths on", + "YouTube." + ], + "flip_lag_ratio": 0.05, + } + + def construct(self): + messages = self.messages + + board1 = Chessboard() + board1.set_width(5.5) + board1.to_corner(DL) + + board2 = board1.copy() + board2.to_corner(DR) + + coins = CoinsOnBoard(board1) + bools = string_to_bools(messages[0]) + for coin, head in zip(coins, bools): + if not head: + coin.flip(RIGHT) + + for cube in board2: + cube.original_color = cube.get_color() + + arrow = Arrow(board1.get_right(), board2.get_left()) + arrow.tip.set_stroke(width=0) + + title1 = TextMobject("Pattern of coins") + title2 = TextMobject("Individual square") + + for title, board in [(title1, board1), (title2, board2)]: + title.scale(0.5 / title[0][0].get_height()) + title.next_to(board, UP, MED_LARGE_BUFF) + + title2.align_to(title1, UP) + + def get_special_square(coins=coins, board=board2): + bools = [coin.is_heads() for coin in coins] + return board[boolian_linear_combo(bools)] + + self.add(board1) + self.add(title1) + self.add(coins) + + self.play( + GrowArrow(arrow), + FadeIn(board2, 2 * LEFT) + ) + square = get_special_square() + rect = SurroundingRectangle(square, buff=0) + rect.set_color(PINK) + rect.next_to(square, OUT, buff=0.01) + self.play( + square.set_color, MAROON_C, + ShowCreation(rect), + FadeIn(title2) + ) + + for message in messages[1:]: + new_bools = string_to_bools(message) + coins_to_flip = Group() + for coin, to_heads in zip(coins, new_bools): + if coin.is_heads() ^ to_heads: + coins_to_flip.add(coin) + coins_to_flip.shuffle() + self.play(LaggedStartMap( + FlipCoin, coins_to_flip, + lag_ratio=self.flip_lag_ratio, + run_time=1, + )) + + new_square = get_special_square() + self.play( + square.set_color, square.original_color, + new_square.set_color, MAROON_C, + rect.move_to, new_square, OUT, + rect.shift, 0.01 * OUT, + ) + square = new_square + self.wait() + + +class FromCoinToSquareMapsSingleFlips(FromCoinToSquareMaps): + CONFIG = { + "messages": [ + "FlipBits", + "BlipBits", + "ClipBits", + "ChipBits", + "ChipBats", + "ChipRats", + ] + } + + +class DiagramOfProgression(ThreeDScene): + def construct(self): + # Setup panels + P1_COLOR = BLUE_C + P2_COLOR = RED + + rect = Rectangle(4, 2) + rect.set_fill(GREY_E, 1) + panels = Group() + for x in range(4): + panels.add(Group(rect.copy())) + panels.arrange_in_grid(buff=1) + panels[::2].shift(0.5 * LEFT) + panels.set_width(FRAME_WIDTH - 2) + panels.center().to_edge(DOWN) + p1_shift = panels[1].get_center() - panels[0].get_center() + panels[1].move_to(panels[0]) + + chessboard = Chessboard() + chessboard.set_height(0.9 * panels[0].get_height()) + coins = CoinsOnBoard( + chessboard, + coin_config={ + "disk_resolution": (2, 25), + } + ) + coins.flip_by_message("Tau > Pi") + + for panel in panels[1:]: + cb = chessboard.copy() + co = coins.copy() + cb.next_to(panel.get_right(), LEFT) + co.next_to(cb, OUT, 0) + panel.chessboard = cb + panel.coins = co + panel.add(cb, co) + + kw = { + "tex_to_color_map": { + "Prisoner 1": P1_COLOR, + "Prisoner 2": P2_COLOR, + } + } + titles = VGroup( + TextMobject("Prisoners conspire", **kw), + TextMobject("Prisoner 1 sees key", **kw), + TextMobject("Prisoner 1 flips coin", **kw), + TextMobject("Prisoner 2 guesses key square", **kw), + ) + + for panel, title in zip(panels, titles): + title.next_to(panel, UP) + panel.title = title + panel.add(title) + + # Darken first chessboard + for coin in panels[1].coins: + coin.remove(coin.edge) + if coin.is_heads(): + coin.remove(coin.bottom) + coin.remove(coin.labels[1]) + else: + coin.remove(coin.top) + coin.remove(coin.labels[0]) + coin.set_opacity(0.25) + + # Add characters + prisoner1 = PiCreature(color=P1_COLOR) + prisoner2 = PiCreature(color=P2_COLOR) + pis = VGroup(prisoner1, prisoner2) + pis.arrange(RIGHT, buff=1) + pis.set_height(1.5) + + p0_pis = pis.copy() + p0_pis.set_height(2.0, about_edge=DOWN) + p0_pis[1].flip() + p0_pis.next_to(panels[0].get_bottom(), UP, SMALL_BUFF) + p0_pis[0].change("pondering", p0_pis[1].eyes) + p0_pis[1].change("speaking", p0_pis[0].eyes) + panels[0].add(p0_pis) + + p1_pi = pis[0].copy() + p1_pi.next_to(panels[1].get_corner(DL), UR, SMALL_BUFF) + p1_pi.change("happy") + key = Key() + key.set_height(0.5) + key.next_to(p1_pi, UP) + key.set_color(YELLOW) + key_cube = panels[1].chessboard[18] + key_square = Square() + key_square.replace(key_cube) + key_square.set_stroke(width=3) + key_square.match_color(key) + p1_pi.look_at(key_square) + key_arrow = Arrow( + key.get_right() + SMALL_BUFF * UP, + key_square.get_corner(UL), + path_arc=-45 * DEGREES, + buff=SMALL_BUFF + ) + key_arrow.tip.set_stroke(width=0) + key_arrow.match_color(key) + panels[1].add(p1_pi, key) + + p2_pi = pis[0].copy() + p2_pi.next_to(panels[2].get_corner(DL), UR, SMALL_BUFF) + p2_pi.change("tease") + flip_coin = panels[2].coins[38] + panels[3].coins[38].flip() + flip_square = Square() + flip_square.replace(panels[2].chessboard[38]) + flip_square.set_stroke(BLUE, 5) + for coin in panels[2].coins: + if coin is not flip_coin: + coin.remove(coin.edge) + if coin.is_heads(): + coin.remove(coin.bottom) + coin.remove(coin.labels[1]) + else: + coin.remove(coin.top) + coin.remove(coin.labels[0]) + coin.set_opacity(0.25) + panels[2].add(p2_pi) + + p3_pi = pis[1].copy() + p3_pi.next_to(panels[3].get_corner(DL), UR, SMALL_BUFF) + p3_pi.shift(MED_LARGE_BUFF * RIGHT) + p3_pi.change("confused") + panels[3].add(p3_pi) + + # Animate each panel in + self.play(FadeIn(panels[1], DOWN)) + self.play( + ShowCreation(key_arrow), + FadeInFromLarge(key_square), + ) + panels[1].add(key_arrow, key_square) + self.wait() + + self.play(FadeIn(panels[2], UP)) + self.play( + ShowCreation(flip_square), + FlipCoin(flip_coin), + p2_pi.look_at, flip_coin, + ) + self.wait() + + self.play(FadeIn(panels[3], LEFT)) + self.wait() + + self.play( + FadeIn(panels[0], LEFT), + panels[1].shift, p1_shift, + ) + self.wait() + + +class ImpossibleVariations(FromCoinToSquareMaps): + CONFIG = { + "messages": [ + "FlipBits", + "BlipBits", + "ClipBits", + "ChipBits", + "ChipBats", + "ChipRats", + "ChipVats", + "ChipFats", + "ChapFats", + ] + } + + def construct(self): + # Definitions + frame = self.camera.frame + title = TextMobject("Describe any square\\\\with one flip") + title.set_height(1.2) + title.to_edge(UP) + title.fix_in_frame() + messages = it.cycle(self.messages) + + left_board = Chessboard() + right_board = Chessboard() + for board, vect in (left_board, LEFT), (right_board, RIGHT): + board.set_width(4.5) + board.to_corner(DOWN + vect, buff=LARGE_BUFF) + coins = CoinsOnBoard(left_board) + coins.flip_by_message(next(messages)) + + arrow = Arrow(left_board.get_right(), right_board.get_left()) + + # Prepare for colorings + for cube in right_board: + cube.original_color = cube.get_color() + + def get_special_square(board=right_board, coins=coins): + return board[boolian_linear_combo(coins.get_bools())] + + frame.set_phi(45 * DEGREES) + + # Introduce boards + self.add(title) + group = Group(*left_board, *coins, *right_board) + self.play( + LaggedStartMap(FadeInFromLarge, group, lambda m: (m, 1.3), lag_ratio=0.01), + GrowArrow(arrow) + ) + + # Flip one at a time + square = Square() + square.set_stroke(TEAL, 3) + square.replace(right_board[0]) + square.move_to(right_board[0], OUT) + self.moving_square = square + self.colored_square = right_board[0] + + for x in range(8): + self.set_board_message(next(messages), left_board, coins, get_special_square) + self.wait() + + # To 6x6 + to_fade = Group() + for grid in left_board, right_board, coins: + for n, mob in enumerate(grid): + row = n // 8 + col = n % 8 + if not ((0 < row < 7) and (0 < col < 7)): + to_fade.add(mob) + + cross = Cross(title) + cross.fix_in_frame() + cross.set_stroke(RED, 8) + cross.shift(2 * LEFT) + imp_words = TextMobject("Impossible!") + imp_words.fix_in_frame() + imp_words.next_to(title, RIGHT, buff=1.5) + imp_words.shift(2 * LEFT) + imp_words.set_height(0.7) + imp_words.set_color(RED) + + self.play(to_fade.set_opacity, 0.05) + self.play( + title.shift, 2 * LEFT, + FadeIn(cross, 2 * RIGHT), + FadeIn(imp_words, LEFT) + ) + self.wait() + self.play(to_fade.set_opacity, 1) + + # Remove a square + to_remove = Group( + left_board[63], right_board[63], coins[63] + ) + remove_words = TextMobject("Remove one\\\\square") + remove_words.set_color(RED) + remove_words.to_corner(DOWN, buff=1.5) + remove_words.fix_in_frame() + + self.play( + FadeIn(remove_words, DOWN), + FadeOut(to_remove, 3 * IN), + ) + + def set_board_message(self, message, left_board, coins, get_special_square): + new_bools = string_to_bools(message) + coins_to_flip = Group() + for coin, to_heads in zip(coins, new_bools): + if coin.is_heads() ^ to_heads: + coins_to_flip.add(coin) + coins_to_flip.shuffle() + self.play(LaggedStartMap( + FlipCoin, coins_to_flip, + lag_ratio=self.flip_lag_ratio, + run_time=1, + )) + + new_colored_square = get_special_square() + self.play( + new_colored_square.set_color, BLUE, + self.colored_square.set_color, self.colored_square.original_color, + self.moving_square.move_to, get_special_square(), OUT, + ) + self.colored_square = new_colored_square + + +class ErrorCorrectionMention(Scene): + def construct(self): + # Setup board + message = "Do math!" + error_message = "Do meth!" + board = Chessboard() + board.set_width(5) + board.to_corner(DL) + coins = CoinsOnBoard(board) + coins.flip_by_message(message) + bools = coins.get_bools() + + right_board = board.copy() + right_board.to_corner(DR) + right_board.set_opacity(0.5) + right_board[boolian_linear_combo(bools)].set_color(BLUE, 1) + + arrow = Arrow(board.get_right(), right_board.get_left()) + + words = TextMobject("Feels a bit like ", "Error correction codes", "$\\dots$") + words.scale(1.2) + words.to_edge(UP) + + self.add(board, coins, right_board) + self.add(arrow) + self.add(words) + + # Go from board diagram to bit string + bits = VGroup() + for coin in coins: + bit = Integer(1 if coin.is_heads() else 0) + bit.replace(coin, dim_to_match=1) + bits.add(bit) + + coin.generate_target() + coin.target.rotate(90 * DEGREES, RIGHT) + coin.target.set_opacity(0) + + bits_rect = SurroundingRectangle(bits, buff=MED_SMALL_BUFF) + bits_rect.set_stroke(YELLOW, 2) + data_label = TextMobject("Data") + data_label.next_to(bits_rect, UP) + data_label.set_color(YELLOW) + + meaning_label = TextMobject(f"``{message}''") + error_meaning_label = TextMobject(f"``{error_message}''") + for label in meaning_label, error_meaning_label: + label.scale(1.5) + label.next_to(arrow, RIGHT) + error_meaning_label[0][5].set_color(RED) + + message_label = TextMobject("Message") + message_label.set_color(BLUE) + message_label.next_to(meaning_label, UP, buff=1.5) + message_label.to_edge(RIGHT, LARGE_BUFF) + message_arrow = Arrow( + message_label.get_bottom(), + meaning_label.get_top(), + ) + message_arrow = Arrow( + message_label.get_left(), + meaning_label.get_top(), + path_arc=70 * DEGREES, + ) + + self.play( + LaggedStartMap(MoveToTarget, coins), + LaggedStartMap(FadeOut, board), + Write(bits), + run_time=3 + ) + self.play( + words[1].set_x, 0, + FadeOut(words[0], LEFT), + FadeOut(words[2], LEFT), + ) + self.play( + ShowCreation(bits_rect), + FadeIn(data_label, DOWN) + ) + self.play( + FadeOut(right_board), + FadeIn(message_label, DOWN), + ShowCreation(message_arrow), + FadeIn(meaning_label) + ) + self.wait() + + # Describe ECC + error_index = 8 * 4 + 5 + error_bit = bits[error_index] + error_bit.unlock_triangulation() + error_bit_rect = SurroundingRectangle(error_bit) + error_bit_rect.set_stroke(RED, 2) + + self.play( + FadeInFromLarge(error_bit_rect), + error_bit.set_value, 1 - error_bit.get_value(), + error_bit.set_color, RED, + ) + meaning_label.save_state() + meaning_label.unlock_triangulation() + self.play( + Transform(meaning_label, error_meaning_label) + ) + self.wait() + + # Ask about correction + question = TextMobject("How can you\\\\detect the error?") + question.next_to(bits_rect, RIGHT, aligned_edge=UP) + self.play(Write(question)) + self.wait(2) + + ecc = VGroup() + for bit in int_to_bit_coords(boolian_linear_combo(bools), 6): + ecc.add(Integer(bit).match_height(bits[0])) + ecc.arrange(RIGHT, buff=0.2) + ecc.set_color(GREEN) + ecc.next_to(bits, RIGHT, MED_LARGE_BUFF, aligned_edge=DOWN) + ecc_rect = SurroundingRectangle(ecc, buff=MED_SMALL_BUFF) + ecc_rect.set_stroke(GREEN, 2) + + ecc_name = words[1] + ecc_name.generate_target() + ecc_name.target[-1].set_opacity(0) + ecc_name.target.scale(0.75) + ecc_name.target.next_to(ecc_rect) + ecc_name.target.match_color(ecc) + + frame = self.camera.frame + + self.play( + MoveToTarget(ecc_name), + ShowIncreasingSubsets(ecc), + ShowCreation(ecc_rect), + ApplyMethod(frame.move_to, DOWN, run_time=2) + ) + self.wait() + + # Show correction at play + lines = VGroup() + for bit in bits: + line = Line(ecc_rect.get_top(), bit.get_center()) + line.set_stroke(GREEN, 1, opacity=0.7) + lines.add(line) + + alert = TexMobject("!!!")[0] + alert.arrange(RIGHT, buff=SMALL_BUFF) + alert.scale(1.5) + alert.set_color(RED) + alert.next_to(ecc_rect, UP) + + self.play(LaggedStartMap( + ShowCreationThenFadeOut, lines, + lag_ratio=0.02, run_time=3 + )) + self.play(FadeIn(alert, DOWN, lag_ratio=0.2)) + self.wait() + self.play(ShowCreation(lines, lag_ratio=0)) + for line in lines: + line.generate_target() + line.target.become(lines[error_index]) + self.play(LaggedStartMap(MoveToTarget, lines, lag_ratio=0, run_time=1)) + self.play( + error_bit.set_value, 0, + Restore(meaning_label) + ) + self.play( + FadeOut(lines), + FadeOut(alert), + FadeOut(error_bit_rect), + error_bit.set_color, WHITE, + ) + self.wait() + + # Hamming name + hamming_label = TextMobject("e.g. Hamming codes") + hamming_label.move_to(ecc_name, LEFT) + + self.play( + Write(hamming_label), + FadeOut(ecc_name, DOWN) + ) + self.wait() + + +class StandupMathsWrapper(Scene): + CONFIG = { + "title": "Solution on Stand-up Maths" + } + + def construct(self): + fsr = FullScreenFadeRectangle() + fsr.set_fill(GREY_E, 1) + self.add(fsr) + + title = TextMobject(self.title) + title.scale(1.5) + title.to_edge(UP) + + rect = ScreenRectangle(height=6) + rect.set_stroke(WHITE, 2) + rect.set_fill(BLACK, 1) + rect.next_to(title, DOWN) + rb = AnimatedBoundary(rect) + + self.add(rect, rb) + self.play(Write(title)) + self.wait(30) + + +class ComingUpWrapper(StandupMathsWrapper): + CONFIG = { + "title": "Coming up" + } + + +class TitleCard(Scene): + def construct(self): + n = 6 + board = Chessboard(shape=(n, n)) + for square in board: + square.set_color(interpolate_color(square.get_color(), BLACK, 0.25)) + # board[0].set_opacity(0) + + grid = NumberPlane( + x_range=(0, n), + y_range=(0, n), + faded_line_ratio=0 + ) + grid.match_height(board) + grid.next_to(board, OUT, 1e-8) + low_grid = grid.copy() + low_grid.next_to(board, IN, 1e-8) + grid.add(low_grid) + grid.set_stroke(GREY, width=1) + grid.set_gloss(0.5) + grid.prepare_for_nonlinear_transform(0) + grid.rotate(PI, RIGHT) + + frame = self.camera.frame + frame.set_phi(45 * DEGREES) + + text = TextMobject("The impossible\\\\chessboard puzzle") + # text.set_width(board.get_width() - 0.5) + text.set_width(FRAME_WIDTH - 2) + text.set_stroke(BLACK, 10, background=True) + text.fix_in_frame() + self.play( + ApplyMethod(frame.set_phi, 0, run_time=5), + ShowCreationThenDestruction(grid, lag_ratio=0.02, run_time=3), + LaggedStartMap(FadeIn, board, run_time=3, lag_ratio=0), + Write(text, lag_ratio=0.1, run_time=3, stroke_color=BLUE_A), + ) + self.wait(2) + + +class WhatAreWeDoingHere(TeacherStudentsScene): + def construct(self): + self.student_says( + "Wait, what are we\\\\doing here then?", + target_mode="sassy", + added_anims=[self.get_student_changes("hesitant", "angry", "sassy")], + run_time=2 + ) + self.play(self.teacher.change, "tease") + self.wait(6) + + +class HowCanWeVisualizeSolutions(TeacherStudentsScene): + def construct(self): + self.teacher_says( + "How can we\\\\visualize solutions", + bubble_kwargs={ + "height": 3, + "width": 4, + "fill_opacity": 0, + }, + added_anims=[self.get_student_changes("pondering", "thinking", "pondering")] + ) + self.look_at(self.screen), + self.wait(1) + self.change_student_modes("thinking", "erm", "confused") + self.wait(5) + + +class TwoSquareCase(ThreeDScene): + CONFIG = { + "coin_names": ["c_0", "c_1"] + } + + def construct(self): + frame = self.camera.frame + + # Transition to just two square + chessboard = Chessboard() + chessboard.shift(2 * IN + UP) + coins = CoinsOnBoard(chessboard) + coins.flip_by_message("To 2 bits") + + to_remove = Group(*it.chain(*zip(chessboard[:1:-1], coins[:1:-1]))) + small_board = chessboard[:2] + coin_pair = coins[:2] + small_group = Group(small_board, coin_pair) + + frame.set_phi(45 * DEGREES) + + two_square_words = TextMobject("What about a 2-square board?") + two_square_words.fix_in_frame() + two_square_words.set_height(0.5) + two_square_words.center().to_edge(UP) + + self.add(chessboard, coins) + self.play( + Write(two_square_words, run_time=1), + LaggedStartMap( + FadeOut, to_remove, + lambda m: (m, DOWN), + run_time=2, + lag_ratio=0.01 + ), + small_group.center, + small_group.set_height, 1.5, + frame.set_phi, 10 * DEGREES, + run_time=2 + ) + self.wait(3) + + coins = coin_pair + + # Show two locations for the key + key = Key() + key.set_width(0.7 * small_board[0].get_width()) + key.move_to(small_board[0]) + key.shift(0.01 * OUT) + + coin_pair.shift(0.04 * OUT) + s0_top = small_board[0][0].get_top() + s1_top = small_board[1][0].get_top() + + s0_rot_group = Group(small_board[0][0], coin_pair[0]) + s1_rot_group = Group(small_board[1][0], coin_pair[1]) + + self.add(key) + angle = 170 * DEGREES + self.play( + Rotate(s0_rot_group, angle, LEFT, about_point=s0_top), + Rotate(s1_rot_group, angle, LEFT, about_point=s1_top), + frame.set_phi, 45 * DEGREES, + ) + self.wait() + self.play( + key.match_x, small_board[1], + path_arc=PI, + path_arc_axis=UP, + ) + self.wait() + self.play( + Rotate(s0_rot_group, angle, RIGHT, about_point=s0_top), + Rotate(s1_rot_group, angle, RIGHT, about_point=s1_top), + frame.set_phi, 0, + run_time=2, + ) + self.wait() + + # Show four states pointing to two message + states = VGroup(*[ + TextMobject(letters, tex_to_color_map={"H": GOLD, "T": RED_D}) + for letters in ["TT", "HT", "TH", "HH"] + ]) + states.set_height(0.8) + states.arrange(DOWN, buff=1) + states.to_edge(LEFT) + + self.play( + FadeOut(two_square_words), + FlipCoin(coins[1]), + FadeIn(states[0]) + ) + self.play( + FlipCoin(coins[0]), + FadeIn(states[1]) + ) + self.play( + FlipCoin(coins[0]), + FlipCoin(coins[1]), + FadeIn(states[2]) + ) + self.play( + FlipCoin(coins[0]), + FadeIn(states[3]) + ) + self.wait() + + key_copy = key.copy() + key_copy.match_x(small_board[0]) + small_board_copy = small_board.copy() + small_boards = Group(small_board_copy, small_board) + for board, vect in zip(small_boards, [UP, DOWN]): + board.generate_target() + board.target.set_opacity(0.7) + board.target.shift(2 * vect) + board.target.set_depth(0.01, stretch=True) + self.add(key, key_copy, *small_boards) + self.play( + FadeOut(coins), + MoveToTarget(small_board), + MaintainPositionRelativeTo(key, small_board), + MoveToTarget(small_board_copy), + MaintainPositionRelativeTo(key_copy, small_board_copy), + ) + self.add(*small_boards, key, key_copy) + + arrows = VGroup() + for i in range(4): + arrows.add(Arrow(states[i].get_right(), small_boards[i // 2].get_left())) + arrows.set_opacity(0.75) + + self.play(LaggedStartMap(GrowArrow, arrows, lag_ratio=0.3)) + self.wait() + + # Show one flip changing interpretation + coins.next_to(states, LEFT, buff=1.5) + + def get_state(coins=coins): + bools = [c.is_heads() for c in coins] + return sum([b * (2**n) for n, b in enumerate(reversed(bools))]) + + n = 3 + state_rect = SurroundingRectangle(states[n]) + board_rects = VGroup() + for board in small_boards: + br = SurroundingRectangle(board, buff=0) + br.move_to(board, OUT) + br.set_stroke(YELLOW, 3) + board_rects.add(br) + + self.play( + ApplyMethod(frame.shift, 4.5 * LEFT, run_time=1), + FadeIn(coins), + FadeIn(state_rect), + FadeIn(board_rects[1]), + arrows[n].set_color, YELLOW, + arrows[n].set_opacity, 1, + ) + self.wait() + self.play( + FlipCoin(coins[1]), + FadeOut(board_rects[1]), + FadeIn(board_rects[0]), + state_rect.move_to, states[1], + arrows[3].match_style, arrows[0], + arrows[1].match_style, arrows[3], + ) + self.wait() + self.play( + FlipCoin(coins[0]), + state_rect.move_to, states[0], + arrows[0].match_style, arrows[1], + arrows[1].match_style, arrows[3], + ) + self.wait() + self.play( + FlipCoin(coins[0]), + state_rect.move_to, states[1], + arrows[0].match_style, arrows[1], + arrows[1].match_style, arrows[0], + ) + self.wait() + + # Erase H and T, replace with 1 and 0 + bin_states = VGroup(*[ + TextMobject(letters, tex_to_color_map={"1": GOLD, "0": RED_D}) + for letters in ["00", "10", "01", "11"] + ]) + for bin_state, state in zip(bin_states, states): + for bit, letter in zip(bin_state, state): + bit.replace(letter, dim_to_match=1) + bin_coins = CoinsOnBoard(small_board, coin_config={"numeric_labels": True}) + bin_coins[1].flip() + bin_coins.move_to(coins) + + self.play( + FadeOut(coins, IN), + FadeIn(bin_coins), + ) + self.play( + LaggedStartMap(GrowFromCenter, Group(*bin_states.family_members_with_points())), + LaggedStartMap(ApplyMethod, Group(*states.family_members_with_points()), lambda m: (m.scale, 0)), + ) + self.wait() + + # Add labels + c_labels = VGroup(*[ + TexMobject(name) + for name in self.coin_names + ]) + arrow_kw = { + "tip_config": { + "width": 0.2, + "length": 0.2, + }, + "buff": 0.1, + "color": GREY_B, + } + # s_label_arrows = VGroup() + # for high_square, low_square, label in zip(*small_boards, s_labels): + # label.move_to(Group(high_square, low_square)) + # label.arrows = VGroup( + # Arrow(label.get_bottom(), low_square.get_top(), **arrow_kw), + # Arrow(label.get_top(), high_square.get_bottom(), **arrow_kw), + # ) + # s_label_arrows.add(*label.arrows) + + # self.play( + # FadeIn(label), + # *map(GrowArrow, label.arrows) + # ) + + c_label_arrows = VGroup() + for label, coin in zip(c_labels, bin_coins): + label.next_to(coin, UP, LARGE_BUFF) + arrow = Arrow(label.get_bottom(), coin.get_top(), **arrow_kw) + c_label_arrows.add(arrow) + + self.play( + FadeIn(label), + GrowArrow(arrow) + ) + self.wait() + + # Coin 1 communicates location + bit1_rect = SurroundingRectangle( + VGroup( + bin_states[0][1], + bin_states[-1][1], + ), + buff=SMALL_BUFF, + ) + coin1_rect = SurroundingRectangle( + Group(c_labels[1], bin_coins[1]), + buff=SMALL_BUFF, + ) + for rect in bit1_rect, coin1_rect: + rect.insert_n_curves(100) + nd = int(12 * get_norm(rect.get_area_vector())) + rect.become(DashedVMobject(rect, num_dashes=nd)) + rect.set_stroke(WHITE, 2) + + kw = { + "stroke_width": 2, + "stroke_color": YELLOW, + "buff": 0.05, + } + zero_rects, one_rects = [ + VGroup( + SurroundingRectangle(bin_states[0][1], **kw), + SurroundingRectangle(bin_states[1][1], **kw), + ), + VGroup( + SurroundingRectangle(bin_states[2][1], **kw), + SurroundingRectangle(bin_states[3][1], **kw), + ), + ] + + self.play( + ShowCreation(bit1_rect), + ShowCreation(coin1_rect), + FadeOut(state_rect), + ) + self.wait() + self.play(board_rects[0].stretch, 0.5, 0, {"about_edge": LEFT}) + self.play(ShowCreation(zero_rects)) + self.wait() + self.play( + FadeOut(board_rects[0]), + FadeOut(zero_rects), + FadeIn(board_rects[1]) + ) + self.play( + board_rects[1].stretch, 0.5, 0, {"about_edge": RIGHT} + ) + self.play( + FlipCoin(bin_coins[1]), + arrows[1].match_style, arrows[0], + arrows[3].match_style, arrows[1], + ) + self.play(ShowCreation(one_rects[1])) + self.wait() + + # Talk about null bit + null_word = TextMobject("Null bit") + null_word.next_to(bin_coins[0], DOWN, buff=1.5, aligned_edge=LEFT) + null_arrow = Arrow(null_word.get_top(), bin_coins[0].get_bottom()) + + self.play( + Write(null_word), + GrowArrow(null_arrow) + ) + self.wait() + + for i in (0, 1, 0): + self.play( + FlipCoin(bin_coins[0]), + arrows[3 - i].match_style, arrows[0], + arrows[2 + i].match_style, arrows[3 - i], + FadeOut(one_rects[1 - i]), + FadeIn(one_rects[i]), + ) + self.wait() + + # Written mathematically + frame.generate_target() + frame.target.set_height(10, about_edge=DOWN) + rule_words = TextMobject("Rule: Just look at coin 1") + rule_words.set_height(0.6) + rule_words.next_to(frame.target.get_corner(UL), DR, buff=0.5) + rule_arrow = Vector(1.5 * RIGHT) + rule_arrow.next_to(rule_words, RIGHT) + rule_arrow.set_color(BLUE) + rule_equation = TexMobject("K", "=", self.coin_names[1]) + rule_equation_long = TexMobject( + "K", "=", "0", "\\cdot", + self.coin_names[0], "+", "1", "\\cdot", + self.coin_names[1], + ) + + for equation in rule_equation, rule_equation_long: + equation.set_color_by_tex("S", YELLOW) + equation.set_height(0.7) + equation.next_to(rule_arrow, RIGHT) + + s_labels = VGroup( + TexMobject("K", "= 0"), + TexMobject("K", "= 1"), + ) + for label, board in zip(s_labels, small_boards): + label.set_height(0.5) + label.next_to(board, UP) + label.set_color_by_tex("S", YELLOW) + + self.play( + MoveToTarget(frame), + FadeIn(rule_words, 2 * DOWN) + ) + self.wait() + for label in s_labels: + self.play(Write(label)) + self.wait() + self.play( + GrowArrow(rule_arrow), + FadeIn(rule_equation, LEFT), + ) + self.wait() + mid_equation = rule_equation_long[2:-1] + mid_equation.save_state() + mid_equation.scale(0, about_edge=LEFT) + self.play( + Transform(rule_equation[:2], rule_equation_long[:2]), + Transform(rule_equation[2], rule_equation_long[-1]), + Restore(mid_equation), + ) + self.wait() + + self.remove(bin_coins) + for mob in self.mobjects: + for submob in mob.get_family(): + if isinstance(submob, TexSymbol): + submob.set_stroke(BLACK, 8, background=True) + self.add(bin_coins) + + +class TwoSquaresAB(TwoSquareCase): + CONFIG = { + "coin_names": ["a", "b"] + } + + +class IGotThis(TeacherStudentsScene): + def construct(self): + self.student_says( + "Pssh, I got this", + target_mode="tease", + look_at_arg=self.screen, + added_anims=[self.teacher.change, "happy", self.screen], + run_time=2, + ) + self.change_student_modes( + "thinking", "pondering", + look_at_arg=self.screen + ) + self.wait(6) + + +class WalkingTheSquare(ThreeDScene): + def construct(self): + # Setup objects + plane = NumberPlane( + x_range=(-2, 2, 1), + y_range=(-2, 2, 1), + height=15, + width=15, + faded_line_ratio=3, + axis_config={"include_tip": False} + ) + plane.move_to(1.5 * DOWN) + plane.add_coordinate_labels() + plane.x_axis.add_numbers([0]) + + board = Chessboard(shape=(1, 2)) + board.set_height(1.5) + board.move_to(plane.c2p(-0.75, 0.75)) + coins = CoinsOnBoard( + board, + coin_config={"numeric_labels": True} + ) + coins.flip(RIGHT) + + coords = [(0, 0), (0, 1), (1, 0), (1, 1)] + coord_labels = VGroup() + dots = VGroup() + for x, y in coords: + label = TexMobject(f"({x}, {y})") + point = plane.c2p(x, y) + label.next_to(point, UR, buff=0.25) + dot = Dot(point, radius=0.075) + dot.set_color(GREY) + dots.add(dot) + coord_labels.add(label) + + active_dot = Dot(radius=0.15, color=YELLOW) + active_dot.move_to(plane.c2p(0, 0)) + + # Walk around square + self.play(Write(plane)) + self.play( + FadeIn(board), + FadeIn(coins), + FadeIn(active_dot), + FadeIn(coord_labels[0]), + ) + edges = VGroup() + for i, j, c in [(0, 1, 1), (1, 3, 0), (3, 2, 1), (2, 0, 0)]: + edge = Line(plane.c2p(*coords[i]), plane.c2p(*coords[j])) + edge.set_stroke(PINK, 3) + edges.add(edge) + + anims = [ + FlipCoin(coins[c]), + ShowCreation(edge), + ApplyMethod(active_dot.move_to, dots[j]) + ] + if j != 0: + anims += [ + FadeInFromPoint(coord_labels[j], coord_labels[i].get_center()), + ] + self.add(edge, dots[i], active_dot) + self.play(*anims) + self.add(edges, dots, active_dot) + self.wait() + + # Show a few more flips + self.play( + FlipCoin(coins[0]), + active_dot.move_to, dots[2], + ) + self.play( + FlipCoin(coins[1]), + active_dot.move_to, dots[3], + ) + self.play( + FlipCoin(coins[1]), + active_dot.move_to, dots[2], + ) + self.wait() + + # Circles illustrating scheme + low_rect = SurroundingRectangle( + VGroup(edges[3], coord_labels[0], coord_labels[2], plane.x_axis.numbers[-1]), + buff=0.25, + ) + low_rect.round_corners() + low_rect.insert_n_curves(30) + low_rect.set_stroke(YELLOW, 3) + high_rect = low_rect.copy() + high_rect.shift(dots[1].get_center() - dots[0].get_center()) + + key = Key() + key.set_color(YELLOW) + key.set_gloss(0) + key.match_width(board[0]) + key.next_to(board[0], UP, SMALL_BUFF) + + s_labels = VGroup( + TexMobject("\\text{Key} = 0").next_to(low_rect, UP, SMALL_BUFF), + TexMobject("\\text{Key} = 1").next_to(high_rect, UP, SMALL_BUFF), + ) + + self.play( + ShowCreation(low_rect), + ) + self.play( + FadeIn(key, DOWN), + FadeIn(s_labels[0], DOWN), + ) + self.play( + FlipCoin(coins[0]), + active_dot.move_to, dots[0], + ) + self.wait(0.5) + self.play( + FlipCoin(coins[0]), + active_dot.move_to, dots[2], + ) + self.wait() + self.play( + TransformFromCopy(low_rect, high_rect), + FadeIn(s_labels[1], DOWN), + low_rect.set_stroke, GREY, 1, + FlipCoin(coins[1]), + active_dot.move_to, dots[3], + key.match_x, board[1], + ) + self.wait() + self.play( + FlipCoin(coins[0]), + active_dot.move_to, dots[1], + ) + self.wait() + self.play( + FlipCoin(coins[1]), + active_dot.move_to, dots[0], + key.match_x, board[0], + high_rect.match_style, low_rect, + low_rect.match_style, high_rect, + ) + self.wait() + + +class ThreeSquareCase(ThreeDScene): + CONFIG = { + "coin_names": ["c_0", "c_1", "c_2"] + } + + def construct(self): + # Show sequence of boards + boards = Group( + Chessboard(shape=(1, 2), height=0.25 * 1), + Chessboard(shape=(1, 3), height=0.25 * 1), + Chessboard(shape=(2, 2), height=0.25 * 2), + Chessboard(shape=(8, 8), height=0.25 * 8), + ) + dots = TexMobject("\\dots") + group = Group(*boards[:3], dots, boards[3]) + + group.arrange(RIGHT) + group.set_width(FRAME_WIDTH - 1) + + board_groups = Group() + for board in boards: + board.coins = CoinsOnBoard(board, coin_config={"numeric_labels": True}) + board_groups.add(Group(board, board.coins)) + boards[0].coins.flip_at_random() + boards[1].coins.flip_by_bools([False, True, False]) + boards[2].coins.flip_at_random() + boards[3].coins.flip_by_message("3 Fails!") + + def get_board_transform(i, bgs=board_groups): + return TransformFromCopy( + bgs[i], bgs[i + 1], + path_arc=PI / 2, + run_time=2, + ) + + frame = self.camera.frame + frame.save_state() + frame.scale(0.5) + frame.move_to(boards[:2]) + + self.add(board_groups[0]) + self.play(get_board_transform(0)) + turn_animation_into_updater(Restore(frame, run_time=4)) + self.add(frame) + self.play(get_board_transform(1)) + self.play( + Write(dots), + get_board_transform(2), + ) + self.wait() + + # Isolate 3 square board + board_group = board_groups[1] + board = boards[1] + coins = board.coins + + title = TextMobject("Three square case") + title.set_height(0.7) + title.to_edge(UP) + + board_group.generate_target() + board_group.target.set_width(4) + board_group.target.move_to(DOWN, OUT) + + self.save_state() + self.play( + MoveToTarget(board_group, rate_func=squish_rate_func(smooth, 0.5, 1)), + Write(title), + LaggedStartMap(FadeOut, Group( + board_groups[0], + board_groups[2], + dots, + board_groups[3], + ), lambda m: (m, DOWN)), + run_time=2, + ) + self.wait() + + # Try 0*c0 + 1*c1 + 2*c2 + s_sum = TexMobject( + "0", "\\cdot", self.coin_names[0], "+", + "1", "\\cdot", self.coin_names[1], "+", + "2", "\\cdot", self.coin_names[2], + ) + s_sum.set_height(0.6) + c_sum = s_sum.copy() + s_sum.center().to_edge(UP) + c_sum.next_to(s_sum, DOWN, LARGE_BUFF) + + coin_copies = Group() + for i in range(3): + part = c_sum.get_part_by_tex(self.coin_names[i], substring=False) + coin_copy = coins[i].copy() + coin_copy.set_height(1.2 * c_sum[0].get_height()) + coin_copy.move_to(part) + coin_copy.align_to(c_sum, UP) + part.set_opacity(0) + coin_copies.add(coin_copy) + + self.play( + FadeOut(title), + FadeIn(s_sum[:3]), + FadeIn(c_sum[:2]), + ) + self.play(TransformFromCopy(coins[0], coin_copies[0])) + self.wait() + self.play( + FadeIn(s_sum[3:7]), + FadeIn(c_sum[3:6]), + ) + self.play(TransformFromCopy(coins[1], coin_copies[1])) + self.wait() + self.play( + FadeIn(s_sum[7:11]), + FadeIn(c_sum[7:10]), + ) + self.play(TransformFromCopy(coins[2], coin_copies[2])) + self.wait() + self.add(s_sum, c_sum, coin_copies) + + rhs = VGroup(TexMobject("="), Integer(1)) + rhs.arrange(RIGHT) + rhs[1].set_color(YELLOW) + rhs.match_height(c_sum[0]) + rhs.next_to(c_sum, RIGHT, aligned_edge=UP) + braces = VGroup( + Brace(c_sum[0:3], DOWN), + Brace(c_sum[0:7], DOWN), + Brace(c_sum[0:11], DOWN), + ) + for brace, n in zip(braces, [0, 1, 1]): + brace.add(brace.get_tex(n)) + brace.unlock_triangulation() + self.play(GrowFromCenter(braces[0])) + self.wait() + self.play(ReplacementTransform(braces[0], braces[1])) + self.wait() + self.play(ReplacementTransform(braces[1], braces[2])) + self.play( + TransformFromCopy(braces[2][-1], rhs[1], path_arc=-PI / 2), + Write(rhs[0]), + ) + self.play(FadeOut(braces[2])) + self.wait() + + # Show values of S + s_labels = VGroup(*[ + TexMobject(f"K = {n}") + for n in range(3) + ]) + for label, square in zip(s_labels, board): + label.next_to(square, UP) + label.set_width(0.8 * square.get_width()) + label.set_color(YELLOW) + + key = Key(depth_test=False) + key.set_stroke(BLACK, 3, background=True) + key.set_width(0.8 * board[0].get_width()) + key.move_to(board[0]) + + self.play( + coins.next_to, board, DOWN, + coins.match_z, coins, + board.set_opacity, 0.75, + FadeIn(key), + FadeIn(s_labels[0]) + ) + self.wait(0.5) + for i in (1, 2): + self.play( + ApplyMethod(key.move_to, board[i], path_arc=-45 * DEGREES), + s_labels[i - 1].set_fill, GREY, 0.25, + FadeIn(s_labels[i]), + ) + self.wait(0.5) + + # Mod 3 label + mod3_label = TextMobject("(mod 3)") + mod3_label.match_height(s_sum) + mod3_label.set_color(BLUE) + mod3_label.next_to(s_sum, RIGHT, buff=0.75) + + rhs_rhs = TexMobject("\\equiv 0") + rhs_rhs.match_height(rhs) + rhs_rhs.next_to(rhs, RIGHT) + + self.play(Write(mod3_label)) + self.wait() + rhs[1].unlock_triangulation() + self.play( + FlipCoin(coins[2]), + FlipCoin(coin_copies[2]), + rhs[1].set_value, 3, + ) + self.wait() + self.play(Write(rhs_rhs)) + self.wait() + self.play( + rhs[1].set_value, 0, + FadeOut(rhs_rhs) + ) + + # Show a few flips + for i in [2, 1, 0, 2, 1, 2, 0]: + bools = coins.get_bools() + bools[i] = not bools[i] + new_sum = sum([n * b for n, b in enumerate(bools)]) % 3 + self.play( + FlipCoin(coins[i]), + FlipCoin(coin_copies[i]), + rhs[1].set_value, new_sum, + ) + self.wait() + + # Show general sum + general_sum = TexMobject(r"\sum ^{63}_{n=0}n\cdot c_n") + mod_64 = TextMobject("(mod 64)") + mod_64.next_to(general_sum, DOWN) + general_sum.add(mod_64) + general_sum.to_corner(UL) + + self.play(FadeIn(general_sum)) + self.wait() + self.play(FadeOut(general_sum)) + + # Walk through 010 example + board.flip_by_bools([False, False, True]) + self.play( + s_labels[2].set_fill, GREY, 0.25, + s_labels[0].set_fill, YELLOW, 1, + ApplyMethod(key.move_to, board[0], path_arc=30 * DEGREES) + ) + self.wait() + self.play( + FlipCoin(coins[1]), + FlipCoin(coin_copies[1]), + rhs[1].set_value, 0, + ) + self.wait() + square = Square() + square.set_stroke(YELLOW, 3) + square.replace(board[0]) + square[0].move_to(board[0], OUT) + self.play(ShowCreation(square)) + self.wait() + self.play(FadeOut(square)) + + # Walk through alternate flip on 010 example + self.play( + FlipCoin(coins[1]), + FlipCoin(coin_copies[1]), + rhs[1].set_value, 1, + ) + + morty = Mortimer(height=1.5, mode="hooray") + morty.to_corner(DR) + bubble = SpeechBubble(height=2, width=2) + bubble.pin_to(morty) + bubble.write("There's another\\\\way!") + + self.play( + FadeIn(morty), + ShowCreation(bubble), + Write(bubble.content, run_time=1), + ) + self.play(Blink(morty)) + self.play( + FadeOut(VGroup(morty, bubble, bubble.content)) + ) + + self.play( + FlipCoin(coins[2]), + FlipCoin(coin_copies[2]), + rhs[1].set_value, 3, + ) + self.wait() + self.play(rhs[1].set_value, 0) + self.wait() + + +class ThreeSquaresABC(ThreeSquareCase): + CONFIG = { + "coin_names": ["a", "b", "c"] + } + + +class FailedMod3Addition(Scene): + def construct(self): + coin = Coin(height=0.5, numeric_labels=True) + csum = Group( + TexMobject("0 \\cdot"), + coin.deepcopy().flip(), + TexMobject(" + 1 \\cdot"), + coin.deepcopy().flip(), + TexMobject("+ 2 \\cdot"), + coin.deepcopy(), + TexMobject("="), + Integer(2, color=YELLOW), + ) + csum.arrange(RIGHT, buff=SMALL_BUFF) + csum[-1].unlock_triangulation() + csum[-1].shift(SMALL_BUFF * RIGHT) + coins = csum[1:7:2] + csum[-1].add_updater(lambda m, coins=coins: m.set_value(coins[1].is_heads() + 2 * coins[2].is_heads())) + + self.add(csum) + + for coin in coins[::-1]: + rect = SurroundingRectangle(coin) + self.play(ShowCreation(rect)) + self.play(FlipCoin(coin)) + self.wait() + self.play(FlipCoin(coin), FadeOut(rect)) + self.wait() + + self.embed() + + +class TreeOfThreeFlips(ThreeDScene): + def construct(self): + # Setup sums + csum = Group( + TexMobject("0 \\cdot"), + Coin(numeric_labels=True), + TexMobject("+\\,1 \\cdot"), + Coin(numeric_labels=True), + TexMobject("+\\,2 \\cdot"), + Coin(numeric_labels=True), + TexMobject("="), + Integer(0) + ) + csum.coins = csum[1:7:2] + csum.coins.set_height(1.5 * csum[0].get_height()) + csum.coins.flip(RIGHT) + csum.coins[1].flip(RIGHT) + csum.arrange(RIGHT, buff=0.1) + csum[-1].align_to(csum[0], DOWN) + csum[-1].shift(SMALL_BUFF * RIGHT) + csum.to_edge(LEFT) + + csum_rect = SurroundingRectangle(csum, buff=SMALL_BUFF) + csum_rect.set_stroke(WHITE, 1) + + # Set rhs values + def set_rhs_target(cs, colors=[RED, GREEN, BLUE]): + bools = [c.is_heads() for c in cs.coins] + value = sum([n * b for n, b in enumerate(bools)]) % 3 + cs[-1].unlock_triangulation() + cs[-1].generate_target() + cs[-1].target.set_value(value) + cs[-1].target.set_color(colors[value]) + return cs[-1] + + rhs = set_rhs_target(csum) + rhs.become(rhs.target) + + # Create copies + new_csums = Group() + for i in range(3): + new_csum = csum.deepcopy() + new_csum.coins = new_csum[1:7:2] + new_csums.add(new_csum) + new_csums.arrange(DOWN, buff=1.5) + new_csums.next_to(csum, RIGHT, buff=3) + + # Arrows + arrows = VGroup() + for i, ncs in enumerate(new_csums): + arrow = Arrow(csum_rect.get_right(), ncs.get_left()) + label = TextMobject(f"Flip coin {i}") + label.set_height(0.3) + label.set_fill(GREY_A) + label.set_stroke(BLACK, 3, background=True) + label.next_to(ORIGIN, UP, buff=0) + label.rotate(arrow.get_angle(), about_point=ORIGIN) + label.shift(arrow.get_center()) + arrow.label = label + arrows.add(arrow) + arrows.set_color(GREY) + + # Initial state label + is_label = TextMobject( + "Initial state: 010", + tex_to_color_map={"0": RED_D, "1": GOLD_D} + ) + is_label.set_height(0.4) + is_label.next_to(csum_rect, UP, aligned_edge=LEFT) + + # Show three flips + self.add(csum) + self.add(csum_rect) + self.add(is_label) + self.wait() + + anims = [] + for i, arrow, ncs in zip(it.count(), arrows, new_csums): + anims += [ + GrowArrow(arrow), + FadeIn(arrow.label, lag_ratio=0.2), + ] + self.play(LaggedStart(*anims)) + for indices in [[0], [1, 2]]: + self.play(*[ + TransformFromCopy(csum, new_csums[i], path_arc=30 * DEGREES, run_time=2) + for i in indices + ]) + self.wait() + for i in indices: + ncs = new_csums[i] + ncs.coins[i].flip() + rhs = set_rhs_target(ncs) + ncs.coins[i].flip() + self.play( + FlipCoin(ncs.coins[i]), + MoveToTarget(rhs) + ) + + # Put key in square 2 + board = Chessboard(shape=(1, 3), square_resolution=(5, 5)) + board.set_gloss(0.5) + board.set_width(3) + board.set_depth(0.25, stretch=True) + board.space_out_submobjects(factor=1.001) + board.next_to(ORIGIN, LEFT) + board.to_edge(UP) + board.shift(IN) + board.rotate(60 * DEGREES, LEFT) + opening_square = board[2][0] + opening_square_top = opening_square.get_corner(UP + IN) + + key = Key() + key.to_corner(UL, buff=LARGE_BUFF) + key.shift(OUT) + key.generate_target() + key.target.scale(0.3) + key.target.rotate(60 * DEGREES, LEFT) + key.target.move_to(board[2][0]) + + self.play( + FadeIn(board, DOWN), + FadeIn(key) + ) + self.play( + MoveToTarget(key, path_arc=30 * DEGREES), + Rotate(opening_square, 90 * DEGREES, LEFT, about_point=opening_square_top), + ) + self.play( + Rotate(opening_square, 90 * DEGREES, RIGHT, about_point=opening_square_top), + key.next_to, board[1], RIGHT, buff=0.01, + ) + self.wait() + self.remove(key) + self.play(Rotate(board, 0 * DEGREES, RIGHT, run_time=0)) + self.play(Rotate(board, 60 * DEGREES, RIGHT)) + + # Put coins on + coins = csum.coins.copy() + for coin, cube in zip(coins, board): + coin.generate_target() + coin.target.next_to(cube, OUT, buff=0) + self.play(LaggedStartMap(MoveToTarget, coins, run_time=2)) + self.wait() + + +class SeventyFivePercentChance(Scene): + def construct(self): + # Setup column + rows = [] + n_shown = 5 + coins = Group() + nums = VGroup() + for n in it.chain(range(n_shown), range(64 - n_shown, 64)): + coin = Coin(numeric_labels=True) + coin.set_height(0.7) + if (random.random() < 0.5 or (n == 2)) and (n != 62): + coin.flip() + num = Integer(n) + row = Group( + coin, + TexMobject("\\cdot"), + num, + TexMobject("+"), + ) + VGroup(*row[1:]).set_stroke(BLACK, 3, background=True) + row.arrange(RIGHT, buff=MED_SMALL_BUFF) + rows.append(row) + coins.add(coin) + nums.add(num) + + vdots = TexMobject("\\vdots") + rows = Group(*rows[:n_shown], vdots, *rows[n_shown:]) + rows.arrange(DOWN, buff=MED_SMALL_BUFF, aligned_edge=LEFT) + vdots.match_x(rows[0][2]) + rows.set_height(7) + rows.to_edge(RIGHT) + rows[-1][-1].set_opacity(0) + + nums = VGroup(*nums[:n_shown], vdots, *nums[n_shown:]) + self.play(Write(nums)) + self.wait() + self.play( + LaggedStartMap(FadeIn, rows, lag_ratio=0.1, run_time=3), + Animation(nums.copy(), remover=True), + ) + self.wait() + + # Show desired sums + brace = Brace(rows, LEFT) + b_label = brace.get_text("Sum mod 64") + sum_label = TextMobject("=\\, 53 (say)") + sum_label.next_to(b_label, DOWN) + want_label = TextMobject("Need to encode 55 (say)") + want_label.next_to(sum_label, DOWN, buff=0.25, aligned_edge=RIGHT) + want_label.set_color(YELLOW) + need_label = TextMobject("Must add 2") + need_label.next_to(want_label, DOWN, buff=0.25) + need_label.set_color(BLUE) + + for label in b_label, sum_label, want_label, need_label: + label.set_stroke(BLACK, 7, background=True) + + self.play( + GrowFromCenter(brace), + FadeIn(b_label, RIGHT) + ) + self.wait(2) + self.play(FadeIn(sum_label, 0.25 * UP)) + self.wait(2) + self.play(LaggedStart( + FadeIn(want_label, UP), + FadeIn(need_label, UP), + lag_ratio=0.3 + )) + self.wait() + + # Show attempts + s_rect = SurroundingRectangle(rows[2]) + + self.play(ShowCreation(s_rect)) + self.wait() + self.play(FlipCoin(rows[2][0])) + self.wait(2) + + self.play( + s_rect.move_to, rows[-2], + s_rect.stretch, 1.1, 0, + ) + self.wait() + self.play(FlipCoin(rows[-2][0])) + self.wait() + + +class ModNStrategy(ThreeDScene): + def construct(self): + # Board + n_shown = 5 + board = Chessboard() + coins = CoinsOnBoard(board, coin_config={"numeric_labels": True}) + coins.flip_by_message(r"75% odds") + + nums = VGroup() + for n, square in enumerate(board): + num = Integer(n) + num.set_height(0.4 * square.get_height()) + num.next_to(square, OUT, buff=0) + nums.add(num) + nums.set_stroke(BLACK, 3, background=True) + + coins.generate_target() + for coin in coins.target: + coin.set_opacity(0.2) + coin[-2:].set_opacity(0) + + self.add(board) + self.add(coins) + self.wait() + self.play( + MoveToTarget(coins), + FadeIn(nums, lag_ratio=0.1) + ) + self.wait() + + # # Compress + # square_groups = Group(*[ + # Group(square, coin, num) + # for square, coin, num in zip(board, coins, nums) + # ]) + # segments = Group( + # square_groups[:n_shown], + # square_groups[n_shown:-n_shown], + # square_groups[-n_shown:], + # ) + # segments.generate_target() + # dots = TexMobject("\\cdots") + # dots.center() + # segments.target[0].next_to(dots, LEFT) + # segments.target[2].next_to(dots, RIGHT) + # segments.target[1].scale(0) + # segments.target[1].move_to(dots) + + # self.play( + # Write(dots), + # MoveToTarget(segments), + # ) + # self.wait() + # self.remove(segments[1]) + + # # Raise coins + # coins = Group(*coins[:n_shown], *coins[-n_shown:]) + # nums = VGroup(*nums[:n_shown], *nums[-n_shown:]) + # board = Group(*board[:n_shown], *board[-n_shown:]) + # self.play( + # coins.shift, UP, + # coins.set_opacity, 1, + # ) + + # Setup sum + mid_coins = coins[n_shown:-n_shown] + mid_nums = nums[n_shown:-n_shown] + coins = Group(*coins[:n_shown], *coins[-n_shown:]) + nums = VGroup(*nums[:n_shown], *nums[-n_shown:]) + nums.generate_target() + coins.generate_target() + coins.target.set_opacity(1) + + full_sum = Group() + to_fade_in = VGroup() + for num, coin in zip(nums.target, coins.target): + coin.set_height(0.7) + num.set_height(0.5) + summand = Group( + coin, + TexMobject("\\cdot"), + num, + TexMobject("+"), + ) + to_fade_in.add(summand[1], summand[3]) + VGroup(*summand[1:]).set_stroke(BLACK, 3, background=True) + summand.arrange(RIGHT, buff=MED_SMALL_BUFF) + + full_sum.add(summand) + + dots = TexMobject("\\dots") + full_sum = Group(*full_sum[:n_shown], dots, *full_sum[n_shown:]) + full_sum.arrange(RIGHT, buff=MED_SMALL_BUFF) + full_sum.set_width(FRAME_WIDTH - 1) + full_sum[-1][-1].scale(0, about_edge=LEFT) + full_sum.move_to(UP) + + brace = Brace(full_sum, DOWN) + s_label = VGroup( + TextMobject("Sum (mod 64) = "), + Integer(53), + ) + s_label[1].set_color(BLUE) + s_label[1].match_height(s_label[0][0][0]) + s_label.arrange(RIGHT) + s_label[1].align_to(s_label[0][0][0], DOWN) + s_label.next_to(brace, DOWN) + + words = TextMobject("Can't know if a flip will add or subtract") + words.to_edge(UP) + + for mob in mid_coins, mid_nums: + mob.generate_target() + mob.target.move_to(dots) + mob.target.scale(0) + mob.target.set_opacity(0) + + self.play( + FadeOut(board, IN), + MoveToTarget(mid_coins, remover=True), + MoveToTarget(mid_nums, remover=True), + MoveToTarget(nums), + MoveToTarget(coins), + Write(dots), + FadeIn(to_fade_in, lag_ratio=0.1), + run_time=2 + ) + self.play( + GrowFromCenter(brace), + FadeIn(s_label, 0.25 * UP) + ) + self.wait() + + self.play(Write(words, run_time=1)) + self.wait() + + # Do some flips + s_label[1].add_updater(lambda m: m.set_value(m.get_value() % 64)) + for x in range(10): + n = random.randint(-n_shown, n_shown - 1) + coin = coins[n] + n = n % 64 + diff_label = Integer(n, include_sign=True) + if not coin.is_heads(): + diff_label.set_color(GREEN) + else: + diff_label.set_color(RED) + diff_label.set_value(-diff_label.get_value()) + diff_label.next_to(coin, UP, aligned_edge=LEFT) + self.play( + ChangeDecimalToValue( + s_label[1], + s_label[1].get_value() + n, + rate_func=squish_rate_func(smooth, 0.5, 1) + ), + FlipCoin(coin), + FadeIn(diff_label, 0.5 * DOWN) + ) + self.play(FadeOut(diff_label)) + self.wait() + + +class ShowCube(ThreeDScene): + def construct(self): + # Camera stuffs + frame = self.camera.frame + light = self.camera.light_source + light.move_to([-10, -10, 20]) + + # Plane and axes + plane = NumberPlane( + x_range=(-2, 2, 1), + y_range=(-2, 2, 1), + height=15, + width=15, + faded_line_ratio=3, + axis_config={"include_tip": False} + ) + plane.add_coordinate_labels() + plane.coordinate_labels.set_stroke(width=0) + axes = ThreeDAxes( + x_range=(-2, 2, 1), + y_range=(-2, 2, 1), + z_range=(-2, 2, 1), + height=15, + width=15, + depth=15, + ) + axes.apply_depth_test() + + # Vertices and edges + vert_coords = [ + (n % 2, (n // 2) % 2, (n // 4) % 2) + for n in range(8) + ] + verts = [] + coord_labels = VGroup() + coord_labels_2d = VGroup() + for coords in vert_coords: + vert = axes.c2p(*coords) + verts.append(vert) + x, y, z = coords + label = TexMobject(f"({x}, {y}, {z})") + label.set_height(0.3) + label.next_to(vert, UR, SMALL_BUFF) + label.rotate(89 * DEGREES, RIGHT, about_point=vert) + coord_labels.add(label) + if z == 0: + label_2d = TexMobject(f"({x}, {y})") + label_2d.set_height(0.3) + label_2d.next_to(vert, UR, SMALL_BUFF) + coord_labels_2d.add(label_2d) + + edge_indices = [ + (0, 1), (0, 2), (1, 3), (2, 3), + (0, 4), (1, 5), (2, 6), (3, 7), + (4, 5), (4, 6), (5, 7), (6, 7), + ] + + # Vertex and edge drawings + spheres = SGroup() + for vert in verts: + sphere = Sphere( + radius=0.1, + resolution=(9, 9), + ) + sphere.set_gloss(0.3) + sphere.set_color(GREY) + sphere.move_to(vert) + spheres.add(sphere) + + edges = SGroup() + for i, j in edge_indices: + edge = Line3D( + verts[i], verts[j], + resolution=(5, 51), + width=0.04, + gloss=0.5, + ) + edge.set_color(GREY_BROWN) + edges.add(edge) + + # Setup highlight animations + def highlight(n, spheres=spheres, coord_labels=coord_labels): + anims = [] + for k, sphere, cl in zip(it.count(), spheres, coord_labels): + if k == n: + sphere.save_state() + cl.save_state() + sphere.generate_target() + cl.generate_target() + cl.target.set_fill(YELLOW) + sphere.target.set_color(YELLOW) + Group(cl.target, sphere.target).scale(1.5, about_point=sphere.get_center()) + anims += [ + MoveToTarget(sphere), + MoveToTarget(cl), + ] + elif sphere.get_color() == Color(YELLOW): + anims += [ + Restore(sphere), + Restore(cl), + ] + return AnimationGroup(*anims) + + # Setup 2d case + frame.move_to(1.5 * UP) + self.add(plane) + self.play( + LaggedStartMap(FadeIn, coord_labels_2d), + LaggedStartMap(GrowFromCenter, spheres[:4]), + LaggedStartMap(GrowFromCenter, edges[:4]), + ) + self.wait() + + # Transition to 3d case + frame.generate_target() + frame.target.set_rotation(-25 * DEGREES, 70 * DEGREES) + frame.target.move_to([1, 2, 0]) + frame.target.set_height(10) + to_grow = Group(*edges[4:], *spheres[4:], *coord_labels[4:]) + to_grow.save_state() + to_grow.set_depth(0, about_edge=IN, stretch=True) + + rf = squish_rate_func(smooth, 0.5, 1) + self.play( + MoveToTarget(frame), + ShowCreation(axes.z_axis), + Restore(to_grow, rate_func=rf), + FadeOut(coord_labels_2d, rate_func=rf), + *[ + FadeInFromPoint(cl, cl2.get_center(), rate_func=squish_rate_func(smooth, 0.5, 1)) + for cl, cl2 in zip(coord_labels[:4], coord_labels_2d) + ], + run_time=3 + ) + + frame.start_time = self.time + frame.scene = self + frame.add_updater(lambda m: m.set_theta( + -25 * DEGREES * math.cos((m.scene.time - m.start_time) * PI / 60) + )) + + self.add(axes.z_axis) + self.add(edges) + self.add(spheres) + + self.play( + LaggedStart(*[Indicate(s, color=GREEN) for s in spheres], run_time=2, lag_ratio=0.1), + LaggedStart(*[Indicate(c, color=GREEN) for c in coord_labels], run_time=2, lag_ratio=0.1), + ) + + # Add chessboard + board = Chessboard( + shape=(1, 3), height=1, + square_resolution=(5, 5), + ) + board.move_to(plane.c2p(-1, 0), DOWN + IN) + coins = CoinsOnBoard(board, coin_config={"numeric_labels": True}) + + self.play( + FadeIn(board), + FadeIn(coins), + highlight(7) + ) + + # Walk along a few edges + for ci in [1, 2, 0, 1, 2, 1, 0, 1]: + coin = coins[ci] + curr_n = sum([(2**k) * c.is_heads() for k, c in enumerate(coins)]) + coin.flip() + new_n = sum([(2**k) * c.is_heads() for k, c in enumerate(coins)]) + coin.flip() + line = Line(verts[curr_n], verts[new_n]) + line.set_stroke(YELLOW, 3) + self.play( + FlipCoin(coin), + highlight(new_n), + ShowCreationThenDestruction(line) + ) + self.wait() + + # Color the corners + self.play( + highlight(-1), + edges.set_color, GREY, 0.5, + ) + + colors = [RED, GREEN, BLUE_D] + title = TextMobject("Strategy", "\\, $\\Leftrightarrow$ \\,", "Coloring") + title[2].set_submobject_colors_by_gradient(*colors) + title.set_stroke(BLACK, 5, background=True) + title.set_height(0.7) + title.to_edge(UP) + title.shift(LEFT) + title.fix_in_frame() + + color_label_templates = [ + TexMobject(char, color=color).rotate(PI / 2, RIGHT).match_depth(coord_labels[0]) + for char, color in zip("RGB", colors) + ] + coord_labels.color_labels = VGroup(*[VMobject() for cl in coord_labels]) + + def get_coloring_animation(ns, + spheres=spheres, + coord_labels=coord_labels, + colors=colors, + color_label_templates=color_label_templates, + ): + anims = [] + new_color_labels = VGroup() + for n, sphere, coord_label, old_color_label in zip(ns, spheres, coord_labels, coord_labels.color_labels): + color = colors[int(n)] + sphere.generate_target() + coord_label.generate_target() + sphere.target.set_color(color) + coord_label.target.set_fill(color) + color_label = color_label_templates[n].copy() + color_label.next_to(coord_label, RIGHT, SMALL_BUFF) + anims += [ + MoveToTarget(sphere), + MoveToTarget(coord_label), + FadeIn(color_label, 0.25 * IN), + FadeOut(old_color_label, 0.25 * OUT), + ] + new_color_labels.add(color_label) + coord_labels.color_labels = new_color_labels + return LaggedStart(*anims, run_time=2) + + self.play( + FadeIn(title, DOWN), + get_coloring_animation(np.random.randint(0, 3, 8)), + ) + self.wait() + for x in range(4): + self.play(get_coloring_animation(np.random.randint(0, 3, 8))) + self.wait() + + # Some specific color examples + S0 = TexMobject("\\text{Key} = 0") + S0.to_edge(LEFT) + S0.shift(UP) + S0.fix_in_frame() + self.play( + FadeIn(S0, DOWN), + get_coloring_animation([0] * 8) + ) + self.wait(5) + + bit_sum = TexMobject("\\text{Key} = \\,&c_0 + c_1") + bit_sum.scale(0.8) + bit_sum.to_edge(LEFT) + bit_sum.shift(UP) + bit_sum.fix_in_frame() + self.play( + FadeIn(bit_sum, DOWN), + FadeOut(S0, UP), + get_coloring_animation([sum(coords[:2]) for coords in vert_coords]) + ) + self.wait(6) + + bit_sum_with_coefs = TexMobject( + "\\text{Key} = \\,&(0\\cdot c_0 + 1\\cdot c_1 + 2\\cdot c_2) \\\\ &\\quad \\mod 3" + ) + bit_sum_with_coefs.scale(0.8) + bit_sum_with_coefs.move_to(bit_sum, LEFT) + bit_sum_with_coefs.fix_in_frame() + self.play( + FadeIn(bit_sum_with_coefs, DOWN), + FadeOut(bit_sum, UP), + get_coloring_animation([np.dot(coords, [0, 1, 2]) % 3 for coords in vert_coords]) + ) + self.wait(4) + + # Focus on (0, 0, 0) + self.play( + FlipCoin(coins), + coord_labels[1:].set_opacity, 0.2, + coord_labels.color_labels[1:].set_opacity, 0.2, + spheres[1:].set_opacity, 0.2, + ) + self.wait(2) + + lines = VGroup() + for n in [1, 2, 4]: + line = Line(verts[0], verts[n], buff=0.1) + line.set_stroke(YELLOW, 3) + coin = coins[int(np.log2(n))] + self.play( + ShowCreationThenDestruction(line), + spheres[n].set_opacity, 1, + coord_labels[n].set_opacity, 1, + coord_labels.color_labels[n].set_opacity, 1, + FlipCoin(coin) + ) + line.reverse_points() + self.add(line, coord_labels) + self.play( + FlipCoin(coin), + ShowCreation(line) + ) + lines.add(line) + self.wait(10) + + # Focus on (0, 1, 0) + self.play( + FlipCoin(coins[1]), + Uncreate(lines[1]), + FadeOut(lines[::2]), + Group( + spheres[0], coord_labels[0], coord_labels.color_labels[0], + spheres[1], coord_labels[1], coord_labels.color_labels[1], + spheres[4], coord_labels[4], coord_labels.color_labels[4], + ).set_opacity, 0.2, + ) + self.wait(3) + + lines = VGroup() + curr_n = 2 + for n in [1, 2, 4]: + new_n = n ^ curr_n + line = Line(verts[curr_n], verts[new_n], buff=0.1) + line.set_stroke(YELLOW, 3) + coin = coins[int(np.log2(n))] + self.play( + ShowCreationThenDestruction(line), + spheres[new_n].set_opacity, 1, + coord_labels[new_n].set_opacity, 1, + coord_labels.color_labels[new_n].set_opacity, 1, + FlipCoin(coin) + ) + line.reverse_points() + self.add(line, coord_labels) + self.play( + FlipCoin(coin), + ShowCreation(line) + ) + lines.add(line) + self.wait(10) + self.play( + LaggedStartMap(Uncreate, lines), + spheres.set_opacity, 1, + coord_labels.set_opacity, 1, + coord_labels.color_labels.set_opacity, 1, + FadeOut(bit_sum_with_coefs), + ) + self.wait() + for x in range(8): + self.play(get_coloring_animation(np.random.randint(0, 3, 8))) + self.wait() + + # Count all strategies + count = TextMobject("$3^8$ total strategies") + count64 = TextMobject("$64^{(2^{64})}$ total strategies") + for words in count, count64: + words.to_edge(LEFT, buff=MED_SMALL_BUFF) + words.shift(UP) + words.fix_in_frame() + + full_board = Chessboard() + full_board.set_height(6) + full_board.next_to(axes.c2p(0, 0, 0), np.array([-1, 1, 1]), buff=0) + full_board.shift(SMALL_BUFF * UP + LEFT) + + full_coins = CoinsOnBoard(full_board, coin_config={"numeric_labels": True}) + full_coins.flip_by_message("64^ 2^64") + + self.play(FadeIn(count, DOWN)) + self.wait(4) + self.remove(board, coins) + frame.clear_updaters() + frame.generate_target() + frame.target.set_rotation(0, 45 * DEGREES) + frame.target.shift(2 * UP) + self.play( + count.shift, UP, + count.set_opacity, 0.5, + ShowIncreasingSubsets(full_board, run_time=4), + ShowIncreasingSubsets(full_coins, run_time=4), + FadeIn(count64, DOWN), + MoveToTarget(frame, run_time=5) + ) + + messages = [ + "Or, use ", + "Burnside", + "to count", + "modulo ", + "symmetry", + ] + for message in messages: + bools = string_to_bools(message) + to_flip = Group() + for head, coin in zip(bools, full_coins): + if head ^ coin.is_heads(): + to_flip.add(coin) + self.play( + LaggedStartMap(FlipCoin, to_flip, run_time=1) + ) + self.wait(0.5) + + frame.generate_target() + frame.target.shift(2 * DOWN) + frame.target.set_rotation(-15 * DEGREES, 70 * DEGREES) + self.play( + MoveToTarget(frame, run_time=3), + LaggedStartMap(FadeOut, full_board), + LaggedStartMap(FadeOut, full_coins), + FadeOut(count), + FadeOut(count64), + ) + frame.add_updater(lambda m, dt: m.increment_theta(0.01 * dt)) + self.wait(30) + + +class CubeSupplement(ThreeDScene): + CONFIG = { + "try_different_strategies": False, + } + + def construct(self): + # Map 8 states to square choices + boards = Group(*[Chessboard(shape=(1, 3)) for x in range(8)]) + boards.arrange(DOWN, buff=0.5 * boards[0].get_height()) + boards.set_height(7) + boards.to_edge(LEFT) + + coin_sets = Group(*[ + CoinsOnBoard(board, coin_config={"numeric_labels": True}) + for board in boards + ]) + vert_coords = [[n // 4, (n // 2) % 2, n % 2] for n in range(7, -1, -1)] + for coords, coins in zip(vert_coords, coin_sets): + coins.flip_by_bools(coords) + + def get_choice_boards(values, boards): + choices = VGroup() + for value, board in zip(values, boards): + choice = VGroup(*[Square() for x in range(3)]) + choice.arrange(RIGHT, buff=0) + choice.match_height(board) + choice.next_to(board, RIGHT, buff=1.25) + choice.set_fill(GREY_D, 1) + choice.set_stroke(WHITE, 1) + choice[value].set_fill(TEAL) + choices.add(choice) + return choices + + colors = [RED, GREEN, BLUE_D] + color_words = ["Red", "Green", "Blue"] + s_values = [sum([n * v for n, v in enumerate(cs)]) % 3 for cs in vert_coords] + choice_boards = get_choice_boards(s_values, boards) + c_labels = VGroup() + s_arrows = VGroup() + for value, board, choice_board in zip(s_values, boards, choice_boards): + arrow = Vector(RIGHT) + arrow.next_to(board, RIGHT, SMALL_BUFF) + c_label = TextMobject(color_words[value], color=colors[value]) + c_label.next_to(choice_board, RIGHT) + c_labels.add(c_label) + s_arrows.add(arrow) + + choice_board.generate_target() + choice_board.target[value].set_fill(colors[value]) + + self.play( + LaggedStartMap(FadeIn, boards, lag_ratio=0.25), + LaggedStartMap(FadeIn, coin_sets, lag_ratio=0.25), + run_time=3 + ) + self.play( + LaggedStartMap(GrowArrow, s_arrows, lag_ratio=0.25), + LaggedStartMap(FadeIn, choice_boards, lambda m: (m, LEFT), lag_ratio=0.25), + ) + self.wait() + + # Fork + if self.try_different_strategies: + for x in range(5): + values = list(np.arange(8) % 3) + random.shuffle(values) + new_cboards = get_choice_boards(values, boards) + self.play( + LaggedStartMap(FadeOut, choice_boards, lambda m: (m, 0.25 * UP)), + LaggedStartMap(FadeIn, new_cboards, lambda m: (m, 0.25 * DOWN)), + ) + choice_boards = new_cboards + self.wait(2) + + else: + # Associate choices with colors + self.play( + LaggedStartMap(MoveToTarget, choice_boards), + LaggedStartMap(FadeIn, c_labels), + ) + self.wait() + + +class TryDifferentCaseThreeStrategies(CubeSupplement): + CONFIG = { + "try_different_strategies": True, + } + + +class CubeEdgeDescription(Scene): + CONFIG = { + "camera_config": {"background_color": GREY_E} + } + + def construct(self): + bits = VGroup(*[ + VGroup(*[ + Integer(int(b)) + for b in string_to_bools(char) + ]).arrange(RIGHT, buff=SMALL_BUFF) + for char in "hi" + ]) + bits.arrange(DOWN, buff=LARGE_BUFF) + arrow = Arrow( + bits[0][7].get_bottom(), + bits[1][7].get_top(), + buff=SMALL_BUFF, + tip_config={"length": 0.15, "width": 0.15} + ) + arrow.set_color(BLUE) + words = TextMobject("Bit flip") + words.set_color(BLUE) + words.next_to(arrow, LEFT) + bf_group = VGroup(bits, arrow, words) + parens = TexMobject("()")[0] + parens.scale(2) + parens.match_height(bf_group, stretch=True) + parens[0].next_to(bf_group, LEFT, SMALL_BUFF) + parens[1].next_to(bf_group, RIGHT, SMALL_BUFF) + bf_group.add(parens) + bf_group.to_edge(UP) + + cube_words = TextMobject("Edge of an\\\\n-dimensional cube") + top_group = VGroup( + bf_group, + Vector(RIGHT), + cube_words + ) + top_group.arrange(RIGHT) + top_group.to_edge(UP) + + self.add(bf_group) + bits.unlock_triangulation() + self.play( + TransformFromCopy(*bits), + GrowArrow(arrow), + FadeIn(words, 0.25 * UP) + ) + self.wait() + self.play( + GrowArrow(top_group[1]), + FadeIn(cube_words, LEFT) + ) + self.wait() + + +class EdgeColoringExample(Scene): + def construct(self): + words = VGroup( + TextMobject( + "Color edges\\\\red or blue", + tex_to_color_map={"red": RED, "blue": BLUE} + ), + TextMobject("Prove there is a\\\\monochromatic triangle", alignment=""), + ) + words.arrange(DOWN, buff=LARGE_BUFF, aligned_edge=LEFT) + words.to_edge(RIGHT) + words.to_edge(UP, buff=LARGE_BUFF) + + def get_graph(words=words): + points = compass_directions(6) + points *= 3 + verts = VGroup(*[Dot(p, radius=0.1) for p in points]) + verts.set_fill(GREY_B, 1) + edges = VGroup(*[ + Line(p1, p2, color=random.choice([RED, BLUE])) + for p1, p2 in it.combinations(points, 2) + ]) + graph = VGroup(verts, edges) + graph.set_height(6) + graph.next_to(words, LEFT, LARGE_BUFF) + graph.set_y(0) + graph.set_stroke(background=True) + return graph + + graph = get_graph() + + self.add(words) + self.add(graph) + self.wait() + for x in range(2): + new_graph = get_graph() + self.play( + ShowCreation( + new_graph, lag_ratio=0.1, + run_time=3, + ), + ApplyMethod( + graph[1].set_stroke, None, 0, + run_time=2, + ) + ) + graph = new_graph + self.wait(4) + + +class GrahamsConstantAlt(Scene): + def construct(self): + # lhs = TexMobject("g_{64}", "=") + # lhs[0][1:].scale(0.7, about_edge=DL) + lhs = TexMobject("") + lhs.scale(2) + + rhs = VGroup() + for ndots in [1, 3, 6, 7, 9, 12]: + row = VGroup(*[ + TexMobject("2"), + TexMobject("\\uparrow\\uparrow"), + VGroup(*[ + TexMobject("\\cdot") for x in range(ndots) + ]).arrange(RIGHT, buff=0.2), + TexMobject("\\uparrow\\uparrow"), + TexMobject("3"), + ]) + row.arrange(RIGHT, buff=MED_SMALL_BUFF) + if ndots == 1: + rc = row.get_center() + row[:2].move_to(rc, RIGHT) + row[2].set_opacity(0) + row[3:].move_to(rc, LEFT) + row.add(Brace(row[1:-1], DOWN, buff=SMALL_BUFF)) + rhs.add(row) + rhs.replace_submobject(0, Integer(12)) + # rhs[0][-1].set_opacity(0) + rhs.replace_submobject(3, TexMobject("\\vdots")) + rhs.arrange(UP) + rhs.next_to(lhs, RIGHT) + + rbrace = Brace(rhs[1:], RIGHT) + rbrace_tex = rbrace.get_text("7 times") + + equation = VGroup(lhs, rhs, rbrace, rbrace_tex) + equation.center().to_edge(LEFT, buff=LARGE_BUFF) + + self.add(lhs, rhs[0]) + self.play(TransformFromCopy(rhs[0], rhs[1]),) + self.play(TransformFromCopy(rhs[1], rhs[2])) + self.play( + Write(rhs[3]), + TransformFromCopy(rhs[2], rhs[4]), + ) + self.play( + TransformFromCopy(rhs[4], rhs[5]), + GrowFromCenter(rbrace), + Write(rbrace_tex) + ) + self.wait() + + +class ThinkAboutNewTrick(PiCreatureScene, ThreeDScene): + def construct(self): + randy = self.pi_creature + + board = Chessboard(shape=(1, 3)) + board.set_height(1.5) + coins = CoinsOnBoard(board) + coins.flip_at_random() + + self.add(board, coins) + self.play(randy.change, "confused", board) + + for x in range(4): + self.play(FlipCoin(random.choice(coins))) + if x == 1: + self.play(randy.change, "maybe") + else: + self.wait() + + +class AttemptAColoring(ThreeDScene): + def construct(self): + # Setup cube + short_vert_height = 0.3 + tall_vert_height = 0.4 + + vert_coords = np.array(list(map(int_to_bit_coords, range(8)))) + vert_coords = vert_coords - 0.5 + vert_coords = vert_coords * 4 + vert_coords[:, 2] *= 1.25 # Stretch in the z + cube = Group() + cube.verts = SGroup() + cube.edges = VGroup() + cube.add(cube.verts, cube.edges) + for n, coords in enumerate(vert_coords): + vert = Sphere(resolution=(21, 21)) + vert.set_height(short_vert_height) + vert.rotate(90 * DEGREES, RIGHT) + vert.move_to(coords) + cube.verts.add(vert) + vert.edges = VGroup() + for m, coords2 in enumerate(vert_coords): + if sum(int_to_bit_coords(n ^ m)) == 1: + edge = Line(coords, coords2) + cube.edges.add(edge) + vert.edges.add(edge) + vert.edges.apply_depth_test() + + cube.edges.set_color(GREY) + cube.edges.apply_depth_test() + + cube.rotate(30 * DEGREES, DOWN) + cube.to_edge(RIGHT) + cube.set_height(4) + + self.play( + ShowCreation(cube.edges, lag_ratio=0.1), + LaggedStartMap(FadeInFromLarge, cube.verts, lambda m: (m, 0.2)), + run_time=2, + ) + + # Setup cube color + def get_colored_vertices(values, verts=cube.verts): + color_choices = [RED, GREEN, BLUE_D] + color_label_choices = ["R", "G", "B"] + vert_targets = SGroup() + labels = VGroup() + for n, vert in zip(values, verts): + color = color_choices[n] + v_target = vert.copy() + if n == -1: + v_target.set_height(short_vert_height) + v_target.set_color(GREY) + label = VectorizedPoint() + else: + v_target.set_color(color) + v_target.set_height(tall_vert_height) + label = TexMobject(color_label_choices[n]) + label.set_color(color) + label.set_stroke(BLACK, 3, background=True) + label.next_to(vert, UR, buff=0) + vert_targets.add(v_target) + labels.add(label) + return vert_targets, labels + + new_verts, color_labels = get_colored_vertices(np.arange(0, 8) % 3) + for vert, label in zip(cube.verts, color_labels): + vert.label = label + + self.play( + Transform(cube.verts, new_verts), + Write(color_labels), + run_time=2, + ) + self.wait() + + def get_color_change_animations(values, verts=cube.verts, labels=color_labels, gcv=get_colored_vertices): + new_verts, new_labels = gcv(values) + old_labels = labels.copy() + labels.become(new_labels) + return [ + Transform(verts, new_verts), + LaggedStartMap(FadeOut, old_labels, lambda m: (m, 0.5 * UP), lag_ratio=0.03), + LaggedStartMap(FadeIn, labels, lambda m: (m, 0.5 * DOWN), lag_ratio=0.03), + ] + + # Prepare a few colorings + mod3_strategy = [ + np.dot(int_to_bit_coords(n), [0, 1, 2]) % 3 + for n in range(8) + ] + sum_bits = [sum(int_to_bit_coords(n)) % 3 for n in range(8)] + + self.play(*get_color_change_animations(sum_bits)) + self.wait() + self.play(*get_color_change_animations(mod3_strategy)) + self.wait() + + # Pull out vertices with their neighbors + # first just one, then all of them. + trees = Group() + tree_targets = Group() + for n, vert in enumerate(cube.verts): + tree = Group() + tree.root = vert.copy() + tree.root.origin = tree.root.get_center() + tree.edges = VGroup() + tree.leafs = Group() + tree.labels = Group() + for mask in [1, 2, 4]: + leaf = cube.verts[n ^ mask] + leaf.origin = leaf.get_center() + label = leaf.label.copy() + label.original = leaf.label + tree.edges.add(Line(vert.get_center(), leaf.get_center())) + tree.leafs.add(leaf.copy()) + tree.labels.add(label) + tree.edges.apply_depth_test() + tree.edges.match_style(vert.edges) + tree.edges.save_state() + tree.add(tree.root, tree.edges, tree.leafs, tree.labels) + trees.add(tree) + + tree.generate_target(use_deepcopy=True) + for edge, leaf, label, y in zip(tree.target.edges, tree.target.leafs, tree.target.labels, [0.4, 0, -0.4]): + start = vert.get_center() + end = start + RIGHT + y * UP + edge.set_points_as_corners([start, end]) + leaf.move_to(edge.get_end()) + label.next_to(leaf, RIGHT, buff=SMALL_BUFF) + label.scale(0.7) + tree_targets.add(tree.target) + tree_targets.arrange_in_grid(4, 2, buff=LARGE_BUFF) + tree_targets[1::2].shift(0.5 * RIGHT) + tree_targets.set_height(6) + tree_targets.center() + tree_targets.to_corner(DL) + + self.play( + MoveToTarget(trees[0]), + run_time=3, + ) + self.wait() + self.play( + LaggedStartMap( + MoveToTarget, trees[1:], + lag_ratio=0.3, + ), + run_time=6, + ) + self.add(trees) + self.wait() + + # Show what we want + want_rect = SurroundingRectangle(trees, buff=MED_SMALL_BUFF) + want_rect.set_stroke(WHITE, 1) + want_label = TextMobject("What we want") + want_label.next_to(want_rect, UP) + + trees.save_state() + anims = [] + for tree in trees: + anims.append(ApplyMethod(tree.root.set_color, GREY)) + colors = [RED, GREEN, BLUE_D] + letters = ["R", "G", "B"] + for color, letter, leaf, label in zip(colors, letters, tree.leafs, tree.labels): + new_label = TextMobject(letter) + new_label.set_fill(color) + new_label.replace(label, dim_to_match=1) + old_label = label.copy() + label.become(new_label) + anims += [ + FadeIn(label, 0.1 * LEFT), + FadeOut(old_label, 0.1 * RIGHT), + ApplyMethod(leaf.set_color, color), + ] + + cube.verts.generate_target() + cube.verts.save_state() + cube.verts.target.set_color(GREY) + for vert in cube.verts.target: + vert.scale(0.75) + self.play( + ShowCreation(want_rect), + Write(want_label), + LaggedStart(*anims, lag_ratio=0.001, run_time=3), + FadeOut(color_labels), + MoveToTarget(cube.verts), + ) + self.add(trees) + self.wait() + + # Try to fit these back onto the cube + # First attempt + def restore_tree(tree, **kwargs): + anims = [] + for mob in [tree.root, *tree.leafs]: + anims.append(ApplyMethod(mob.move_to, mob.origin)) + for label in tree.labels: + label.generate_target() + label.target.replace(label.original, dim_to_match=1) + anims.append(MoveToTarget(label)) + anims.append(Restore(tree.edges)) + return AnimationGroup(*anims, **kwargs) + + tree_copies = trees.deepcopy() + self.play(restore_tree(tree_copies[0], run_time=2)) + self.wait() + self.play(restore_tree(tree_copies[1], run_time=2)) + self.wait() + + frame = self.camera.frame + self.play( + UpdateFromAlphaFunc( + frame, + lambda m, a: m.move_to(0.1 * wiggle(a, 6) * RIGHT), + ), + FadeOut(tree_copies[0]), + FadeOut(tree_copies[1]), + ) + + # Second attempt + def restore_vertex(n, verts=cube.verts, labels=color_labels): + return AnimationGroup( + Transform(verts[n], verts.saved_state[n]), + FadeIn(labels[n], DOWN) + ) + + for i in [0, 4, 2, 1]: + self.play(restore_vertex(i)) + self.wait() + self.play(ShowCreationThenFadeAround(cube.verts[4])) + for i in [6, 5]: + self.play(restore_vertex(i)) + self.wait() + + q_marks = VGroup(*[TexMobject("???") for x in range(2)]) + q_marks[0].next_to(cube.verts[7], UP, SMALL_BUFF) + q_marks[1].next_to(cube.verts[3], UP, SMALL_BUFF) + self.play(Write(q_marks)) + self.wait() + + # Mention it'll never work + nv_label = TextMobject("It'll never work!") + nv_label.set_height(0.5) + nv_label.next_to(cube, UP, buff=0.75) + + cube_copy = cube.deepcopy() + self.remove(cube) + self.add(cube_copy) + new_verts, new_labels = get_colored_vertices([-1] * 8) + self.play( + Transform(cube_copy.verts, new_verts), + FadeOut(q_marks), + FadeOut(color_labels[:3]), + FadeOut(color_labels[4:7]), + ) + self.add(cube_copy) + self.play(FadeIn(nv_label, DOWN)) + for vert in cube_copy.verts: + vert.generate_target() + vert.target.scale(0.01) + vert.target.set_opacity(0) + self.play( + LaggedStartMap(Uncreate, cube_copy.edges), + LaggedStartMap(MoveToTarget, cube_copy.verts), + ) + + # Highlight symmetry + rects = VGroup() + for tree in trees: + t_rect = SurroundingRectangle( + Group(tree.leafs, tree.labels), + buff=SMALL_BUFF + ) + t_rect.set_stroke(YELLOW, 2) + rects.add(t_rect) + + self.play(LaggedStartMap(ShowCreationThenFadeOut, rects, lag_ratio=0.025, run_time=3)) + self.wait() + + # Show implication + implies = TexMobject("\\Rightarrow") + implies.set_height(0.7) + implies.next_to(want_rect, RIGHT) + number_labels = VGroup(*[ + TextMobject("Number of ", f"{color} vertices") + for color in ["red", "green", "blue"] + ]) + for color, label in zip(colors, number_labels): + label[1].set_color(color) + number_labels.set_height(0.5) + number_labels.arrange(DOWN, buff=1.5, aligned_edge=LEFT) + number_labels.next_to(implies, RIGHT, MED_LARGE_BUFF) + + vert_eqs = VGroup(*[TexMobject("=") for x in range(2)]) + vert_eqs.scale(1.5) + vert_eqs.rotate(90 * DEGREES) + vert_eqs[0].move_to(number_labels[0:2]) + vert_eqs[1].move_to(number_labels[1:3]) + + rhss = VGroup() + for label in number_labels: + rhs = TexMobject("= \\frac{8}{3}") + rhs.scale(1.25) + rhs.next_to(label, RIGHT) + rhss.add(rhs) + + self.play( + Write(implies), + FadeOut(nv_label), + ) + self.play( + GrowFromCenter(vert_eqs), + FadeIn(number_labels[0], DOWN), + FadeIn(number_labels[1]), + FadeIn(number_labels[2], UP), + ) + self.wait() + self.play(Write(rhss)) + self.wait(2) + self.play( + LaggedStartMap( + FadeOut, VGroup(*number_labels, *vert_eqs, *rhss, *implies), + ), + ShowCreation(cube.edges, lag_ratio=0.1), + LaggedStartMap(FadeInFromLarge, cube.verts, lambda m: (m, 0.2)), + ) + self.add(cube) + new_verts, color_labels = get_colored_vertices(mod3_strategy) + true_trees = trees.saved_state + self.play( + Transform(cube.verts, new_verts), + FadeIn(color_labels), + FadeOut(trees), + FadeOut(want_label) + ) + self.play(FadeIn(true_trees)) + self.wait() + + # Count colors + for edge in cube.edges: + edge.insert_n_curves(10) + + red_total = Integer(height=0.6) + red_total.next_to(want_rect, UP) + red_total.set_color(RED) + self.play(FadeIn(red_total)) + + all_label_rects = VGroup() + for n in range(8): + tree = true_trees[n] + vert = cube.verts[n] + neighbor_highlights = VGroup() + new_edges = VGroup() + label_rects = VGroup() + for mask, label in zip([1, 2, 4], tree.labels): + neighbor = cube.verts[n ^ mask] + edge = Line(vert, neighbor, buff=0) + edge.set_stroke(YELLOW, 5) + edge.insert_n_curves(10) + new_edges.add(edge) + if neighbor.get_color() == Color(RED): + circ = Circle() + circ.set_stroke(YELLOW, 3) + circ.replace(neighbor) + neighbor_highlights.add(circ) + rect = SurroundingRectangle(label, buff=0.025) + rect.set_stroke(YELLOW, 2) + label_rects.add(rect) + new_edges.apply_depth_test() + new_edges.shift(0.01 * OUT) + new_tree_edges = tree.edges.copy() + new_tree_edges.set_stroke(YELLOW, 3) + new_tree_edges.shift(0.01 * OUT) + + self.play( + *map(ShowCreation, [*new_edges, *new_tree_edges]), + ) + for highlight, rect in zip(neighbor_highlights, label_rects): + self.play( + FadeInFromLarge(highlight, 1.2), + FadeInFromLarge(rect, 1.2), + run_time=0.25 + ) + red_total.increment_value() + self.wait(0.25) + + self.play( + FadeOut(neighbor_highlights), + FadeOut(new_edges), + FadeOut(new_tree_edges), + ) + all_label_rects.add(*label_rects) + self.wait() + + # Show count to 8 + new_verts = get_colored_vertices([-1] * 8)[0] + self.play( + FadeOut(true_trees), + FadeOut(all_label_rects), + FadeOut(red_total), + FadeOut(color_labels), + Transform(cube.verts, new_verts), + ) + self.play(FadeIn(trees)) + label_rects = VGroup() + for tree in trees: + rect = SurroundingRectangle(tree.labels[0], buff=0.025) + rect.match_style(all_label_rects[0]) + label_rects.add(rect) + + self.play( + ShowIncreasingSubsets(label_rects, rate_func=linear), + UpdateFromFunc( + red_total, lambda m, lr=label_rects: m.set_value(len(lr)) + ) + ) + self.wait() + + # Show red corners + r_verts = SGroup(cube.verts[3], cube.verts[4]).copy() + r_labels = VGroup() + r_edge_groups = VGroup() + for r_vert in r_verts: + r_label = TexMobject("R") + r_label.set_color(RED) + r_label.next_to(r_vert, UR, buff=0) + r_labels.add(r_label) + r_vert.set_height(tall_vert_height) + r_vert.set_color(RED) + edges = VGroup() + for edge in r_vert.edges: + to_r_edge = edge.copy() + to_r_edge.reverse_points() + to_r_edge.set_stroke(YELLOW, 3) + to_r_edge.shift(0.01 * OUT) + edges.add(to_r_edge) + edges.apply_depth_test() + r_edge_groups.add(edges) + + self.play( + LaggedStartMap(FadeInFromLarge, r_verts), + LaggedStartMap(FadeInFromLarge, r_labels), + run_time=1, + ) + self.wait() + for edges in r_edge_groups: + self.play(ShowCreationThenDestruction(edges, lag_ratio=0.1)) + self.wait() + + rhs = TexMobject("=", "3", "\\, (\\text{\\# Red corners})") + rhs[2].set_color(RED) + rhs.match_height(red_total) + rhs[:2].match_height(red_total, about_edge=RIGHT) + rhs.next_to(red_total, RIGHT) + + self.play(Write(rhs)) + self.wait() + + three = rhs[1] + three.generate_target() + three.target.move_to(red_total, RIGHT) + over = TexMobject("/") + over.match_height(three) + over.next_to(three.target, LEFT, MED_SMALL_BUFF) + self.play( + MoveToTarget(three, path_arc=90 * DEGREES), + red_total.next_to, over, LEFT, MED_SMALL_BUFF, + FadeIn(over, UR), + rhs[2].move_to, three, LEFT, + ) + self.wait() + + np_label = TextMobject("Not possible!") + np_label.set_height(0.6) + np_label.next_to(rhs, RIGHT, LARGE_BUFF) + self.play(Write(np_label)) + self.wait() + + +class TryTheProofYourself(TeacherStudentsScene): + def construct(self): + self.teacher_says( + "Can you predict\\\\the proof?", + target_mode="hooray", + bubble_kwargs={ + "height": 3, + "width": 3, + }, + ) + self.teacher.bubble.set_fill(opacity=0) + self.change_student_modes( + "pondering", "thinking", "confused", + look_at_arg=self.screen, + ) + self.wait(3) + self.change_student_modes("thinking", "pondering", "erm", look_at_arg=self.screen) + self.wait(4) + self.change_student_modes("tease", "pondering", "thinking", look_at_arg=self.screen) + self.wait(5) + + +class HighDimensionalCount(ThreeDScene): + def construct(self): + # Definitions + N = 6 + colors = [RED, GREEN, BLUE_D, YELLOW, PINK, TEAL] + coords = np.array([0, 1, 1, 1, 0, 0]) + + # Add chess board + board = Chessboard(shape=(2, 3)) + board.set_height(2) + board.to_corner(UL) + + grid = NumberPlane( + x_range=(0, 3), y_range=(0, 2), + faded_line_ratio=0 + ) + grid.match_height(board) + grid.match_width(board, stretch=True) + grid.next_to(board, OUT, 1e-8) + grid.set_gloss(0.5) + + coins = CoinsOnBoard(board, coin_config={"numeric_labels": True}) + coins.flip_by_bools(coords) + coin_labels = VGroup() + for i, coin in zip(coords, coins): + coin_labels.add(coin.labels[1 - i]) + + self.play( + ShowCreationThenFadeOut(grid, lag_ratio=0.1), + FadeIn(board), + FadeIn(coins, lag_ratio=0.1), + run_time=2 + ) + + # Setup corners + def get_vert(height=0.4, color=RED): + return get_vertex_sphere(height, color) + + def get_vert_label(coords): + args = ["("] + for coord in coords: + args.append(str(coord)) + args.append(",") + args[-1] = ")" + return TexMobject(*args) + + def get_board_with_highlights(n, height=1, N=N, colors=colors): + board = VGroup(*[Square() for x in range(N)]) + board.arrange_in_grid(2, 3, buff=0) + board.set_fill(GREY_E, 1) + board.set_stroke(WHITE, 1) + board.set_height(height) + board[n].set_fill(colors[n]) + return board + + vert = get_vert() + vert_label = get_vert_label(coords) + vert_board = get_board_with_highlights(0) + vert_label.next_to(vert, LEFT) + vert_board.next_to(vert_label, DOWN, MED_LARGE_BUFF) + neighbors = SGroup() + for color in colors: + neighbors.add(get_vert(color=color)) + neighbors.arrange(DOWN, buff=0.75) + neighbors.next_to(vert, RIGHT, buff=2) + neighbor_labels = VGroup() + edges = VGroup() + neighbor_boards = VGroup() + for n, neighbor in enumerate(neighbors): + edge = Line( + vert.get_center(), + neighbor.get_center(), + buff=vert.get_height() / 2, + ) + new_coords = list(coords) + new_coords[n] ^= 1 + label = get_vert_label(new_coords) + label.next_to(neighbor, RIGHT) + label.add(SurroundingRectangle(label[2 * n + 1], buff=0.05)) + n_board = get_board_with_highlights(n, height=0.7) + n_board.next_to(label, RIGHT) + + neighbor_boards.add(n_board) + edges.add(edge) + neighbor_labels.add(label) + + vertex_group = Group( + vert_board, vert_label, vert, + edges, + neighbors, neighbor_labels, neighbor_boards + ) + vertex_group.to_corner(DL) + + # Show coords with states + cl_mover = coin_labels.copy() + cl_mover.generate_target() + for m1, m2 in zip(cl_mover.target, vert_label[1::2]): + m1.replace(m2) + self.play( + MoveToTarget(cl_mover), + ) + self.play( + FadeIn(vert_label), + FadeOut(cl_mover) + ) + self.play(FadeIn(vert_board)) + self.wait() + self.play( + ShowIncreasingSubsets(neighbor_labels), + ShowCreation(edges), + ) + self.wait() + self.play(LaggedStartMap( + TransformFromCopy, neighbor_boards, + lambda m, b=vert_board: (b, m) + )) + self.wait() + + # Show one vertex + self.play(FadeInFromLarge(vert)) + self.play(LaggedStartMap( + TransformFromCopy, neighbors, + lambda m, v=vert: (v, m) + )) + self.wait() + + # Isolate vertex + edges.apply_depth_test() + tree = Group(vert, edges, neighbors) + tree.generate_target() + tree.target[0].scale(0.5) + tree.target[2].scale(0.5) + tree.target[2].arrange(DOWN, buff=0) + tree.target[2].next_to(vert, RIGHT, MED_LARGE_BUFF) + for edge, nv in zip(tree.target[1], tree.target[2]): + new_edge = Line( + vert.get_center(), + nv.get_center(), + ) + edge.become(new_edge) + edge.set_stroke(WHITE, 2) + tree.target.rotate(-90 * DEGREES) + tree.target.center() + + short_label = vert_label[1::2] + short_label.generate_target() + short_label.target.arrange(RIGHT, buff=SMALL_BUFF) + short_label.target.match_width(tree.target) + short_label.target.next_to(tree.target, UP) + short_label.target.set_fill(GREY_A) + + self.play( + MoveToTarget(tree), + MoveToTarget(short_label), + LaggedStartMap(FadeOut, Group( + vert_label[0::2], + vert_board, + *neighbor_labels, + *neighbor_boards, + *board, + *coins, + )), + run_time=2, + ) + tree.add(short_label) + + # Show all vertices + def get_bit_string(n, template=short_label): + bits = VGroup(*map(Integer, int_to_bit_coords(n, min_dim=6))) + bits.arrange(RIGHT, buff=0.075) + bits.match_height(template) + bits.set_color(GREY_A) + return bits + + new_trees = Group() + for n in [0, 1, 62, 63]: + new_tree = tree.copy() + bits = get_bit_string(n) + bits.move_to(new_tree[3]) + new_tree.replace_submobject(3, bits) + new_trees.add(new_tree) + for new_tree, color in zip(new_trees, [YELLOW, GREEN, RED, BLUE_D]): + new_tree[0].set_color(color) + new_trees.arrange(RIGHT, buff=MED_LARGE_BUFF) + new_trees.move_to(tree) + new_trees[:2].to_edge(LEFT) + new_trees[2:].to_edge(RIGHT) + + dots = VGroup(*[TexMobject("\\dots") for x in range(2)]) + dots.scale(2) + dots[0].move_to(Group(new_trees[1], tree)) + dots[1].move_to(Group(new_trees[2], tree)) + + top_brace = Brace(new_trees, UP, buff=MED_LARGE_BUFF) + total_label = top_brace.get_text("$2^n$ total vertices", buff=MED_LARGE_BUFF) + + low_brace = Brace(tree, DOWN) + neighbors_label = low_brace.get_text("n neighbors") + + self.play( + GrowFromCenter(low_brace), + Write(neighbors_label, run_time=1) + ) + self.wait() + self.play( + GrowFromCenter(dots), + GrowFromCenter(top_brace), + LaggedStartMap( + TransformFromCopy, new_trees, + lambda m, t=tree: (t, m) + ), + Write(total_label, run_time=1), + run_time=2, + ) + self.wait() + + # Count red neighbors + middle_tree = tree + frame = self.camera.frame + self.play(frame.move_to, UP) + + count = Integer(1) + count.set_color(RED) + count.scale(1.5) + count.next_to(total_label, UP, LARGE_BUFF, aligned_edge=LEFT) + two_to_n_label = TexMobject("2^n") + two_to_n_label.scale(1.5) + two_to_n_label.set_color(RED) + two_to_n_label.move_to(count, LEFT) + + n_arrows = VGroup() + for tree in [*new_trees[:2], middle_tree, *new_trees[2:]]: + arrow = Vector( + [-1, -2, 0], + tip_config={"width": 0.2, "length": 0.2} + ) + arrow.match_height(tree[1]) + arrow.next_to(tree[2][0], UR, buff=0) + arrow.set_color(RED) + n_arrows.add(arrow) + + self.add(n_arrows[0], count) + self.wait() + self.add(n_arrows[1]) + count.increment_value() + self.wait() + self.play( + ChangeDecimalToValue(count, 63, rate_func=rush_into), + LaggedStartMap(FadeIn, n_arrows[2:], lag_ratio=0.5), + run_time=3 + ) + self.remove(count) + self.add(two_to_n_label) + self.wait() + + rhs = TexMobject("=", "n", "\\cdot", "(\\text{\\# Red vertices})") + rhs.scale(1.5) + rhs.next_to(two_to_n_label, RIGHT) + rhs.shift(0.05 * DOWN) + rhs.set_color_by_tex("Red", RED) + highlighted_edges = VGroup(*middle_tree[1], new_trees[2][1]).copy() + highlighted_edges.set_stroke(YELLOW, 3) + highlighted_edges.shift(0.01 * OUT) + edge_anim = ShowCreationThenFadeOut( + highlighted_edges, lag_ratio=0.3 + ) + + self.play(edge_anim) + self.play(Write(rhs), run_time=1) + self.play(edge_anim) + self.wait(2) + + # Conclusion + pairs = VGroup(VGroup(TexMobject("n"), TexMobject("2^n"))) + pairs.set_color(YELLOW) + for n in range(1, 10): + pairs.add(VGroup(Integer(n), Integer(2**n))) + for pair in pairs: + pair.arrange(RIGHT, buff=0.75, aligned_edge=DOWN) + line = Line(LEFT, RIGHT) + line.set_stroke(WHITE, 1) + line.set_width(2) + line.next_to(pair, DOWN, aligned_edge=LEFT) + line.shift(SMALL_BUFF * LEFT) + pair.add(line) + pairs.add(pair) + pairs.arrange(DOWN, aligned_edge=LEFT, buff=0.25) + pairs.set_height(7) + pairs.to_edge(LEFT) + pairs.shift(UP) + + marks = VGroup() + for n, pair in zip(it.count(1), pairs[1:]): + if sum(int_to_bit_coords(n)) == 1: + mark = Checkmark() + else: + mark = Exmark() + mark.move_to(pair[1], LEFT) + mark.shift(RIGHT) + marks.add(mark) + + v_line = Line(UP, DOWN) + v_line.set_height(7) + v_line.set_stroke(WHITE, 1) + v_line.set_x((pairs[0][0].get_right() + pairs[0][1].get_left())[0] / 2) + v_line.match_y(pairs) + pairs.add(v_line) + + new_trees.generate_target() + new_trees.target[:2].move_to(middle_tree, RIGHT) + shift_vect = new_trees.target[0].get_center() - new_trees[0].get_center() + + self.play( + MoveToTarget(new_trees), + top_brace.match_width, new_trees.target, {"about_edge": RIGHT}, + total_label.shift, shift_vect * 0.5, + n_arrows[:2].shift, shift_vect, + FadeOut(middle_tree, RIGHT), + FadeOut(n_arrows[2], RIGHT), + FadeOut(dots[0], 2 * RIGHT), + Write(pairs) + ) + self.play(LaggedStartMap( + FadeIn, marks, + lambda m: (m, 0.2 * LEFT), + lag_ratio=0.4, + run_time=5, + )) + self.wait() + + +class SimpleRect(Scene): + def construct(self): + rect = SurroundingRectangle( + VGroup(Integer(4), Integer(16), Integer(0)).arrange(RIGHT, MED_LARGE_BUFF), + ) + self.play(ShowCreation(rect)) + self.wait(2) + self.play(FadeOut(rect)) + + +class WhenIsItHopeless(Scene): + def construct(self): + boards = Group( + Chessboard(shape=(1, 3)), + Chessboard(shape=(2, 2)), + Chessboard(shape=(2, 3)), + Chessboard(shape=(2, 3)), + Chessboard(shape=(2, 4)), + Chessboard(shape=(2, 4)), + Chessboard(shape=(3, 3)), + Chessboard(shape=(3, 4)), + Chessboard(shape=(3, 4)), + Chessboard(shape=(3, 4)), + ) + last_board = None + last_coins = None + last_words = None + for n, board in zip(it.count(3), boards): + board.scale(1 / board[0].get_height()) + coins = CoinsOnBoard(board) + coins.flip_at_random() + diff = len(board) - n + if diff > 0: + board[-diff:].set_opacity(0) + coins[-diff:].set_opacity(0) + + if sum(int_to_bit_coords(n)) == 1: + words = TextMobject("Maybe possible") + words.set_color(GREEN) + else: + words = TextMobject("Futile!") + words.set_color(RED) + words.scale(1.5) + words.next_to(board, UP, MED_LARGE_BUFF) + + if n == 3: + self.play( + FadeIn(board), + FadeIn(coins), + FadeIn(words, DOWN), + ) + else: + self.play( + ReplacementTransform(last_board, board), + ReplacementTransform(last_coins, coins), + FadeOut(last_words), + FadeIn(words, DOWN), + ) + self.wait() + + last_board = board + last_coins = coins + last_words = words + + +class FourDCubeColoringFromTrees(ThreeDScene): + def construct(self): + # Camera stuffs + frame = self.camera.frame + light = self.camera.light_source + light.move_to([-25, -20, 20]) + + # Setup cube + colors = [RED, GREEN, BLUE_D, YELLOW] + cube = self.get_hypercube() + for n, vert in enumerate(cube.verts): + code = boolian_linear_combo(int_to_bit_coords(n, 4)) + cube.verts[n].set_color(colors[code]) + + # Create trees + trees = Group() + original_trees = Group() + for vert in cube.verts: + tree = Group( + vert, + vert.edges, + vert.neighbors, + ).copy() + original = tree.copy() + original[0].set_color(GREY) + original[0].scale(0) + original_trees.add(original) + trees.add(tree) + for tree in trees: + tree[0].set_color(GREY) + tree[0].rotate(90 * DEGREES, LEFT) + sorted_verts = Group(*tree[2]) + sorted_verts.submobjects.sort(key=lambda m: m.get_color().hex) + sorted_verts.arrange(DOWN, buff=SMALL_BUFF) + sorted_verts.next_to(tree[0], RIGHT, buff=0.75) + for edge, neighbor in zip(tree[1], tree[2]): + edge.become(Line3D( + tree[0].get_center(), + neighbor.get_center(), + resolution=edge.resolution, + )) + neighbor.rotate(90 * DEGREES, LEFT) + + trees.arrange_in_grid(4, 4, buff=MED_LARGE_BUFF) + for i in range(4): + trees[i::4].shift(0.5 * i * RIGHT) + trees.center() + trees.set_height(6) + trees.rotate(PI / 2, RIGHT) + trees.move_to(10 * LEFT, LEFT) + + frame.set_phi(90 * DEGREES) + frame.move_to(5 * LEFT) + self.add(trees) + self.wait() + + # Show transition + anims = [] + for tree, original in zip(trees, original_trees): + anims.append(Transform(tree, original)) + self.play( + frame.set_rotation, 20 * DEGREES, 70 * DEGREES, + frame.move_to, ORIGIN, + LaggedStart(*anims, lag_ratio=0.2), + run_time=8, + ) + self.remove(trees) + self.add(cube) + frame.add_updater(lambda m, dt: m.increment_theta(2 * dt * DEGREES)) + self.wait(30) + + def get_hypercube(self, dim=4, width=4): + hc_points = self.get_hypercube_points(dim, width) + cube = Group() + cube.verts = SGroup() + cube.edges = SGroup() + cube.add(cube.verts, cube.edges) + for point in hc_points: + vert = get_vertex_sphere(resolution=(25, 13)) + vert.rotate(PI / 2, UP) + vert.move_to(point) + cube.verts.add(vert) + vert.edges = SGroup() + vert.neighbors = SGroup() + for n in range(2**dim): + for power in range(dim): + k = n ^ (1 << power) + edge = Line3D( + hc_points[n], + hc_points[k], + width=0.05, + resolution=(31, 31) + ) + cube.edges.add(edge) + cube.verts[n].edges.add(edge) + cube.verts[n].neighbors.add(cube.verts[k]) + return cube + + def get_hypercube_points(self, dim=4, width=4): + all_coords = [ + int_to_bit_coords(n, dim).astype(float) + for n in range(2**dim) + ] + vertex_holder = Mobject() + vertex_holder.set_points([ + sum([c * v for c, v in zip(reversed(coords), [RIGHT, UP, OUT])]) + for coords in all_coords + ]) + vertex_holder.center() + if dim == 4: + vertex_holder.points[8:] *= 2 + vertex_holder.set_width(width) + return vertex_holder.points + + +class IntroduceHypercube(FourDCubeColoringFromTrees): + def construct(self): + # Camera stuffs + frame = self.camera.frame + light = self.camera.light_source + light.move_to([-25, -20, 20]) + + # Setup cubes + cubes = [ + self.get_hypercube(dim=d) + for d in range(5) + ] + + def reconnect_edges(cube): + for vert in cube.verts: + for edge, neighbor in zip(vert.edges, vert.neighbors): + edge.become(Line3D( + vert.get_center(), + neighbor.get_center(), + resolution=edge.resolution + )) + + # Show increasing dimensions + label = VGroup(Integer(0), TexMobject("D")) + label.arrange(RIGHT, buff=SMALL_BUFF) + label.scale(1.5) + label.to_edge(UP) + label.fix_in_frame() + + def get_cube_intro_anim(n, cubes=cubes, reconnect_edges=reconnect_edges, label=label): + if n == 0: + return GrowFromCenter(cubes[n]) + self.remove(cubes[n - 1]) + cubes[n].save_state() + for v1, v2 in zip(cubes[n].verts, it.cycle(cubes[n - 1].verts)): + v1.move_to(v2) + reconnect_edges(cubes[n]) + if n == 1: + cubes[n].edges.scale(0) + return AnimationGroup( + Restore(cubes[n]), + ChangeDecimalToValue(label[0], n), + ) + + self.play( + FadeIn(label, DOWN), + get_cube_intro_anim(0) + ) + self.wait() + for n in [1, 2]: + self.play(get_cube_intro_anim(n)) + self.wait() + self.play( + get_cube_intro_anim(3), + ApplyMethod( + frame.set_rotation, -20 * DEGREES, 75 * DEGREES, + run_time=3 + ) + ) + frame.add_updater(lambda m, dt: m.increment_theta(dt * DEGREES)) + self.wait(4) + + # Flatten cube + flat_cube = self.get_hypercube(3) + for n, vert in enumerate(flat_cube.verts): + point = vert.get_center() + if n < 4: + point *= 1.5 + else: + point *= 0.75 + point[2] = 0 + vert.move_to(point) + reconnect_edges(flat_cube) + + plane = NumberPlane(x_range=(-10, 10), y_range=(-10, 10), faded_line_ratio=0) + plane.set_opacity(0.25) + plane.apply_depth_test() + plane.axes.shift(0.01 * OUT) + plane.shift(0.02 * IN) + + cubes[3].save_state() + self.add(cubes[3], plane) + self.play( + FadeIn(plane, run_time=2), + Transform(cubes[3], flat_cube, run_time=2), + ) + self.wait(7) + self.play( + Restore(cubes[3], run_time=2), + FadeOut(plane) + ) + self.play(get_cube_intro_anim(4), run_time=3) + self.wait(10) + + # Highlight some neighbor groups + colors = [RED, GREEN, BLUE_D, YELLOW] + for x in range(6): + vert = random.choice(cubes[4].verts) + neighbors = vert.neighbors.copy() + neighbors.save_state() + neighbors.generate_target() + new_edges = VGroup() + for neighbor, color in zip(neighbors.target, colors): + neighbor.set_color(color) + edge = Line( + vert.get_center(), + neighbor.get_center(), + buff=vert.get_height() / 2, + ) + edge.set_stroke(color, 5) + new_edges.add(edge) + self.remove(vert.neighbors) + self.play( + ShowCreation(new_edges, lag_ratio=0.2), + MoveToTarget(neighbors), + ) + self.wait(1) + self.play( + FadeOut(new_edges), + Restore(neighbors), + ) + self.remove(neighbors) + self.add(vert.neighbors) + + # Show valid coloring + cubes[4].generate_target() + for n, vert in enumerate(cubes[4].target[0]): + code = boolian_linear_combo(int_to_bit_coords(n, 4)) + vert.set_color(colors[code]) + self.play(MoveToTarget(cubes[4], lag_ratio=0.2, run_time=3)) + self.wait(15) + + +# Animations for Matt +class WantAdditionToBeSubtraction(ThreeDScene): + def construct(self): + # Add sum + coins = CoinsOnBoard( + Chessboard(shape=(1, 4)), + coin_config={"numeric_labels": True}, + ) + for coin in coins[0], coins[2]: + coin.flip() + + coefs = VGroup(*[TexMobject(f"X_{i}") for i in range(len(coins))]) + full_sum = Group() + to_fade = VGroup() + for coin, coef in zip(coins, coefs): + coin.set_height(0.7) + coef.set_height(0.5) + summand = Group(coin, TexMobject("\\cdot"), coef, TexMobject("+")) + to_fade.add(*summand[1::2]) + summand.arrange(RIGHT, buff=0.2) + full_sum.add(summand) + full_sum.add(TexMobject("\\dots")) + full_sum.arrange(RIGHT, buff=0.2) + to_fade.add(full_sum[-1]) + + some_label = TextMobject("Some kind of ``numbers''") + some_label.next_to(full_sum, DOWN, buff=2) + arrows = VGroup(*[ + Arrow(some_label.get_top(), coef.get_bottom()) + for coef in coefs + ]) + + for coin in coins: + coin.save_state() + coin.rotate(90 * DEGREES, UP) + coin.set_opacity(0) + + self.play( + LaggedStartMap(Restore, coins, lag_ratio=0.3), + run_time=1 + ) + self.play( + FadeIn(to_fade), + LaggedStartMap(FadeInFromPoint, coefs, lambda m: (m, some_label.get_top())), + LaggedStartMap(GrowArrow, arrows), + Write(some_label, run_time=1) + ) + self.wait() + self.play(FadeOut(some_label), FadeOut(arrows)) + + # Show a flip + add_label = TexMobject("+X_2", color=GREEN) + sub_label = TexMobject("-X_2", color=RED) + for label in add_label, sub_label: + label.next_to(coins[2], UR) + label.match_height(coefs[2]) + self.play( + FlipCoin(coins[2]), + FadeIn(label, 0.5 * DOWN) + ) + self.play(FadeOut(label)) + + # What we want + want_label = TextMobject("Want: ", "$X_i = -X_i$") + eq = TextMobject("$X_i + X_i = 0$") + want_label.next_to(full_sum, DOWN, LARGE_BUFF) + eq.next_to(want_label[1], DOWN, aligned_edge=LEFT) + + self.play(FadeIn(want_label)) + self.wait() + self.play(FadeIn(eq, UP)) + self.wait() + + +class BitVectorSum(ThreeDScene): + def construct(self): + # Setup + board = Chessboard(shape=(1, 4)) + board.set_height(1) + coins = CoinsOnBoard(board, coin_config={"numeric_labels": True}) + coins[2].flip() + + all_coords = [np.array([b0, b1]) for b0, b1 in it.product(range(2), range(2))] + bit_vectors = VGroup(*[ + IntegerMatrix(coords.reshape((2, 1))).set_height(1) + for coords in all_coords + ]) + bit_vectors.arrange(RIGHT, buff=2) + bit_vectors.to_edge(UP) + bit_vectors.set_stroke(BLACK, 4, background=True) + + arrows = VGroup( + Arrow(board[0].get_corner(UL), bit_vectors[0].get_corner(DR)), + Arrow(board[1].get_corner(UP), bit_vectors[1].get_corner(DOWN)), + Arrow(board[2].get_corner(UP), bit_vectors[2].get_corner(DOWN)), + Arrow(board[3].get_corner(UR), bit_vectors[3].get_corner(DL)), + ) + + # Show vectors + self.add(board) + self.add(coins) + for arrow, vector in zip(arrows, bit_vectors): + self.play( + GrowArrow(arrow), + FadeInFromPoint(vector, arrow.get_start()), + ) + self.wait() + + # Move coins + coin_copies = coins.copy() + cdots = VGroup() + plusses = VGroup() + for cc, vector in zip(coin_copies, bit_vectors): + dot = TexMobject("\\cdot") + dot.next_to(vector, LEFT, MED_SMALL_BUFF) + cdots.add(dot) + plus = TexMobject("+") + plus.next_to(vector, RIGHT, MED_SMALL_BUFF) + plusses.add(plus) + cc.next_to(dot, LEFT, MED_SMALL_BUFF) + plusses[-1].set_opacity(0) + + for coin, cc, dot, plus in zip(coins, coin_copies, cdots, plusses): + self.play( + TransformFromCopy(coin, cc), + Write(dot), + ) + self.play(Write(plus)) + self.wait() + + # Show sum + eq = TexMobject("=") + eq.move_to(plusses[-1]) + + def get_rhs(coins=coins, bit_vectors=bit_vectors, all_coords=all_coords, eq=eq): + bit_coords = sum([ + (b * coords) + for coords, b in zip(all_coords, coins.get_bools()) + ]) % 2 + n = bit_coords_to_int(bit_coords) + result = bit_vectors[n].copy() + result.next_to(eq, RIGHT) + result.n = n + return result + + def get_rhs_anim(rhs, bit_vectors=bit_vectors): + bv_copies = bit_vectors.copy() + bv_copies.generate_target() + for bv in bv_copies.target: + bv.move_to(rhs) + bv.set_opacity(0) + bv_copies.target[rhs.n].set_opacity(1) + return AnimationGroup( + MoveToTarget(bv_copies, remover=True), + ShowIncreasingSubsets(Group(rhs), int_func=np.floor) + ) + + rhs = get_rhs() + + mod2_label = TextMobject("(Add mod 2)") + mod2_label.next_to(rhs, DOWN, MED_LARGE_BUFF) + mod2_label.to_edge(RIGHT) + + self.play( + Write(eq), + get_rhs_anim(rhs), + FadeIn(mod2_label), + FadeOut(board), + FadeOut(coins), + FadeOut(arrows), + ) + self.wait(2) + + # Show some flips + for x in range(8): + i = random.randint(0, 3) + rect = SurroundingRectangle(Group(coin_copies[i], bit_vectors[i])) + old_rhs = rhs + coins[i].flip() + rhs = get_rhs() + self.play( + ShowCreation(rect), + FlipCoin(coin_copies[i]), + FadeOut(old_rhs, RIGHT), + FadeIn(rhs, LEFT), + ) + self.play(FadeOut(rect)) + self.wait(2) + + +class ExampleSquareAsBinaryNumber(Scene): + def construct(self): + # Setup + board = Chessboard() + nums = VGroup() + bin_nums = VGroup() + for n, square in enumerate(board): + bin_num = VGroup(*[ + Integer(int(b)) + for b in int_to_bit_coords(n, min_dim=6) + ]) + bin_num.arrange(RIGHT, buff=SMALL_BUFF) + bin_num.set_width((square.get_width() * 0.8)) + num = Integer(n) + num.set_height(square.get_height() * 0.4) + + for mob in num, bin_num: + mob.move_to(square, OUT) + mob.set_stroke(BLACK, 4, background=True) + + num.generate_target() + num.target.replace(bin_num, stretch=True) + num.target.set_opacity(0) + bin_num.save_state() + bin_num.replace(num, stretch=True) + bin_num.set_opacity(0) + + nums.add(num) + bin_nums.add(bin_num) + + # Transform to binary + self.add(board, nums) + self.wait() + original_nums = nums.copy() + self.play(LaggedStart(*[ + AnimationGroup(MoveToTarget(num), Restore(bin_num)) + for num, bin_num in zip(nums, bin_nums) + ]), lag_ratio=0.1) + self.remove(nums) + nums = original_nums + self.wait(2) + + self.play( + bin_nums.set_stroke, None, 0, + bin_nums.set_opacity, 0.1, + ) + self.wait() + + # Count + n = 43 + self.play( + board[n].set_color, MAROON_E, + Animation(bin_nums[n]), + ) + + last = VMobject() + shown_nums = VGroup() + for k in [0, 8, 16, 24, 32, 40, 41, 42, 43]: + nums[k].set_fill(YELLOW) + self.add(nums[k]) + self.play(last.set_fill, WHITE, run_time=0.5) + last = nums[k] + shown_nums.add(last) + if k == 40: + self.wait() + self.wait() + self.play(LaggedStartMap(FadeOut, shown_nums[:-1])) + self.wait() + self.play( + FadeOut(last), + bin_nums[n].set_opacity, 1, + bin_nums[n].set_fill, YELLOW + ) + self.wait() + + +class SkipSkipYesYes(Scene): + def construct(self): + board = Chessboard() + board.next_to(ORIGIN, DOWN) + words = VGroup( + TextMobject("Skip"), + TextMobject("Skip"), + TextMobject("Yes"), + TextMobject("Yes"), + ) + words.add(*words.copy()) + words.set_width(board[0].get_width() * 0.8) + for word, square in zip(words, board): + word.move_to(square) + word.set_y(0, UP) + + for group in words[:4], words[4:]: + self.play(ShowIncreasingSubsets(group, rate_func=double_smooth, run_time=2)) + self.play(FadeOut(group)) + self.wait() + + +class ShowCurrAndTarget(Scene): + CONFIG = { + "bit_strings": [ + "011010", + "110001", + "101011", + ] + } + + def construct(self): + words = VGroup( + TextMobject("Current: "), + TextMobject("Need to\\\\change:"), + TextMobject("Target: "), + ) + words.arrange(DOWN, buff=0.75, aligned_edge=RIGHT) + words.to_corner(UL) + + def get_bit_aligned_bit_string(bit_coords): + result = VGroup(*[Integer(int(b)) for b in bit_coords]) + for i, bit in enumerate(result): + bit.move_to(ORIGIN, LEFT) + bit.shift(i * RIGHT * 0.325) + result.set_stroke(BLACK, 4, background=True) + return result + + bit_strings = VGroup(*[ + get_bit_aligned_bit_string(bs) + for bs in self.bit_strings + ]) + for word, bs in zip(words, bit_strings): + bs.next_to(word.family_members_with_points()[-1], RIGHT, aligned_edge=DOWN) + + words[1].set_fill(YELLOW) + bit_strings[1].set_fill(YELLOW) + + self.add(words[::2]) + self.add(bit_strings[::2]) + self.wait() + self.play(FadeIn(words[1])) + + curr_rect = None + for n in reversed(range(6)): + rect = SurroundingRectangle(Group( + bit_strings[0][n], + bit_strings[2][n], + buff=0.05, + )) + rect.stretch(0.9, 0) + rect.set_stroke(WHITE, 1) + if curr_rect is None: + curr_rect = rect + self.play(ShowCreation(curr_rect)) + else: + self.play(Transform(curr_rect, rect, run_time=0.25)) + self.wait(0.75) + self.play(FadeIn(bit_strings[1][n])) + self.play(FadeOut(curr_rect)) + + +class ShowCurrAndTargetAlt(ShowCurrAndTarget): + CONFIG = { + "bit_strings": [ + "110100", + "010101", + "100001", + ] + } + + +class EulerDiagram(Scene): + def construct(self): + colors = [RED, GREEN, BLUE] + vects = compass_directions(3, UP) + circles = VGroup(*[ + Circle( + radius=2, + fill_color=color, + stroke_color=color, + fill_opacity=0.5, + stroke_width=3, + ).shift(1.2 * vect) + for vect, color in zip(vects, colors) + ]) + bit_coords = list(map(int_to_bit_coords, range(8))) + bit_strings = VGroup(*map(get_bit_string, bit_coords)) + bit_strings.center() + r1 = 2.2 + r2 = 1.4 + bit_strings[0].next_to(circles[0], LEFT).shift(UP) + bit_strings[1].shift(r1 * vects[0]) + bit_strings[2].shift(r1 * vects[1]) + bit_strings[3].shift(r2 * (vects[0] + vects[1])) + bit_strings[4].shift(r1 * vects[2]) + bit_strings[5].shift(r2 * (vects[0] + vects[2])) + bit_strings[6].shift(r2 * (vects[1] + vects[2])) + + self.add(circles) + + for circle in circles: + circle.save_state() + + for coords, bstring in zip(bit_coords[1:], bit_strings[1:]): + for circ, coord in zip(circles, reversed(coords)): + circ.generate_target() + if coord: + circ.target.become(circ.saved_state) + else: + circ.target.set_opacity(0.1) + self.play( + FadeIn(bstring), + *map(MoveToTarget, circles), + run_time=0.25, + ) + self.wait(0.75) + self.wait() + self.play(FadeIn(bit_strings[0], DOWN)) + self.wait() + + +class ShowBoardRegions(ThreeDScene): + def construct(self): + # Setup + board = Chessboard() + nums = VGroup() + pre_bin_nums = VGroup() + bin_nums = VGroup() + for n, square in enumerate(board): + bin_num = VGroup(*[ + Integer(int(b), fill_color=GREY_A) + for b in int_to_bit_coords(n, min_dim=6) + ]) + bin_num.arrange(RIGHT, buff=SMALL_BUFF) + bin_num.set_width((square.get_width() * 0.8)) + num = Integer(n) + num.set_height(square.get_height() * 0.4) + + for mob in num, bin_num: + mob.move_to(square, OUT) + mob.set_stroke(BLACK, 4, background=True) + + bin_num.align_to(square, DOWN) + bin_num.shift(SMALL_BUFF * UP) + + pre_bin_num = num.copy() + pre_bin_num.generate_target() + pre_bin_num.target.replace(bin_num, stretch=True) + pre_bin_num.target.set_opacity(0) + + num.generate_target() + num.target.scale(0.7) + num.target.align_to(square, UP) + num.target.shift(SMALL_BUFF * DOWN) + + bin_num.save_state() + bin_num.replace(num, stretch=True) + bin_num.set_opacity(0) + + nums.add(num) + bin_nums.add(bin_num) + pre_bin_nums.add(pre_bin_num) + + # Transform to binary + self.add(board) + self.play( + ShowIncreasingSubsets(nums, run_time=4, rate_func=bezier([0, 0, 1, 1])) + ) + self.wait() + self.play( + LaggedStart(*[ + AnimationGroup( + MoveToTarget(num), + MoveToTarget(pbn), + Restore(bin_num), + ) + for num, pbn, bin_num in zip(nums, pre_bin_nums, bin_nums) + ], lag_ratio=1.5 / 64), + ) + self.remove(pre_bin_nums) + self.wait(2) + + # Build groups to highlight + one_groups = VGroup() + highlights = VGroup() + for i in reversed(range(6)): + one_group = VGroup() + highlight = VGroup() + for bin_num, square in zip(bin_nums, board): + boundary_square = Square() + # boundary_square.set_stroke(YELLOW, 4) + boundary_square.set_stroke(BLUE, 4) + boundary_square.set_fill(BLUE, 0.5) + boundary_square.replace(square) + boundary_square.move_to(square, OUT) + bit = bin_num[i] + if bit.get_value() == 1: + one_group.add(bit) + highlight.add(boundary_square) + one_group.save_state() + one_groups.add(one_group) + highlights.add(highlight) + + # Highlight hit_groups + curr_highlight = None + for one_group, highlight in zip(one_groups, highlights): + one_group.generate_target() + one_group.target.set_fill(YELLOW) + one_group.target.set_stroke(YELLOW, 2) + if curr_highlight is None: + self.play(MoveToTarget(one_group)) + self.wait() + self.play(LaggedStartMap(DrawBorderThenFill, highlight, lag_ratio=0.1, run_time=3)) + curr_highlight = highlight + else: + self.add(one_group, curr_highlight) + self.play( + MoveToTarget(one_group), + Transform(curr_highlight, highlight) + ) + self.wait() + self.play(Restore(one_group)) + self.wait() + self.play(FadeOut(curr_highlight)) + + +class ShowFinalStrategy(Scene): + CONFIG = { + "show_with_lines": False, + } + + def construct(self): + # Setup board and such + board = Chessboard() + board.to_edge(RIGHT) + coins = CoinsOnBoard(board, coin_config={"numeric_labels": True}) + coins.flip_by_message("3b1b :)") + + encoding_lines = VGroup(*[Line(ORIGIN, 0.5 * RIGHT) for x in range(6)]) + encoding_lines.arrange(LEFT, buff=SMALL_BUFF) + encoding_lines.next_to(board, LEFT, LARGE_BUFF) + encoding_lines.shift(UP) + + code_words = TextMobject("Encoding") + code_words.next_to(encoding_lines, DOWN) + + add_words = TextMobject("Check the parity\\\\of these coins") + add_words.next_to(board, LEFT, LARGE_BUFF, aligned_edge=UP) + + self.add(board, coins) + self.add(encoding_lines) + self.add(code_words) + + # Set up groups + fade_groups = Group() + line_groups = VGroup() + mover_groups = VGroup() + count_mobs = VGroup() + one_groups = VGroup() + bits = VGroup() + for i in range(6): + bit = Integer(0) + bit.next_to(encoding_lines[i], UP, SMALL_BUFF) + bits.add(bit) + + count_mob = Integer(0) + count_mob.set_color(RED) + count_mob.next_to(add_words, DOWN, MED_SMALL_BUFF) + count_mobs.add(count_mob) + + line_group = VGroup() + fade_group = Group() + mover_group = VGroup() + one_rect_group = VGroup() + count = 0 + for n, coin in enumerate(coins): + if bool(n & (1 << i)): + line_group.add(Line( + coin.get_center(), + bit.get_center(), + )) + mover_group.add(coin.labels[1 - int(coin.is_heads())].copy()) + if coin.is_heads(): + one_rect_group.add(SurroundingRectangle(coin)) + count += 1 + else: + fade_group.add(coin) + bit.set_value(count % 2) + fade_group.save_state() + line_group.set_stroke(BLUE, width=1, opacity=0.5) + fade_groups.add(fade_group) + line_groups.add(line_group) + mover_groups.add(mover_group) + one_groups.add(one_rect_group) + + # Animate + for lines, fades, movers, og, cm, bit in zip(line_groups, fade_groups, mover_groups, one_groups, count_mobs, bits): + self.play( + FadeIn(add_words), + fades.set_opacity, 0.1, + ) + if self.show_with_lines: + for mover in movers: + mover.generate_target() + mover.target.replace(bit) + mover.target.set_opacity(0) + bit.save_state() + bit.replace(movers[0]) + bit.set_opacity(0) + self.play( + LaggedStartMap(ShowCreation, lines, run_time=2), + LaggedStartMap(MoveToTarget, movers, lag_ratio=0.01), + Restore(bit) + ) + self.remove(movers) + self.add(bit) + self.play( + FadeOut(lines) + ) + else: + self.play( + ShowIncreasingSubsets(og), + UpdateFromFunc(cm, lambda m: m.set_value(len(og))) + ) + self.play(FadeInFromPoint(bit, cm.get_center())) + self.play( + FadeOut(og), + FadeOut(cm), + ) + + self.play( + FadeOut(add_words), + Restore(fades), + ) + self.remove(fades) + self.add(coins) + self.wait() + + +class ShowFinalStrategyWithFadeLines(ShowFinalStrategy): + CONFIG = { + "show_with_lines": True, + } + + +class Thumbnail(ThreeDScene): + def construct(self): + # Board + board = Chessboard( + shape=(8, 8), + # shape=(6, 6), + square_resolution=(5, 5), + top_square_resolution=(7, 7), + ) + board.set_gloss(0.5) + + coins = CoinsOnBoard( + board, + coin_config={ + "disk_resolution": (8, 51), + } + ) + coins.flip_by_message("A colab!") + + # bools = np.array(string_to_bools("A colab!")) + # bools = bools.reshape((6, 8))[:, 2:] + # coins.flip_by_bools(bools.flatten()) + + # board[0].set_opacity(0) + # coins[0].set_opacity(0) + + # k = boolian_linear_combo(coins.get_bools()) + k = 6 + + board[k].set_color(YELLOW) + + self.add(board) + self.add(coins) + + # Move them + Group(board, coins).shift(DOWN + 2 * RIGHT) + + frame = self.camera.frame + frame.set_rotation(phi=50 * DEGREES) + + # Title + title = TextMobject("Impossible?") + title.fix_in_frame() + title.set_width(8) + title.to_edge(UP) + title.set_stroke(BLACK, 6, background=True) + # self.add(title) + + # Instructions + message = TextMobject( + "Flip one coin\\\\to describe a\\\\", + "unique square", + alignment="", + ) + message[1].set_color(YELLOW) + message.scale(1.25) + message.to_edge(LEFT) + message.shift(1.25 * DOWN) + message.fix_in_frame() + arrow = Arrow( + message.get_corner(UR), + message.get_corner(UR) + 3 * RIGHT + UP, + path_arc=-90 * DEGREES, + ) + arrow.fix_in_frame() + arrow.shift(1.5 * LEFT) + arrow.set_color(YELLOW) + + self.add(message) + self.add(arrow) + + +class ChessEndScreen(PatreonEndScreen): + CONFIG = { + "scroll_time": 25, + } diff --git a/from_3b1b/active/diffyq/part1/pendulum.py b/from_3b1b/active/diffyq/part1/pendulum.py index 97cf34c765..8efee88e1d 100644 --- a/from_3b1b/active/diffyq/part1/pendulum.py +++ b/from_3b1b/active/diffyq/part1/pendulum.py @@ -508,7 +508,7 @@ def label_function(self): ) formula.set_stroke(width=0, background=True) - self.play(FadeInFrom(hm_word, DOWN)) + self.play(FadeIn(hm_word, DOWN)) self.wait() self.play( Write(formula), @@ -918,7 +918,7 @@ def construct(self): get_theta = pendulum.get_theta spring = always_redraw( - lambda: ParametricFunction( + lambda: ParametricCurve( lambda t: np.array([ np.cos(TAU * t) + (1.4 + get_theta()) * t, np.sin(TAU * t) - 0.5, @@ -970,12 +970,12 @@ def construct(self): self.wait(5) self.play( Animation(VectorizedPoint(pendulum.get_top())), - FadeOutAndShift(q_marks, UP, lag_ratio=0.3), + FadeOut(q_marks, UP, lag_ratio=0.3), ) self.add(spring_system) self.play( FadeOut(spring_rect), - FadeInFrom(linear_formula, UP), + FadeIn(linear_formula, UP), FadeInFromDown(l_title), ) self.play(FadeInFromDown(c_title)) @@ -1084,7 +1084,7 @@ def show_arc_length(self): path_arc=angle ), ) - self.play(FadeInFrom(x_sym, UP)) + self.play(FadeIn(x_sym, UP)) self.wait() # Show equation @@ -1135,7 +1135,7 @@ def add_g_vect(self): self.play( GrowArrow(g_vect), - FadeInFrom(g_word, UP, lag_ratio=0.1), + FadeIn(g_word, UP, lag_ratio=0.1), ) self.wait() @@ -1300,7 +1300,7 @@ def show_sign(self): )) self.play( - FadeInFrom(theta_decimal, UP), + FadeIn(theta_decimal, UP), FadeOut(self.x_eq), FadeOut(self.line_L), ) @@ -1831,7 +1831,7 @@ def talk_about_sine_component(self): word.next_to(arrow, DOWN) self.play( - FadeInFrom(word, UP), + FadeIn(word, UP), GrowArrow(arrow) ) self.play( diff --git a/from_3b1b/active/diffyq/part1/phase_space.py b/from_3b1b/active/diffyq/part1/phase_space.py index ff84860487..2e14f7e72c 100644 --- a/from_3b1b/active/diffyq/part1/phase_space.py +++ b/from_3b1b/active/diffyq/part1/phase_space.py @@ -561,8 +561,8 @@ def add_equation(self): ode_word.next_to(ode, DOWN) self.play( - FadeInFrom(ode, 0.5 * DOWN), - FadeInFrom(ode_word, 0.5 * UP), + FadeIn(ode, 0.5 * DOWN), + FadeIn(ode_word, 0.5 * UP), ) self.ode = ode @@ -663,7 +663,7 @@ def write_vector_derivative(self): self.play(Write(ddt)) self.play( plane.y_axis.numbers.fade, 1, - FadeInFrom(equals, LEFT), + FadeIn(equals, LEFT), TransformFromCopy(vect_sym, d_vect_sym) ) self.wait() @@ -775,7 +775,7 @@ def interpret_second_coordinate(self): ) self.wait() self.play( - FadeInFrom(expanded_derivative, LEFT), + FadeIn(expanded_derivative, LEFT), FadeIn(equals2), equation.next_to, equals2, LEFT, SMALL_BUFF, MaintainPositionRelativeTo(rect, equation), @@ -1899,7 +1899,7 @@ def show_step(self): self.add(vector, dot) self.play( ShowCreation(vector), - FadeInFrom(v_label, RIGHT), + FadeIn(v_label, RIGHT), ) self.play(FadeInFromDown(dt_label)) self.wait() diff --git a/from_3b1b/active/diffyq/part1/pi_scenes.py b/from_3b1b/active/diffyq/part1/pi_scenes.py index 8b1fde4412..3bd07780f3 100644 --- a/from_3b1b/active/diffyq/part1/pi_scenes.py +++ b/from_3b1b/active/diffyq/part1/pi_scenes.py @@ -83,7 +83,7 @@ def construct(self): ) theta_eq.add(*theta_eq.sqrt_part) self.play( - FadeInFrom(theta0_words, LEFT), + FadeIn(theta0_words, LEFT), GrowArrow(arrow), ) self.wait() @@ -446,7 +446,7 @@ def construct(self): modes = ["erm", "sad", "sad", "horrified"] for part, mode in zip(solution, modes): self.play( - FadeInFrom(part, UP), + FadeIn(part, UP), self.get_student_changes( *3 * [mode], look_at_arg=part, @@ -459,7 +459,7 @@ def construct(self): self.look_at(solution) self.wait(5) self.play( - FadeOutAndShift(solution, 2 * LEFT), + FadeOut(solution, 2 * LEFT), Restore(ode), self.get_student_changes( "sick", "angry", "tired", diff --git a/from_3b1b/active/diffyq/part1/staging.py b/from_3b1b/active/diffyq/part1/staging.py index 94ca053e80..cca425ca22 100644 --- a/from_3b1b/active/diffyq/part1/staging.py +++ b/from_3b1b/active/diffyq/part1/staging.py @@ -79,7 +79,7 @@ def construct(self): brace = Brace(Line(ORIGIN, RIGHT), DOWN) word = TextMobject("Air resistance") word.next_to(brace, DOWN) - self.play(GrowFromCenter(brace), FadeInFrom(word, UP)) + self.play(GrowFromCenter(brace), FadeIn(word, UP)) self.wait() @@ -130,12 +130,12 @@ def show_thumbnails(self): n = len(thumbnails) thumbnails.set_height(1.5) - line = self.line = CubicBezier([ + line = self.line = CubicBezier( [-5, 3, 0], [3, 3, 0], [-3, -3, 0], [5, -3, 0], - ]) + ) line.shift(MED_SMALL_BUFF * LEFT) for thumbnail, a in zip(thumbnails, np.linspace(0, 1, n)): thumbnail.move_to(line.point_from_proportion(a)) @@ -197,7 +197,7 @@ def show_words(self): for word1, word2 in zip(words, words[1:]): self.play( FadeInFromDown(word2), - FadeOutAndShift(word1, UP), + FadeOut(word1, UP), ) self.wait() self.play( @@ -360,12 +360,12 @@ def show_g_value(self): self.add(num) self.wait(0.75) self.play( - FadeInFrom(ms, 0.25 * DOWN, run_time=0.5) + FadeIn(ms, 0.25 * DOWN, run_time=0.5) ) self.wait(0.25) self.play(LaggedStart( GrowFromPoint(per, per.get_left()), - FadeInFrom(s, 0.5 * UP), + FadeIn(s, 0.5 * UP), lag_ratio=0.7, run_time=0.75 )) @@ -383,7 +383,7 @@ def show_trajectory(self): p0 = 3 * DOWN + 5 * LEFT v0 = 2.8 * UP + 1.5 * RIGHT g = 0.9 * DOWN - graph = ParametricFunction( + graph = ParametricCurve( lambda t: p0 + v0 * t + 0.5 * g * t**2, t_min=0, t_max=total_time, @@ -398,7 +398,7 @@ def show_trajectory(self): ) times = np.arange(0, total_time + 1) - velocity_graph = ParametricFunction( + velocity_graph = ParametricCurve( lambda t: v0 + g * t, t_min=0, t_max=total_time, ) @@ -534,7 +534,7 @@ def show_g_symbol(self): self.play( FadeOut(self.title), GrowFromCenter(brace), - FadeInFrom(g, UP), + FadeIn(g, UP), ) self.wait() @@ -909,8 +909,8 @@ def solve_for_velocity(self): y_dot_equation.to_corner(UR) self.play( - FadeInFrom(tex_question, DOWN), - FadeInFrom(question, UP) + FadeIn(tex_question, DOWN), + FadeIn(question, UP) ) self.wait() self.add(v_graph, question) @@ -1001,7 +1001,7 @@ def solve_for_position(self): ) self.play( - FadeInFrom(tex_question, DOWN), + FadeIn(tex_question, DOWN), ) self.wait() self.add(graph, tex_question) @@ -1117,19 +1117,19 @@ def construct(self): self.play( ShowCreationThenFadeOut(rects[0]), GrowFromCenter(braces[0]), - FadeInFrom(words[0], UP) + FadeIn(words[0], UP) ) self.wait() self.play( ShowCreationThenFadeOut(rects[1]), GrowFromCenter(braces[1]), - FadeInFrom(words[1], UP) + FadeIn(words[1], UP) ) self.wait() self.play( ShowCreationThenFadeOut(rects[2]), TransformFromCopy(*braces[1:3]), - FadeInFrom(words[2], UP), + FadeIn(words[2], UP), ) self.wait() @@ -1223,18 +1223,18 @@ def construct(self): examples.to_edge(UP) self.play( - FadeInFrom(examples[0], UP), + FadeIn(examples[0], UP), self.teacher.change, "raise_right_hand", ) self.play( - FadeInFrom(examples[1], UP), + FadeIn(examples[1], UP), self.get_student_changes( *3 * ["pondering"], look_at_arg=examples, ), ) self.play( - FadeInFrom(examples[2], UP) + FadeIn(examples[2], UP) ) self.wait(5) @@ -1481,14 +1481,14 @@ def get_curve(): self.play( ShowCreation(v_line), FadeInFromPoint(dot, v_line.get_start()), - FadeInFrom(theta, DOWN), - FadeInFrom(theta.word, DOWN), + FadeIn(theta, DOWN), + FadeIn(theta.word, DOWN), ) self.add(slope_line, dot) self.play( ShowCreation(slope_line), - FadeInFrom(d_theta, LEFT), - FadeInFrom(d_theta.word, LEFT), + FadeIn(d_theta, LEFT), + FadeIn(d_theta.word, LEFT), ) a_tracker = ValueTracker(0) @@ -1504,8 +1504,8 @@ def get_curve(): self.add(curve, dot) self.play( ShowCreation(curve), - FadeInFrom(dd_theta, LEFT), - FadeInFrom(dd_theta.word, LEFT), + FadeIn(dd_theta, LEFT), + FadeIn(dd_theta.word, LEFT), ) self.add(changing_slope) self.play( @@ -1554,7 +1554,7 @@ def write_ode(self): ) self.play( MoveToTarget(de_word), - FadeInFrom(ordinary, RIGHT), + FadeIn(ordinary, RIGHT), GrowFromCenter(ordinary_underline) ) self.play(FadeOut(ordinary_underline)) @@ -1746,7 +1746,7 @@ def write_differential_equation(self): self.add(de_word, equation) self.play( MoveToTarget(de_word), - FadeInFrom(so_word, RIGHT), + FadeIn(so_word, RIGHT), GrowFromCenter(so_line), ) self.play(ReplacementTransform(so_line, dd_x_rect)) @@ -1851,14 +1851,14 @@ def get_curve(): self.play( ShowCreation(v_line), FadeInFromPoint(dot, v_line.get_start()), - FadeInFrom(x, DOWN), - FadeInFrom(x.word, DOWN), + FadeIn(x, DOWN), + FadeIn(x.word, DOWN), ) self.add(slope_line, dot) self.play( ShowCreation(slope_line), - FadeInFrom(d_x, LEFT), - FadeInFrom(d_x.word, LEFT), + FadeIn(d_x, LEFT), + FadeIn(d_x.word, LEFT), ) a_tracker = ValueTracker(0) @@ -1874,8 +1874,8 @@ def get_curve(): self.add(curve, dot) self.play( ShowCreation(curve), - FadeInFrom(dd_x, LEFT), - FadeInFrom(dd_x.word, LEFT), + FadeIn(dd_x, LEFT), + FadeIn(dd_x.word, LEFT), ) self.add(changing_slope) self.play( @@ -2110,7 +2110,7 @@ def construct(self): )) self.add(ode) - self.play(FadeInFrom(so_word, 0.5 * DOWN)) + self.play(FadeIn(so_word, 0.5 * DOWN)) self.wait() self.play( @@ -2872,8 +2872,8 @@ def construct(self): arrow = DoubleArrow(love.get_top(), ode.get_bottom()) - self.play(FadeInFrom(ode, DOWN)) - self.play(FadeInFrom(love, UP)) + self.play(FadeIn(ode, DOWN)) + self.play(FadeIn(love, UP)) self.wait() self.play(LaggedStartMap( ShowCreationThenFadeAround, @@ -3019,7 +3019,7 @@ def construct(self): run_time=2, ) self.wait() - self.play(FadeInFrom(errors, 3 * LEFT)) + self.play(FadeIn(errors, 3 * LEFT)) self.wait() diff --git a/from_3b1b/active/diffyq/part1/wordy_scenes.py b/from_3b1b/active/diffyq/part1/wordy_scenes.py index ac2e9163f8..b53968c036 100644 --- a/from_3b1b/active/diffyq/part1/wordy_scenes.py +++ b/from_3b1b/active/diffyq/part1/wordy_scenes.py @@ -30,7 +30,7 @@ def construct(self): self.add(approx, approx_brace, approx_words) self.play( Write(implies), - FadeInFrom(period, LEFT) + FadeIn(period, LEFT) ) self.wait() @@ -47,7 +47,7 @@ def construct(self): disc.move_to(mover) mover.become(disc) self.play( - FadeInFrom(quote.author_part, LEFT), + FadeIn(quote.author_part, LEFT), LaggedStartMap( # FadeInFromLarge, # quote[:-1].family_members_with_points(), @@ -151,7 +151,7 @@ def construct(self): arrow = Vector(UP) arrow.next_to(label, UP) self.play( - FadeInFrom(label, UP), + FadeIn(label, UP), GrowArrow(arrow) ) self.wait() @@ -272,19 +272,19 @@ def construct(self): self.add(eq_word) self.add(equation) self.play( - FadeInFrom(s_word, LEFT), + FadeIn(s_word, LEFT), GrowArrow(arrows[0]), TransformFromCopy(equation, solution) ) self.wait() self.play( - FadeInFrom(c_word, UL), + FadeIn(c_word, UL), GrowArrow(arrows[2]), - FadeInFrom(computation, UP) + FadeIn(computation, UP) ) self.wait() self.play( - FadeInFrom(u_word, DL), + FadeIn(u_word, DL), GrowArrow(arrows[1]), FadeInFromDown(graph) ) @@ -357,8 +357,8 @@ def construct(self): quote.to_edge(UP) self.play( - FadeInFrom(book, RIGHT), - FadeInFrom(gleick, LEFT), + FadeIn(book, RIGHT), + FadeIn(gleick, LEFT), ) self.wait() self.play(Write(quote)) @@ -448,7 +448,7 @@ def construct(self): q1.scale, 0.3, q1.to_corner, UR, MED_SMALL_BUFF, ) - self.play(FadeInFrom(q2, DOWN)) + self.play(FadeIn(q2, DOWN)) self.play( eyes.blink, rate_func=lambda t: smooth(1 - t), @@ -698,8 +698,8 @@ def construct(self): randy = self.pi_creature self.play( - FadeInFrom(quote[:-1], DOWN), - FadeInFrom(quote[-1:], LEFT), + FadeIn(quote[:-1], DOWN), + FadeIn(quote[-1:], LEFT), randy.change, "raise_right_hand", ) self.play(Blink(randy)) diff --git a/from_3b1b/active/diffyq/part2/fourier_series.py b/from_3b1b/active/diffyq/part2/fourier_series.py index 8ca76b2840..c2032abf44 100644 --- a/from_3b1b/active/diffyq/part2/fourier_series.py +++ b/from_3b1b/active/diffyq/part2/fourier_series.py @@ -18,17 +18,16 @@ class FourierCirclesScene(Scene): "vector_config": { "buff": 0, "max_tip_length_to_length_ratio": 0.35, - "tip_length": 0.15, - "max_stroke_width_to_length_ratio": 10, - "stroke_width": 2, + "fill_opacity": 0.75, }, "circle_config": { "stroke_width": 1, + "stroke_opacity": 0.75, }, "base_frequency": 1, "slow_factor": 0.25, "center_point": ORIGIN, - "parametric_function_step_size": 0.001, + "parametric_function_step_size": 0.01, "drawn_path_color": YELLOW, "drawn_path_stroke_width": 2, } @@ -142,11 +141,9 @@ def get_vector_sum_path(self, vectors, color=YELLOW): freqs = [v.freq for v in vectors] center = vectors[0].get_start() - path = ParametricFunction( + path = ParametricCurve( lambda t: center + reduce(op.add, [ - complex_to_R3( - coef * np.exp(TAU * 1j * freq * t) - ) + complex_to_R3(coef * np.exp(TAU * 1j * freq * t)) for coef, freq in zip(coefs, freqs) ]), t_min=0, @@ -192,7 +189,7 @@ def get_y_component_wave(self, n_copies=2, right_shift_rate=5): path = self.get_vector_sum_path(vectors) - wave = ParametricFunction( + wave = ParametricCurve( lambda t: op.add( right_shift_rate * t * LEFT, path.function(t)[1] * UP @@ -331,6 +328,8 @@ def construct(self): def add_vectors_circles_path(self): path = self.get_path() coefs = self.get_coefficients_of_path(path) + for coef in coefs: + print(coef) vectors = self.get_rotating_vectors(coefficients=coefs) circles = self.get_circles(vectors) self.set_decreasing_stroke_widths(circles) @@ -379,7 +378,7 @@ class FourierOfTexPaths(FourierOfPiSymbol, MovingCameraScene): "animated_name": "Abc", "time_per_symbol": 5, "slow_factor": 1 / 5, - "parametric_function_step_size": 0.01, + "parametric_function_step_size": 0.001, } def construct(self): @@ -393,28 +392,31 @@ def construct(self): frame = self.camera.frame frame.save_state() - vectors = VGroup(VectorizedPoint()) - circles = VGroup(VectorizedPoint()) + vectors = None + circles = None for path in name.family_members_with_points(): for subpath in path.get_subpaths(): sp_mob = VMobject() sp_mob.set_points(subpath) + sp_mob.set_color("#2561d9") coefs = self.get_coefficients_of_path(sp_mob) - new_vectors = self.get_rotating_vectors( - coefficients=coefs - ) + new_vectors = self.get_rotating_vectors(coefficients=coefs) new_circles = self.get_circles(new_vectors) self.set_decreasing_stroke_widths(new_circles) drawn_path = self.get_drawn_path(new_vectors) drawn_path.clear_updaters() drawn_path.set_stroke(self.name_color, 3) + drawn_path.set_fill(opacity=0) static_vectors = VMobject().become(new_vectors) static_circles = VMobject().become(new_circles) - # static_circles = new_circles.deepcopy() - # static_vectors.clear_updaters() - # static_circles.clear_updaters() + + if vectors is None: + vectors = static_vectors.copy() + circles = static_circles.copy() + vectors.scale(0) + circles.scale(0) self.play( Transform(vectors, static_vectors, remover=True), @@ -423,7 +425,7 @@ def construct(self): frame.move_to, path, ) - self.add(new_vectors, new_circles) + self.add(drawn_path, new_vectors, new_circles) self.vector_clock.set_value(0) self.play( ShowCreation(drawn_path), diff --git a/from_3b1b/active/diffyq/part2/heat_equation.py b/from_3b1b/active/diffyq/part2/heat_equation.py index 88eb1853b4..7aabcf70ab 100644 --- a/from_3b1b/active/diffyq/part2/heat_equation.py +++ b/from_3b1b/active/diffyq/part2/heat_equation.py @@ -253,11 +253,11 @@ def show_rods(self): rod.label = label self.play( - FadeInFrom(rod1, UP), + FadeIn(rod1, UP), Write(rod1.label), ) self.play( - FadeInFrom(rod2, DOWN), + FadeIn(rod2, DOWN), Write(rod2.label) ) self.wait() @@ -612,7 +612,7 @@ def emphasize_graph(self): self.play(LaggedStart(*[ Succession( FadeInFromLarge(q_mark), - FadeOutAndShift(q_mark, DOWN), + FadeOut(q_mark, DOWN), ) for q_mark in q_marks ])) @@ -651,7 +651,7 @@ def show_x_axis(self): self.play( rod.set_opacity, 0.5, - FadeInFrom(x_axis_label, UL), + FadeIn(x_axis_label, UL), LaggedStartMap( FadeInFrom, x_numbers, lambda m: (m, UP), @@ -705,7 +705,7 @@ def get_graph_point(): ) self.play( - FadeInFrom(triangle, UP), + FadeIn(triangle, UP), FadeIn(x_label), FadeIn(rod_piece), FadeOut(self.rod_word), @@ -1135,12 +1135,12 @@ def show_changes_with_x(self): self.play( ShowCreation(dx_line), - FadeInFrom(dx, LEFT) + FadeIn(dx, LEFT) ) self.wait() self.play( ShowCreation(dT_line), - FadeInFrom(dT, IN) + FadeIn(dT, IN) ) self.wait() self.play(*map(FadeOut, [ @@ -1186,7 +1186,7 @@ def show_changes_with_t(self): graph.copy(), remover=True ), - FadeInFrom(plane, 6 * DOWN, run_time=2), + FadeIn(plane, 6 * DOWN, run_time=2), VFadeIn(line), ApplyMethod( alpha_tracker.set_value, 1, @@ -1358,7 +1358,7 @@ def show_del_x(self): ) for sym in reversed(syms): self.play( - FadeInFrom(sym, -sym.direction), + FadeIn(sym, -sym.direction), ShowCreation( sym.line.copy(), remover=True @@ -1433,7 +1433,7 @@ def show_del_t(self): ) for sym in reversed(syms): self.play( - FadeInFrom(sym, -sym.direction), + FadeIn(sym, -sym.direction), ShowCreation( sym.line.copy(), remover=True @@ -1725,10 +1725,10 @@ def show_nieghbor_rule(self): self.play( dot.scale, 0, dot.set_opacity, 0, - FadeInFrom(point_label, DOWN) + FadeIn(point_label, DOWN) ) self.play( - FadeInFrom(neighbors_label, DOWN), + FadeIn(neighbors_label, DOWN), *map(GrowArrow, arrows) ) self.wait() @@ -2161,8 +2161,8 @@ def write_second_difference(self): second_difference_word.next_to(delta_delta, DOWN) self.play( - FadeOutAndShift(dd_word, UP), - FadeInFrom(delta_delta, UP), + FadeOut(dd_word, UP), + FadeIn(delta_delta, UP), ) self.wait() self.play( @@ -2381,7 +2381,7 @@ def show_temperature_difference(self): self.play( ShowCreation(VGroup(dx_line, dT_line)), - FadeInFrom(delta_T, LEFT) + FadeIn(delta_T, LEFT) ) self.play( GrowFromCenter(brace), @@ -2828,7 +2828,7 @@ def show_temperatures(self): room_words.next_to(room_line, DOWN, SMALL_BUFF) self.play( - FadeInFrom(water_dot, RIGHT), + FadeIn(water_dot, RIGHT), GrowArrow(water_arrow), Write(water_words), run_time=1, diff --git a/from_3b1b/active/diffyq/part2/staging.py b/from_3b1b/active/diffyq/part2/staging.py index 234ecd326f..09d74635ba 100644 --- a/from_3b1b/active/diffyq/part2/staging.py +++ b/from_3b1b/active/diffyq/part2/staging.py @@ -526,7 +526,7 @@ def construct(self): variable.move_to(lhs, LEFT) self.play(LaggedStart(*[ - FadeInFrom(v, RIGHT) + FadeIn(v, RIGHT) for v in variables ])) self.wait() @@ -686,7 +686,7 @@ def show_book(self): self.play(FadeInFromDown(steve)) self.wait() self.play( - FadeInFrom(book, DOWN), + FadeIn(book, DOWN), steve.shift, 4 * RIGHT, RemovePiCreatureBubble( morty, target_mode="thinking" diff --git a/from_3b1b/active/diffyq/part2/wordy_scenes.py b/from_3b1b/active/diffyq/part2/wordy_scenes.py index 861474ae4e..a78e78fa21 100644 --- a/from_3b1b/active/diffyq/part2/wordy_scenes.py +++ b/from_3b1b/active/diffyq/part2/wordy_scenes.py @@ -101,8 +101,8 @@ def construct(self): equation.set_color_by_tex("{T}", RED) self.play( - FadeInFrom(title, DOWN), - FadeInFrom(equation, UP), + FadeIn(title, DOWN), + FadeIn(equation, UP), ) self.wait() @@ -207,9 +207,9 @@ def update_heat_colors(heat): self.wait() self.add(rot_square) self.play( - FadeInFrom(physics, RIGHT), + FadeIn(physics, RIGHT), GrowArrow(arrow2), - FadeInFrom(heat, RIGHT), + FadeIn(heat, RIGHT), GrowArrow(arrow1), MoveToTarget(title), ) @@ -273,7 +273,7 @@ def construct(self): ])) dTdx.to_edge(UP) - self.play(FadeInFrom(dTdx, DOWN)) + self.play(FadeIn(dTdx, DOWN)) self.wait() self.play(ShowCreationThenFadeAround(dTdx[3:5])) self.play(ShowCreationThenFadeAround(dTdx[:2])) @@ -384,8 +384,8 @@ def construct(self): del_outlines.set_fill(opacity=0) self.play( - FadeInFrom(title, 0.5 * DOWN), - FadeInFrom(equation, 0.5 * UP), + FadeIn(title, 0.5 * DOWN), + FadeIn(equation, 0.5 * UP), ) self.wait() self.play(ShowCreation(dt_rect)) @@ -402,8 +402,8 @@ def construct(self): ) ) self.play( - FadeOutAndShift(title, UP), - FadeInFrom(pde, DOWN), + FadeOut(title, UP), + FadeIn(pde, DOWN), FadeOut(dt_rect), FadeOut(dx_rect), ) @@ -462,13 +462,13 @@ def construct(self): self.play( Write(d1_words), - FadeInFrom(d1_equation, UP), + FadeIn(d1_equation, UP), run_time=1, ) self.wait(2) self.play( Restore(d1_group), - FadeInFrom(d3_group, LEFT) + FadeIn(d3_group, LEFT) ) self.wait() self.play( @@ -576,7 +576,7 @@ def construct(self): for part in parts[1:]: self.play( rp.become, part.rp, - FadeInFrom(part, LEFT), + FadeIn(part, LEFT), Write(part.plus), ShowCreation(part.rect), ) @@ -640,10 +640,10 @@ def construct(self): FadeInFromDown(q2), q1.shift, 1.5 * UP, ) - self.play(FadeInFrom(formula, UP)) + self.play(FadeIn(formula, UP)) self.play( GrowArrow(arrow), - FadeInFrom(q3, LEFT) + FadeIn(q3, LEFT) ) self.wait() diff --git a/from_3b1b/active/diffyq/part3/discrete_case.py b/from_3b1b/active/diffyq/part3/discrete_case.py index e79e13aeca..3d5c6f430d 100644 --- a/from_3b1b/active/diffyq/part3/discrete_case.py +++ b/from_3b1b/active/diffyq/part3/discrete_case.py @@ -92,7 +92,7 @@ def show_boundary_point_influenced_by_neighbor(self): self.play( LaggedStartMap(ShowCreation, arrows), LaggedStart(*[ - FadeInFrom(q_mark, -arrow.get_vector()) + FadeIn(q_mark, -arrow.get_vector()) for q_mark, arrow in zip(q_marks, arrows) ]), run_time=1.5 @@ -126,7 +126,7 @@ def show_boundary_point_influenced_by_neighbor(self): ReplacementTransform(*blocking_rects) ) self.wait() - self.play(FadeInFrom(braces, UP)) + self.play(FadeIn(braces, UP)) self.wait() self.play( FadeOut(new_arrows), diff --git a/from_3b1b/active/diffyq/part3/pi_creature_scenes.py b/from_3b1b/active/diffyq/part3/pi_creature_scenes.py index 7183fba9c4..e9cf8895b8 100644 --- a/from_3b1b/active/diffyq/part3/pi_creature_scenes.py +++ b/from_3b1b/active/diffyq/part3/pi_creature_scenes.py @@ -76,7 +76,7 @@ def construct(self): triangle.move_to(time_line.n2p(2019), DOWN) triangle.set_color(WHITE) - self.play(FadeInFrom(fourier, 2 * LEFT)) + self.play(FadeIn(fourier, 2 * LEFT)) self.play(randy.change, "pondering") self.wait() self.play( diff --git a/from_3b1b/active/diffyq/part3/staging.py b/from_3b1b/active/diffyq/part3/staging.py index fc917a4913..efb53da4e8 100644 --- a/from_3b1b/active/diffyq/part3/staging.py +++ b/from_3b1b/active/diffyq/part3/staging.py @@ -41,11 +41,11 @@ def show_two_titles(self): v_line.set_stroke(WHITE, 2) self.play( - FadeInFrom(lt, RIGHT), + FadeIn(lt, RIGHT), ShowCreation(v_line) ) self.play( - FadeInFrom(rt, LEFT), + FadeIn(rt, LEFT), ) # Edit in images of circle animations # and clips from FT video @@ -55,12 +55,12 @@ def show_two_titles(self): # Maybe do it for left variant, maybe not... self.play( MoveToTarget(title), - FadeInFrom(variants[0][0], LEFT) + FadeIn(variants[0][0], LEFT) ) for v1, v2 in zip(variants, variants[1:]): self.play( - FadeOutAndShift(v1[0], UP), - FadeInFrom(v2[0], DOWN), + FadeOut(v1[0], UP), + FadeIn(v2[0], DOWN), run_time=0.5, ) self.wait(0.5) @@ -177,7 +177,7 @@ def show_paper(self): color=heat_rect.get_color(), ) - self.play(FadeInFrom(paper, LEFT)) + self.play(FadeIn(paper, LEFT)) self.play( ShowCreation(date_rect), ) @@ -775,7 +775,7 @@ def construct(self): self.play(FadeInFromDown(ode)) self.wait(6) - self.play(FadeInFrom(exp, UP)) + self.play(FadeIn(exp, UP)) self.wait(2) self.play( Restore(exp_part), @@ -936,7 +936,7 @@ def construct(self): for im1, im2, arrow in zip(storyline, storyline[1:], storyline.arrows): self.add(im2, im1) self.play( - FadeInFrom(im2, -im_to_im), + FadeIn(im2, -im_to_im), ShowCreation(arrow), ) self.wait() @@ -1011,7 +1011,7 @@ def construct(self): ) self.play( GrowFromCenter(brace), - FadeInFrom(nv_text, RIGHT) + FadeIn(nv_text, RIGHT) ) self.wait() diff --git a/from_3b1b/active/diffyq/part3/temperature_graphs.py b/from_3b1b/active/diffyq/part3/temperature_graphs.py index 13d2c1967a..3cfbadfc31 100644 --- a/from_3b1b/active/diffyq/part3/temperature_graphs.py +++ b/from_3b1b/active/diffyq/part3/temperature_graphs.py @@ -120,7 +120,7 @@ def get_time_slice_graph(self, axes, func, t, **kwargs): "t_max": axes.x_max, }) config.update(kwargs) - return ParametricFunction( + return ParametricCurve( lambda x: axes.c2p( x, t, func(x, t) ), @@ -344,8 +344,8 @@ def construct(self): Write(axes2.surface), ), LaggedStart( - FadeInFrom(axes1.checkmark, DOWN), - FadeInFrom(axes2.checkmark, DOWN), + FadeIn(axes1.checkmark, DOWN), + FadeIn(axes2.checkmark, DOWN), ), lag_ratio=0.2, run_time=1, @@ -361,10 +361,10 @@ def construct(self): axes2.copy().set_fill(opacity=0), axes3 ), - FadeInFrom(equals, LEFT) + FadeIn(equals, LEFT) ) self.play( - FadeInFrom(axes3.checkmark, DOWN), + FadeIn(axes3.checkmark, DOWN), ) self.wait() @@ -528,13 +528,13 @@ def show_break_down(self): self.add(top_axes) self.play(ShowCreation(top_graph)) self.play( - FadeInFrom(top_words, RIGHT), + FadeIn(top_words, RIGHT), ShowCreation(top_arrow) ) self.wait() self.play( LaggedStartMap(FadeIn, low_axes_group), - FadeInFrom(low_words, UP), + FadeIn(low_words, UP), LaggedStartMap(FadeInFromDown, [*plusses, dots]), *[ TransformFromCopy(top_graph, low_graph) @@ -786,7 +786,7 @@ def reference_boundary_conditions(self): for x in [0, axes.x_max] ]) surface_boundary_lines = always_redraw(lambda: VGroup(*[ - ParametricFunction( + ParametricCurve( lambda t: axes.c2p( x, t, self.surface_func(x, t) @@ -978,7 +978,7 @@ def show_sine_wave_on_axes(self): self.play( Write(axes), self.quick_sine_curve.become, graph, - FadeOutAndShift(self.question_group, UP), + FadeOut(self.question_group, UP), ) self.play( FadeInFromDown(graph_label), @@ -1043,12 +1043,12 @@ def show_derivatives(self): self.play( Animation(Group(*self.get_mobjects())), - FadeInFrom(deriv1, LEFT), + FadeIn(deriv1, LEFT), self.camera.frame_center.shift, 2 * RIGHT, ) self.wait() self.play( - FadeInFrom(deriv2, UP) + FadeIn(deriv2, UP) ) self.wait() @@ -1152,8 +1152,8 @@ def show_time_step_scalings(self): graph_label[i + 1:i + 3], new_label[i + 1:i + 3], ), - FadeOutAndShift(graph_label[i], UP), - FadeInFrom(new_label[i], DOWN), + FadeOut(graph_label[i], UP), + FadeIn(new_label[i], DOWN), ) self.play( ReplacementTransform( @@ -1187,14 +1187,14 @@ def show_time_step_scalings(self): aligned_edge=DOWN, ) exp.move_to(c.get_corner(UR), DL) - anims1 = [FadeInFrom(coef, 0.25 * DOWN)] - anims2 = [FadeInFrom(exp, 0.25 * DOWN)] + anims1 = [FadeIn(coef, 0.25 * DOWN)] + anims2 = [FadeIn(exp, 0.25 * DOWN)] if last_coef: anims1.append( - FadeOutAndShift(last_coef, 0.25 * UP) + FadeOut(last_coef, 0.25 * UP) ) anims2.append( - FadeOutAndShift(last_exp, 0.25 * UP) + FadeOut(last_exp, 0.25 * UP) ) last_coef = coef last_exp = exp @@ -1369,9 +1369,7 @@ def setup_camera(self): theta=-80 * DEGREES, distance=50, ) - self.camera.set_frame_center( - 2 * RIGHT, - ) + self.camera.frame.move_to(2 * RIGHT) def show_sine_wave(self): time_tracker = ValueTracker(0) @@ -1395,7 +1393,7 @@ def show_sine_wave(self): self.play( ShowCreation(graph), - FadeInFrom(graph_label, IN) + FadeIn(graph_label, IN) ) self.wait() graph.resume_updating() @@ -1438,7 +1436,7 @@ def show_decay_surface(self): ).set_stroke(opacity=0) ) - exp_graph = ParametricFunction( + exp_graph = ParametricCurve( lambda t: axes.c2p( PI / 2, t, @@ -1480,7 +1478,7 @@ def show_decay_surface(self): self.play( ShowCreation(exp_graph), FadeOut(plane), - FadeInFrom(exp_label, IN), + FadeIn(exp_label, IN), time_slices.set_stroke, {"width": 1}, ) @@ -2173,8 +2171,8 @@ def shift_sine_to_cosine(self): ApplyMethod( self.phi_tracker.set_value, 0, ), - FadeOutAndShift(sin_label, LEFT), - FadeInFrom(cos_label, RIGHT), + FadeOut(sin_label, LEFT), + FadeIn(cos_label, RIGHT), run_time=2, ) left_tangent.move_to(graph.get_start(), LEFT) @@ -2679,8 +2677,8 @@ def show_formula(self): new_n_sym.move_to(n_sym, DR) new_n_sym.match_style(n_sym) self.play( - FadeOutAndShift(n_sym, UP), - FadeInFrom(new_n_sym, DOWN), + FadeOut(n_sym, UP), + FadeIn(new_n_sym, DOWN), omega_tracker.set_value, n * PI / L, ) self.wait() diff --git a/from_3b1b/active/diffyq/part3/wordy_scenes.py b/from_3b1b/active/diffyq/part3/wordy_scenes.py index 285cdc98de..33e6084298 100644 --- a/from_3b1b/active/diffyq/part3/wordy_scenes.py +++ b/from_3b1b/active/diffyq/part3/wordy_scenes.py @@ -62,7 +62,7 @@ def construct(self): ) self.wait() for obs in observations: - self.play(FadeInFrom(obs[1], LEFT)) + self.play(FadeIn(obs[1], LEFT)) self.wait() @@ -138,7 +138,7 @@ def show_three_conditions(self): self.play( FadeInFromDown(title), - FadeOutAndShift(to_remove, UP), + FadeOut(to_remove, UP), equation.scale, 0.6, equation.next_to, items[0], DOWN, equation.shift_onto_screen, @@ -151,7 +151,7 @@ def show_three_conditions(self): self.wait() self.play(Write(items[1][1])) bc_paren.match_y(equation) - self.play(FadeInFrom(bc_paren, UP)) + self.play(FadeIn(bc_paren, UP)) self.wait(2) self.play(Write(items[2][1])) self.wait(2) @@ -229,7 +229,7 @@ def construct(self): words[0].to_edge, DOWN, words[0].set_opacity, 0.5, Transform(phrase, phrases[1]), - FadeInFrom(words[1], UP) + FadeIn(words[1], UP) ) self.wait() # self.play( @@ -244,7 +244,7 @@ def construct(self): MaintainPositionRelativeTo( phrase, words[1] ), - FadeInFrom(solutions, LEFT), + FadeIn(solutions, LEFT), FadeIn(words[3]), ) self.wait() @@ -259,7 +259,7 @@ def construct(self): self.play( MoveToTarget(words[0]), ShowCreation(low_arrow), - FadeInFrom(models, LEFT) + FadeIn(models, LEFT) ) self.wait() @@ -308,8 +308,8 @@ def construct(self): self.play(ShowCreationThenFadeAround(rhs)) self.wait() self.play( - FadeOutAndShift(t_terms, UP), - FadeInFrom(zeros, DOWN), + FadeOut(t_terms, UP), + FadeIn(zeros, DOWN), ) t_terms.fade(1) self.wait() @@ -353,8 +353,8 @@ def construct(self): screen.center() self.play( - # FadeInFrom(title, LEFT), - FadeInFrom(screen, DOWN), + # FadeIn(title, LEFT), + FadeIn(screen, DOWN), ) self.wait() @@ -413,7 +413,7 @@ def construct(self): self.wait() self.play( MoveToTarget(pde), - FadeInFrom(new_rhs, LEFT) + FadeIn(new_rhs, LEFT) ) self.wait() self.play( @@ -610,7 +610,7 @@ def construct(self): checkmark.move_to(q_mark, DOWN) self.play( FadeInFromDown(checkmark), - FadeOutAndShift(q_mark, UP) + FadeOut(q_mark, UP) ) self.wait() @@ -737,8 +737,8 @@ def write_bc_words(self): VGroup(self.items[0], self.pde) )) self.play( - FadeOutAndShift(bc_paren, UP), - FadeInFrom(bc_words, DOWN), + FadeOut(bc_paren, UP), + FadeIn(bc_words, DOWN), ) self.wait() @@ -758,7 +758,7 @@ def write_bc_equation(self): self.play( self.camera_frame.shift, 0.8 * DOWN, ) - self.play(FadeInFrom(equation, UP)) + self.play(FadeIn(equation, UP)) self.wait() @@ -891,7 +891,7 @@ def construct(self): self.get_student_changes(*3 * ["pondering"]), ) self.play( - FadeInFrom(themes, UP), + FadeIn(themes, UP), self.get_student_changes(*3 * ["thinking"]), self.teacher.change, "happy" ) diff --git a/from_3b1b/active/diffyq/part4/complex_functions.py b/from_3b1b/active/diffyq/part4/complex_functions.py index 222e458d76..53f8bd1fa1 100644 --- a/from_3b1b/active/diffyq/part4/complex_functions.py +++ b/from_3b1b/active/diffyq/part4/complex_functions.py @@ -91,7 +91,7 @@ def func(x): self.wait(2) self.play( - FadeInFrom(real_words, RIGHT), + FadeIn(real_words, RIGHT), FadeIn(real_arrow), ) self.wait(5) @@ -357,7 +357,7 @@ def get_output_point(): def describe_input(self): input_tracker = self.input_tracker - self.play(FadeInFrom(self.input_words, UP)) + self.play(FadeIn(self.input_words, UP)) self.play( FadeInFromLarge(self.input_dot), FadeIn(self.input_decimal), @@ -387,14 +387,14 @@ def describe_output(self): self.play( ShowCreation(real_line), - FadeInFrom(real_words, DOWN) + FadeIn(real_words, DOWN) ) self.play( FadeOut(real_line), FadeOut(real_words), ) self.play( - FadeInFrom(plane.sublabel, UP) + FadeIn(plane.sublabel, UP) ) self.play( FadeIn(output_decimal), @@ -633,7 +633,7 @@ def get_output_point(): ) t_max = 40 - full_output_path = ParametricFunction( + full_output_path = ParametricCurve( lambda t: plane.n2p(np.exp(complex(0, t))), t_min=0, t_max=t_max diff --git a/from_3b1b/active/diffyq/part4/fourier_series_scenes.py b/from_3b1b/active/diffyq/part4/fourier_series_scenes.py index e346cf1640..ed16727c67 100644 --- a/from_3b1b/active/diffyq/part4/fourier_series_scenes.py +++ b/from_3b1b/active/diffyq/part4/fourier_series_scenes.py @@ -533,7 +533,7 @@ def ask_about_labels(self): for circle in circles ]) - self.play(FadeInFrom(formulas, DOWN)) + self.play(FadeIn(formulas, DOWN)) self.play(LaggedStartMap( FadeInFrom, q_marks, lambda m: (m, UP), @@ -575,8 +575,8 @@ def initialize_at_one(self): vector_clock.add_updater(vc_updater) self.wait() self.play( - FadeOutAndShift(q_marks[0], UP), - FadeInFrom(one_label, DOWN), + FadeOut(q_marks[0], UP), + FadeIn(one_label, DOWN), ) self.wait(4) @@ -633,11 +633,11 @@ def show_complex_exponents(self): self.play( ReplacementTransform(vg1_copy, vg1), ) - self.play(FadeInFrom(cps_1, DOWN)) + self.play(FadeIn(cps_1, DOWN)) self.wait(2) self.play( - FadeOutAndShift(q_marks[1], UP), - FadeInFrom(f1_exp, DOWN), + FadeOut(q_marks[1], UP), + FadeIn(f1_exp, DOWN), ) self.wait(2) self.play(ShowCreationThenFadeAround( @@ -679,7 +679,7 @@ def show_complex_exponents(self): ) self.wait(2) self.play( - FadeOutAndShift(q_marks[2], UP), + FadeOut(q_marks[2], UP), FadeInFromDown(fm1_exp), v1_rect.stretch, 1.4, 0, ) @@ -710,7 +710,7 @@ def show_complex_exponents(self): ) self.wait() self.play( - FadeOutAndShift(q_marks[3], UP), + FadeOut(q_marks[3], UP), FadeInFromDown(f2_exp), ) self.wait(3) @@ -757,7 +757,7 @@ def show_complex_exponents(self): FadeIn(v_lines, lag_ratio=0.2) ) self.play( - FadeInFrom(f_exp_general, UP) + FadeIn(f_exp_general, UP) ) self.play(ShowCreationThenFadeAround(f_exp_general)) self.wait(3) diff --git a/from_3b1b/active/diffyq/part4/pi_creature_scenes.py b/from_3b1b/active/diffyq/part4/pi_creature_scenes.py index 45959e9d61..f49b949f21 100644 --- a/from_3b1b/active/diffyq/part4/pi_creature_scenes.py +++ b/from_3b1b/active/diffyq/part4/pi_creature_scenes.py @@ -202,7 +202,7 @@ def construct(self): run_time=2, ) self.play( - FadeInFrom(terms[1], DOWN), + FadeIn(terms[1], DOWN), self.get_student_changes( "thinking", "pondering", "erm", look_at_arg=terms, diff --git a/from_3b1b/active/diffyq/part4/staging.py b/from_3b1b/active/diffyq/part4/staging.py index ea1a05b6eb..eaf6283001 100644 --- a/from_3b1b/active/diffyq/part4/staging.py +++ b/from_3b1b/active/diffyq/part4/staging.py @@ -86,7 +86,7 @@ def construct(self): self.play( MoveToTarget(group), - FadeInFrom(fourier, LEFT) + FadeIn(fourier, LEFT) ) self.play(Write(bubble, run_time=1)) self.wait() @@ -106,7 +106,7 @@ def construct(self): Transform(brace, new_brace), text.scale, 0.7, text.next_to, new_brace, UP, - FadeOutAndShift(bubble, LEFT), + FadeOut(bubble, LEFT), ) self.play( videos[2].scale, 1.7, @@ -470,7 +470,7 @@ def show_to_infinity(self): words.next_to(arrow, DOWN) self.play( - FadeInFrom(words, LEFT), + FadeIn(words, LEFT), GrowArrow(arrow) ) self.wait() @@ -604,8 +604,8 @@ def show_inf_sum_of_numbers(self): num_inf_sum.move_to(UP) self.play( - FadeOutAndShift(graph_group, DOWN), - FadeInFrom(number_line, UP), + FadeOut(graph_group, DOWN), + FadeIn(number_line, UP), FadeOut(self.inf_words), *[ TransformFromCopy(t1[-1:], t2) @@ -908,7 +908,7 @@ def get_brace_value_label(brace, u, n): )) self.play( - FadeInFrom(input_label, LEFT), + FadeIn(input_label, LEFT), dot.scale, 1.5, ) self.wait() @@ -1116,7 +1116,7 @@ def construct(self): question.next_to(line, DOWN, MED_LARGE_BUFF) self.play( FadeInFromDown(question), - FadeOutAndShift(last_question, UP) + FadeOut(last_question, UP) ) self.wait(2) last_question = question @@ -1192,7 +1192,7 @@ def construct(self): ) for q, ex in zip(questions, group): self.play( - FadeInFrom(q, LEFT), + FadeIn(q, LEFT), FadeIn(ex) ) self.wait() @@ -1321,7 +1321,7 @@ def perform_swap(self): self.add(int_ft) self.play( - FadeInFrom(breakdown_label, LEFT), + FadeIn(breakdown_label, LEFT), GrowArrow(arrow), ) self.wait() @@ -1496,8 +1496,8 @@ def multiply_f_by_exp_neg_2(self): ]) self.wait() self.play( - FadeOutAndShift(something, UP), - FadeInFrom(new_exp, DOWN), + FadeOut(something, UP), + FadeIn(new_exp, DOWN), ) self.play(FadeOut(self.to_fade)) @@ -1547,13 +1547,13 @@ def multiply_f_by_exp_neg_2(self): moving_exp[1], replacement1[1], ), - FadeOutAndShift(exp1[1], DOWN), + FadeOut(exp1[1], DOWN), Transform(exp1[0], replacement1[0]), Transform(exp1[2:], replacement1[2:]), ) self.play( TransformFromCopy(replacement1, replacement2), - FadeOutAndShift(exp2, DOWN), + FadeOut(exp2, DOWN), FadeOut(diagram), ) self.play(Restore(moving_exp)) @@ -1846,7 +1846,7 @@ def construct(self): rate_func=lambda t: 0.3 * there_and_back(t) )) self.wait() - self.play(FadeInFrom(answer, UP)) + self.play(FadeIn(answer, UP)) self.play( FadeOut(path), FadeOut(dot), @@ -2065,7 +2065,7 @@ def construct(self): self.add(cn_expression) self.wait() - self.play(FadeInFrom(expansion, LEFT)) + self.play(FadeIn(expansion, LEFT)) for words in all_words: self.play(FadeIn(words)) self.wait() @@ -2305,7 +2305,7 @@ def show_matrix_exponent(self): self.play( FadeInFromDown(m_exp), - FadeOutAndShift(formula, UP), + FadeOut(formula, UP), FadeOut(c_exp) ) self.add(vector_field, circle, dot, m_exp) diff --git a/from_3b1b/active/diffyq/part4/three_d_graphs.py b/from_3b1b/active/diffyq/part4/three_d_graphs.py index 420b5f74db..b4ba47caa7 100644 --- a/from_3b1b/active/diffyq/part4/three_d_graphs.py +++ b/from_3b1b/active/diffyq/part4/three_d_graphs.py @@ -95,7 +95,7 @@ def show_words(self): ShowCreation(arrow), group.next_to, arrow, LEFT ) - self.play(FadeInFrom(linear_word, LEFT)) + self.play(FadeIn(linear_word, LEFT)) self.wait() def add_function_labels(self): @@ -144,12 +144,12 @@ def add_function_labels(self): )) self.play( - FadeInFrom(T1[1], DOWN), - FadeInFrom(solution_labels[0], UP), + FadeIn(T1[1], DOWN), + FadeIn(solution_labels[0], UP), ) self.play( - FadeInFrom(T2[1], DOWN), - FadeInFrom(solution_labels[1], UP), + FadeIn(T2[1], DOWN), + FadeIn(solution_labels[1], UP), ) self.wait() self.play( @@ -166,7 +166,7 @@ def add_function_labels(self): ] ) self.wait() - self.play(FadeInFrom(solution_labels[2], UP)) + self.play(FadeIn(solution_labels[2], UP)) self.wait() # Show constants diff --git a/from_3b1b/active/diffyq/part5/staging.py b/from_3b1b/active/diffyq/part5/staging.py index baf34ba148..66266719fa 100644 --- a/from_3b1b/active/diffyq/part5/staging.py +++ b/from_3b1b/active/diffyq/part5/staging.py @@ -97,7 +97,7 @@ def construct(self): self.wait() self.play( Restore(derivative), - FadeInFrom(ic, LEFT) + FadeIn(ic, LEFT) ) self.wait() self.play( @@ -388,7 +388,7 @@ def show_formulas(self): self.play( ShowCreation(rhs_rect), - FadeInFrom(rhs_word, UP), + FadeIn(rhs_word, UP), ShowCreation(self.position_vect) ) self.add( @@ -410,7 +410,7 @@ def show_formulas(self): self.wait() self.play( ShowCreation(lhs_rect), - FadeInFrom(lhs_word, UP), + FadeIn(lhs_word, UP), ) self.wait() self.play( @@ -938,7 +938,7 @@ def add_initial_objects(self): self.play(randy.change, "confused") self.play( ShowCreation(p_rect), - FadeInFrom(p_label, UP) + FadeIn(p_label, UP) ) self.add(self.number_line, self.position_vect) self.play( @@ -1466,24 +1466,24 @@ def construct(self): self.add(epii) self.play( - FadeInFrom(arrow, LEFT), - FadeInFrom(repeated_mult, 2 * LEFT), + FadeIn(arrow, LEFT), + FadeIn(repeated_mult, 2 * LEFT), randy.change, "maybe", ) self.wait() self.play(randy.change, "confused") self.play( - FadeOutAndShift(arrow, DOWN), - FadeInFrom(does_not_mean, UP), + FadeOut(arrow, DOWN), + FadeIn(does_not_mean, UP), ) self.play(Write(nonsense)) self.play(randy.change, "angry") # self.play(ShowCreation(cross)) self.wait() self.play( - FadeOutAndShift(randy, DOWN), - FadeInFrom(down_arrow, UP), - FadeInFrom(actually_means, LEFT), + FadeOut(randy, DOWN), + FadeIn(down_arrow, UP), + FadeIn(actually_means, LEFT), FadeIn(series, lag_ratio=0.1, run_time=2) ) self.wait() diff --git a/from_3b1b/old/WindingNumber.py b/from_3b1b/old/WindingNumber.py index f104c36460..afcdb0e8e9 100644 --- a/from_3b1b/old/WindingNumber.py +++ b/from_3b1b/old/WindingNumber.py @@ -1605,7 +1605,7 @@ def construct(self): curved_arrow = Arc(0, color = MAROON_E) curved_arrow.set_bound_angles(np.pi, 0) - curved_arrow.generate_points() + curved_arrow.init_points() curved_arrow.add_tip() curved_arrow.move_arc_center_to(base_point + RIGHT) # Could do something smoother, with arrowhead moving along partial arc? @@ -1629,7 +1629,7 @@ def construct(self): new_curved_arrow = Arc(0).match_style(curved_arrow) new_curved_arrow.set_bound_angles(np.pi * 3/4, 0) - new_curved_arrow.generate_points() + new_curved_arrow.init_points() new_curved_arrow.add_tip() input_diff = input_dot.get_center() - curved_arrow.points[0] diff --git a/from_3b1b/old/WindingNumber_G.py b/from_3b1b/old/WindingNumber_G.py index b0fb62012a..55ce98d510 100644 --- a/from_3b1b/old/WindingNumber_G.py +++ b/from_3b1b/old/WindingNumber_G.py @@ -1337,7 +1337,7 @@ def update_inspector_image(inspector_image): ])) for x in range(2): for zero in zeros: - path = ParametricFunction( + path = ParametricCurve( bezier([ inspector.get_center(), input_plane.coords_to_point(0, 0), diff --git a/from_3b1b/old/alt_calc.py b/from_3b1b/old/alt_calc.py index c4d6ccc5e6..d74720e1ba 100644 --- a/from_3b1b/old/alt_calc.py +++ b/from_3b1b/old/alt_calc.py @@ -681,7 +681,7 @@ def get_not_so_neat_example_image(self): def get_physics_image(self): t_max = 6.5 r = 0.2 - spring = ParametricFunction( + spring = ParametricCurve( lambda t: op.add( r * (np.sin(TAU * t) * RIGHT + np.cos(TAU * t) * UP), t * DOWN, @@ -2860,7 +2860,7 @@ def draw_spider_web(self): dot = Dot(color=RED, fill_opacity=0.7) dot.move_to(self.coords_to_point(x_val, curr_output)) - self.play(FadeInFrom(dot, 2 * UR)) + self.play(FadeIn(dot, 2 * UR)) self.wait() for n in range(self.n_jumps): diff --git a/from_3b1b/old/basel/basel.py b/from_3b1b/old/basel/basel.py index 44b2ccdad1..064bef3975 100644 --- a/from_3b1b/old/basel/basel.py +++ b/from_3b1b/old/basel/basel.py @@ -99,7 +99,7 @@ def update_mobject(self, dt): start = self.spotlight.start_angle(), stop = self.spotlight.stop_angle() ) - new_arc.generate_points() + new_arc.init_points() new_arc.move_arc_center_to(self.spotlight.get_source_point()) self.angle_arc.points = new_arc.points self.angle_arc.add_tip(tip_length = ARC_TIP_LENGTH, @@ -120,7 +120,7 @@ class LightIndicator(VMobject): "light_source": None } - def generate_points(self): + def init_points(self): self.background = Circle(color=BLACK, radius = self.radius) self.background.set_fill(opacity=1.0) self.foreground = Circle(color=self.color, radius = self.radius) @@ -997,7 +997,7 @@ def morph_lighthouse_into_sun(self): #self.sun.move_source_to(sun_position) #self.sun.set_radius(120) - self.sun.spotlight.generate_points() + self.sun.spotlight.init_points() self.wait() @@ -1295,7 +1295,7 @@ def slant_screen(self): self.slanted_brightness_rect = self.brightness_rect.copy() self.slanted_brightness_rect.width *= 2 - self.slanted_brightness_rect.generate_points() + self.slanted_brightness_rect.init_points() self.slanted_brightness_rect.set_fill(opacity = 0.25) self.slanted_screen = Line(lower_slanted_screen_point,upper_slanted_screen_point, diff --git a/from_3b1b/old/basel/basel2.py b/from_3b1b/old/basel/basel2.py index 6f1148cfae..40147c9bf6 100644 --- a/from_3b1b/old/basel/basel2.py +++ b/from_3b1b/old/basel/basel2.py @@ -71,7 +71,7 @@ def update_mobject(self, dt): start = self.spotlight.start_angle(), stop = self.spotlight.stop_angle() ) - new_arc.generate_points() + new_arc.init_points() new_arc.move_arc_center_to(self.spotlight.get_source_point()) self.angle_arc.points = new_arc.points self.angle_arc.add_tip( @@ -92,7 +92,7 @@ class LightIndicator(Mobject): "light_source": None } - def generate_points(self): + def init_points(self): self.background = Circle(color=BLACK, radius = self.radius) self.background.set_fill(opacity = 1.0) self.foreground = Circle(color=self.color, radius = self.radius) diff --git a/from_3b1b/old/borsuk.py b/from_3b1b/old/borsuk.py index 46d20fc95f..cdd986f727 100644 --- a/from_3b1b/old/borsuk.py +++ b/from_3b1b/old/borsuk.py @@ -10,7 +10,7 @@ class Jewel(VMobject): "num_equator_points" : 5, "sun_vect" : OUT+LEFT+UP, } - def generate_points(self): + def init_points(self): for vect in OUT, IN: compass_vects = list(compass_directions(self.num_equator_points)) if vect is IN: @@ -46,7 +46,7 @@ def get_default_colors(self): random.shuffle(result) return result - def generate_points(self): + def init_points(self): jewels = VGroup(*[ Jewel(color = color) for color in self.colors @@ -913,7 +913,7 @@ def sphere_point(self, portion_around_equator, equator_tilt = 0): def get_great_arc_images(self): curves = VGroup(*[ - ParametricFunction( + ParametricCurve( lambda t : self.sphere_point(t, s) ).apply_function(self.sphere_to_plane) for s in np.arange(0, 1, 1./self.num_great_arcs) diff --git a/from_3b1b/old/borsuk_addition.py b/from_3b1b/old/borsuk_addition.py index 0c59f3caec..25bef10983 100644 --- a/from_3b1b/old/borsuk_addition.py +++ b/from_3b1b/old/borsuk_addition.py @@ -134,7 +134,7 @@ def get_arc(): self.wait() self.play( Restore(arrow), - FadeInFrom(formula, LEFT) + FadeIn(formula, LEFT) ) self.wait() @@ -452,7 +452,7 @@ def color_tex(tex_mob): self.play(WiggleOutThenIn(f)) self.wait() self.play( - FadeOutAndShift(dec_rhs, DOWN), + FadeOut(dec_rhs, DOWN), FadeInFromDown(f_of_neg_p) ) self.wait() @@ -468,7 +468,7 @@ def color_tex(tex_mob): MoveToTarget(f_of_p, path_arc=PI), MoveToTarget(f_of_neg_p, path_arc=-PI), FadeInFromLarge(minus), - FadeInFrom(zero_zero, LEFT) + FadeIn(zero_zero, LEFT) ) self.wait() @@ -493,7 +493,7 @@ def color_tex(tex_mob): self.play( FadeInFromLarge(g_of_p), - FadeInFrom(def_eq, LEFT) + FadeIn(def_eq, LEFT) ) self.play( FadeInFromDown(seeking_text), @@ -596,7 +596,7 @@ def show_input_dot(self): self.play( point_mob.move_to, start_point, GrowArrow(arrow), - FadeInFrom(p_label, IN) + FadeIn(p_label, IN) ) self.wait() self.play( @@ -706,7 +706,7 @@ def init_dot(self): ) def get_start_path(self): - path = ParametricFunction( + path = ParametricCurve( lambda t: np.array([ -np.sin(TAU * t + TAU / 4), np.cos(2 * TAU * t + TAU / 4), @@ -725,7 +725,7 @@ def get_start_path(self): def get_antipodal_path(self): start = self.get_start_point() - path = ParametricFunction( + path = ParametricCurve( lambda t: 2.03 * np.array([ 0, np.sin(PI * t), @@ -741,7 +741,7 @@ def get_antipodal_path(self): return dashed_path def get_lat_line(self, lat=0): - equator = ParametricFunction(lambda t: 2.03 * np.array([ + equator = ParametricCurve(lambda t: 2.03 * np.array([ np.cos(lat) * np.sin(TAU * t), np.cos(lat) * (-np.cos(TAU * t)), np.sin(lat) @@ -835,7 +835,7 @@ def show_input_dot(self): self.wait(3) dc = dot.copy() self.play( - FadeInFrom(dc, 2 * UP, remover=True), + FadeIn(dc, 2 * UP, remover=True), UpdateFromFunc(fp_label, lambda fp: fp.next_to(dc, UL, SMALL_BUFF)) ) self.add(dot) diff --git a/from_3b1b/old/brachistochrone/curves.py b/from_3b1b/old/brachistochrone/curves.py index 504f015a92..7b5f00049e 100644 --- a/from_3b1b/old/brachistochrone/curves.py +++ b/from_3b1b/old/brachistochrone/curves.py @@ -4,7 +4,7 @@ -class Cycloid(ParametricFunction): +class Cycloid(ParametricCurve): CONFIG = { "point_a" : 6*LEFT+3*UP, "radius" : 2, @@ -14,7 +14,7 @@ class Cycloid(ParametricFunction): } def __init__(self, **kwargs): digest_config(self, kwargs) - ParametricFunction.__init__(self, self.pos_func, **kwargs) + ParametricCurve.__init__(self, self.pos_func, **kwargs) def pos_func(self, t): T = t*self.end_theta @@ -24,7 +24,7 @@ def pos_func(self, t): 0 ]) -class LoopTheLoop(ParametricFunction): +class LoopTheLoop(ParametricCurve): CONFIG = { "color" : YELLOW_D, "density" : 10*DEFAULT_POINT_DENSITY_1D @@ -35,7 +35,7 @@ def func(t): t = (6*np.pi/2)*(t-0.5) return (t/2-np.sin(t))*RIGHT + \ (np.cos(t)+(t**2)/10)*UP - ParametricFunction.__init__(self, func, **kwargs) + ParametricCurve.__init__(self, func, **kwargs) class SlideWordDownCycloid(Animation): @@ -658,7 +658,7 @@ def midslide_action(self, point, angle): class WonkyDefineCurveWithKnob(DefineCurveWithKnob): def get_path(self): - return ParametricFunction( + return ParametricCurve( lambda t : t*RIGHT + (-0.2*t-np.sin(2*np.pi*t/6))*UP, start = -7, end = 10 @@ -667,7 +667,7 @@ def get_path(self): class SlowDefineCurveWithKnob(DefineCurveWithKnob): def get_path(self): - return ParametricFunction( + return ParametricCurve( lambda t : t*RIGHT + (np.exp(-(t+2)**2)-0.2*np.exp(t-2)), start = -4, end = 4 diff --git a/from_3b1b/old/brachistochrone/cycloid.py b/from_3b1b/old/brachistochrone/cycloid.py index 8013dc56a5..e0b5b37dba 100644 --- a/from_3b1b/old/brachistochrone/cycloid.py +++ b/from_3b1b/old/brachistochrone/cycloid.py @@ -158,7 +158,7 @@ class LeviSolution(CycloidScene): def construct(self): CycloidScene.construct(self) self.add(self.ceiling) - self.generate_points() + self.init_points() methods = [ self.draw_cycloid, self.roll_into_position, @@ -176,7 +176,7 @@ def construct(self): self.wait() - def generate_points(self): + def init_points(self): index = int(self.cycloid_fraction*self.cycloid.get_num_points()) p_point = self.cycloid.points[index] p_dot = Dot(p_point) diff --git a/from_3b1b/old/brachistochrone/light.py b/from_3b1b/old/brachistochrone/light.py index 2543611ee4..853753e0a2 100644 --- a/from_3b1b/old/brachistochrone/light.py +++ b/from_3b1b/old/brachistochrone/light.py @@ -17,8 +17,8 @@ def __init__(self, **kwargs): digest_config(self, kwargs) Arc.__init__(self, self.angle, **kwargs) - def generate_points(self): - Arc.generate_points(self) + def init_points(self): + Arc.init_points(self) self.rotate(-np.pi/4) self.shift(-self.get_left()) self.add_points(self.copy().rotate(np.pi).points) @@ -361,7 +361,7 @@ def construct(self): def get_paths(self): - squaggle = ParametricFunction( + squaggle = ParametricCurve( lambda t : (0.5*t+np.cos(t))*RIGHT+np.sin(t)*UP, start = -np.pi, end = 2*np.pi @@ -566,7 +566,7 @@ class Spring(Line): "color" : GREY } - def generate_points(self): + def init_points(self): ## self.start, self.end length = get_norm(self.end-self.start) angle = angle_of_vector(self.end-self.start) diff --git a/from_3b1b/old/brachistochrone/misc.py b/from_3b1b/old/brachistochrone/misc.py index a887d34895..ad2a751e34 100644 --- a/from_3b1b/old/brachistochrone/misc.py +++ b/from_3b1b/old/brachistochrone/misc.py @@ -227,7 +227,7 @@ def construct(self): theta_mob.to_edge(UP) t_mob.next_to(t_axis, UP) t_mob.to_edge(RIGHT) - graph = ParametricFunction( + graph = ParametricCurve( lambda t : 4*t*RIGHT + 2*smooth(t)*UP ) line = Line(graph.points[0], graph.points[-1], color = WHITE) @@ -237,7 +237,7 @@ def construct(self): stars.scale(0.1).shift(q_mark.get_center()) - squiggle = ParametricFunction( + squiggle = ParametricCurve( lambda t : t*RIGHT + 0.2*t*(5-t)*(np.sin(t)**2)*UP, start = 0, end = 5 @@ -378,7 +378,7 @@ def construct(self): dot.add(letter) dots.append(dot) - path = ParametricFunction( + path = ParametricCurve( lambda t : (t/2 + np.cos(t))*RIGHT + np.sin(t)*UP, start = -2*np.pi, end = 2*np.pi diff --git a/from_3b1b/old/clacks/question.py b/from_3b1b/old/clacks/question.py index a3b61cb5cc..b5f6745f64 100644 --- a/from_3b1b/old/clacks/question.py +++ b/from_3b1b/old/clacks/question.py @@ -377,6 +377,7 @@ def add_clack_sounds(self, clack_data): return self def tear_down(self): + super().tear_down() if self.include_sound: self.add_clack_sounds(self.clack_data) @@ -1061,7 +1062,7 @@ def construct(self): for title in titles: self.play( FadeInFromDown(title), - FadeOutAndShift(last_title, UP), + FadeOut(last_title, UP), ) self.wait() last_title = title @@ -1137,7 +1138,7 @@ def add_digits_of_pi(self): group.next_to(brace, DOWN) self.play( MoveToTarget(counter), - FadeInFrom(digits_word, LEFT), + FadeIn(digits_word, LEFT), ) self.wait() @@ -1365,8 +1366,8 @@ def construct(self): digits.set_width(FRAME_WIDTH - 1) digits.to_edge(UP) self.play( - FadeOutAndShift(right_pic, 5 * RIGHT), - # FadeOutAndShift(left_rect, 5 * LEFT), + FadeOut(right_pic, 5 * RIGHT), + # FadeOut(left_rect, 5 * LEFT), FadeOut(left_rect), PiCreatureBubbleIntroduction( morty, "This doesn't seem \\\\ like me...", diff --git a/from_3b1b/old/clacks/solution1.py b/from_3b1b/old/clacks/solution1.py index 8751802ca0..913dbb5b71 100644 --- a/from_3b1b/old/clacks/solution1.py +++ b/from_3b1b/old/clacks/solution1.py @@ -307,13 +307,13 @@ def ask_about_transfer(self): self.play(Blink(randy)) self.wait() self.play( - FadeInFrom(energy_words, RIGHT), + FadeIn(energy_words, RIGHT), FadeInFromDown(energy_expression), FadeOut(randy), ) self.wait() self.play( - FadeInFrom(momentum_words, RIGHT), + FadeIn(momentum_words, RIGHT), FadeInFromDown(momentum_expression) ) self.wait() @@ -520,7 +520,7 @@ def go_through_next_collision(self, include_velocity_label_animation=False): self.play(*anims, run_time=0.5) def get_next_velocity_labels_animation(self): - return FadeInFrom( + return FadeIn( self.get_next_velocity_labels(), LEFT, run_time=0.5 @@ -851,7 +851,7 @@ def rescale_axes(self): ) self.play( - FadeInFrom(xy_equation, UP), + FadeIn(xy_equation, UP), FadeOut(equations[1]) ) self.wait() @@ -928,7 +928,7 @@ def show_conservation_of_momentum_equation(self): ) self.play(ShowCreationThenFadeAround(momentum_expression)) self.wait() - self.play(FadeInFrom(momentum_xy_equation, UP)) + self.play(FadeIn(momentum_xy_equation, UP)) self.wait() def show_momentum_line(self): @@ -962,7 +962,7 @@ def show_momentum_line(self): self.add(line, *foreground_mobs) self.play(ShowCreation(line)) self.play( - FadeInFrom(slope_label, RIGHT), + FadeIn(slope_label, RIGHT), GrowArrow(slope_arrow), ) self.wait() @@ -1354,7 +1354,7 @@ def construct(self): ) self.wait() self.play( - FadeInFrom(simple_words, RIGHT), + FadeIn(simple_words, RIGHT), GrowArrow(simple_arrow), self.teacher.change, "hooray", ) @@ -1608,7 +1608,7 @@ def focus_on_three_points(self): self.wait() self.play( ShowCreation(theta_arc), - FadeInFrom(theta_label, UP) + FadeIn(theta_label, UP) ) self.wait() self.play( @@ -1703,8 +1703,8 @@ def drop_arc_for_each_hop(self): for line, arc, label, wedge in zip(lines, arcs, two_theta_labels, wedges): self.play( ShowCreation(line), - FadeInFrom(arc, normalize(arc.get_center())), - FadeInFrom(label, normalize(arc.get_center())), + FadeIn(arc, normalize(arc.get_center())), + FadeIn(label, normalize(arc.get_center())), FadeIn(wedge), ) @@ -1976,9 +1976,9 @@ def construct(self): self.add(expression[:2 * n + 1]) self.wait(0.25) self.play( - FadeInFrom(expression[-2:], LEFT), + FadeIn(expression[-2:], LEFT), GrowFromCenter(brace), - FadeInFrom(question, UP) + FadeIn(question, UP) ) self.wait(3) self.play( @@ -2130,7 +2130,7 @@ def add_circle_with_three_lines(self): self.play( lines_to_fade.set_stroke, WHITE, 1, 0.3, ShowCreation(arc), - FadeInFrom(theta_label, UP) + FadeIn(theta_label, UP) ) self.two_lines = two_lines @@ -2165,7 +2165,7 @@ def write_slope(self): self.remove(new_line) line.match_style(new_line) self.play( - FadeInFrom(slope_label[3:5], LEFT) + FadeIn(slope_label[3:5], LEFT) ) self.wait() self.play( @@ -2756,13 +2756,13 @@ def show_fraction(self): self.play( ShowCreation(height_line.copy().clear_updaters(), remover=True), - FadeInFrom(h_label.copy().clear_updaters(), RIGHT, remover=True), + FadeIn(h_label.copy().clear_updaters(), RIGHT, remover=True), Write(rhs[:2]) ) self.add(height_line, h_label) self.play( ShowCreation(width_line.copy().clear_updaters(), remover=True), - FadeInFrom(w_label.copy().clear_updaters(), UP, remover=True), + FadeIn(w_label.copy().clear_updaters(), UP, remover=True), self.r_label.fade, 1, Write(rhs[2]) ) @@ -3026,18 +3026,18 @@ def state_to_point(self): geometry.move_to(point, LEFT) self.play( - FadeOutAndShift(self.to_fade, UP), - FadeInFrom(state, UP) + FadeOut(self.to_fade, UP), + FadeIn(state, UP) ) self.play( GrowArrow(arrow), - FadeInFrom(point, LEFT) + FadeIn(point, LEFT) ) self.wait(2) for w1, w2 in [(state, dynamics), (point, geometry)]: self.play( - FadeOutAndShift(w1, UP), - FadeInFrom(w2, DOWN), + FadeOut(w1, UP), + FadeIn(w2, DOWN), ) self.wait() self.wait() diff --git a/from_3b1b/old/clacks/solution2/position_phase_space.py b/from_3b1b/old/clacks/solution2/position_phase_space.py index 46f82921ff..582d259ef2 100644 --- a/from_3b1b/old/clacks/solution2/position_phase_space.py +++ b/from_3b1b/old/clacks/solution2/position_phase_space.py @@ -1128,7 +1128,7 @@ def show_angles(self): for arc in arcs: self.play( - FadeInFrom(arc.word, LEFT), + FadeIn(arc.word, LEFT), GrowArrow(arc.arrow, path_arc=arc.arrow.path_arc), ) self.play( @@ -1556,13 +1556,13 @@ def break_down_components(self): TransformFromCopy(ps_vect, x_vect), ShowCreation(x_line), ) - self.play(FadeInFrom(dx_label, 0.25 * DOWN)) + self.play(FadeIn(dx_label, 0.25 * DOWN)) self.wait() self.play( TransformFromCopy(ps_vect, y_vect), ShowCreation(y_line), ) - self.play(FadeInFrom(dy_label, 0.25 * LEFT)) + self.play(FadeIn(dy_label, 0.25 * LEFT)) self.wait() # Ask about dx_dt @@ -1742,11 +1742,11 @@ def calculate_magnitude(self): self.wait() self.play(Write(rhs[1:])) self.wait() - self.play(FadeInFrom(new_rhs, UP)) + self.play(FadeIn(new_rhs, UP)) for equation in self.derivative_equations: self.play(ShowCreationThenFadeAround(equation)) self.wait() - self.play(FadeInFrom(final_rhs, UP)) + self.play(FadeIn(final_rhs, UP)) self.wait() def let_process_play_out(self): @@ -1995,7 +1995,7 @@ def ask_what_next(self): question.set_background_stroke(color=BLACK, width=3) question.next_to(self.ps_point, UP) - self.play(FadeInFrom(question, DOWN)) + self.play(FadeIn(question, DOWN)) ps_vect.suspend_updating() angles = [0.75 * PI, -0.5 * PI, -0.25 * PI] for last_angle, angle in zip(np.cumsum([0] + angles), angles): diff --git a/from_3b1b/old/clacks/solution2/simple_scenes.py b/from_3b1b/old/clacks/solution2/simple_scenes.py index 988ac98ddf..ac197ad12a 100644 --- a/from_3b1b/old/clacks/solution2/simple_scenes.py +++ b/from_3b1b/old/clacks/solution2/simple_scenes.py @@ -21,8 +21,8 @@ def construct(self): self.add(big_rect, screen_rect) self.play( FadeIn(big_rect), - FadeInFrom(title, DOWN), - FadeInFrom(screen_rect, UP), + FadeIn(title, DOWN), + FadeIn(screen_rect, UP), ) self.wait() @@ -513,7 +513,7 @@ def construct(self): group.to_edge(UP) self.play(Write(text)) - self.play(FadeInFrom(formula)) + self.play(FadeIn(formula)) self.play(ShowCreationThenFadeAround(formula)) self.wait() diff --git a/from_3b1b/old/clacks/solution2/wordy_scenes.py b/from_3b1b/old/clacks/solution2/wordy_scenes.py index 39f880d0f0..5afaa014c9 100644 --- a/from_3b1b/old/clacks/solution2/wordy_scenes.py +++ b/from_3b1b/old/clacks/solution2/wordy_scenes.py @@ -45,13 +45,13 @@ def construct(self): self.play(FadeInFromDown(e_group)) self.play( Write(arrows[0]), - FadeInFrom(c_group, LEFT) + FadeIn(c_group, LEFT) ) self.wait() self.play(FadeInFromDown(m_group)) self.play( Write(arrows[1]), - FadeInFrom(a_group, LEFT) + FadeIn(a_group, LEFT) ) self.wait(4) for k in range(2): @@ -281,15 +281,15 @@ def show_with_x_and_y(self): new_eq_group.target.next_to(new_dot_product, DOWN, LARGE_BUFF) self.play( - FadeInFrom(new_equation, UP), + FadeIn(new_equation, UP), simple_dot_product.to_edge, DOWN, LARGE_BUFF, ) self.wait() self.play( GrowFromCenter(x_brace), GrowFromCenter(y_brace), - FadeInFrom(dx_dt, UP), - FadeInFrom(dy_dt, UP), + FadeIn(dx_dt, UP), + FadeIn(dy_dt, UP), ) self.wait() self.play( diff --git a/from_3b1b/old/covid.py b/from_3b1b/old/covid.py new file mode 100644 index 0000000000..6df3e6da26 --- /dev/null +++ b/from_3b1b/old/covid.py @@ -0,0 +1,2093 @@ +from manimlib.imports import * +import scipy.stats + + +CASE_DATA = [ + 9, + 15, + 30, + 40, + 56, + 66, + 84, + 102, + 131, + 159, + 173, + 186, + 190, + 221, + 248, + 278, + 330, + 354, + 382, + 461, + 481, + 526, + 587, + 608, + 697, + 781, + 896, + 999, + 1124, + 1212, + 1385, + 1715, + 2055, + 2429, + 2764, + 3323, + 4288, + 5364, + 6780, + 8555, + 10288, + 12742, + 14901, + 17865, + 21395, + # 25404, + # 29256, + # 33627, + # 38170, + # 45421, + # 53873, +] +SICKLY_GREEN = "#9BBD37" + + +class IntroducePlot(Scene): + def construct(self): + axes = self.get_axes() + self.add(axes) + + # Dots + dots = VGroup() + for day, nc in zip(it.count(1), CASE_DATA): + dot = Dot() + dot.set_height(0.075) + dot.x = day + dot.y = nc + dot.axes = axes + dot.add_updater(lambda d: d.move_to(d.axes.c2p(d.x, d.y))) + dots.add(dot) + dots.set_color(YELLOW) + + # Rescale y axis + origin = axes.c2p(0, 0) + axes.y_axis.tick_marks.save_state() + for tick in axes.y_axis.tick_marks: + tick.match_width(axes.y_axis.tick_marks[0]) + axes.y_axis.add( + axes.h_lines, + axes.small_h_lines, + axes.tiny_h_lines, + axes.tiny_ticks, + ) + axes.y_axis.stretch(25, 1, about_point=origin) + dots.update() + + self.add(axes.small_y_labels) + self.add(axes.tiny_y_labels) + + # Add title + title = self.get_title(axes) + self.add(title) + + # Introduce the data + day = 10 + self.add(*dots[:day + 1]) + + dot = Dot() + dot.match_style(dots[day]) + dot.replace(dots[day]) + count = Integer(CASE_DATA[day]) + count.add_updater(lambda m: m.next_to(dot, UP)) + count.add_updater(lambda m: m.set_stroke(BLACK, 5, background=True)) + + v_line = Line(DOWN, UP) + v_line.set_stroke(YELLOW, 1) + v_line.add_updater( + lambda m: m.put_start_and_end_on( + axes.c2p( + axes.x_axis.p2n(dot.get_center()), + 0, + ), + dot.get_bottom(), + ) + ) + + self.add(dot) + self.add(count) + self.add(v_line) + + for new_day in range(day + 1, len(dots)): + new_dot = dots[new_day] + new_dot.update() + line = Line(dot.get_center(), new_dot.get_center()) + line.set_stroke(PINK, 3) + + self.add(line, dot) + self.play( + dot.move_to, new_dot.get_center(), + dot.set_color, RED, + ChangeDecimalToValue(count, CASE_DATA[new_day]), + ShowCreation(line), + ) + line.rotate(PI) + self.play( + dot.set_color, YELLOW, + Uncreate(line), + run_time=0.5 + ) + self.add(dots[new_day]) + + day = new_day + + if day == 27: + self.add( + axes.y_axis, axes.tiny_y_labels, axes.tiny_h_lines, axes.tiny_ticks, + title + ) + self.play( + axes.y_axis.stretch, 0.2, 1, {"about_point": origin}, + VFadeOut(axes.tiny_y_labels), + VFadeOut(axes.tiny_h_lines), + VFadeOut(axes.tiny_ticks), + MaintainPositionRelativeTo(dot, dots[new_day]), + run_time=2, + ) + self.add(axes, title, *dots[:new_day]) + if day == 36: + self.add(axes.y_axis, axes.small_y_labels, axes.small_h_lines, title) + self.play( + axes.y_axis.stretch, 0.2, 1, {"about_point": origin}, + VFadeOut(axes.small_y_labels), + VFadeOut(axes.small_h_lines), + MaintainPositionRelativeTo(dot, dots[new_day]), + run_time=2, + ) + self.add(axes, title, *dots[:new_day]) + + count.add_background_rectangle() + count.background_rectangle.stretch(1.1, 0) + self.add(count) + + # Show multiplications + last_label = VectorizedPoint(dots[25].get_center()) + last_line = VMobject() + for d1, d2 in zip(dots[25:], dots[26:]): + line = Line( + d1.get_top(), + d2.get_corner(UL), + path_arc=-90 * DEGREES, + ) + line.set_stroke(PINK, 2) + + label = VGroup( + TexMobject("\\times"), + DecimalNumber( + axes.y_axis.p2n(d2.get_center()) / + axes.y_axis.p2n(d1.get_center()), + ) + ) + label.arrange(RIGHT, buff=SMALL_BUFF) + label.set_height(0.25) + label.next_to(line.point_from_proportion(0.5), UL, SMALL_BUFF) + label.match_color(line) + label.add_background_rectangle() + label.save_state() + label.move_to(last_label) + label.set_opacity(0) + + self.play( + ShowCreation(line), + Restore(label), + last_label.move_to, label.saved_state, + VFadeOut(last_label), + FadeOut(last_line), + ) + last_line = line + last_label = label + self.wait() + self.play( + FadeOut(last_label), + FadeOut(last_line), + ) + + # + def get_title(self, axes): + title = TextMobject( + "Recorded COVID-19 cases\\\\outside mainland China", + tex_to_color_map={"COVID-19": RED} + ) + title.next_to(axes.c2p(0, 1e3), RIGHT, LARGE_BUFF) + title.to_edge(UP) + title.add_background_rectangle() + return title + + def get_axes(self, width=12, height=6): + n_cases = len(CASE_DATA) + axes = Axes( + x_range=(0, n_cases), + y_range=(0, 25000, 1000), + width=width, + height=height, + ) + axes.center() + axes.to_edge(DOWN, buff=LARGE_BUFF) + + # Add dates + text_pos_pairs = [ + ("Mar 6", 0), + ("Feb 23", -12), + ("Feb 12", -23), + ("Feb 1", -34), + ("Jan 22", -44), + ] + labels = VGroup() + extra_ticks = VGroup() + for text, pos in text_pos_pairs: + label = TextMobject(text) + label.set_height(0.2) + label.rotate(45 * DEGREES) + axis_point = axes.c2p(n_cases + pos, 0) + label.move_to(axis_point, UR) + label.shift(MED_SMALL_BUFF * DOWN) + label.shift(SMALL_BUFF * RIGHT) + labels.add(label) + + tick = Line(UP, DOWN) + tick.set_stroke(GREEN, 3) + tick.set_height(0.25) + tick.move_to(axis_point) + extra_ticks.add(tick) + + axes.x_labels = labels + axes.extra_x_ticks = extra_ticks + axes.add(labels, extra_ticks) + + # Adjust y ticks + axes.y_axis.ticks.stretch(0.5, 0) + axes.y_axis.ticks[0::5].stretch(2, 0) + + # Add y_axis_labels + def get_y_labels(axes, y_values): + labels = VGroup() + for y in y_values: + try: + label = TextMobject(f"{y}k") + label.set_height(0.25) + tick = axes.y_axis.ticks[y] + always(label.next_to, tick, LEFT, SMALL_BUFF) + labels.add(label) + except IndexError: + pass + return labels + + main_labels = get_y_labels(axes, range(5, 30, 5)) + axes.y_labels = main_labels + axes.add(main_labels) + axes.small_y_labels = get_y_labels(axes, range(1, 6)) + + tiny_labels = VGroup() + tiny_ticks = VGroup() + for y in range(200, 1000, 200): + tick = axes.y_axis.ticks[0].copy() + point = axes.c2p(0, y) + tick.move_to(point) + label = Integer(y) + label.set_height(0.25) + always(label.next_to, tick, LEFT, SMALL_BUFF) + tiny_labels.add(label) + tiny_ticks.add(tick) + + axes.tiny_y_labels = tiny_labels + axes.tiny_ticks = tiny_ticks + + # Horizontal lines + axes.h_lines = VGroup() + axes.small_h_lines = VGroup() + axes.tiny_h_lines = VGroup() + group_range_pairs = [ + (axes.h_lines, 5e3 * np.arange(1, 6)), + (axes.small_h_lines, 1e3 * np.arange(1, 5)), + (axes.tiny_h_lines, 200 * np.arange(1, 5)), + ] + for group, _range in group_range_pairs: + for y in _range: + group.add( + Line( + axes.c2p(0, y), + axes.c2p(n_cases, y), + ) + ) + group.set_stroke(WHITE, 1, opacity=0.5) + + return axes + + +class Thumbnail(IntroducePlot): + def construct(self): + axes = self.get_axes() + self.add(axes) + + dots = VGroup() + data = CASE_DATA + data.append(25398) + for day, nc in zip(it.count(1), CASE_DATA): + dot = Dot() + dot.set_height(0.2) + dot.x = day + dot.y = nc + dot.axes = axes + dot.add_updater(lambda d: d.move_to(d.axes.c2p(d.x, d.y))) + dots.add(dot) + dots.set_color(YELLOW) + dots.set_submobject_colors_by_gradient(BLUE, GREEN, RED) + + self.add(dots) + + title = TextMobject("COVID-19") + title.set_height(1) + title.set_color(RED) + title.to_edge(UP, buff=LARGE_BUFF) + + subtitle = TextMobject("and exponential growth") + subtitle.match_width(title) + subtitle.next_to(title, DOWN) + + # self.add(title) + # self.add(subtitle) + + title = TextMobject("The early warning\\\\of ", "COVID-19\\\\") + title.set_color_by_tex("COVID", RED) + title.set_height(2.5) + title.to_edge(UP, buff=LARGE_BUFF) + self.add(title) + + # self.remove(words) + # words = TextMobject("Exponential growth") + # words.move_to(ORIGIN, DL) + # words.apply_function( + # lambda p: [ + # p[0], p[1] + np.exp(0.2 * p[0]), p[2] + # ] + # ) + # self.add(words) + + self.embed() + + +class IntroQuestion(Scene): + def construct(self): + questions = VGroup( + TextMobject("What is exponential growth?"), + TextMobject("Where does it come from?"), + TextMobject("What does it imply?"), + TextMobject("When does it stop?"), + ) + questions.arrange(DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT) + + for question in questions: + self.play(FadeIn(question, RIGHT)) + self.wait() + self.play(LaggedStartMap( + FadeOutAndShift, questions, + lambda m: (m, DOWN), + )) + + +class ViralSpreadModel(Scene): + CONFIG = { + "num_neighbors": 5, + "infection_probability": 0.3, + "random_seed": 1, + } + + def construct(self): + # Init population + randys = self.get_randys() + self.add(*randys) + + # Show the sicko + self.show_patient0(randys) + + # Repeatedly spread + for x in range(20): + self.spread_infection(randys) + + def get_randys(self): + randys = VGroup(*[ + Randolph() + for x in range(150) + ]) + for randy in randys: + randy.set_height(0.5) + randys.arrange_in_grid(10, 15, buff=0.5) + randys.set_height(FRAME_HEIGHT - 1) + + for i in range(0, 10, 2): + randys[i * 15:(i + 1) * 15].shift(0.25 * RIGHT) + for randy in randys: + randy.shift(0.2 * random.random() * RIGHT) + randy.shift(0.2 * random.random() * UP) + randy.infected = False + randys.center() + return randys + + def show_patient0(self, randys): + patient0 = random.choice(randys) + patient0.infected = True + + circle = Circle() + circle.set_stroke(SICKLY_GREEN) + circle.replace(patient0) + circle.scale(1.5) + self.play( + patient0.change, "sick", + patient0.set_color, SICKLY_GREEN, + ShowCreationThenFadeOut(circle), + ) + + def spread_infection(self, randys): + E = self.num_neighbors + inf_p = self.infection_probability + + cough_anims = [] + new_infection_anims = [] + + for randy in randys: + if randy.infected: + cough_anims.append(Flash( + randy, + color=SICKLY_GREEN, + num_lines=16, + line_stroke_width=1, + flash_radius=0.5, + line_length=0.1, + )) + random.shuffle(cough_anims) + self.play(LaggedStart( + *cough_anims, + run_time=1, + lag_ratio=1 / len(cough_anims), + )) + + newly_infected = [] + for randy in randys: + if randy.infected: + distances = [ + get_norm(r2.get_center() - randy.get_center()) + for r2 in randys + ] + for i in np.argsort(distances)[1:E + 1]: + r2 = randys[i] + if random.random() < inf_p and not r2.infected and r2 not in newly_infected: + newly_infected.append(r2) + r2.generate_target() + r2.target.change("sick") + r2.target.set_color(SICKLY_GREEN) + new_infection_anims.append(MoveToTarget(r2)) + random.shuffle(new_infection_anims) + self.play(LaggedStart(*new_infection_anims, run_time=1)) + + for randy in newly_infected: + randy.infected = True + + +class GrowthEquation(Scene): + def construct(self): + # Add labels + N_label = TextMobject("$N_d$", " = Number of cases on a given day", ) + E_label = TextMobject("$E$", " = Average number of people someone infected is exposed to each day") + p_label = TextMobject("$p$", " = Probability of each exposure becoming an infection") + + N_label[0].set_color(YELLOW) + E_label[0].set_color(BLUE) + p_label[0].set_color(TEAL) + + labels = VGroup( + N_label, + E_label, + p_label + ) + labels.arrange(DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT) + labels.set_width(FRAME_WIDTH - 1) + labels.to_edge(UP) + + for label in labels: + self.play(FadeInFromDown(label)) + self.wait() + + delta_N = TexMobject("\\Delta", "N_d") + delta_N.set_color(YELLOW) + eq = TexMobject("=") + eq.center() + delta_N.next_to(eq, LEFT) + + delta_N_brace = Brace(delta_N, DOWN) + delta_N_text = delta_N_brace.get_text("Change over a day") + + nep = TexMobject("E", "\\cdot", "p", "\\cdot", "N_d") + nep[4].match_color(N_label[0]) + nep[0].match_color(E_label[0]) + nep[2].match_color(p_label[0]) + nep.next_to(eq, RIGHT) + + self.play(FadeIn(delta_N), FadeIn(eq)) + self.play( + GrowFromCenter(delta_N_brace), + FadeIn(delta_N_text, 0.5 * UP), + ) + self.wait() + self.play(LaggedStart( + TransformFromCopy(N_label[0], nep[4]), + TransformFromCopy(E_label[0], nep[0]), + TransformFromCopy(p_label[0], nep[2]), + FadeIn(nep[1]), + FadeIn(nep[3]), + lag_ratio=0.2, + run_time=2, + )) + self.wait() + self.play(ShowCreationThenFadeAround( + nep[-1], + surrounding_rectangle_config={"color": RED}, + )) + + # Recursive equation + lhs = TexMobject("N_{d + 1}", "=") + lhs[0].set_color(YELLOW) + lhs.move_to(eq, RIGHT) + lhs.shift(DOWN) + + rhs = VGroup( + nep[-1].copy(), + TexMobject("+"), + nep.copy(), + ) + rhs.arrange(RIGHT) + rhs.next_to(lhs, RIGHT) + + self.play( + FadeOut(delta_N_brace), + FadeOut(delta_N_text), + FadeIn(lhs, UP), + ) + self.play(FadeIn(rhs[:2])) + self.play(TransformFromCopy(nep, rhs[2])) + self.wait() + + alt_rhs = TexMobject( + "(", "1", "+", "E", "\\cdot", "p", ")", "N_d", + tex_to_color_map={ + "E": BLUE, + "p": TEAL, + "N_d": YELLOW, + } + ) + new_lhs = lhs.copy() + new_lhs.shift(DOWN) + alt_rhs.next_to(new_lhs, RIGHT) + self.play(TransformFromCopy(lhs, new_lhs)) + + rhs.unlock_triangulation() + self.play( + TransformFromCopy(rhs[0], alt_rhs[7].copy()), + TransformFromCopy(rhs[2][4], alt_rhs[7]), + ) + self.play( + TransformFromCopy(rhs[1][0], alt_rhs[2]), + TransformFromCopy(rhs[2][0], alt_rhs[3]), + TransformFromCopy(rhs[2][1], alt_rhs[4]), + TransformFromCopy(rhs[2][2], alt_rhs[5]), + TransformFromCopy(rhs[2][3], alt_rhs[6]), + FadeIn(alt_rhs[0]), + FadeIn(alt_rhs[1]), + ) + self.wait() + + # Comment on factor + brace = Brace(alt_rhs[:7], DOWN) + text = TextMobject("For example, ", "1.15") + text.next_to(brace, DOWN) + self.play( + GrowFromCenter(brace), + FadeIn(text, 0.5 * UP) + ) + self.wait() + + # Show exponential + eq_group = VGroup( + delta_N, eq, nep, + lhs, rhs, + new_lhs, alt_rhs, + brace, + text, + ) + self.clear() + self.add(labels, eq_group) + + self.play(ShowCreationThenFadeAround( + VGroup(delta_N, eq, nep), + surrounding_rectangle_config={"color": RED}, + )) + self.play(ShowCreationThenFadeAround( + VGroup(new_lhs, alt_rhs, brace, text), + surrounding_rectangle_config={"color": RED}, + )) + self.wait() + self.play(eq_group.to_edge, LEFT, LARGE_BUFF) + + exp_eq = TexMobject( + "N_d = (1 + E \\cdot p)^{d} \\cdot N_0", + tex_to_color_map={ + "N_d": YELLOW, + "E": BLUE, + "p": TEAL, + "{d}": YELLOW, + "N_0": YELLOW, + } + ) + exp_eq.next_to(alt_rhs, RIGHT, buff=3) + arrow = Arrow(alt_rhs.get_right(), exp_eq.get_left()) + + self.play( + GrowArrow(arrow), + FadeIn(exp_eq, 2 * LEFT) + ) + self.wait() + + # Discuss factor in front of N + ep = nep[:3] + ep_rect = SurroundingRectangle(ep) + ep_rect.set_stroke(RED, 2) + + ep_label = TextMobject("This factor will decrease") + ep_label.next_to(ep_rect, UP, aligned_edge=LEFT) + ep_label.set_color(RED) + + self.play( + ShowCreation(ep_rect), + FadeIn(ep_label, lag_ratio=0.1), + ) + self.wait() + self.play( + FadeOut(ep_rect), + FadeOut(ep_label), + ) + + # Add carrying capacity factor to p + p_factors = TexMobject( + "\\left(1 - {N_d \\over \\text{pop. size}} \\right)", + tex_to_color_map={"N_d": YELLOW}, + ) + p_factors.next_to(nep, RIGHT, buff=3) + p_factors_rect = SurroundingRectangle(p_factors) + p_factors_rect.set_stroke(TEAL, 2) + p_arrow = Arrow( + p_factors_rect.get_corner(UL), + nep[2].get_top(), + path_arc=75 * DEGREES, + color=TEAL, + ) + + self.play( + ShowCreation(p_factors_rect), + ShowCreation(p_arrow) + ) + self.wait() + self.play(Write(p_factors)) + self.wait() + self.play( + FadeOut(p_factors), + FadeOut(p_arrow), + FadeOut(p_factors_rect), + ) + + # Ask about ep shrinking + ep_question = TextMobject("What makes this shrink?") + ep_question.set_color(RED) + ep_question.next_to(ep_rect, UP, aligned_edge=LEFT) + + E_line = Underline(E_label) + E_line.set_color(BLUE) + p_line = Underline(p_label) + p_line.set_color(TEAL) + + self.play( + ShowCreation(ep_rect), + FadeIn(ep_question, LEFT) + ) + self.wait() + for line in E_line, p_line: + self.play(ShowCreation(line)) + self.wait() + self.play(FadeOut(line)) + self.wait() + + # Show alternate projections + ep_value = DecimalNumber(0.15) + ep_value.next_to(ep_rect, UP) + + self.play( + FadeOut(ep_question), + FadeIn(ep_value), + FadeOut(text[0]), + text[1].next_to, brace, DOWN, + ) + + eq1 = TexMobject("(", "1.15", ")", "^{61}", "\\cdot", "21{,}000", "=") + eq2 = TexMobject("(", "1.05", ")", "^{61}", "\\cdot", "21{,}000", "=") + eq1_rhs = Integer((1.15**61) * (21000)) + eq2_rhs = Integer((1.05**61) * (21000)) + + for eq, rhs in (eq1, eq1_rhs), (eq2, eq2_rhs): + eq[1].set_color(RED) + eq.move_to(nep) + eq.to_edge(RIGHT, buff=3) + rhs.next_to(eq, RIGHT) + rhs.align_to(eq[-2], UP) + + self.play(FadeIn(eq1)) + for tex in ["21{,}000", "61"]: + self.play(ShowCreationThenFadeOut( + Underline( + eq1.get_part_by_tex(tex), + stroke_color=YELLOW, + stroke_width=2, + buff=SMALL_BUFF, + ), + run_time=2, + )) + value = eq1_rhs.get_value() + eq1_rhs.set_value(0) + self.play(ChangeDecimalToValue(eq1_rhs, value)) + self.wait() + eq1.add(eq1_rhs) + self.play( + eq1.shift, DOWN, + FadeIn(eq2), + ) + + new_text = TextMobject("1.05") + new_text.move_to(text[1]) + self.play( + ChangeDecimalToValue(ep_value, 0.05), + FadeOut(text[1]), + FadeIn(new_text), + ) + + self.wait() + + eq2_rhs.align_to(eq1_rhs, RIGHT) + value = eq2_rhs.get_value() + eq2_rhs.set_value(0) + self.play(ChangeDecimalToValue(eq2_rhs, value)) + self.wait() + + # Pi creature quote + morty = Mortimer() + morty.set_height(1) + morty.next_to(eq2_rhs, UP) + bubble = SpeechBubble( + direction=RIGHT, + height=2.5, + width=5, + ) + bubble.next_to(morty, UL, buff=0) + bubble.write("The only thing to fear\\\\is the lack of fear itself.") + + self.add(morty) + self.add(bubble) + self.add(bubble.content) + + self.play( + labels.set_opacity, 0.5, + VFadeIn(morty), + morty.change, "speaking", + FadeIn(bubble), + Write(bubble.content), + ) + self.play(Blink(morty)) + self.wait() + + +class RescaleToLogarithmic(IntroducePlot): + def construct(self): + # Setup axes + axes = self.get_axes(width=10) + title = self.get_title(axes) + + dots = VGroup() + for day, nc in zip(it.count(1), CASE_DATA): + dot = Dot() + dot.set_height(0.075) + dot.move_to(axes.c2p(day, nc)) + dots.add(dot) + dots.set_color(YELLOW) + + self.add(axes, axes.h_lines, dots, title) + + # Create logarithmic y axis + log_y_axis = NumberLine( + x_min=0, + x_max=9, + ) + log_y_axis.rotate(90 * DEGREES) + log_y_axis.move_to(axes.c2p(0, 0), DOWN) + + labels_text = [ + "10", "100", + "1k", "10k", "100k", + "1M", "10M", "100M", + "1B", + ] + log_y_labels = VGroup() + for text, tick in zip(labels_text, log_y_axis.tick_marks[1:]): + label = TextMobject(text) + label.set_height(0.25) + always(label.next_to, tick, LEFT, SMALL_BUFF) + log_y_labels.add(label) + + # Animate the rescaling to a logarithmic plot + logarithm_title = TextMobject("(Logarithmic scale)") + logarithm_title.set_color(TEAL) + logarithm_title.next_to(title, DOWN) + logarithm_title.add_background_rectangle() + + def scale_logarithmically(p): + result = np.array(p) + y = axes.y_axis.p2n(p) + result[1] = log_y_axis.n2p(np.log10(y))[1] + return result + + log_h_lines = VGroup() + for exponent in range(0, 9): + for mult in range(2, 12, 2): + y = mult * 10**exponent + line = Line( + axes.c2p(0, y), + axes.c2p(axes.x_max, y), + ) + log_h_lines.add(line) + log_h_lines.set_stroke(WHITE, 0.5, opacity=0.5) + log_h_lines[4::5].set_stroke(WHITE, 1, opacity=1) + + movers = [dots, axes.y_axis.tick_marks, axes.h_lines, log_h_lines] + for group in movers: + group.generate_target() + for mob in group.target: + mob.move_to(scale_logarithmically(mob.get_center())) + + log_y_labels.suspend_updating() + log_y_labels.save_state() + for exponent, label in zip(it.count(1), log_y_labels): + label.set_y(axes.y_axis.n2p(10**exponent)[1]) + label.set_opacity(0) + + self.add(log_y_axis) + log_y_axis.save_state() + log_y_axis.tick_marks.set_opacity(0) + log_h_lines.set_opacity(0) + self.wait() + self.add(log_h_lines, title, logarithm_title) + self.play( + MoveToTarget(dots), + MoveToTarget(axes.y_axis.tick_marks), + MoveToTarget(axes.h_lines), + MoveToTarget(log_h_lines), + VFadeOut(axes.y_labels), + VFadeOut(axes.y_axis.tick_marks), + VFadeOut(axes.h_lines), + Restore(log_y_labels), + FadeIn(logarithm_title), + run_time=2, + ) + self.play(Restore(log_y_axis)) + self.wait() + + # Walk up y axis + brace = Brace( + log_y_axis.tick_marks[1:3], + RIGHT, + buff=SMALL_BUFF, + ) + brace_label = brace.get_tex( + "\\times 10", + buff=SMALL_BUFF + ) + VGroup(brace, brace_label).set_color(TEAL) + brace_label.set_stroke(BLACK, 8, background=True) + + self.play( + GrowFromCenter(brace), + FadeIn(brace_label) + ) + brace.add(brace_label) + for i in range(2, 5): + self.play( + brace.next_to, + log_y_axis.tick_marks[i:i + 2], + {"buff": SMALL_BUFF} + ) + self.wait(0.5) + self.play(FadeOut(brace)) + self.wait() + + # Show order of magnitude jumps + remove_anims = [] + for i, j in [(7, 27), (27, 40)]: + line = Line(dots[i].get_center(), dots[j].get_center()) + rect = Rectangle() + rect.set_fill(TEAL, 0.5) + rect.set_stroke(width=0) + rect.replace(line, stretch=True) + label = TextMobject(f"{j - i} days") + label.next_to(rect, UP, SMALL_BUFF) + label.set_color(TEAL) + + rect.save_state() + rect.stretch(0, 0, about_edge=LEFT) + self.play( + Restore(rect), + FadeIn(label, LEFT) + ) + self.wait() + + remove_anims += [ + ApplyMethod( + rect.stretch, 0, 0, {"about_edge": RIGHT}, + remover=True, + ), + FadeOut(label, RIGHT), + ] + self.wait() + + # Linear regression + def c2p(x, y): + xp = axes.x_axis.n2p(x) + yp = log_y_axis.n2p(np.log10(y)) + return np.array([xp[0], yp[1], 0]) + + reg = scipy.stats.linregress( + range(7, len(CASE_DATA)), + np.log10(CASE_DATA[7:]) + ) + x_max = axes.x_max + axes.y_axis = log_y_axis + reg_line = Line( + c2p(0, 10**reg.intercept), + c2p(x_max, 10**(reg.intercept + reg.slope * x_max)), + ) + reg_line.set_stroke(TEAL, 3) + + self.add(reg_line, dots) + dots.set_stroke(BLACK, 3, background=True) + self.play( + LaggedStart(*remove_anims), + ShowCreation(reg_line) + ) + + # Describe linear regression + reg_label = TextMobject("Linear regression") + reg_label.move_to(c2p(25, 10), DOWN) + reg_arrows = VGroup() + for prop in [0.4, 0.6, 0.5]: + reg_arrows.add( + Arrow( + reg_label.get_top(), + reg_line.point_from_proportion(prop), + buff=SMALL_BUFF, + ) + ) + + reg_arrow = reg_arrows[0].copy() + self.play( + Write(reg_label, run_time=1), + Transform(reg_arrow, reg_arrows[1], run_time=2), + VFadeIn(reg_arrow), + ) + self.play(Transform(reg_arrow, reg_arrows[2])) + self.wait() + + # Label slope + slope_label = TextMobject("$\\times 10$ every $16$ days (on average)") + slope_label.set_color(BLUE) + slope_label.set_stroke(BLACK, 8, background=True) + slope_label.rotate(reg_line.get_angle()) + slope_label.move_to(reg_line.get_center()) + slope_label.shift(MED_LARGE_BUFF * UP) + + self.play(FadeIn(slope_label, lag_ratio=0.1)) + self.wait() + + # R^2 label + R2_label = VGroup( + TexMobject("R^2 = "), + DecimalNumber(0, num_decimal_places=3) + ) + R2_label.arrange(RIGHT, aligned_edge=DOWN) + R2_label.next_to(reg_label[0][-1], RIGHT, LARGE_BUFF, aligned_edge=DOWN) + + self.play( + ChangeDecimalToValue(R2_label[1], reg.rvalue**2, run_time=2), + UpdateFromAlphaFunc( + R2_label, + lambda m, a: m.set_opacity(a), + ) + ) + self.wait() + + rect = SurroundingRectangle(R2_label, buff=0.15) + rect.set_stroke(YELLOW, 3) + rect.set_fill(BLACK, 0) + self.add(rect, R2_label) + self.play(ShowCreation(rect)) + self.play( + rect.set_stroke, WHITE, 2, + rect.set_fill, GREY_E, 1, + ) + self.wait() + self.play( + FadeOut(rect), + FadeOut(R2_label), + FadeOut(reg_label), + FadeOut(reg_arrow), + ) + + # Zoom out + extended_x_axis = NumberLine( + x_min=axes.x_axis.x_max, + x_max=axes.x_axis.x_max + 90, + unit_size=get_norm( + axes.x_axis.n2p(1) - + axes.x_axis.n2p(0) + ), + numbers_with_elongated_ticks=[], + ) + extended_x_axis.move_to(axes.x_axis.get_right(), LEFT) + self.play( + self.camera.frame.scale, 2, {"about_edge": DL}, + self.camera.frame.shift, 2.5 * DOWN + RIGHT, + log_h_lines.stretch, 3, 0, {"about_edge": LEFT}, + ShowCreation(extended_x_axis, rate_func=squish_rate_func(smooth, 0.5, 1)), + run_time=3, + ) + self.play( + reg_line.scale, 3, {"about_point": reg_line.get_start()} + ) + self.wait() + + # Show future projections + target_ys = [1e6, 1e7, 1e8, 1e9] + last_point = dots[-1].get_center() + last_label = None + last_rect = None + + date_labels_text = [ + "Apr 5", + "Apr 22", + "May 9", + "May 26", + ] + + for target_y, date_label_text in zip(target_ys, date_labels_text): + log_y = np.log10(target_y) + x = (log_y - reg.intercept) / reg.slope + line = Line(last_point, c2p(x, target_y)) + rect = Rectangle().replace(line, stretch=True) + rect.set_stroke(width=0) + rect.set_fill(TEAL, 0.5) + label = TextMobject(f"{int(x) - axes.x_max} days") + label.scale(1.5) + label.next_to(rect, UP, SMALL_BUFF) + + date_label = TextMobject(date_label_text) + date_label.set_height(0.25) + date_label.rotate(45 * DEGREES) + axis_point = axes.c2p(int(x), 0) + date_label.move_to(axis_point, UR) + date_label.shift(MED_SMALL_BUFF * DOWN) + date_label.shift(SMALL_BUFF * RIGHT) + + v_line = DashedLine( + axes.c2p(x, 0), + c2p(x, target_y), + ) + v_line.set_stroke(WHITE, 2) + + if target_y is target_ys[-1]: + self.play(self.camera.frame.scale, 1.1, {"about_edge": LEFT}) + + if last_label: + last_label.unlock_triangulation() + self.play( + ReplacementTransform(last_label, label), + ReplacementTransform(last_rect, rect), + ) + else: + rect.save_state() + rect.stretch(0, 0, about_edge=LEFT) + self.play(Restore(rect), FadeIn(label, LEFT)) + self.wait() + + self.play( + ShowCreation(v_line), + FadeIn(date_label), + ) + + last_label = label + last_rect = rect + + self.wait() + self.play( + FadeOut(last_label, RIGHT), + ApplyMethod( + last_rect.stretch, 0, 0, {"about_edge": RIGHT}, + remover=True + ), + ) + + # Show alternate petering out possibilities + def get_dots_along_curve(curve): + x_min = int(axes.x_axis.p2n(curve.get_start())) + x_max = int(axes.x_axis.p2n(curve.get_end())) + result = VGroup() + for x in range(x_min, x_max): + prop = binary_search( + lambda p: axes.x_axis.p2n( + curve.point_from_proportion(p), + ), + x, 0, 1, + ) + prop = prop or 0 + point = curve.point_from_proportion(prop) + dot = Dot(point) + dot.shift(0.02 * (random.random() - 0.5)) + dot.set_height(0.075) + dot.set_color(RED) + result.add(dot) + dots.remove(dots[0]) + return result + + def get_point_from_y(y): + log_y = np.log10(y) + x = (log_y - reg.intercept) / reg.slope + return c2p(x, 10**log_y) + + p100k = get_point_from_y(1e5) + p100M = get_point_from_y(1e8) + curve1 = VMobject() + curve1.append_points([ + dots[-1].get_center(), + p100k, + p100k + 5 * RIGHT, + ]) + curve2 = VMobject() + curve2.append_points([ + dots[-1].get_center(), + p100M, + p100M + 5 * RIGHT + 0.25 * UP, + ]) + + proj_dots1 = get_dots_along_curve(curve1) + proj_dots2 = get_dots_along_curve(curve2) + + for proj_dots in [proj_dots1, proj_dots2]: + self.play(FadeIn(proj_dots, lag_ratio=0.1)) + self.wait() + self.play(FadeOut(proj_dots, lag_ratio=0.1)) + + +class LinRegNote(Scene): + def construct(self): + text = TextMobject("(Starting from when\\\\there were 100 cases)") + text.set_stroke(BLACK, 8, background=True) + self.add(text) + + +class CompareCountries(Scene): + def construct(self): + # Introduce + sk_flag = ImageMobject(os.path.join("flags", "kr")) + au_flag = ImageMobject(os.path.join("flags", "au")) + flags = Group(sk_flag, au_flag) + flags.set_height(3) + flags.arrange(RIGHT, buff=LARGE_BUFF) + flags.next_to(ORIGIN, UP) + + labels = VGroup() + case_numbers = [6593, 64] + for flag, cn in zip(flags, case_numbers): + label = VGroup(Integer(cn), TextMobject("cases")) + label.arrange(RIGHT, buff=MED_SMALL_BUFF) + label[1].align_to(label[0][-1], DOWN) + label.scale(1.5) + label.next_to(flag, DOWN, MED_LARGE_BUFF) + label[0].set_value(0) + labels.add(label) + + self.play(LaggedStartMap(FadeInFromDown, flags, lag_ratio=0.25)) + self.play( + ChangeDecimalToValue(labels[0][0], case_numbers[0]), + ChangeDecimalToValue(labels[1][0], case_numbers[1]), + UpdateFromAlphaFunc( + labels, + lambda m, a: m.set_opacity(a), + ) + ) + self.wait() + + # Compare + arrow = Arrow( + labels[1][0].get_bottom(), + labels[0][0].get_bottom(), + path_arc=-90 * DEGREES, + ) + arrow_label = TextMobject("100x better") + arrow_label.set_color(YELLOW) + arrow_label.next_to(arrow, DOWN) + + alt_arrow_label = TextMobject("1 month behind") + alt_arrow_label.set_color(RED) + alt_arrow_label.next_to(arrow, DOWN) + + self.play(ShowCreation(arrow)) + self.play(FadeIn(arrow_label, 0.5 * UP)) + self.wait(2) + self.play( + FadeIn(alt_arrow_label, 0.5 * UP), + FadeOut(arrow_label, 0.5 * DOWN), + ) + self.wait(2) + + +class SARSvs1918(Scene): + def construct(self): + titles = VGroup( + TextMobject("2002 SARS outbreak"), + TextMobject("1918 Spanish flu"), + ) + images = Group( + ImageMobject("sars_icon"), + ImageMobject("spanish_flu"), + ) + for title, vect, color, image in zip(titles, [LEFT, RIGHT], [YELLOW, RED], images): + image.set_height(4) + image.move_to(vect * FRAME_WIDTH / 4) + image.to_edge(UP) + title.scale(1.25) + title.next_to(image, DOWN, MED_LARGE_BUFF) + title.set_color(color) + title.underline = Underline(title) + title.underline.set_stroke(WHITE, 1) + title.add_to_back(title.underline) + + titles[1].underline.match_y(titles[0].underline) + + n_cases_labels = VGroup( + TextMobject("8,096 cases"), + TextMobject("$\\sim$513{,}000{,}000 cases"), + ) + + for n_cases_label, title in zip(n_cases_labels, titles): + n_cases_label.scale(1.25) + n_cases_label.next_to(title, DOWN, MED_LARGE_BUFF) + + for image, title, label in zip(images, titles, n_cases_labels): + self.play( + FadeIn(image, DOWN), + Write(title), + run_time=1, + ) + self.play(FadeIn(label, UP)) + self.wait() + + +class ViralSpreadModelWithShuffling(ViralSpreadModel): + def construct(self): + # Init population + randys = self.get_randys() + self.add(*randys) + + # Show the sicko + self.show_patient0(randys) + + # Repeatedly spread + for x in range(15): + self.spread_infection(randys) + self.shuffle_randys(randys) + + def shuffle_randys(self, randys): + indices = list(range(len(randys))) + np.random.shuffle(indices) + + anims = [] + for i, randy in zip(indices, randys): + randy.generate_target() + randy.target.move_to(randys[i]) + anims.append(MoveToTarget( + randy, path_arc=30 * DEGREES, + )) + + self.play(LaggedStart( + *anims, + lag_ratio=1 / len(randys), + run_time=3 + )) + + +class SneezingOnNeighbors(Scene): + def construct(self): + randys = VGroup(*[PiCreature() for x in range(3)]) + randys.set_height(1) + randys.arrange(RIGHT) + + self.add(randys) + self.play( + randys[1].change, "sick", + randys[1].set_color, SICKLY_GREEN, + ) + self.play( + Flash( + randys[1], + color=SICKLY_GREEN, + flash_radius=0.8, + ), + randys[0].change, "sassy", randys[1], + randys[2].change, "angry", randys[1], + ) + self.play( + randys[0].change, "sick", + randys[0].set_color, SICKLY_GREEN, + randys[2].change, "sick", + randys[2].set_color, SICKLY_GREEN, + ) + self.play( + Flash( + randys[1], + color=SICKLY_GREEN, + flash_radius=0.8, + ) + ) + self.play( + randys[0].change, "sad", randys[1], + randys[2].change, "tired", randys[1], + ) + self.play( + Flash( + randys[1], + color=SICKLY_GREEN, + flash_radius=0.8, + ) + ) + self.play( + randys[0].change, "angry", randys[1], + randys[2].change, "angry", randys[1], + ) + self.wait() + + +class ViralSpreadModelWithClusters(ViralSpreadModel): + def construct(self): + randys = self.get_randys() + self.add(*randys) + self.show_patient0(randys) + + for x in range(6): + self.spread_infection(randys) + + def get_randys(self): + cluster = VGroup(*[Randolph() for x in range(16)]) + cluster.arrange_in_grid(4, 4) + cluster.set_height(1) + cluster.space_out_submobjects(1.3) + + clusters = VGroup(*[cluster.copy() for x in range(12)]) + clusters.arrange_in_grid(3, 4, buff=LARGE_BUFF) + clusters.set_height(FRAME_HEIGHT - 1) + + for cluster in clusters: + for randy in cluster: + randy.infected = False + + self.add(clusters) + + self.clusters = clusters + return VGroup(*it.chain(*clusters)) + + +class ViralSpreadModelWithClustersAndTravel(ViralSpreadModelWithClusters): + CONFIG = { + "random_seed": 2, + } + + def construct(self): + randys = self.get_randys() + self.add(*randys) + self.show_patient0(randys) + + for x in range(20): + self.spread_infection(randys) + self.travel_between_clusters() + self.update_frame(ignore_skipping=True) + + def travel_between_clusters(self): + reps = VGroup(*[ + random.choice(cluster) + for cluster in self.clusters + ]) + targets = list(reps) + random.shuffle(targets) + + anims = [] + for rep, target in zip(reps, targets): + rep.generate_target() + rep.target.move_to(target) + anims.append(MoveToTarget( + rep, + path_arc=30 * DEGREES, + )) + self.play(LaggedStart(*anims, run_time=3)) + + +class ShowLogisticCurve(Scene): + def construct(self): + # Init axes + axes = self.get_axes() + self.add(axes) + + # Add ODE + ode = TexMobject( + "{dN \\over dt} =", + "c", + "\\left(1 - {N \\over \\text{pop.}}\\right)", + "N", + tex_to_color_map={"N": YELLOW} + ) + ode.set_height(0.75) + ode.center() + ode.to_edge(RIGHT) + ode.shift(1.5 * UP) + self.add(ode) + + # Show curve + curve = axes.get_graph( + lambda x: 8 * smooth(x / 10) + 0.2, + ) + curve.set_stroke(YELLOW, 3) + + curve_title = TextMobject("Logistic curve") + curve_title.set_height(0.75) + curve_title.next_to(curve.get_end(), UL) + + self.play(ShowCreation(curve, run_time=3)) + self.play(FadeIn(curve_title, lag_ratio=0.1)) + self.wait() + + # Early part + line = Line( + curve.point_from_proportion(0), + curve.point_from_proportion(0.25), + ) + rect = Rectangle() + rect.set_stroke(width=0) + rect.set_fill(TEAL, 0.5) + rect.replace(line, stretch=True) + + exp_curve = axes.get_graph( + lambda x: 0.15 * np.exp(0.68 * x) + ) + exp_curve.set_stroke(RED, 3) + + rect.save_state() + rect.stretch(0, 0, about_edge=LEFT) + self.play(Restore(rect)) + self.play(ShowCreation(exp_curve, run_time=4)) + + # Show capacity + line = DashedLine( + axes.c2p(0, 8.2), + axes.c2p(axes.x_max, 8.2), + ) + line.set_stroke(BLUE, 2) + + self.play(ShowCreation(line)) + self.wait() + self.play(FadeOut(rect), FadeOut(exp_curve)) + + # Show inflection point + infl_point = axes.input_to_graph_point(5, curve) + infl_dot = Dot(infl_point) + infl_dot.set_stroke(WHITE, 3) + + curve_up_part = curve.copy() + curve_up_part.pointwise_become_partial(curve, 0, 0.4) + curve_up_part.set_stroke(GREEN) + curve_down_part = curve.copy() + curve_down_part.pointwise_become_partial(curve, 0.4, 1) + curve_down_part.set_stroke(RED) + for part in curve_up_part, curve_down_part: + part.save_state() + part.stretch(0, 1) + part.set_y(axes.c2p(0, 0)[1]) + + pre_dot = curve.copy() + pre_dot.pointwise_become_partial(curve, 0.375, 0.425) + pre_dot.unlock_triangulation() + + infl_name = TextMobject("Inflection point") + infl_name.next_to(infl_dot, LEFT) + + self.play(ReplacementTransform(pre_dot, infl_dot, path_arc=90 * DEGREES)) + self.add(curve_up_part, infl_dot) + self.play(Restore(curve_up_part)) + self.add(curve_down_part, infl_dot) + self.play(Restore(curve_down_part)) + self.wait() + self.play(Write(infl_name, run_time=1)) + self.wait() + + # Show tangent line + x_tracker = ValueTracker(0) + tan_line = Line(LEFT, RIGHT) + tan_line.set_width(5) + tan_line.set_stroke(YELLOW, 2) + + def update_tan_line(line): + x1 = x_tracker.get_value() + x2 = x1 + 0.001 + p1 = axes.input_to_graph_point(x1, curve) + p2 = axes.input_to_graph_point(x2, curve) + angle = angle_of_vector(p2 - p1) + line.rotate(angle - line.get_angle()) + line.move_to(p1) + + tan_line.add_updater(update_tan_line) + + dot = Dot() + dot.scale(0.75) + dot.set_fill(BLUE, 0.75) + dot.add_updater( + lambda m: m.move_to(axes.input_to_graph_point( + x_tracker.get_value(), curve + )) + ) + + self.play( + ShowCreation(tan_line), + FadeInFromLarge(dot), + ) + self.play( + x_tracker.set_value, 5, + run_time=6, + ) + self.wait() + self.play( + x_tracker.set_value, 9.9, + run_time=6, + ) + self.wait() + + # Define growth factor + gf_label = TexMobject( + "\\text{Growth factor} =", + "{\\Delta N_d \\over \\Delta N_{d - 1}}", + tex_to_color_map={ + "\\Delta": WHITE, + "N_d": YELLOW, + "N_{d - 1}": BLUE, + } + ) + gf_label.next_to(infl_dot, RIGHT, LARGE_BUFF) + + numer_label = TextMobject("New cases one day") + denom_label = TextMobject("New cases the\\\\previous day") + + for label, tex, vect in (numer_label, "N_d", UL), (denom_label, "N_{d - 1}", DL): + part = gf_label.get_part_by_tex(tex) + label.match_color(part) + label.next_to(part, vect, LARGE_BUFF) + label.shift(2 * RIGHT) + arrow = Arrow( + label.get_corner(vect[1] * DOWN), + part.get_corner(vect[1] * UP) + 0.25 * LEFT, + buff=0.1, + ) + arrow.match_color(part) + label.add_to_back(arrow) + + self.play( + FadeIn(gf_label[0], RIGHT), + FadeIn(gf_label[1:], LEFT), + FadeOut(ode) + ) + self.wait() + for label in numer_label, denom_label: + self.play(FadeIn(label, lag_ratio=0.1)) + self.wait() + + # Show example growth factors + self.play(x_tracker.set_value, 1) + + eq = TexMobject("=") + eq.next_to(gf_label, RIGHT) + gf = DecimalNumber(1.15) + gf.set_height(0.4) + gf.next_to(eq, RIGHT) + + def get_growth_factor(): + x1 = x_tracker.get_value() + x0 = x1 - 0.2 + x2 = x1 + 0.2 + p0, p1, p2 = [ + axes.input_to_graph_point(x, curve) + for x in [x0, x1, x2] + ] + return (p2[1] - p1[1]) / (p1[1] - p0[1]) + + gf.add_updater(lambda m: m.set_value(get_growth_factor())) + + self.add(eq, gf) + self.play( + x_tracker.set_value, 5, + run_time=6, + rate_func=linear, + ) + self.wait() + self.play( + x_tracker.set_value, 9, + run_time=6, + rate_func=linear, + ) + + def get_axes(self): + axes = Axes( + x_min=0, + x_max=13, + y_min=0, + y_max=10, + y_axis_config={ + "unit_size": 0.7, + "include_tip": False, + } + ) + axes.center() + axes.to_edge(DOWN) + + x_label = TextMobject("Time") + x_label.next_to(axes.x_axis, UP, aligned_edge=RIGHT) + y_label = TextMobject("N cases") + y_label.next_to(axes.y_axis, RIGHT, aligned_edge=UP) + axes.add(x_label, y_label) + return axes + + +class SubtltyOfGrowthFactorShift(Scene): + def construct(self): + # Set up totals + total_title = TextMobject("Totals") + total_title.add(Underline(total_title)) + total_title.to_edge(UP) + total_title.scale(1.25) + total_title.shift(LEFT) + total_title.set_color(YELLOW) + total_title.shift(LEFT) + + data = CASE_DATA[-4:] + data.append(int(data[-1] + 1.15 * (data[-1] - data[-2]))) + totals = VGroup(*[Integer(value) for value in data]) + totals.scale(1.25) + totals.arrange(DOWN, buff=0.6, aligned_edge=LEFT) + totals.next_to(total_title, DOWN, buff=0.6) + totals[-1].set_color(BLUE) + + # Set up dates + dates = VGroup( + TextMobject("March 3, 2020"), + TextMobject("March 4, 2020"), + TextMobject("March 5, 2020"), + TextMobject("March 6, 2020"), + ) + for date, total in zip(dates, totals): + date.scale(0.75) + date.set_color(LIGHT_GREY) + date.next_to(total, LEFT, buff=0.75, aligned_edge=DOWN) + + # Set up changes + change_arrows = VGroup() + change_labels = VGroup() + for t1, t2 in zip(totals, totals[1:]): + arrow = Arrow( + t1.get_right(), + t2.get_right(), + path_arc=-150 * DEGREES, + buff=0.1, + max_tip_length_to_length_ratio=0.15, + ) + arrow.shift(MED_SMALL_BUFF * RIGHT) + arrow.set_stroke(width=3) + change_arrows.add(arrow) + + diff = t2.get_value() - t1.get_value() + label = Integer(diff, include_sign=True) + label.set_color(GREEN) + label.next_to(arrow, RIGHT) + change_labels.add(label) + + change_labels[-1].set_color(BLUE) + + change_title = TextMobject("Changes") + change_title.add(Underline(change_title).shift(0.128 * UP)) + change_title.scale(1.25) + change_title.set_color(GREEN) + change_title.move_to(change_labels) + change_title.align_to(total_title, UP) + + # Set up growth factors + gf_labels = VGroup() + gf_arrows = VGroup() + for c1, c2 in zip(change_labels, change_labels[1:]): + arrow = Arrow( + c1.get_right(), + c2.get_right(), + path_arc=-150 * DEGREES, + buff=0.1, + max_tip_length_to_length_ratio=0.15, + ) + arrow.set_stroke(width=1) + gf_arrows.add(arrow) + + line = Line(LEFT, RIGHT) + line.match_width(c2) + line.set_stroke(WHITE, 2) + numer = c2.deepcopy() + denom = c1.deepcopy() + frac = VGroup(numer, line, denom) + frac.arrange(DOWN, buff=SMALL_BUFF) + frac.scale(0.7) + frac.next_to(arrow, RIGHT) + eq = TexMobject("=") + eq.next_to(frac, RIGHT) + gf = DecimalNumber(c2.get_value() / c1.get_value()) + gf.next_to(eq, RIGHT) + gf_labels.add(VGroup(frac, eq, gf)) + + gf_title = TextMobject("Growth factors") + gf_title.add(Underline(gf_title)) + gf_title.scale(1.25) + gf_title.move_to(gf_labels[0][-1]) + gf_title.align_to(total_title, DOWN) + + # Add things + self.add(dates, total_title) + self.play( + LaggedStartMap( + FadeInFrom, totals[:-1], + lambda m: (m, UP), + ) + ) + self.wait() + self.play( + ShowCreation(change_arrows[:-1]), + LaggedStartMap( + FadeInFrom, change_labels[:-1], + lambda m: (m, LEFT), + ), + FadeIn(change_title), + ) + self.wait() + self.play( + ShowCreation(gf_arrows[:-1]), + LaggedStartMap(FadeIn, gf_labels[:-1]), + FadeIn(gf_title), + ) + self.wait() + + # Show hypothetical new value + self.play(LaggedStart( + FadeIn(gf_labels[-1]), + FadeIn(gf_arrows[-1]), + FadeIn(change_labels[-1]), + FadeIn(change_arrows[-1]), + FadeIn(totals[-1]), + )) + self.wait() + + # Change it + alt_change = data[-2] - data[-3] + alt_total = data[-2] + alt_change + alt_gf = 1 + + self.play( + ChangeDecimalToValue(gf_labels[-1][-1], alt_gf), + ChangeDecimalToValue(gf_labels[-1][0][0], alt_change), + ChangeDecimalToValue(change_labels[-1], alt_change), + ChangeDecimalToValue(totals[-1], alt_total), + ) + self.wait() + + +class ContrastRandomShufflingWithClustersAndTravel(Scene): + def construct(self): + background = FullScreenFadeRectangle() + background.set_fill(GREY_E) + self.add(background) + + squares = VGroup(*[Square() for x in range(2)]) + squares.set_width(FRAME_WIDTH / 2 - 1) + squares.arrange(RIGHT, buff=0.75) + squares.to_edge(DOWN) + squares.set_fill(BLACK, 1) + squares.stretch(0.8, 1) + self.add(squares) + + titles = VGroup( + TextMobject("Random shuffling"), + TextMobject("Clusters with travel"), + ) + for title, square in zip(titles, squares): + title.scale(1.4) + title.next_to(square, UP) + titles[1].align_to(titles[0], UP) + + self.play(LaggedStartMap( + FadeInFrom, titles, + lambda m: (m, 0.25 * DOWN), + )) + self.wait() + + +class ShowVaryingExpFactor(Scene): + def construct(self): + factor = DecimalNumber(0.15) + rect = BackgroundRectangle(factor, buff=SMALL_BUFF) + rect.set_fill(BLACK, 1) + arrow = Arrow( + factor.get_right(), + factor.get_right() + 4 * RIGHT + 0.5 * DOWN, + ) + + self.add(rect, factor, arrow) + for value in [0.05, 0.25, 0.15]: + self.play( + ChangeDecimalToValue(factor, value), + run_time=3, + ) + self.wait() + + +class ShowVaryingBaseFactor(ShowLogisticCurve): + def construct(self): + factor = DecimalNumber(1.15) + rect = BackgroundRectangle(factor, buff=SMALL_BUFF) + rect.set_fill(BLACK, 1) + + self.add(rect, factor) + for value in [1.05, 1.25, 1.15]: + self.play( + ChangeDecimalToValue(factor, value), + run_time=3, + ) + self.wait() + + +class ShowVaryingExpCurve(ShowLogisticCurve): + def construct(self): + axes = self.get_axes() + self.add(axes) + + curve = axes.get_graph(lambda x: np.exp(0.15 * x)) + curve.set_stroke([BLUE, YELLOW, RED]) + curve.make_jagged() + self.add(curve) + + self.camera.frame.scale(2, about_edge=DOWN) + self.camera.frame.shift(DOWN) + rect = FullScreenFadeRectangle() + rect.set_stroke(WHITE, 3) + rect.set_fill(opacity=0) + self.add(rect) + + for value in [0.05, 0.25, 0.15]: + new_curve = axes.get_graph(lambda x: np.exp(value * x)) + new_curve.set_stroke([BLUE, YELLOW, RED]) + new_curve.make_jagged() + self.play( + Transform(curve, new_curve), + run_time=3, + ) + + +class EndScreen(PatreonEndScreen): + CONFIG = { + "specific_patrons": [ + "1stViewMaths", + "Adam Dřínek", + "Aidan Shenkman", + "Alan Stein", + "Alex Mijalis", + "Alexis Olson", + "Ali Yahya", + "Andrew Busey", + "Andrew Cary", + "Andrew R. Whalley", + "Aravind C V", + "Arjun Chakroborty", + "Arthur Zey", + "Austin Goodman", + "Avi Finkel", + "Awoo", + "AZsorcerer", + "Barry Fam", + "Bernd Sing", + "Boris Veselinovich", + "Bradley Pirtle", + "Brian Staroselsky", + "Britt Selvitelle", + "Britton Finley", + "Burt Humburg", + "Calvin Lin", + "Charles Southerland", + "Charlie N", + "Chenna Kautilya", + "Chris Connett", + "Christian Kaiser", + "cinterloper", + "Clark Gaebel", + "Colwyn Fritze-Moor", + "Cooper Jones", + "Corey Ogburn", + "D. Sivakumar", + "Daniel Herrera C", + "Dave B", + "Dave Kester", + "dave nicponski", + "David B. Hill", + "David Clark", + "David Gow", + "Delton Ding", + "Dominik Wagner", + "Douglas Cantrell", + "emptymachine", + "Eric Younge", + "Eryq Ouithaqueue", + "Federico Lebron", + "Frank R. Brown, Jr.", + "Giovanni Filippi", + "Hal Hildebrand", + "Hitoshi Yamauchi", + "Ivan Sorokin", + "Jacob Baxter", + "Jacob Harmon", + "Jacob Hartmann", + "Jacob Magnuson", + "Jake Vartuli - Schonberg", + "Jameel Syed", + "Jason Hise", + "Jayne Gabriele", + "Jean-Manuel Izaret", + "Jeff Linse", + "Jeff Straathof", + "John C. Vesey", + "John Haley", + "John Le", + "John V Wertheim", + "Jonathan Heckerman", + "Jonathan Wilson", + "Joseph John Cox", + "Joseph Kelly", + "Josh Kinnear", + "Joshua Claeys", + "Juan Benet", + "Kai-Siang Ang", + "Kanan Gill", + "Karl Niu", + "Kartik Cating-Subramanian", + "Kaustuv DeBiswas", + "Killian McGuinness", + "Kros Dai", + "L0j1k", + "Lambda GPU Workstations", + "Lee Redden", + "Linh Tran", + "Luc Ritchie", + "Ludwig Schubert", + "Lukas Biewald", + "Magister Mugit", + "Magnus Dahlström", + "Manoj Rewatkar - RITEK SOLUTIONS", + "Mark Heising", + "Mark Mann", + "Martin Price", + "Mathias Jansson", + "Matt Godbolt", + "Matt Langford", + "Matt Roveto", + "Matt Russell", + "Matteo Delabre", + "Matthew Bouchard", + "Matthew Cocke", + "Mia Parent", + "Michael Hardel", + "Michael W White", + "Mirik Gogri", + "Mustafa Mahdi", + "Márton Vaitkus", + "Nicholas Cahill", + "Nikita Lesnikov", + "Oleg Leonov", + "Oliver Steele", + "Omar Zrien", + "Owen Campbell-Moore", + "Patrick Lucas", + "Peter Ehrnstrom", + "Peter Mcinerney", + "Pierre Lancien", + "Quantopian", + "Randy C. Will", + "rehmi post", + "Rex Godby", + "Ripta Pasay", + "Rish Kundalia", + "Roman Sergeychik", + "Roobie", + "Ryan Atallah", + "Samuel Judge", + "SansWord Huang", + "Scott Gray", + "Scott Walter, Ph.D.", + "Sebastian Garcia", + "soekul", + "Solara570", + "Steve Huynh", + "Steve Sperandeo", + "Steven Braun", + "Steven Siddals", + "Stevie Metke", + "supershabam", + "Suteerth Vishnu", + "Suthen Thomas", + "Tal Einav", + "Tauba Auerbach", + "Ted Suzman", + "Thomas J Sargent", + "Thomas Tarler", + "Tianyu Ge", + "Tihan Seale", + "Tyler VanValkenburg", + "Vassili Philippov", + "Veritasium", + "Vinicius Reis", + "Xuanji Li", + "Yana Chernobilsky", + "Yavor Ivanov", + "YinYangBalance.Asia", + "Yu Jun", + "Yurii Monastyrshyn", + ] + } diff --git a/from_3b1b/old/ctracing.py b/from_3b1b/old/ctracing.py new file mode 100644 index 0000000000..9974b7d8b6 --- /dev/null +++ b/from_3b1b/old/ctracing.py @@ -0,0 +1,754 @@ +from manimlib.imports import * +from from_3b1b.old.sir import * + + +class LastFewMonths(Scene): + def construct(self): + words = TextMobject("Last ", "few\\\\", "months:") + words.set_height(4) + underlines = VGroup() + for word in words: + underline = Line(LEFT, RIGHT) + underline.match_width(word) + underline.next_to(word, DOWN, SMALL_BUFF) + underlines.add(underline) + underlines[0].stretch(1.4, 0, about_edge=LEFT) + underlines.set_color(BLUE) + + # self.play(ShowCreation(underlines)) + self.play(ShowIncreasingSubsets(words, run_time=0.75, rate_func=linear)) + self.wait() + + +class UnemploymentTitle(Scene): + def construct(self): + words = TextMobject("Unemployment claims\\\\per week in the US")[0] + words.set_width(FRAME_WIDTH - 1) + words.to_edge(UP) + arrow = Arrow( + words.get_bottom(), + words.get_bottom() + 3 * RIGHT + 3 * DOWN, + stroke_width=10, + tip_length=0.5, + ) + arrow.set_color(BLUE_E) + words.set_color(BLACK) + self.play( + ShowIncreasingSubsets(words), + ShowCreation(arrow), + ) + self.wait() + + +class ExplainTracing(Scene): + def construct(self): + # Words + words = VGroup( + TextMobject("Testing, ", "Testing, ", "Testing!"), + TextMobject("Contact Tracing"), + ) + words[0].set_color(GREEN) + words[1].set_color(BLUE_B) + words.set_width(FRAME_WIDTH - 2) + words.arrange(DOWN, buff=1) + + self.play(ShowIncreasingSubsets(words[0], rate_func=linear)) + self.wait() + self.play(Write(words[1], run_time=1)) + self.wait() + + self.play( + words[1].to_edge, UP, + FadeOut(words[0], 6 * UP) + ) + + ct_word = words[1][0] + + # Groups + clusters = VGroup() + for x in range(4): + cluster = VGroup() + for y in range(4): + cluster.add(Randolph()) + cluster.arrange_in_grid(buff=LARGE_BUFF) + clusters.add(cluster) + clusters.scale(0.5) + clusters.arrange_in_grid(buff=2) + clusters.set_height(4) + + self.play(FadeIn(clusters)) + + pis = VGroup() + boxes = VGroup() + for cluster in clusters: + for pi in cluster: + pis.add(pi) + box = SurroundingRectangle(pi, buff=0.05) + boxes.add(box) + pi.box = box + + boxes.set_stroke(WHITE, 1) + + sicky = clusters[0][2] + covid_words = TextMobject("COVID-19\\\\Positive!") + covid_words.set_color(RED) + arrow = Vector(RIGHT, color=RED) + arrow.next_to(sicky, LEFT) + covid_words.next_to(arrow, LEFT, SMALL_BUFF) + + self.play( + sicky.change, "sick", + sicky.set_color, "#9BBD37", + FadeIn(covid_words, RIGHT), + GrowArrow(arrow), + ) + self.play(ShowCreation(sicky.box)) + self.wait(2) + anims = [] + for pi in clusters[0]: + if pi is not sicky: + anims.append(ApplyMethod(pi.change, "tired")) + anims.append(ShowCreation(pi.box)) + self.play(*anims) + self.wait() + + self.play(VFadeIn( + boxes[4:], + run_time=2, + rate_func=there_and_back_with_pause, + )) + self.wait() + + self.play(FadeOut( + VGroup( + covid_words, + arrow, + *boxes[:4], + *pis, + ), + lag_ratio=0.1, + run_time=3, + )) + self.play(ct_word.move_to, 2 * UP) + + # Underlines + implies = TexMobject("\\Downarrow") + implies.scale(2) + implies.next_to(ct_word, DOWN, MED_LARGE_BUFF) + loc_tracking = TextMobject("Location Tracking") + loc_tracking.set_color(GREY_BROWN) + loc_tracking.match_height(ct_word) + loc_tracking.next_to(implies, DOWN, MED_LARGE_BUFF) + + q_marks = TexMobject("???") + q_marks.scale(2) + q_marks.next_to(implies, RIGHT) + + cross = Cross(implies) + cross.set_stroke(RED, 7) + + self.play( + Write(implies), + FadeIn(loc_tracking, UP) + ) + self.play(FadeIn(q_marks, lag_ratio=0.1)) + self.wait() + + parts = VGroup(ct_word[:7], ct_word[7:]) + lines = VGroup() + for part in parts: + line = Line(part.get_left(), part.get_right()) + line.align_to(part[0], DOWN) + line.shift(0.1 * DOWN) + lines.add(line) + + ct_word.set_stroke(BLACK, 2, background=True) + self.add(lines[1], ct_word) + self.play(ShowCreation(lines[1])) + self.wait() + self.play(ShowCreation(lines[0])) + self.wait() + + self.play( + ShowCreation(cross), + FadeOut(q_marks, RIGHT), + FadeOut(lines), + ) + self.wait() + + dp_3t = TextMobject("DP-3T") + dp_3t.match_height(ct_word) + dp_3t.move_to(loc_tracking) + dp_3t_long = TextMobject("Decentralized Privacy-Preserving Proximity Tracing") + dp_3t_long.next_to(dp_3t, DOWN, LARGE_BUFF) + + arrow = Vector(UP) + arrow.set_stroke(width=8) + arrow.move_to(implies) + + self.play( + FadeInFromDown(dp_3t), + FadeOut(loc_tracking), + FadeOut(implies), + FadeOut(cross), + ShowCreation(arrow) + ) + self.play(Write(dp_3t_long)) + self.wait() + + +class ContactTracingMisnomer(Scene): + def construct(self): + # Word play + words = TextMobject("Contact ", "Tracing") + words.scale(2) + rects = VGroup(*[ + SurroundingRectangle(word, buff=0.2) + for word in words + ]) + expl1 = TextMobject("Doesn't ``trace'' you...") + expl2 = TextMobject("...or your contacts") + expls = VGroup(expl1, expl2) + colors = [RED, BLUE] + + self.add(words) + for vect, rect, expl, color in zip([UP, DOWN], reversed(rects), expls, colors): + arrow = Vector(-vect) + arrow.next_to(rect, vect, SMALL_BUFF) + expl.next_to(arrow, vect, SMALL_BUFF) + rect.set_color(color) + arrow.set_color(color) + expl.set_color(color) + + self.play( + FadeIn(expl, -vect), + GrowArrow(arrow), + ShowCreation(rect), + ) + self.wait() + + self.play(Write( + VGroup(*self.mobjects), + rate_func=lambda t: smooth(1 - t), + run_time=3, + )) + + +class ContactTracingWords(Scene): + def construct(self): + words = TextMobject("Contact\\\\", "Tracing") + words.set_height(4) + for word in words: + self.add(word) + self.wait() + self.wait() + return + self.play(ShowIncreasingSubsets(words)) + self.wait() + self.play( + words.set_height, 1, + words.to_corner, UL, + ) + self.wait() + + +class WanderingDotsWithLines(Scene): + def construct(self): + sim = SIRSimulation( + city_population=20, + person_type=DotPerson, + person_config={ + "color_map": { + "S": GREY, + "I": GREY, + "R": GREY, + }, + "infection_ring_style": { + "stroke_color": YELLOW, + }, + "max_speed": 0.5, + }, + infection_time=100, + ) + + for person in sim.people: + person.set_status("S") + person.infection_start_time += random.random() + + lines = VGroup() + + max_dist = 1.25 + + def update_lines(lines): + lines.remove(*lines.submobjects) + for p1 in sim.people: + for p2 in sim.people: + if p1 is p2: + continue + dist = get_norm(p1.get_center() - p2.get_center()) + if dist < max_dist: + line = Line(p1.get_center(), p2.get_center()) + alpha = (max_dist - dist) / max_dist + line.set_stroke( + interpolate_color(WHITE, RED, alpha), + width=4 * alpha + ) + lines.add(line) + + lines.add_updater(update_lines) + + self.add(lines) + self.add(sim) + self.wait(10) + for person in sim.people: + person.set_status("I") + person.infection_start_time += random.random() + self.wait(50) + + +class WhatAboutPeopleWithoutPhones(TeacherStudentsScene): + def construct(self): + self.student_says( + "What about people\\\\without phones?", + target_mode="sassy", + added_anims=[self.teacher.change, "guilty"] + ) + self.change_student_modes("angry", "angry", "sassy") + self.wait() + self.play(self.teacher.change, "tease") + self.wait() + + words = VectorizedPoint() + words.scale(1.5) + words.to_corner(UL) + + self.play( + FadeInFromDown(words), + RemovePiCreatureBubble(self.students[2]), + *[ + ApplyMethod(pi.change, "pondering", words) + for pi in self.pi_creatures + ] + ) + self.wait(5) + + +class PiGesture1(Scene): + def construct(self): + randy = Randolph(mode="raise_right_hand", height=2) + bubble = randy.get_bubble( + bubble_class=SpeechBubble, + height=2, width=3, + ) + bubble.write("This one's\\\\great") + bubble.content.scale(0.8) + bubble.content.set_color(BLACK) + bubble.set_color(BLACK) + bubble.set_fill(opacity=0) + randy.set_stroke(BLACK, 5, background=True) + self.add(randy, bubble, bubble.content) + + +class PiGesture2(Scene): + def construct(self): + randy = Randolph(mode="raise_left_hand", height=2) + randy.look(UL) + # randy.flip() + randy.set_color(GREY_BROWN) + bubble = randy.get_bubble( + bubble_class=SpeechBubble, + height=2, width=3, + direction=LEFT, + ) + bubble.write("So is\\\\this one") + bubble.content.scale(0.8) + bubble.content.set_color(BLACK) + bubble.set_color(BLACK) + bubble.set_fill(opacity=0) + randy.set_stroke(BLACK, 5, background=True) + self.add(randy, bubble, bubble.content) + + +class PiGesture3(Scene): + def construct(self): + randy = Randolph(mode="hooray", height=2) + randy.flip() + bubble = randy.get_bubble( + bubble_class=SpeechBubble, + height=2, width=3, + direction=LEFT, + ) + bubble.write("And this\\\\one") + bubble.content.scale(0.8) + bubble.content.set_color(BLACK) + bubble.set_color(BLACK) + bubble.set_fill(opacity=0) + randy.set_stroke(BLACK, 5, background=True) + self.add(randy, bubble, bubble.content) + + +class AppleGoogleCoop(Scene): + def construct(self): + logos = Group( + self.get_apple_logo(), + self.get_google_logo(), + ) + for logo in logos: + logo.set_height(2) + apple, google = logos + + logos.arrange(RIGHT, buff=3) + + arrows = VGroup() + for vect, u in zip([UP, DOWN], [0, 1]): + m1, m2 = logos[u], logos[1 - u] + arrows.add(Arrow( + m1.get_edge_center(vect), + m2.get_edge_center(vect), + path_arc=-90 * DEGREES, + buff=MED_LARGE_BUFF, + stroke_width=10, + )) + + self.play(LaggedStart( + Write(apple), + FadeIn(google), + lag_ratio=0.7, + )) + self.wait() + self.play(ShowCreation(arrows, run_time=2)) + self.wait() + + def get_apple_logo(self): + result = SVGMobject("apple_logo") + result.set_color("#b3b3b3") + return result + + def get_google_logo(self): + result = ImageMobject("google_logo_black") + return result + + +class LocationTracking(Scene): + def construct(self): + question = TextMobject( + "Would you like this company to track\\\\", + "and occasionally sell your location?" + ) + question.to_edge(UP, buff=LARGE_BUFF) + + slider = Rectangle(width=1.25, height=0.5) + slider.round_corners(radius=0.25) + slider.set_fill(GREEN, 1) + slider.next_to(question, DOWN, buff=MED_LARGE_BUFF) + + dot = Dot(radius=0.25) + dot.set_fill(GREY_C, 1) + dot.set_stroke(WHITE, 3) + dot.move_to(slider, RIGHT) + + morty = Mortimer() + morty.next_to(slider, RIGHT) + morty.to_edge(DOWN) + + bubble = morty.get_bubble( + height=2, + width=3, + direction=LEFT, + ) + + answer = TextMobject("Um...", "no.") + answer.set_height(0.4) + answer.set_color(YELLOW) + bubble.add_content(answer) + + self.add(morty) + + self.play( + FadeInFromDown(question), + Write(slider), + FadeIn(dot), + ) + self.play(morty.change, "confused", slider) + self.play(Blink(morty)) + self.play( + FadeIn(bubble), + Write(answer[0]), + ) + self.wait() + self.play( + dot.move_to, slider, LEFT, + slider.set_fill, {"opacity": 0}, + FadeIn(answer[1]), + morty.change, "sassy" + ) + self.play(Blink(morty)) + self.wait(2) + self.play(Blink(morty)) + self.wait(2) + + +class MoreLinks(Scene): + def construct(self): + words = TextMobject("See more links\\\\in the description.") + words.scale(2) + words.to_edge(UP, buff=2) + arrows = VGroup(*[ + Vector(1.5 * DOWN, stroke_width=10) + for x in range(4) + ]) + arrows.arrange(RIGHT, buff=0.75) + arrows.next_to(words, DOWN, buff=0.5) + for arrow, color in zip(arrows, [BLUE_D, BLUE_C, BLUE_E, GREY_BROWN]): + arrow.set_color(color) + self.play(Write(words)) + self.play(LaggedStartMap(ShowCreation, arrows)) + self.wait() + + +class LDMEndScreen(PatreonEndScreen): + CONFIG = { + "scroll_time": 20, + "specific_patrons": [ + "1stViewMaths", + "Aaron", + "Adam Dřínek", + "Adam Margulies", + "Aidan Shenkman", + "Alan Stein", + "Albin Egasse", + "Alex Mijalis", + "Alexander Mai", + "Alexis Olson", + "Ali Yahya", + "Andreas Snekloth Kongsgaard", + "Andrew Busey", + "Andrew Cary", + "Andrew R. Whalley", + "Aravind C V", + "Arjun Chakroborty", + "Arthur Zey", + "Ashwin Siddarth", + "Augustine Lim", + "Austin Goodman", + "Avi Finkel", + "Awoo", + "Axel Ericsson", + "Ayan Doss", + "AZsorcerer", + "Barry Fam", + "Bartosz Burclaf", + "Ben Delo", + "Benjamin Bailey", + "Bernd Sing", + "Bill Gatliff", + "Boris Veselinovich", + "Bradley Pirtle", + "Brandon Huang", + "Brendan Shah", + "Brian Cloutier", + "Brian Staroselsky", + "Britt Selvitelle", + "Britton Finley", + "Burt Humburg", + "Calvin Lin", + "Carl-Johan R. Nordangård", + "Charles Southerland", + "Charlie N", + "Chris Connett", + "Chris Druta", + "Christian Kaiser", + "cinterloper", + "Clark Gaebel", + "Colwyn Fritze-Moor", + "Corey Ogburn", + "D. Sivakumar", + "Dan Herbatschek", + "Daniel Brown", + "Daniel Herrera C", + "Darrell Thomas", + "Dave B", + "Dave Cole", + "Dave Kester", + "dave nicponski", + "David B. Hill", + "David Clark", + "David Gow", + "Delton Ding", + "Dominik Wagner", + "Eduardo Rodriguez", + "Emilio Mendoza", + "emptymachine", + "Eric Younge", + "Eryq Ouithaqueue", + "Federico Lebron", + "Fernando Via Canel", + "Frank R. Brown, Jr.", + "gary", + "Giovanni Filippi", + "Goodwine", + "Hal Hildebrand", + "Heptonion", + "Hitoshi Yamauchi", + "Isaac Gubernick", + "Ivan Sorokin", + "Jacob Baxter", + "Jacob Harmon", + "Jacob Hartmann", + "Jacob Magnuson", + "Jalex Stark", + "Jameel Syed", + "James Beall", + "Jason Hise", + "Jayne Gabriele", + "Jean-Manuel Izaret", + "Jeff Dodds", + "Jeff Linse", + "Jeff Straathof", + "Jeffrey Wolberg", + "Jimmy Yang", + "Joe Pregracke", + "Johan Auster", + "John C. Vesey", + "John Camp", + "John Haley", + "John Le", + "John Luttig", + "John Rizzo", + "John V Wertheim", + "jonas.app", + "Jonathan Heckerman", + "Jonathan Wilson", + "Joseph John Cox", + "Joseph Kelly", + "Josh Kinnear", + "Joshua Claeys", + "Joshua Ouellette", + "Juan Benet", + "Julien Dubois", + "Kai-Siang Ang", + "Kanan Gill", + "Karl Niu", + "Kartik Cating-Subramanian", + "Kaustuv DeBiswas", + "Killian McGuinness", + "kkm", + "Klaas Moerman", + "Kristoffer Börebäck", + "Kros Dai", + "L0j1k", + "Lael S Costa", + "LAI Oscar", + "Lambda GPU Workstations", + "Laura Gast", + "Lee Redden", + "Linh Tran", + "Luc Ritchie", + "Ludwig Schubert", + "Lukas Biewald", + "Lukas Zenick", + "Magister Mugit", + "Magnus Dahlström", + "Magnus Hiie", + "Manoj Rewatkar - RITEK SOLUTIONS", + "Mark B Bahu", + "Mark Heising", + "Mark Hopkins", + "Mark Mann", + "Martin Price", + "Mathias Jansson", + "Matt Godbolt", + "Matt Langford", + "Matt Roveto", + "Matt Russell", + "Matteo Delabre", + "Matthew Bouchard", + "Matthew Cocke", + "Maxim Nitsche", + "Michael Bos", + "Michael Hardel", + "Michael W White", + "Mirik Gogri", + "Molly Mackinlay", + "Mustafa Mahdi", + "Márton Vaitkus", + "Nero Li", + "Nicholas Cahill", + "Nikita Lesnikov", + "Nitu Kitchloo", + "Oleg Leonov", + "Oliver Steele", + "Omar Zrien", + "Omer Tuchfeld", + "Patrick Gibson", + "Patrick Lucas", + "Pavel Dubov", + "Pesho Ivanov", + "Petar Veličković", + "Peter Ehrnstrom", + "Peter Francis", + "Peter Mcinerney", + "Pierre Lancien", + "Pradeep Gollakota", + "Rafael Bove Barrios", + "Raghavendra Kotikalapudi", + "Randy C. Will", + "rehmi post", + "Rex Godby", + "Ripta Pasay", + "Rish Kundalia", + "Roman Sergeychik", + "Roobie", + "Ryan Atallah", + "Samuel Judge", + "SansWord Huang", + "Scott Gray", + "Scott Walter, Ph.D.", + "soekul", + "Solara570", + "Spyridon Michalakis", + "Stephen Shanahan", + "Steve Huynh", + "Steve Muench", + "Steve Sperandeo", + "Steven Siddals", + "Stevie Metke", + "Sundar Subbarayan", + "supershabam", + "Suteerth Vishnu", + "Suthen Thomas", + "Tal Einav", + "Taras Bobrovytsky", + "Tauba Auerbach", + "Ted Suzman", + "Terry Hayes", + "THIS IS THE point OF NO RE tUUurRrhghgGHhhnnn", + "Thomas J Sargent", + "Thomas Tarler", + "Tianyu Ge", + "Tihan Seale", + "Tim Erbes", + "Tim Kazik", + "Tomasz Legutko", + "Tyler Herrmann", + "Tyler Parcell", + "Tyler VanValkenburg", + "Tyler Veness", + "Ubiquity Ventures", + "Vassili Philippov", + "Vasu Dubey", + "Veritasium", + "Vignesh Ganapathi Subramanian", + "Vinicius Reis", + "Vladimir Solomatin", + "Wooyong Ee", + "Xuanji Li", + "Yana Chernobilsky", + "Yavor Ivanov", + "Yetinother", + "YinYangBalance.Asia", + "Yu Jun", + "Yurii Monastyrshyn", + "Zachariah Rosenberg", + ], + } diff --git a/from_3b1b/old/dandelin.py b/from_3b1b/old/dandelin.py index d89482c17c..e2c5abd807 100644 --- a/from_3b1b/old/dandelin.py +++ b/from_3b1b/old/dandelin.py @@ -67,7 +67,7 @@ def construct(self): self.add(bubble) self.play( - FadeInFrom(you, LEFT), + FadeIn(you, LEFT), GrowArrow(you_arrow), ) self.play( @@ -293,7 +293,7 @@ def construct(self): self.play( GrowArrow(xy_arrow), Write(xy), - FadeInFrom(start_point, UP), + FadeIn(start_point, UP), ) self.wait() self.add(circle_ghost) @@ -508,7 +508,7 @@ def construct(self): eccentricity_label, lambda a: self.get_eccentricity(comet_orbit) ), - FadeOutAndShift(earth_orbit_words, UP), + FadeOut(earth_orbit_words, UP), FadeInFromDown(comet_orbit_words) ) self.add(orbiting_comet) @@ -753,8 +753,8 @@ def construct(self): baby_morty.to_corner(DL) self.play( - FadeOutAndShift(bubble), - FadeOutAndShift(bubble.content), + FadeOut(bubble), + FadeOut(bubble.content), LaggedStartMap( FadeOutAndShift, self.students, lambda m: (m, 3 * DOWN), @@ -832,7 +832,7 @@ def construct(self): run_time=3, )) self.wait() - self.play(FadeOutAndShift(arrows[1:])) + self.play(FadeOut(arrows[1:])) self.wait() @@ -869,7 +869,7 @@ def construct(self): self.wait() self.play( GrowArrow(arrow), - FadeInFrom(words, RIGHT), + FadeIn(words, RIGHT), self.get_student_changes( "thinking", "happy", "pondering", look_at_arg=arrow @@ -1274,7 +1274,7 @@ def construct(self): self.play(FadeInFromDown(portrait)) self.play(Write(title[1])) self.wait() - self.play(FadeInFrom(google_result, LEFT)) + self.play(FadeIn(google_result, LEFT)) self.play(Write(cmon_google, run_time=1)) self.wait() @@ -1332,7 +1332,7 @@ def construct(self): for pi, mode in (randy, "hooray"), (other, "tired"): self.play( GrowArrow(pi.arrow), - FadeInFrom(pi.label, RIGHT), + FadeIn(pi.label, RIGHT), pi.change, mode, ) self.play( @@ -1390,7 +1390,7 @@ def construct(self): {"buff": LARGE_BUFF, "aligned_edge": UP}, randy.change, "pondering", VFadeIn(randy), - FadeOutAndShift(dandelin, DOWN), + FadeOut(dandelin, DOWN), ) self.play( diff --git a/from_3b1b/old/div_curl.py b/from_3b1b/old/div_curl.py index 90731ce2a9..a65819fd0c 100644 --- a/from_3b1b/old/div_curl.py +++ b/from_3b1b/old/div_curl.py @@ -2216,7 +2216,7 @@ def comment_on_relevant_region(self): twig, rate=-90 * DEGREES, ) - self.play(FadeInFrom(twig, UP)) + self.play(FadeIn(twig, UP)) self.add(twig_rotation) self.wait(16) @@ -3621,7 +3621,7 @@ def zoom_in(self): ) self.add_foreground_mobjects(input_dot) self.play( - FadeInFrom(input_dot, SMALL_BUFF * DL), + FadeIn(input_dot, SMALL_BUFF * DL), Write(input_words), ) self.play( @@ -3924,13 +3924,13 @@ def switch_to_curl_words(self): dot_product.fade, 1, remover=True ) - self.play(FadeInFrom(cross_product, sf * DOWN)) + self.play(FadeIn(cross_product, sf * DOWN)) self.play( div_text.shift, sf * DOWN, div_text.fade, 1, remover=True ) - self.play(FadeInFrom(curl_text, sf * DOWN)) + self.play(FadeIn(curl_text, sf * DOWN)) self.wait() def rotate_difference_vectors(self): @@ -4358,7 +4358,7 @@ def construct(self): self.play( FadeIn(left_text), - FadeInFrom(knob, 2 * RIGHT) + FadeIn(knob, 2 * RIGHT) ) self.wait() self.play( diff --git a/from_3b1b/old/domino_play.py b/from_3b1b/old/domino_play.py index e150739163..ff47d1e40a 100644 --- a/from_3b1b/old/domino_play.py +++ b/from_3b1b/old/domino_play.py @@ -841,7 +841,7 @@ def construct(self): arcs = VGroup(arc1, arc2) for arc, vect in zip(arcs, [DOWN+RIGHT, RIGHT]): arc_copy = arc.copy() - point = domino1.get_critical_point(vect) + point = domino1.get_bounding_box_point(vect) arc_copy.add_line_to([point]) arc_copy.set_stroke(width = 0) arc_copy.set_fill( diff --git a/from_3b1b/old/efvgt.py b/from_3b1b/old/efvgt.py index 56880d5374..c5e3251b2b 100644 --- a/from_3b1b/old/efvgt.py +++ b/from_3b1b/old/efvgt.py @@ -3220,7 +3220,7 @@ def construct(self): self.play(Blink(randy)) self.wait(3) -class EfvgtPatreonThanks(PatreonThanks): +class EfvgtPatreonThanks(PatreonEndScreen): CONFIG = { "specific_patrons" : [ "Ali Yahya", diff --git a/from_3b1b/old/eoc/chapter3.py b/from_3b1b/old/eoc/chapter3.py index 736bf0dd82..e100b01554 100644 --- a/from_3b1b/old/eoc/chapter3.py +++ b/from_3b1b/old/eoc/chapter3.py @@ -94,7 +94,7 @@ def get_car_anim(self, alignement_mob): def get_spring_anim(self, alignement_mob): compact_spring, extended_spring = [ - ParametricFunction( + ParametricCurve( lambda t : (t/denom)*RIGHT+np.sin(t)*UP+np.cos(t)*OUT, t_max = 12*np.pi, ) diff --git a/from_3b1b/old/eoc/chapter4.py b/from_3b1b/old/eoc/chapter4.py index c36b606c68..0e76d32cbd 100644 --- a/from_3b1b/old/eoc/chapter4.py +++ b/from_3b1b/old/eoc/chapter4.py @@ -205,7 +205,7 @@ def construct(self): class DampenedSpring(Scene): def construct(self): compact_spring, extended_spring = [ - ParametricFunction( + ParametricCurve( lambda t : (t/denom)*RIGHT+np.sin(t)*UP+np.cos(t)*OUT, t_max = 12*np.pi, color = GREY, diff --git a/from_3b1b/old/eoc/chapter6.py b/from_3b1b/old/eoc/chapter6.py index e6beaa4c39..4ad37a07ab 100644 --- a/from_3b1b/old/eoc/chapter6.py +++ b/from_3b1b/old/eoc/chapter6.py @@ -560,7 +560,7 @@ class Ladder(VMobject): "width" : 1, "n_rungs" : 7, } - def generate_points(self): + def init_points(self): left_line, right_line = [ Line(ORIGIN, self.height*UP).shift(self.width*vect/2.0) for vect in (LEFT, RIGHT) diff --git a/from_3b1b/old/eoc/chapter7.py b/from_3b1b/old/eoc/chapter7.py index 4f5c17b8f4..096fb8b929 100644 --- a/from_3b1b/old/eoc/chapter7.py +++ b/from_3b1b/old/eoc/chapter7.py @@ -2468,7 +2468,7 @@ def add_graphs(self): x_val = 3, direction = RIGHT ) - g_graph = ParametricFunction( + g_graph = ParametricCurve( lambda y : self.coords_to_point(np.exp(y)+self.a_value-1, y), t_min = self.y_min, t_max = self.y_max, diff --git a/from_3b1b/old/eoc/old_chapter1.py b/from_3b1b/old/eoc/old_chapter1.py index d3356d5fd5..33884aff92 100644 --- a/from_3b1b/old/eoc/old_chapter1.py +++ b/from_3b1b/old/eoc/old_chapter1.py @@ -1427,7 +1427,7 @@ def func(alpha): y = y_axis.number_to_point(output)[1] return x*RIGHT + y*UP - graph = ParametricFunction(func, color = BLUE) + graph = ParametricCurve(func, color = BLUE) graph_label = TexMobject("A(R) = \\pi R^2") graph_label.set_color(BLUE) graph_label.next_to( diff --git a/from_3b1b/old/eola/chapter1.py b/from_3b1b/old/eola/chapter1.py index 8f0b8efaaf..bf597bcfb0 100644 --- a/from_3b1b/old/eola/chapter1.py +++ b/from_3b1b/old/eola/chapter1.py @@ -1147,7 +1147,7 @@ def construct(self): class DataAnalyst(Scene): def construct(self): plane = NumberPlane() - ellipse = ParametricFunction( + ellipse = ParametricCurve( lambda x : 2*np.cos(x)*(UP+RIGHT) + np.sin(x)*(UP+LEFT), color = PINK, t_max = 2*np.pi diff --git a/from_3b1b/old/eola/chapter11.py b/from_3b1b/old/eola/chapter11.py index 7d9931b45a..930052826a 100644 --- a/from_3b1b/old/eola/chapter11.py +++ b/from_3b1b/old/eola/chapter11.py @@ -254,7 +254,7 @@ class HyperCube(VMobject): "color2" : BLUE_D, "dims" : 4, } - def generate_points(self): + def init_points(self): corners = np.array(list(map(np.array, it.product(*[(-1, 1)]*self.dims)))) def project(four_d_array): result = four_d_array[:3] diff --git a/from_3b1b/old/eola/chapter6.py b/from_3b1b/old/eola/chapter6.py index 6f0992f7ad..9ed9c22920 100644 --- a/from_3b1b/old/eola/chapter6.py +++ b/from_3b1b/old/eola/chapter6.py @@ -154,7 +154,7 @@ class StockLine(VMobject): "num_points" : 15, "step_range" : 2 } - def generate_points(self): + def init_points(self): points = [ORIGIN] for x in range(self.num_points): step_size = self.step_range*(random.random() - 0.5) diff --git a/from_3b1b/old/for_flammy.py b/from_3b1b/old/for_flammy.py index fbd7045fb4..e8c10ad48b 100644 --- a/from_3b1b/old/for_flammy.py +++ b/from_3b1b/old/for_flammy.py @@ -179,7 +179,7 @@ def show_radial_line(self): ring.set_fill, {"opacity": 0.5}, ring.set_stroke, {"opacity": 0.1}, ShowCreation(R_line), - FadeInFrom(R_label, IN), + FadeIn(R_label, IN), ] ) self.wait() @@ -191,7 +191,7 @@ def show_radial_line(self): self.wait() self.play( ShowCreation(r_line), - FadeInFrom(r_label, IN), + FadeIn(r_label, IN), ) self.wait() self.move_camera( @@ -267,7 +267,7 @@ def construct(self): self.add(int_sign) self.play( GrowFromCenter(area_brace), - FadeInFrom(area_text, UP), + FadeIn(area_text, UP), ) self.wait() self.play(FadeInFromDown(circumference)) @@ -283,7 +283,7 @@ def construct(self): circumference.shift, SMALL_BUFF * UR, GrowFromCenter(circum_brace), ) - self.play(FadeInFrom(circum_formula, UP)) + self.play(FadeIn(circum_formula, UP)) self.wait() self.play( thickness.next_to, circumference, RIGHT, MED_SMALL_BUFF, @@ -291,7 +291,7 @@ def construct(self): area_brace.stretch, 0.84, 0, {"about_edge": LEFT}, MaintainPositionRelativeTo(area_text, area_brace), ) - self.play(FadeInFrom(R_dtheta, UP)) + self.play(FadeIn(R_dtheta, UP)) self.wait() self.play(ReplacementTransform(all_rings, bounds)) self.wait() @@ -308,7 +308,7 @@ def construct(self): one = TexMobject("1") one.move_to(q_marks) - self.play(FadeInFrom(rhs, 4 * LEFT)) + self.play(FadeIn(rhs, 4 * LEFT)) self.wait() self.play(ShowCreationThenFadeAround(rhs[1])) self.wait() diff --git a/from_3b1b/old/fourier.py b/from_3b1b/old/fourier.py index 72427d0896..14c2af85bc 100644 --- a/from_3b1b/old/fourier.py +++ b/from_3b1b/old/fourier.py @@ -664,7 +664,7 @@ class Quadrant(VMobject): "density" : 50, "density_exp" : 2.0, } - def generate_points(self): + def init_points(self): points = [r*RIGHT for r in np.arange(0, self.radius, 1./self.density)] points += [ self.radius*(np.cos(theta)*RIGHT + np.sin(theta)*UP) @@ -1417,7 +1417,7 @@ def introduce_frequency_plot(self): RIGHT, aligned_edge = UP, buff = LARGE_BUFF ) x_coord_label.add_background_rectangle() - flower_path = ParametricFunction( + flower_path = ParametricCurve( lambda t : self.circle_plane.coords_to_point( np.sin(2*t)*np.cos(t), np.sin(2*t)*np.sin(t), @@ -2704,7 +2704,7 @@ def show_plane_as_complex_plane(self): ), ) number_label_update_anim.update(0) - flower_path = ParametricFunction( + flower_path = ParametricCurve( lambda t : plane.coords_to_point( np.sin(2*t)*np.cos(t), np.sin(2*t)*np.sin(t), @@ -4248,7 +4248,7 @@ def func(t): pol_graphs = VGroup() for f in np.linspace(1.98, 2.02, 7): - pol_graph = ParametricFunction( + pol_graph = ParametricCurve( lambda t : complex_to_R3( (2+np.cos(2*TAU*t)+np.cos(3*TAU*t))*np.exp(-complex(0, TAU*f*t)) ), diff --git a/from_3b1b/old/hilbert/section2.py b/from_3b1b/old/hilbert/section2.py index 8c5dde1248..f0eedd357f 100644 --- a/from_3b1b/old/hilbert/section2.py +++ b/from_3b1b/old/hilbert/section2.py @@ -479,11 +479,11 @@ def spiril(t): theta = 2*np.pi*t return t*np.cos(theta)*RIGHT+t*np.sin(theta)*UP - self.spiril1 = ParametricFunction( + self.spiril1 = ParametricCurve( lambda t : 1.5*RIGHT + DOWN + 2*spiril(t), density = 5*DEFAULT_POINT_DENSITY_1D, ) - self.spiril2 = ParametricFunction( + self.spiril2 = ParametricCurve( lambda t : 5.5*RIGHT + UP - 2*spiril(1-t), density = 5*DEFAULT_POINT_DENSITY_1D, ) diff --git a/from_3b1b/old/hyperdarts.py b/from_3b1b/old/hyperdarts.py index 23e76e7f00..660c49631c 100644 --- a/from_3b1b/old/hyperdarts.py +++ b/from_3b1b/old/hyperdarts.py @@ -594,7 +594,7 @@ def construct(self): self.play( circle.set_color, DARK_GREY, TransformFromCopy(chord, chord_copy), - FadeInFrom(new_diam_word, UP) + FadeIn(new_diam_word, UP) ) self.play( Rotate(chord_copy, PI), @@ -2056,7 +2056,7 @@ def construct(self): self.add(mean_label) self.play( mean_label[1:].shift, LEFT, - FadeInFrom(equation, LEFT) + FadeIn(equation, LEFT) ) p_parts = VGroup() @@ -2140,7 +2140,7 @@ def construct(self): bars[0].set_opacity, 0.2, bars[1:].set_opacity, 0.8, ShowCreationThenFadeOut(outlines), - FadeInFrom(label, LEFT), + FadeIn(label, LEFT), ) self.wait() @@ -2174,7 +2174,7 @@ def construct(self): self.play( label.shift, shift_val, - FadeInFrom(rhs, LEFT) + FadeIn(rhs, LEFT) ) self.wait() @@ -2223,7 +2223,7 @@ def construct(self): self.play( MoveToTarget(new_label), - FadeInFrom(new_rhs, LEFT) + FadeIn(new_rhs, LEFT) ) self.wait() @@ -2372,7 +2372,7 @@ def construct(self): self.wait() self.play( FadeOut(braces[:3]), - FadeInFrom(gen_form, UP), + FadeIn(gen_form, UP), ) self.wait() @@ -2522,7 +2522,7 @@ def introduce_bullseye(self): self.add(radius, self.circle_center_dot) self.play( ShowCreation(radius), - FadeInFrom(radius_label, RIGHT), + FadeIn(radius_label, RIGHT), FadeIn(self.circle_center_dot), ) self.play( @@ -2588,7 +2588,7 @@ def show_shrink_rule(self): point = 0.2 * circle.point_from_proportion(3 / 8) self.play( FadeInFromDown(new_label), - FadeOutAndShift(label, UP), + FadeOut(label, UP), ) self.show_full_hit_process(point) self.wait() @@ -2666,8 +2666,8 @@ def increment_score(self): new_score = score.copy() new_score.increment_value(1) self.play( - FadeOutAndShift(score, UP), - FadeInFrom(new_score, DOWN), + FadeOut(score, UP), + FadeIn(new_score, DOWN), run_time=1, ) self.remove(new_score) @@ -2718,8 +2718,8 @@ def reset_board(self): new_score.set_value(0) self.play( self.circle.match_width, self.square, - FadeOutAndShift(score, UP), - FadeInFrom(new_score, DOWN), + FadeOut(score, UP), + FadeIn(new_score, DOWN), ) score.set_value(0) self.add(score) @@ -2804,7 +2804,7 @@ def show_random_points(self): def exchange_titles(self): self.play( FadeInFromDown(self.new_title), - FadeOutAndShift(self.title, UP), + FadeOut(self.title, UP), ) @@ -2821,6 +2821,6 @@ def construct(self): self.play(Write(equation)) self.wait(2) - self.play(FadeInFrom(aka, UP)) + self.play(FadeIn(aka, UP)) self.wait() diff --git a/from_3b1b/old/ldm.py b/from_3b1b/old/ldm.py new file mode 100644 index 0000000000..d6880eb4a2 --- /dev/null +++ b/from_3b1b/old/ldm.py @@ -0,0 +1,1445 @@ +from manimlib.imports import * +from from_3b1b.active.bayes.beta_helpers import * +import math + + +class StreamIntro(Scene): + def construct(self): + # Add logo + logo = Logo() + spikes = VGroup(*[ + spike + for layer in logo.spike_layers + for spike in layer + ]) + self.add(*logo.family_members_with_points()) + + # Add label + label = TextMobject("The lesson will\\\\begin shortly") + label.scale(2) + label.next_to(logo, DOWN) + self.add(label) + + self.camera.frame.move_to(DOWN) + + for spike in spikes: + point = spike.get_start() + spike.angle = angle_of_vector(point) + + anims = [] + for spike in spikes: + anims.append(Rotate( + spike, spike.angle * 28 * 2, + about_point=ORIGIN, + rate_func=linear, + )) + self.play(*anims, run_time=60 * 5) + self.wait(20) + + +class OldStreamIntro(Scene): + def construct(self): + morty = Mortimer() + morty.flip() + morty.set_height(2) + morty.to_corner(DL) + self.play(PiCreatureSays( + morty, "The lesson will\\\\begin soon.", + bubble_kwargs={ + "height": 2, + "width": 3, + }, + target_mode="hooray", + )) + bound = AnimatedBoundary(morty.bubble.content, max_stroke_width=1) + self.add(bound, morty.bubble, morty.bubble.content) + self.remove(morty.bubble.content) + morty.bubble.set_fill(opacity=0) + + self.camera.frame.scale(0.6, about_edge=DL) + + self.play(Blink(morty)) + self.wait(5) + self.play(Blink(morty)) + self.wait(3) + return + + text = TextMobject("The lesson will\\\\begin soon.") + text.set_height(1.5) + text.to_corner(DL, buff=LARGE_BUFF) + self.add(text) + + +class QuadraticFormula(TeacherStudentsScene): + def construct(self): + formula = TexMobject( + "\\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}", + ) + formula.next_to(self.students, UP, buff=MED_LARGE_BUFF, aligned_edge=LEFT) + self.add(formula) + + self.change_student_modes( + "angry", "tired", "sad", + look_at_arg=formula, + ) + self.teacher_says( + "It doesn't have\\\\to be this way.", + bubble_kwargs={ + "width": 4, + "height": 3, + } + ) + self.wait(5) + self.change_student_modes( + "pondering", "thinking", "erm", + look_at_arg=formula + ) + self.wait(12) + + +class SimplerQuadratic(Scene): + def construct(self): + tex = TexMobject("m \\pm \\sqrt{m^2 - p}") + tex.set_stroke(BLACK, 12, background=True) + tex.scale(1.5) + self.add(tex) + + +class CosGraphs(Scene): + def construct(self): + axes = Axes( + x_min=-0.75 * TAU, + x_max=0.75 * TAU, + y_min=-1.5, + y_max=1.5, + x_axis_config={ + "tick_frequency": PI / 4, + "include_tip": False, + }, + y_axis_config={ + "tick_frequency": 0.5, + "include_tip": False, + "unit_size": 1.5, + } + ) + + graph1 = axes.get_graph(np.cos) + graph2 = axes.get_graph(lambda x: np.cos(x)**2) + + graph1.set_stroke(YELLOW, 5) + graph2.set_stroke(BLUE, 5) + + label1 = TexMobject("\\cos(x)") + label2 = TexMobject("\\cos^2(x)") + + label1.match_color(graph1) + label1.set_height(0.75) + label1.next_to(axes.input_to_graph_point(-PI, graph1), DOWN) + + label2.match_color(graph2) + label2.set_height(0.75) + label2.next_to(axes.input_to_graph_point(PI, graph2), UP) + + for mob in [graph1, graph2, label1, label2]: + mc = mob.copy() + mc.set_stroke(BLACK, 10, background=True) + self.add(mc) + + self.add(axes) + self.add(graph1) + self.add(graph2) + self.add(label1) + self.add(label2) + + self.embed() + + +class SineWave(Scene): + def construct(self): + w_axes = self.get_wave_axes() + square, circle, c_axes = self.get_edge_group() + + self.add(w_axes) + self.add(square, circle, c_axes) + + theta_tracker = ValueTracker(0) + c_dot = Dot(color=YELLOW) + c_line = Line(DOWN, UP, color=GREEN) + w_dot = Dot(color=YELLOW) + w_line = Line(DOWN, UP, color=GREEN) + + def update_c_dot(dot, axes=c_axes, tracker=theta_tracker): + theta = tracker.get_value() + dot.move_to(axes.c2p( + np.cos(theta), + np.sin(theta), + )) + + def update_c_line(line, axes=c_axes, tracker=theta_tracker): + theta = tracker.get_value() + x = np.cos(theta) + y = np.sin(theta) + if y == 0: + y = 1e-6 + line.put_start_and_end_on( + axes.c2p(x, 0), + axes.c2p(x, y), + ) + + def update_w_dot(dot, axes=w_axes, tracker=theta_tracker): + theta = tracker.get_value() + dot.move_to(axes.c2p(theta, np.sin(theta))) + + def update_w_line(line, axes=w_axes, tracker=theta_tracker): + theta = tracker.get_value() + x = theta + y = np.sin(theta) + if y == 0: + y = 1e-6 + line.put_start_and_end_on( + axes.c2p(x, 0), + axes.c2p(x, y), + ) + + def get_partial_circle(circle=circle, tracker=theta_tracker): + result = circle.copy() + theta = tracker.get_value() + result.pointwise_become_partial( + circle, 0, clip(theta / TAU, 0, 1), + ) + result.set_stroke(RED, width=3) + return result + + def get_partial_wave(axes=w_axes, tracker=theta_tracker): + theta = tracker.get_value() + graph = axes.get_graph(np.sin, x_min=0, x_max=theta, step_size=0.025) + graph.set_stroke(BLUE, 3) + return graph + + def get_h_line(axes=w_axes, tracker=theta_tracker): + theta = tracker.get_value() + return Line( + axes.c2p(0, 0), + axes.c2p(theta, 0), + stroke_color=RED + ) + + c_dot.add_updater(update_c_dot) + c_line.add_updater(update_c_line) + w_dot.add_updater(update_w_dot) + w_line.add_updater(update_w_line) + partial_circle = always_redraw(get_partial_circle) + partial_wave = always_redraw(get_partial_wave) + h_line = always_redraw(get_h_line) + + self.add(partial_circle) + self.add(partial_wave) + self.add(h_line) + self.add(c_line, c_dot) + self.add(w_line, w_dot) + + sin_label = TexMobject( + "\\sin\\left(\\theta\\right)", + tex_to_color_map={"\\theta": RED} + ) + sin_label.next_to(w_axes.get_top(), UR) + self.add(sin_label) + + self.play( + theta_tracker.set_value, 1.25 * TAU, + run_time=15, + rate_func=linear, + ) + + def get_wave_axes(self): + wave_axes = Axes( + x_min=0, + x_max=1.25 * TAU, + y_min=-1.0, + y_max=1.0, + x_axis_config={ + "tick_frequency": TAU / 8, + "unit_size": 1.0, + }, + y_axis_config={ + "tick_frequency": 0.5, + "include_tip": False, + "unit_size": 1.5, + } + ) + wave_axes.y_axis.add_numbers( + -1, 1, number_config={"num_decimal_places": 1} + ) + wave_axes.to_edge(RIGHT, buff=MED_SMALL_BUFF) + + pairs = [ + (PI / 2, "\\frac{\\pi}{2}"), + (PI, "\\pi"), + (3 * PI / 2, "\\frac{3\\pi}{2}"), + (2 * PI, "2\\pi"), + ] + syms = VGroup() + for val, tex in pairs: + sym = TexMobject(tex) + sym.scale(0.5) + sym.next_to(wave_axes.c2p(val, 0), DOWN, MED_SMALL_BUFF) + syms.add(sym) + wave_axes.add(syms) + + theta = TexMobject("\\theta") + theta.set_color(RED) + theta.next_to(wave_axes.x_axis.get_end(), UP) + wave_axes.add(theta) + + return wave_axes + + def get_edge_group(self): + axes_max = 1.25 + radius = 1.5 + axes = Axes( + x_min=-axes_max, + x_max=axes_max, + y_min=-axes_max, + y_max=axes_max, + axis_config={ + "tick_frequency": 0.5, + "include_tip": False, + "numbers_with_elongated_ticks": [-1, 1], + "tick_size": 0.05, + "unit_size": radius, + }, + ) + axes.to_edge(LEFT, buff=MED_LARGE_BUFF) + + background = SurroundingRectangle(axes, buff=MED_SMALL_BUFF) + background.set_stroke(WHITE, 1) + background.set_fill(GREY_E, 1) + + circle = Circle(radius=radius) + circle.move_to(axes) + circle.set_stroke(WHITE, 1) + + nums = VGroup() + for u in 1, -1: + num = Integer(u) + num.set_height(0.2) + num.set_stroke(BLACK, 3, background=True) + num.next_to(axes.c2p(u, 0), DOWN + u * RIGHT, SMALL_BUFF) + nums.add(num) + + axes.add(nums) + + return background, circle, axes + + +class CosWave(SineWave): + CONFIG = { + "include_square": False, + } + + def construct(self): + w_axes = self.get_wave_axes() + square, circle, c_axes = self.get_edge_group() + + self.add(w_axes) + self.add(square, circle, c_axes) + + theta_tracker = ValueTracker(0) + c_dot = Dot(color=YELLOW) + c_line = Line(DOWN, UP, color=GREEN) + w_dot = Dot(color=YELLOW) + w_line = Line(DOWN, UP, color=GREEN) + + def update_c_dot(dot, axes=c_axes, tracker=theta_tracker): + theta = tracker.get_value() + dot.move_to(axes.c2p( + np.cos(theta), + np.sin(theta), + )) + + def update_c_line(line, axes=c_axes, tracker=theta_tracker): + theta = tracker.get_value() + x = np.cos(theta) + y = np.sin(theta) + line.set_points_as_corners([ + axes.c2p(0, y), + axes.c2p(x, y), + ]) + + def update_w_dot(dot, axes=w_axes, tracker=theta_tracker): + theta = tracker.get_value() + dot.move_to(axes.c2p(theta, np.cos(theta))) + + def update_w_line(line, axes=w_axes, tracker=theta_tracker): + theta = tracker.get_value() + x = theta + y = np.cos(theta) + if y == 0: + y = 1e-6 + line.set_points_as_corners([ + axes.c2p(x, 0), + axes.c2p(x, y), + ]) + + def get_partial_circle(circle=circle, tracker=theta_tracker): + result = circle.copy() + theta = tracker.get_value() + result.pointwise_become_partial( + circle, 0, clip(theta / TAU, 0, 1), + ) + result.set_stroke(RED, width=3) + return result + + def get_partial_wave(axes=w_axes, tracker=theta_tracker): + theta = tracker.get_value() + graph = axes.get_graph(np.cos, x_min=0, x_max=theta, step_size=0.025) + graph.set_stroke(PINK, 3) + return graph + + def get_h_line(axes=w_axes, tracker=theta_tracker): + theta = tracker.get_value() + return Line( + axes.c2p(0, 0), + axes.c2p(theta, 0), + stroke_color=RED + ) + + def get_square(line=c_line): + square = Square() + square.set_stroke(WHITE, 1) + square.set_fill(MAROON_B, opacity=0.5) + square.match_width(line) + square.move_to(line, DOWN) + return square + + def get_square_graph(axes=w_axes, tracker=theta_tracker): + theta = tracker.get_value() + graph = axes.get_graph( + lambda x: np.cos(x)**2, x_min=0, x_max=theta, step_size=0.025 + ) + graph.set_stroke(MAROON_B, 3) + return graph + + c_dot.add_updater(update_c_dot) + c_line.add_updater(update_c_line) + w_dot.add_updater(update_w_dot) + w_line.add_updater(update_w_line) + h_line = always_redraw(get_h_line) + partial_circle = always_redraw(get_partial_circle) + partial_wave = always_redraw(get_partial_wave) + + self.add(partial_circle) + self.add(partial_wave) + self.add(h_line) + self.add(c_line, c_dot) + self.add(w_line, w_dot) + + if self.include_square: + self.add(always_redraw(get_square)) + self.add(always_redraw(get_square_graph)) + + cos_label = TexMobject( + "\\cos\\left(\\theta\\right)", + tex_to_color_map={"\\theta": RED} + ) + cos_label.next_to(w_axes.get_top(), UR) + self.add(cos_label) + + self.play( + theta_tracker.set_value, 1.25 * TAU, + run_time=15, + rate_func=linear, + ) + + +class CosSquare(CosWave): + CONFIG = { + "include_square": True + } + + +class ComplexNumberPreview(Scene): + def construct(self): + plane = ComplexPlane(axis_config={"stroke_width": 4}) + plane.add_coordinates() + + z = complex(2, 1) + dot = Dot() + dot.move_to(plane.n2p(z)) + label = TexMobject("2+i") + label.set_color(YELLOW) + dot.set_color(YELLOW) + label.next_to(dot, UR, SMALL_BUFF) + label.set_stroke(BLACK, 5, background=True) + + line = Line(plane.n2p(0), plane.n2p(z)) + arc = Arc(start_angle=0, angle=np.log(z).imag, radius=0.5) + + self.add(plane) + self.add(line, arc) + self.add(dot) + self.add(label) + + self.embed() + + +class ComplexMultiplication(Scene): + def construct(self): + # Add plane + plane = ComplexPlane() + plane.add_coordinates() + + z = complex(2, 1) + z_dot = Dot(color=PINK) + z_dot.move_to(plane.n2p(z)) + z_label = TexMobject("z") + z_label.next_to(z_dot, UR, buff=0.5 * SMALL_BUFF) + z_label.match_color(z_dot) + + self.add(plane) + self.add(z_dot) + self.add(z_label) + + # Show 1 + one_vect = Vector(RIGHT) + one_vect.set_color(YELLOW) + one_vect.target = Vector(plane.n2p(z)) + one_vect.target.match_style(one_vect) + + z_rhs = TexMobject("=", "z \\cdot 1") + z_rhs[1].match_color(one_vect) + z_rhs.next_to(z_label, RIGHT, 1.5 * SMALL_BUFF, aligned_edge=DOWN) + z_rhs.set_stroke(BLACK, 3, background=True) + + one_label, i_label = [l for l in plane.coordinate_labels if l.get_value() == 1] + + self.play(GrowArrow(one_vect)) + self.wait() + self.add(one_vect, z_dot) + self.play( + MoveToTarget(one_vect), + TransformFromCopy(one_label, z_rhs), + ) + self.wait() + + # Show i + i_vect = Vector(UP, color=GREEN) + zi_point = plane.n2p(z * complex(0, 1)) + i_vect.target = Vector(zi_point) + i_vect.target.match_style(i_vect) + i_vect_label = TexMobject("z \\cdot i") + i_vect_label.match_color(i_vect) + i_vect_label.set_stroke(BLACK, 3, background=True) + i_vect_label.next_to(zi_point, UL, SMALL_BUFF) + + self.play(GrowArrow(i_vect)) + self.wait() + self.play( + MoveToTarget(i_vect), + TransformFromCopy(i_label, i_vect_label), + run_time=1, + ) + self.wait() + + self.play( + TransformFromCopy(one_vect, i_vect.target, path_arc=-90 * DEGREES), + ) + self.wait() + + # Transform plane + plane.generate_target() + for mob in plane.target.family_members_with_points(): + if isinstance(mob, Line): + mob.set_stroke(GREY, opacity=0.5) + new_plane = ComplexPlane(faded_line_ratio=0) + + self.remove(plane) + self.add(plane, new_plane, *self.mobjects) + + new_plane.generate_target() + new_plane.target.apply_complex_function(lambda w, z=z: w * z) + + self.play( + MoveToTarget(plane), + MoveToTarget(new_plane), + run_time=6, + rate_func=there_and_back_with_pause + ) + self.wait() + + # Show Example Point + w = complex(2, -1) + w_dot = Dot(plane.n2p(w), color=WHITE) + one_vects = VGroup(*[Vector(RIGHT) for x in range(2)]) + one_vects.arrange(RIGHT, buff=0) + one_vects.move_to(plane.n2p(0), LEFT) + one_vects.set_color(YELLOW) + new_i_vect = Vector(DOWN) + new_i_vect.move_to(plane.n2p(2), UP) + new_i_vect.set_color(GREEN) + vects = VGroup(*one_vects, new_i_vect) + vects.set_opacity(0.8) + + w_group = VGroup(*vects, w_dot) + w_group.target = VGroup( + one_vect.copy().set_opacity(0.8), + one_vect.copy().shift(plane.n2p(z)).set_opacity(0.8), + i_vect.copy().rotate(PI, about_point=ORIGIN).shift(2 * plane.n2p(z)).set_opacity(0.8), + Dot(plane.n2p(w * z), color=WHITE) + ) + + self.play(FadeInFromLarge(w_dot)) + self.wait() + self.play(ShowCreation(vects)) + self.wait() + + self.play( + MoveToTarget(plane), + MoveToTarget(new_plane), + MoveToTarget(w_group), + run_time=2, + path_arc=np.log(z).imag, + ) + self.wait() + + +class RotatePiCreature(Scene): + def construct(self): + randy = Randolph(mode="thinking") + randy.set_height(6) + + plane = ComplexPlane(x_min=-12, x_max=12) + plane.add_coordinates() + + self.camera.frame.move_to(3 * RIGHT) + + self.add(randy) + self.wait() + self.play(Rotate(randy, 30 * DEGREES, run_time=3)) + self.wait() + self.play(Rotate(randy, -30 * DEGREES)) + + self.add(plane, randy) + self.play( + ShowCreation(plane), + randy.set_opacity, 0.75, + ) + self.wait() + + dots = VGroup() + for mob in randy.family_members_with_points(): + for point in mob.get_anchors(): + dot = Dot(point) + dot.set_height(0.05) + dots.add(dot) + + self.play(ShowIncreasingSubsets(dots)) + self.wait() + + label = VGroup( + TexMobject("(x + iy)"), + Vector(DOWN), + TexMobject("(\\cos(30^\\circ) + i\\sin(30^\\circ))", "(x + iy)"), + ) + label[2][0].set_color(YELLOW) + label.arrange(DOWN) + label.to_corner(DR) + label.shift(3 * RIGHT) + + for mob in label: + mob.add_background_rectangle() + + self.play(FadeIn(label)) + self.wait() + + randy.add(dots) + self.play(Rotate(randy, 30 * DEGREES), run_time=3) + self.wait() + + +class ExpMeaning(Scene): + CONFIG = { + "include_circle": True + } + + def construct(self): + # Plane + plane = ComplexPlane(y_min=-6, y_max=6) + plane.shift(1.5 * DOWN) + plane.add_coordinates() + if self.include_circle: + circle = Circle(radius=1) + circle.set_stroke(RED, 1) + circle.move_to(plane.n2p(0)) + plane.add(circle) + + # Equation + equation = TexMobject( + "\\text{exp}(i\\theta) = ", + "1 + ", + "i\\theta + ", + "{(i\\theta)^2 \\over 2} + ", + "{(i\\theta)^3 \\over 6} + ", + "{(i\\theta)^4 \\over 24} + ", + "\\cdots", + tex_to_color_map={ + "\\theta": YELLOW, + "i": GREEN, + }, + ) + equation.add_background_rectangle(buff=MED_SMALL_BUFF, opacity=1) + equation.to_edge(UL, buff=0) + + # Label + theta_tracker = ValueTracker(0) + theta_label = VGroup( + TexMobject("\\theta = "), + DecimalNumber(0, num_decimal_places=4) + ) + theta_decimal = theta_label[1] + theta_decimal.add_updater( + lambda m, tt=theta_tracker: m.set_value(tt.get_value()) + ) + theta_label.arrange(RIGHT, buff=SMALL_BUFF) + theta_label.set_color(YELLOW) + theta_label.add_to_back(BackgroundRectangle( + theta_label, + buff=MED_SMALL_BUFF, + fill_opacity=1, + )) + theta_label.next_to(equation, DOWN, aligned_edge=LEFT, buff=0) + + # Vectors + def get_vectors(n_vectors=20, plane=plane, tracker=theta_tracker): + last_tip = plane.n2p(0) + z = complex(0, tracker.get_value()) + vects = VGroup() + colors = color_gradient([GREEN, YELLOW, RED], 6) + for i, color in zip(range(n_vectors), it.cycle(colors)): + vect = Vector(complex_to_R3(z**i / math.factorial(i))) + vect.set_color(color) + vect.shift(last_tip) + last_tip = vect.get_end() + vects.add(vect) + return vects + + vectors = always_redraw(get_vectors) + dot = Dot() + dot.set_height(0.03) + dot.add_updater(lambda m, vs=vectors: m.move_to(vs[-1].get_end())) + + self.add(plane) + self.add(vectors) + self.add(dot) + self.add(equation) + self.add(theta_label) + + self.play( + theta_tracker.set_value, 1, + run_time=3, + rate_func=smooth, + ) + self.wait() + for target in PI, TAU: + self.play( + theta_tracker.set_value, target, + run_time=10, + ) + self.wait() + + self.embed() + + +class ExpMeaningWithoutCircle(ExpMeaning): + CONFIG = { + "include_circle": False, + } + + +class PositionAndVelocityExample(Scene): + def construct(self): + plane = NumberPlane() + + self.add(plane) + + self.embed() + + +class EulersFormula(Scene): + def construct(self): + kw = {"tex_to_color_map": {"\\theta": YELLOW}} + formula = TexMobject( + "&e^{i\\theta} = \\\\ &\\cos\\left(\\theta\\right) + i\\cdot\\sin\\left(\\theta\\right)", + )[0] + formula[:4].scale(2, about_edge=UL) + formula[:4].shift(SMALL_BUFF * RIGHT + MED_LARGE_BUFF * UP) + VGroup(formula[2], formula[8], formula[17]).set_color(YELLOW) + formula.scale(1.5) + formula.set_stroke(BLACK, 5, background=True) + self.add(formula) + + +class EtoILimit(Scene): + def construct(self): + tex = TexMobject( + "\\lim_{n \\to \\infty} \\left(1 + \\frac{it}{n}\\right)^n", + )[0] + VGroup(tex[3], tex[12], tex[14]).set_color(YELLOW) + tex[9].set_color(BLUE) + tex.scale(1.5) + tex.set_stroke(BLACK, 5, background=True) + # self.add(tex) + + text = TextMobject("Interest rate\\\\of ", "$\\sqrt{-1}$") + text[1].set_color(BLUE) + text.scale(1.5) + text.set_stroke(BLACK, 5, background=True) + self.add(text) + + +class ImaginaryInterestRates(Scene): + def construct(self): + plane = ComplexPlane(x_min=-20, x_max=20, y_min=-20, y_max=20) + plane.add_coordinates() + circle = Circle(radius=1) + circle.set_stroke(YELLOW, 1) + self.add(plane, circle) + + frame = self.camera.frame + frame.save_state() + frame.generate_target() + frame.target.set_width(25) + frame.target.move_to(8 * RIGHT + 2 * DOWN) + + dt_tracker = ValueTracker(1) + + def get_vectors(tracker=dt_tracker, plane=plane, T=8): + dt = tracker.get_value() + last_z = 1 + vects = VGroup() + for t in np.arange(0, T, dt): + next_z = last_z + complex(0, 1) * last_z * dt + vects.add(Arrow( + plane.n2p(last_z), + plane.n2p(next_z), + buff=0, + )) + last_z = next_z + vects.set_submobject_colors_by_gradient(YELLOW, GREEN, BLUE) + return vects + + vects = get_vectors() + + line = Line() + line.add_updater(lambda m, v=vects: m.put_start_and_end_on( + ORIGIN, v[-1].get_start() if len(v) > 0 else RIGHT, + )) + + self.add(line) + self.play( + ShowIncreasingSubsets( + vects, + rate_func=linear, + int_func=np.ceil, + ), + MoveToTarget( + frame, + rate_func=squish_rate_func(smooth, 0.5, 1), + ), + run_time=8, + ) + self.wait() + self.play(FadeOut(line)) + + self.remove(vects) + vects = always_redraw(get_vectors) + self.add(vects) + self.play( + Restore(frame), + dt_tracker.set_value, 0.2, + run_time=5, + ) + self.wait() + self.play(dt_tracker.set_value, 0.01, run_time=3) + vects.clear_updaters() + self.wait() + + theta_tracker = ValueTracker(0) + + def get_arc(tracker=theta_tracker): + theta = tracker.get_value() + arc = Arc( + radius=1, + stroke_width=3, + stroke_color=RED, + start_angle=0, + angle=theta + ) + return arc + + arc = always_redraw(get_arc) + dot = Dot() + dot.add_updater(lambda m, arc=arc: m.move_to(arc.get_end())) + + label = VGroup( + DecimalNumber(0, num_decimal_places=3), + TextMobject("Years") + ) + label.arrange(RIGHT, aligned_edge=DOWN) + label.move_to(3 * LEFT + 1.5 * UP) + + label[0].set_color(RED) + label[0].add_updater(lambda m, tt=theta_tracker: m.set_value(tt.get_value())) + + self.add(BackgroundRectangle(label), label, arc, dot) + for n in range(1, 5): + target = n * PI / 2 + self.play( + theta_tracker.set_value, target, + run_time=3 + ) + self.wait(2) + + +class Logs(Scene): + def construct(self): + log = TexMobject( + "&\\text{log}(ab) = \\\\ &\\text{log}(a) + \\text{log}(b)", + tex_to_color_map={"a": BLUE, "b": YELLOW}, + alignment="", + ) + + log.scale(1.5) + log.set_stroke(BLACK, 5, background=True) + + self.add(log) + + +class LnX(Scene): + def construct(self): + sym = TexMobject("\\ln(x)") + sym.scale(3) + sym.shift(UP) + sym.set_stroke(BLACK, 5, background=True) + + word = TextMobject("Natural?") + word.scale(1.5) + word.set_color(YELLOW) + word.set_stroke(BLACK, 5, background=True) + word.next_to(sym, DOWN, buff=0.5) + arrow = Arrow(word.get_top(), sym[0][1].get_bottom()) + + self.add(sym, word, arrow) + + +class HarmonicSum(Scene): + def construct(self): + axes = Axes( + x_min=0, + x_max=13, + y_min=0, + y_max=1.25, + y_axis_config={ + "unit_size": 4, + "tick_frequency": 0.25, + } + ) + axes.to_corner(DL, buff=1) + axes.x_axis.add_numbers() + axes.y_axis.add_numbers( + *np.arange(0.25, 1.25, 0.25), + number_config={"num_decimal_places": 2}, + ) + self.add(axes) + + graph = axes.get_graph(lambda x: 1 / x, x_min=0.1, x_max=15) + graph_fill = graph.copy() + graph_fill.add_line_to(axes.c2p(15, 0)) + graph_fill.add_line_to(axes.c2p(1, 0)) + graph_fill.add_line_to(axes.c2p(1, 1)) + graph.set_stroke(WHITE, 3) + graph_fill.set_fill(BLUE_E, 0.5) + graph_fill.set_stroke(width=0) + self.add(graph, graph_fill) + + bars = VGroup() + bar_labels = VGroup() + for x in range(1, 15): + line = Line(axes.c2p(x, 0), axes.c2p(x + 1, 1 / x)) + bar = Rectangle() + bar.set_fill(GREEN_E, 1) + bar.replace(line, stretch=True) + bars.add(bar) + + label = TexMobject(f"1 \\over {x}") + label.set_height(0.7) + label.next_to(bar, UP, SMALL_BUFF) + bar_labels.add(label) + + bars.set_submobject_colors_by_gradient(GREEN_C, GREEN_E) + bars.set_stroke(WHITE, 1) + bars.set_fill(opacity=0.25) + + self.add(bars) + self.add(bar_labels) + + + self.embed() + + +class PowerTower(Scene): + def construct(self): + mob = TexMobject("4 = x^{x^{{x^{x^{x^{\cdot^{\cdot^{\cdot}}}}}}}}") + mob[0][-1].shift(0.1 * DL) + mob[0][-2].shift(0.05 * DL) + + mob.set_height(4) + mob.set_stroke(BLACK, 5, background=True) + + self.add(mob) + + +class ItoTheI(Scene): + def construct(self): + tex = TexMobject("i^i") + # tex = TexMobject("\\sqrt{-1}^{\\sqrt{-1}}") + tex.set_height(3) + tex.set_stroke(BLACK, 8, background=True) + self.add(tex) + + +class ComplexExponentialPlay(Scene): + def setup(self): + self.transform_alpha = 0 + + def construct(self): + # Plane + plane = ComplexPlane( + x_min=-2 * FRAME_WIDTH, + x_max=2 * FRAME_WIDTH, + y_min=-2 * FRAME_HEIGHT, + y_max=2 * FRAME_HEIGHT, + ) + plane.add_coordinates() + self.add(plane) + + # R Dot + r_dot = Dot(color=YELLOW) + + def update_r_dot(dot, point_tracker=self.mouse_drag_point): + point = point_tracker.get_location() + if abs(point[0]) < 0.1: + point[0] = 0 + if abs(point[1]) < 0.1: + point[1] = 0 + dot.move_to(point) + + r_dot.add_updater(update_r_dot) + self.mouse_drag_point.move_to(plane.n2p(1)) + + # Transformed sample dots + def func(z, dot=r_dot, plane=plane): + r = plane.p2n(dot.get_center()) + result = np.exp(r * z) + if abs(result) > 20: + result *= 20 / abs(result) + return result + + sample_dots = VGroup() + dot_template = Dot(radius=0.05) + dot_template.set_opacity(0.8) + spacing = 0.05 + for x in np.arange(-7, 7, spacing): + dot = dot_template.copy() + dot.set_color(TEAL) + dot.z = x + dot.move_to(plane.n2p(dot.z)) + sample_dots.add(dot) + for y in np.arange(-6, 6, spacing): + dot = dot_template.copy() + dot.set_color(MAROON) + dot.z = complex(0, y) + dot.move_to(plane.n2p(dot.z)) + sample_dots.add(dot) + + special_values = [1, complex(0, 1), -1, complex(0, -1)] + special_dots = VGroup(*[ + list(filter(lambda d: abs(d.z - x) < 0.01, sample_dots))[0] + for x in special_values + ]) + for dot in special_dots: + dot.set_fill(opacity=1) + dot.scale(1.2) + dot.set_stroke(WHITE, 2) + + sample_dots.save_state() + + def update_sample(sample, f=func, plane=plane, scene=self): + sample.restore() + sample.apply_function_to_submobject_positions( + lambda p: interpolate( + p, + plane.n2p(f(plane.p2n(p))), + scene.transform_alpha, + ) + ) + return sample + + sample_dots.add_updater(update_sample) + + # Sample lines + x_line = Line(plane.n2p(plane.x_min), plane.n2p(plane.x_max)) + y_line = Line(plane.n2p(plane.y_min), plane.n2p(plane.y_max)) + y_line.rotate(90 * DEGREES) + x_line.set_color(GREEN) + y_line.set_color(PINK) + axis_lines = VGroup(x_line, y_line) + for line in axis_lines: + line.insert_n_curves(50) + axis_lines.save_state() + + def update_axis_liens(lines=axis_lines, f=func, plane=plane, scene=self): + lines.restore() + lines.apply_function( + lambda p: interpolate( + p, + plane.n2p(f(plane.p2n(p))), + scene.transform_alpha, + ) + ) + lines.make_smooth() + + axis_lines.add_updater(update_axis_liens) + + # Labels + labels = VGroup( + TexMobject("f(1)"), + TexMobject("f(i)"), + TexMobject("f(-1)"), + TexMobject("f(-i)"), + ) + for label, dot in zip(labels, special_dots): + label.set_height(0.3) + label.match_color(dot) + label.set_stroke(BLACK, 3, background=True) + label.add_background_rectangle(opacity=0.5) + + def update_labels(labels, dots=special_dots, scene=self): + for label, dot in zip(labels, dots): + label.next_to(dot, UR, 0.5 * SMALL_BUFF) + label.set_opacity(self.transform_alpha) + + labels.add_updater(update_labels) + + # Titles + title = TexMobject( + "f(x) =", "\\text{exp}(r\\cdot x)", + tex_to_color_map={"r": YELLOW} + ) + title.to_corner(UL) + title.set_stroke(BLACK, 5, background=True) + brace = Brace(title[1:], UP, buff=SMALL_BUFF) + e_pow = TexMobject("e^{rx}", tex_to_color_map={"r": YELLOW}) + e_pow.add_background_rectangle() + e_pow.next_to(brace, UP, buff=SMALL_BUFF) + title.add(brace, e_pow) + + r_eq = VGroup( + TexMobject("r=", tex_to_color_map={"r": YELLOW}), + DecimalNumber(1) + ) + r_eq.arrange(RIGHT, aligned_edge=DOWN) + r_eq.next_to(title, DOWN, aligned_edge=LEFT) + r_eq[0].set_stroke(BLACK, 5, background=True) + r_eq[1].set_color(YELLOW) + r_eq[1].add_updater(lambda m: m.set_value(plane.p2n(r_dot.get_center()))) + + self.add(title) + self.add(r_eq) + + # self.add(axis_lines) + self.add(sample_dots) + self.add(r_dot) + self.add(labels) + + # Animations + def update_transform_alpha(mob, alpha, scene=self): + scene.transform_alpha = alpha + + frame = self.camera.frame + frame.set_height(10) + r_dot.clear_updaters() + r_dot.move_to(plane.n2p(1)) + + self.play( + UpdateFromAlphaFunc( + VectorizedPoint(), + update_transform_alpha, + ) + ) + self.play(r_dot.move_to, plane.n2p(2)) + self.wait() + self.play(r_dot.move_to, plane.n2p(PI)) + self.wait() + self.play(r_dot.move_to, plane.n2p(np.log(2))) + self.wait() + self.play(r_dot.move_to, plane.n2p(complex(0, np.log(2))), path_arc=90 * DEGREES, run_time=2) + self.wait() + self.play(r_dot.move_to, plane.n2p(complex(0, PI / 2))) + self.wait() + self.play(r_dot.move_to, plane.n2p(np.log(2)), run_time=2) + self.wait() + self.play(frame.set_height, 14) + self.play(r_dot.move_to, plane.n2p(complex(np.log(2), PI)), run_time=3) + self.wait() + self.play(r_dot.move_to, plane.n2p(complex(np.log(2), TAU)), run_time=3) + self.wait() + + self.embed() + + def on_mouse_scroll(self, point, offset): + frame = self.camera.frame + if self.zoom_on_scroll: + factor = 1 + np.arctan(10 * offset[1]) + frame.scale(factor, about_point=ORIGIN) + else: + self.transform_alpha = clip(self.transform_alpha + 5 * offset[1], 0, 1) + + +class LDMEndScreen(PatreonEndScreen): + CONFIG = { + "scroll_time": 20, + "specific_patrons": [ + "1stViewMaths", + "Adam Dřínek", + "Adam Margulies", + "Aidan Shenkman", + "Alan Stein", + "Alex Mijalis", + "Alexander Mai", + "Alexis Olson", + "Ali Yahya", + "Andreas Snekloth Kongsgaard", + "Andrew Busey", + "Andrew Cary", + "Andrew R. Whalley", + "Anthony Losego", + "Aravind C V", + "Arjun Chakroborty", + "Arthur Zey", + "Ashwin Siddarth", + "Augustine Lim", + "Austin Goodman", + "Avi Finkel", + "Awoo", + "Axel Ericsson", + "Ayan Doss", + "AZsorcerer", + "Barry Fam", + "Bartosz Burclaf", + "Ben Delo", + "Bernd Sing", + "Bill Gatliff", + "Bob Sanderson", + "Boris Veselinovich", + "Bradley Pirtle", + "Brandon Huang", + "Brendan Shah", + "Brian Cloutier", + "Brian Staroselsky", + "Britt Selvitelle", + "Britton Finley", + "Burt Humburg", + "Calvin Lin", + "Charles Southerland", + "Charlie N", + "Chenna Kautilya", + "Chris Connett", + "Chris Druta", + "Christian Kaiser", + "cinterloper", + "Clark Gaebel", + "Colwyn Fritze-Moor", + "Cooper Jones", + "Corey Ogburn", + "D. Sivakumar", + "Dan Herbatschek", + "Daniel Herrera C", + "Darrell Thomas", + "Dave B", + "Dave Cole", + "Dave Kester", + "dave nicponski", + "David B. Hill", + "David Clark", + "David Gow", + "Delton Ding", + "Eduardo Rodriguez", + "Emilio Mendoza Palafox", + "emptymachine", + "Eric Younge", + "Eryq Ouithaqueue", + "Federico Lebron", + "Fernando Via Canel", + "Frank R. Brown, Jr.", + "Giovanni Filippi", + "Goodwine", + "Hal Hildebrand", + "Heptonion", + "Hitoshi Yamauchi", + "Ivan Sorokin", + "Jacob Baxter", + "Jacob Harmon", + "Jacob Hartmann", + "Jacob Magnuson", + "Jalex Stark", + "Jameel Syed", + "James Beall", + "Jason Hise", + "Jayne Gabriele", + "Jean-Manuel Izaret", + "Jeff Dodds", + "Jeff Linse", + "Jeff Straathof", + "Jeffrey Wolberg", + "Jimmy Yang", + "Joe Pregracke", + "Johan Auster", + "John C. Vesey", + "John Camp", + "John Haley", + "John Le", + "John Luttig", + "John Rizzo", + "John V Wertheim", + "Jonathan Heckerman", + "Jonathan Wilson", + "Joseph John Cox", + "Joseph Kelly", + "Josh Kinnear", + "Joshua Claeys", + "Joshua Ouellette", + "Juan Benet", + "Julien Dubois", + "Kai-Siang Ang", + "Kanan Gill", + "Karl Niu", + "Kartik Cating-Subramanian", + "Kaustuv DeBiswas", + "Killian McGuinness", + "kkm", + "Klaas Moerman", + "Kristoffer Börebäck", + "Kros Dai", + "L0j1k", + "Lael S Costa", + "LAI Oscar", + "Lambda GPU Workstations", + "Laura Gast", + "Lee Redden", + "Linh Tran", + "Luc Ritchie", + "Ludwig Schubert", + "Lukas Biewald", + "Lukas Zenick", + "Magister Mugit", + "Magnus Dahlström", + "Magnus Hiie", + "Manoj Rewatkar", + "Mark B Bahu", + "Mark Heising", + "Mark Hopkins", + "Mark Mann", + "Martin Price", + "Mathias Jansson", + "Matt Godbolt", + "Matt Langford", + "Matt Roveto", + "Matt Russell", + "Matteo Delabre", + "Matthew Bouchard", + "Matthew Cocke", + "Maxim Nitsche", + "Michael Bos", + "Michael Day", + "Michael Hardel", + "Michael W White", + "Mihran Vardanyan", + "Mirik Gogri", + "Molly Mackinlay", + "Mustafa Mahdi", + "Márton Vaitkus", + "Nate Heckmann", + "Nero Li", + "Nicholas Cahill", + "Nikita Lesnikov", + "Oleg Leonov", + "Oliver Steele", + "Omar Zrien", + "Omer Tuchfeld", + "Patrick Lucas", + "Pavel Dubov", + "Pesho Ivanov", + "Petar Veličković", + "Peter Ehrnstrom", + "Peter Francis", + "Peter Mcinerney", + "Pierre Lancien", + "Pradeep Gollakota", + "Rafael Bove Barrios", + "Raghavendra Kotikalapudi", + "Randy C. Will", + "rehmi post", + "Rex Godby", + "Ripta Pasay", + "Rish Kundalia", + "Roman Sergeychik", + "Roobie", + "Ryan Atallah", + "Samuel Judge", + "SansWord Huang", + "Scott Gray", + "Scott Walter, Ph.D.", + "soekul", + "Solara570", + "Stephen Shanahan", + "Steve Huynh", + "Steve Muench", + "Steve Sperandeo", + "Steven Siddals", + "Stevie Metke", + "Sundar Subbarayan", + "Sunil Nagaraj", + "supershabam", + "Suteerth Vishnu", + "Suthen Thomas", + "Tal Einav", + "Taras Bobrovytsky", + "Tauba Auerbach", + "Ted Suzman", + "Thomas J Sargent", + "Thomas Tarler", + "Tianyu Ge", + "Tihan Seale", + "Tim Erbes", + "Tim Kazik", + "Tomasz Legutko", + "Tyler Herrmann", + "Tyler Parcell", + "Tyler VanValkenburg", + "Tyler Veness", + "Vassili Philippov", + "Vasu Dubey", + "Veritasium", + "Vignesh Ganapathi Subramanian", + "Vinicius Reis", + "Vladimir Solomatin", + "Wooyong Ee", + "Xuanji Li", + "Yana Chernobilsky", + "Yavor Ivanov", + "YinYangBalance.Asia", + "Yu Jun", + "Yurii Monastyrshyn", + ], + } diff --git a/from_3b1b/old/lost_lecture.py b/from_3b1b/old/lost_lecture.py index caaef69401..88fc2894a1 100644 --- a/from_3b1b/old/lost_lecture.py +++ b/from_3b1b/old/lost_lecture.py @@ -198,7 +198,7 @@ def construct(self): self.play(ShowCreation(circle)) self.play( - FadeInFrom(e_dot, LEFT), + FadeIn(e_dot, LEFT), Write(eccentric_words, run_time=1) ) self.wait() @@ -547,7 +547,7 @@ def get_feynman_diagram(self): wave_label.next_to(wave, UP, SMALL_BUFF) labels.add(wave_label) - squiggle = ParametricFunction( + squiggle = ParametricCurve( lambda t: np.array([ t + 0.5 * np.sin(TAU * t), 0.5 * np.cos(TAU * t), @@ -2385,7 +2385,7 @@ def add_orbit(self): self.ellipse = orbit_shape def get_ellipse(self): - orbit_shape = ParametricFunction( + orbit_shape = ParametricCurve( lambda t: (1 + 0.2 * np.sin(5 * TAU * t)) * np.array([ np.cos(TAU * t), np.sin(TAU * t), @@ -2772,7 +2772,7 @@ def construct(self): ) self.wait() self.play(*[ - FadeInFrom( + FadeIn( mob, direction=3 * LEFT ) for mob in (principia, principia.rect) diff --git a/from_3b1b/old/moser_main.py b/from_3b1b/old/moser_main.py index 9165a49518..67bd23a1ca 100644 --- a/from_3b1b/old/moser_main.py +++ b/from_3b1b/old/moser_main.py @@ -671,7 +671,7 @@ def __init__(self, *args, **kwargs): NumberLine(), NumberLine().rotate(np.pi / 2) ) - graph = ParametricFunction( + graph = ParametricCurve( lambda t : (10*t, ((10*t)**3 - 10*t), 0), expected_measure = 40.0 ) diff --git a/from_3b1b/old/music_and_measure.py b/from_3b1b/old/music_and_measure.py index c9d71f084c..02b0d1e855 100644 --- a/from_3b1b/old/music_and_measure.py +++ b/from_3b1b/old/music_and_measure.py @@ -61,7 +61,7 @@ def zero_to_one_interval(): return interval class LeftParen(Mobject): - def generate_points(self): + def init_points(self): self.add(TexMobject("(")) self.center() @@ -69,7 +69,7 @@ def get_center(self): return Mobject.get_center(self) + 0.04*LEFT class RightParen(Mobject): - def generate_points(self): + def init_points(self): self.add(TexMobject(")")) self.center() @@ -478,7 +478,7 @@ def construct(self, fraction, color): self.play(string1, string2) class LongSine(Mobject1D): - def generate_points(self): + def init_points(self): self.add_points([ (x, np.sin(2*np.pi*x), 0) for x in np.arange(0, 100, self.epsilon/10) diff --git a/from_3b1b/old/patreon.py b/from_3b1b/old/patreon.py index dc3fc8454d..278906af11 100644 --- a/from_3b1b/old/patreon.py +++ b/from_3b1b/old/patreon.py @@ -563,7 +563,7 @@ def construct(self): # f = lambda t : 4*np.sin(t*np.pi/2) f = lambda t : 4*t g = lambda t : 3*smooth(t) - curve = ParametricFunction(lambda t : f(t)*RIGHT + g(t)*DOWN) + curve = ParametricCurve(lambda t : f(t)*RIGHT + g(t)*DOWN) curve.set_color(YELLOW) curve.center() rect = Rectangle() diff --git a/from_3b1b/old/pi_day.py b/from_3b1b/old/pi_day.py index e533d012b4..7125c92cfd 100644 --- a/from_3b1b/old/pi_day.py +++ b/from_3b1b/old/pi_day.py @@ -31,7 +31,7 @@ def __init__(self, mobject = None, new_angle = TAU/3, **kwargs): def interpolate_mobject(self,alpha): angle = interpolate(self.old_angle, self.new_angle, alpha) self.mobject.angle = angle - self.mobject.generate_points() + self.mobject.init_points() class LabelTracksLine(Animation): diff --git a/from_3b1b/old/quat3d.py b/from_3b1b/old/quat3d.py index 28f62a723a..ebe6b7ce0e 100644 --- a/from_3b1b/old/quat3d.py +++ b/from_3b1b/old/quat3d.py @@ -214,7 +214,7 @@ def construct(self): self.wait() for image, name, quote in zip(images, names, quotes): self.play( - FadeInFrom(image, 3 * DOWN), + FadeIn(image, 3 * DOWN), FadeInFromLarge(name), LaggedStartMap( FadeIn, VGroup(*it.chain(*quote)), @@ -295,9 +295,9 @@ def construct(self): # self.play( # hn_quote.scale, 2, {"about_edge": DL}, - # FadeOutAndShift(quotes[0], 5 * UP), - # FadeOutAndShift(quotes[2], UR), - # FadeOutAndShift(quotes[3], RIGHT), + # FadeOut(quotes[0], 5 * UP), + # FadeOut(quotes[2], UR), + # FadeOut(quotes[3], RIGHT), # FadeInFromDown(hn_context), # ) # hn_rect = Rectangle( @@ -333,9 +333,9 @@ def construct(self): # t_quote.next_to(FRAME_WIDTH * LEFT / 2 + FRAME_WIDTH * UP / 2, UR) # t_quote.set_opacity(0) # self.play( - # FadeOutAndShift(hn_quote, 4 * LEFT), - # FadeOutAndShift(hn_rect, 4 * LEFT), - # FadeOutAndShift(hn_context, UP), + # FadeOut(hn_quote, 4 * LEFT), + # FadeOut(hn_rect, 4 * LEFT), + # FadeOut(hn_context, UP), # FadeOut(vr_headsets), # t_quote.set_opacity, 1, # t_quote.scale, 2, @@ -1068,9 +1068,9 @@ def show_product(self): Write(parens) ) self.wait() - self.play(FadeInFrom(mid_line, UP)) + self.play(FadeIn(mid_line, UP)) self.wait() - self.play(FadeInFrom(low_line, UP)) + self.play(FadeIn(low_line, UP)) self.wait(2) self.play(FadeOut(self.unit_z_group)) self.rotation_mobs.save_state() diff --git a/from_3b1b/old/quaternions.py b/from_3b1b/old/quaternions.py index 6d2a12dba4..23cf18233b 100644 --- a/from_3b1b/old/quaternions.py +++ b/from_3b1b/old/quaternions.py @@ -464,7 +464,7 @@ def construct(self): R_label.move_to, 0.25 * FRAME_WIDTH * LEFT + 2 * UP, C_label.move_to, 0.25 * FRAME_WIDTH * RIGHT + 2 * UP, H_label.move_to, 0.75 * FRAME_WIDTH * RIGHT + 2 * UP, - FadeOutAndShift(systems[3:], 2 * DOWN), + FadeOut(systems[3:], 2 * DOWN), Write(number_line), Write(plane), GrowFromCenter(R_example_dot), @@ -711,16 +711,16 @@ def get_colored_tex_mobject(tex): aligned_edge=LEFT, ) - self.play(FadeInFrom(dot_product, 2 * RIGHT)) - self.play(FadeInFrom(cross_product, 2 * LEFT)) + self.play(FadeIn(dot_product, 2 * RIGHT)) + self.play(FadeIn(cross_product, 2 * LEFT)) self.wait() self.play(FadeInFromDown(date)) self.play(ApplyMethod(dot_product.fade, 0.7)) self.play(ApplyMethod(cross_product.fade, 0.7)) self.wait() self.play( - FadeOutAndShift(dot_product, 2 * LEFT), - FadeOutAndShift(cross_product, 2 * RIGHT), + FadeOut(dot_product, 2 * LEFT), + FadeOut(cross_product, 2 * RIGHT), ) self.date = date @@ -811,12 +811,12 @@ def blink_wait(n_loops): ) blink_wait(3) self.play( - FadeOutAndShift(hamilton, RIGHT), + FadeOut(hamilton, RIGHT), LaggedStartMap( FadeOutAndShift, images_with_labels, lambda m: (m, UP) ), - FadeOutAndShift(students, DOWN), + FadeOut(students, DOWN), FadeOut(society_title), run_time=1 ) @@ -924,7 +924,7 @@ def get_change_places(): for x in range(4): self.play(get_change_places()) self.play( - FadeOutAndShift(VGroup(title, author_brace, aka)), + FadeOut(VGroup(title, author_brace, aka)), FadeInFromDown(quote), ) self.play(get_change_places()) @@ -1148,7 +1148,7 @@ def construct(self): self.wait(3) self.play( self.teacher.change, "hooray", - FadeInFrom(titles[1]), + FadeIn(titles[1]), ApplyMethod( titles[0].shift, 2 * UP, rate_func=squish_rate_func(smooth, 0.2, 1) @@ -1882,7 +1882,7 @@ def introduce_z_and_w(self): self.add(product[:-1]) self.play( ReplacementTransform(w_label[1][0].copy(), w_sym), - FadeInFrom(product[2], LEFT), + FadeIn(product[2], LEFT), FadeIn(product[0]), ) self.wait() @@ -2005,11 +2005,11 @@ def show_action_on_all_complex_numbers(self): rate_func=lambda t: there_and_back_with_pause(t, 2 / 9) ) self.wait() - self.play(FadeInFrom(pin, UL)) + self.play(FadeIn(pin, UL)) self.play(Write(zero_eq)) self.play( FadeInFromLarge(one_dot), - FadeInFrom(hand, UR) + FadeIn(hand, UR) ) self.play(Write(one_eq)) self.wait() @@ -2446,13 +2446,13 @@ def describe_individual_points(self): self.wait() dot.move_to(i_point) self.play(ShowCreation(dot)) - self.play(FadeInFrom(i_pin, UL)) + self.play(FadeIn(i_pin, UL)) self.wait() self.play( dot.move_to, neg_i_point, path_arc=-60 * DEGREES ) - self.play(FadeInFrom(neg_i_pin, UL)) + self.play(FadeIn(neg_i_pin, UL)) self.wait() self.play( dot.move_to, one_point, @@ -2888,7 +2888,7 @@ def update_circle(circle): lambda h: h.move_to(one_dot.get_center(), LEFT) ) self.play( - FadeInFrom(hand, RIGHT), + FadeIn(hand, RIGHT), FadeInFromLarge(one_dot, 3), ) for angle in 90 * DEGREES, -90 * DEGREES: @@ -3170,7 +3170,7 @@ def reorient_axes(self): ) self.begin_ambient_camera_rotation(rate=0.02) self.wait() - self.play(FadeInFrom(j_labels, IN)) + self.play(FadeIn(j_labels, IN)) z_axis.add(j_labels) self.play( ShowCreationThenDestruction(z_unit_line), @@ -3238,7 +3238,7 @@ def show_example_number(self): self.add_fixed_in_frame_mobjects(number_label) self.play( ShowCreation(point_line), - FadeInFrom(dot, -coords), + FadeIn(dot, -coords), FadeInFromDown(number_label) ) self.wait() @@ -4219,7 +4219,7 @@ def bring_back_complex(self): group = VGroup(numbers, labels) self.play( group.to_edge, UP, - FadeOutAndShift(self.three_axes, DOWN) + FadeOut(self.three_axes, DOWN) ) self.wait() @@ -4249,15 +4249,15 @@ def show_components_of_quaternion(self): ) self.wait() self.play( - FadeOutAndShift(real_word, DOWN), - FadeInFrom(scalar_word, DOWN), + FadeOut(real_word, DOWN), + FadeIn(scalar_word, DOWN), ) self.wait(2) self.play(ChangeDecimalToValue(real_part, 0)) self.wait() self.play( - FadeOutAndShift(imag_word, DOWN), - FadeInFrom(vector_word, DOWN) + FadeOut(imag_word, DOWN), + FadeIn(vector_word, DOWN) ) self.wait(2) @@ -4600,7 +4600,7 @@ def construct(self): self.wait() self.play( GrowFromCenter(rotate_brace), - FadeInFrom(rotate_words, UP), + FadeIn(rotate_words, UP), ) self.play( Rotate( @@ -4944,7 +4944,7 @@ def show_reference_spheres(self): # Show xy plane self.play( - FadeOutAndShift(circle_words, DOWN), + FadeOut(circle_words, DOWN), FadeInFromDown(sphere_1ij_words), FadeOut(circle), sphere_ijk.set_stroke, {"width": 0.0} @@ -4955,7 +4955,7 @@ def show_reference_spheres(self): # Show yz plane self.play( - FadeOutAndShift(sphere_1ij_words, DOWN), + FadeOut(sphere_1ij_words, DOWN), FadeInFromDown(sphere_1jk_words), sphere_1ij.set_fill, BLUE_E, 0.25, sphere_1ij.set_stroke, {"width": 0.0}, @@ -4965,7 +4965,7 @@ def show_reference_spheres(self): # Show xz plane self.play( - FadeOutAndShift(sphere_1jk_words, DOWN), + FadeOut(sphere_1jk_words, DOWN), FadeInFromDown(sphere_1ik_words), sphere_1jk.set_fill, GREEN_E, 0.25, sphere_1jk.set_stroke, {"width": 0.0}, @@ -5148,11 +5148,11 @@ def construct(self): self.add(q_times_p) self.play( - FadeInFrom(q_words, UP), + FadeIn(q_words, UP), GrowArrow(q_arrow), ) self.play( - FadeInFrom(p_words, DOWN), + FadeIn(p_words, DOWN), GrowArrow(p_arrow), ) self.wait() @@ -5162,7 +5162,7 @@ def construct(self): ])) self.play( FadeInFromDown(i_mob), - FadeOutAndShift(q_mob, UP) + FadeOut(q_mob, UP) ) product = VGroup(i_mob, times_mob, p_mob) self.play(product.to_edge, UP) @@ -5841,14 +5841,14 @@ def normalize_tracker(t): self.stop_ambient_camera_rotation() self.begin_ambient_camera_rotation(rate=0.02) self.set_quat(special_q) - self.play(FadeInFrom(label, IN)) + self.play(FadeIn(label, IN)) self.wait(3) for circle in [circle1, circle2]: self.play(ShowCreation(circle, run_time=3)) circle.updaters = circle.tucked_away_updaters self.wait(2) self.play( - FadeInFrom(hand, 2 * IN + 2 * RIGHT), + FadeIn(hand, 2 * IN + 2 * RIGHT), run_time=2 ) hand.add_updater( @@ -5896,7 +5896,7 @@ def construct(self): self.teacher.change, "raise_right_hand", self.get_student_changes("erm", "confused", "sassy") ) - self.play(FadeInFrom(words, RIGHT)) + self.play(FadeIn(words, RIGHT)) self.wait(2) self.play( ReplacementTransform(words, joke), @@ -5907,7 +5907,7 @@ def construct(self): self.look_at(self.screen) self.wait(3) self.play( - FadeInFrom(ji_eq), + FadeIn(ji_eq), LaggedStartMap( ApplyMethod, VGroup(ij_eq, general_eq), lambda m: (m.shift, UP), @@ -5977,11 +5977,11 @@ def construct(self): self.add(axes, cube) self.play( Rotate(cube, 90 * DEGREES, OUT, run_time=2), - FadeInFrom(label1[0], IN), + FadeIn(label1[0], IN), ) self.play( Rotate(cube, 90 * DEGREES, RIGHT, run_time=2), - FadeInFrom(label1[1], IN), + FadeIn(label1[1], IN), ) self.wait() self.play( @@ -5991,11 +5991,11 @@ def construct(self): ) self.play( Rotate(cube2, 90 * DEGREES, RIGHT, run_time=2), - FadeInFrom(label2[0], IN), + FadeIn(label2[0], IN), ) self.play( Rotate(cube2, 90 * DEGREES, OUT, run_time=2), - FadeInFrom(label2[1], IN), + FadeIn(label2[1], IN), ) self.wait(5) @@ -6196,7 +6196,7 @@ def construct(self): self.play( Animation(VectorizedPoint().next_to(pi1, UL, LARGE_BUFF)), pi2.change, "sad", - FadeOutAndShift(bubble.content, DOWN), + FadeOut(bubble.content, DOWN), FadeInFromDown(time_words, DOWN), ) self.wait(7) diff --git a/from_3b1b/old/sir.py b/from_3b1b/old/sir.py new file mode 100644 index 0000000000..8ab210ce4d --- /dev/null +++ b/from_3b1b/old/sir.py @@ -0,0 +1,3366 @@ +from manimlib.imports import * +from from_3b1b.active.bayes.beta_helpers import fix_percent +from from_3b1b.active.bayes.beta_helpers import XMARK_TEX +from from_3b1b.active.bayes.beta_helpers import CMARK_TEX + + +SICKLY_GREEN = "#9BBD37" +COLOR_MAP = { + "S": BLUE, + "I": RED, + "R": GREY_D, +} + + +def update_time(mob, dt): + mob.time += dt + + +class Person(VGroup): + CONFIG = { + "status": "S", # S, I or R + "height": 0.2, + "color_map": COLOR_MAP, + "infection_ring_style": { + "stroke_color": RED, + "stroke_opacity": 0.8, + "stroke_width": 0, + }, + "infection_radius": 0.5, + "infection_animation_period": 2, + "symptomatic": False, + "p_symptomatic_on_infection": 1, + "max_speed": 1, + "dl_bound": [-FRAME_WIDTH / 2, -FRAME_HEIGHT / 2], + "ur_bound": [FRAME_WIDTH / 2, FRAME_HEIGHT / 2], + "gravity_well": None, + "gravity_strength": 1, + "wall_buffer": 1, + "wander_step_size": 1, + "wander_step_duration": 1, + "social_distance_factor": 0, + "social_distance_color_threshold": 2, + "n_repulsion_points": 10, + "social_distance_color": YELLOW, + "max_social_distance_stroke_width": 5, + "asymptomatic_color": YELLOW, + } + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.time = 0 + self.last_step_change = -1 + self.change_anims = [] + self.velocity = np.zeros(3) + self.infection_start_time = np.inf + self.infection_end_time = np.inf + self.repulsion_points = [] + self.num_infected = 0 + + self.center_point = VectorizedPoint() + self.add(self.center_point) + self.add_body() + self.add_infection_ring() + self.set_status(self.status, run_time=0) + + # Updaters + self.add_updater(update_time) + self.add_updater(lambda m, dt: m.update_position(dt)) + self.add_updater(lambda m, dt: m.update_infection_ring(dt)) + self.add_updater(lambda m: m.progress_through_change_anims()) + + def add_body(self): + body = self.get_body() + body.set_height(self.height) + body.move_to(self.get_center()) + self.add(body) + self.body = body + + def get_body(self, status): + person = SVGMobject(file_name="person") + person.set_stroke(width=0) + return person + + def set_status(self, status, run_time=1): + start_color = self.color_map[self.status] + end_color = self.color_map[status] + + if status == "I": + self.infection_start_time = self.time + self.infection_ring.set_stroke(width=0, opacity=0) + if random.random() < self.p_symptomatic_on_infection: + self.symptomatic = True + else: + self.infection_ring.set_color(self.asymptomatic_color) + end_color = self.asymptomatic_color + if self.status == "I": + self.infection_end_time = self.time + self.symptomatic = False + + anims = [ + UpdateFromAlphaFunc( + self.body, + lambda m, a: m.set_color(interpolate_color( + start_color, end_color, a + )), + run_time=run_time, + ) + ] + for anim in anims: + self.push_anim(anim) + + self.status = status + + def push_anim(self, anim): + anim.suspend_mobject_updating = False + anim.begin() + anim.start_time = self.time + self.change_anims.append(anim) + return self + + def pop_anim(self, anim): + anim.update(1) + anim.finish() + self.change_anims.remove(anim) + + def add_infection_ring(self): + self.infection_ring = Circle( + radius=self.height / 2, + ) + self.infection_ring.set_style(**self.infection_ring_style) + self.add(self.infection_ring) + self.infection_ring.time = 0 + return self + + def update_position(self, dt): + center = self.get_center() + total_force = np.zeros(3) + + # Gravity + if self.wander_step_size != 0: + if (self.time - self.last_step_change) > self.wander_step_duration: + vect = rotate_vector(RIGHT, TAU * random.random()) + self.gravity_well = center + self.wander_step_size * vect + self.last_step_change = self.time + + if self.gravity_well is not None: + to_well = (self.gravity_well - center) + dist = get_norm(to_well) + if dist != 0: + total_force += self.gravity_strength * to_well / (dist**3) + + # Potentially avoid neighbors + if self.social_distance_factor > 0: + repulsion_force = np.zeros(3) + min_dist = np.inf + for point in self.repulsion_points: + to_point = point - center + dist = get_norm(to_point) + if 0 < dist < min_dist: + min_dist = dist + if dist > 0: + repulsion_force -= self.social_distance_factor * to_point / (dist**3) + sdct = self.social_distance_color_threshold + self.body.set_stroke( + self.social_distance_color, + width=clip( + (sdct / min_dist) - sdct, + # 2 * (sdct / min_dist), + 0, + self.max_social_distance_stroke_width + ), + background=True, + ) + total_force += repulsion_force + + # Avoid walls + wall_force = np.zeros(3) + for i in range(2): + to_lower = center[i] - self.dl_bound[i] + to_upper = self.ur_bound[i] - center[i] + + # Bounce + if to_lower < 0: + self.velocity[i] = abs(self.velocity[i]) + self.set_coord(self.dl_bound[i], i) + if to_upper < 0: + self.velocity[i] = -abs(self.velocity[i]) + self.set_coord(self.ur_bound[i], i) + + # Repelling force + wall_force[i] += max((-1 / self.wall_buffer + 1 / to_lower), 0) + wall_force[i] -= max((-1 / self.wall_buffer + 1 / to_upper), 0) + total_force += wall_force + + # Apply force + self.velocity += total_force * dt + + # Limit speed + speed = get_norm(self.velocity) + if speed > self.max_speed: + self.velocity *= self.max_speed / speed + + # Update position + self.shift(self.velocity * dt) + + def update_infection_ring(self, dt): + ring = self.infection_ring + if not (self.infection_start_time <= self.time <= self.infection_end_time + 1): + return self + + ring_time = self.time - self.infection_start_time + period = self.infection_animation_period + + alpha = (ring_time % period) / period + ring.set_height(interpolate( + self.height, + self.infection_radius, + smooth(alpha), + )) + ring.set_stroke( + width=interpolate( + 0, 5, + there_and_back(alpha), + ), + opacity=min([ + min([ring_time, 1]), + min([self.infection_end_time + 1 - self.time, 1]), + ]), + ) + + return self + + def progress_through_change_anims(self): + for anim in self.change_anims: + if anim.run_time == 0: + alpha = 1 + else: + alpha = (self.time - anim.start_time) / anim.run_time + anim.interpolate(alpha) + if alpha >= 1: + self.pop_anim(anim) + + def get_center(self): + return self.center_point.points[0] + + +class DotPerson(Person): + def get_body(self): + return Dot() + + +class PiPerson(Person): + CONFIG = { + "mode_map": { + "S": "guilty", + "I": "sick", + "R": "tease", + } + } + + def get_body(self): + return Randolph() + + def set_status(self, status, run_time=1): + super().set_status(status) + + target = self.body.copy() + target.change(self.mode_map[status]) + target.set_color(self.color_map[status]) + + transform = Transform(self.body, target) + transform.begin() + + def update(body, alpha): + transform.update(alpha) + body.move_to(self.center_point) + + anims = [ + UpdateFromAlphaFunc(self.body, update, run_time=run_time), + ] + for anim in anims: + self.push_anim(anim) + + return self + + +class SIRSimulation(VGroup): + CONFIG = { + "n_cities": 1, + "city_population": 100, + "box_size": 7, + "person_type": PiPerson, + "person_config": { + "height": 0.2, + "infection_radius": 0.6, + "gravity_strength": 1, + "wander_step_size": 1, + }, + "p_infection_per_day": 0.2, + "infection_time": 5, + "travel_rate": 0, + "limit_social_distancing_to_infectious": False, + } + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.time = 0 + self.add_updater(update_time) + + self.add_boxes() + self.add_people() + + self.add_updater(lambda m, dt: m.update_statusses(dt)) + + def add_boxes(self): + boxes = VGroup() + for x in range(self.n_cities): + box = Square() + box.set_height(self.box_size) + box.set_stroke(WHITE, 3) + boxes.add(box) + boxes.arrange_in_grid(buff=LARGE_BUFF) + self.add(boxes) + self.boxes = boxes + + def add_people(self): + people = VGroup() + for box in self.boxes: + dl_bound = box.get_corner(DL) + ur_bound = box.get_corner(UR) + box.people = VGroup() + for x in range(self.city_population): + person = self.person_type( + dl_bound=dl_bound, + ur_bound=ur_bound, + **self.person_config + ) + person.move_to([ + interpolate(lower, upper, random.random()) + for lower, upper in zip(dl_bound, ur_bound) + ]) + person.box = box + box.people.add(person) + people.add(person) + + # Choose a patient zero + random.choice(people).set_status("I") + self.add(people) + self.people = people + + def update_statusses(self, dt): + for box in self.boxes: + s_group, i_group = [ + list(filter( + lambda m: m.status == status, + box.people + )) + for status in ["S", "I"] + ] + + for s_person in s_group: + for i_person in i_group: + dist = get_norm(i_person.get_center() - s_person.get_center()) + if dist < s_person.infection_radius and random.random() < self.p_infection_per_day * dt: + s_person.set_status("I") + i_person.num_infected += 1 + for i_person in i_group: + if (i_person.time - i_person.infection_start_time) > self.infection_time: + i_person.set_status("R") + + # Travel + if self.travel_rate > 0: + path_func = path_along_arc(45 * DEGREES) + for person in self.people: + if random.random() < self.travel_rate * dt: + new_box = random.choice(self.boxes) + person.box.people.remove(person) + new_box.people.add(person) + person.box = new_box + person.dl_bound = new_box.get_corner(DL) + person.ur_bound = new_box.get_corner(UR) + + person.old_center = person.get_center() + person.new_center = new_box.get_center() + anim = UpdateFromAlphaFunc( + person, + lambda m, a: m.move_to(path_func( + m.old_center, m.new_center, a, + )), + run_time=1, + ) + person.push_anim(anim) + + # Social distancing + centers = np.array([person.get_center() for person in self.people]) + if self.limit_social_distancing_to_infectious: + repelled_centers = np.array([ + person.get_center() + for person in self.people + if person.symptomatic + ]) + else: + repelled_centers = centers + + if len(repelled_centers) > 0: + for center, person in zip(centers, self.people): + if person.social_distance_factor > 0: + diffs = np.linalg.norm(repelled_centers - center, axis=1) + person.repulsion_points = repelled_centers[np.argsort(diffs)[1:person.n_repulsion_points + 1]] + + def get_status_counts(self): + return np.array([ + len(list(filter( + lambda m: m.status == status, + self.people + ))) + for status in "SIR" + ]) + + def get_status_proportions(self): + counts = self.get_status_counts() + return counts / sum(counts) + + +class SIRGraph(VGroup): + CONFIG = { + "color_map": COLOR_MAP, + "height": 7, + "width": 5, + "update_frequency": 0.5, + "include_braces": False, + } + + def __init__(self, simulation, **kwargs): + super().__init__(**kwargs) + self.simulation = simulation + self.data = [simulation.get_status_proportions()] * 2 + self.add_axes() + self.add_graph() + self.add_x_labels() + + self.time = 0 + self.last_update_time = 0 + self.add_updater(update_time) + self.add_updater(lambda m: m.update_graph()) + self.add_updater(lambda m: m.update_x_labels()) + + def add_axes(self): + axes = Axes( + y_min=0, + y_max=1, + y_axis_config={ + "tick_frequency": 0.1, + }, + x_min=0, + x_max=1, + axis_config={ + "include_tip": False, + }, + ) + origin = axes.c2p(0, 0) + axes.x_axis.set_width(self.width, about_point=origin, stretch=True) + axes.y_axis.set_height(self.height, about_point=origin, stretch=True) + + self.add(axes) + self.axes = axes + + def add_graph(self): + self.graph = self.get_graph(self.data) + self.add(self.graph) + + def add_x_labels(self): + self.x_labels = VGroup() + self.x_ticks = VGroup() + self.add(self.x_ticks, self.x_labels) + + def get_graph(self, data): + axes = self.axes + i_points = [] + s_points = [] + for x, props in zip(np.linspace(0, 1, len(data)), data): + i_point = axes.c2p(x, props[1]) + s_point = axes.c2p(x, sum(props[:2])) + i_points.append(i_point) + s_points.append(s_point) + + r_points = [ + axes.c2p(0, 1), + axes.c2p(1, 1), + *s_points[::-1], + axes.c2p(0, 1), + ] + s_points.extend([ + *i_points[::-1], + s_points[0], + ]) + i_points.extend([ + axes.c2p(1, 0), + axes.c2p(0, 0), + i_points[0], + ]) + + points_lists = [s_points, i_points, r_points] + regions = VGroup(VMobject(), VMobject(), VMobject()) + + for region, status, points in zip(regions, "SIR", points_lists): + region.set_points_as_corners(points) + region.set_stroke(width=0) + region.set_fill(self.color_map[status], 1) + regions[0].set_fill(opacity=0.5) + + return regions + + def update_graph(self): + if (self.time - self.last_update_time) > self.update_frequency: + self.data.append(self.simulation.get_status_proportions()) + self.graph.become(self.get_graph(self.data)) + self.last_update_time = self.time + + def update_x_labels(self): + tick_height = 0.03 * self.graph.get_height() + tick_template = Line(DOWN, UP) + tick_template.set_height(tick_height) + + def get_tick(x): + tick = tick_template.copy() + tick.move_to(self.axes.c2p(x / self.time, 0)) + return tick + + def get_label(x, tick): + label = Integer(x) + label.set_height(tick_height) + label.next_to(tick, DOWN, buff=0.5 * tick_height) + return label + + self.x_labels.set_submobjects([]) + self.x_ticks.set_submobjects([]) + + if self.time < 15: + tick_range = range(1, int(self.time) + 1) + elif self.time < 50: + tick_range = range(5, int(self.time) + 1, 5) + elif self.time < 100: + tick_range = range(10, int(self.time) + 1, 10) + else: + tick_range = range(20, int(self.time) + 1, 20) + + for x in tick_range: + tick = get_tick(x) + label = get_label(x, tick) + self.x_ticks.add(tick) + self.x_labels.add(label) + + # TODO, if I care, refactor + if 10 < self.time < 15: + alpha = (self.time - 10) / 5 + for tick, label in zip(self.x_ticks, self.x_labels): + if label.get_value() % 5 != 0: + label.set_opacity(1 - alpha) + tick.set_opacity(1 - alpha) + if 45 < self.time < 50: + alpha = (self.time - 45) / 5 + for tick, label in zip(self.x_ticks, self.x_labels): + if label.get_value() % 10 == 5: + label.set_opacity(1 - alpha) + tick.set_opacity(1 - alpha) + + def add_v_line(self, line_time=None, color=YELLOW, stroke_width=3): + if line_time is None: + line_time = self.time + + axes = self.axes + v_line = Line( + axes.c2p(1, 0), axes.c2p(1, 1), + stroke_color=color, + stroke_width=stroke_width, + ) + v_line.add_updater( + lambda m: m.move_to( + axes.c2p(line_time / max(self.time, 1e-6), 0), + DOWN, + ) + ) + + self.add(v_line) + + +class GraphBraces(VGroup): + CONFIG = { + "update_frequency": 0.5, + } + + def __init__(self, graph, simulation, **kwargs): + super().__init__(**kwargs) + axes = self.axes = graph.axes + self.simulation = simulation + + ys = np.linspace(0, 1, 4) + self.lines = VGroup(*[ + Line(axes.c2p(1, y1), axes.c2p(1, y2)) + for y1, y2 in zip(ys, ys[1:]) + ]) + self.braces = VGroup(*[Brace(line, RIGHT) for line in self.lines]) + self.labels = VGroup( + TextMobject("Susceptible", color=COLOR_MAP["S"]), + TextMobject("Infectious", color=COLOR_MAP["I"]), + TextMobject("Removed", color=COLOR_MAP["R"]), + ) + + self.max_label_height = graph.get_height() * 0.05 + + self.add(self.braces, self.labels) + + self.time = 0 + self.last_update_time = -1 + self.add_updater(update_time) + self.add_updater(lambda m: m.update_braces()) + self.update(0) + + def update_braces(self): + if (self.time - self.last_update_time) <= self.update_frequency: + return + + self.last_update_time = self.time + lines = self.lines + braces = self.braces + labels = self.labels + axes = self.axes + + props = self.simulation.get_status_proportions() + ys = np.cumsum([0, props[1], props[0], props[2]]) + + epsilon = 1e-6 + for i, y1, y2 in zip([1, 0, 2], ys, ys[1:]): + lines[i].set_points_as_corners([ + axes.c2p(1, y1), + axes.c2p(1, y2), + ]) + height = lines[i].get_height() + + braces[i].set_height( + max(height, epsilon), + stretch=True + ) + braces[i].next_to(lines[i], RIGHT) + label_height = clip(height, epsilon, self.max_label_height) + labels[i].scale(label_height / labels[i][0][0].get_height()) + labels[i].next_to(braces[i], RIGHT) + return self + + +class ValueSlider(NumberLine): + CONFIG = { + "x_min": 0, + "x_max": 1, + "tick_frequency": 0.1, + "numbers_with_elongated_ticks": [], + "numbers_to_show": np.linspace(0, 1, 6), + "decimal_number_config": { + "num_decimal_places": 1, + }, + "stroke_width": 5, + "width": 8, + "marker_color": BLUE, + } + + def __init__(self, name, value, **kwargs): + super().__init__(**kwargs) + self.set_width(self.width, stretch=True) + self.add_numbers() + + self.marker = ArrowTip(start_angle=-90 * DEGREES) + self.marker.move_to(self.n2p(value), DOWN) + self.marker.set_color(self.marker_color) + self.add(self.marker) + + # self.label = DecimalNumber(value) + # self.label.next_to(self.marker, UP) + # self.add(self.label) + + self.name = TextMobject(name) + self.name.scale(1.25) + self.name.next_to(self, DOWN) + self.name.match_color(self.marker) + self.add(self.name) + + def get_change_anim(self, new_value, **kwargs): + start_value = self.p2n(self.marker.get_bottom()) + # m2l = self.label.get_center() - self.marker.get_center() + + def update(mob, alpha): + interim_value = interpolate(start_value, new_value, alpha) + mob.marker.move_to(mob.n2p(interim_value), DOWN) + # mob.label.move_to(mob.marker.get_center() + m2l) + # mob.label.set_value(interim_value) + + return UpdateFromAlphaFunc(self, update, **kwargs) + + +# Scenes + +class Test(Scene): + def construct(self): + path_func = path_along_arc(45 * DEGREES) + person = PiPerson(height=1, gravity_strength=0.2) + person.gravity_strength = 0 + + person.old_center = person.get_center() + person.new_center = 4 * RIGHT + + self.add(person) + self.wait() + + self.play(UpdateFromAlphaFunc( + person, + lambda m, a: m.move_to(path_func( + m.old_center, + m.new_center, + a, + )), + run_time=3, + rate_func=there_and_back, + )) + + self.wait(3) + self.wait(3) + + +class RunSimpleSimulation(Scene): + CONFIG = { + "simulation_config": { + "person_type": PiPerson, + "n_cities": 1, + "city_population": 100, + "person_config": { + "infection_radius": 0.75, + "social_distance_factor": 0, + "gravity_strength": 0.2, + "max_speed": 0.5, + }, + "travel_rate": 0, + "infection_time": 5, + }, + "graph_config": { + "update_frequency": 1 / 15, + }, + "graph_height_to_frame_height": 0.5, + "graph_width_to_frame_height": 0.75, + "include_graph_braces": True, + } + + def setup(self): + self.add_simulation() + self.position_camera() + self.add_graph() + self.add_sliders() + self.add_R_label() + self.add_total_cases_label() + + def construct(self): + self.run_until_zero_infections() + + def wait_until_infection_threshold(self, threshold): + self.wait_until(lambda: self.simulation.get_status_counts()[1] > threshold) + + def run_until_zero_infections(self): + while True: + self.wait(5) + if self.simulation.get_status_counts()[1] == 0: + self.wait(5) + break + + def add_R_label(self): + label = VGroup( + TexMobject("R = "), + DecimalNumber(), + ) + label.arrange(RIGHT) + boxes = self.simulation.boxes + label.set_width(0.25 * boxes.get_width()) + label.next_to(boxes.get_corner(DL), DR) + self.add(label) + + all_R0_values = [] + + def update_label(label): + if (self.time - label.last_update_time) < label.update_period: + return + label.last_update_time = self.time + + values = [] + for person in self.simulation.people: + if person.status == "I": + prop = (person.time - person.infection_start_time) / self.simulation.infection_time + if prop > 0.1: + values.append(person.num_infected / prop) + if len(values) > 0: + all_R0_values.append(np.mean(values)) + average = np.mean(all_R0_values[-20:]) + label[1].set_value(average) + + label.last_update_time = 0 + label.update_period = 1 + label.add_updater(update_label) + + def add_total_cases_label(self): + label = VGroup( + TextMobject("\\# Active cases = "), + Integer(1), + ) + label.arrange(RIGHT) + label[1].align_to(label[0][0][1], DOWN) + label.set_color(RED) + boxes = self.simulation.boxes + label.set_width(0.5 * boxes.get_width()) + label.next_to(boxes, UP, buff=0.03 * boxes.get_width()) + + label.add_updater( + lambda m: m[1].set_value(self.simulation.get_status_counts()[1]) + ) + self.total_cases_label = label + self.add(label) + + def add_simulation(self): + self.simulation = SIRSimulation(**self.simulation_config) + self.add(self.simulation) + + def position_camera(self): + frame = self.camera.frame + boxes = self.simulation.boxes + min_height = boxes.get_height() + 1 + min_width = 3 * boxes.get_width() + if frame.get_height() < min_height: + frame.set_height(min_height) + if frame.get_width() < min_width: + frame.set_width(min_width) + + frame.next_to(boxes.get_right(), LEFT, buff=-0.1 * boxes.get_width()) + + def add_graph(self): + frame = self.camera.frame + frame_height = frame.get_height() + graph = SIRGraph( + self.simulation, + height=self.graph_height_to_frame_height * frame_height, + width=self.graph_width_to_frame_height * frame_height, + **self.graph_config, + ) + graph.move_to(frame, UL) + graph.shift(0.05 * DR * frame_height) + self.add(graph) + self.graph = graph + + if self.include_graph_braces: + self.graph_braces = GraphBraces( + graph, + self.simulation, + update_frequency=graph.update_frequency + ) + self.add(self.graph_braces) + + def add_sliders(self): + pass + + +class RunSimpleSimulationWithDots(RunSimpleSimulation): + CONFIG = { + "simulation_config": { + "person_type": DotPerson, + } + } + + +class LargerCity(RunSimpleSimulation): + CONFIG = { + "simulation_config": { + "person_type": DotPerson, + "city_population": 1000, + "person_config": { + "infection_radius": 0.25, + "social_distance_factor": 0, + "gravity_strength": 0.2, + "max_speed": 0.25, + "height": 0.2 / 3, + "wall_buffer": 1 / 3, + "social_distance_color_threshold": 2 / 3, + }, + } + } + + +class LargerCity2(LargerCity): + CONFIG = { + "random_seed": 1, + } + + +class LargeCityHighInfectionRadius(LargerCity): + CONFIG = { + "simulation_config": { + "person_config": { + "infection_radius": 0.5, + }, + }, + "graph_config": { + "update_frequency": 1 / 60, + }, + } + + +class LargeCityLowerInfectionRate(LargerCity): + CONFIG = { + "p_infection_per_day": 0.1, + } + + +class SimpleSocialDistancing(RunSimpleSimulation): + CONFIG = { + "simulation_config": { + "person_type": PiPerson, + "n_cities": 1, + "city_population": 100, + "person_config": { + "infection_radius": 0.75, + "social_distance_factor": 2, + "gravity_strength": 0.1, + }, + "travel_rate": 0, + "infection_time": 5, + }, + } + + +class DelayedSocialDistancing(RunSimpleSimulation): + CONFIG = { + "delay_time": 8, + "target_sd_factor": 2, + "sd_probability": 1, + "random_seed": 1, + } + + def construct(self): + self.wait(self.delay_time) + self.change_social_distance_factor( + self.target_sd_factor, + self.sd_probability, + ) + self.graph.add_v_line() + self.play(self.sd_slider.get_change_anim(self.target_sd_factor)) + + self.run_until_zero_infections() + + def change_social_distance_factor(self, new_factor, prob): + for person in self.simulation.people: + if random.random() < prob: + person.social_distance_factor = new_factor + + def add_sliders(self): + slider = ValueSlider( + self.get_sd_slider_name(), + value=0, + x_min=0, + x_max=2, + tick_frequency=0.5, + numbers_with_elongated_ticks=[], + numbers_to_show=range(3), + decimal_number_config={ + "num_decimal_places": 0, + } + ) + fix_percent(slider.name[0][23 + int(self.sd_probability == 1)]) # So dumb + slider.match_width(self.graph) + slider.next_to(self.graph, DOWN, buff=0.2 * self.graph.get_height()) + self.add(slider) + self.sd_slider = slider + + def get_sd_slider_name(self): + return f"Social Distance Factor\\\\({int(100 * self.sd_probability)}$\\%$ of population)" + + +class DelayedSocialDistancingDot(DelayedSocialDistancing): + CONFIG = { + "simulation_config": { + "person_type": DotPerson, + } + } + + +class DelayedSocialDistancingLargeCity(DelayedSocialDistancing, LargerCity): + CONFIG = { + "trigger_infection_count": 50, + "simulation_config": { + 'city_population': 900, + } + } + + def construct(self): + self.wait_until_infection_threshold(self.trigger_infection_count) + self.change_social_distance_factor( + self.target_sd_factor, + self.sd_probability, + ) + self.graph.add_v_line() + self.play(self.sd_slider.get_change_anim(self.target_sd_factor)) + + self.run_until_zero_infections() + + +class DelayedSocialDistancingLargeCity90p(DelayedSocialDistancingLargeCity): + CONFIG = { + "sd_probability": 0.9, + } + + +class DelayedSocialDistancingLargeCity90pAlt(DelayedSocialDistancingLargeCity): + CONFIG = { + "sd_probability": 0.9, + "random_seed": 5, + } + + +class DelayedSocialDistancingLargeCity70p(DelayedSocialDistancingLargeCity): + CONFIG = { + "sd_probability": 0.7, + } + + +class DelayedSocialDistancingLargeCity50p(DelayedSocialDistancingLargeCity): + CONFIG = { + "sd_probability": 0.5, + } + + +class DelayedSocialDistancingWithDots(DelayedSocialDistancing): + CONFIG = { + "person_type": DotPerson, + } + + +class DelayedSocialDistancingProbHalf(DelayedSocialDistancing): + CONFIG = { + "sd_probability": 0.5, + } + + +class ReduceInfectionDuration(LargerCity): + CONFIG = { + "trigger_infection_count": 50, + } + + def construct(self): + self.wait_until_infection_threshold(self.trigger_infection_count) + self.play(self.slider.get_change_anim(1)) + self.simulation.infection_time = 1 + self.graph.add_v_line() + self.run_until_zero_infections() + + def add_sliders(self): + slider = ValueSlider( + "Infection duration", + value=5, + x_min=0, + x_max=5, + tick_frequency=1, + numbers_with_elongated_ticks=[], + numbers_to_show=range(6), + decimal_number_config={ + "num_decimal_places": 0, + }, + marker_color=RED, + ) + slider.match_width(self.graph) + slider.next_to(self.graph, DOWN, buff=0.2 * self.graph.get_height()) + self.add(slider) + self.slider = slider + + +class SimpleTravel(RunSimpleSimulation): + CONFIG = { + "simulation_config": { + "person_type": DotPerson, + "n_cities": 12, + "city_population": 100, + "person_config": { + "infection_radius": 0.75, + "social_distance_factor": 0, + "gravity_strength": 0.5, + }, + "travel_rate": 0.02, + "infection_time": 5, + }, + } + + def add_sliders(self): + slider = ValueSlider( + "Travel rate", + self.simulation.travel_rate, + x_min=0, + x_max=0.02, + tick_frequency=0.005, + numbers_with_elongated_ticks=[], + numbers_to_show=np.arange(0, 0.03, 0.01), + decimal_number_config={ + "num_decimal_places": 2, + } + ) + slider.match_width(self.graph) + slider.next_to(self.graph, DOWN, buff=0.2 * self.graph.get_height()) + self.add(slider) + self.tr_slider = slider + + +class SimpleTravel2(SimpleTravel): + CONFIG = { + "random_seed": 1, + } + + +class SimpleTravelLongInfectionPeriod(SimpleTravel): + CONFIG = { + "simulation_config": { + "infection_time": 10, + } + } + + +class SimpleTravelDelayedSocialDistancing(DelayedSocialDistancing, SimpleTravel): + CONFIG = { + "target_sd_factor": 2, + "sd_probability": 0.7, + "delay_time": 15, + "trigger_infection_count": 50, + } + + def construct(self): + self.wait_until_infection_threshold(self.trigger_infection_count) + self.change_social_distance_factor( + self.target_sd_factor, + self.sd_probability, + ) + self.graph.add_v_line() + self.play(self.sd_slider.get_change_anim(self.target_sd_factor)) + + self.run_until_zero_infections() + + def add_sliders(self): + SimpleTravel.add_sliders(self) + DelayedSocialDistancing.add_sliders(self) + + buff = 0.1 * self.graph.get_height() + + self.tr_slider.scale(0.8, about_edge=UP) + self.tr_slider.next_to(self.graph, DOWN, buff=buff) + + self.sd_slider.scale(0.8) + self.sd_slider.marker.set_color(YELLOW) + self.sd_slider.name.set_color(YELLOW) + self.sd_slider.next_to(self.tr_slider, DOWN, buff=buff) + + +class SimpleTravelDelayedSocialDistancing70p(SimpleTravelDelayedSocialDistancing): + pass + + +class SimpleTravelDelayedSocialDistancing99p(SimpleTravelDelayedSocialDistancing): + CONFIG = { + "sd_probability": 0.99, + } + + +class SimpleTravelDelayedSocialDistancing20p(SimpleTravelDelayedSocialDistancing): + CONFIG = { + "sd_probability": 0.20, + } + + +class SimpleTravelDelayedSocialDistancing50p(SimpleTravelDelayedSocialDistancing): + CONFIG = { + "sd_probability": 0.50, + "random_seed": 1, + } + + +class SimpleTravelDelayedSocialDistancing50pThreshold100(SimpleTravelDelayedSocialDistancing): + CONFIG = { + "sd_probability": 0.50, + "trigger_infection_count": 100, + "random_seed": 5, + } + + +class SimpleTravelDelayedSocialDistancing70pThreshold100(SimpleTravelDelayedSocialDistancing): + CONFIG = { + "sd_probability": 0.70, + "trigger_infection_count": 100, + } + + +class SimpleTravelSocialDistancePlusZeroTravel(SimpleTravelDelayedSocialDistancing): + CONFIG = { + "sd_probability": 1, + "target_travel_rate": 0, + } + + def construct(self): + self.wait_until_infection_threshold(self.trigger_infection_count) + self.change_social_distance_factor( + self.target_sd_factor, + self.sd_probability, + ) + self.simulation.travel_rate = self.target_travel_rate + self.graph.add_v_line() + self.play( + self.tr_slider.get_change_anim(self.simulation.travel_rate), + self.sd_slider.get_change_anim(self.target_sd_factor), + ) + + self.run_until_zero_infections() + + +class SecondWave(SimpleTravelSocialDistancePlusZeroTravel): + def run_until_zero_infections(self): + self.wait_until(lambda: self.simulation.get_status_counts()[1] < 10) + self.change_social_distance_factor(0, 1) + self.simulation.travel_rate = 0.02 + self.graph.add_v_line() + self.play( + self.tr_slider.get_change_anim(0.02), + self.sd_slider.get_change_anim(0), + ) + super().run_until_zero_infections() + + +class SimpleTravelSocialDistancePlusZeroTravel99p(SimpleTravelSocialDistancePlusZeroTravel): + CONFIG = { + "sd_probability": 0.99, + } + + +class SimpleTravelDelayedTravelReduction(SimpleTravelDelayedSocialDistancing): + CONFIG = { + "trigger_infection_count": 50, + "target_travel_rate": 0.002, + } + + def construct(self): + self.wait_until_infection_threshold(self.trigger_infection_count) + self.simulation.travel_rate = self.target_travel_rate + self.graph.add_v_line() + self.play(self.tr_slider.get_change_anim(self.simulation.travel_rate)) + self.run_until_zero_infections() + + +class SimpleTravelDelayedTravelReductionThreshold100(SimpleTravelDelayedTravelReduction): + CONFIG = { + "random_seed": 2, + "trigger_infection_count": 100, + } + + +class SimpleTravelDelayedTravelReductionThreshold100TargetHalfPercent(SimpleTravelDelayedTravelReduction): + CONFIG = { + "random_seed": 2, + "trigger_infection_count": 100, + "target_travel_rate": 0.005, + } + + +class SimpleTravelDelayedTravelReductionThreshold100TargetHalfPercent2(SimpleTravelDelayedTravelReductionThreshold100TargetHalfPercent): + CONFIG = { + "random_seed": 1, + "sd_probability": 0.5, + } + + def setup(self): + super().setup() + for x in range(2): + random.choice(self.simulation.people).set_status("I") + + +class SimpleTravelLargeCity(SimpleTravel, LargerCity): + CONFIG = { + "simulation_config": { + "n_cities": 12, + "travel_rate": 0.02, + } + } + + +class SimpleTravelLongerDelayedSocialDistancing(SimpleTravelDelayedSocialDistancing): + CONFIG = { + "trigger_infection_count": 100, + } + + +class SimpleTravelLongerDelayedTravelReduction(SimpleTravelDelayedTravelReduction): + CONFIG = { + "trigger_infection_count": 100, + } + + +class SocialDistanceAfterFiveDays(DelayedSocialDistancing): + CONFIG = { + "sd_probability": 0.7, + "delay_time": 5, + "simulation_config": { + "travel_rate": 0 + }, + } + + +class QuarantineInfectious(RunSimpleSimulation): + CONFIG = { + "trigger_infection_count": 10, + "target_sd_factor": 3, + "infection_time_before_quarantine": 1, + } + + def construct(self): + self.wait_until_infection_threshold(self.trigger_infection_count) + self.add_quarantine_box() + self.set_quarantine_updaters() + self.run_until_zero_infections() + + def add_quarantine_box(self): + boxes = self.simulation.boxes + q_box = boxes[0].copy() + q_box.set_color(RED_E) + q_box.set_width(boxes.get_width() / 3) + q_box.next_to( + boxes, LEFT, + aligned_edge=DOWN, + buff=0.25 * q_box.get_width() + ) + + label = TextMobject("Quarantine zone") + label.set_color(RED) + label.match_width(q_box) + label.next_to(q_box, DOWN, buff=0.1 * q_box.get_width()) + + self.add(q_box) + self.add(label) + self.q_box = q_box + + def set_quarantine_updaters(self): + def quarantine_if_ready(simulation): + for person in simulation.people: + send_to_q_box = all([ + not person.is_quarantined, + person.symptomatic, + (person.time - person.infection_start_time) > self.infection_time_before_quarantine, + ]) + if send_to_q_box: + person.box = self.q_box + person.dl_bound = self.q_box.get_corner(DL) + person.ur_bound = self.q_box.get_corner(UR) + person.old_center = person.get_center() + person.new_center = self.q_box.get_center() + point = VectorizedPoint(person.get_center()) + person.push_anim(ApplyMethod(point.move_to, self.q_box.get_center(), run_time=0.5)) + person.push_anim(MaintainPositionRelativeTo(person, point)) + person.move_to(self.q_box.get_center()) + person.is_quarantined = True + + for person in self.simulation.people: + person.is_quarantined = False + # person.add_updater(quarantine_if_ready) + self.simulation.add_updater(quarantine_if_ready) + + +class QuarantineInfectiousLarger(QuarantineInfectious, LargerCity): + CONFIG = { + "trigger_infection_count": 50, + } + + +class QuarantineInfectiousLargerWithTail(QuarantineInfectiousLarger): + def construct(self): + super().construct() + self.simulation.clear_updaters() + self.wait(25) + + +class QuarantineInfectiousTravel(QuarantineInfectious, SimpleTravel): + CONFIG = { + "trigger_infection_count": 50, + } + + def add_sliders(self): + pass + + +class QuarantineInfectious80p(QuarantineInfectious): + CONFIG = { + "simulation_config": { + "person_config": { + "p_symptomatic_on_infection": 0.8, + } + } + } + + +class QuarantineInfectiousLarger80p(QuarantineInfectiousLarger): + CONFIG = { + "simulation_config": { + "person_config": { + "p_symptomatic_on_infection": 0.8, + } + } + } + + +class QuarantineInfectiousTravel80p(QuarantineInfectiousTravel): + CONFIG = { + "simulation_config": { + "person_config": { + "p_symptomatic_on_infection": 0.8, + } + } + } + + +class QuarantineInfectious50p(QuarantineInfectious): + CONFIG = { + "simulation_config": { + "person_config": { + "p_symptomatic_on_infection": 0.5, + } + } + } + + +class QuarantineInfectiousLarger50p(QuarantineInfectiousLarger): + CONFIG = { + "simulation_config": { + "person_config": { + "p_symptomatic_on_infection": 0.5, + } + } + } + + +class QuarantineInfectiousTravel50p(QuarantineInfectiousTravel): + CONFIG = { + "simulation_config": { + "person_config": { + "p_symptomatic_on_infection": 0.5, + } + } + } + + +class CentralMarket(DelayedSocialDistancing): + CONFIG = { + "sd_probability": 0.7, + "delay_time": 5, + "simulation_config": { + "person_type": DotPerson, + "travel_rate": 0 + }, + "shopping_frequency": 0.05, + "shopping_time": 1, + } + + def setup(self): + super().setup() + for person in self.simulation.people: + person.last_shopping_trip = -3 + person.is_shopping = False + + square = Square() + square.set_height(0.2) + square.set_color(WHITE) + square.move_to(self.simulation.boxes[0].get_center()) + self.add(square) + + self.simulation.add_updater( + lambda m, dt: self.add_travel_anims(m, dt) + ) + + def construct(self): + self.run_until_zero_infections() + + def add_travel_anims(self, simulation, dt): + shopping_time = self.shopping_time + for person in simulation.people: + time_since_trip = person.time - person.last_shopping_trip + if time_since_trip > shopping_time: + if random.random() < dt * self.shopping_frequency: + person.last_shopping_trip = person.time + + point = VectorizedPoint(person.get_center()) + anim1 = ApplyMethod( + point.move_to, person.box.get_center(), + path_arc=45 * DEGREES, + run_time=shopping_time, + rate_func=there_and_back_with_pause, + ) + anim2 = MaintainPositionRelativeTo(person, point, run_time=shopping_time) + + person.push_anim(anim1) + person.push_anim(anim2) + + def add_sliders(self): + pass + + +class CentralMarketLargePopulation(CentralMarket, LargerCity): + pass + + +class CentralMarketLowerInfection(CentralMarketLargePopulation): + CONFIG = { + "simulation_config": { + "p_infection_per_day": 0.1, + } + } + + +class CentralMarketVeryFrequentLargePopulationDelayedSocialDistancing(CentralMarketLowerInfection): + CONFIG = { + "sd_probability": 0.7, + "trigger_infection_count": 25, + "simulation_config": { + "person_type": DotPerson, + }, + "target_sd_factor": 2, + } + + def construct(self): + self.wait_until_infection_threshold(self.trigger_infection_count) + self.graph.add_v_line() + for person in self.simulation.people: + person.social_distance_factor = self.target_sd_factor + self.run_until_zero_infections() + + +class CentralMarketLessFrequent(CentralMarketVeryFrequentLargePopulationDelayedSocialDistancing): + CONFIG = { + "target_shopping_frequency": 0.01, + "trigger_infection_count": 100, + "random_seed": 1, + "simulation_config": { + 'city_population': 900, + }, + } + + def construct(self): + self.wait_until(lambda: self.simulation.get_status_counts()[1] > self.trigger_infection_count) + for person in self.simulation.people: + person.social_distance_factor = 2 + # Decrease shopping rate + self.graph.add_v_line() + self.change_slider() + self.run_until_zero_infections() + + def change_slider(self): + self.play(self.shopping_slider.get_change_anim(self.target_shopping_frequency)) + self.shopping_frequency = self.target_shopping_frequency + + def add_sliders(self): + slider = ValueSlider( + "Shopping frequency", + value=self.shopping_frequency, + x_min=0, + x_max=0.05, + tick_frequency=0.01, + numbers_with_elongated_ticks=[], + numbers_to_show=np.arange(0, 0.06, 0.01), + decimal_number_config={ + "num_decimal_places": 2, + } + ) + slider.match_width(self.graph) + slider.next_to(self.graph, DOWN, buff=0.2 * self.graph.get_height()) + self.add(slider) + self.shopping_slider = slider + + +class CentralMarketDownToZeroFrequency(CentralMarketLessFrequent): + CONFIG = { + "target_shopping_frequency": 0, + } + + +class CentralMarketQuarantine(QuarantineInfectiousLarger, CentralMarketLowerInfection): + CONFIG = { + "random_seed": 1, + } + + def construct(self): + self.wait_until_infection_threshold(self.trigger_infection_count) + self.graph.add_v_line() + self.add_quarantine_box() + self.set_quarantine_updaters() + self.run_until_zero_infections() + + +class CentralMarketQuarantine80p(CentralMarketQuarantine): + CONFIG = { + "simulation_config": { + "person_config": { + "p_symptomatic_on_infection": 0.8, + } + } + } + + +class CentralMarketTransitionToLowerInfection(CentralMarketLessFrequent): + CONFIG = { + "target_p_infection_per_day": 0.05, # From 0.1 + "trigger_infection_count": 100, + "random_seed": 1, + "simulation_config": { + 'city_population': 900, + }, + } + + def change_slider(self): + self.play(self.infection_slider.get_change_anim(self.target_p_infection_per_day)) + self.simulation.p_infection_per_day = self.target_p_infection_per_day + + def add_sliders(self): + slider = ValueSlider( + "Infection rate", + value=self.simulation.p_infection_per_day, + x_min=0, + x_max=0.2, + tick_frequency=0.05, + numbers_with_elongated_ticks=[], + numbers_to_show=np.arange(0, 0.25, 0.05), + decimal_number_config={ + "num_decimal_places": 2, + }, + marker_color=RED, + ) + slider.match_width(self.graph) + slider.next_to(self.graph, DOWN, buff=0.2 * self.graph.get_height()) + self.add(slider) + self.infection_slider = slider + + +class CentralMarketTransitionToLowerInfectionAndLowerFrequency(CentralMarketTransitionToLowerInfection): + CONFIG = { + "random_seed": 2, + } + + def change_slider(self): + super().change_slider() + self.shopping_frequency = self.target_shopping_frequency + + +# Filler animations + +class TableOfContents(Scene): + def construct(self): + chapters = VGroup( + TextMobject("Basic setup"), + TextMobject("Identify and isolate"), + TextMobject("Social distancing"), + TextMobject("Travel restrictions"), + TextMobject("$R$"), + TextMobject("Central hubs"), + ) + chapters.arrange(DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT) + chapters.set_height(FRAME_HEIGHT - 1) + chapters.to_edge(LEFT, buff=LARGE_BUFF) + + for chapter in chapters: + dot = Dot() + dot.next_to(chapter, LEFT, SMALL_BUFF) + chapter.add(dot) + chapter.save_state() + + self.add(chapters) + + for chapter in chapters: + non_chapter = [c for c in chapters if c is not chapter] + chapter.target = chapter.saved_state.copy() + chapter.target.scale(1.5, about_edge=LEFT) + chapter.target.set_fill(YELLOW, 1) + for nc in non_chapter: + nc.target = nc.saved_state.copy() + nc.target.scale(0.8, about_edge=LEFT) + nc.target.set_fill(WHITE, 0.5) + self.play(*map(MoveToTarget, chapters)) + self.wait() + self.play(*map(Restore, chapters)) + self.wait() + + +class DescribeModel(Scene): + def construct(self): + # Setup words + words = TextMobject( + "Susceptible", + "Infectious", + "Recovered", + ) + words.scale(0.8 / words[0][0].get_height()) + + colors = [ + COLOR_MAP["S"], + COLOR_MAP["I"], + interpolate_color(COLOR_MAP["R"], WHITE, 0.5), + ] + + initials = VGroup() + for i, word, color in zip(it.count(), words, colors): + initials.add(word[0][0]) + word.set_color(color) + word.move_to(2 * i * DOWN) + word.to_edge(LEFT) + words.to_corner(UL) + + # Rearrange initials + initials.save_state() + initials.arrange(RIGHT, buff=SMALL_BUFF) + initials.set_color(WHITE) + + title = VGroup(initials, TextMobject("Model")) + title[1].match_height(title[0]) + title.arrange(RIGHT, buff=MED_LARGE_BUFF) + title.center() + title.to_edge(UP) + + self.play(FadeInFromDown(title)) + self.wait() + self.play( + Restore( + initials, + path_arc=-90 * DEGREES, + ), + FadeOut(title[1]) + ) + self.wait() + + # Show each category + pi = PiPerson( + height=3, + max_speed=0, + infection_radius=5, + ) + pi.color_map["R"] = words[2].get_color() + pi.center() + pi.body.change("pondering", initials[0]) + + word_anims = [] + for word in words: + word_anims.append(LaggedStartMap( + FadeInFrom, word[1:], + lambda m: (m, 0.2 * LEFT), + )) + + self.play( + Succession( + FadeInFromDown(pi), + ApplyMethod(pi.body.change, "guilty"), + ), + word_anims[0], + run_time=2, + ) + words[0].pi = pi.copy() + self.play( + words[0].pi.set_height, 1, + words[0].pi.next_to, words[0], RIGHT, + ) + self.play(Blink(pi.body)) + + pi.set_status("I") + point = VectorizedPoint(pi.get_center()) + self.play( + point.shift, 3 * RIGHT, + MaintainPositionRelativeTo(pi, point), + word_anims[1], + run_time=2, + ) + words[1].pi = pi.copy() + self.play( + words[1].pi.set_height, 1, + words[1].pi.next_to, words[1], RIGHT, + ) + self.wait(3) + + pi.set_status("R") + self.play( + word_anims[2], + Animation(pi, suspend_mobject_updating=False) + ) + words[2].pi = pi.copy() + self.play( + words[2].pi.set_height, 1, + words[2].pi.next_to, words[2], RIGHT, + ) + self.wait() + + # Show rules + i_pi = PiPerson( + height=1.5, + max_speed=0, + infection_radius=6, + status="S", + ) + i_pi.set_status("I") + s_pis = VGroup() + for vect in [RIGHT, UP, LEFT, DOWN]: + s_pi = PiPerson( + height=1.5, + max_speed=0, + infection_radius=6, + status="S", + ) + s_pi.next_to(i_pi, vect, MED_LARGE_BUFF) + s_pis.add(s_pi) + + VGroup(i_pi, s_pis).to_edge(RIGHT) + + circle = Circle(radius=3) + circle.move_to(i_pi) + dashed_circle = DashedVMobject(circle, num_dashes=30) + dashed_circle.set_color(RED) + + self.play( + FadeOut(pi), + FadeIn(s_pis), + FadeIn(i_pi), + ) + anims = [] + for s_pi in s_pis: + anims.append(ApplyMethod(s_pi.body.look_at, i_pi.body.eyes)) + self.play(*anims) + self.add(VGroup(i_pi, *s_pis)) + self.wait() + self.play(ShowCreation(dashed_circle)) + self.wait() + shuffled = list(s_pis) + random.shuffle(shuffled) + for s_pi in shuffled: + s_pi.set_status("I") + self.wait(3 * random.random()) + self.wait(2) + self.play(FadeOut(s_pis), FadeOut(dashed_circle)) + + # Let time pass + clock = Clock() + clock.next_to(i_pi.body, UP, buff=LARGE_BUFF) + + self.play( + VFadeIn(clock), + ClockPassesTime( + clock, + run_time=5, + hours_passed=5, + ), + ) + i_pi.set_status("R") + self.wait(1) + self.play(Blink(i_pi.body)) + self.play(FadeOut(clock)) + + # Removed + removed = TextMobject("Removed") + removed.match_color(words[2]) + removed.match_height(words[2]) + removed.move_to(words[2], DL) + + self.play( + FadeOut(words[2], UP), + FadeIn(removed, DOWN), + ) + self.play( + i_pi.body.change, 'pleading', removed, + ) + self.play(Blink(i_pi.body)) + self.wait() + + +class SubtlePangolin(Scene): + def construct(self): + pangolin = SVGMobject(file_name="pangolin") + pangolin.set_height(0.5) + pangolin.set_fill(GREY_BROWN, opacity=0) + pangolin.set_stroke(GREY_BROWN, width=1) + self.play(ShowCreationThenFadeOut(pangolin)) + self.play(FadeOut(pangolin)) + + self.embed() + + +class DoubleRadiusInGroup(Scene): + def construct(self): + radius = 1 + + pis = VGroup(*[ + PiPerson( + height=0.5, + max_speed=0, + wander_step_size=0, + infection_radius=4 * radius, + ) + for x in range(49) + ]) + pis.arrange_in_grid() + pis.set_height(FRAME_HEIGHT - 1) + sicky = pis[24] + sicky.set_status("I") + + circle = Circle(radius=radius) + circle.move_to(sicky) + dashed_circle = DashedVMobject(circle, num_dashes=30) + dashed_circle2 = dashed_circle.copy() + dashed_circle2.scale(2) + + self.add(pis) + self.play(ShowCreation(dashed_circle, lag_ratio=0)) + self.play(ShowCreation(dashed_circle2, lag_ratio=0)) + anims = [] + for pi in pis: + if pi.status == "S": + anims.append(ApplyMethod( + pi.body.change, "pleading", sicky.body.eyes + )) + random.shuffle(anims) + self.play(LaggedStart(*anims)) + self.wait(10) + + +class CutPInfectionInHalf(Scene): + def construct(self): + # Add people + sicky = PiPerson( + height=1, + infection_radius=4, + max_speed=0, + wander_step_size=0, + ) + normy = sicky.deepcopy() + normy.next_to(sicky, RIGHT) + normy.body.look_at(sicky.body.eyes) + + circ = Circle(radius=4) + d_circ = DashedVMobject(circ, num_dashes=30) + d_circ.set_color(RED) + d_circ.move_to(sicky) + + sicky.set_status("I") + self.add(sicky, normy) + self.add(d_circ) + self.play(d_circ.scale, 0.5) + self.wait() + + # Prob label + eq = VGroup( + TexMobject("P(\\text{Infection}) = "), + DecimalNumber(0.2), + ) + eq.arrange(RIGHT, buff=0.2) + eq.to_edge(UP) + + arrow = Vector(0.5 * RIGHT) + arrow.next_to(eq, RIGHT) + new_rhs = eq[1].copy() + new_rhs.next_to(arrow, RIGHT) + new_rhs.set_color(YELLOW) + + self.play(FadeIn(eq)) + self.play( + TransformFromCopy(eq[1], new_rhs), + GrowArrow(arrow) + ) + self.play(ChangeDecimalToValue(new_rhs, 0.1)) + self.wait(2) + + # Each day + clock = Clock() + clock.set_height(1) + clock.next_to(normy, UR, buff=0.7) + + def get_clock_run(clock): + return ClockPassesTime( + clock, + hours_passed=1, + run_time=1, + ) + + self.play( + VFadeIn(clock), + get_clock_run(clock), + ) + + # Random choice + choices = VGroup() + for x in range(9): + choices.add(TexMobject(CMARK_TEX, color=BLUE)) + for x in range(1): + choices.add(TexMobject(XMARK_TEX, color=RED)) + choices.arrange(DOWN) + choices.set_height(3) + choices.next_to(clock, DOWN) + + rect = SurroundingRectangle(choices[0]) + self.add(choices, rect) + + def show_random_choice(scene, rect, choices): + for x in range(10): + rect.move_to(random.choice(choices[:-1])) + scene.wait(0.1) + + show_random_choice(self, rect, choices) + + for x in range(6): + self.play(get_clock_run(clock)) + show_random_choice(self, rect, choices) + rect.move_to(choices[-1]) + normy.set_status("I") + self.add(normy) + self.play( + FadeOut(clock), + FadeOut(choices), + FadeOut(rect), + ) + self.wait(4) + + +class KeyTakeaways(Scene): + def construct(self): + takeaways = VGroup(*[ + TextMobject( + text, + alignment="", + ) + for text in [ + """ + Changes in how many people slip through the tests\\\\ + cause disproportionately large changes to the total\\\\ + number of people infected. + """, + """ + Social distancing slows the spread, but\\\\ + even small imperfections prolong it. + """, + """ + Reducing transit between communities, late in the game,\\\\ + has a very limited effect. + """, + """ + Shared central locations dramatically speed up\\\\ + the spread. + """, + ] + ]) + takeaway = TextMobject( + "The growth rate is very sensitive to:\\\\", + "- \\# Daily interactions\\\\", + "- Probability of infection\\\\", + "- Duration of illness\\\\", + alignment="", + ) + takeaway[1].set_color(GREEN_D) + takeaway[2].set_color(GREEN_C) + takeaway[3].set_color(GREEN_B) + takeaway.arrange(DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT) + takeaways.add_to_back(takeaway) + + takeaways.scale(1.25) + + titles = VGroup() + for i in range(len(takeaways)): + title = TextMobject("Key takeaway \\#") + num = Integer(i + 1) + num.next_to(title, RIGHT, buff=SMALL_BUFF) + title.add(num) + titles.add(title) + + titles.arrange(DOWN, buff=LARGE_BUFF, aligned_edge=LEFT) + titles.to_edge(LEFT) + titles.set_color(GREY_D) + + h_line = Line(LEFT, RIGHT) + h_line.set_width(FRAME_WIDTH - 1) + h_line.to_edge(UP, buff=1.5) + + for takeaway in takeaways: + takeaway.next_to(h_line, DOWN, buff=0.75) + max_width = FRAME_WIDTH - 2 + if takeaway.get_width() > max_width: + takeaway.set_width(max_width) + + takeaways[3].shift(0.25 * UP) + + self.add(titles) + self.wait() + + for title, takeaway in zip(titles, takeaways): + other_titles = VGroup(*titles) + other_titles.remove(title) + self.play(title.set_color, WHITE, run_time=0.5) + title.save_state() + temp_h_line = h_line.copy() + self.play( + title.scale, 1.5, + title.to_edge, UP, + title.set_x, 0, + title.set_color, YELLOW, + FadeOut(other_titles), + ShowCreation(temp_h_line), + ) + + self.play(FadeIn(takeaway, lag_ratio=0.1, run_time=2, rate_func=linear)) + self.wait(2) + temp_h_line.rotate(PI) + self.play( + Restore(title), + FadeIn(other_titles), + Uncreate(temp_h_line), + FadeOut(takeaway, DOWN, lag_ratio=0.25 / len(takeaway.family_members_with_points())) + ) + self.wait() + + self.embed() + + +class AsymptomaticCases(Scene): + def construct(self): + pis = VGroup(*[ + PiPerson( + height=1, + infection_radius=2, + wander_step_size=0, + max_speed=0, + ) + for x in range(5) + ]) + pis.arrange(RIGHT, buff=2) + pis.to_edge(DOWN, buff=2) + + sneaky = pis[1] + sneaky.p_symptomatic_on_infection = 0 + + self.add(pis) + + for pi in pis: + if pi is sneaky: + pi.color_map["I"] = YELLOW + pi.mode_map["I"] = "coin_flip_1" + else: + pi.color_map["I"] = RED + pi.mode_map["I"] = "sick" + pi.unlock_triangulation() + pi.set_status("I") + self.wait(0.1) + self.wait(2) + + label = TextMobject("Never isolated") + label.set_height(0.8) + label.to_edge(UP) + label.set_color(YELLOW) + + arrow = Arrow( + label.get_bottom(), + sneaky.body.get_top(), + buff=0.5, + max_tip_length_to_length_ratio=0.5, + stroke_width=6, + max_stroke_width_to_length_ratio=10, + ) + + self.play( + FadeInFromDown(label), + GrowArrow(arrow), + ) + self.wait(13) + + +class WeDontHaveThat(TeacherStudentsScene): + def construct(self): + self.student_says( + "But we don't\\\\have that!", + target_mode="angry", + added_anims=[self.teacher.change, "guilty"] + ) + self.change_all_student_modes( + "angry", + look_at_arg=self.teacher.eyes + ) + self.wait(5) + + +class IntroduceSocialDistancing(Scene): + def construct(self): + pis = VGroup(*[ + PiPerson( + height=2, + wander_step_size=0, + gravity_well=None, + social_distance_color_threshold=5, + max_social_distance_stroke_width=10, + dl_bound=[-FRAME_WIDTH / 2 + 1, -2], + ur_bound=[FRAME_WIDTH / 2 - 1, 2], + ) + for x in range(3) + ]) + pis.arrange(RIGHT, buff=0.25) + pis.move_to(DOWN) + pi1, pi2, pi3 = pis + + slider = ValueSlider( + "Social distance factor", + 0, + x_min=0, + x_max=5, + tick_frequency=1, + numbers_to_show=range(6), + marker_color=YELLOW, + ) + slider.center() + slider.to_edge(UP) + self.add(slider) + + def update_pi(pi): + pi.social_distance_factor = 4 * slider.p2n(slider.marker.get_center()) + + for pi in pis: + pi.add_updater(update_pi) + pi.repulsion_points = [ + pi2.get_center() + for pi2 in pis + if pi2 is not pi + ] + + self.add(pis) + self.play( + FadeIn(slider), + *[ + ApplyMethod(pi1.body.look_at, pi2.body.eyes) + for pi1, pi2 in zip(pis, [*pis[1:], pis[0]]) + ] + ) + self.add(*pis) + self.wait() + self.play(slider.get_change_anim(3)) + self.wait(4) + + for i, vect in (0, RIGHT), (2, LEFT): + pis.suspend_updating() + pis[1].generate_target() + pis[1].target.next_to(pis[i], vect, SMALL_BUFF) + pis[1].target.body.look_at(pis[i].body.eyes) + self.play( + MoveToTarget(pis[1]), + path_arc=PI, + ) + pis.resume_updating() + self.wait(5) + self.wait(5) + + self.embed() + + +class FastForwardBy2(Scene): + CONFIG = { + "n": 2, + } + + def construct(self): + n = self.n + triangles = VGroup(*[ + ArrowTip(start_angle=0) + for x in range(n) + ]) + triangles.arrange(RIGHT, buff=0.01) + + label = VGroup(TexMobject("\\times"), Integer(n)) + label.set_height(0.4) + label.arrange(RIGHT, buff=SMALL_BUFF) + label.next_to(triangles, RIGHT, buff=SMALL_BUFF) + + for mob in triangles, label: + mob.set_color(GREY_A) + mob.set_stroke(BLACK, 4, background=True) + + self.play( + LaggedStartMap( + FadeInFrom, triangles, + lambda m: (m, 0.4 * LEFT), + ), + FadeIn(label, 0.2 * LEFT), + run_time=1, + ) + self.play( + FadeOut(label), + FadeOut(triangles), + ) + + +class FastForwardBy4(FastForwardBy2): + CONFIG = { + "n": 4, + } + + +class DontLetThisHappen(Scene): + def construct(self): + text = TextMobject("Don't let\\\\this happen!") + text.scale(1.5) + text.set_stroke(BLACK, 5, background=True) + arrow = Arrow( + text.get_top(), + text.get_top() + 2 * UR + 0.5 * RIGHT, + path_arc=-120 * DEGREES, + ) + + self.add(text) + self.play( + Write(text, run_time=1), + ShowCreation(arrow), + ) + self.wait() + + +class ThatsNotHowIBehave(TeacherStudentsScene): + def construct(self): + self.student_says( + "That's...not\\\\how I behave.", + target_mode="sassy", + look_at_arg=self.screen, + ) + self.play( + self.teacher.change, "guilty", + self.get_student_changes("erm", "erm", "sassy") + ) + self.look_at(self.screen) + self.wait(20) + + +class BetweenNothingAndQuarantineWrapper(Scene): + def construct(self): + self.add(FullScreenFadeRectangle( + fill_color=GREY_E, + fill_opacity=1, + )) + rects = VGroup(*[ + ScreenRectangle() + for x in range(3) + ]) + rects.arrange(RIGHT) + rects.set_width(FRAME_WIDTH - 1) + self.add(rects) + + rects.set_fill(BLACK, 1) + rects.set_stroke(WHITE, 2) + + titles = VGroup( + TextMobject("Do nothing"), + TextMobject("Quarantine\\\\", "80$\\%$ of cases"), + TextMobject("Quarantine\\\\", "all cases"), + ) + fix_percent(titles[1][1][2]) + for title, rect in zip(titles, rects): + title.next_to(rect, UP) + + q_marks = TexMobject("???") + q_marks.scale(2) + q_marks.move_to(rects[1]) + + self.add(rects) + self.play(LaggedStartMap( + FadeInFromDown, + VGroup(titles[0], titles[2], titles[1]), + lag_ratio=0.3, + )) + self.play(Write(q_marks)) + self.wait() + + +class DarkerInterpretation(Scene): + def construct(self): + qz = TextMobject("Quarantine zone") + qz.set_color(RED) + qz.set_width(2) + line = Line(qz.get_left(), qz.get_right()) + + new_words = TextMobject("Deceased") + new_words.replace(qz, dim_to_match=1) + new_words.set_color(RED) + + self.add(qz) + self.wait() + self.play(ShowCreation(line)) + self.play( + FadeOut(qz), + FadeOut(line), + FadeIn(new_words) + ) + self.wait() + + +class SARS2002(TeacherStudentsScene): + def construct(self): + image = ImageMobject("sars_icon") + image.set_height(3.5) + image.move_to(self.hold_up_spot, DR) + image.shift(RIGHT) + + name = TextMobject("2002 SARS Outbreak") + name.next_to(image, LEFT, MED_LARGE_BUFF, aligned_edge=UP) + + n_cases = VGroup( + Integer(0, edge_to_fix=UR), + TextMobject("total cases") + ) + n_cases.arrange(RIGHT) + n_cases.scale(1.25) + n_cases.next_to(name, DOWN, buff=2) + + arrow = Arrow(name.get_bottom(), n_cases.get_top()) + + n_cases.shift(MED_SMALL_BUFF * RIGHT) + + self.play( + self.teacher.change, "raise_right_hand", + FadeIn(image, DOWN, run_time=2), + self.get_student_changes( + "pondering", "thinking", "pondering", + look_at_arg=image, + ) + ) + self.play( + FadeIn(name, RIGHT), + ) + self.play( + GrowArrow(arrow), + UpdateFromAlphaFunc( + n_cases, + lambda m, a: m.set_opacity(a), + ), + ChangeDecimalToValue(n_cases[0], 8098), + self.get_student_changes(look_at_arg=n_cases), + ) + self.wait() + self.change_all_student_modes( + "thinking", look_at_arg=n_cases, + ) + self.play(self.teacher.change, "tease") + self.wait(6) + + self.embed() + + +class QuarteringLines(Scene): + def construct(self): + lines = VGroup( + Line(UP, DOWN), + Line(LEFT, RIGHT), + ) + lines.set_width(FRAME_WIDTH) + lines.set_height(FRAME_HEIGHT, stretch=True) + lines.set_stroke(WHITE, 3) + self.play(ShowCreation(lines)) + self.wait() + + +class Eradicated(Scene): + def construct(self): + word = TextMobject("Eradicated") + word.set_color(GREEN) + self.add(word) + + +class LeftArrow(Scene): + def construct(self): + arrow = Vector(2 * LEFT) + self.play(GrowArrow(arrow)) + self.wait() + self.play(FadeOut(arrow)) + + +class IndicationArrow(Scene): + def construct(self): + vect = Vector( + 0.5 * DR, + max_tip_length_to_length_ratio=0.4, + max_stroke_width_to_length_ratio=10, + stroke_width=5, + ) + vect.set_color(YELLOW) + self.play(GrowArrow(vect)) + self.play(FadeOut(vect)) + + +class REq(Scene): + def construct(self): + mob = TexMobject("R_0 = ")[0] + mob[1].set_color(BLACK) + mob[2].shift(mob[1].get_width() * LEFT * 0.7) + self.add(mob) + + +class IntroduceR0(Scene): + def construct(self): + # Infect + pis = VGroup(*[ + PiPerson( + height=0.5, + infection_radius=1.5, + wander_step_size=0, + max_speed=0, + ) + for x in range(5) + ]) + + pis[:4].arrange(RIGHT, buff=2) + pis[:4].to_edge(DOWN, buff=2) + sicky = pis[4] + sicky.move_to(2 * UP) + sicky.set_status("I") + + pis[1].set_status("R") + for anim in pis[1].change_anims: + pis[1].pop_anim(anim) + + count = VGroup( + TextMobject("Infection count: "), + Integer(0) + ) + count.arrange(RIGHT, aligned_edge=DOWN) + count.to_corner(UL) + + self.add(pis) + self.add(count) + self.wait(2) + + for pi in pis[:4]: + point = VectorizedPoint(sicky.get_center()) + self.play( + point.move_to, pi.get_right() + 0.25 * RIGHT, + MaintainPositionRelativeTo(sicky, point), + run_time=0.5, + ) + if pi.status == "S": + count[1].increment_value() + count[1].set_color(WHITE) + pi.set_status("I") + self.play( + Flash( + sicky.get_center(), + color=RED, + line_length=0.3, + flash_radius=0.7, + ), + count[1].set_color, RED, + ) + self.wait() + + # Attach count to sicky + self.play( + point.move_to, 2 * UP, + MaintainPositionRelativeTo(sicky, point), + ) + count_copy = count[1].copy() + self.play( + count_copy.next_to, sicky.body, UR, + count_copy.set_color, WHITE, + ) + self.wait() + + # Zeros + zeros = VGroup() + for pi in pis[:4]: + if pi.status == "I": + zero = Integer(0) + zero.next_to(pi.body, UR) + zeros.add(zero) + + # R label + R_label = TextMobject("Average :=", " $R$") + R_label.set_color_by_tex("R", YELLOW) + R_label.next_to(count, DOWN, buff=1.5) + + arrow = Arrow(R_label[0].get_top(), count.get_bottom()) + + self.play( + LaggedStartMap(FadeInFromDown, zeros), + GrowArrow(arrow), + FadeIn(R_label), + ) + + brace = Brace(R_label[1], DOWN) + name = TextMobject("``Effective reproductive number''") + name.set_color(YELLOW) + name.next_to(brace, DOWN) + name.to_edge(LEFT) + self.play( + GrowFromCenter(brace), + FadeIn(name, 0.5 * UP), + ) + self.wait(5) + + # R0 + brr = TextMobject("``Basic reproductive number''") + brr.set_color(TEAL) + brr.move_to(name, LEFT) + R0 = TexMobject("R_0") + R0.move_to(R_label[1], UL) + R0.set_color(TEAL) + + for pi in pis[:4]: + pi.set_status("S") + + self.play( + FadeOut(R_label[1], UP), + FadeOut(name, UP), + FadeIn(R0, DOWN), + FadeIn(brr, DOWN), + FadeOut(zeros), + FadeOut(count_copy), + brace.match_width, R0, {"stretch": True}, + brace.match_x, R0, + ) + self.wait() + + # Copied from above + count[1].set_value(0) + + for pi in pis[:4]: + point = VectorizedPoint(sicky.get_center()) + self.play( + point.move_to, pi.body.get_right() + 0.25 * RIGHT, + MaintainPositionRelativeTo(sicky, point), + run_time=0.5, + ) + count[1].increment_value() + count[1].set_color(WHITE) + pi.set_status("I") + self.play( + Flash( + sicky.get_center(), + color=RED, + line_length=0.3, + flash_radius=0.7, + ), + count[1].set_color, RED, + ) + self.play( + point.move_to, 2 * UP, + MaintainPositionRelativeTo(sicky, point), + ) + self.wait(4) + + +class HowR0IsCalculatedHere(Scene): + def construct(self): + words = VGroup( + TextMobject("Count ", "\\# transfers"), + TextMobject("for every infectious case") + ) + words.arrange(DOWN) + words[1].set_color(RED) + words.to_edge(UP) + + estimate = TextMobject("Estimate") + estimate.move_to(words[0][0], RIGHT) + + lp, rp = parens = TexMobject("(", ")") + parens.match_height(words) + lp.next_to(words, LEFT, SMALL_BUFF) + rp.next_to(words, RIGHT, SMALL_BUFF) + average = TextMobject("Average") + average.scale(1.5) + average.next_to(parens, LEFT, SMALL_BUFF) + + words.save_state() + words[1].move_to(words[0]) + words[0].set_opacity(0) + + self.add(FullScreenFadeRectangle(fill_color=GREY_E, fill_opacity=1).scale(2)) + self.wait() + self.play(Write(words[1], run_time=1)) + self.wait() + self.add(words) + self.play(Restore(words)) + self.wait() + self.play( + FadeOut(words[0][0], UP), + FadeIn(estimate, DOWN), + ) + self.wait() + + self.play( + Write(parens), + FadeIn(average, 0.5 * RIGHT), + self.camera.frame.shift, LEFT, + ) + self.wait() + + self.embed() + + +class R0Rect(Scene): + def construct(self): + rect = SurroundingRectangle(TextMobject("R0 = 2.20")) + rect.set_stroke(YELLOW, 4) + self.play(ShowCreation(rect)) + self.play(FadeOut(rect)) + + +class DoubleInfectionRadius(Scene): + CONFIG = { + "random_seed": 1, + } + + def construct(self): + c1 = Circle(radius=0.25, color=RED) + c2 = Circle(radius=0.5, color=RED) + arrow = Vector(RIGHT) + c1.next_to(arrow, LEFT) + c2.next_to(arrow, RIGHT) + + title = TextMobject("Double the\\\\infection radius") + title.next_to(VGroup(c1, c2), UP) + + self.add(c1, title) + self.wait() + self.play( + GrowArrow(arrow), + TransformFromCopy(c1, c2), + ) + self.wait() + + c2.label = TextMobject("4x area") + c2.label.scale(0.5) + c2.label.next_to(c2, DOWN) + + for circ, count in (c1, 4), (c2, 16): + dots = VGroup() + for x in range(count): + dot = Dot(color=BLUE) + dot.set_stroke(BLACK, 2, background=True) + dot.set_height(0.05) + vect = rotate_vector(RIGHT, TAU * random.random()) + vect *= 0.9 * random.random() * circ.get_height() / 2 + dot.move_to(circ.get_center() + vect) + dots.add(dot) + circ.dot = dots + anims = [ShowIncreasingSubsets(dots)] + if hasattr(circ, "label"): + anims.append(FadeIn(circ.label, 0.5 * UP)) + self.play(*anims) + self.wait() + + +class PInfectionSlider(Scene): + def construct(self): + slider = ValueSlider( + "Probability of infection", + 0.2, + x_min=0, + x_max=0.2, + numbers_to_show=np.arange(0.05, 0.25, 0.05), + decimal_number_config={ + "num_decimal_places": 2, + }, + tick_frequency=0.05, + ) + self.add(slider) + self.wait() + self.play(slider.get_change_anim(0.1)) + self.wait() + self.play(slider.get_change_anim(0.05)) + self.wait() + + +class R0Categories(Scene): + def construct(self): + # Titles + titles = VGroup( + TexMobject("R > 1", color=RED), + TexMobject("R = 1", color=YELLOW), + TexMobject("R < 1", color=GREEN), + ) + titles.scale(1.25) + titles.arrange(RIGHT, buff=2.7) + titles.to_edge(UP) + + v_lines = VGroup() + for t1, t2 in zip(titles, titles[1:]): + v_line = Line(UP, DOWN) + v_line.set_height(FRAME_HEIGHT) + v_line.set_x(midpoint(t1.get_right(), t2.get_left())[0]) + v_lines.add(v_line) + + self.add(titles) + self.add(v_lines) + + # Names + names = VGroup( + TextMobject("Epidemic", color=RED), + TextMobject("Endemic", color=YELLOW), + TextMobject("...Hypodemic?", color=GREEN), + ) + for name, title in zip(names, titles): + name.next_to(title, DOWN, MED_LARGE_BUFF) + + # Doubling dots + dot = Dot(color=RED) + dot.next_to(names[0], DOWN, LARGE_BUFF) + rows = VGroup(VGroup(dot)) + lines = VGroup() + for x in range(4): + new_row = VGroup() + new_lines = VGroup() + for dot in rows[-1]: + dot.children = [dot.copy(), dot.copy()] + new_row.add(*dot.children) + new_row.arrange(RIGHT) + max_width = 4 + if new_row.get_width() > max_width: + new_row.set_width(max_width) + new_row.next_to(rows[-1], DOWN, LARGE_BUFF) + for dot in rows[-1]: + for child in dot.children: + new_lines.add(Line( + dot.get_center(), + child.get_center(), + buff=0.2, + )) + rows.add(new_row) + lines.add(new_lines) + + for row, line_row in zip(rows[:-1], lines): + self.add(row) + anims = [] + for dot in row: + for child in dot.children: + anims.append(TransformFromCopy(dot, child)) + for line in line_row: + anims.append(ShowCreation(line)) + self.play(*anims) + self.play(FadeIn(names[0], UP)) + self.wait() + + exp_tree = VGroup(rows, lines) + + # Singleton dots + mid_rows = VGroup() + mid_lines = VGroup() + + for row in rows: + dot = Dot(color=RED) + dot.match_y(row) + mid_rows.add(dot) + + for d1, d2 in zip(mid_rows[:-1], mid_rows[1:]): + d1.child = d2 + mid_lines.add(Line( + d1.get_center(), + d2.get_center(), + buff=0.2, + )) + + for dot, line in zip(mid_rows[:-1], mid_lines): + self.add(dot) + self.play( + TransformFromCopy(dot, dot.child), + ShowCreation(line) + ) + self.play(FadeIn(names[1], UP)) + self.wait() + + # Inverted tree + exp_tree_copy = exp_tree.copy() + exp_tree_copy.rotate(PI) + exp_tree_copy.match_x(titles[2]) + + self.play(TransformFromCopy(exp_tree, exp_tree_copy)) + self.play(FadeIn(names[2], UP)) + self.wait() + + +class RealR0Estimates(Scene): + def construct(self): + labels = VGroup( + TextMobject("COVID-19\\\\", "$R_0 \\approx 2$"), + TextMobject("1918 Spanish flu\\\\", "$R_0 \\approx 2$"), + TextMobject("Usual seasonal flu\\\\", "$R_0 \\approx 1.3$"), + ) + images = Group( + ImageMobject("COVID-19_Map"), + ImageMobject("spanish_flu"), + ImageMobject("Influenza"), + ) + for image in images: + image.set_height(3) + + images.arrange(RIGHT, buff=0.5) + images.to_edge(UP, buff=2) + + for label, image in zip(labels, images): + label.next_to(image, DOWN) + + for label, image in zip(labels, images): + self.play( + FadeIn(image, DOWN), + FadeIn(label, UP), + ) + self.wait() + + self.embed() + + +class WhyChooseJustOne(TeacherStudentsScene): + def construct(self): + self.student_says( + "Why choose\\\\just one?", + target_mode="sassy", + added_anims=[self.teacher.change, "thinking"], + run_time=1, + ) + self.play( + self.get_student_changes( + "confused", "confused", "sassy", + ), + ) + self.wait(2) + self.teacher_says( + "For science!", target_mode="hooray", + look_at_arg=self.students[2].eyes, + ) + self.change_student_modes("hesitant", "hesitant", "hesitant") + self.wait(3) + + self.embed() + + +class NickyCaseMention(Scene): + def construct(self): + words = TextMobject( + "Artwork by Nicky Case\\\\", + "...who is awesome." + ) + words.scale(1) + words.to_edge(LEFT) + arrow = Arrow( + words.get_top(), + words.get_top() + 2 * UR + RIGHT, + path_arc=-90 * DEGREES, + ) + self.play( + Write(words[0]), + ShowCreation(arrow), + run_time=1, + ) + self.wait(2) + self.play(Write(words[1]), run_time=1) + self.wait() + + +class PonderingRandy(Scene): + def construct(self): + randy = Randolph() + self.play(FadeIn(randy)) + self.play(randy.change, "pondering") + for x in range(3): + self.play(Blink(randy)) + self.wait(2) + + +class WideSpreadTesting(Scene): + def construct(self): + # Add dots + dots = VGroup(*[ + DotPerson( + height=0.2, + infection_radius=0.6, + max_speed=0, + wander_step_size=0, + p_symptomatic_on_infection=0.8, + ) + for x in range(600) + ]) + dots.arrange_in_grid(20, 30) + dots.set_height(FRAME_HEIGHT - 1) + + self.add(dots) + sick_dots = VGroup() + for x in range(36): + sicky = random.choice(dots) + sicky.set_status("I") + sick_dots.add(sicky) + self.wait(0.1) + self.wait(2) + + healthy_dots = VGroup() + for dot in dots: + if dot.status != "I": + healthy_dots.add(dot) + + # Show Flash + rings = self.get_rings(ORIGIN, FRAME_WIDTH + FRAME_HEIGHT, 0.1) + rings.shift(7 * LEFT) + for i, ring in enumerate(rings): + ring.shift(0.05 * i**1.2 * RIGHT) + + self.play(LaggedStartMap( + FadeIn, rings, + lag_ratio=3 / len(rings), + run_time=2.5, + rate_func=there_and_back, + )) + + # Quarantine + box = Square() + box.set_height(2) + box.to_corner(DL) + box.shift(LEFT) + + anims = [] + points = VGroup() + points_target = VGroup() + for dot in sick_dots: + point = VectorizedPoint(dot.get_center()) + point.generate_target() + points.add(point) + points_target.add(point.target) + + dot.push_anim(MaintainPositionRelativeTo(dot, point, run_time=3)) + anims.append(MoveToTarget(point)) + + points_target.arrange_in_grid() + points_target.set_width(box.get_width() - 1) + points_target.move_to(box) + + self.play( + ShowCreation(box), + LaggedStartMap(MoveToTarget, points, lag_ratio=0.05), + self.camera.frame.shift, LEFT, + run_time=3, + ) + self.wait(9) + + def get_rings(self, center, max_radius, delta_r): + radii = np.arange(0, max_radius, delta_r) + rings = VGroup(*[ + Circle( + radius=r, + stroke_opacity=0.75 * (1 - fdiv(r, max_radius)), + stroke_color=TEAL, + stroke_width=100 * delta_r, + ) + for r in radii + ]) + rings.move_to(center) + return rings + + +class VirusSpreading(Scene): + def construct(self): + virus = SVGMobject(file_name="virus") + virus.lock_triangulation() + virus.set_fill(RED_E, 1) + virus.set_stroke([RED, WHITE], width=0) + height = 3 + virus.set_height(height) + + self.play(DrawBorderThenFill(virus)) + + viruses = VGroup(virus) + + for x in range(8): + height *= 0.8 + anims = [] + new_viruses = VGroup() + for virus in viruses: + children = [virus.copy(), virus.copy()] + for child in children: + child.set_height(height) + child.set_color(interpolate_color( + RED_E, + GREY_D, + 0.7 * random.random(), + )) + child.shift([ + (random.random() - 0.5) * 3, + (random.random() - 0.5) * 3, + 0, + ]) + anims.append(TransformFromCopy(virus, child)) + new_viruses.add(child) + new_viruses.center() + self.remove(viruses) + self.play(*anims, run_time=0.5) + viruses.set_submobjects(list(new_viruses)) + self.wait() + + # Eliminate + for virus in viruses: + virus.generate_target() + virus.target.scale(3) + virus.target.set_color(WHITE) + virus.target.set_opacity(0) + + self.play(LaggedStartMap( + MoveToTarget, viruses, + run_time=8, + lag_ratio=3 / len(viruses) + )) + + +class GraciousPi(Scene): + def construct(self): + morty = Mortimer() + morty.flip() + self.play(FadeIn(morty)) + self.play(morty.change, "hesitant") + self.play(Blink(morty)) + self.wait() + self.play(morty.change, "gracious", morty.get_bottom()) + self.play(Blink(morty)) + self.wait(2) + self.play(Blink(morty)) + self.wait(2) + + +class SIREndScreen(PatreonEndScreen): + CONFIG = { + "specific_patrons": [ + "1stViewMaths", + "Adam Dřínek", + "Aidan Shenkman", + "Alan Stein", + "Alex Mijalis", + "Alexis Olson", + "Ali Yahya", + "Andrew Busey", + "Andrew Cary", + "Andrew R. Whalley", + "Aravind C V", + "Arjun Chakroborty", + "Arthur Zey", + "Ashwin Siddarth", + "Austin Goodman", + "Avi Finkel", + "Awoo", + "Axel Ericsson", + "Ayan Doss", + "AZsorcerer", + "Barry Fam", + "Ben Delo", + "Bernd Sing", + "Boris Veselinovich", + "Bradley Pirtle", + "Brandon Huang", + "Brian Staroselsky", + "Britt Selvitelle", + "Britton Finley", + "Burt Humburg", + "Calvin Lin", + "Charles Southerland", + "Charlie N", + "Chenna Kautilya", + "Chris Connett", + "Christian Kaiser", + "cinterloper", + "Clark Gaebel", + "Colwyn Fritze-Moor", + "Cooper Jones", + "Corey Ogburn", + "D. Sivakumar", + "Dan Herbatschek", + "Daniel Herrera C", + "Dave B", + "Dave Kester", + "dave nicponski", + "David B. Hill", + "David Clark", + "David Gow", + "Delton Ding", + "Dominik Wagner", + "Eddie Landesberg", + "emptymachine", + "Eric Younge", + "Eryq Ouithaqueue", + "Farzaneh Sarafraz", + "Federico Lebron", + "Frank R. Brown, Jr.", + "Giovanni Filippi", + "Goodwine Carlos", + "Hal Hildebrand", + "Hitoshi Yamauchi", + "Ivan Sorokin", + "Jacob Baxter", + "Jacob Harmon", + "Jacob Hartmann", + "Jacob Magnuson", + "Jake Vartuli - Schonberg", + "Jalex Stark", + "Jameel Syed", + "Jason Hise", + "Jayne Gabriele", + "Jean-Manuel Izaret", + "Jeff Linse", + "Jeff Straathof", + "Jimmy Yang", + "John C. Vesey", + "John Camp", + "John Haley", + "John Le", + "John Rizzo", + "John V Wertheim", + "Jonathan Heckerman", + "Jonathan Wilson", + "Joseph John Cox", + "Joseph Kelly", + "Josh Kinnear", + "Joshua Claeys", + "Juan Benet", + "Kai-Siang Ang", + "Kanan Gill", + "Karl Niu", + "Kartik Cating-Subramanian", + "Kaustuv DeBiswas", + "Killian McGuinness", + "Kros Dai", + "L0j1k", + "Lael S Costa", + "LAI Oscar", + "Lambda GPU Workstations", + "Lee Redden", + "Linh Tran", + "lol I totally forgot I had a patreon", + "Luc Ritchie", + "Ludwig Schubert", + "Lukas Biewald", + "Magister Mugit", + "Magnus Dahlström", + "Manoj Rewatkar - RITEK SOLUTIONS", + "Mark B Bahu", + "Mark Heising", + "Mark Mann", + "Martin Price", + "Mathias Jansson", + "Matt Godbolt", + "Matt Langford", + "Matt Roveto", + "Matt Russell", + "Matteo Delabre", + "Matthew Bouchard", + "Matthew Cocke", + "Maxim Nitsche", + "Mia Parent", + "Michael Hardel", + "Michael W White", + "Mirik Gogri", + "Mustafa Mahdi", + "Márton Vaitkus", + "Nate Heckmann", + "Nicholas Cahill", + "Nikita Lesnikov", + "Oleg Leonov", + "Oliver Steele", + "Omar Zrien", + "Owen Campbell-Moore", + "Patrick Lucas", + "Pavel Dubov", + "Petar Veličković", + "Peter Ehrnstrom", + "Peter Mcinerney", + "Pierre Lancien", + "Pradeep Gollakota", + "Quantopian", + "Rafael Bove Barrios", + "Randy C. Will", + "rehmi post", + "Rex Godby", + "Ripta Pasay", + "Rish Kundalia", + "Roman Sergeychik", + "Roobie", + "Ryan Atallah", + "Samuel Judge", + "SansWord Huang", + "Scott Gray", + "Scott Walter, Ph.D.", + "soekul", + "Solara570", + "Steve Huynh", + "Steve Sperandeo", + "Steven Siddals", + "Stevie Metke", + "Still working on an upcoming skeptical humanist SciFi novels- Elux Luc", + "supershabam", + "Suteerth Vishnu", + "Suthen Thomas", + "Tal Einav", + "Taras Bobrovytsky", + "Tauba Auerbach", + "Ted Suzman", + "Thomas J Sargent", + "Thomas Tarler", + "Tianyu Ge", + "Tihan Seale", + "Tyler VanValkenburg", + "Vassili Philippov", + "Veritasium", + "Vignesh Ganapathi Subramanian", + "Vinicius Reis", + "Xuanji Li", + "Yana Chernobilsky", + "Yavor Ivanov", + "YinYangBalance.Asia", + "Yu Jun", + "Yurii Monastyrshyn", + ], + } + + +# For assembling script +# SCENES_IN_ORDER = [ +WILL_BE_SCENES_IN_ORDER = [ + # Quarantining + QuarantineInfectious, + QuarantineInfectiousLarger, + QuarantineInfectiousLarger80p, + QuarantineInfectiousTravel, + QuarantineInfectiousTravel80p, + QuarantineInfectiousTravel50p, + # Social distancing + SimpleSocialDistancing, + DelayedSocialDistancing, # Maybe remove? + DelayedSocialDistancingLargeCity, # Probably don't use + SimpleTravelSocialDistancePlusZeroTravel, + SimpleTravelDelayedSocialDistancing99p, + SimpleTravelDelayedTravelReductionThreshold100TargetHalfPercent, + SimpleTravelDelayedSocialDistancing50pThreshold100, + SimpleTravelDelayedTravelReductionThreshold100, # Maybe don't use + # Describing R0 + LargerCity2, + LargeCityHighInfectionRadius, + LargeCityLowerInfectionRate, + SimpleTravelDelayedSocialDistancing99p, # Need to re-render + # Market + CentralMarketLargePopulation, + CentralMarketLowerInfection, + CentralMarketVeryFrequentLargePopulationDelayedSocialDistancing, + CentralMarketLessFrequent, + CentralMarketTransitionToLowerInfection, + CentralMarketTransitionToLowerInfectionAndLowerFrequency, + CentralMarketQuarantine, + CentralMarketQuarantine80p, +] + + +to_run_later = [ + CentralMarketTransitionToLowerInfectionAndLowerFrequency, + SimpleTravelDelayedTravelReduction, + CentralMarketLargePopulation, + CentralMarketLowerInfection, + CentralMarketVeryFrequentLargePopulationDelayedSocialDistancing, + CentralMarketLessFrequent, + CentralMarketTransitionToLowerInfection, + CentralMarketTransitionToLowerInfectionAndLowerFrequency, + CentralMarketQuarantine, + CentralMarketQuarantine80p, + SecondWave, +] diff --git a/from_3b1b/old/sphere_area.py b/from_3b1b/old/sphere_area.py index 5d3c86a17b..c718209de4 100644 --- a/from_3b1b/old/sphere_area.py +++ b/from_3b1b/old/sphere_area.py @@ -138,7 +138,7 @@ def show_surface_area(self): Write(sphere, stroke_width=1), FadeInFromDown(sa_equation), # ShowCreation(radial_line), - # FadeInFrom(R_label, IN), + # FadeIn(R_label, IN), ) # self.play( # Transform( @@ -459,12 +459,12 @@ def construct(self): self.play( ShowCreation(top_line), - FadeInFrom(two_pi_R, IN) + FadeIn(two_pi_R, IN) ) self.wait() self.play( ShowCreation(side_line), - FadeInFrom(two_R, RIGHT) + FadeIn(two_R, RIGHT) ) self.wait() @@ -772,7 +772,7 @@ def label_sides(self): u_values, v_values = sphere.get_u_values_and_v_values() radius = sphere.radius lat_lines = VGroup(*[ - ParametricFunction( + ParametricCurve( lambda t: radius * sphere.func(u, t), t_min=sphere.v_min, t_max=sphere.v_max, @@ -780,7 +780,7 @@ def label_sides(self): for u in u_values ]) lon_lines = VGroup(*[ - ParametricFunction( + ParametricCurve( lambda t: radius * sphere.func(t, v), t_min=sphere.u_min, t_max=sphere.u_max, @@ -1167,7 +1167,7 @@ def label_R(self): self.add_fixed_orientation_mobjects(R_label) self.play( ShowCreation(R_line), - FadeInFrom(R_label, IN), + FadeIn(R_label, IN), ) self.wait() @@ -1189,7 +1189,7 @@ def label_d(self): self.add_fixed_orientation_mobjects(d_label) self.play( ShowCreation(d_line), - FadeInFrom(d_label, IN), + FadeIn(d_label, IN), ) self.wait() for x in range(self.d_ambiguity_iterations): @@ -1263,7 +1263,7 @@ def show_similar_triangles(self): ) self.add_fixed_in_frame_mobjects(equation, eq_d, eq_R) self.play( - FadeInFrom(equation[0], 7 * RIGHT + 2.5 * DOWN), + FadeIn(equation[0], 7 * RIGHT + 2.5 * DOWN), FadeIn(equation[1:]), FadeInFromDown(eq_d), FadeInFromDown(eq_R), @@ -1751,9 +1751,9 @@ def label_angles(self): ShowCreation(beta_arc), ) self.wait() - self.play(FadeInFrom(alpha_label, UP)) + self.play(FadeIn(alpha_label, UP)) self.wait() - self.play(FadeInFrom(beta_label, LEFT)) + self.play(FadeIn(beta_label, LEFT)) self.wait() self.play(ShowCreation(elbow)) self.wait() @@ -1763,7 +1763,7 @@ def label_angles(self): LaggedStartMap(FadeInFromDown, equation[1:4:2]) ) self.wait() - self.play(FadeInFrom(equation[-2:], LEFT)) + self.play(FadeIn(equation[-2:], LEFT)) self.remove(equation, movers) self.add(equation) self.wait() @@ -1847,13 +1847,13 @@ def label_angles(self): beta_label1.shift(0.01 * LEFT) self.play(FadeOut(words)) - self.play(FadeInFrom(deg90, 0.1 * UP)) + self.play(FadeIn(deg90, 0.1 * UP)) self.wait(0.25) self.play(WiggleOutThenIn(beta_label)) self.wait(0.25) self.play( ShowCreation(alpha_arc1), - FadeInFrom(q_mark, 0.1 * RIGHT) + FadeIn(q_mark, 0.1 * RIGHT) ) self.wait() self.play(ShowPassingFlash( @@ -2291,8 +2291,8 @@ def construct(self): for i in range(n_shapes): anims = [ - FadeInFrom(spheres[i], LEFT), - FadeInFrom(cylinders[i], LEFT), + FadeIn(spheres[i], LEFT), + FadeIn(cylinders[i], LEFT), ] if i > 0: anims += [ @@ -2302,7 +2302,7 @@ def construct(self): self.play(*anims, run_time=1) self.play(GrowFromCenter(all_equals[i])) self.play( - FadeInFrom(q_marks, LEFT), + FadeIn(q_marks, LEFT), Write(final_arrows) ) self.wait() @@ -2699,7 +2699,7 @@ def construct(self): self.play(ShowCreation(cross)) self.play( VGroup(lectures, cross).shift, DOWN, - FadeInFrom(exercises, UP) + FadeIn(exercises, UP) ) self.wait() @@ -3010,7 +3010,7 @@ def show_theta(self): ) self.wait() self.play( - FadeInFrom(brace_label, IN), + FadeIn(brace_label, IN), ) self.play( ShowCreation(radial_line), @@ -3335,7 +3335,7 @@ def construct(self): word.circum = word.get_part_by_tex("circumference") word.remove(word.circum) self.play( - FadeOutAndShift(question, UP), + FadeOut(question, UP), FadeInFromDown(prompt), question.circum.replace, prompt.circum, run_time=1.5 @@ -3394,7 +3394,7 @@ def construct(self): which_one.next_to(brace, DOWN, SMALL_BUFF) self.add(question) - self.play(FadeInFrom(equation)) + self.play(FadeIn(equation)) self.wait() self.play( GrowFromCenter(brace), diff --git a/from_3b1b/old/spirals.py b/from_3b1b/old/spirals.py index ed5b3cb939..32635c24a8 100644 --- a/from_3b1b/old/spirals.py +++ b/from_3b1b/old/spirals.py @@ -247,7 +247,7 @@ def construct(self): ) names.arrange(DOWN, buff=1) for name in names: - self.play(FadeInFrom(name, RIGHT)) + self.play(FadeIn(name, RIGHT)) self.wait() @@ -511,7 +511,7 @@ def show_polar_coordinates(self): self.play(ShowCreation(degree_cross)) self.play( - FadeOutAndShift( + FadeOut( VGroup(degree_label, degree_cross), DOWN ), @@ -647,7 +647,7 @@ def show_all_nn_tuples(self): for n in [5, 6, 7, 8, 9] ])) - spiral = ParametricFunction( + spiral = ParametricCurve( lambda t: self.get_polar_point(t, t), t_min=0, t_max=25, @@ -770,7 +770,7 @@ def get_polar_point(self, r, theta): def get_arc(self, theta, r=1, color=None): if color is None: color = self.theta_color - return ParametricFunction( + return ParametricCurve( lambda t: self.get_polar_point(1 + 0.025 * t, t), t_min=0, t_max=theta, @@ -1074,10 +1074,10 @@ def construct(self): ), self.teacher.change, "maybe", numbers, ShowCreation(arrow), - FadeInFrom(numbers, RIGHT) + FadeIn(numbers, RIGHT) ) self.play( - FadeInFrom(primes, LEFT), + FadeIn(primes, LEFT), ) self.play( LaggedStartMap(FadeInFromDown, q_marks[0]), @@ -1210,7 +1210,7 @@ def construct(self): text.to_corner(UL) self.play(Write(text)) self.wait(3) - self.play(FadeOutAndShift(text, DOWN)) + self.play(FadeOut(text, DOWN)) class DirichletComingUp(Scene): @@ -1228,8 +1228,8 @@ def construct(self): Group(words, image).center() self.play( - FadeInFrom(image, RIGHT), - FadeInFrom(words, LEFT), + FadeIn(image, RIGHT), + FadeIn(words, LEFT), ) self.wait() @@ -1744,7 +1744,7 @@ def arc_func(self, t): def get_arc(self, n): if n == 0: return VectorizedPoint() - return ParametricFunction( + return ParametricCurve( self.arc_func, t_min=0, t_max=n, @@ -1781,7 +1781,7 @@ def add_title(self): underline.set_color(BLUE) self.play( ReplacementTransform(pre_title[0], title[1]), - FadeInFrom(title[0], RIGHT), + FadeIn(title[0], RIGHT), GrowFromCenter(underline) ) self.play( @@ -1961,7 +1961,7 @@ def simple_english(self): self.play( FadeOut(new_phrase), FadeIn(terminology), - FadeOutAndShift(randy, DOWN) + FadeOut(randy, DOWN) ) self.wait() self.wait(6) @@ -2146,7 +2146,7 @@ def show_top_right_arithmetic(self): self.play(MoveToTarget(ff)) top_line.add(ff) - self.play(FadeInFrom(radians, LEFT)) + self.play(FadeIn(radians, LEFT)) self.wait() self.add(rect, top_line, unit_conversion) self.play( @@ -2560,7 +2560,7 @@ def add_title(self): MoveToTarget(words), FadeOut(self.teacher.bubble), LaggedStart(*[ - FadeOutAndShift(pi, 4 * DOWN) + FadeOut(pi, 4 * DOWN) for pi in self.pi_creatures ]), ShowCreation(underline) @@ -2607,7 +2607,7 @@ def eliminate_non_coprimes(self): # Show coprimes self.play( ShowIncreasingSubsets(numbers, run_time=3), - FadeInFrom(words, LEFT) + FadeIn(words, LEFT) ) self.wait() for group in evens, div11: @@ -2728,7 +2728,7 @@ def eliminate_non_coprimes(self): self.play( GrowFromCenter(brace), - FadeInFrom(etf, UP) + FadeIn(etf, UP) ) self.wait() self.play( @@ -2870,7 +2870,7 @@ def get_line(row): self.play(ShowCreation(v_line)) self.wait() - self.play(FadeInFrom(approx, DOWN)) + self.play(FadeIn(approx, DOWN)) self.wait() self.play(FadeIn(residue_classes)) self.wait() @@ -2967,7 +2967,7 @@ def update_labeled_box(mob): FadeIn(fade_rect), FadeOut(labels), FadeInFromLarge(box_710), - FadeInFrom(label_710, DOWN), + FadeIn(label_710, DOWN), ShowCreation(arrow), ) self.wait() @@ -3018,7 +3018,7 @@ def show_arithmetic(self): FadeIn(equation[1:3]), ) self.play( - FadeInFrom(equation[3:], LEFT) + FadeIn(equation[3:], LEFT) ) self.play(GrowFromCenter(brace)) self.play( @@ -3204,8 +3204,8 @@ def construct(self): eq.set_stroke(BLACK, 8, background=True) self.play(LaggedStart( - FadeInFrom(eqs[0], DOWN), - FadeInFrom(eqs[1], UP), + FadeIn(eqs[0], DOWN), + FadeIn(eqs[1], UP), )) self.play(MoveToTarget(eqs)) self.wait() @@ -3376,12 +3376,12 @@ def construct(self): self.add(equation) self.play( - FadeInFrom(n_label, UP), + FadeIn(n_label, UP), ShowCreation(n_arrow), ) self.wait() self.play( - FadeInFrom(r_label, DOWN), + FadeIn(r_label, DOWN), ShowCreation(r_arrow), ) self.wait() @@ -3564,14 +3564,14 @@ def construct(self): # Introduce everything self.play(LaggedStart(*[ - FadeInFrom(label, UP) + FadeIn(label, UP) for label in labels ])) self.wait() self.play( LaggedStart(*[ LaggedStart(*[ - FadeInFrom(item, LEFT) + FadeIn(item, LEFT) for item in sequence ]) for sequence in sequences @@ -3895,8 +3895,8 @@ def construct(self): Write(rp), Write(eq), ) - self.play(FadeInFrom(fourth, LEFT)) - self.play(FadeInFrom(lim, RIGHT)) + self.play(FadeIn(fourth, LEFT)) + self.play(FadeIn(lim, RIGHT)) self.play( ChangeDecimalToValue( x_example[1], int(1e7), @@ -3920,7 +3920,7 @@ def construct(self): 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.play(FadeIn(num, UP)) self.wait() @@ -3997,7 +3997,7 @@ def construct(self): image = ImageMobject("Dirichlet") image.set_height(3) image.next_to(d_label, LEFT) - self.play(FadeInFrom(image, RIGHT)) + self.play(FadeIn(image, RIGHT)) self.wait() # Flash @@ -4117,12 +4117,12 @@ def construct(self): self.play(ShowCreationThenFadeAround(mob)) self.wait() self.play( - FadeInFrom(r, DOWN), - FadeOutAndShift(one, UP), + FadeIn(r, DOWN), + FadeOut(one, UP), ) self.play( - FadeInFrom(N, DOWN), - FadeOutAndShift(ten, UP), + FadeIn(N, DOWN), + FadeOut(ten, UP), ) self.wait() self.play( @@ -4143,13 +4143,13 @@ def construct(self): ShowCreationThenFadeAround(fourth), ) self.play( - FadeInFrom(one_over_phi_N[2:], LEFT), - FadeOutAndShift(four, RIGHT), + FadeIn(one_over_phi_N[2:], LEFT), + FadeOut(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) + FadeIn(phi_N_label, DOWN) ) self.wait() @@ -4570,7 +4570,7 @@ def define_important(self): Restore(title[0]), GrowFromCenter(title[1]), FadeIn(arrow_words), - FadeInFrom(title[2], LEFT), + FadeIn(title[2], LEFT), LaggedStartMap( ShowCreation, sd.edges, run_time=3, diff --git a/from_3b1b/old/tau_poem.py b/from_3b1b/old/tau_poem.py index c0008df7da..6dacab07ac 100644 --- a/from_3b1b/old/tau_poem.py +++ b/from_3b1b/old/tau_poem.py @@ -379,7 +379,7 @@ def sine_curve(t): result *= interval_size result += axes_center return result - sine = ParametricFunction(sine_curve) + sine = ParametricCurve(sine_curve) sine_period = Line( axes_center, axes_center + 2*np.pi*interval_size*RIGHT diff --git a/from_3b1b/old/three_dimensions.py b/from_3b1b/old/three_dimensions.py index 453657ad35..da17fdd44f 100644 --- a/from_3b1b/old/three_dimensions.py +++ b/from_3b1b/old/three_dimensions.py @@ -11,7 +11,7 @@ class Stars(Mobject1D): "radius" : FRAME_X_RADIUS, "num_points" : 1000, } - def generate_points(self): + def init_points(self): radii, phis, thetas = [ scalar*np.random.random(self.num_points) for scalar in [self.radius, np.pi, 2*np.pi] @@ -26,7 +26,7 @@ def generate_points(self): ]) class CubeWithFaces(Mobject2D): - def generate_points(self): + def init_points(self): self.add_points([ sgn * np.array(coords) for x in np.arange(-1, 1, self.epsilon) @@ -41,7 +41,7 @@ def unit_normal(self, coords): return np.array([1 if abs(x) == 1 else 0 for x in coords]) class Cube(Mobject1D): - def generate_points(self): + def init_points(self): self.add_points([ ([a, b, c][p[0]], [a, b, c][p[1]], [a, b, c][p[2]]) for p in [(0, 1, 2), (2, 0, 1), (1, 2, 0)] @@ -51,7 +51,7 @@ def generate_points(self): self.set_color(YELLOW) class Octohedron(Mobject1D): - def generate_points(self): + def init_points(self): x = np.array([1, 0, 0]) y = np.array([0, 1, 0]) z = np.array([0, 0, 1]) @@ -68,7 +68,7 @@ def generate_points(self): self.set_color(MAROON_D) class Dodecahedron(Mobject1D): - def generate_points(self): + def init_points(self): phi = (1 + np.sqrt(5)) / 2 x = np.array([1, 0, 0]) y = np.array([0, 1, 0]) @@ -95,7 +95,7 @@ def generate_points(self): self.set_color(GREEN) class Sphere(Mobject2D): - def generate_points(self): + def init_points(self): self.add_points([ ( np.sin(phi) * np.cos(theta), diff --git a/from_3b1b/old/triangle_of_power/triangle.py b/from_3b1b/old/triangle_of_power/triangle.py index ee718d62bf..1940b05d2f 100644 --- a/from_3b1b/old/triangle_of_power/triangle.py +++ b/from_3b1b/old/triangle_of_power/triangle.py @@ -72,7 +72,7 @@ def __init__(self, x = None, y = None, z = None, **kwargs): digest_config(self, kwargs, locals()) VMobject.__init__(self, **kwargs) - def generate_points(self): + def init_points(self): vertices = [ self.radius*rotate_vector(RIGHT, 7*np.pi/6 - i*2*np.pi/3) for i in range(3) diff --git a/from_3b1b/old/turbulence.py b/from_3b1b/old/turbulence.py index c3765f44ed..b0657a997d 100644 --- a/from_3b1b/old/turbulence.py +++ b/from_3b1b/old/turbulence.py @@ -78,7 +78,7 @@ def get_lines(self): ]) def get_line(self, r): - return ParametricFunction( + return ParametricCurve( lambda t: r * (t + 1)**(-1) * np.array([ np.cos(TAU * t), np.sin(TAU * t), @@ -969,8 +969,8 @@ def construct(self): Group(feynman, name, quote).center() self.play( - FadeInFrom(feynman, UP), - FadeInFrom(name, DOWN), + FadeIn(feynman, UP), + FadeIn(name, DOWN), Write(quote, run_time=4) ) self.wait() @@ -1292,7 +1292,7 @@ def construct(self): ) self.wait() for item in left_items: - self.play(FadeInFrom(item)) + self.play(FadeIn(item)) self.wait() @@ -1378,7 +1378,7 @@ def construct(self): poem.next_to(picture, RIGHT, LARGE_BUFF) self.add(picture) - self.play(FadeInFrom(title, DOWN)) + self.play(FadeIn(title, DOWN)) self.wait() for word in poem: if "whirl" in word.get_tex_string(): @@ -1415,7 +1415,7 @@ def construct(self): self.play(*map(ShowCreation, swirl)) self.play( GrowFromCenter(h_line), - FadeInFrom(D_label, UP), + FadeIn(D_label, UP), ) self.wait() @@ -1540,7 +1540,7 @@ def construct(self): attribution.to_edge(DOWN) self.play(Write(title)) - self.play(FadeInFrom(attribution, UP)) + self.play(FadeIn(attribution, UP)) self.wait() @@ -1594,7 +1594,7 @@ def construct(self): def get_cylinder_circles(self, radius, radius_var, max_z): return VGroup(*[ - ParametricFunction( + ParametricCurve( lambda t: np.array([ np.cos(TAU * t) * r, np.sin(TAU * t) * r, @@ -1610,7 +1610,7 @@ def get_torus_circles(self, out_r, in_r, in_r_var): result = VGroup() for u in sorted(np.random.random(self.n_circles)): r = in_r + in_r_var * random.random() - circle = ParametricFunction( + circle = ParametricCurve( lambda t: r * np.array([ np.cos(TAU * t), np.sin(TAU * t), diff --git a/from_3b1b/old/uncertainty.py b/from_3b1b/old/uncertainty.py index f707a3f06a..c01c832b5c 100644 --- a/from_3b1b/old/uncertainty.py +++ b/from_3b1b/old/uncertainty.py @@ -3426,7 +3426,7 @@ def get_spring(alpha, height = 2): t_max = 6.5 r = self.spring_radius s = (height - r)/(t_max**2) - spring = ParametricFunction( + spring = ParametricCurve( lambda t : op.add( r*(np.sin(TAU*t)*RIGHT+np.cos(TAU*t)*UP), s*((t_max - t)**2)*DOWN, diff --git a/from_3b1b/old/very_old_three_dimensions.py b/from_3b1b/old/very_old_three_dimensions.py new file mode 100644 index 0000000000..da17fdd44f --- /dev/null +++ b/from_3b1b/old/very_old_three_dimensions.py @@ -0,0 +1,113 @@ +import numpy as np +import itertools as it + +from mobject.mobject import Mobject, Mobject1D, Mobject2D, Mobject +from geometry import Line +from constants import * + +class Stars(Mobject1D): + CONFIG = { + "stroke_width" : 1, + "radius" : FRAME_X_RADIUS, + "num_points" : 1000, + } + def init_points(self): + radii, phis, thetas = [ + scalar*np.random.random(self.num_points) + for scalar in [self.radius, np.pi, 2*np.pi] + ] + self.add_points([ + ( + r * np.sin(phi)*np.cos(theta), + r * np.sin(phi)*np.sin(theta), + r * np.cos(phi) + ) + for r, phi, theta in zip(radii, phis, thetas) + ]) + +class CubeWithFaces(Mobject2D): + def init_points(self): + self.add_points([ + sgn * np.array(coords) + for x in np.arange(-1, 1, self.epsilon) + for y in np.arange(x, 1, self.epsilon) + for coords in it.permutations([x, y, 1]) + for sgn in [-1, 1] + ]) + self.pose_at_angle() + self.set_color(BLUE) + + def unit_normal(self, coords): + return np.array([1 if abs(x) == 1 else 0 for x in coords]) + +class Cube(Mobject1D): + def init_points(self): + self.add_points([ + ([a, b, c][p[0]], [a, b, c][p[1]], [a, b, c][p[2]]) + for p in [(0, 1, 2), (2, 0, 1), (1, 2, 0)] + for a, b, c in it.product([-1, 1], [-1, 1], np.arange(-1, 1, self.epsilon)) + ]) + self.pose_at_angle() + self.set_color(YELLOW) + +class Octohedron(Mobject1D): + def init_points(self): + x = np.array([1, 0, 0]) + y = np.array([0, 1, 0]) + z = np.array([0, 0, 1]) + vertex_pairs = [(x+y, x-y), (x+y,-x+y), (-x-y,-x+y), (-x-y,x-y)] + vertex_pairs += [ + (b[0]*x+b[1]*y, b[2]*np.sqrt(2)*z) + for b in it.product(*[(-1, 1)]*3) + ] + for pair in vertex_pairs: + self.add_points( + Line(pair[0], pair[1], density = 1/self.epsilon).points + ) + self.pose_at_angle() + self.set_color(MAROON_D) + +class Dodecahedron(Mobject1D): + def init_points(self): + phi = (1 + np.sqrt(5)) / 2 + x = np.array([1, 0, 0]) + y = np.array([0, 1, 0]) + z = np.array([0, 0, 1]) + v1, v2 = (phi, 1/phi, 0), (phi, -1/phi, 0) + vertex_pairs = [ + (v1, v2), + (x+y+z, v1), + (x+y-z, v1), + (x-y+z, v2), + (x-y-z, v2), + ] + five_lines_points = Mobject(*[ + Line(pair[0], pair[1], density = 1.0/self.epsilon) + for pair in vertex_pairs + ]).points + #Rotate those 5 edges into all 30. + for i in range(3): + perm = [j%3 for j in range(i, i+3)] + for b in [-1, 1]: + matrix = b*np.array([x[perm], y[perm], z[perm]]) + self.add_points(np.dot(five_lines_points, matrix)) + self.pose_at_angle() + self.set_color(GREEN) + +class Sphere(Mobject2D): + def init_points(self): + self.add_points([ + ( + np.sin(phi) * np.cos(theta), + np.sin(phi) * np.sin(theta), + np.cos(phi) + ) + for phi in np.arange(self.epsilon, np.pi, self.epsilon) + for theta in np.arange(0, 2 * np.pi, 2 * self.epsilon / np.sin(phi)) + ]) + self.set_color(BLUE) + + def unit_normal(self, coords): + return np.array(coords) / get_norm(coords) + + \ No newline at end of file diff --git a/from_3b1b/old/wallis.py b/from_3b1b/old/wallis.py index 890e0c30c0..cbd08f195d 100644 --- a/from_3b1b/old/wallis.py +++ b/from_3b1b/old/wallis.py @@ -2061,7 +2061,7 @@ def introduce_observer(self): ] self.play( - FadeInFrom(observer, direction=-vect), + FadeIn(observer, direction=-vect), GrowArrow(arrow) ) self.play(Write(full_name)) @@ -4181,7 +4181,7 @@ def construct(self): ) self.play( GrowArrow(result_limit_arrow), - FadeInFrom(result_limit, direction=UP), + FadeIn(result_limit, direction=UP), morty.change, "confused", ) self.wait(2) @@ -4447,7 +4447,7 @@ def construct(self): for product in products: self.play( GrowArrow(product.arrow), - FadeInFrom(product.limit, direction=LEFT) + FadeIn(product.limit, direction=LEFT) ) self.wait() self.play( @@ -4518,7 +4518,7 @@ def construct(self): ) self.play( GrowArrow(new_arrow), - FadeInFrom(new_limit, LEFT), + FadeIn(new_limit, LEFT), bottom_product.parts[3:].fade, 1, ) self.play(FadeIn(randy)) diff --git a/from_3b1b/old/wcat.py b/from_3b1b/old/wcat.py index fd82bc0419..37fc0a6b17 100644 --- a/from_3b1b/old/wcat.py +++ b/from_3b1b/old/wcat.py @@ -1280,7 +1280,7 @@ def construct(self): self.loop = line dots = VGroup(*[ - Dot(line.get_critical_point(vect)) + Dot(line.get_bounding_box_point(vect)) for vect in (LEFT, RIGHT) ]) dots.set_color(BLUE) diff --git a/from_3b1b/old/windmill.py b/from_3b1b/old/windmill.py index 06cff94597..65eebd9b72 100644 --- a/from_3b1b/old/windmill.py +++ b/from_3b1b/old/windmill.py @@ -155,7 +155,7 @@ def isolate_usa(self): self.play( LaggedStart( *[ - FadeOutAndShift(flag, DOWN) + FadeOut(flag, DOWN) for flag in random_flags ], lag_ratio=0.05, @@ -299,7 +299,7 @@ def construct(self): class FootnoteToIMOIntro(Scene): def construct(self): words = TextMobject("$^*$Based on data from 2019 test") - self.play(FadeInFrom(words, UP)) + self.play(FadeIn(words, UP)) self.wait() @@ -361,20 +361,20 @@ def introduce_test(self): # Introduce test self.play( LaggedStart( - FadeInFrom(test[0], 2 * RIGHT), - FadeInFrom(test[1], 2 * LEFT), + FadeIn(test[0], 2 * RIGHT), + FadeIn(test[1], 2 * LEFT), lag_ratio=0.3, ) ) self.wait() self.play( MoveToTarget(test, lag_ratio=0.2), - FadeInFrom(day_labels, UP, lag_ratio=0.2), + FadeIn(day_labels, UP, lag_ratio=0.2), ) self.wait() self.play( *map(Restore, day_labels), - FadeInFrom(hour_labels, LEFT), + FadeIn(hour_labels, LEFT), ) self.wait() @@ -387,7 +387,7 @@ def introduce_test(self): ) self.play( LaggedStart(*[ - FadeInFrom(word, LEFT) + FadeIn(word, LEFT) for word in proof_words ]), LaggedStart(*[ @@ -746,7 +746,7 @@ def add_title(self): group.shift(LEFT) self.add(group) - self.play(FadeInFrom(year, RIGHT)) + self.play(FadeIn(year, RIGHT)) self.title = group @@ -865,7 +865,7 @@ def comment_on_primality(self): self.play( randy.change, "thinking", LaggedStart(*[ - FadeInFrom(word, UP) + FadeIn(word, UP) for word in words ], run_time=3, lag_ratio=0.5) ) @@ -922,7 +922,7 @@ def show_top_three_scorers(self): self.wait() self.play( LaggedStart(*[ - FadeInFrom(row, UP) + FadeIn(row, UP) for row in grid.rows[2:4] ]), LaggedStart(*[ @@ -940,8 +940,8 @@ def show_top_three_scorers(self): self.wait() student_counter.clear_updaters() self.play( - FadeOutAndShift(self.title, UP), - FadeOutAndShift(student_counter, UP), + FadeOut(self.title, UP), + FadeOut(student_counter, UP), grid.rows[:4].shift, 3 * UP, grid.h_lines[:3].shift, 3 * UP, ) @@ -1072,7 +1072,7 @@ def ask_about_questions(self): ) self.wait() self.play( - FadeInFrom(research, DOWN), + FadeIn(research, DOWN), question.shift, 2 * UP, ) self.wait() @@ -1474,8 +1474,8 @@ def add_points(self): self.play( FadeIn(S_eq), - FadeInFrom(braces[0], RIGHT), - FadeInFrom(braces[1], LEFT), + FadeIn(braces[0], RIGHT), + FadeIn(braces[1], LEFT), ) self.play( LaggedStartMap(FadeInFromLarge, dots) @@ -1506,7 +1506,7 @@ def exclude_colinear(self): self.add(line, dots) self.play( ShowCreation(line), - FadeInFrom(words, LEFT), + FadeIn(words, LEFT), dots[-1].set_color, RED, ) self.wait() @@ -1515,7 +1515,7 @@ def exclude_colinear(self): FadeOut(words), ) self.play( - FadeOutAndShift( + FadeOut( dots[-1], 3 * RIGHT, path_arc=-PI / 4, rate_func=running_start, @@ -1557,7 +1557,7 @@ def add_line(self): self.add(windmill, dots) self.play( GrowFromCenter(windmill), - FadeInFrom(l_label, DL), + FadeIn(l_label, DL), ) self.wait() self.play( @@ -1606,7 +1606,7 @@ def switch_pivots(self): self.rotate_to_next_pivot(windmill) self.play( - FadeInFrom(q_label, LEFT), + FadeIn(q_label, LEFT), FadeOut(p_label), FadeOut(arcs), ) @@ -1635,8 +1635,8 @@ def continue_and_count(self): buff=SMALL_BUFF, ) - self.play(FadeInFrom(p_label, UL)) - self.play(FadeInFrom(l_label, LEFT)) + self.play(FadeIn(p_label, UL)) + self.play(FadeIn(l_label, LEFT)) self.wait() self.add( @@ -1845,7 +1845,7 @@ def construct(self): ) self.wait() self.play( - FadeInFrom(words, UP), + FadeIn(words, UP), self.get_student_changes(*3 * ["horrified"]), ) self.wait(3) @@ -1895,7 +1895,7 @@ def construct(self): self.play( FadeIn(harder_words), GrowArrow(arrow), - LaggedStart(*[FadeInFrom(p, UP) for p in p_labels[:3]]), + LaggedStart(*[FadeIn(p, UP) for p in p_labels[:3]]), LaggedStartMap(ShowCreation, rects[:3]), ) self.wait() @@ -1920,16 +1920,16 @@ def construct(self): Transform(big_rect, big_rects[1]), FadeOut(p_labels[0::3]), FadeIn(p_labels[1::3]), - FadeOutAndShift(p_words[0::3], DOWN), - FadeInFrom(p_words[1::3], UP), + FadeOut(p_words[0::3], DOWN), + FadeIn(p_words[1::3], UP), ) self.wait() self.play( Transform(big_rect, big_rects[2]), FadeOut(p_labels[1::3]), FadeIn(p_labels[2::3]), - FadeOutAndShift(p_words[1::3], DOWN), - FadeInFrom(p_words[2::3], UP), + FadeOut(p_words[1::3], DOWN), + FadeIn(p_words[2::3], UP), ) self.wait() @@ -2136,14 +2136,14 @@ def construct(self): self.play( Write(title), - LaggedStart(*[FadeInFrom(row, UP) for row in grid.rows]), + LaggedStart(*[FadeIn(row, UP) for row in grid.rows]), LaggedStart(*[ShowCreation(line) for line in grid.h_lines]), ) self.play(ShowCreation(six_rect)) self.wait() self.play( ReplacementTransform(six_rect, two_rect), - FadeInFrom(subtitle, UP) + FadeIn(subtitle, UP) ) self.wait() @@ -2215,7 +2215,7 @@ def add_fourth_point(self): # Shift point self.play( dot.next_to, words, DOWN, - FadeInFrom(words, RIGHT), + FadeIn(words, RIGHT), ) windmill.point_set[3] = dot.get_center() self.let_windmill_run(windmill, 4) @@ -2239,7 +2239,7 @@ def move_starting_line(self): counters = self.get_pivot_counters(windmill) self.play( LaggedStart(*[ - FadeInFrom(counter, DOWN) + FadeIn(counter, DOWN) for counter in counters ]) ) @@ -2329,14 +2329,14 @@ def show_stays_in_middle(self): self.play( ShowCreation(windmill), GrowFromCenter(pivot_dot), - FadeInFrom(start_words, LEFT), + FadeIn(start_words, LEFT), ) self.wait() self.start_leaving_shadows() self.add(windmill, dots, pivot_dot) half_time = PI / windmill.rot_speed self.let_windmill_run(windmill, time=half_time) - self.play(FadeInFrom(end_words, UP)) + self.play(FadeIn(end_words, UP)) self.wait() self.let_windmill_run(windmill, time=half_time) self.wait() @@ -2388,7 +2388,7 @@ def ask_about_proof(self): Write(middle_words), ) self.wait() - self.play(FadeInFrom(proof_words2, UP)) + self.play(FadeIn(proof_words2, UP)) self.wait() self.let_windmill_run(self.windmill, time=10) @@ -2472,7 +2472,7 @@ def problem_solving_tip(self): self.wait() for arrow, step in zip(arrows, steps[1:]): self.play( - FadeInFrom(step, UP), + FadeIn(step, UP), GrowArrow(arrow), ) self.wait() @@ -2615,7 +2615,7 @@ def mention_odd_case(self): dot_rect.match_color(dot) dot_rects.add(dot_rect) - self.play(FadeInFrom(words, DOWN)) + self.play(FadeIn(words, DOWN)) self.wait() self.play( @@ -2623,7 +2623,7 @@ def mention_odd_case(self): self.pivot_dot.set_color, WHITE, ) - self.play(FadeInFrom(example, UP)) + self.play(FadeIn(example, UP)) self.play( ShowIncreasingSubsets(dot_rects), ChangingDecimal( @@ -2789,12 +2789,12 @@ def show_above_and_below(self): self.add(top_half, tips) self.play( ShowCreationThenFadeOut(top_half), - FadeInFrom(top_words, -vect), + FadeIn(top_words, -vect), ) self.add(low_half, tips) self.play( ShowCreationThenFadeOut(low_half), - FadeInFrom(low_words, vect), + FadeIn(low_words, vect), ) self.wait() @@ -2851,7 +2851,7 @@ def change_pivot(self): blue_rect.move_to, windmill.pivot, blue_rect.set_color, GREY_BROWN, old_pivot_word.move_to, new_pivot_word, - FadeOutAndShift(new_pivot_word, DL) + FadeOut(new_pivot_word, DL) ) self.let_windmill_run(windmill, 1) self.wait() @@ -3117,11 +3117,11 @@ def ask_about_even_number(self): groups.next_to(counter_group, DOWN, aligned_edge=LEFT) self.play( - FadeInFrom(blues_group, UP), + FadeIn(blues_group, UP), ShowCreation(blue_rects), ) self.play( - FadeInFrom(browns_group, UP), + FadeIn(browns_group, UP), ShowCreation(brown_rects), ) self.wait() @@ -3133,7 +3133,7 @@ def ask_about_even_number(self): pivot_words.next_to(arrow, RIGHT, SMALL_BUFF) self.play( - FadeInFrom(pivot_words, LEFT), + FadeIn(pivot_words, LEFT), ShowCreation(arrow), ) self.play( @@ -3191,7 +3191,7 @@ def update_pivot(w): pivot_tracker.move_to, points[n // 2], run_time=2 ) - self.play(FadeInFrom(p_label, LEFT)) + self.play(FadeIn(p_label, LEFT)) self.wait() windmill.remove_updater(update_pivot) @@ -3232,7 +3232,7 @@ def construct(self): self.change_all_student_modes("pondering") self.wait() for item in items: - self.play(FadeInFrom(item, LEFT)) + self.play(FadeIn(item, LEFT)) item.big = item.copy() item.small = item.copy() item.big.scale(1.5, about_edge=LEFT) @@ -3282,7 +3282,7 @@ def construct(self): self.wait() self.play(morty.change, "thinking") self.play( - FadeInFrom(fool_word, LEFT), + FadeIn(fool_word, LEFT), ShowCreation(fool_arrow), ) self.wait() @@ -3510,7 +3510,7 @@ def construct(self): considerations.to_edge(LEFT) self.play(LaggedStart(*[ - FadeInFrom(mob, UP) + FadeIn(mob, UP) for mob in considerations ], run_time=3, lag_ratio=0.2)) @@ -3539,7 +3539,7 @@ def construct(self): self.play(FadeInFromDown(title1)) self.wait() self.play( - FadeOutAndShift(title1, UP), + FadeOut(title1, UP), FadeInFromDown(title2), ) self.wait() @@ -3580,10 +3580,10 @@ def construct(self): FadeOut(labels[0::2]), ) self.play( - FadeInFrom(equation, RIGHT), + FadeIn(equation, RIGHT), GrowArrow(arrow), ) - self.play(FadeInFrom(equation_text, UP)) + self.play(FadeIn(equation_text, UP)) self.wait() @@ -3667,7 +3667,7 @@ def construct(self): ) self.wait() self.play( - FadeInFrom(tiny_tao, LEFT) + FadeIn(tiny_tao, LEFT) ) self.wait() self.play(FadeOut(tiny_tao)) @@ -3728,8 +3728,8 @@ def construct(self): self.wait() self.play( LaggedStart( - FadeInFrom(paths[1], RIGHT), - FadeInFrom(paths[2], RIGHT), + FadeIn(paths[1], RIGHT), + FadeIn(paths[2], RIGHT), lag_ratio=0.2, run_time=3, ) @@ -3761,7 +3761,7 @@ def construct(self): randy.change, "pondering", ) self.play( - FadeInFrom(you, LEFT), + FadeIn(you, LEFT), GrowArrow(arrow) ) self.wait(2) @@ -4092,11 +4092,11 @@ def construct(self): for dot in sorted_dots[7:] ]), LaggedStart(*[ - FadeOutAndShift(word, RIGHT) + FadeOut(word, RIGHT) for word in words ]), LaggedStart(*[ - FadeOutAndShift(word, LEFT) + FadeOut(word, LEFT) for word in words2 ]), LaggedStartMap( diff --git a/from_3b1b/old/zeta.py b/from_3b1b/old/zeta.py index 5bf88bed11..8aa21c71b3 100644 --- a/from_3b1b/old/zeta.py +++ b/from_3b1b/old/zeta.py @@ -1645,17 +1645,17 @@ def plug_in_specific_values(self): ]) arrows = VGroup() VGroup(*[ - ParametricFunction( + ParametricCurve( lambda t : self.z_to_point(z**(1.1+0.8*t)) ) for z in inputs ]) for z, dot in zip(inputs, input_dots): - path = ParametricFunction( + path = ParametricCurve( lambda t : self.z_to_point(z**(1+t)) ) dot.path = path - arrow = ParametricFunction( + arrow = ParametricCurve( lambda t : self.z_to_point(z**(1.1+0.8*t)) ) stand_in_arrow = Arrow( @@ -2897,7 +2897,7 @@ def func(t): result += 3*np.cos(2*2*np.pi*t)*UP return result - self.wandering_path = ParametricFunction(func) + self.wandering_path = ParametricCurve(func) for i, dot in enumerate(self.dots): dot.target = dot.copy() q_mark = TexMobject("?") @@ -2948,7 +2948,7 @@ def func(t): z = zeta(complex(0.5, t)) return z.real*RIGHT + z.imag*UP full_line = VGroup(*[ - ParametricFunction(func, t_min = t0, t_max = t0+1) + ParametricCurve(func, t_min = t0, t_max = t0+1) for t0 in range(100) ]) full_line.set_color_by_gradient( diff --git a/from_3b1b/on_hold/eop/chapter1/brick_row_scene.py b/from_3b1b/on_hold/eop/chapter1/brick_row_scene.py index 5f1efc8313..758fd57e53 100644 --- a/from_3b1b/on_hold/eop/chapter1/brick_row_scene.py +++ b/from_3b1b/on_hold/eop/chapter1/brick_row_scene.py @@ -141,7 +141,7 @@ def merge_rects_by_subdiv(self, row): half_merged_row = row.copy() half_merged_row.subdiv_level += 1 - half_merged_row.generate_points() + half_merged_row.init_points() half_merged_row.move_to(row) self.play(FadeIn(half_merged_row)) @@ -191,7 +191,7 @@ def merge_rects_by_coloring(self, row): merged_row = row.copy() merged_row.coloring_level += 1 - merged_row.generate_points() + merged_row.init_points() merged_row.move_to(row) self.play(FadeIn(merged_row)) diff --git a/from_3b1b/on_hold/eop/pascal.py b/from_3b1b/on_hold/eop/pascal.py index 4ac2a1e5d9..085f2a9d68 100644 --- a/from_3b1b/on_hold/eop/pascal.py +++ b/from_3b1b/on_hold/eop/pascal.py @@ -69,7 +69,7 @@ def __init__(self,mobject, duplicate_row = None, **kwargs): new_pt = mobject.copy() new_pt.nrows += 1 - new_pt.generate_points() + new_pt.init_points() # align with original (copy got centered on screen) c1 = new_pt.coords_to_mobs[0][0].get_center() c2 = mobject.coords_to_mobs[0][0].get_center() @@ -117,7 +117,7 @@ def build_new_pascal_row(self,old_pt): new_pt.height = new_pt.nrows * cell_height new_pt.width = new_pt.nrows * cell_width - new_pt.generate_points() + new_pt.init_points() # align with original (copy got centered on screen) c1 = new_pt.coords_to_mobs[0][0].get_center() c2 = old_pt.coords_to_mobs[0][0].get_center() diff --git a/from_3b1b/on_hold/eop/reusables/brick_row.py b/from_3b1b/on_hold/eop/reusables/brick_row.py index 928dc7972e..071348b153 100644 --- a/from_3b1b/on_hold/eop/reusables/brick_row.py +++ b/from_3b1b/on_hold/eop/reusables/brick_row.py @@ -20,7 +20,7 @@ def __init__(self, n, **kwargs): VMobject.__init__(self, **kwargs) - def generate_points(self): + def init_points(self): self.submobjects = [] self.rects = self.get_rects_for_level(self.coloring_level) diff --git a/from_3b1b/on_hold/eop/reusables/coin_flipping_pi_creature.py b/from_3b1b/on_hold/eop/reusables/coin_flipping_pi_creature.py index 20475532d2..a46f482310 100644 --- a/from_3b1b/on_hold/eop/reusables/coin_flipping_pi_creature.py +++ b/from_3b1b/on_hold/eop/reusables/coin_flipping_pi_creature.py @@ -20,7 +20,7 @@ class PiCreatureCoin(VMobject): "fill_opacity": 0.7, } - def generate_points(self): + def init_points(self): outer_rect = Rectangle( width = self.diameter, height = self.thickness, diff --git a/from_3b1b/on_hold/eop/reusables/coin_stacks.py b/from_3b1b/on_hold/eop/reusables/coin_stacks.py index 92d78c37a5..07e8bf4cb4 100644 --- a/from_3b1b/on_hold/eop/reusables/coin_stacks.py +++ b/from_3b1b/on_hold/eop/reusables/coin_stacks.py @@ -10,7 +10,7 @@ class CoinStack(VGroup): "face": FlatCoin, } - def generate_points(self): + def init_points(self): for n in range(self.size): coin = self.face(thickness = self.coin_thickness) coin.shift(n * self.coin_thickness * UP) @@ -55,7 +55,7 @@ def __init__(self, h, t, anchor = ORIGIN, **kwargs): self.anchor = anchor VGroup.__init__(self,**kwargs) - def generate_points(self): + def init_points(self): stack1 = HeadsStack(size = self.nb_heads, coin_thickness = self.coin_thickness) stack2 = TailsStack(size = self.nb_tails, coin_thickness = self.coin_thickness) stack1.next_to(self.anchor, LEFT, buff = 0.5 * SMALL_BUFF) diff --git a/from_3b1b/on_hold/eop/reusables/dice.py b/from_3b1b/on_hold/eop/reusables/dice.py index 6073001c60..39195fadb3 100644 --- a/from_3b1b/on_hold/eop/reusables/dice.py +++ b/from_3b1b/on_hold/eop/reusables/dice.py @@ -17,7 +17,7 @@ class RowOfDice(VGroup): "direction": RIGHT, } - def generate_points(self): + def init_points(self): for value in self.values: new_die = DieFace(value) new_die.submobjects[0].set_fill(opacity = 0) diff --git a/from_3b1b/on_hold/eop/reusables/histograms.py b/from_3b1b/on_hold/eop/reusables/histograms.py index dd1837bb6b..63fb8f85de 100644 --- a/from_3b1b/on_hold/eop/reusables/histograms.py +++ b/from_3b1b/on_hold/eop/reusables/histograms.py @@ -75,7 +75,7 @@ def process_values(self): - def generate_points(self): + def init_points(self): self.process_values() for submob in self.submobjects: @@ -130,7 +130,7 @@ def num_arr_to_string_arr(arr): # converts number array to string array ) if bar.height == 0: bar.height = 0.01 - bar.generate_points() + bar.init_points() t = float(x - self.x_min)/(self.x_max - self.x_min) bar_color = interpolate_color( @@ -273,7 +273,7 @@ def interpolate_mobject(self,t): cell = self.cell_for_index(i,j) self.prototype_cell.width = cell.get_width() self.prototype_cell.height = cell.get_height() - self.prototype_cell.generate_points() + self.prototype_cell.init_points() self.prototype_cell.move_to(cell.get_center()) if t == 1: diff --git a/from_3b1b/on_hold/moduli.py b/from_3b1b/on_hold/moduli.py index 7b79cdc9af..e083005c76 100644 --- a/from_3b1b/on_hold/moduli.py +++ b/from_3b1b/on_hold/moduli.py @@ -142,7 +142,7 @@ def show_meaning_of_similar(self): similar_word.to_edge(UP) self.play( - FadeOutAndShift(VGroup(title, subtitle), UP), + FadeOut(VGroup(title, subtitle), UP), tri1.next_to, sim_sign, LEFT, 0.75, tri2.next_to, sim_sign, RIGHT, 0.75, ) @@ -230,7 +230,7 @@ def show_meaning_of_similar(self): FadeOut(not_similar_word), FadeOut(sim_sign), FadeOut(sim_cross), - FadeInFrom(new_title[1], UP), + FadeIn(new_title[1], UP), ) self.play( ShowCreationThenDestruction(new_title_underline), @@ -380,7 +380,7 @@ def get_length_labels(): var.suspend_updating() var.brace.suspend_updating() self.play( - FadeInFrom(var, DOWN), + FadeIn(var, DOWN), Write(var.brace, run_time=1), # MoveToTarget(num) ) @@ -602,8 +602,8 @@ def update_triangle(tri): self.play( ShowCreation(xpy1_line), - # FadeInFrom(xpy1_label, DOWN), - FadeInFrom(xpy1_ineq, UP) + # FadeIn(xpy1_label, DOWN), + FadeIn(xpy1_ineq, UP) ) self.wait() self.play( @@ -652,7 +652,7 @@ def update_triangle(tri): ms_arrow.scale(0.95) self.play( - FadeInFrom(ms_words, LEFT), + FadeIn(ms_words, LEFT), ) self.play(ShowCreation(ms_arrow)) self.wait() @@ -691,8 +691,8 @@ def update_triangle(tri): ) self.play( ShowCreation(elbow), - FadeInFrom(right_words, UP), - FadeOutAndShift(ineqs, DOWN), + FadeIn(right_words, UP), + FadeOut(ineqs, DOWN), ) self.play( ShowCreationThenFadeOut(elbow_circle), @@ -711,7 +711,7 @@ def update_triangle(tri): arc.replace(box) self.play( - FadeInFrom(pythag_eq, UP), + FadeIn(pythag_eq, UP), ) self.add(arc, arc) self.play(ShowCreation(arc)) @@ -757,10 +757,10 @@ def update_triangle(tri): coord_label.set_opacity, 0, FadeOut(elbow), FadeIn(acute_region), - FadeOutAndShift(right_words, UP), - FadeOutAndShift(eq, UP), - FadeInFrom(acute_words, DOWN), - FadeInFrom(gt, DOWN), + FadeOut(right_words, UP), + FadeOut(eq, UP), + FadeIn(acute_words, DOWN), + FadeIn(gt, DOWN), ) self.wait() self.play(tip_tracker.shift, 0.5 * RIGHT) @@ -769,10 +769,10 @@ def update_triangle(tri): self.play( tip_tracker.shift, 1.5 * DOWN, FadeIn(obtuse_region), - FadeOutAndShift(acute_words, DOWN), - FadeOutAndShift(gt, DOWN), - FadeInFrom(obtuse_words, UP), - FadeInFrom(lt, UP), + FadeOut(acute_words, DOWN), + FadeOut(gt, DOWN), + FadeIn(obtuse_words, UP), + FadeIn(lt, UP), ) self.wait() self.play(tip_tracker.shift, 0.5 * LEFT) diff --git a/manim.py b/manim.py index 2f659056b2..2bebaea661 100755 --- a/manim.py +++ b/manim.py @@ -3,5 +3,3 @@ if __name__ == "__main__": manimlib.main() -else: - manimlib.stream_starter.start_livestream() diff --git a/manimlib/__init__.py b/manimlib/__init__.py index 5eb9e228b8..4c8d73319e 100644 --- a/manimlib/__init__.py +++ b/manimlib/__init__.py @@ -2,17 +2,13 @@ import manimlib.config import manimlib.constants import manimlib.extract_scene -import manimlib.stream_starter def main(): args = manimlib.config.parse_cli() - if not args.livestream: - config = manimlib.config.get_configuration(args) - manimlib.constants.initialize_directories(config) - manimlib.extract_scene.main(config) - else: - manimlib.stream_starter.start_livestream( - to_twitch=args.to_twitch, - twitch_key=args.twitch_key, - ) + config = manimlib.config.get_configuration(args) + manimlib.constants.initialize_directories(config) + scenes = manimlib.extract_scene.main(config) + + for scene in scenes: + scene.run() diff --git a/manimlib/animation/animation.py b/manimlib/animation/animation.py index 17ea65fa9d..ec49a226d3 100644 --- a/manimlib/animation/animation.py +++ b/manimlib/animation/animation.py @@ -1,10 +1,9 @@ from copy import deepcopy -import numpy as np - from manimlib.mobject.mobject import Mobject from manimlib.utils.config_ops import digest_config from manimlib.utils.rate_functions import smooth +from manimlib.utils.simple_functions import clip DEFAULT_ANIMATION_RUN_TIME = 1.0 @@ -42,6 +41,7 @@ def begin(self): # played. As much initialization as possible, # especially any mobject copying, should live in # this method + self.mobject.prepare_for_animation() self.starting_mobject = self.create_starting_mobject() if self.suspend_mobject_updating: # All calls to self.mobject's internal updaters @@ -51,10 +51,12 @@ def begin(self): # the internal updaters of self.starting_mobject, # or any others among self.get_all_mobjects() self.mobject.suspend_updating() + self.families = list(self.get_all_families_zipped()) self.interpolate(0) def finish(self): self.interpolate(1) + self.mobject.cleanup_from_animation() if self.suspend_mobject_updating: self.mobject.resume_updating() @@ -107,7 +109,7 @@ def update_config(self, **kwargs): # Methods for interpolation, the mean of an Animation def interpolate(self, alpha): - alpha = np.clip(alpha, 0, 1) + alpha = clip(alpha, 0, 1) self.interpolate_mobject(self.rate_func(alpha)) def update(self, alpha): @@ -118,9 +120,8 @@ def update(self, alpha): self.interpolate(alpha) def interpolate_mobject(self, alpha): - families = list(self.get_all_families_zipped()) - for i, mobs in enumerate(families): - sub_alpha = self.get_sub_alpha(alpha, i, len(families)) + for i, mobs in enumerate(self.families): + sub_alpha = self.get_sub_alpha(alpha, i, len(self.families)) self.interpolate_submobject(*mobs, sub_alpha) def interpolate_submobject(self, submobject, starting_sumobject, alpha): @@ -135,7 +136,7 @@ def get_sub_alpha(self, alpha, index, num_submobjects): full_length = (num_submobjects - 1) * lag_ratio + 1 value = alpha * full_length lower = index * lag_ratio - return np.clip((value - lower), 0, 1) + return clip((value - lower), 0, 1) # Getters and setters def set_run_time(self, run_time): diff --git a/manimlib/animation/composition.py b/manimlib/animation/composition.py index 753f5111bc..c120f60b09 100644 --- a/manimlib/animation/composition.py +++ b/manimlib/animation/composition.py @@ -7,6 +7,7 @@ from manimlib.utils.config_ops import digest_config from manimlib.utils.iterables import remove_list_redundancies from manimlib.utils.rate_functions import linear +from manimlib.utils.simple_functions import clip DEFAULT_LAGGED_START_LAG_RATIO = 0.05 @@ -98,7 +99,7 @@ def interpolate(self, alpha): if anim_time == 0: sub_alpha = 0 else: - sub_alpha = np.clip( + sub_alpha = clip( (time - start_time) / anim_time, 0, 1 ) @@ -159,4 +160,4 @@ def __init__(self, AnimationClass, mobject, arg_creator=None, **kwargs): AnimationClass(*args, **anim_kwargs) for args in args_list ] - super().__init__(*animations, **kwargs) + super().__init__(*animations, group=mobject, **kwargs) diff --git a/manimlib/animation/creation.py b/manimlib/animation/creation.py index 692e384ecb..2df293f900 100644 --- a/manimlib/animation/creation.py +++ b/manimlib/animation/creation.py @@ -89,11 +89,10 @@ def get_all_mobjects(self): def interpolate_submobject(self, submob, start, outline, alpha): index, subalpha = integer_interpolate(0, 2, alpha) if index == 0: - submob.pointwise_become_partial( - outline, 0, subalpha - ) + submob.pointwise_become_partial(outline, 0, subalpha) submob.match_style(outline) else: + submob.pointwise_become_partial(outline, 0, 1) submob.interpolate(outline, start, subalpha) @@ -125,7 +124,7 @@ def set_default_config_from_length(self, mobject): class ShowIncreasingSubsets(Animation): CONFIG = { "suspend_mobject_updating": False, - "int_func": np.floor, + "int_func": np.round, } def __init__(self, group, **kwargs): @@ -138,7 +137,7 @@ def interpolate_mobject(self, alpha): self.update_submobject_list(index) def update_submobject_list(self, index): - self.mobject.submobjects = self.all_submobs[:index] + self.mobject.set_submobjects(self.all_submobs[:index]) class ShowSubmobjectsOneByOne(ShowIncreasingSubsets): @@ -153,9 +152,9 @@ def __init__(self, group, **kwargs): def update_submobject_list(self, index): # N = len(self.all_submobs) if index == 0: - self.mobject.submobjects = [] + self.mobject.set_submobjects([]) else: - self.mobject.submobjects = self.all_submobs[index - 1] + self.mobject.set_submobjects(self.all_submobs[index - 1]) # TODO, this is broken... diff --git a/manimlib/animation/fading.py b/manimlib/animation/fading.py index 9f5ea190d8..373d22a366 100644 --- a/manimlib/animation/fading.py +++ b/manimlib/animation/fading.py @@ -1,8 +1,8 @@ from manimlib.animation.animation import Animation from manimlib.animation.animation import DEFAULT_ANIMATION_LAG_RATIO from manimlib.animation.transform import Transform +from manimlib.constants import ORIGIN from manimlib.constants import DOWN -from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.utils.bezier import interpolate from manimlib.utils.rate_functions import there_and_back @@ -16,8 +16,15 @@ class FadeOut(Transform): "lag_ratio": DEFAULT_FADE_LAG_RATIO, } + def __init__(self, mobject, to_vect=ORIGIN, **kwargs): + self.to_vect = to_vect + super().__init__(mobject, **kwargs) + def create_target(self): - return self.mobject.copy().fade(1) + result = self.mobject.copy() + result.set_opacity(0) + result.shift(self.to_vect) + return result def clean_up_from_scene(self, scene=None): super().clean_up_from_scene(scene) @@ -29,85 +36,46 @@ class FadeIn(Transform): "lag_ratio": DEFAULT_FADE_LAG_RATIO, } + def __init__(self, mobject, from_vect=ORIGIN, **kwargs): + self.from_vect = from_vect + super().__init__(mobject, **kwargs) + def create_target(self): return self.mobject def create_starting_mobject(self): start = super().create_starting_mobject() - start.fade(1) - if isinstance(start, VMobject): - start.set_stroke(width=0) - start.set_fill(opacity=0) + start.set_opacity(0) + start.shift(self.from_vect) return start -class FadeInFrom(Transform): - CONFIG = { - "direction": DOWN, - "lag_ratio": DEFAULT_ANIMATION_LAG_RATIO, - } - - def __init__(self, mobject, direction=None, **kwargs): - if direction is not None: - self.direction = direction - super().__init__(mobject, **kwargs) - - def create_target(self): - return self.mobject.copy() - - def begin(self): - super().begin() - self.starting_mobject.shift(self.direction) - self.starting_mobject.fade(1) +# Below will be deprecated -class FadeInFromDown(FadeInFrom): +class FadeInFromDown(FadeIn): """ - Identical to FadeInFrom, just with a name that + Identical to FadeIn, just with a name that communicates the default """ - CONFIG = { - "direction": DOWN, - "lag_ratio": DEFAULT_ANIMATION_LAG_RATIO, - } - -class FadeOutAndShift(FadeOut): - CONFIG = { - "direction": DOWN, - } + def __init__(self, mobject, **kwargs): + super().__init__(mobject, DOWN, **kwargs) - def __init__(self, mobject, direction=None, **kwargs): - if direction is not None: - self.direction = direction - super().__init__(mobject, **kwargs) - def create_target(self): - target = super().create_target() - target.shift(self.direction) - return target - - -class FadeOutAndShiftDown(FadeOutAndShift): +class FadeOutAndShiftDown(FadeOut): """ - Identical to FadeOutAndShift, just with a name that + Identical to FadeOut, just with a name that communicates the default """ - CONFIG = { - "direction": DOWN, - } + + def __init__(self, mobject, **kwargs): + super().__init__(mobject, DOWN, **kwargs) class FadeInFromPoint(FadeIn): def __init__(self, mobject, point, **kwargs): - self.point = point - super().__init__(mobject, **kwargs) - - def create_starting_mobject(self): - start = super().create_starting_mobject() - start.scale(0) - start.move_to(self.point) - return start + super().__init__(mobject, point - mobject.get_center(), **kwargs) class FadeInFromLarge(FadeIn): diff --git a/manimlib/animation/growing.py b/manimlib/animation/growing.py index e2a53cb737..a4dcfebaa0 100644 --- a/manimlib/animation/growing.py +++ b/manimlib/animation/growing.py @@ -32,7 +32,7 @@ def __init__(self, mobject, **kwargs): class GrowFromEdge(GrowFromPoint): def __init__(self, mobject, edge, **kwargs): - point = mobject.get_critical_point(edge) + point = mobject.get_bounding_box_point(edge) super().__init__(mobject, point, **kwargs) diff --git a/manimlib/animation/indication.py b/manimlib/animation/indication.py index 268ad5f482..62026e6322 100644 --- a/manimlib/animation/indication.py +++ b/manimlib/animation/indication.py @@ -94,8 +94,10 @@ def create_lines(self): line.shift((self.flash_radius - self.line_length) * RIGHT) line.rotate(angle, about_point=ORIGIN) lines.add(line) - lines.set_color(self.color) - lines.set_stroke(width=3) + lines.set_stroke( + color=self.color, + width=self.line_stroke_width + ) lines.add_updater(lambda l: l.move_to(self.point)) return lines diff --git a/manimlib/animation/numbers.py b/manimlib/animation/numbers.py index e1f354d273..3f999242cd 100644 --- a/manimlib/animation/numbers.py +++ b/manimlib/animation/numbers.py @@ -50,3 +50,13 @@ def __init__(self, decimal_mob, target_number, **kwargs): lambda a: interpolate(start_number, target_number, a), **kwargs ) + + +class CountInFrom(ChangingDecimal): + def __init__(self, decimal_mob, source_number=0, **kwargs): + start_number = decimal_mob.number + super().__init__( + decimal_mob, + lambda a: interpolate(source_number, start_number, a), + **kwargs + ) diff --git a/manimlib/animation/rotation.py b/manimlib/animation/rotation.py index 7578c4d6fe..e668bba205 100644 --- a/manimlib/animation/rotation.py +++ b/manimlib/animation/rotation.py @@ -1,52 +1,43 @@ from manimlib.animation.animation import Animation -from manimlib.animation.transform import Transform from manimlib.constants import OUT from manimlib.constants import PI from manimlib.constants import TAU from manimlib.utils.rate_functions import linear +from manimlib.utils.rate_functions import smooth class Rotating(Animation): CONFIG = { - "axis": OUT, - "radians": TAU, + # "axis": OUT, + # "radians": TAU, "run_time": 5, "rate_func": linear, "about_point": None, "about_edge": None, + "suspend_mobject_updating": False, } + def __init__(self, mobject, angle=TAU, axis=OUT, **kwargs): + self.angle = angle + self.axis = axis + super().__init__(mobject, **kwargs) + def interpolate_mobject(self, alpha): - self.mobject.become(self.starting_mobject) + for sm1, sm2 in self.get_all_families_zipped(): + sm1.points[:] = sm2.points self.mobject.rotate( - alpha * self.radians, + alpha * self.angle, axis=self.axis, about_point=self.about_point, about_edge=self.about_edge, ) -class Rotate(Transform): +class Rotate(Rotating): CONFIG = { - "about_point": None, - "about_edge": None, + "run_time": 1, + "rate_func": smooth, } def __init__(self, mobject, angle=PI, axis=OUT, **kwargs): - if "path_arc" not in kwargs: - kwargs["path_arc"] = angle - if "path_arc_axis" not in kwargs: - kwargs["path_arc_axis"] = axis - self.angle = angle - self.axis = axis - super().__init__(mobject, **kwargs) - - def create_target(self): - target = self.mobject.copy() - target.rotate( - self.angle, - axis=self.axis, - about_point=self.about_point, - about_edge=self.about_edge, - ) - return target + super().__init__(mobject, angle, axis, **kwargs) diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index b6bb4be4b9..ae49cf3d70 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -1,41 +1,140 @@ -from functools import reduce -import itertools as it -import operator as op -import time -import copy +import moderngl +from colour import Color +import OpenGL.GL as gl from PIL import Image -from scipy.spatial.distance import pdist -import cairo import numpy as np +import itertools as it from manimlib.constants import * -from manimlib.mobject.types.image_mobject import AbstractImageMobject from manimlib.mobject.mobject import Mobject -from manimlib.mobject.types.point_cloud_mobject import PMobject -from manimlib.mobject.types.vectorized_mobject import VMobject -from manimlib.utils.color import color_to_int_rgba +from manimlib.mobject.mobject import Point from manimlib.utils.config_ops import digest_config -from manimlib.utils.images import get_full_raster_image_path -from manimlib.utils.iterables import batch_by_property -from manimlib.utils.iterables import list_difference_update -from manimlib.utils.iterables import remove_list_redundancies +from manimlib.utils.bezier import interpolate from manimlib.utils.simple_functions import fdiv +from manimlib.utils.simple_functions import clip from manimlib.utils.space_ops import angle_of_vector -from manimlib.utils.space_ops import get_norm +from manimlib.utils.space_ops import rotation_matrix_transpose_from_quaternion +from manimlib.utils.space_ops import rotation_matrix_transpose +from manimlib.utils.space_ops import quaternion_from_angle_axis +from manimlib.utils.space_ops import quaternion_mult + + +class CameraFrame(Mobject): + CONFIG = { + "frame_shape": (FRAME_WIDTH, FRAME_HEIGHT), + "center_point": ORIGIN, + # Theta, phi, gamma + "euler_angles": [0, 0, 0], + "focal_distance": 4, + } + + def init_points(self): + self.points = np.array([ORIGIN, LEFT, RIGHT, DOWN, UP]) + self.set_width(self.frame_shape[0], stretch=True) + self.set_height(self.frame_shape[1], stretch=True) + self.move_to(self.center_point) + self.euler_angles = np.array(self.euler_angles, dtype='float64') + self.refresh_camera_rotation_matrix() + + def to_default_state(self): + self.center() + self.set_height(FRAME_HEIGHT) + self.set_width(FRAME_WIDTH) + self.set_rotation(0, 0, 0) + return self + + def get_inverse_camera_position_matrix(self): + mat = np.identity(4) + # First shift so that origin of real space coincides with camera origin + mat[:3, 3] = -self.get_center().T + # Rotate based on camera orientation + mat[:3, :3] = np.dot(self.inverse_camera_rotation_matrix, mat[:3, :3]) + return mat + + def refresh_camera_rotation_matrix(self): + theta, phi, gamma = self.euler_angles + quat = quaternion_mult( + quaternion_from_angle_axis(theta, OUT, axis_normalized=True), + quaternion_from_angle_axis(phi, RIGHT, axis_normalized=True), + quaternion_from_angle_axis(gamma, OUT, axis_normalized=True), + ) + self.inverse_camera_rotation_matrix = rotation_matrix_transpose_from_quaternion(quat) + return self + + def rotate(self, angle, axis=OUT, **kwargs): + curr_rot_T = self.get_inverse_camera_rotation_matrix() + added_rot_T = rotation_matrix_transpose(angle, axis) + new_rot_T = np.dot(curr_rot_T, added_rot_T) + Fz = new_rot_T[2] + phi = np.arccos(Fz[2]) + theta = angle_of_vector(Fz[:2]) + PI / 2 + partial_rot_T = np.dot( + rotation_matrix_transpose(phi, RIGHT), + rotation_matrix_transpose(theta, OUT), + ) + gamma = angle_of_vector(np.dot(partial_rot_T, new_rot_T.T)[:, 0]) + # TODO, write a function that converts quaternions to euler angles + self.euler_angles[:] = theta, phi, gamma + return self + + def set_rotation(self, theta=None, phi=None, gamma=None): + if theta is not None: + self.euler_angles[0] = theta + if phi is not None: + self.euler_angles[1] = phi + if gamma is not None: + self.euler_angles[2] = gamma + self.refresh_camera_rotation_matrix() + return self + + def set_theta(self, theta): + return self.set_rotation(theta=theta) + + def set_phi(self, phi): + return self.set_rotation(phi=phi) + + def set_gamma(self, gamma): + return self.set_rotation(phi=phi) + + def increment_theta(self, dtheta): + return self.set_rotation(theta=self.euler_angles[0] + dtheta) + + def increment_phi(self, dphi): + new_phi = clip(self.euler_angles[1] + dphi, 0, PI) + return self.set_rotation(phi=new_phi) + + def increment_gamma(self, dgamma): + return self.set_rotation(theta=self.euler_angles[2] + dgamma) + + def get_shape(self): + return ( + self.points[2, 0] - self.points[1, 0], + self.points[4, 1] - self.points[3, 1], + ) + + def get_center(self): + # Assumes first point is at the center + return self.points[0] + + def get_focal_distance(self): + return self.focal_distance + + def interpolate(self, frame1, frame2, alpha, path_func): + self.euler_angles[:] = interpolate(frame1.euler_angles, frame2.euler_angles, alpha) + self.refresh_camera_rotation_matrix() + self.points = interpolate(frame1.points, frame2.points, alpha) class Camera(object): CONFIG = { "background_image": None, + "frame_config": {}, "pixel_height": DEFAULT_PIXEL_HEIGHT, "pixel_width": DEFAULT_PIXEL_WIDTH, - "frame_rate": DEFAULT_FRAME_RATE, + "frame_rate": DEFAULT_FRAME_RATE, # TODO, move this elsewhere # Note: frame height and width will be resized to match # the pixel aspect ratio - "frame_height": FRAME_HEIGHT, - "frame_width": FRAME_WIDTH, - "frame_center": ORIGIN, "background_color": BLACK, "background_opacity": 1, # Points in vectorized mobjects with norm greater @@ -44,57 +143,141 @@ class Camera(object): "image_mode": "RGBA", "n_channels": 4, "pixel_array_dtype": 'uint8', - # z_buff_func is only used if the flag above is set to True. - # round z coordinate to nearest hundredth when comparring - "z_buff_func": lambda m: np.round(m.get_center()[2], 2), - "cairo_line_width_multiple": 0.01, + "light_source_position": [-10, 10, 10], + # Measured in pixel widths, used for vector graphics + "anti_alias_width": 1.5, + # Although vector graphics handle antialiasing fine + # without multisampling, for 3d scenes one might want + # to set samples to be greater than 0. + "samples": 0, } - def __init__(self, background=None, **kwargs): + def __init__(self, ctx=None, **kwargs): digest_config(self, kwargs, locals()) self.rgb_max_val = np.iinfo(self.pixel_array_dtype).max - self.pixel_array_to_cairo_context = {} - self.init_background() - self.resize_frame_shape() - self.reset() - - def __deepcopy__(self, memo): - # This is to address a strange bug where deepcopying - # will result in a segfault, which is somehow related - # to the aggdraw library - self.canvas = None - return copy.copy(self) - - def reset_pixel_shape(self, new_height, new_width): + self.background_rgba = [ + *Color(self.background_color).get_rgb(), + self.background_opacity + ] + self.init_frame() + self.init_context(ctx) + self.init_shaders() + self.init_textures() + self.init_light_source() + self.refresh_perspective_uniforms() + self.static_mobject_to_render_group_list = {} + + def init_frame(self): + self.frame = CameraFrame(**self.frame_config) + + def init_context(self, ctx=None): + if ctx is None: + ctx = moderngl.create_standalone_context() + fbo = self.get_fbo(ctx, 0) + else: + fbo = ctx.detect_framebuffer() + + # For multisample antialiasing + fbo_msaa = self.get_fbo(ctx, self.samples) + fbo_msaa.use() + + ctx.enable(moderngl.BLEND) + ctx.blend_func = ( + moderngl.SRC_ALPHA, moderngl.ONE_MINUS_SRC_ALPHA, + moderngl.ONE, moderngl.ONE + ) + + self.ctx = ctx + self.fbo = fbo + self.fbo_msaa = fbo_msaa + + def init_light_source(self): + self.light_source = Point(self.light_source_position) + + # Methods associated with the frame buffer + def get_fbo(self, ctx, samples=0): + pw = self.pixel_width + ph = self.pixel_height + return ctx.framebuffer( + color_attachments=ctx.texture( + (pw, ph), + components=self.n_channels, + samples=samples, + ), + depth_attachment=ctx.depth_renderbuffer( + (pw, ph), + samples=samples + ) + ) + + def clear(self): + self.fbo.clear(*self.background_rgba) + self.fbo_msaa.clear(*self.background_rgba) + + def reset_pixel_shape(self, new_width, new_height): self.pixel_width = new_width self.pixel_height = new_height - self.init_background() - self.resize_frame_shape() - self.reset() + self.refresh_perspective_uniforms() + + def get_raw_fbo_data(self, dtype='f1'): + # Copy blocks from the fbo_msaa to the drawn fbo using Blit + pw, ph = (self.pixel_width, self.pixel_height) + gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, self.fbo_msaa.glo) + gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, self.fbo.glo) + gl.glBlitFramebuffer(0, 0, pw, ph, 0, 0, pw, ph, gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR) + return self.fbo.read( + viewport=self.fbo.viewport, + components=self.n_channels, + dtype=dtype, + ) - def get_pixel_height(self): - return self.pixel_height + def get_image(self, pixel_array=None): + return Image.frombytes( + 'RGBA', + self.get_pixel_shape(), + self.get_raw_fbo_data(), + 'raw', 'RGBA', 0, -1 + ) + + def get_pixel_array(self): + raw = self.get_raw_fbo_data(dtype='f4') + flat_arr = np.frombuffer(raw, dtype='f4') + arr = flat_arr.reshape([*self.fbo.size, self.n_channels]) + # Convert from float + return (self.rgb_max_val * arr).astype(self.pixel_array_dtype) + + # Needed? + def get_texture(self): + texture = self.ctx.texture( + size=self.fbo.size, + components=4, + data=self.get_raw_fbo_data(), + dtype='f4' + ) + return texture + + # Getting camera attributes + def get_pixel_shape(self): + return self.fbo.viewport[2:4] + # return (self.pixel_width, self.pixel_height) def get_pixel_width(self): - return self.pixel_width + return self.get_pixel_shape()[0] + + def get_pixel_height(self): + return self.get_pixel_shape()[1] def get_frame_height(self): - return self.frame_height + return self.frame.get_height() def get_frame_width(self): - return self.frame_width + return self.frame.get_width() - def get_frame_center(self): - return self.frame_center + def get_frame_shape(self): + return (self.get_frame_width(), self.get_frame_height()) - def set_frame_height(self, frame_height): - self.frame_height = frame_height - - def set_frame_width(self, frame_width): - self.frame_width = frame_width - - def set_frame_center(self, frame_center): - self.frame_center = frame_center + def get_frame_center(self): + return self.frame.get_center() def resize_frame_shape(self, fixed_dimension=0): """ @@ -112,601 +295,157 @@ def resize_frame_shape(self, fixed_dimension=0): frame_height = frame_width / aspect_ratio else: frame_width = aspect_ratio * frame_height - self.set_frame_height(frame_height) - self.set_frame_width(frame_width) - - def init_background(self): - height = self.get_pixel_height() - width = self.get_pixel_width() - if self.background_image is not None: - path = get_full_raster_image_path(self.background_image) - image = Image.open(path).convert(self.image_mode) - # TODO, how to gracefully handle backgrounds - # with different sizes? - self.background = np.array(image)[:height, :width] - self.background = self.background.astype(self.pixel_array_dtype) + self.frame.set_height(frame_height) + self.frame.set_width(frame_width) + + def pixel_coords_to_space_coords(self, px, py, relative=False): + # pw, ph = self.fbo.size + # Bad hack, not sure why this is needed. + pw, ph = self.get_pixel_shape() + pw //= 2 + ph //= 2 + fw, fh = self.get_frame_shape() + fc = self.get_frame_center() + if relative: + return 2 * np.array([px / pw, py / ph, 0]) else: - background_rgba = color_to_int_rgba( - self.background_color, self.background_opacity - ) - self.background = np.zeros( - (height, width, self.n_channels), - dtype=self.pixel_array_dtype - ) - self.background[:, :] = background_rgba - - def get_image(self, pixel_array=None): - if pixel_array is None: - pixel_array = self.pixel_array - return Image.fromarray( - pixel_array, - mode=self.image_mode - ) - - def get_pixel_array(self): - return self.pixel_array - - def convert_pixel_array(self, pixel_array, convert_from_floats=False): - retval = np.array(pixel_array) - if convert_from_floats: - retval = np.apply_along_axis( - lambda f: (f * self.rgb_max_val).astype(self.pixel_array_dtype), - 2, - retval - ) - return retval - - def set_pixel_array(self, pixel_array, convert_from_floats=False): - converted_array = self.convert_pixel_array( - pixel_array, convert_from_floats) - if not (hasattr(self, "pixel_array") and self.pixel_array.shape == converted_array.shape): - self.pixel_array = converted_array + # Only scale wrt one axis + scale = fh / ph + return fc + scale * np.array([(px - pw / 2), (py - ph / 2), 0]) + + # Rendering + def capture(self, *mobjects, **kwargs): + self.refresh_perspective_uniforms() + for mobject in mobjects: + for render_group in self.get_render_group_list(mobject): + self.render(render_group) + + def render(self, render_group): + shader_wrapper = render_group["shader_wrapper"] + shader_program = render_group["prog"] + self.set_shader_uniforms(shader_program, shader_wrapper) + self.update_depth_test(shader_wrapper) + render_group["vao"].render(int(shader_wrapper.render_primative)) + if render_group["single_use"]: + self.release_render_group(render_group) + + def update_depth_test(self, shader_wrapper): + if shader_wrapper.depth_test: + self.ctx.enable(moderngl.DEPTH_TEST) else: - # Set in place - self.pixel_array[:, :, :] = converted_array[:, :, :] - - def set_background(self, pixel_array, convert_from_floats=False): - self.background = self.convert_pixel_array( - pixel_array, convert_from_floats) - - # TODO, this should live in utils, not as a method of Camera - def make_background_from_func(self, coords_to_colors_func): - """ - Sets background by using coords_to_colors_func to determine each pixel's color. Each input - to coords_to_colors_func is an (x, y) pair in space (in ordinary space coordinates; not - pixel coordinates), and each output is expected to be an RGBA array of 4 floats. - """ - - print("Starting set_background; for reference, the current time is ", time.strftime("%H:%M:%S")) - coords = self.get_coords_of_all_pixels() - new_background = np.apply_along_axis( - coords_to_colors_func, - 2, - coords - ) - print("Ending set_background; for reference, the current time is ", time.strftime("%H:%M:%S")) - - return self.convert_pixel_array(new_background, convert_from_floats=True) - - def set_background_from_func(self, coords_to_colors_func): - self.set_background( - self.make_background_from_func(coords_to_colors_func)) - - def reset(self): - self.set_pixel_array(self.background) - return self - - #### - - # TODO, it's weird that this is part of camera. - # Clearly it should live elsewhere. - def extract_mobject_family_members( - self, mobjects, - only_those_with_points=False): - if only_those_with_points: - method = Mobject.family_members_with_points + self.ctx.disable(moderngl.DEPTH_TEST) + + def get_render_group_list(self, mobject): + try: + return self.static_mobject_to_render_group_list[id(mobject)] + except KeyError: + return map(self.get_render_group, mobject.get_shader_wrapper_list()) + + def get_render_group(self, shader_wrapper, single_use=True): + # Data buffers + vbo = self.ctx.buffer(shader_wrapper.vert_data.tobytes()) + if shader_wrapper.vert_indices is None: + ibo = None else: - method = Mobject.get_family - return remove_list_redundancies(list( - it.chain(*[method(m) for m in mobjects]) - )) - - def get_mobjects_to_display( - self, mobjects, - include_submobjects=True, - excluded_mobjects=None): - if include_submobjects: - mobjects = self.extract_mobject_family_members( - mobjects, only_those_with_points=True, - ) - if excluded_mobjects: - all_excluded = self.extract_mobject_family_members( - excluded_mobjects - ) - mobjects = list_difference_update(mobjects, all_excluded) - return mobjects - - def is_in_frame(self, mobject): - fc = self.get_frame_center() - fh = self.get_frame_height() - fw = self.get_frame_width() - return not reduce(op.or_, [ - mobject.get_right()[0] < fc[0] - fw, - mobject.get_bottom()[1] > fc[1] + fh, - mobject.get_left()[0] > fc[0] + fw, - mobject.get_top()[1] < fc[1] - fh, - ]) - - def capture_mobject(self, mobject, **kwargs): - return self.capture_mobjects([mobject], **kwargs) - - def capture_mobjects(self, mobjects, **kwargs): - mobjects = self.get_mobjects_to_display(mobjects, **kwargs) - - # Organize this list into batches of the same type, and - # apply corresponding function to those batches - type_func_pairs = [ - (VMobject, self.display_multiple_vectorized_mobjects), - (PMobject, self.display_multiple_point_cloud_mobjects), - (AbstractImageMobject, self.display_multiple_image_mobjects), - (Mobject, lambda batch, pa: batch), # Do nothing - ] - - def get_mobject_type(mobject): - for mobject_type, func in type_func_pairs: - if isinstance(mobject, mobject_type): - return mobject_type - raise Exception( - "Trying to display something which is not of type Mobject" - ) - batch_type_pairs = batch_by_property(mobjects, get_mobject_type) - - # Display in these batches - for batch, batch_type in batch_type_pairs: - # check what the type is, and call the appropriate function - for mobject_type, func in type_func_pairs: - if batch_type == mobject_type: - func(batch, self.pixel_array) - - # Methods associated with svg rendering - - def get_cached_cairo_context(self, pixel_array): - return self.pixel_array_to_cairo_context.get( - id(pixel_array), None - ) - - def cache_cairo_context(self, pixel_array, ctx): - self.pixel_array_to_cairo_context[id(pixel_array)] = ctx - - def get_cairo_context(self, pixel_array): - cached_ctx = self.get_cached_cairo_context(pixel_array) - if cached_ctx: - return cached_ctx - pw = self.get_pixel_width() - ph = self.get_pixel_height() - fw = self.get_frame_width() - fh = self.get_frame_height() - fc = self.get_frame_center() - surface = cairo.ImageSurface.create_for_data( - pixel_array, - cairo.FORMAT_ARGB32, - pw, ph - ) - ctx = cairo.Context(surface) - ctx.scale(pw, ph) - ctx.set_matrix(cairo.Matrix( - fdiv(pw, fw), 0, - 0, -fdiv(ph, fh), - (pw / 2) - fc[0] * fdiv(pw, fw), - (ph / 2) + fc[1] * fdiv(ph, fh), - )) - self.cache_cairo_context(pixel_array, ctx) - return ctx - - def display_multiple_vectorized_mobjects(self, vmobjects, pixel_array): - if len(vmobjects) == 0: - return - batch_file_pairs = batch_by_property( - vmobjects, - lambda vm: vm.get_background_image_file() - ) - for batch, file_name in batch_file_pairs: - if file_name: - self.display_multiple_background_colored_vmobject(batch, pixel_array) - else: - self.display_multiple_non_background_colored_vmobjects(batch, pixel_array) - - def display_multiple_non_background_colored_vmobjects(self, vmobjects, pixel_array): - ctx = self.get_cairo_context(pixel_array) - for vmobject in vmobjects: - self.display_vectorized(vmobject, ctx) - - def display_vectorized(self, vmobject, ctx): - self.set_cairo_context_path(ctx, vmobject) - self.apply_stroke(ctx, vmobject, background=True) - self.apply_fill(ctx, vmobject) - self.apply_stroke(ctx, vmobject) - return self - - def set_cairo_context_path(self, ctx, vmobject): - points = self.transform_points_pre_display( - vmobject, vmobject.points - ) - # TODO, shouldn't this be handled in transform_points_pre_display? - # points = points - self.get_frame_center() - if len(points) == 0: - return - - ctx.new_path() - subpaths = vmobject.get_subpaths_from_points(points) - for subpath in subpaths: - quads = vmobject.get_cubic_bezier_tuples_from_points(subpath) - ctx.new_sub_path() - start = subpath[0] - ctx.move_to(*start[:2]) - for p0, p1, p2, p3 in quads: - ctx.curve_to(*p1[:2], *p2[:2], *p3[:2]) - if vmobject.consider_points_equals(subpath[0], subpath[-1]): - ctx.close_path() - return self - - def set_cairo_context_color(self, ctx, rgbas, vmobject): - if len(rgbas) == 1: - # Use reversed rgb because cairo surface is - # encodes it in reverse order - ctx.set_source_rgba( - *rgbas[0][2::-1], rgbas[0][3] - ) - else: - points = vmobject.get_gradient_start_and_end_points() - points = self.transform_points_pre_display( - vmobject, points - ) - pat = cairo.LinearGradient(*it.chain(*[ - point[:2] for point in points - ])) - step = 1.0 / (len(rgbas) - 1) - offsets = np.arange(0, 1 + step, step) - for rgba, offset in zip(rgbas, offsets): - pat.add_color_stop_rgba( - offset, *rgba[2::-1], rgba[3] - ) - ctx.set_source(pat) - return self - - def apply_fill(self, ctx, vmobject): - self.set_cairo_context_color( - ctx, self.get_fill_rgbas(vmobject), vmobject - ) - ctx.fill_preserve() - return self - - def apply_stroke(self, ctx, vmobject, background=False): - width = vmobject.get_stroke_width(background) - if width == 0: - return self - self.set_cairo_context_color( - ctx, - self.get_stroke_rgbas(vmobject, background=background), - vmobject - ) - ctx.set_line_width( - width * self.cairo_line_width_multiple * - # This ensures lines have constant width - # as you zoom in on them. - (self.get_frame_width() / FRAME_WIDTH) - ) - ctx.stroke_preserve() - return self - - def get_stroke_rgbas(self, vmobject, background=False): - return vmobject.get_stroke_rgbas(background) - - def get_fill_rgbas(self, vmobject): - return vmobject.get_fill_rgbas() - - def get_background_colored_vmobject_displayer(self): - # Quite wordy to type out a bunch - bcvd = "background_colored_vmobject_displayer" - if not hasattr(self, bcvd): - setattr(self, bcvd, BackgroundColoredVMobjectDisplayer(self)) - return getattr(self, bcvd) - - def display_multiple_background_colored_vmobject(self, cvmobjects, pixel_array): - displayer = self.get_background_colored_vmobject_displayer() - cvmobject_pixel_array = displayer.display(*cvmobjects) - self.overlay_rgba_array(pixel_array, cvmobject_pixel_array) - return self - - # Methods for other rendering - - def display_multiple_point_cloud_mobjects(self, pmobjects, pixel_array): - for pmobject in pmobjects: - self.display_point_cloud( - pmobject, - pmobject.points, - pmobject.rgbas, - self.adjusted_thickness(pmobject.stroke_width), - pixel_array, - ) - - def display_point_cloud(self, pmobject, points, rgbas, thickness, pixel_array): - if len(points) == 0: - return - pixel_coords = self.points_to_pixel_coords( - pmobject, points - ) - pixel_coords = self.thickened_coordinates( - pixel_coords, thickness - ) - rgba_len = pixel_array.shape[2] - - rgbas = (self.rgb_max_val * rgbas).astype(self.pixel_array_dtype) - target_len = len(pixel_coords) - factor = target_len // len(rgbas) - rgbas = np.array([rgbas] * factor).reshape((target_len, rgba_len)) - - on_screen_indices = self.on_screen_pixels(pixel_coords) - pixel_coords = pixel_coords[on_screen_indices] - rgbas = rgbas[on_screen_indices] - - ph = self.get_pixel_height() - pw = self.get_pixel_width() - - flattener = np.array([1, pw], dtype='int') - flattener = flattener.reshape((2, 1)) - indices = np.dot(pixel_coords, flattener)[:, 0] - indices = indices.astype('int') - - new_pa = pixel_array.reshape((ph * pw, rgba_len)) - new_pa[indices] = rgbas - pixel_array[:, :] = new_pa.reshape((ph, pw, rgba_len)) - - def display_multiple_image_mobjects(self, image_mobjects, pixel_array): - for image_mobject in image_mobjects: - self.display_image_mobject(image_mobject, pixel_array) - - def display_image_mobject(self, image_mobject, pixel_array): - corner_coords = self.points_to_pixel_coords( - image_mobject, image_mobject.points - ) - ul_coords, ur_coords, dl_coords = corner_coords - right_vect = ur_coords - ul_coords - down_vect = dl_coords - ul_coords - center_coords = ul_coords + (right_vect + down_vect) / 2 - - sub_image = Image.fromarray( - image_mobject.get_pixel_array(), - mode="RGBA" - ) - - # Reshape - pixel_width = max(int(pdist([ul_coords, ur_coords])), 1) - pixel_height = max(int(pdist([ul_coords, dl_coords])), 1) - sub_image = sub_image.resize( - (pixel_width, pixel_height), resample=Image.BICUBIC - ) - - # Rotate - angle = angle_of_vector(right_vect) - adjusted_angle = -int(360 * angle / TAU) - if adjusted_angle != 0: - sub_image = sub_image.rotate( - adjusted_angle, resample=Image.BICUBIC, expand=1 - ) - - # TODO, there is no accounting for a shear... - - # Paste into an image as large as the camear's pixel array - full_image = Image.fromarray( - np.zeros((self.get_pixel_height(), self.get_pixel_width())), - mode="RGBA" - ) - new_ul_coords = center_coords - np.array(sub_image.size) / 2 - new_ul_coords = new_ul_coords.astype(int) - full_image.paste( - sub_image, - box=( - new_ul_coords[0], - new_ul_coords[1], - new_ul_coords[0] + sub_image.size[0], - new_ul_coords[1] + sub_image.size[1], - ) - ) - # Paint on top of existing pixel array - self.overlay_PIL_image(pixel_array, full_image) - - def overlay_rgba_array(self, pixel_array, new_array): - self.overlay_PIL_image( - pixel_array, - self.get_image(new_array), - ) - - def overlay_PIL_image(self, pixel_array, image): - pixel_array[:, :] = np.array( - Image.alpha_composite( - self.get_image(pixel_array), - image - ), - dtype='uint8' - ) - - def adjust_out_of_range_points(self, points): - if not np.any(points > self.max_allowable_norm): - return points - norms = np.apply_along_axis(get_norm, 1, points) - violator_indices = norms > self.max_allowable_norm - violators = points[violator_indices, :] - violator_norms = norms[violator_indices] - reshaped_norms = np.repeat( - violator_norms.reshape((len(violator_norms), 1)), - points.shape[1], 1 - ) - rescaled = self.max_allowable_norm * violators / reshaped_norms - points[violator_indices] = rescaled - return points - - def transform_points_pre_display(self, mobject, points): - # Subclasses (like ThreeDCamera) may want to - # adjust points futher before they're shown - if np.any(np.isnan(points)) or np.any(points == np.inf): - # TODO, print some kind of warning about - # mobject having invalid points? - points = np.zeros((1, 3)) - return points - - def points_to_pixel_coords(self, mobject, points): - points = self.transform_points_pre_display( - mobject, points + ibo = self.ctx.buffer(shader_wrapper.vert_indices.astype('i4').tobytes()) + + # Program and vertex array + shader_program, vert_format = self.get_shader_program(shader_wrapper) + vao = self.ctx.vertex_array( + program=shader_program, + content=[(vbo, vert_format, *shader_wrapper.vert_attributes)], + index_buffer=ibo, ) - shifted_points = points - self.get_frame_center() - - result = np.zeros((len(points), 2)) - pixel_height = self.get_pixel_height() - pixel_width = self.get_pixel_width() - frame_height = self.get_frame_height() - frame_width = self.get_frame_width() - width_mult = pixel_width / frame_width - width_add = pixel_width / 2 - height_mult = pixel_height / frame_height - height_add = pixel_height / 2 - # Flip on y-axis as you go - height_mult *= -1 - - result[:, 0] = shifted_points[:, 0] * width_mult + width_add - result[:, 1] = shifted_points[:, 1] * height_mult + height_add - return result.astype('int') - - def on_screen_pixels(self, pixel_coords): - return reduce(op.and_, [ - pixel_coords[:, 0] >= 0, - pixel_coords[:, 0] < self.get_pixel_width(), - pixel_coords[:, 1] >= 0, - pixel_coords[:, 1] < self.get_pixel_height(), - ]) - - def adjusted_thickness(self, thickness): - # TODO: This seems...unsystematic - big_sum = op.add( - PRODUCTION_QUALITY_CAMERA_CONFIG["pixel_height"], - PRODUCTION_QUALITY_CAMERA_CONFIG["pixel_width"], - ) - this_sum = op.add( - self.get_pixel_height(), - self.get_pixel_width(), - ) - factor = fdiv(big_sum, this_sum) - return 1 + (thickness - 1) / factor - - def get_thickening_nudges(self, thickness): - thickness = int(thickness) - _range = list(range(-thickness // 2 + 1, thickness // 2 + 1)) - return np.array(list(it.product(_range, _range))) - - def thickened_coordinates(self, pixel_coords, thickness): - nudges = self.get_thickening_nudges(thickness) - pixel_coords = np.array([ - pixel_coords + nudge - for nudge in nudges - ]) - size = pixel_coords.size - return pixel_coords.reshape((size // 2, 2)) - - # TODO, reimplement using cairo matrix - def get_coords_of_all_pixels(self): - # These are in x, y order, to help me keep things straight - full_space_dims = np.array([ - self.get_frame_width(), - self.get_frame_height() - ]) - full_pixel_dims = np.array([ - self.get_pixel_width(), - self.get_pixel_height() - ]) - - # These are addressed in the same y, x order as in pixel_array, but the values in them - # are listed in x, y order - uncentered_pixel_coords = np.indices( - [self.get_pixel_height(), self.get_pixel_width()] - )[::-1].transpose(1, 2, 0) - uncentered_space_coords = fdiv( - uncentered_pixel_coords * full_space_dims, - full_pixel_dims) - # Could structure above line's computation slightly differently, but figured (without much - # thought) multiplying by frame_shape first, THEN dividing by pixel_shape, is probably - # better than the other order, for avoiding underflow quantization in the division (whereas - # overflow is unlikely to be a problem) - - centered_space_coords = ( - uncentered_space_coords - fdiv(full_space_dims, 2) - ) - - # Have to also flip the y coordinates to account for pixel array being listed in - # top-to-bottom order, opposite of screen coordinate convention - centered_space_coords = centered_space_coords * (1, -1) - - return centered_space_coords - - -class BackgroundColoredVMobjectDisplayer(object): - def __init__(self, camera): - self.camera = camera - self.file_name_to_pixel_array_map = {} - self.pixel_array = np.array(camera.get_pixel_array()) - self.reset_pixel_array() - - def reset_pixel_array(self): - self.pixel_array[:, :] = 0 - - def resize_background_array( - self, background_array, - new_width, new_height, - mode="RGBA" - ): - image = Image.fromarray(background_array) - image = image.convert(mode) - resized_image = image.resize((new_width, new_height)) - return np.array(resized_image) - - def resize_background_array_to_match(self, background_array, pixel_array): - height, width = pixel_array.shape[:2] - mode = "RGBA" if pixel_array.shape[2] == 4 else "RGB" - return self.resize_background_array(background_array, width, height, mode) - - def get_background_array(self, file_name): - if file_name in self.file_name_to_pixel_array_map: - return self.file_name_to_pixel_array_map[file_name] - full_path = get_full_raster_image_path(file_name) - image = Image.open(full_path) - back_array = np.array(image) - - pixel_array = self.pixel_array - if not np.all(pixel_array.shape == back_array.shape): - back_array = self.resize_background_array_to_match( - back_array, pixel_array + return { + "vbo": vbo, + "ibo": ibo, + "vao": vao, + "prog": shader_program, + "shader_wrapper": shader_wrapper, + "single_use": single_use, + } + + def release_render_group(self, render_group): + for key in ["vbo", "ibo", "vao"]: + if render_group[key] is not None: + render_group[key].release() + + def set_mobjects_as_static(self, *mobjects): + # Creates buffer and array objects holding each mobjects shader data + for mob in mobjects: + self.static_mobject_to_render_group_list[id(mob)] = [ + self.get_render_group(sw, single_use=False) + for sw in mob.get_shader_wrapper_list() + ] + + def release_static_mobjects(self): + for rg_list in self.static_mobject_to_render_group_list.values(): + for render_group in rg_list: + self.release_render_group(render_group) + self.static_mobject_to_render_group_list = {} + + # Shaders + def init_shaders(self): + # Initialize with the null id going to None + self.id_to_shader_program = {"": None} + + def get_shader_program(self, shader_wrapper): + sid = shader_wrapper.get_program_id() + if sid not in self.id_to_shader_program: + # Create shader program for the first time, then cache + # in the id_to_shader_program dictionary + program = self.ctx.program(**shader_wrapper.get_program_code()) + vert_format = moderngl.detect_format(program, shader_wrapper.vert_attributes) + self.id_to_shader_program[sid] = (program, vert_format) + return self.id_to_shader_program[sid] + + def set_shader_uniforms(self, shader, shader_wrapper): + for name, path in shader_wrapper.texture_paths.items(): + tid = self.get_texture_id(path) + shader[name].value = tid + for name, value in it.chain(shader_wrapper.uniforms.items(), self.perspective_uniforms.items()): + try: + shader[name].value = value + except KeyError: + pass + + def refresh_perspective_uniforms(self): + pw, ph = self.get_pixel_shape() + fw, fh = self.frame.get_shape() + # TODO, this should probably be a mobject uniform, with + # the camera taking care of the conversion factor + anti_alias_width = self.anti_alias_width / (ph / fh) + transform = self.frame.get_inverse_camera_position_matrix() + light = self.light_source.get_location() + transformed_light = np.dot(transform, [*light, 1])[:3] + self.perspective_uniforms = { + 'to_screen_space': tuple(transform.T.flatten()), + 'frame_shape': self.frame.get_shape(), + 'focal_distance': self.frame.get_focal_distance(), + 'anti_alias_width': anti_alias_width, + 'light_source_position': tuple(transformed_light), + } + + def init_textures(self): + self.path_to_texture_id = {} + + def get_texture_id(self, path): + if path not in self.path_to_texture_id: + # A way to increase tid's sequentially + tid = len(self.path_to_texture_id) + im = Image.open(path) + texture = self.ctx.texture( + size=im.size, + components=len(im.getbands()), + data=im.tobytes(), ) + texture.use(location=tid) + self.path_to_texture_id[path] = tid + return self.path_to_texture_id[path] - self.file_name_to_pixel_array_map[file_name] = back_array - return back_array - def display(self, *cvmobjects): - batch_image_file_pairs = batch_by_property( - cvmobjects, lambda cv: cv.get_background_image_file() - ) - curr_array = None - for batch, image_file in batch_image_file_pairs: - background_array = self.get_background_array(image_file) - pixel_array = self.pixel_array - self.camera.display_multiple_non_background_colored_vmobjects( - batch, pixel_array - ) - new_array = np.array( - (background_array * pixel_array.astype('float') / 255), - dtype=self.camera.pixel_array_dtype - ) - if curr_array is None: - curr_array = new_array - else: - curr_array = np.maximum(curr_array, new_array) - self.reset_pixel_array() - return curr_array +# Mostly just defined so old scenes don't break +class ThreeDCamera(Camera): + CONFIG = { + "samples": 8, + } diff --git a/manimlib/camera/mapping_camera.py b/manimlib/camera/mapping_camera.py index c80c91cfec..a143459d02 100644 --- a/manimlib/camera/mapping_camera.py +++ b/manimlib/camera/mapping_camera.py @@ -9,6 +9,7 @@ # map their centers but remain otherwise undistorted (useful for labels, etc.) +# TODO, this class is deprecated class MappingCamera(Camera): CONFIG = { "mapping_func": lambda p: p, @@ -31,7 +32,6 @@ def capture_mobjects(self, mobjects, **kwargs): mobject.insert_n_curves(self.min_num_curves) Camera.capture_mobjects( self, mobject_copies, - include_submobjects=False, excluded_mobjects=None, ) @@ -104,7 +104,7 @@ def __init__(self, left_camera, right_camera, **kwargs): half_width = self.get_pixel_width() / 2 for camera in [self.left_camera, self.right_camera]: # TODO: Round up on one if width is odd - camera.reset_pixel_shape(camera.get_pixel_height(), half_width) + camera.reset_pixel_shape(half_width, camera.get_pixel_height()) OldMultiCamera.__init__( self, diff --git a/manimlib/camera/moving_camera.py b/manimlib/camera/moving_camera.py index c54a1e6b2b..74c83a4ce1 100644 --- a/manimlib/camera/moving_camera.py +++ b/manimlib/camera/moving_camera.py @@ -8,18 +8,7 @@ from manimlib.utils.config_ops import digest_config -# TODO, think about how to incorporate perspective -class CameraFrame(VGroup): - CONFIG = { - "width": FRAME_WIDTH, - "height": FRAME_HEIGHT, - "center": ORIGIN, - } - - def __init__(self, **kwargs): - pass - - +# Depricated? class MovingCamera(Camera): """ Stays in line with the height, width and position of it's 'frame', which is a Rectangle @@ -65,11 +54,6 @@ def set_frame_width(self, frame_width): def set_frame_center(self, frame_center): self.frame.move_to(frame_center) - def capture_mobjects(self, mobjects, **kwargs): - # self.reset_frame_center() - # self.realign_frame_shape() - Camera.capture_mobjects(self, mobjects, **kwargs) - # Since the frame can be moving around, the cairo # context used for updating should be regenerated # at each frame. So no caching. diff --git a/manimlib/camera/multi_camera.py b/manimlib/camera/multi_camera.py index 9283b0e79a..920b7da366 100644 --- a/manimlib/camera/multi_camera.py +++ b/manimlib/camera/multi_camera.py @@ -29,8 +29,8 @@ def update_sub_cameras(self): imfc.camera.frame.get_width(), ) imfc.camera.reset_pixel_shape( - int(pixel_height * imfc.get_height() / self.get_frame_height()), int(pixel_width * imfc.get_width() / self.get_frame_width()), + int(pixel_height * imfc.get_height() / self.get_frame_height()), ) def reset(self): diff --git a/manimlib/camera/three_d_camera.py b/manimlib/camera/three_d_camera.py deleted file mode 100644 index 5cf4ba0d10..0000000000 --- a/manimlib/camera/three_d_camera.py +++ /dev/null @@ -1,232 +0,0 @@ -import numpy as np - -from manimlib.camera.camera import Camera -from manimlib.constants import * -from manimlib.mobject.three_d_utils import get_3d_vmob_end_corner -from manimlib.mobject.three_d_utils import get_3d_vmob_end_corner_unit_normal -from manimlib.mobject.three_d_utils import get_3d_vmob_start_corner -from manimlib.mobject.three_d_utils import get_3d_vmob_start_corner_unit_normal -from manimlib.mobject.types.point_cloud_mobject import Point -from manimlib.mobject.value_tracker import ValueTracker -from manimlib.utils.color import get_shaded_rgb -from manimlib.utils.simple_functions import clip_in_place -from manimlib.utils.space_ops import rotation_about_z -from manimlib.utils.space_ops import rotation_matrix - - -class ThreeDCamera(Camera): - CONFIG = { - "shading_factor": 0.2, - "distance": 20.0, - "default_distance": 5.0, - "phi": 0, # Angle off z axis - "theta": -90 * DEGREES, # Rotation about z axis - "gamma": 0, # Rotation about normal vector to camera - "light_source_start_point": 9 * DOWN + 7 * LEFT + 10 * OUT, - "frame_center": ORIGIN, - "should_apply_shading": True, - "exponential_projection": False, - "max_allowable_norm": 3 * FRAME_WIDTH, - } - - def __init__(self, *args, **kwargs): - Camera.__init__(self, *args, **kwargs) - self.phi_tracker = ValueTracker(self.phi) - self.theta_tracker = ValueTracker(self.theta) - self.distance_tracker = ValueTracker(self.distance) - self.gamma_tracker = ValueTracker(self.gamma) - self.light_source = Point(self.light_source_start_point) - self.frame_center = Point(self.frame_center) - self.fixed_orientation_mobjects = dict() - self.fixed_in_frame_mobjects = set() - self.reset_rotation_matrix() - - def capture_mobjects(self, mobjects, **kwargs): - self.reset_rotation_matrix() - Camera.capture_mobjects(self, mobjects, **kwargs) - - def get_value_trackers(self): - return [ - self.phi_tracker, - self.theta_tracker, - self.distance_tracker, - self.gamma_tracker, - ] - - def modified_rgbas(self, vmobject, rgbas): - if not self.should_apply_shading: - return rgbas - if vmobject.shade_in_3d and (vmobject.get_num_points() > 0): - light_source_point = self.light_source.points[0] - if len(rgbas) < 2: - shaded_rgbas = rgbas.repeat(2, axis=0) - else: - shaded_rgbas = np.array(rgbas[:2]) - shaded_rgbas[0, :3] = get_shaded_rgb( - shaded_rgbas[0, :3], - get_3d_vmob_start_corner(vmobject), - get_3d_vmob_start_corner_unit_normal(vmobject), - light_source_point, - ) - shaded_rgbas[1, :3] = get_shaded_rgb( - shaded_rgbas[1, :3], - get_3d_vmob_end_corner(vmobject), - get_3d_vmob_end_corner_unit_normal(vmobject), - light_source_point, - ) - return shaded_rgbas - return rgbas - - def get_stroke_rgbas(self, vmobject, background=False): - return self.modified_rgbas( - vmobject, vmobject.get_stroke_rgbas(background) - ) - - def get_fill_rgbas(self, vmobject): - return self.modified_rgbas( - vmobject, vmobject.get_fill_rgbas() - ) - - def get_mobjects_to_display(self, *args, **kwargs): - mobjects = Camera.get_mobjects_to_display( - self, *args, **kwargs - ) - rot_matrix = self.get_rotation_matrix() - - def z_key(mob): - if not (hasattr(mob, "shade_in_3d") and mob.shade_in_3d): - return np.inf - # Assign a number to a three dimensional mobjects - # based on how close it is to the camera - return np.dot( - mob.get_z_index_reference_point(), - rot_matrix.T - )[2] - return sorted(mobjects, key=z_key) - - def get_phi(self): - return self.phi_tracker.get_value() - - def get_theta(self): - return self.theta_tracker.get_value() - - def get_distance(self): - return self.distance_tracker.get_value() - - def get_gamma(self): - return self.gamma_tracker.get_value() - - def get_frame_center(self): - return self.frame_center.points[0] - - def set_phi(self, value): - self.phi_tracker.set_value(value) - - def set_theta(self, value): - self.theta_tracker.set_value(value) - - def set_distance(self, value): - self.distance_tracker.set_value(value) - - def set_gamma(self, value): - self.gamma_tracker.set_value(value) - - def set_frame_center(self, point): - self.frame_center.move_to(point) - - def reset_rotation_matrix(self): - self.rotation_matrix = self.generate_rotation_matrix() - - def get_rotation_matrix(self): - return self.rotation_matrix - - def generate_rotation_matrix(self): - phi = self.get_phi() - theta = self.get_theta() - gamma = self.get_gamma() - matrices = [ - rotation_about_z(-theta - 90 * DEGREES), - rotation_matrix(-phi, RIGHT), - rotation_about_z(gamma), - ] - result = np.identity(3) - for matrix in matrices: - result = np.dot(matrix, result) - return result - - def project_points(self, points): - frame_center = self.get_frame_center() - distance = self.get_distance() - rot_matrix = self.get_rotation_matrix() - - points = points - frame_center - points = np.dot(points, rot_matrix.T) - zs = points[:, 2] - for i in 0, 1: - if self.exponential_projection: - # Proper projedtion would involve multiplying - # x and y by d / (d-z). But for points with high - # z value that causes weird artifacts, and applying - # the exponential helps smooth it out. - factor = np.exp(zs / distance) - lt0 = zs < 0 - factor[lt0] = (distance / (distance - zs[lt0])) - else: - factor = (distance / (distance - zs)) - factor[(distance - zs) < 0] = 10**6 - # clip_in_place(factor, 0, 10**6) - points[:, i] *= factor - points = points + frame_center - return points - - def project_point(self, point): - return self.project_points(point.reshape((1, 3)))[0, :] - - def transform_points_pre_display(self, mobject, points): - points = super().transform_points_pre_display(mobject, points) - fixed_orientation = mobject in self.fixed_orientation_mobjects - fixed_in_frame = mobject in self.fixed_in_frame_mobjects - - if fixed_in_frame: - return points - if fixed_orientation: - center_func = self.fixed_orientation_mobjects[mobject] - center = center_func() - new_center = self.project_point(center) - return points + (new_center - center) - else: - return self.project_points(points) - - def add_fixed_orientation_mobjects( - self, *mobjects, - use_static_center_func=False, - center_func=None): - # This prevents the computation of mobject.get_center - # every single time a projetion happens - def get_static_center_func(mobject): - point = mobject.get_center() - return (lambda: point) - - for mobject in mobjects: - if center_func: - func = center_func - elif use_static_center_func: - func = get_static_center_func(mobject) - else: - func = mobject.get_center - for submob in mobject.get_family(): - self.fixed_orientation_mobjects[submob] = func - - def add_fixed_in_frame_mobjects(self, *mobjects): - for mobject in self.extract_mobject_family_members(mobjects): - self.fixed_in_frame_mobjects.add(mobject) - - def remove_fixed_orientation_mobjects(self, *mobjects): - for mobject in self.extract_mobject_family_members(mobjects): - if mobject in self.fixed_orientation_mobjects: - self.fixed_orientation_mobjects.remove(mobject) - - def remove_fixed_in_frame_mobjects(self, *mobjects): - for mobject in self.extract_mobject_family_members(mobjects): - if mobject in self.fixed_in_frame_mobjects: - self.fixed_in_frame_mobjects.remove(mobject) diff --git a/manimlib/config.py b/manimlib/config.py index 7518d14d73..cf7eb7efd8 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -28,12 +28,12 @@ def parse_cli(): help="Automatically open the saved file once its done", ), parser.add_argument( - "-w", "--write_to_movie", + "-w", "--write_file", action="store_true", help="Render the scene as a movie file", ), parser.add_argument( - "-s", "--save_last_frame", + "-s", "--skip_animations", action="store_true", help="Save the last frame", ), @@ -83,9 +83,13 @@ def parse_cli(): help="Write all the scenes from a file", ), parser.add_argument( - "-o", "--file_name", - help="Specify the name of the output file, if" - "it should be different from the scene class name", + "-o", "--open", + action="store_true", + help="Automatically open the saved file once its done", + ) + parser.add_argument( + "--file_name", + help="Name for the movie or image file", ) parser.add_argument( "-n", "--start_at_animation_number", @@ -102,11 +106,6 @@ def parse_cli(): "-c", "--color", help="Background color", ) - parser.add_argument( - "--sound", - action="store_true", - help="Play a success/failure sound", - ) parser.add_argument( "--leave_progress_bars", action="store_true", @@ -183,10 +182,17 @@ def get_module(file_name): def get_configuration(args): module = get_module(args.file) + + write_file = any([ + args.write_file, + args.open, + args.show_file_in_finder, + ]) + file_writer_config = { # By default, write to file - "write_to_movie": args.write_to_movie or not args.save_last_frame, - "save_last_frame": args.save_last_frame, + "write_to_movie": not args.skip_animations and write_file, + "save_last_frame": args.skip_animations and write_file, "save_pngs": args.save_pngs, "save_as_gif": args.save_as_gif, # If -t is passed in (for transparent), this will be RGBA @@ -194,21 +200,27 @@ def get_configuration(args): "movie_file_extension": ".mov" if args.transparent else ".mp4", "file_name": args.file_name, "input_file_path": args.file, + "open_file_upon_completion": args.open, + "show_file_location_upon_completion": args.show_file_in_finder, + "quiet": args.quiet, } if hasattr(module, "OUTPUT_DIRECTORY"): file_writer_config["output_directory"] = module.OUTPUT_DIRECTORY + + # If preview wasn't set, but there is no filewriting, preview anyway + # so that the user sees something + if not (args.preview or write_file): + args.preview = True + config = { "module": module, "scene_names": args.scene_names, - "open_video_upon_completion": args.preview, - "show_file_in_finder": args.show_file_in_finder, + "preview": args.preview, "file_writer_config": file_writer_config, "quiet": args.quiet or args.write_all, - "ignore_waits": args.preview, "write_all": args.write_all, "start_at_animation_number": args.start_at_animation_number, "end_at_animation_number": None, - "sound": args.sound, "leave_progress_bars": args.leave_progress_bars, "media_dir": args.media_dir, "video_dir": args.video_dir, @@ -218,6 +230,12 @@ def get_configuration(args): # Camera configuration config["camera_config"] = get_camera_configuration(args) + config["window_config"] = { + "size": ( + config["camera_config"]["pixel_width"], + config["camera_config"]["pixel_height"], + ) + } # Arguments related to skipping stan = config["start_at_animation_number"] @@ -230,8 +248,8 @@ def get_configuration(args): config["start_at_animation_number"] = int(stan) config["skip_animations"] = any([ - file_writer_config["save_last_frame"], - config["start_at_animation_number"], + args.skip_animations, + args.start_at_animation_number, ]) return config @@ -244,7 +262,9 @@ def get_camera_configuration(args): camera_config.update(manimlib.constants.MEDIUM_QUALITY_CAMERA_CONFIG) elif args.high_quality: camera_config.update(manimlib.constants.HIGH_QUALITY_CAMERA_CONFIG) - else: + elif args.preview: # Without a quality specified, preview at medium quality + camera_config.update(manimlib.constants.MEDIUM_QUALITY_CAMERA_CONFIG) + else: # Without anything specified, render to production quality camera_config.update(manimlib.constants.PRODUCTION_QUALITY_CAMERA_CONFIG) # If the resolution was passed in via -r diff --git a/manimlib/constants.py b/manimlib/constants.py index 82d2298df8..d6dd1770bb 100644 --- a/manimlib/constants.py +++ b/manimlib/constants.py @@ -6,6 +6,7 @@ VIDEO_OUTPUT_DIR = "" TEX_DIR = "" TEXT_DIR = "" +MOBJECT_POINTS_DIR = "" def initialize_directories(config): @@ -14,6 +15,7 @@ def initialize_directories(config): global VIDEO_OUTPUT_DIR global TEX_DIR global TEXT_DIR + global MOBJECT_POINTS_DIR video_path_specified = config["video_dir"] or config["video_output_dir"] @@ -40,6 +42,7 @@ def initialize_directories(config): TEX_DIR = config["tex_dir"] or os.path.join(MEDIA_DIR, "Tex") TEXT_DIR = os.path.join(MEDIA_DIR, "texts") + MOBJECT_POINTS_DIR = os.path.join(MEDIA_DIR, "mobject_points") if not video_path_specified: VIDEO_DIR = os.path.join(MEDIA_DIR, "videos") VIDEO_OUTPUT_DIR = os.path.join(MEDIA_DIR, "videos") @@ -48,11 +51,12 @@ def initialize_directories(config): else: VIDEO_DIR = config["video_dir"] - for folder in [VIDEO_DIR, VIDEO_OUTPUT_DIR, TEX_DIR, TEXT_DIR]: + for folder in [VIDEO_DIR, VIDEO_OUTPUT_DIR, TEX_DIR, TEXT_DIR, MOBJECT_POINTS_DIR]: if folder != "" and not os.path.exists(folder): os.makedirs(folder) -NOT_SETTING_FONT_MSG=''' + +NOT_SETTING_FONT_MSG = ''' Warning: You haven't set font. If you are not using English, this may cause text rendering problem. @@ -84,6 +88,12 @@ class MyText(Text): "\\begin{align*}\n" + TEX_TEXT_TO_REPLACE + "\n\\end{align*}", ) + +SHADER_DIR = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + "shaders" +) + HELP_MESSAGE = """ Usage: python extract_scene.py [] @@ -143,9 +153,6 @@ class MyText(Text): DEFAULT_PIXEL_WIDTH = PRODUCTION_QUALITY_CAMERA_CONFIG["pixel_width"] DEFAULT_FRAME_RATE = 60 -DEFAULT_POINT_DENSITY_2D = 25 -DEFAULT_POINT_DENSITY_1D = 250 - DEFAULT_STROKE_WIDTH = 4 FRAME_HEIGHT = 8.0 @@ -242,6 +249,12 @@ class MyText(Text): "PURPLE_A": "#CAA3E8", "WHITE": "#FFFFFF", "BLACK": "#000000", + "GREY_A": "#DDDDDD", + "GREY_B": "#BBBBBB", + "GREY_C": "#888888", + "GREY_D": "#444444", + "GREY_E": "#222222", + # TODO, remove these greys "LIGHT_GRAY": "#BBBBBB", "LIGHT_GREY": "#BBBBBB", "GRAY": "#888888", diff --git a/manimlib/extract_scene.py b/manimlib/extract_scene.py index 7185f6eb56..a70c7cffdc 100644 --- a/manimlib/extract_scene.py +++ b/manimlib/extract_scene.py @@ -1,62 +1,11 @@ import inspect import itertools as it -import os -import platform -import subprocess as sp import sys -import traceback from manimlib.scene.scene import Scene -from manimlib.utils.sounds import play_error_sound -from manimlib.utils.sounds import play_finish_sound import manimlib.constants -def open_file_if_needed(file_writer, **config): - if config["quiet"]: - curr_stdout = sys.stdout - sys.stdout = open(os.devnull, "w") - - open_file = any([ - config["open_video_upon_completion"], - config["show_file_in_finder"] - ]) - if open_file: - current_os = platform.system() - file_paths = [] - - if config["file_writer_config"]["save_last_frame"]: - file_paths.append(file_writer.get_image_file_path()) - if config["file_writer_config"]["write_to_movie"]: - file_paths.append(file_writer.get_movie_file_path()) - - for file_path in file_paths: - if current_os == "Windows": - os.startfile(file_path) - else: - commands = [] - if current_os == "Linux": - commands.append("xdg-open") - elif current_os.startswith("CYGWIN"): - commands.append("cygstart") - else: # Assume macOS - commands.append("open") - - if config["show_file_in_finder"]: - commands.append("-R") - - commands.append(file_path) - - # commands.append("-g") - FNULL = open(os.devnull, 'w') - sp.call(commands, stdout=FNULL, stderr=sp.STDOUT) - FNULL.close() - - if config["quiet"]: - sys.stdout.close() - sys.stdout = curr_stdout - - def is_child_scene(obj, module): if not inspect.isclass(obj): return False @@ -97,14 +46,31 @@ def get_scenes_to_render(scene_classes, config): if len(scene_classes) == 0: print(manimlib.constants.NO_SCENE_MESSAGE) return [] + + scene_kwargs = dict([ + (key, config[key]) + for key in [ + "window_config", + "camera_config", + "file_writer_config", + "skip_animations", + "start_at_animation_number", + "end_at_animation_number", + "leave_progress_bars", + "preview", + ] + ]) + if config["write_all"]: - return scene_classes + return [sc(**scene_kwargs) for sc in scene_classes] + result = [] for scene_name in config["scene_names"]: found = False for scene_class in scene_classes: if scene_class.__name__ == scene_name: - result.append(scene_class) + scene = scene_class(**scene_kwargs) + result.append(scene) found = True break if not found and (scene_name != ""): @@ -135,33 +101,8 @@ def get_scene_classes_from_module(module): def main(config): module = config["module"] all_scene_classes = get_scene_classes_from_module(module) - scene_classes_to_render = get_scenes_to_render(all_scene_classes, config) - - scene_kwargs = dict([ - (key, config[key]) - for key in [ - "camera_config", - "file_writer_config", - "skip_animations", - "start_at_animation_number", - "end_at_animation_number", - "leave_progress_bars", - ] - ]) - - for SceneClass in scene_classes_to_render: - try: - # By invoking, this renders the full scene - scene = SceneClass(**scene_kwargs) - open_file_if_needed(scene.file_writer, **config) - if config["sound"]: - play_finish_sound() - except Exception: - print("\n\n") - traceback.print_exc() - print("\n\n") - if config["sound"]: - play_error_sound() + scenes = get_scenes_to_render(all_scene_classes, config) + return scenes if __name__ == "__main__": diff --git a/manimlib/for_3b1b_videos/common_scenes.py b/manimlib/for_3b1b_videos/common_scenes.py index da83fbb470..d32d8a84f1 100644 --- a/manimlib/for_3b1b_videos/common_scenes.py +++ b/manimlib/for_3b1b_videos/common_scenes.py @@ -1,29 +1,24 @@ import random -from manimlib.animation.composition import LaggedStartMap -from manimlib.animation.creation import DrawBorderThenFill +from manimlib.animation.animation import Animation +from manimlib.animation.composition import Succession from manimlib.animation.creation import Write from manimlib.animation.fading import FadeIn -from manimlib.animation.fading import FadeOut +from manimlib.animation.transform import ApplyMethod from manimlib.constants import * from manimlib.for_3b1b_videos.pi_creature import Mortimer from manimlib.for_3b1b_videos.pi_creature import Randolph -from manimlib.for_3b1b_videos.pi_creature_animations import Blink -from manimlib.for_3b1b_videos.pi_creature_scene import PiCreatureScene +from manimlib.mobject.mobject import Mobject from manimlib.mobject.geometry import DashedLine from manimlib.mobject.geometry import Line from manimlib.mobject.geometry import Rectangle from manimlib.mobject.geometry import Square from manimlib.mobject.svg.drawings import Logo -from manimlib.mobject.svg.drawings import PatreonLogo from manimlib.mobject.svg.tex_mobject import TextMobject from manimlib.mobject.types.vectorized_mobject import VGroup -from manimlib.mobject.mobject_update_utils import always_shift from manimlib.scene.moving_camera_scene import MovingCameraScene from manimlib.scene.scene import Scene from manimlib.utils.rate_functions import linear -from manimlib.utils.space_ops import get_norm -from manimlib.utils.space_ops import normalize class OpeningQuote(Scene): @@ -88,105 +83,34 @@ def get_author(self, quote): return author -class PatreonThanks(Scene): +class PatreonEndScreen(Scene): CONFIG = { - "specific_patrons": [], "max_patron_group_size": 20, "patron_scale_val": 0.8, - } - - def construct(self): - morty = Mortimer() - morty.next_to(ORIGIN, DOWN) - - patreon_logo = PatreonLogo() - patreon_logo.to_edge(UP) - - patrons = list(map(TextMobject, self.specific_patronds)) - num_groups = float(len(patrons)) / self.max_patron_group_size - proportion_range = np.linspace(0, 1, num_groups + 1) - indices = (len(patrons) * proportion_range).astype('int') - patron_groups = [ - VGroup(*patrons[i:j]) - for i, j in zip(indices, indices[1:]) - ] - - for i, group in enumerate(patron_groups): - left_group = VGroup(*group[:len(group) / 2]) - right_group = VGroup(*group[len(group) / 2:]) - for subgroup, vect in (left_group, LEFT), (right_group, RIGHT): - subgroup.arrange(DOWN, aligned_edge=LEFT) - subgroup.scale(self.patron_scale_val) - subgroup.to_edge(vect) - - last_group = None - for i, group in enumerate(patron_groups): - anims = [] - if last_group is not None: - self.play( - FadeOut(last_group), - morty.look, UP + LEFT - ) - else: - anims += [ - DrawBorderThenFill(patreon_logo), - ] - self.play( - LaggedStartMap( - FadeIn, group, - run_time=2, - ), - morty.change, "gracious", group.get_corner(UP + LEFT), - *anims - ) - self.play(morty.look_at, group.get_corner(DOWN + LEFT)) - self.play(morty.look_at, group.get_corner(UP + RIGHT)) - self.play(morty.look_at, group.get_corner(DOWN + RIGHT)) - self.play(Blink(morty)) - last_group = group - - -class PatreonEndScreen(PatreonThanks, PiCreatureScene): - CONFIG = { "n_patron_columns": 4, "max_patron_width": 5, - "run_time": 20, - "randomize_order": True, + "randomize_order": False, "capitalize": True, "name_y_spacing": 0.6, - "thanks_words": "Find value in this? Join me in thanking these patrons:", + "thanks_words": "Many thanks to this channel's supporters", + "scroll_time": 20, } def construct(self): - if self.randomize_order: - random.shuffle(self.specific_patrons) - if self.capitalize: - self.specific_patrons = [ - " ".join(map( - lambda s: s.capitalize(), - patron.split(" ") - )) - for patron in self.specific_patrons - ] - - # self.add_title() - self.scroll_through_patrons() - - def create_pi_creatures(self): + # Add title title = self.title = TextMobject("Clicky Stuffs") title.scale(1.5) title.to_edge(UP, buff=MED_SMALL_BUFF) - randy, morty = self.pi_creatures = VGroup(Randolph(), Mortimer()) - for pi, vect in (randy, LEFT), (morty, RIGHT): + pi_creatures = VGroup(Randolph(), Mortimer()) + for pi, vect in zip(pi_creatures, [LEFT, RIGHT]): pi.set_height(title.get_height()) pi.change_mode("thinking") pi.look(DOWN) pi.next_to(title, vect, buff=MED_LARGE_BUFF) - self.add_foreground_mobjects(title, randy, morty) - return self.pi_creatures + self.add(title, pi_creatures) - def scroll_through_patrons(self): + # Set the top of the screen logo_box = Square(side_length=2.5) logo_box.to_corner(DOWN + LEFT, buff=MED_LARGE_BUFF) @@ -202,6 +126,7 @@ def scroll_through_patrons(self): line = DashedLine(FRAME_X_RADIUS * LEFT, FRAME_X_RADIUS * RIGHT) line.move_to(ORIGIN) + # Add thanks thanks = TextMobject(self.thanks_words) thanks.scale(0.9) thanks.next_to(black_rect.get_bottom(), UP, SMALL_BUFF) @@ -212,21 +137,25 @@ def scroll_through_patrons(self): underline.next_to(thanks, DOWN, SMALL_BUFF) thanks.add(underline) - changed_patron_names = list(map( - self.modify_patron_name, - self.specific_patrons, - )) - changed_patron_names.sort() - patrons = VGroup(*map( - TextMobject, - changed_patron_names, - )) - patrons.scale(self.patron_scale_val) - for patron in patrons: - if patron.get_width() > self.max_patron_width: - patron.set_width(self.max_patron_width) + # Build name list + with open("manimlib/files/patrons.txt", 'r') as fp: + names = [ + self.modify_patron_name(name.strip()) + for name in fp.readlines() + ] + + if self.randomize_order: + random.shuffle(names) + else: + names.sort() + + name_labels = VGroup(*map(TextMobject, names)) + name_labels.scale(self.patron_scale_val) + for label in name_labels: + if label.get_width() > self.max_patron_width: + label.set_width(self.max_patron_width) columns = VGroup(*[ - VGroup(*patrons[i::self.n_patron_columns]) + VGroup(*name_labels[i::self.n_patron_columns]) for i in range(self.n_patron_columns) ]) column_x_spacing = 0.5 + max([c.get_width() for c in columns]) @@ -242,22 +171,31 @@ def scroll_through_patrons(self): if columns.get_width() > max_width: columns.set_width(max_width) underline.match_width(columns) - # thanks.to_edge(RIGHT, buff=MED_SMALL_BUFF) - columns.next_to(underline, DOWN, buff=4) + columns.next_to(underline, DOWN, buff=3) + # Set movement columns.generate_target() - columns.target.to_edge(DOWN, buff=4) - vect = columns.target.get_center() - columns.get_center() - distance = get_norm(vect) - wait_time = 20 - always_shift( - columns, - direction=normalize(vect), - rate=(distance / wait_time) + distance = columns.get_height() + 2 + wait_time = self.scroll_time + frame = self.camera.frame + frame_shift = ApplyMethod( + frame.shift, distance * DOWN, + run_time=wait_time, + rate_func=linear, ) + blink_anims = [] + blank_mob = Mobject() + for x in range(wait_time): + if random.random() < 0.25: + blink_anims.append(Blink(random.choice(pi_creatures))) + else: + blink_anims.append(Animation(blank_mob)) + blinks = Succession(*blink_anims) - self.add(columns, black_rect, line, thanks) - self.wait(wait_time) + static_group = VGroup(black_rect, line, thanks, pi_creatures, title) + static_group.fix_in_frame() + self.add(columns, static_group) + self.play(frame_shift, blinks) def modify_patron_name(self, name): modification_map = { @@ -265,10 +203,19 @@ def modify_patron_name(self, name): "DeathByShrimp": "Henry Bresnahan", "akostrikov": "Aleksandr Kostrikov", "Jacob Baxter": "Will Fleshman", + "Sansword Huang": "SansWord@TW", + "Sunil Nagaraj": "Ubiquity Ventures", + "Nitu Kitchloo": "Ish Kitchloo", + "PedroTristão": "Tristan", } for n1, n2 in modification_map.items(): if name.lower() == n1.lower(): - return n2 + name = n2 + if self.capitalize: + name = " ".join(map( + lambda s: s.capitalize(), + name.split(" ") + )) return name @@ -342,6 +289,7 @@ def construct(self): pis.set_height(self.pi_height) pis.arrange(RIGHT, aligned_edge=DOWN) pis.move_to(self.pi_bottom, DOWN) + self.pis = pis self.add(pis) if self.use_date: diff --git a/manimlib/for_3b1b_videos/pi_creature.py b/manimlib/for_3b1b_videos/pi_creature.py index 4e02646341..4d02ebfe45 100644 --- a/manimlib/for_3b1b_videos/pi_creature.py +++ b/manimlib/for_3b1b_videos/pi_creature.py @@ -57,23 +57,23 @@ def __init__(self, mode="plain", **kwargs): try: svg_file = os.path.join( PI_CREATURE_DIR, - "%s_%s.svg" % (self.file_name_prefix, mode) + f"{self.file_name_prefix}_{mode}.svg" ) - SVGMobject.__init__(self, file_name=svg_file, **kwargs) except Exception: - warnings.warn("No %s design with mode %s" % - (self.file_name_prefix, mode)) - # TODO, this needs to change to a different, better directory + warnings.warn(f"No {self.file_name_prefix} design with mode {mode}") svg_file = os.path.join( - FILE_DIR, + os.path.dirname(os.path.realpath(__file__)), + os.pardir, + "files", "PiCreatures_plain.svg", ) - SVGMobject.__init__(self, mode="plain", file_name=svg_file, **kwargs) + SVGMobject.__init__(self, file_name=svg_file, **kwargs) if self.flip_at_start: self.flip() if self.start_corner is not None: self.to_corner(self.start_corner) + self.unlock_triangulation() def align_data(self, mobject): # This ensures that after a transform into a different mode, @@ -132,8 +132,8 @@ def init_pupils(self): new_pupil.move_to(pupil) pupil.become(new_pupil) dot.shift( - new_pupil.get_boundary_point(UL) - - dot.get_boundary_point(UL) + new_pupil.point_from_proportion(3 / 8) - + dot.point_from_proportion(3 / 8) ) pupil.add(dot) @@ -148,9 +148,7 @@ def set_color(self, color): return self def change_mode(self, mode): - new_self = self.__class__( - mode=mode, - ) + new_self = self.__class__(mode=mode) new_self.match_style(self) new_self.match_height(self) if self.is_flipped() != new_self.is_flipped(): @@ -210,10 +208,11 @@ def is_flipped(self): def blink(self): eye_parts = self.eye_parts - eye_bottom_y = eye_parts.get_bottom()[1] - eye_parts.apply_function( - lambda p: [p[0], eye_bottom_y, p[2]] - ) + eye_bottom_y = eye_parts.get_y(DOWN) + + for eye_part in eye_parts.family_members_with_points(): + eye_part.points[:, 1] = eye_bottom_y + return self def to_corner(self, vect=None, **kwargs): @@ -260,10 +259,13 @@ def get_arm_copies(self): for alpha_range in (self.right_arm_range, self.left_arm_range) ]) + def prepare_for_animation(self): + self.unlock_triangulation() + def get_all_pi_creature_modes(): result = [] - prefix = "%s_" % PiCreature.CONFIG["file_name_prefix"] + prefix = PiCreature.CONFIG["file_name_prefix"] + "_" suffix = ".svg" for file in os.listdir(PI_CREATURE_DIR): if file.startswith(prefix) and file.endswith(suffix): diff --git a/manimlib/for_3b1b_videos/pi_creature_scene.py b/manimlib/for_3b1b_videos/pi_creature_scene.py index def1216d7b..72bdbbb39c 100644 --- a/manimlib/for_3b1b_videos/pi_creature_scene.py +++ b/manimlib/for_3b1b_videos/pi_creature_scene.py @@ -14,6 +14,7 @@ from manimlib.for_3b1b_videos.pi_creature_animations import RemovePiCreatureBubble from manimlib.mobject.mobject import Group from manimlib.mobject.frame import ScreenRectangle +from manimlib.mobject.frame import FullScreenFadeRectangle from manimlib.mobject.svg.drawings import SpeechBubble from manimlib.mobject.svg.drawings import ThoughtBubble from manimlib.mobject.types.vectorized_mobject import VGroup @@ -86,9 +87,7 @@ def introduce_bubble(self, *args, **kwargs): added_anims = kwargs.pop("added_anims", []) anims = [] - on_screen_mobjects = self.camera.extract_mobject_family_members( - self.get_mobjects() - ) + on_screen_mobjects = self.get_mobject_family_members() def has_bubble(pi): return hasattr(pi, "bubble") and \ @@ -251,15 +250,18 @@ class TeacherStudentsScene(PiCreatureScene): CONFIG = { "student_colors": [BLUE_D, BLUE_E, BLUE_C], "teacher_color": GREY_BROWN, + "background_color": DARKER_GREY, "student_scale_factor": 0.8, "seconds_to_blink": 2, "screen_height": 3, - "camera_config": { - "background_color": DARKER_GREY, - }, } def setup(self): + self.background = FullScreenFadeRectangle( + fill_color=self.background_color, + fill_opacity=1, + ) + self.add(self.background) PiCreatureScene.setup(self) self.screen = ScreenRectangle(height=self.screen_height) self.screen.to_corner(UP + LEFT) diff --git a/manimlib/imports.py b/manimlib/imports.py index 23c1498e85..a7fd7c6b71 100644 --- a/manimlib/imports.py +++ b/manimlib/imports.py @@ -30,9 +30,6 @@ from manimlib.animation.update import * from manimlib.camera.camera import * -from manimlib.camera.mapping_camera import * -from manimlib.camera.moving_camera import * -from manimlib.camera.three_d_camera import * from manimlib.mobject.coordinate_systems import * from manimlib.mobject.changing import * @@ -50,10 +47,10 @@ from manimlib.mobject.svg.svg_mobject import * from manimlib.mobject.svg.tex_mobject import * from manimlib.mobject.svg.text_mobject import * -from manimlib.mobject.three_d_utils import * from manimlib.mobject.three_dimensions import * from manimlib.mobject.types.image_mobject import * from manimlib.mobject.types.point_cloud_mobject import * +from manimlib.mobject.types.surface import * from manimlib.mobject.types.vectorized_mobject import * from manimlib.mobject.mobject_update_utils import * from manimlib.mobject.value_tracker import * diff --git a/manimlib/mobject/coordinate_systems.py b/manimlib/mobject/coordinate_systems.py index 4d3fc235d5..5a9ae287bc 100644 --- a/manimlib/mobject/coordinate_systems.py +++ b/manimlib/mobject/coordinate_systems.py @@ -2,13 +2,12 @@ import numbers from manimlib.constants import * -from manimlib.mobject.functions import ParametricFunction +from manimlib.mobject.functions import ParametricCurve from manimlib.mobject.geometry import Arrow from manimlib.mobject.geometry import Line from manimlib.mobject.number_line import NumberLine from manimlib.mobject.svg.tex_mobject import TexMobject from manimlib.mobject.types.vectorized_mobject import VGroup -from manimlib.utils.config_ops import digest_config from manimlib.utils.config_ops import merge_dicts_recursively from manimlib.utils.simple_functions import binary_search from manimlib.utils.space_ops import angle_of_vector @@ -22,10 +21,10 @@ class CoordinateSystem(): """ CONFIG = { "dimension": 2, - "x_min": -FRAME_X_RADIUS, - "x_max": FRAME_X_RADIUS, - "y_min": -FRAME_Y_RADIUS, - "y_max": FRAME_Y_RADIUS, + "x_range": [-8, 8, 1], + "y_range": [-4, 4, 1], + "width": None, + "height": None, } def coords_to_point(self, *coords): @@ -85,13 +84,12 @@ def get_axis_labels(self, x_label_tex="x", y_label_tex="y"): ) return self.axis_labels - def get_graph(self, function, **kwargs): - x_min = kwargs.pop("x_min", self.x_min) - x_max = kwargs.pop("x_max", self.x_max) - graph = ParametricFunction( + def get_graph(self, function, x_range=None, **kwargs): + if x_range is None: + x_range = self.x_range + graph = ParametricCurve( lambda t: self.coords_to_point(t, function(t)), - t_min=x_min, - t_max=x_max, + t_range=x_range, **kwargs ) graph.underlying_function = function @@ -99,10 +97,8 @@ def get_graph(self, function, **kwargs): def get_parametric_curve(self, function, **kwargs): dim = self.dimension - graph = ParametricFunction( - lambda t: self.coords_to_point( - *function(t)[:dim] - ), + graph = ParametricCurve( + lambda t: self.coords_to_point(*function(t)[:dim]), **kwargs ) graph.underlying_function = function @@ -117,8 +113,8 @@ def input_to_graph_point(self, x, graph): graph.point_from_proportion(a) )[0], target=x, - lower_bound=self.x_min, - upper_bound=self.x_max, + lower_bound=self.x_range[0], + upper_bound=self.x_range[1], ) if alpha is not None: return graph.point_from_proportion(alpha) @@ -129,40 +125,40 @@ def input_to_graph_point(self, x, graph): class Axes(VGroup, CoordinateSystem): CONFIG = { "axis_config": { - "color": LIGHT_GREY, "include_tip": True, - "exclude_zero_from_default_numbers": True, }, "x_axis_config": {}, "y_axis_config": { - "label_direction": LEFT, + "line_to_number_direction": LEFT, }, - "center_point": ORIGIN, } - def __init__(self, **kwargs): + def __init__(self, x_range=None, y_range=None, **kwargs): VGroup.__init__(self, **kwargs) self.x_axis = self.create_axis( - self.x_min, self.x_max, self.x_axis_config + x_range or self.x_range, + self.x_axis_config, + self.width, ) self.y_axis = self.create_axis( - self.y_min, self.y_max, self.y_axis_config + y_range or self.y_range, + self.y_axis_config, + self.height ) self.y_axis.rotate(90 * DEGREES, about_point=ORIGIN) - # Add as a separate group incase various other + # Add as a separate group in case various other # mobjects are added to self, as for example in # NumberPlane below self.axes = VGroup(self.x_axis, self.y_axis) self.add(*self.axes) - self.shift(self.center_point) + self.center() - def create_axis(self, min_val, max_val, axis_config): - new_config = merge_dicts_recursively( - self.axis_config, - {"x_min": min_val, "x_max": max_val}, - axis_config, - ) - return NumberLine(**new_config) + def create_axis(self, range_terms, axis_config, length): + new_config = merge_dicts_recursively(self.axis_config, axis_config) + new_config["width"] = length + axis = NumberLine(range_terms, **new_config) + axis.shift(-axis.n2p(0)) + return axis def coords_to_point(self, *coords): origin = self.x_axis.number_to_point(0) @@ -171,89 +167,57 @@ def coords_to_point(self, *coords): result += (axis.number_to_point(coord) - origin) return result - def c2p(self, *coords): - return self.coords_to_point(*coords) - def point_to_coords(self, point): return tuple([ axis.point_to_number(point) for axis in self.get_axes() ]) - def p2c(self, point): - return self.point_to_coords(point) - def get_axes(self): return self.axes - def get_coordinate_labels(self, x_vals=None, y_vals=None): - if x_vals is None: - x_vals = [] - if y_vals is None: - y_vals = [] - x_mobs = self.get_x_axis().get_number_mobjects(*x_vals) - y_mobs = self.get_y_axis().get_number_mobjects(*y_vals) - - self.coordinate_labels = VGroup(x_mobs, y_mobs) + def add_coordinate_labels(self, x_values=None, y_values=None): + axes = self.get_axes() + self.coordinate_labels = VGroup() + for axis, values in zip(axes, [x_values, y_values]): + numbers = axis.add_numbers(values, excluding=[0]) + self.coordinate_labels.add(numbers) return self.coordinate_labels - def add_coordinates(self, x_vals=None, y_vals=None): - self.add(self.get_coordinate_labels(x_vals, y_vals)) - return self - class ThreeDAxes(Axes): CONFIG = { "dimension": 3, - "x_min": -5.5, - "x_max": 5.5, - "y_min": -5.5, - "y_max": 5.5, + "x_range": (-6, 6, 1), + "y_range": (-5, 5, 1), + "z_range": (-4, 4, 1), "z_axis_config": {}, - "z_min": -3.5, - "z_max": 3.5, "z_normal": DOWN, + "depth": None, "num_axis_pieces": 20, - "light_source": 9 * DOWN + 7 * LEFT + 10 * OUT, + "gloss": 0.5, } - def __init__(self, **kwargs): - Axes.__init__(self, **kwargs) - z_axis = self.z_axis = self.create_axis( - self.z_min, self.z_max, self.z_axis_config + def __init__(self, x_range=None, y_range=None, z_range=None, **kwargs): + Axes.__init__(self, x_range, y_range, **kwargs) + + z_axis = self.create_axis( + z_range or self.z_range, + self.z_axis_config, + self.depth, ) - z_axis.rotate(-np.pi / 2, UP, about_point=ORIGIN) + z_axis.rotate(-PI / 2, UP, about_point=ORIGIN) z_axis.rotate( angle_of_vector(self.z_normal), OUT, about_point=ORIGIN ) + z_axis.shift(self.x_axis.n2p(0)) self.axes.add(z_axis) self.add(z_axis) + self.z_axis = z_axis - self.add_3d_pieces() - self.set_axis_shading() - - def add_3d_pieces(self): for axis in self.axes: - axis.pieces = VGroup( - *axis.get_pieces(self.num_axis_pieces) - ) - axis.add(axis.pieces) - axis.set_stroke(width=0, family=False) - axis.set_shade_in_3d(True) - - def set_axis_shading(self): - def make_func(axis): - vect = self.light_source - return lambda: ( - axis.get_edge_center(-vect), - axis.get_edge_center(vect), - ) - for axis in self: - for submob in axis.family_members_with_points(): - submob.get_gradient_start_and_end_points = make_func(axis) - submob.get_unit_normal = lambda a: np.ones(3) - submob.set_sheen(0.2) + axis.insert_n_curves(self.num_axis_pieces - 1) class NumberPlane(Axes): @@ -264,11 +228,13 @@ class NumberPlane(Axes): "include_ticks": False, "include_tip": False, "line_to_number_buff": SMALL_BUFF, - "label_direction": DR, - "number_scale_val": 0.5, + "line_to_number_direction": DL, + "decimal_number_config": { + "height": 0.2, + } }, "y_axis_config": { - "label_direction": DR, + "line_to_number_direction": DL, }, "background_line_style": { "stroke_color": BLUE_D, @@ -277,14 +243,12 @@ class NumberPlane(Axes): }, # Defaults to a faded version of line_config "faded_line_style": None, - "x_line_frequency": 1, - "y_line_frequency": 1, "faded_line_ratio": 1, "make_smooth_after_applying_functions": True, } def __init__(self, **kwargs): - super.__init__(**kwargs) + super().__init__(**kwargs) self.init_background_lines() def init_background_lines(self): @@ -298,12 +262,8 @@ def init_background_lines(self): self.faded_line_style = style self.background_lines, self.faded_lines = self.get_lines() - self.background_lines.set_style( - **self.background_line_style, - ) - self.faded_lines.set_style( - **self.faded_line_style, - ) + self.background_lines.set_style(**self.background_line_style) + self.faded_lines.set_style(**self.faded_line_style) self.add_to_back( self.faded_lines, self.background_lines, @@ -312,45 +272,32 @@ def init_background_lines(self): def get_lines(self): x_axis = self.get_x_axis() y_axis = self.get_y_axis() - x_freq = self.x_line_frequency - y_freq = self.y_line_frequency - x_lines1, x_lines2 = self.get_lines_parallel_to_axis( - x_axis, y_axis, x_freq, - self.faded_line_ratio, - ) - y_lines1, y_lines2 = self.get_lines_parallel_to_axis( - y_axis, x_axis, y_freq, - self.faded_line_ratio, - ) + x_lines1, x_lines2 = self.get_lines_parallel_to_axis(x_axis, y_axis) + y_lines1, y_lines2 = self.get_lines_parallel_to_axis(y_axis, x_axis) lines1 = VGroup(*x_lines1, *y_lines1) lines2 = VGroup(*x_lines2, *y_lines2) return lines1, lines2 - def get_lines_parallel_to_axis(self, axis1, axis2, freq, ratio): + def get_lines_parallel_to_axis(self, axis1, axis2): + freq = axis1.x_step + ratio = self.faded_line_ratio line = Line(axis1.get_start(), axis1.get_end()) dense_freq = (1 + ratio) step = (1 / dense_freq) * freq lines1 = VGroup() lines2 = VGroup() - ranges = ( - np.arange(0, axis2.x_max, step), - np.arange(0, axis2.x_min, -step), - ) - for inputs in ranges: - for k, x in enumerate(inputs): - new_line = line.copy() - new_line.move_to(axis2.number_to_point(x)) - if k % (1 + ratio) == 0: - lines1.add(new_line) - else: - lines2.add(new_line) + inputs = np.arange(axis2.x_min, axis2.x_max + step, step) + for i, x in enumerate(inputs): + new_line = line.copy() + new_line.shift(axis2.n2p(x) - axis2.n2p(0)) + if i % (1 + ratio) == 0: + lines1.add(new_line) + else: + lines2.add(new_line) return lines1, lines2 - def get_center_point(self): - return self.coords_to_point(0, 0) - def get_x_unit_size(self): return self.get_x_axis().get_unit_size() @@ -362,19 +309,13 @@ def get_axes(self): def get_vector(self, coords, **kwargs): kwargs["buff"] = 0 - return Arrow( - self.coords_to_point(0, 0), - self.coords_to_point(*coords), - **kwargs - ) + return Arrow(self.c2p(0, 0), self.c2p(*coords), **kwargs) def prepare_for_nonlinear_transform(self, num_inserted_curves=50): for mob in self.family_members_with_points(): num_curves = mob.get_num_curves() if num_inserted_curves > num_curves: - mob.insert_n_curves( - num_inserted_curves - num_curves - ) + mob.insert_n_curves(num_inserted_curves - num_curves) return self @@ -399,15 +340,13 @@ def p2n(self, point): return self.point_to_number(point) def get_default_coordinate_values(self): - x_numbers = self.get_x_axis().default_numbers_to_display() - y_numbers = self.get_y_axis().default_numbers_to_display() - y_numbers = [ - complex(0, y) for y in y_numbers if y != 0 - ] + x_numbers = self.get_x_axis().get_tick_range() + y_numbers = self.get_y_axis().get_tick_range() + y_numbers = [complex(0, y) for y in y_numbers if y != 0] return [*x_numbers, *y_numbers] - def get_coordinate_labels(self, *numbers, **kwargs): - if len(numbers) == 0: + def add_coordinate_labels(self, numbers=None, **kwargs): + if numbers is None: numbers = self.get_default_coordinate_values() self.coordinate_labels = VGroup() @@ -425,8 +364,5 @@ def get_coordinate_labels(self, *numbers, **kwargs): value = z.real number_mob = axis.get_number_mobject(value, **kwargs) self.coordinate_labels.add(number_mob) - return self.coordinate_labels - - def add_coordinates(self, *numbers): - self.add(self.get_coordinate_labels(*numbers)) + self.add(self.coordinate_labels) return self diff --git a/manimlib/mobject/functions.py b/manimlib/mobject/functions.py index b081fc4120..11e8daf58f 100644 --- a/manimlib/mobject/functions.py +++ b/manimlib/mobject/functions.py @@ -1,104 +1,70 @@ from manimlib.constants import * from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.utils.config_ops import digest_config -import math +from manimlib.utils.space_ops import get_norm -class ParametricFunction(VMobject): +class ParametricCurve(VMobject): CONFIG = { - "t_min": 0, - "t_max": 1, - "step_size": 0.01, # Use "auto" (lowercase) for automatic step size - "dt": 1e-8, - # TODO, be smarter about figuring these out? + "t_range": [0, 1, 0.1], + "min_samples": 10, + "epsilon": 1e-8, + # TODO, automatically figure out discontinuities "discontinuities": [], } - def __init__(self, function=None, **kwargs): - # either get a function from __init__ or from CONFIG - self.function = function or self.function + def __init__(self, t_func, t_range=None, **kwargs): + digest_config(self, kwargs) + if t_range is not None: + self.t_range[:len(t_range)] = t_range + # To be backward compatible with all the scenes specifying t_min, t_max, step_size + self.t_range = [ + kwargs.get("t_min", self.t_range[0]), + kwargs.get("t_max", self.t_range[1]), + kwargs.get("step_size", self.t_range[2]), + ] + self.t_func = t_func VMobject.__init__(self, **kwargs) - def get_function(self): - return self.function - def get_point_from_function(self, t): - return self.function(t) - - def get_step_size(self, t=None): - if self.step_size == "auto": - """ - for x between -1 to 1, return 0.01 - else, return log10(x) (rounded) - e.g.: 10.5 -> 0.1 ; 1040 -> 10 - """ - if t == 0: - scale = 0 - else: - scale = math.log10(abs(t)) - if scale < 0: - scale = 0 + return self.t_func(t) - scale = math.floor(scale) + def init_points(self): + t_min, t_max, step = self.t_range - scale -= 2 - return math.pow(10, scale) - else: - return self.step_size - - def generate_points(self): - t_min, t_max = self.t_min, self.t_max - dt = self.dt - - discontinuities = filter( - lambda t: t_min <= t <= t_max, - self.discontinuities - ) - discontinuities = np.array(list(discontinuities)) - boundary_times = [ - self.t_min, self.t_max, - *(discontinuities - dt), - *(discontinuities + dt), - ] + jumps = np.array(self.discontinuities) + jumps = jumps[(jumps > t_min) & (jumps < t_max)] + boundary_times = [t_min, t_max, *(jumps - self.epsilon), *(jumps + self.epsilon)] boundary_times.sort() for t1, t2 in zip(boundary_times[0::2], boundary_times[1::2]): - t_range = list(np.arange(t1, t2, self.get_step_size(t1))) - if t_range[-1] != t2: - t_range.append(t2) - points = np.array([self.function(t) for t in t_range]) - valid_indices = np.apply_along_axis( - np.all, 1, np.isfinite(points) - ) - points = points[valid_indices] - if len(points) > 0: - self.start_new_path(points[0]) - self.add_points_as_corners(points[1:]) + t_range = [*np.arange(t1, t2, step), t2] + points = np.array([self.t_func(t) for t in t_range]) + self.start_new_path(points[0]) + self.add_points_as_corners(points[1:]) self.make_smooth() return self -class FunctionGraph(ParametricFunction): +class FunctionGraph(ParametricCurve): CONFIG = { "color": YELLOW, - "x_min": -FRAME_X_RADIUS, - "x_max": FRAME_X_RADIUS, + "x_range": [-8, 8, 0.25], } - def __init__(self, function, **kwargs): + def __init__(self, function, x_range=None, **kwargs): digest_config(self, kwargs) - self.parametric_function = \ - lambda t: np.array([t, function(t), 0]) - ParametricFunction.__init__( - self, - self.parametric_function, - t_min=self.x_min, - t_max=self.x_max, - **kwargs - ) self.function = function + if x_range is not None: + self.x_range[:len(x_range)] = x_range + + def parametric_function(t): + return [t, function(t), 0] + + super().__init__(parametric_function, self.x_range, **kwargs) + def get_function(self): return self.function def get_point_from_function(self, x): - return self.parametric_function(x) + return self.t_func(x) diff --git a/manimlib/mobject/geometry.py b/manimlib/mobject/geometry.py index b24e406db2..b007318a3f 100644 --- a/manimlib/mobject/geometry.py +++ b/manimlib/mobject/geometry.py @@ -1,4 +1,3 @@ -import warnings import numpy as np from manimlib.constants import * @@ -10,19 +9,22 @@ from manimlib.utils.iterables import adjacent_n_tuples from manimlib.utils.iterables import adjacent_pairs from manimlib.utils.simple_functions import fdiv +from manimlib.utils.simple_functions import clip from manimlib.utils.space_ops import angle_of_vector from manimlib.utils.space_ops import angle_between_vectors from manimlib.utils.space_ops import compass_directions -from manimlib.utils.space_ops import line_intersection +from manimlib.utils.space_ops import find_intersection from manimlib.utils.space_ops import get_norm from manimlib.utils.space_ops import normalize from manimlib.utils.space_ops import rotate_vector +from manimlib.utils.space_ops import rotation_matrix_transpose DEFAULT_DOT_RADIUS = 0.08 DEFAULT_SMALL_DOT_RADIUS = 0.04 DEFAULT_DASH_LENGTH = 0.05 DEFAULT_ARROW_TIP_LENGTH = 0.35 +DEFAULT_ARROW_TIP_WIDTH = 0.35 class TipableVMobject(VMobject): @@ -42,56 +44,46 @@ class TipableVMobject(VMobject): * Getters - Straightforward accessors, returning information pertaining to the TipableVMobject instance's tip(s), its length etc - """ CONFIG = { - "tip_length": DEFAULT_ARROW_TIP_LENGTH, - # TODO - "normal_vector": OUT, - "tip_style": { + "tip_config": { "fill_opacity": 1, "stroke_width": 0, - } + }, + "normal_vector": OUT, } - - # Adding, Creating, Modifying tips - def add_tip(self, tip_length=None, at_start=False): + # Adding, Creating, Modifying tips + def add_tip(self, at_start=False, **kwargs): """ Adds a tip to the TipableVMobject instance, recognising that the endpoints might need to be switched if it's a 'starting tip' or not. """ - tip = self.create_tip(tip_length, at_start) + tip = self.create_tip(at_start, **kwargs) self.reset_endpoints_based_on_tip(tip, at_start) self.asign_tip_attr(tip, at_start) self.add(tip) return self - def create_tip(self, tip_length=None, at_start=False): + def create_tip(self, at_start=False, **kwargs): """ Stylises the tip, positions it spacially, and returns the newly instantiated tip to the caller. """ - tip = self.get_unpositioned_tip(tip_length) + tip = self.get_unpositioned_tip(**kwargs) self.position_tip(tip, at_start) return tip - def get_unpositioned_tip(self, tip_length=None): + def get_unpositioned_tip(self, **kwargs): """ Returns a tip that has been stylistically configured, but has not yet been given a position in space. """ - if tip_length is None: - tip_length = self.get_default_tip_length() - color = self.get_color() - style = { - "fill_color": color, - "stroke_color": color - } - style.update(self.tip_style) - tip = ArrowTip(length=tip_length, **style) - return tip + config = dict() + config.update(self.tip_config) + config.update(kwargs) + return ArrowTip(**config) def position_tip(self, tip, at_start=False): # Last two control points, defining both @@ -102,10 +94,7 @@ def position_tip(self, tip, at_start=False): else: handle = self.get_last_handle() anchor = self.get_end() - tip.rotate( - angle_of_vector(handle - anchor) - - PI - tip.get_angle() - ) + tip.rotate(angle_of_vector(handle - anchor) - PI - tip.get_angle()) tip.shift(anchor - tip.get_tip_point()) return tip @@ -116,13 +105,12 @@ def reset_endpoints_based_on_tip(self, tip, at_start): return self if at_start: - self.put_start_and_end_on( - tip.get_base(), self.get_end() - ) + start = tip.get_base() + end = self.get_end() else: - self.put_start_and_end_on( - self.get_start(), tip.get_base(), - ) + start = self.get_start() + end = tip.get_base() + self.put_start_and_end_on(start, end) return self def asign_tip_attr(self, tip, at_start): @@ -133,16 +121,13 @@ def asign_tip_attr(self, tip, at_start): return self # Checking for tips - def has_tip(self): return hasattr(self, "tip") and self.tip in self def has_start_tip(self): return hasattr(self, "start_tip") and self.start_tip in self - # Getters - def pop_tips(self): start, end = self.get_start_and_end() result = VGroup() @@ -205,7 +190,7 @@ def get_length(self): class Arc(TipableVMobject): CONFIG = { "radius": 1.0, - "num_components": 9, + "n_components": 8, "anchors_span_full_range": True, "arc_center": ORIGIN, } @@ -215,35 +200,33 @@ def __init__(self, start_angle=0, angle=TAU / 4, **kwargs): self.angle = angle VMobject.__init__(self, **kwargs) - def generate_points(self): - self.set_pre_positioned_points() + def init_points(self): + self.set_points(Arc.create_quadratic_bezier_points( + angle=self.angle, + start_angle=self.start_angle, + n_components=self.n_components + )) self.scale(self.radius, about_point=ORIGIN) self.shift(self.arc_center) - def set_pre_positioned_points(self): - anchors = np.array([ - np.cos(a) * RIGHT + np.sin(a) * UP + @staticmethod + def create_quadratic_bezier_points(angle, start_angle=0, n_components=8): + samples = np.array([ + [np.cos(a), np.sin(a), 0] for a in np.linspace( - self.start_angle, - self.start_angle + self.angle, - self.num_components, + start_angle, + start_angle + angle, + 2 * n_components + 1, ) ]) - # Figure out which control points will give the - # Appropriate tangent lines to the circle - d_theta = self.angle / (self.num_components - 1.0) - tangent_vectors = np.zeros(anchors.shape) - # Rotate all 90 degress, via (x, y) -> (-y, x) - tangent_vectors[:, 1] = anchors[:, 0] - tangent_vectors[:, 0] = -anchors[:, 1] - # Use tangent vectors to deduce anchors - handles1 = anchors[:-1] + (d_theta / 3) * tangent_vectors[:-1] - handles2 = anchors[1:] - (d_theta / 3) * tangent_vectors[1:] - self.set_anchors_and_handles( - anchors[:-1], - handles1, handles2, - anchors[1:], - ) + theta = angle / n_components + samples[1::2] /= np.cos(theta / 2) + + points = np.zeros((3 * n_components, 3)) + points[0::3] = samples[0:-1:2] + points[1::3] = samples[1::2] + points[2::3] = samples[2::2] + return points def get_arc_center(self): """ @@ -251,39 +234,31 @@ def get_arc_center(self): anchors, and finds their intersection points """ # First two anchors and handles - a1, h1, h2, a2 = self.points[:4] + a1, h, a2 = self.points[:3] # Tangent vectors - t1 = h1 - a1 - t2 = h2 - a2 + t1 = h - a1 + t2 = h - a2 # Normals n1 = rotate_vector(t1, TAU / 4) n2 = rotate_vector(t2, TAU / 4) - try: - return line_intersection( - line1=(a1, a1 + n1), - line2=(a2, a2 + n2), - ) - except Exception: - warnings.warn("Can't find Arc center, using ORIGIN instead") - return np.array(ORIGIN) + return find_intersection(a1, n1, a2, n2) + + def get_start_angle(self): + angle = angle_of_vector(self.get_start() - self.get_arc_center()) + return angle % TAU + + def get_stop_angle(self): + angle = angle_of_vector(self.get_end() - self.get_arc_center()) + return angle % TAU def move_arc_center_to(self, point): self.shift(point - self.get_arc_center()) return self - def stop_angle(self): - return angle_of_vector( - self.points[-1] - self.get_arc_center() - ) % TAU - class ArcBetweenPoints(Arc): def __init__(self, start, end, angle=TAU / 4, **kwargs): - Arc.__init__( - self, - angle=angle, - **kwargs, - ) + super().__init__(angle=angle, **kwargs) if angle == 0: self.set_points_as_corners([LEFT, RIGHT]) self.put_start_and_end_on(start, end) @@ -297,9 +272,7 @@ def __init__(self, start_point, end_point, **kwargs): class CurvedDoubleArrow(CurvedArrow): def __init__(self, start_point, end_point, **kwargs): - CurvedArrow.__init__( - self, start_point, end_point, **kwargs - ) + CurvedArrow.__init__(self, start_point, end_point, **kwargs) self.add_tip(at_start=True) @@ -313,23 +286,16 @@ class Circle(Arc): def __init__(self, **kwargs): Arc.__init__(self, 0, TAU, **kwargs) - def surround(self, mobject, dim_to_match=0, stretch=False, buffer_factor=1.2): + def surround(self, mobject, dim_to_match=0, stretch=False, buff=MED_SMALL_BUFF): # Ignores dim_to_match and stretch; result will always be a circle # TODO: Perhaps create an ellipse class to handle singele-dimension stretching - # Something goes wrong here when surrounding lines? - # TODO: Figure out and fix self.replace(mobject, dim_to_match, stretch) - - self.set_width( - np.sqrt(mobject.get_width()**2 + mobject.get_height()**2) - ) - self.scale(buffer_factor) + self.stretch((self.get_width() + 2 * buff) / self.get_width(), 0) + self.stretch((self.get_height() + 2 * buff) / self.get_height(), 1) def point_at_angle(self, angle): - start_angle = angle_of_vector( - self.points[0] - self.get_center() - ) + start_angle = self.get_start_angle() return self.point_from_proportion( (angle - start_angle) / TAU ) @@ -345,6 +311,7 @@ class Dot(Circle): def __init__(self, point=ORIGIN, **kwargs): Circle.__init__(self, arc_center=point, **kwargs) + self.lock_triangulation() class SmallDot(Dot): @@ -376,7 +343,7 @@ class AnnularSector(Arc): "color": WHITE, } - def generate_points(self): + def init_points(self): inner_arc, outer_arc = [ Arc( start_angle=self.start_angle, @@ -410,7 +377,7 @@ class Annulus(Circle): "mark_paths_closed": False, } - def generate_points(self): + def init_points(self): self.radius = self.outer_radius outer_circle = Circle(radius=self.outer_radius) inner_circle = Circle(radius=self.inner_radius) @@ -423,31 +390,32 @@ def generate_points(self): class Line(TipableVMobject): CONFIG = { "buff": 0, - "path_arc": None, # angle of arc specified here + # Angle of arc specified here + "path_arc": 0, } def __init__(self, start=LEFT, end=RIGHT, **kwargs): digest_config(self, kwargs) self.set_start_and_end_attrs(start, end) - VMobject.__init__(self, **kwargs) + super().__init__(**kwargs) - def generate_points(self): - if self.path_arc: - arc = ArcBetweenPoints( - self.start, self.end, - angle=self.path_arc - ) - self.set_points(arc.points) + def init_points(self): + self.set_points_by_ends(self.start, self.end, self.buff, self.path_arc) + + def set_points_by_ends(self, start, end, buff=0, path_arc=0): + if path_arc: + self.set_points(Arc.create_quadratic_bezier_points(path_arc)) + self.put_start_and_end_on(start, end) else: - self.set_points_as_corners([self.start, self.end]) - self.account_for_buff() + self.set_points_as_corners([start, end]) + self.account_for_buff(self.buff) def set_path_arc(self, new_value): self.path_arc = new_value - self.generate_points() + self.init_points() - def account_for_buff(self): - if self.buff == 0: + def account_for_buff(self, buff): + if buff == 0: return # if self.path_arc == 0: @@ -455,12 +423,10 @@ def account_for_buff(self): else: length = self.get_arc_length() # - if length < 2 * self.buff: + if length < 2 * buff: return - buff_proportion = self.buff / length - self.pointwise_become_partial( - self, buff_proportion, 1 - buff_proportion - ) + buff_prop = buff / length + self.pointwise_become_partial(self, buff_prop, 1 - buff_prop) return self def set_start_and_end_attrs(self, start, end): @@ -470,10 +436,10 @@ def set_start_and_end_attrs(self, start, end): rough_end = self.pointify(end) vect = normalize(rough_end - rough_start) # Now that we know the direction between them, - # we can the appropriate boundary point from + # we can find the appropriate boundary point from # start and end, if they're mobjects - self.start = self.pointify(start, vect) - self.end = self.pointify(end, -vect) + self.start = self.pointify(start, vect) + self.buff * vect + self.end = self.pointify(end, -vect) - self.buff * vect def pointify(self, mob_or_point, direction=None): if isinstance(mob_or_point, Mobject): @@ -482,16 +448,12 @@ def pointify(self, mob_or_point, direction=None): return mob.get_center() else: return mob.get_boundary_point(direction) - return np.array(mob_or_point) + return mob_or_point def put_start_and_end_on(self, start, end): curr_start, curr_end = self.get_start_and_end() - if np.all(curr_start == curr_end): - # TODO, any problems with resetting - # these attrs? - self.start = start - self.end = end - self.generate_points() + if (curr_start == curr_end).all(): + self.set_points_by_ends(start, end, self.path_arc) return super().put_start_and_end_on(start, end) def get_vector(self): @@ -515,15 +477,6 @@ def set_angle(self, angle): def set_length(self, length): self.scale(length / self.get_length()) - def set_opacity(self, opacity, family=True): - # Overwrite default, which would set - # the fill opacity - self.set_stroke(opacity=opacity) - if family: - for sm in self.submobjects: - sm.set_opacity(opacity, family) - return self - class DashedLine(Line): CONFIG = { @@ -533,7 +486,7 @@ class DashedLine(Line): } def __init__(self, *args, **kwargs): - Line.__init__(self, *args, **kwargs) + super().__init__(*args, **kwargs) ps_ratio = self.positive_space_ratio num_dashes = self.calculate_num_dashes(ps_ratio) dashes = DashedVMobject( @@ -547,9 +500,7 @@ def __init__(self, *args, **kwargs): def calculate_num_dashes(self, positive_space_ratio): try: full_length = self.dash_length / positive_space_ratio - return int(np.ceil( - self.get_length() / full_length - )) + return int(np.ceil(self.get_length() / full_length)) except ZeroDivisionError: return 1 @@ -587,13 +538,9 @@ class TangentLine(Line): def __init__(self, vmob, alpha, **kwargs): digest_config(self, kwargs) da = self.d_alpha - a1 = np.clip(alpha - da, 0, 1) - a2 = np.clip(alpha + da, 0, 1) - super().__init__( - vmob.point_from_proportion(a1), - vmob.point_from_proportion(a2), - **kwargs - ) + a1 = clip(alpha - da, 0, 1) + a2 = clip(alpha + da, 0, 1) + super().__init__(vmob.pfp(a1), vmob.pfp(a2), **kwargs) self.scale(self.length / self.get_length()) @@ -604,7 +551,7 @@ class Elbow(VMobject): } def __init__(self, **kwargs): - VMobject.__init__(self, **kwargs) + super().__init__(self, **kwargs) self.set_points_as_corners([UP, UP + RIGHT, RIGHT]) self.set_width(self.width, about_point=ORIGIN) self.rotate(self.angle, about_point=ORIGIN) @@ -612,77 +559,103 @@ def __init__(self, **kwargs): class Arrow(Line): CONFIG = { - "stroke_width": 6, + "fill_color": GREY_A, + "fill_opacity": 1, + "stroke_width": 0, "buff": MED_SMALL_BUFF, - "max_tip_length_to_length_ratio": 0.25, - "max_stroke_width_to_length_ratio": 5, - "preserve_tip_size_when_scaling": True, + "width": 0.05, + "tip_width_ratio": 5, + "tip_angle": PI / 3, + "max_tip_length_to_length_ratio": 0.5, + "max_width_to_length_ratio": 0.1, } - def __init__(self, *args, **kwargs): - Line.__init__(self, *args, **kwargs) - # TODO, should this be affected when - # Arrow.set_stroke is called? - self.initial_stroke_width = self.stroke_width - self.add_tip() - self.set_stroke_width_from_length() - - def scale(self, factor, **kwargs): - if self.get_length() == 0: - return self + def set_points_by_ends(self, start, end, buff=0, path_arc=0): + # Find the right tip length and width + vect = end - start + length = max(get_norm(vect), 1e-8) + width = self.width + w_ratio = fdiv(self.max_width_to_length_ratio, fdiv(width, length)) + if w_ratio < 1: + width *= w_ratio + + tip_width = self.tip_width_ratio * width + tip_length = tip_width / (2 * np.tan(self.tip_angle / 2)) + t_ratio = fdiv(self.max_tip_length_to_length_ratio, fdiv(tip_length, length)) + if t_ratio < 1: + tip_length *= t_ratio + tip_width *= t_ratio + + # Find points for the stem + if path_arc == 0: + points1 = (length - tip_length) * np.array([RIGHT, 0.5 * RIGHT, ORIGIN]) + points1 += width * UP / 2 + points2 = points1[::-1] + width * DOWN + else: + # Solve for radius so that the tip-to-tail length matches |end - start| + a = 2 * (1 - np.cos(path_arc)) + b = -2 * tip_length * np.sin(path_arc) + c = tip_length**2 - length**2 + R = (-b + np.sqrt(b**2 - 4 * a * c)) / (2 * a) + + # Find arc points + points1 = Arc.create_quadratic_bezier_points(path_arc) + points2 = np.array(points1[::-1]) + points1 *= (R + width / 2) + points2 *= (R - width / 2) + if path_arc < 0: + tip_length *= -1 + rot_T = rotation_matrix_transpose(PI / 2 - path_arc, OUT) + for points in points1, points2: + points[:] = np.dot(points, rot_T) + points += R * DOWN + + self.set_points(points1) + # Tip + self.add_line_to(tip_width * UP / 2) + self.add_line_to(tip_length * LEFT) + self.tip_index = len(self.points) - 1 + self.add_line_to(tip_width * DOWN / 2) + self.add_line_to(points2[0]) + # Close it out + self.append_points(points2) + self.add_line_to(points1[0]) + + if length > 0: + self.points *= length / self.get_length() # Final correction + + self.rotate(angle_of_vector(vect) - self.get_angle()) + self.shift(start - self.get_start()) + self.refresh_triangulation() + + def reset_points_around_ends(self): + self.set_points_by_ends(self.get_start(), self.get_end(), path_arc=self.path_arc) + return self - has_tip = self.has_tip() - has_start_tip = self.has_start_tip() - if has_tip or has_start_tip: - old_tips = self.pop_tips() + def get_start(self): + return (self.points[0] + self.points[-1]) / 2 - VMobject.scale(self, factor, **kwargs) - self.set_stroke_width_from_length() + def get_end(self): + return self.points[self.tip_index] - # So horribly confusing, must redo - if has_tip: - self.add_tip() - old_tips[0].points[:, :] = self.tip.points - self.remove(self.tip) - self.tip = old_tips[0] - self.add(self.tip) - if has_start_tip: - self.add_tip(at_start=True) - old_tips[1].points[:, :] = self.start_tip.points - self.remove(self.start_tip) - self.start_tip = old_tips[1] - self.add(self.start_tip) + def put_start_and_end_on(self, start, end): + self.set_points_by_ends(start, end, buff=0, path_arc=self.path_arc) return self - def get_normal_vector(self): - p0, p1, p2 = self.tip.get_start_anchors()[:3] - return normalize(np.cross(p2 - p1, p1 - p0)) - - def reset_normal_vector(self): - self.normal_vector = self.get_normal_vector() + def scale(self, *args, **kwargs): + super().scale(*args, **kwargs) + self.reset_points_around_ends() return self - def get_default_tip_length(self): - max_ratio = self.max_tip_length_to_length_ratio - return min( - self.tip_length, - max_ratio * self.get_length(), - ) - - def set_stroke_width_from_length(self): - max_ratio = self.max_stroke_width_to_length_ratio - self.set_stroke( - width=min( - self.initial_stroke_width, - max_ratio * self.get_length(), - ), - family=False, - ) + def set_width(self, width): + self.width = width + self.reset_points_around_ends() return self - # TODO, should this be the default for everything? - def copy(self): - return self.deepcopy() + def set_path_arc(self, path_arc): + self.path_arc = path_arc + self.reset_points_around_ends() + return self class Vector(Arrow): @@ -692,8 +665,8 @@ class Vector(Arrow): def __init__(self, direction=RIGHT, **kwargs): if len(direction) == 2: - direction = np.append(np.array(direction), 0) - Arrow.__init__(self, ORIGIN, direction, **kwargs) + direction = np.hstack([direction, 0]) + super().__init__(ORIGIN, direction, **kwargs) class DoubleArrow(Arrow): @@ -703,21 +676,19 @@ def __init__(self, *args, **kwargs): class CubicBezier(VMobject): - def __init__(self, points, **kwargs): + def __init__(self, a0, h0, h1, a1, **kwargs): VMobject.__init__(self, **kwargs) - self.set_points(points) + self.add_cubic_bezier_curve(a0, h0, h1, a1) class Polygon(VMobject): - CONFIG = { - "color": BLUE, - } - def __init__(self, *vertices, **kwargs): - VMobject.__init__(self, **kwargs) - self.set_points_as_corners( - [*vertices, vertices[0]] - ) + self.vertices = vertices + super().__init__(**kwargs) + + def init_points(self): + verts = self.vertices + self.set_points_as_corners([*verts, verts[0]]) def get_vertices(self): return self.get_start_anchors() @@ -740,7 +711,8 @@ def round_corners(self, radius=0.5): arc = ArcBetweenPoints( v2 - unit_vect1 * cut_off_length, v2 + unit_vect2 * cut_off_length, - angle=sign * angle + angle=sign * angle, + n_components=2, ) arcs.append(arc) @@ -767,32 +739,33 @@ class RegularPolygon(Polygon): def __init__(self, n=6, **kwargs): digest_config(self, kwargs, locals()) if self.start_angle is None: - if n % 2 == 0: - self.start_angle = 0 - else: - self.start_angle = 90 * DEGREES + # 0 for odd, 90 for even + self.start_angle = (n % 2) * 90 * DEGREES start_vect = rotate_vector(RIGHT, self.start_angle) vertices = compass_directions(n, start_vect) - Polygon.__init__(self, *vertices, **kwargs) + super().__init__(*vertices, **kwargs) class Triangle(RegularPolygon): def __init__(self, **kwargs): - RegularPolygon.__init__(self, n=3, **kwargs) + super().__init__(n=3, **kwargs) class ArrowTip(Triangle): CONFIG = { "fill_opacity": 1, + "fill_color": WHITE, "stroke_width": 0, + "width": DEFAULT_ARROW_TIP_WIDTH, "length": DEFAULT_ARROW_TIP_LENGTH, - "start_angle": PI, + "angle": 0, } def __init__(self, **kwargs): - Triangle.__init__(self, **kwargs) - self.set_width(self.length) - self.set_height(self.length, stretch=True) + Triangle.__init__(self, start_angle=0, **kwargs) + self.set_height(self.width) + self.set_width(self.length, stretch=True) + self.rotate(self.angle) def get_base(self): return self.point_from_proportion(0.5) @@ -813,16 +786,22 @@ def get_length(self): class Rectangle(Polygon): CONFIG = { "color": WHITE, - "height": 2.0, "width": 4.0, + "height": 2.0, "mark_paths_closed": True, "close_new_points": True, } - def __init__(self, **kwargs): - Polygon.__init__(self, UL, UR, DR, DL, **kwargs) - self.set_width(self.width, stretch=True) - self.set_height(self.height, stretch=True) + def __init__(self, width=None, height=None, **kwargs): + Polygon.__init__(self, UR, UL, DL, DR, **kwargs) + + if width is None: + width = self.width + if height is None: + height = self.height + + self.set_width(width, stretch=True) + self.set_height(height, stretch=True) class Square(Rectangle): @@ -830,14 +809,13 @@ class Square(Rectangle): "side_length": 2.0, } - def __init__(self, **kwargs): + def __init__(self, side_length=None, **kwargs): digest_config(self, kwargs) - Rectangle.__init__( - self, - height=self.side_length, - width=self.side_length, - **kwargs - ) + + if side_length is None: + side_length = self.side_length + + super().__init__(side_length, side_length, **kwargs) class RoundedRectangle(Rectangle): diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 7cd8b30f83..48059363d7 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -5,8 +5,8 @@ import os import random import sys +import moderngl -from colour import Color import numpy as np import manimlib.constants as consts @@ -14,16 +14,19 @@ from manimlib.container.container import Container from manimlib.utils.color import color_gradient from manimlib.utils.color import interpolate_color +from manimlib.utils.iterables import batch_by_property from manimlib.utils.iterables import list_update -from manimlib.utils.iterables import remove_list_redundancies +from manimlib.utils.bezier import interpolate from manimlib.utils.paths import straight_path from manimlib.utils.simple_functions import get_parameters from manimlib.utils.space_ops import angle_of_vector from manimlib.utils.space_ops import get_norm -from manimlib.utils.space_ops import rotation_matrix +from manimlib.utils.space_ops import rotation_matrix_transpose +from manimlib.shader_wrapper import ShaderWrapper # TODO: Explain array_attrs +# TODO: Incorporate shader defaults class Mobject(Container): """ @@ -33,20 +36,44 @@ class Mobject(Container): "color": WHITE, "name": None, "dim": 3, - "target": None, + # Lighting parameters + # Positive gloss up to 1 makes it reflect the light. + "gloss": 0.0, + # Positive shadow up to 1 makes a side opposite the light darker + "shadow": 0.0, + # For shaders + "vert_shader_file": "", + "geom_shader_file": "", + "frag_shader_file": "", + "render_primative": moderngl.TRIANGLE_STRIP, + "texture_paths": None, + "depth_test": False, + # If true, the mobject will not get rotated according to camera position + "is_fixed_in_frame": False, + # Must match in attributes of vert shader + "shader_dtype": [ + ('point', np.float32, (3,)), + ] } def __init__(self, **kwargs): Container.__init__(self, **kwargs) self.submobjects = [] - self.color = Color(self.color) + self.parents = [] + self.family = [self] if self.name is None: self.name = self.__class__.__name__ - self.updaters = [] - self.updating_suspended = False + + self.init_updaters() self.reset_points() - self.generate_points() + self.init_points() self.init_colors() + self.init_shader_data() + + if self.is_fixed_in_frame: + self.fix_in_frame() + if self.depth_test: + self.apply_depth_test() def __str__(self): return str(self.name) @@ -58,28 +85,81 @@ def init_colors(self): # For subclasses pass - def generate_points(self): + def init_points(self): # Typically implemented in subclass, unless purposefully left blank pass + # Family matters + def __getitem__(self, value): + self_list = self.split() + if isinstance(value, slice): + GroupClass = self.get_group_class() + return GroupClass(*self_list.__getitem__(value)) + return self_list.__getitem__(value) + + def __iter__(self): + return iter(self.split()) + + def __len__(self): + return len(self.split()) + + def split(self): + result = [self] if len(self.points) > 0 else [] + return result + self.submobjects + + def assemble_family(self): + sub_families = [sm.get_family() for sm in self.submobjects] + self.family = [self, *it.chain(*sub_families)] + self.refresh_has_updater_status() + for parent in self.parents: + parent.assemble_family() + return self + + def get_family(self): + return self.family + + def family_members_with_points(self): + return [m for m in self.get_family() if m.points.size > 0] + def add(self, *mobjects): if self in mobjects: raise Exception("Mobject cannot contain self") - self.submobjects = list_update(self.submobjects, mobjects) - return self - - def add_to_back(self, *mobjects): - self.remove(*mobjects) - self.submobjects = list(mobjects) + self.submobjects + for mobject in mobjects: + if mobject not in self.submobjects: + self.submobjects.append(mobject) + if self not in mobject.parents: + mobject.parents.append(self) + self.assemble_family() return self def remove(self, *mobjects): for mobject in mobjects: if mobject in self.submobjects: self.submobjects.remove(mobject) + if self in mobject.parents: + mobject.parents.remove(self) + self.assemble_family() + return self + + def add_to_back(self, *mobjects): + self.set_submobjects(list_update(mobjects, self.submobjects)) + return self + + def replace_submobject(self, index, new_submob): + old_submob = self.submobjects[index] + if self in old_submob.parents: + old_submob.parents.remove(self) + self.submobjects[index] = new_submob + self.assemble_family() + return self + + def set_submobjects(self, submobject_list): + self.remove(*self.submobjects) + self.add(*submobject_list) return self def get_array_attrs(self): + # May be more for other Mobject types return ["points"] def digest_mobject_attrs(self): @@ -88,7 +168,7 @@ def digest_mobject_attrs(self): in the submobjects list. """ mobject_attrs = [x for x in list(self.__dict__.values()) if isinstance(x, Mobject)] - self.submobjects = list_update(self.submobjects, mobject_attrs) + self.set_submobjects(list_update(self.submobjects, mobject_attrs)) return self def apply_over_attr_arrays(self, func): @@ -99,14 +179,13 @@ def apply_over_attr_arrays(self, func): # Displaying def get_image(self, camera=None): - if camera is None: - from manimlib.camera.camera import Camera - camera = Camera() - camera.capture_mobject(self) + # TODO, this doesn't...you know, seem to actually work + camera.clear() + camera.capture(self) return camera.get_image() - def show(self, camera=None): - self.get_image(camera=camera).show() + def show(self, camera): + self.get_image(camera).show() def save_image(self, name=None): self.get_image().save( @@ -118,22 +197,33 @@ def copy(self): # remove this redundancy everywhere # return self.deepcopy() + parents = self.parents + self.parents = [] copy_mobject = copy.copy(self) + self.parents = parents + copy_mobject.points = np.array(self.points) - copy_mobject.submobjects = [ - submob.copy() for submob in self.submobjects - ] - copy_mobject.updaters = list(self.updaters) + copy_mobject.submobjects = [] + copy_mobject.add(*[sm.copy() for sm in self.submobjects]) + copy_mobject.match_updaters(self) + + # Make sure any mobject or numpy array attributes are copied family = self.get_family() for attr, value in list(self.__dict__.items()): if isinstance(value, Mobject) and value in family and value is not self: setattr(copy_mobject, attr, value.copy()) if isinstance(value, np.ndarray): setattr(copy_mobject, attr, np.array(value)) + if isinstance(value, ShaderWrapper): + setattr(copy_mobject, attr, value.copy()) return copy_mobject def deepcopy(self): - return copy.deepcopy(self) + parents = self.parents + self.parents = [] + result = copy.deepcopy(self) + self.parents = parents + return result def generate_target(self, use_deepcopy=False): self.target = None # Prevent exponential explosion @@ -145,60 +235,65 @@ def generate_target(self, use_deepcopy=False): # Updating + def init_updaters(self): + self.time_based_updaters = [] + self.non_time_updaters = [] + self.has_updaters = False + self.updating_suspended = False + def update(self, dt=0, recursive=True): - if self.updating_suspended: + if not self.has_updaters or self.updating_suspended: return self - for updater in self.updaters: - parameters = get_parameters(updater) - if "dt" in parameters: - updater(self, dt) - else: - updater(self) + for updater in self.time_based_updaters: + updater(self, dt) + for updater in self.non_time_updaters: + updater(self) if recursive: for submob in self.submobjects: submob.update(dt, recursive) return self def get_time_based_updaters(self): - return [ - updater for updater in self.updaters - if "dt" in get_parameters(updater) - ] + return self.time_based_updaters def has_time_based_updater(self): - for updater in self.updaters: - if "dt" in get_parameters(updater): - return True - return False + return len(self.time_based_updaters) > 0 def get_updaters(self): - return self.updaters + return self.time_based_updaters + self.non_time_updaters def get_family_updaters(self): - return list(it.chain(*[ - sm.get_updaters() - for sm in self.get_family() - ])) + return list(it.chain(*[sm.get_updaters() for sm in self.get_family()])) def add_updater(self, update_function, index=None, call_updater=True): + if "dt" in get_parameters(update_function): + updater_list = self.time_based_updaters + else: + updater_list = self.non_time_updaters + if index is None: - self.updaters.append(update_function) + updater_list.append(update_function) else: - self.updaters.insert(index, update_function) + updater_list.insert(index, update_function) + + self.refresh_has_updater_status() if call_updater: - self.update(0) + self.update() return self def remove_updater(self, update_function): - while update_function in self.updaters: - self.updaters.remove(update_function) + for updater_list in [self.time_based_updaters, self.non_time_updaters]: + while update_function in updater_list: + updater_list.remove(update_function) return self def clear_updaters(self, recursive=True): - self.updaters = [] + self.time_based_updaters = [] + self.non_time_updaters = [] if recursive: for submob in self.submobjects: submob.clear_updaters() + self.suspend_updating(recursive) return self def match_updaters(self, mobject): @@ -214,15 +309,25 @@ def suspend_updating(self, recursive=True): submob.suspend_updating(recursive) return self - def resume_updating(self, recursive=True): + def resume_updating(self, recursive=True, call_updater=True): self.updating_suspended = False if recursive: for submob in self.submobjects: submob.resume_updating(recursive) - self.update(dt=0, recursive=recursive) + for parent in self.parents: + parent.resume_updating(recursive=False, call_updater=False) + if call_updater: + self.update(dt=0, recursive=recursive) + return self + + def refresh_has_updater_status(self): + self.has_updaters = len(self.get_family_updaters()) > 0 return self # Transforming operations + def set_points(self, points): + self.points = np.array(points) + return self def apply_to_family(self, func): for mob in self.family_members_with_points(): @@ -230,8 +335,7 @@ def apply_to_family(self, func): def shift(self, *vectors): total_vector = reduce(op.add, vectors) - for mob in self.family_members_with_points(): - mob.points = mob.points.astype('float') + for mob in self.get_family(): mob.points += total_vector return self @@ -246,7 +350,8 @@ def scale(self, scale_factor, **kwargs): respect to that point. """ self.apply_points_function_about_point( - lambda points: scale_factor * points, **kwargs + lambda points: scale_factor * points, + **kwargs ) return self @@ -254,9 +359,9 @@ def rotate_about_origin(self, angle, axis=OUT, axes=[]): return self.rotate(angle, axis, about_point=ORIGIN) def rotate(self, angle, axis=OUT, **kwargs): - rot_matrix = rotation_matrix(angle, axis) + rot_matrix_T = rotation_matrix_transpose(angle, axis) self.apply_points_function_about_point( - lambda points: np.dot(points, rot_matrix.T), + lambda points: np.dot(points, rot_matrix_T), **kwargs ) return self @@ -276,7 +381,7 @@ def apply_function(self, function, **kwargs): if len(kwargs) == 0: kwargs["about_point"] = ORIGIN self.apply_points_function_about_point( - lambda points: np.apply_along_axis(function, 1, points), + lambda points: np.array([function(p) for p in points]), **kwargs ) return self @@ -328,22 +433,15 @@ def wag(self, direction=RIGHT, axis=DOWN, wag_factor=1.0): def reverse_points(self): for mob in self.family_members_with_points(): - mob.apply_over_attr_arrays( - lambda arr: np.array(list(reversed(arr))) - ) + mob.apply_over_attr_arrays(lambda arr: arr[::-1]) return self def repeat(self, count): """ This can make transition animations nicer """ - def repeat_array(array): - return reduce( - lambda a1, a2: np.append(a1, a2, axis=0), - [array] * count - ) for mob in self.family_members_with_points(): - mob.apply_over_attr_arrays(repeat_array) + mob.apply_over_attr_arrays(lambda arr: np.vstack([arr] * count)) return self # In place operations. @@ -354,10 +452,10 @@ def apply_points_function_about_point(self, func, about_point=None, about_edge=N if about_point is None: if about_edge is None: about_edge = ORIGIN - about_point = self.get_critical_point(about_edge) + about_point = self.get_bounding_box_point(about_edge) for mob in self.family_members_with_points(): mob.points -= about_point - mob.points = func(mob.points) + mob.points[:] = func(mob.points) mob.points += about_point return self @@ -373,10 +471,6 @@ def scale_about_point(self, scale_factor, point): # Redundant with default behavior of scale now. return self.scale(scale_factor, about_point=point) - def pose_at_angle(self, **kwargs): - self.rotate(TAU / 14, RIGHT + UP, **kwargs) - return self - # Positioning methods def center(self): @@ -389,7 +483,7 @@ def align_on_border(self, direction, buff=DEFAULT_MOBJECT_TO_EDGE_BUFFER): corner in the 2d plane. """ target_point = np.sign(direction) * (FRAME_X_RADIUS, FRAME_Y_RADIUS, 0) - point_to_align = self.get_critical_point(direction) + point_to_align = self.get_bounding_box_point(direction) shift_val = target_point - point_to_align - buff * np.array(direction) shift_val = shift_val * abs(np.sign(direction)) self.shift(shift_val) @@ -415,7 +509,7 @@ def next_to(self, mobject_or_point, target_aligner = mob[index_of_submobject_to_align] else: target_aligner = mob - target_point = target_aligner.get_critical_point( + target_point = target_aligner.get_bounding_box_point( aligned_edge + direction ) else: @@ -426,7 +520,7 @@ def next_to(self, mobject_or_point, aligner = self[index_of_submobject_to_align] else: aligner = self - point_to_align = aligner.get_critical_point(aligned_edge - direction) + point_to_align = aligner.get_bounding_box_point(aligned_edge - direction) self.shift((target_point - point_to_align + buff * direction) * coor_mask) return self @@ -513,10 +607,10 @@ def space_out_submobjects(self, factor=1.5, **kwargs): def move_to(self, point_or_mobject, aligned_edge=ORIGIN, coor_mask=np.array([1, 1, 1])): if isinstance(point_or_mobject, Mobject): - target = point_or_mobject.get_critical_point(aligned_edge) + target = point_or_mobject.get_bounding_box_point(aligned_edge) else: target = point_or_mobject - point_to_align = self.get_critical_point(aligned_edge) + point_to_align = self.get_bounding_box_point(aligned_edge) self.shift((target - point_to_align) * coor_mask) return self @@ -525,8 +619,8 @@ def replace(self, mobject, dim_to_match=0, stretch=False): raise Warning("Attempting to replace mobject with no points") return self if stretch: - self.stretch_to_fit_width(mobject.get_width()) - self.stretch_to_fit_height(mobject.get_height()) + for i in range(self.dim): + self.rescale_to_fit(mobject.length_over_dim(i), i, stretch=True) else: self.rescale_to_fit( mobject.length_over_dim(dim_to_match), @@ -546,6 +640,7 @@ def surround(self, mobject, return self def put_start_and_end_on(self, start, end): + # TODO, this doesn't currently work in 3d curr_start, curr_end = self.get_start_and_end() curr_vect = curr_end - curr_start if np.all(curr_vect == 0): @@ -556,8 +651,7 @@ def put_start_and_end_on(self, start, end): about_point=curr_start, ) self.rotate( - angle_of_vector(target_vect) - - angle_of_vector(curr_vect), + angle_of_vector(target_vect) - angle_of_vector(curr_vect), about_point=curr_start ) self.shift(start - curr_start) @@ -588,19 +682,30 @@ def add_background_rectangle_to_family_members_with_points(self, **kwargs): # Color functions - def set_color(self, color=YELLOW_C, family=True): - """ - Condition is function which takes in one arguments, (x, y, z). - Here it just recurses to submobjects, but in subclasses this - should be further implemented based on the the inner workings - of color - """ + def set_color(self, color, family=True): + # Here it just recurses to submobjects, but in subclasses this + # should be further implemented based on the the inner workings + # of color if family: for submob in self.submobjects: submob.set_color(color, family=family) - self.color = color return self + def set_opacity(self, opacity, family=True): + # Here it just recurses to submobjects, but in subclasses this + # should be further implemented based on the the inner workings + # of color + if family: + for submob in self.submobjects: + submob.set_opacity(opacity, family=family) + return self + + def get_color(self): + raise Exception("Not implemented") + + def get_opacity(self): + raise Exception("Not implemented") + def set_color_by_gradient(self, *colors): self.set_submobject_colors_by_gradient(*colors) return self @@ -635,10 +740,6 @@ def set_submobject_colors_by_radial_gradient(self, center=None, radius=1, inner_ return self - def to_original_color(self): - self.set_color(self.color) - return self - def fade_to(self, color, alpha, family=True): if self.get_num_points() > 0: new_color = interpolate_color( @@ -656,8 +757,25 @@ def fade(self, darkness=0.5, family=True): submob.fade(darkness, family) return self - def get_color(self): - return self.color + def get_gloss(self): + return self.gloss + + def set_gloss(self, gloss, family=True): + self.gloss = gloss + if family: + for submob in self.submobjects: + submob.set_gloss(gloss, family) + return self + + def get_shadow(self): + return self.shadow + + def set_shadow(self, shadow, family=True): + self.shadow = shadow + if family: + for submob in self.submobjects: + submob.set_shadow(shadow, family) + return self ## @@ -679,33 +797,22 @@ def restore(self): ## - def reduce_across_dimension(self, points_func, reduce_func, dim): - points = self.get_all_points() - if points is None or len(points) == 0: - # Note, this default means things like empty VGroups - # will appear to have a center at [0, 0, 0] - return 0 - values = points_func(points[:, dim]) - return reduce_func(values) - - def nonempty_submobjects(self): - return [ - submob for submob in self.submobjects - if len(submob.submobjects) != 0 or len(submob.points) != 0 - ] - def get_merged_array(self, array_attr): - result = getattr(self, array_attr) - for submob in self.submobjects: - result = np.append( - result, submob.get_merged_array(array_attr), - axis=0 - ) - submob.get_merged_array(array_attr) - return result + if self.submobjects: + return np.vstack([ + getattr(sm, array_attr) + for sm in self.get_family() + ]) + else: + return getattr(self, array_attr) def get_all_points(self): - return self.get_merged_array("points") + if self.submobjects: + return np.vstack([ + sm.points for sm in self.get_family() + ]) + else: + return self.points # Getters @@ -715,46 +822,38 @@ def get_points_defining_boundary(self): def get_num_points(self): return len(self.points) - def get_extremum_along_dim(self, points=None, dim=0, key=0): - if points is None: - points = self.get_points_defining_boundary() - values = points[:, dim] - if key < 0: - return np.min(values) - elif key == 0: - return (np.min(values) + np.max(values)) / 2 - else: - return np.max(values) - - def get_critical_point(self, direction): - """ - Picture a box bounding the mobject. Such a box has - 9 'critical points': 4 corners, 4 edge center, the - center. This returns one of them. - """ + def get_bounding_box_point(self, direction): result = np.zeros(self.dim) + bb = self.get_bounding_box() + result[direction < 0] = bb[0, direction < 0] + result[direction == 0] = bb[1, direction == 0] + result[direction > 0] = bb[2, direction > 0] + return result + + def get_bounding_box(self): all_points = self.get_points_defining_boundary() if len(all_points) == 0: - return result - for dim in range(self.dim): - result[dim] = self.get_extremum_along_dim( - all_points, dim=dim, key=direction[dim] - ) - return result + return np.zeros((3, self.dim)) + else: + # Lower left and upper right corners + mins = all_points.min(0) + maxs = all_points.max(0) + mids = (mins + maxs) / 2 + return np.array([mins, mids, maxs]) - # Pseudonyms for more general get_critical_point method + # Pseudonyms for more general get_bounding_box_point method def get_edge_center(self, direction): - return self.get_critical_point(direction) + return self.get_bounding_box_point(direction) def get_corner(self, direction): - return self.get_critical_point(direction) + return self.get_bounding_box_point(direction) def get_center(self): - return self.get_critical_point(np.zeros(self.dim)) + return self.get_bounding_box_point(np.zeros(self.dim)) def get_center_of_mass(self): - return np.apply_along_axis(np.mean, 0, self.get_all_points()) + return self.get_all_points().mean(0) def get_boundary_point(self, direction): all_points = self.get_points_defining_boundary() @@ -780,10 +879,8 @@ def get_nadir(self): return self.get_edge_center(IN) def length_over_dim(self, dim): - return ( - self.reduce_across_dimension(np.max, np.max, dim) - - self.reduce_across_dimension(np.min, np.min, dim) - ) + bb = self.get_bounding_box() + return (bb[2] - bb[0])[dim] def get_width(self): return self.length_over_dim(0) @@ -798,9 +895,7 @@ def get_coord(self, dim, direction=ORIGIN): """ Meant to generalize get_x, get_y, get_z """ - return self.get_extremum_along_dim( - dim=dim, key=direction[dim] - ) + return self.get_bounding_box_point(direction)[dim] def get_x(self, direction=ORIGIN): return self.get_coord(0, direction) @@ -825,9 +920,13 @@ def get_start_and_end(self): def point_from_proportion(self, alpha): raise Exception("Not implemented") + def pfp(self, alpha): + """Abbreviation fo point_from_proportion""" + return self.point_from_proportion(alpha) + def get_pieces(self, n_pieces): template = self.copy() - template.submobjects = [] + template.set_submobjects([]) alphas = np.linspace(0, 1, n_pieces + 1) return Group(*[ template.copy().pointwise_become_partial( @@ -894,7 +993,7 @@ def align_to(self, mobject_or_point, direction=ORIGIN, alignment_vect=UP): the center of mob2 """ if isinstance(mobject_or_point, Mobject): - point = mobject_or_point.get_critical_point(direction) + point = mobject_or_point.get_bounding_box_point(direction) else: point = mobject_or_point @@ -903,36 +1002,10 @@ def align_to(self, mobject_or_point, direction=ORIGIN, alignment_vect=UP): self.set_coord(point[dim], dim, direction) return self - # Family matters - - def __getitem__(self, value): - self_list = self.split() - if isinstance(value, slice): - GroupClass = self.get_group_class() - return GroupClass(*self_list.__getitem__(value)) - return self_list.__getitem__(value) - - def __iter__(self): - return iter(self.split()) - - def __len__(self): - return len(self.split()) - def get_group_class(self): return Group - def split(self): - result = [self] if len(self.points) > 0 else [] - return result + self.submobjects - - def get_family(self): - sub_families = list(map(Mobject.get_family, self.submobjects)) - all_mobjects = [self] + list(it.chain(*sub_families)) - return remove_list_redundancies(all_mobjects) - - def family_members_with_points(self): - return [m for m in self.get_family() if m.get_num_points() > 0] - + # Submobject organization def arrange(self, direction=RIGHT, center=True, **kwargs): for m1, m2 in zip(self.submobjects, self.submobjects[1:]): m2.next_to(m1, direction, **kwargs) @@ -943,7 +1016,7 @@ def arrange(self, direction=RIGHT, center=True, **kwargs): def arrange_in_grid(self, n_rows=None, n_cols=None, **kwargs): submobs = self.submobjects if n_rows is None and n_cols is None: - n_cols = int(np.sqrt(len(submobs))) + n_rows = int(np.sqrt(len(submobs))) if n_rows is not None: v1 = RIGHT @@ -970,6 +1043,7 @@ def shuffle(self, recursive=False): for submob in self.submobjects: submob.shuffle(recursive=True) random.shuffle(self.submobjects) + return self # Just here to keep from breaking old scenes. def arrange_submobjects(self, *args, **kwargs): @@ -983,20 +1057,10 @@ def shuffle_submobjects(self, *args, **kwargs): # Alignment def align_data(self, mobject): - self.null_point_align(mobject) + self.null_point_align(mobject) # Needed? self.align_submobjects(mobject) - self.align_points(mobject) - # Recurse - for m1, m2 in zip(self.submobjects, mobject.submobjects): - m1.align_data(m2) - - def get_point_mobject(self, center=None): - """ - The simplest mobject to be transformed to or from self. - Should by a point of the appropriate type - """ - message = "get_point_mobject not implemented for {}" - raise Exception(message.format(self.__class__.__name__)) + for mob1, mob2 in zip(self.get_family(), mobject.get_family()): + mob1.align_points(mob2) def align_points(self, mobject): count1 = self.get_num_points() @@ -1017,6 +1081,9 @@ def align_submobjects(self, mobject): n2 = len(mob2.submobjects) mob1.add_n_more_submobjects(max(0, n2 - n1)) mob2.add_n_more_submobjects(max(0, n1 - n2)) + # Recurse + for sm1, sm2 in zip(mob1.submobjects, mob2.submobjects): + sm1.align_submobjects(sm2) return self def null_point_align(self, mobject): @@ -1032,8 +1099,8 @@ def null_point_align(self, mobject): return self def push_self_into_submobjects(self): - copy = self.copy() - copy.submobjects = [] + copy = self.deepcopy() + copy.set_submobjects([]) self.reset_points() self.add(copy) return self @@ -1045,48 +1112,52 @@ def add_n_more_submobjects(self, n): curr = len(self.submobjects) if curr == 0: # If empty, simply add n point mobjects - self.submobjects = [ - self.get_point_mobject() + self.set_submobjects([ + self.copy().scale(0) for k in range(n) - ] + ]) return - target = curr + n - # TODO, factor this out to utils so as to reuse - # with VMobject.insert_n_curves repeat_indices = (np.arange(target) * curr) // target split_factors = [ - sum(repeat_indices == i) + (repeat_indices == i).sum() for i in range(curr) ] new_submobs = [] for submob, sf in zip(self.submobjects, split_factors): new_submobs.append(submob) for k in range(1, sf): - new_submobs.append( - submob.copy().fade(1) - ) - self.submobjects = new_submobs + new_submobs.append(submob.copy().fade(1)) + self.set_submobjects(new_submobs) return self - def repeat_submobject(self, submob): - return submob.copy() - - def interpolate(self, mobject1, mobject2, - alpha, path_func=straight_path): + def interpolate(self, mobject1, mobject2, alpha, path_func=straight_path): """ Turns self into an interpolation between mobject1 and mobject2. """ - self.points = path_func( - mobject1.points, mobject2.points, alpha - ) + self.points[:] = path_func(mobject1.points, mobject2.points, alpha) self.interpolate_color(mobject1, mobject2, alpha) + self.interpolate_light_style(mobject1, mobject2, alpha) return self def interpolate_color(self, mobject1, mobject2, alpha): pass # To implement in subclass + def interpolate_light_style(self, mobject1, mobject2, alpha): + g0 = self.get_gloss() + g1 = mobject1.get_gloss() + g2 = mobject2.get_gloss() + if not (g0 == g1 == g2): + self.set_gloss(interpolate(g1, g2, alpha)) + + s0 = self.get_shadow() + s1 = mobject1.get_shadow() + s2 = mobject2.get_shadow() + if not (s0 == s1 == s2): + self.set_shadow(interpolate(s1, s2, alpha)) + return self + def become_partial(self, mobject, a, b): """ Set points in such a way as to become only @@ -1101,17 +1172,118 @@ def become_partial(self, mobject, a, b): def pointwise_become_partial(self, mobject, a, b): pass # To implement in subclass - def become(self, mobject, copy_submobjects=True): + def become(self, mobject): """ Edit points, colors and submobjects to be idential to another mobject """ - self.align_data(mobject) + self.align_submobjects(mobject) for sm1, sm2 in zip(self.get_family(), mobject.get_family()): - sm1.points = np.array(sm2.points) + sm1.set_points(sm2.points) sm1.interpolate_color(sm1, sm2, 1) return self + def prepare_for_animation(self): + pass + + def cleanup_from_animation(self): + pass + + # Operations touching shader uniforms + def affects_shader_info_id(func): + def wrapper(self): + for mob in self.get_family(): + func(mob) + mob.refresh_shader_wrapper_id() + return wrapper + + @affects_shader_info_id + def fix_in_frame(self): + self.is_fixed_in_frame = True + return self + + @affects_shader_info_id + def unfix_from_frame(self): + self.is_fixed_in_frame = False + return self + + @affects_shader_info_id + def apply_depth_test(self): + self.depth_test = True + return self + + @affects_shader_info_id + def deactivate_depth_test(self): + self.depth_test = False + return self + + # For shaders + def init_shader_data(self): + self.shader_data = np.zeros(len(self.points), dtype=self.shader_dtype) + self.shader_indices = None + self.shader_wrapper = ShaderWrapper( + vert_data=self.shader_data, + vert_file=self.vert_shader_file, + geom_file=self.geom_shader_file, + frag_file=self.frag_shader_file, + texture_paths=self.texture_paths, + depth_test=self.depth_test, + render_primative=self.render_primative, + ) + + def refresh_shader_wrapper_id(self): + self.shader_wrapper.refresh_id() + return self + + def get_blank_shader_data_array(self, size, name="shader_data"): + # If possible, try to populate an existing array, rather + # than recreating it each frame + arr = getattr(self, name) + if arr.size != size: + new_arr = np.resize(arr, size) + setattr(self, name, new_arr) + return new_arr + return arr + + def get_shader_wrapper(self): + self.shader_wrapper.vert_data = self.get_shader_data() + self.shader_wrapper.vert_indices = self.get_shader_vert_indices() + self.shader_wrapper.uniforms = self.get_shader_uniforms() + self.shader_wrapper.depth_test = self.depth_test + return self.shader_wrapper + + def get_shader_wrapper_list(self): + shader_wrappers = it.chain( + [self.get_shader_wrapper()], + *[sm.get_shader_wrapper_list() for sm in self.submobjects] + ) + batches = batch_by_property(shader_wrappers, lambda sw: sw.get_id()) + + result = [] + for wrapper_group, sid in batches: + shader_wrapper = wrapper_group[0] + if not shader_wrapper.is_valid(): + continue + shader_wrapper.combine_with(*wrapper_group[1:]) + if len(shader_wrapper.vert_data) > 0: + result.append(shader_wrapper) + return result + + def get_shader_uniforms(self): + return { + "is_fixed_in_frame": float(self.is_fixed_in_frame), + "gloss": self.gloss, + "shadow": self.shadow, + } + + def get_shader_data(self): + # Typically to be implemented by subclasses + # Must return a structured numpy array + return self.shader_data + + def get_shader_vert_indices(self): + return self.shader_indices + # Errors def throw_error_if_no_points(self): if self.has_no_points(): @@ -1127,3 +1299,29 @@ def __init__(self, *mobjects, **kwargs): raise Exception("All submobjects must be of type Mobject") Mobject.__init__(self, **kwargs) self.add(*mobjects) + + +class Point(Mobject): + CONFIG = { + "artificial_width": 1e-6, + "artificial_height": 1e-6, + } + + def __init__(self, location=ORIGIN, **kwargs): + Mobject.__init__(self, **kwargs) + self.set_location(location) + + def get_width(self): + return self.artificial_width + + def get_height(self): + return self.artificial_height + + def get_location(self): + return np.array(self.points[0]) + + def get_bounding_box_point(self, *args, **kwargs): + return self.get_location() + + def set_location(self, new_loc): + self.points = np.array(new_loc, ndmin=2, dtype=float) diff --git a/manimlib/mobject/mobject_update_utils.py b/manimlib/mobject/mobject_update_utils.py index 0f256f2c9d..71eb12e862 100644 --- a/manimlib/mobject/mobject_update_utils.py +++ b/manimlib/mobject/mobject_update_utils.py @@ -1,9 +1,9 @@ import inspect -import numpy as np from manimlib.constants import DEGREES from manimlib.constants import RIGHT from manimlib.mobject.mobject import Mobject +from manimlib.utils.simple_functions import clip def assert_is_mobject_method(method): @@ -81,7 +81,7 @@ def update(m, dt): if cycle: alpha = time_ratio % 1 else: - alpha = np.clip(time_ratio, 0, 1) + alpha = clip(time_ratio, 0, 1) if alpha >= 1: animation.finish() m.remove_updater(update) diff --git a/manimlib/mobject/number_line.py b/manimlib/mobject/number_line.py index 40577194a3..b5925fe58b 100644 --- a/manimlib/mobject/number_line.py +++ b/manimlib/mobject/number_line.py @@ -1,5 +1,3 @@ -import operator as op - from manimlib.constants import * from manimlib.mobject.geometry import Line from manimlib.mobject.numbers import DecimalNumber @@ -7,6 +5,7 @@ from manimlib.utils.bezier import interpolate from manimlib.utils.config_ops import digest_config from manimlib.utils.config_ops import merge_dicts_recursively +from manimlib.utils.iterables import list_difference_update from manimlib.utils.simple_functions import fdiv from manimlib.utils.space_ops import normalize @@ -14,70 +13,81 @@ class NumberLine(Line): CONFIG = { "color": LIGHT_GREY, - "x_min": -FRAME_X_RADIUS, - "x_max": FRAME_X_RADIUS, + "stroke_width": 2, + # List of 2 or 3 elements, x_min, x_max, step_size + "x_range": [-8, 8, 1], + # How big is one one unit of this number line in terms of absolute spacial distance "unit_size": 1, + "width": None, "include_ticks": True, "tick_size": 0.1, - "tick_frequency": 1, - # Defaults to value near x_min s.t. 0 is a tick - # TODO, rename this - "leftmost_tick": None, + "longer_tick_multiple": 1.5, + "tick_offset": 0, # Change name - "numbers_with_elongated_ticks": [0], + "numbers_with_elongated_ticks": [], "include_numbers": False, - "numbers_to_show": None, - "longer_tick_multiple": 2, - "number_at_center": 0, - "number_scale_val": 0.75, - "label_direction": DOWN, + "line_to_number_direction": DOWN, "line_to_number_buff": MED_SMALL_BUFF, "include_tip": False, - "tip_width": 0.25, - "tip_height": 0.25, + "tip_config": { + "width": 0.25, + "length": 0.25, + }, "decimal_number_config": { "num_decimal_places": 0, + "height": 0.25, }, "exclude_zero_from_default_numbers": False, } - def __init__(self, **kwargs): + def __init__(self, x_range=None, **kwargs): digest_config(self, kwargs) - start = self.unit_size * self.x_min * RIGHT - end = self.unit_size * self.x_max * RIGHT - Line.__init__(self, start, end, **kwargs) - self.shift(-self.number_to_point(self.number_at_center)) + if x_range is None: + x_range = self.x_range + if len(x_range) == 2: + x_range = [*x_range, 1] + + x_min, x_max, x_step = x_range + # A lot of old scenes pass in x_min or x_max explicitly, + # so this is just here to keep those workin + self.x_min = kwargs.get("x_min", x_min) + self.x_max = kwargs.get("x_max", x_max) + self.x_step = kwargs.get("x_step", x_step) + + super().__init__(self.x_min * RIGHT, self.x_max * RIGHT, **kwargs) + if self.width: + self.set_width(self.width) + else: + self.scale(self.unit_size) + self.center() - self.init_leftmost_tick() if self.include_tip: self.add_tip() + self.tip.set_stroke( + self.stroke_color, + self.stroke_width, + ) if self.include_ticks: - self.add_tick_marks() + self.add_ticks() if self.include_numbers: self.add_numbers() - def init_leftmost_tick(self): - if self.leftmost_tick is None: - self.leftmost_tick = op.mul( - self.tick_frequency, - np.ceil(self.x_min / self.tick_frequency) - ) - - def add_tick_marks(self): - tick_size = self.tick_size - self.tick_marks = VGroup(*[ - self.get_tick(x, tick_size) - for x in self.get_tick_numbers() - ]) - big_tick_size = tick_size * self.longer_tick_multiple - self.big_tick_marks = VGroup(*[ - self.get_tick(x, big_tick_size) - for x in self.numbers_with_elongated_ticks - ]) - self.add( - self.tick_marks, - self.big_tick_marks, - ) + def get_tick_range(self): + if self.include_tip: + x_max = self.x_max + else: + x_max = self.x_max + self.x_step + return np.arange(self.x_min, x_max, self.x_step) + + def add_ticks(self): + ticks = VGroup() + for x in self.get_tick_range(): + size = self.tick_size + if x in self.numbers_with_elongated_ticks: + size *= self.longer_tick_multiple + ticks.add(self.get_tick(x, size)) + self.add(ticks) + self.ticks = ticks def get_tick(self, x, size=None): if size is None: @@ -89,36 +99,18 @@ def get_tick(self, x, size=None): return result def get_tick_marks(self): - return VGroup( - *self.tick_marks, - *self.big_tick_marks, - ) - - def get_tick_numbers(self): - u = -1 if self.include_tip else 1 - return np.arange( - self.leftmost_tick, - self.x_max + u * self.tick_frequency / 2, - self.tick_frequency - ) + return self.tick_marks def number_to_point(self, number): alpha = float(number - self.x_min) / (self.x_max - self.x_min) - return interpolate( - self.get_start(), self.get_end(), alpha - ) + return interpolate(self.get_start(), self.get_end(), alpha) def point_to_number(self, point): - start_point, end_point = self.get_start_and_end() - full_vect = end_point - start_point - unit_vect = normalize(full_vect) - - def distance_from_start(p): - return np.dot(p - start_point, unit_vect) - + start, end = self.get_start_and_end() + unit_vect = normalize(end - start) proportion = fdiv( - distance_from_start(point), - distance_from_start(end_point) + np.dot(point - start, unit_vect), + np.dot(end - start, unit_vect), ) return interpolate(self.x_min, self.x_max, proportion) @@ -133,68 +125,49 @@ def p2n(self, point): def get_unit_size(self): return (self.x_max - self.x_min) / self.get_length() - def default_numbers_to_display(self): - if self.numbers_to_show is not None: - return self.numbers_to_show - numbers = np.arange( - np.floor(self.leftmost_tick), - np.ceil(self.x_max), - ) - if self.exclude_zero_from_default_numbers: - numbers = numbers[numbers != 0] - return numbers - - def get_number_mobject(self, number, + def get_number_mobject(self, x, number_config=None, - scale_val=None, direction=None, buff=None): + if number_config is None: + number_config = {} number_config = merge_dicts_recursively( - self.decimal_number_config, - number_config or {}, + self.decimal_number_config, number_config ) - if scale_val is None: - scale_val = self.number_scale_val if direction is None: - direction = self.label_direction - buff = buff or self.line_to_number_buff + direction = self.line_to_number_direction + if buff is None: + buff = self.line_to_number_buff - num_mob = DecimalNumber(number, **number_config) - num_mob.scale(scale_val) + num_mob = DecimalNumber(x, **number_config) num_mob.next_to( - self.number_to_point(number), + self.number_to_point(x), direction=direction, buff=buff ) + if x < 0 and self.line_to_number_direction[0] == 0: + # Align without the minus sign + num_mob.shift(num_mob[0].get_width() * LEFT / 2) return num_mob - def get_number_mobjects(self, *numbers, **kwargs): - if len(numbers) == 0: - numbers = self.default_numbers_to_display() - return VGroup(*[ - self.get_number_mobject(number, **kwargs) - for number in numbers - ]) + def add_numbers(self, x_values=None, excluding=None, **kwargs): + if x_values is None: + x_values = self.get_tick_range() + if excluding is not None: + x_values = list_difference_update(x_values, excluding) - def get_labels(self): - return self.get_number_mobjects() - - def add_numbers(self, *numbers, **kwargs): - self.numbers = self.get_number_mobjects( - *numbers, **kwargs - ) + self.numbers = VGroup() + for x in x_values: + self.numbers.add(self.get_number_mobject(x, **kwargs)) self.add(self.numbers) - return self + return self.numbers class UnitInterval(NumberLine): CONFIG = { - "x_min": 0, - "x_max": 1, - "unit_size": 6, - "tick_frequency": 0.1, + "x_range": [0, 1, 0.1], + "unit_size": 10, "numbers_with_elongated_ticks": [0, 1], - "number_at_center": 0.5, "decimal_number_config": { "num_decimal_places": 1, } diff --git a/manimlib/mobject/numbers.py b/manimlib/mobject/numbers.py index 4c14b4020a..bab189d839 100644 --- a/manimlib/mobject/numbers.py +++ b/manimlib/mobject/numbers.py @@ -3,8 +3,11 @@ from manimlib.mobject.types.vectorized_mobject import VMobject +# TODO, have this cache TexMobjects class DecimalNumber(VMobject): CONFIG = { + "stroke_width": 0, + "fill_opacity": 1.0, "num_decimal_places": 2, "include_sign": False, "group_with_commas": True, @@ -13,6 +16,7 @@ class DecimalNumber(VMobject): "unit": None, # Aligned to bottom unless it starts with "^" "include_background_rectangle": False, "edge_to_fix": LEFT, + "height": 0.4, } def __init__(self, number=0, **kwargs): @@ -33,10 +37,7 @@ def __init__(self, number=0, **kwargs): else: num_string = num_string[1:] - self.add(*[ - SingleStringTexMobject(char, **kwargs) - for char in num_string - ]) + self.add(*map(SingleStringTexMobject, num_string)) # Add non-numerical bits if self.show_ellipsis: @@ -63,12 +64,16 @@ def __init__(self, number=0, **kwargs): for i, c in enumerate(num_string): if c == "-" and len(num_string) > i + 1: self[i].align_to(self[i + 1], UP) - self[i].shift(self[i+1].get_height() * DOWN / 2) + self[i].shift(self[i + 1].get_height() * DOWN / 2) elif c == ",": self[i].shift(self[i].get_height() * DOWN / 2) if self.unit and self.unit.startswith("^"): self.unit_sign.align_to(self, UP) - # + + # Styling + self.set_height(self.height) + self.init_colors() + if self.include_background_rectangle: self.add_background_rectangle() @@ -119,9 +124,11 @@ def set_value(self, number, **config): ) new_decimal.move_to(self, self.edge_to_fix) new_decimal.match_style(self) + if self.is_fixed_in_frame: + new_decimal.fix_in_frame() old_family = self.get_family() - self.submobjects = new_decimal.submobjects + self.set_submobjects(new_decimal.submobjects) for mob in old_family: # Dumb hack...due to how scene handles families # of animated mobjects diff --git a/manimlib/mobject/probability.py b/manimlib/mobject/probability.py index 662154c157..55705af1c5 100644 --- a/manimlib/mobject/probability.py +++ b/manimlib/mobject/probability.py @@ -7,7 +7,7 @@ from manimlib.mobject.svg.tex_mobject import TextMobject from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.utils.color import color_gradient -from manimlib.utils.iterables import tuplify +from manimlib.utils.iterables import listify EPSILON = 0.0001 @@ -37,7 +37,7 @@ def add_label(self, label): self.label = label def complete_p_list(self, p_list): - new_p_list = list(tuplify(p_list)) + new_p_list = listify(p_list) remainder = 1.0 - sum(new_p_list) if abs(remainder) > EPSILON: new_p_list.append(remainder) diff --git a/manimlib/mobject/svg/brace.py b/manimlib/mobject/svg/brace.py index c048d14326..c6a0796f08 100644 --- a/manimlib/mobject/svg/brace.py +++ b/manimlib/mobject/svg/brace.py @@ -97,7 +97,7 @@ def __init__(self, obj, text, brace_direction=DOWN, **kwargs): self.label.scale(self.label_scale) self.brace.put_at_tip(self.label) - self.submobjects = [self.brace, self.label] + self.set_submobjects([self.brace, self.label]) def creation_anim(self, label_anim=FadeIn, brace_anim=GrowFromCenter): return AnimationGroup(brace_anim(self.brace), label_anim(self.label)) @@ -128,7 +128,7 @@ def copy(self): copy_mobject = copy.copy(self) copy_mobject.brace = self.brace.copy() copy_mobject.label = self.label.copy() - copy_mobject.submobjects = [copy_mobject.brace, copy_mobject.label] + copy_mobject.set_submobjects([copy_mobject.brace, copy_mobject.label]) return copy_mobject diff --git a/manimlib/mobject/svg/drawings.py b/manimlib/mobject/svg/drawings.py index 866250853a..ff703869a2 100644 --- a/manimlib/mobject/svg/drawings.py +++ b/manimlib/mobject/svg/drawings.py @@ -1,5 +1,5 @@ import itertools as it -import string +from colour import Color from manimlib.animation.animation import Animation from manimlib.animation.rotation import Rotating @@ -25,6 +25,26 @@ from manimlib.utils.space_ops import angle_of_vector from manimlib.utils.space_ops import complex_to_R3 from manimlib.utils.space_ops import rotate_vector +from manimlib.utils.space_ops import center_of_mass + + +class Checkmark(TextMobject): + CONFIG = { + "color": GREEN + } + + def __init__(self, **kwargs): + super().__init__("\\ding{51}") + + +class Exmark(TextMobject): + CONFIG = { + "color": RED + } + + def __init__(self, **kwargs): + super().__init__("\\ding{55}") + class Lightbulb(SVGMobject): @@ -91,7 +111,7 @@ class Speedometer(VMobject): "needle_color": YELLOW, } - def generate_points(self): + def init_points(self): start_angle = np.pi / 2 + self.arc_angle / 2 end_angle = np.pi / 2 - self.arc_angle / 2 self.add(Arc( @@ -391,13 +411,13 @@ def __init__(self, clock, **kwargs): hour_radians = -self.hours_passed * 2 * np.pi / 12 self.hour_rotation = Rotating( clock.hour_hand, - radians=hour_radians, + angle=hour_radians, **rot_kwargs ) self.hour_rotation.begin() self.minute_rotation = Rotating( clock.minute_hand, - radians=12 * hour_radians, + angle=12 * hour_radians, **rot_kwargs ) self.minute_rotation.begin() @@ -429,7 +449,7 @@ def __init__(self, **kwargs): raise Exception("Must invoke Bubble subclass") try: SVGMobject.__init__(self, **kwargs) - except IOError as err: + except IOError: self.file_name = os.path.join(FILE_DIR, self.file_name) SVGMobject.__init__(self, **kwargs) self.center() @@ -439,6 +459,7 @@ def __init__(self, **kwargs): self.flip() self.direction_was_specified = ("direction" in kwargs) self.content = Mobject() + self.refresh_triangulation() def get_tip(self): # TODO, find a better way @@ -457,6 +478,7 @@ def move_tip_to(self, point): def flip(self, axis=UP): Mobject.flip(self, axis=axis) + self.refresh_triangulation() if abs(axis[1]) > 0: self.direction = -np.array(self.direction) return self @@ -467,7 +489,7 @@ def pin_to(self, mobject): can_flip = not self.direction_was_specified if want_to_flip and can_flip: self.flip() - boundary_point = mobject.get_critical_point(UP - self.direction) + boundary_point = mobject.get_bounding_box_point(UP - self.direction) vector_from_center = 1.0 * (boundary_point - mob_center) self.move_tip_to(mob_center + vector_from_center) return self @@ -857,16 +879,16 @@ class PlayingCard(VGroup): "card_height_to_symbol_height": 7, "card_width_to_corner_num_width": 10, "card_height_to_corner_num_height": 10, - "color": LIGHT_GREY, + "color": GREY_A, "turned_over": False, "possible_suits": ["hearts", "diamonds", "spades", "clubs"], "possible_values": list(map(str, list(range(2, 11)))) + ["J", "Q", "K", "A"], } def __init__(self, key=None, **kwargs): - VGroup.__init__(self, key=key, **kwargs) + VGroup.__init__(self, **kwargs) - def generate_points(self): + self.key = key self.add(Rectangle( height=self.height, width=self.height / self.height_to_width, @@ -896,7 +918,7 @@ def get_value(self): value = self.key[:-1] else: value = random.choice(self.possible_values) - value = string.upper(str(value)) + value = str(value).upper() if value == "1": value = "A" if value not in self.possible_values: @@ -910,7 +932,7 @@ def get_value(self): } try: self.numerical_value = int(value) - except: + except Exception: self.numerical_value = face_card_to_value[value] return value @@ -919,9 +941,9 @@ def get_symbol(self): if suit is None: if self.key is not None: suit = dict([ - (string.upper(s[0]), s) + (s[0].upper(), s) for s in self.possible_suits - ])[string.upper(self.key[-1])] + ])[self.key[-1].upper()] else: suit = random.choice(self.possible_suits) if suit not in self.possible_suits: @@ -994,7 +1016,7 @@ def get_number_design(self, value, symbol): return design def get_face_card_design(self, value, symbol): - from for_3b1b_videos.pi_creature import PiCreature + from manimlib.for_3b1b_videos.pi_creature import PiCreature sub_rect = Rectangle( stroke_color=BLACK, fill_opacity=0, @@ -1005,6 +1027,8 @@ def get_face_card_design(self, value, symbol): # pi_color = average_color(symbol.get_color(), GREY) pi_color = symbol.get_color() + if Color(pi_color) == Color(BLACK): + pi_color = GREY_D pi_mode = { "J": "plain", "Q": "thinking", diff --git a/manimlib/mobject/svg/svg_mobject.py b/manimlib/mobject/svg/svg_mobject.py index 3759182f83..79fcc9a810 100644 --- a/manimlib/mobject/svg/svg_mobject.py +++ b/manimlib/mobject/svg/svg_mobject.py @@ -2,10 +2,17 @@ import re import string import warnings +import os +import hashlib from xml.dom import minidom -from manimlib.constants import * +from manimlib.constants import DEFAULT_STROKE_WIDTH +from manimlib.constants import ORIGIN, UP, DOWN, LEFT, RIGHT +from manimlib.constants import BLACK +from manimlib.constants import WHITE +import manimlib.constants as consts + from manimlib.mobject.geometry import Circle from manimlib.mobject.geometry import Rectangle from manimlib.mobject.geometry import RoundedRectangle @@ -13,7 +20,6 @@ from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.utils.color import * from manimlib.utils.config_ops import digest_config -from manimlib.utils.config_ops import digest_locals def string_to_numbers(num_string): @@ -34,37 +40,46 @@ class SVGMobject(VMobject): # Must be filled in in a subclass, or when called "file_name": None, "unpack_groups": True, # if False, creates a hierarchy of VGroups + # TODO, style components should be read in, not defaulted "stroke_width": DEFAULT_STROKE_WIDTH, "fill_opacity": 1.0, - # "fill_color" : LIGHT_GREY, } def __init__(self, file_name=None, **kwargs): digest_config(self, kwargs) self.file_name = file_name or self.file_name self.ensure_valid_file() - VMobject.__init__(self, **kwargs) + super().__init__(**kwargs) self.move_into_position() def ensure_valid_file(self): - if self.file_name is None: + file_name = self.file_name + if file_name is None: raise Exception("Must specify file for SVGMobject") possible_paths = [ - os.path.join(os.path.join("assets", "svg_images"), self.file_name), - os.path.join(os.path.join("assets", "svg_images"), self.file_name + ".svg"), - os.path.join(os.path.join("assets", "svg_images"), self.file_name + ".xdv"), - self.file_name, + os.path.join(os.path.join("assets", "svg_images"), file_name), + os.path.join(os.path.join("assets", "svg_images"), file_name + ".svg"), + os.path.join(os.path.join("assets", "svg_images"), file_name + ".xdv"), + file_name, ] for path in possible_paths: if os.path.exists(path): self.file_path = path return - raise IOError("No file matching %s in image directory" % - self.file_name) + raise IOError(f"No file matching {file_name} in image directory") - def generate_points(self): + def move_into_position(self): + if self.should_center: + self.center() + if self.height is not None: + self.set_height(self.height) + if self.width is not None: + self.set_width(self.width) + + def init_points(self): doc = minidom.parse(self.file_path) self.ref_to_element = {} + for svg in doc.getElementsByTagName("svg"): mobjects = self.get_mobjects_from(svg) if self.unpack_groups: @@ -122,7 +137,7 @@ def use_to_mobjects(self, use_element): # Remove initial "#" character ref = use_element.getAttribute("xlink:href")[1:] if ref not in self.ref_to_element: - warnings.warn("%s not recognized" % ref) + warnings.warn(f"{ref} not recognized") return VGroup() return self.get_mobjects_from( self.ref_to_element[ref] @@ -136,15 +151,12 @@ def attribute_to_float(self, attr): return float(stripped_attr) def polygon_to_mobject(self, polygon_element): - # TODO, This seems hacky... path_string = polygon_element.getAttribute("points") for digit in string.digits: - path_string = path_string.replace(" " + digit, " L" + digit) + path_string = path_string.replace(f" {digit}", f"{digit} L") path_string = "M" + path_string return self.path_string_to_mobject(path_string) - # - def circle_to_mobject(self, circle_element): x, y, r = [ self.attribute_to_float( @@ -224,13 +236,14 @@ def rect_to_mobject(self, rect_element): return mob def handle_transforms(self, element, mobject): + # TODO, this could use some cleaning... x, y = 0, 0 try: x = self.attribute_to_float(element.getAttribute('x')) # Flip y y = -self.attribute_to_float(element.getAttribute('y')) - mobject.shift(x * RIGHT + y * UP) - except: + mobject.shift([x, y, 0]) + except Exception: pass transform = element.getAttribute('transform') @@ -307,125 +320,115 @@ def update_ref_to_element(self, defs): new_refs = dict([(e.getAttribute('id'), e) for e in self.get_all_childNodes_have_id(defs)]) self.ref_to_element.update(new_refs) - def move_into_position(self): - if self.should_center: - self.center() - if self.height is not None: - self.set_height(self.height) - if self.width is not None: - self.set_width(self.width) - class VMobjectFromSVGPathstring(VMobject): + CONFIG = { + "long_lines": True, + "should_subdivide_sharp_curves": False, + "should_remove_null_curves": False, + } + def __init__(self, path_string, **kwargs): - digest_locals(self) - VMobject.__init__(self, **kwargs) - - def get_path_commands(self): - result = [ - "M", # moveto - "L", # lineto - "H", # horizontal lineto - "V", # vertical lineto - "C", # curveto - "S", # smooth curveto - "Q", # quadratic Bezier curve - "T", # smooth quadratic Bezier curveto - "A", # elliptical Arc - "Z", # closepath - ] - result += [s.lower() for s in result] - return result + self.path_string = path_string + super().__init__(**kwargs) + + def init_points(self): + # TODO, move this caching operation + # higher up to Mobject somehow. + hasher = hashlib.sha256() + hasher.update(self.path_string.encode()) + path_hash = hasher.hexdigest()[:16] + + filepath = os.path.join( + consts.MOBJECT_POINTS_DIR, + f"{path_hash}.npy" + ) - def generate_points(self): - pattern = "[%s]" % ("".join(self.get_path_commands())) - pairs = list(zip( + if os.path.exists(filepath): + self.points = np.load(filepath) + else: + self.relative_point = np.array(ORIGIN) + for command, coord_string in self.get_commands_and_coord_strings(): + new_points = self.string_to_points(command, coord_string) + self.handle_command(command, new_points) + if self.should_subdivide_sharp_curves: + # For a healthy triangulation later + self.subdivide_sharp_curves() + if self.should_remove_null_curves: + # Get rid of any null curves + self.points = self.get_points_without_null_curves() + # SVG treats y-coordinate differently + self.stretch(-1, 1, about_point=ORIGIN) + # Save to a file for future use + np.save(filepath, self.points) + + def get_commands_and_coord_strings(self): + all_commands = list(self.get_command_to_function_map().keys()) + all_commands += [c.lower() for c in all_commands] + pattern = "[{}]".format("".join(all_commands)) + return zip( re.findall(pattern, self.path_string), re.split(pattern, self.path_string)[1:] - )) - # Which mobject should new points be added to - self = self - for command, coord_string in pairs: - self.handle_command(command, coord_string) - # people treat y-coordinate differently - self.rotate(np.pi, RIGHT, about_point=ORIGIN) - - def handle_command(self, command, coord_string): - isLower = command.islower() - command = command.upper() - # new_points are the points that will be added to the curr_points - # list. This variable may get modified in the conditionals below. - points = self.points - new_points = self.string_to_points(coord_string) - - if isLower and len(points) > 0: - new_points += points[-1] - - if command == "M": # moveto - self.start_new_path(new_points[0]) - if len(new_points) <= 1: - return - - # Draw relative line-to values. - points = self.points - new_points = new_points[1:] - command = "L" - - for p in new_points: - if isLower: - # Treat everything as relative line-to until empty - p[0] += self.points[-1, 0] - p[1] += self.points[-1, 1] - self.add_line_to(p) - return - - elif command in ["L", "H", "V"]: # lineto - if command == "H": - new_points[0, 1] = points[-1, 1] - elif command == "V": - if isLower: - new_points[0, 0] -= points[-1, 0] - new_points[0, 0] += points[-1, 1] - new_points[0, 1] = new_points[0, 0] - new_points[0, 0] = points[-1, 0] - self.add_line_to(new_points[0]) - return - - if command == "C": # curveto - pass # Yay! No action required - elif command in ["S", "T"]: # smooth curveto - self.add_smooth_curve_to(*new_points) - # handle1 = points[-1] + (points[-1] - points[-2]) - # new_points = np.append([handle1], new_points, axis=0) - return - elif command == "Q": # quadratic Bezier curve - # TODO, this is a suboptimal approximation - new_points = np.append([new_points[0]], new_points, axis=0) - elif command == "A": # elliptical Arc - raise Exception("Not implemented") - elif command == "Z": # closepath - return - - # Add first three points - self.add_cubic_bezier_curve_to(*new_points[0:3]) + ) - # Handle situations where there's multiple relative control points - if len(new_points) > 3: - # Add subsequent offset points relatively. - for i in range(3, len(new_points), 3): - if isLower: - new_points[i:i + 3] -= points[-1] - new_points[i:i + 3] += new_points[i - 1] - self.add_cubic_bezier_curve_to(*new_points[i:i+3]) + def handle_command(self, command, new_points): + if command.islower(): + # Treat it as a relative command + new_points += self.relative_point + + func, n_points = self.command_to_function(command) + func(*new_points[:n_points]) + leftover_points = new_points[n_points:] + + # Recursively handle the rest of the points + if len(leftover_points) > 0: + if command.upper() == "M": + # Treat following points as relative line coordinates + command = "l" + if command.islower(): + leftover_points -= self.relative_point + self.relative_point = self.points[-1] + self.handle_command(command, leftover_points) + else: + # Command is over, reset for future relative commands + self.relative_point = self.points[-1] - def string_to_points(self, coord_string): + def string_to_points(self, command, coord_string): numbers = string_to_numbers(coord_string) - if len(numbers) % 2 == 1: - numbers.append(0) - num_points = len(numbers) // 2 - result = np.zeros((num_points, self.dim)) - result[:, :2] = np.array(numbers).reshape((num_points, 2)) + if command.upper() in ["H", "V"]: + i = {"H": 0, "V": 1}[command.upper()] + xy = np.zeros((len(numbers), 2)) + xy[:, i] = numbers + if command.isupper(): + xy[:, 1 - i] = self.relative_point[1 - i] + elif command.upper() == "A": + raise Exception("Not implemented") + else: + xy = np.array(numbers).reshape((len(numbers) // 2, 2)) + result = np.zeros((xy.shape[0], self.dim)) + result[:, :2] = xy return result + def command_to_function(self, command): + return self.get_command_to_function_map()[command.upper()] + + def get_command_to_function_map(self): + """ + Associates svg command to VMobject function, and + the number of arguments it takes in + """ + return { + "M": (self.start_new_path, 1), + "L": (self.add_line_to, 1), + "H": (self.add_line_to, 1), + "V": (self.add_line_to, 1), + "C": (self.add_cubic_bezier_curve_to, 3), + "S": (self.add_smooth_cubic_curve_to, 2), + "Q": (self.add_quadratic_bezier_curve_to, 2), + "T": (self.add_smooth_curve_to, 1), + "A": (self.add_quadratic_bezier_curve_to, 2), # TODO + "Z": (self.close_path, 0), + } + def get_original_path_string(self): return self.path_string diff --git a/manimlib/mobject/svg/tex_mobject.py b/manimlib/mobject/svg/tex_mobject.py index 60b299e6d1..f2f7936d19 100644 --- a/manimlib/mobject/svg/tex_mobject.py +++ b/manimlib/mobject/svg/tex_mobject.py @@ -16,19 +16,17 @@ class TexSymbol(VMobjectFromSVGPathstring): - """ - Purely a renaming of VMobjectFromSVGPathstring - """ - pass + CONFIG = { + "should_subdivide_sharp_curves": True, + "should_remove_null_curves": True, + } class SingleStringTexMobject(SVGMobject): CONFIG = { "template_tex_file_body": TEMPLATE_TEX_FILE_BODY, - "stroke_width": 0, "fill_opacity": 1.0, - "background_stroke_width": 1, - "background_stroke_color": BLACK, + "stroke_width": 0, "should_center": True, "height": None, "organize_left_to_right": False, @@ -144,9 +142,8 @@ def __init__(self, *tex_strings, **kwargs): digest_config(self, kwargs) tex_strings = self.break_up_tex_strings(tex_strings) self.tex_strings = tex_strings - SingleStringTexMobject.__init__( - self, self.arg_separator.join(tex_strings), **kwargs - ) + tex_string = self.arg_separator.join(tex_strings) + super().__init__(tex_string, **kwargs) self.break_up_by_substrings() self.set_color_by_tex_to_color_map(self.tex_to_color_map) @@ -173,6 +170,10 @@ def break_up_by_substrings(self): deeper based on the structure of tex_strings (as a list of tex_strings) """ + if len(self.tex_strings) == 1: + submob = self.copy() + self.set_submobjects([submob]) + return self new_submobjects = [] curr_index = 0 config = dict(self.CONFIG) @@ -185,14 +186,14 @@ def break_up_by_substrings(self): # For cases like empty tex_strings, we want the corresponing # part of the whole TexMobject to be a VectorizedPoint # positioned in the right part of the TexMobject - sub_tex_mob.submobjects = [VectorizedPoint()] + sub_tex_mob.set_submobjects([VectorizedPoint()]) last_submob_index = min(curr_index, len(self.submobjects) - 1) sub_tex_mob.move_to(self.submobjects[last_submob_index], RIGHT) else: - sub_tex_mob.submobjects = self.submobjects[curr_index:new_index] + sub_tex_mob.set_submobjects(self.submobjects[curr_index:new_index]) new_submobjects.append(sub_tex_mob) curr_index = new_index - self.submobjects = new_submobjects + self.set_submobjects(new_submobjects) return self def get_parts_by_tex(self, tex, substring=True, case_sensitive=True): @@ -205,7 +206,11 @@ def test(tex1, tex2): else: return tex1 == tex2 - return VGroup(*[m for m in self.submobjects if test(tex, m.get_tex_string())]) + return VGroup(*[ + m + for m in self.submobjects + if isinstance(m, SingleStringTexMobject) and test(tex, m.get_tex_string()) + ]) def get_part_by_tex(self, tex, **kwargs): all_parts = self.get_parts_by_tex(tex, **kwargs) @@ -229,6 +234,10 @@ def set_color_by_tex_to_color_map(self, texs_to_color_map, **kwargs): self.set_color_by_tex(tex, color, **kwargs) return self + def set_bstroke(self, color=BLACK, width=4): + self.set_stroke(color, width, background=True) + return self + def index_of_part(self, part): split_self = self.split() if part not in split_self: diff --git a/manimlib/mobject/svg/text_mobject.py b/manimlib/mobject/svg/text_mobject.py index 2be9b7589c..8b6ff7d124 100644 --- a/manimlib/mobject/svg/text_mobject.py +++ b/manimlib/mobject/svg/text_mobject.py @@ -9,6 +9,9 @@ from manimlib.utils.config_ops import digest_config +TEXT_MOB_SCALE_FACTOR = 0.05 + + class TextSetting(object): def __init__(self, start, end, font, slant, weight, line_num=-1): self.start = start @@ -45,8 +48,23 @@ def __init__(self, text, **config): self.lsh = self.size if self.lsh == -1 else self.lsh file_name = self.text2svg() + self.remove_last_M(file_name) SVGMobject.__init__(self, file_name, **config) + nppc = self.n_points_per_curve + for each in self: + if len(each.points) == 0: + continue + points = each.points + last = points[0] + each.clear_points() + for index, point in enumerate(points): + each.append_points([point]) + if index != len(points) - 1 and (index + 1) % nppc == 0 and any(point != points[index+1]): + each.add_line_to(last) + last = points[index + 1] + each.add_line_to(last) + if self.t2c: self.set_color_by_t2c() if self.gradient: @@ -55,7 +73,15 @@ def __init__(self, text, **config): self.set_color_by_t2g() # anti-aliasing - self.scale(0.1) + if self.height is None: + self.scale(TEXT_MOB_SCALE_FACTOR) + + def remove_last_M(self, file_name): + with open(file_name, 'r') as fpr: + content = fpr.read() + content = re.sub(r'Z M [^A-Za-z]*? "\/>', 'Z "/>', content) + with open(file_name, 'w') as fpw: + fpw.write(content) def find_indexes(self, word): m = re.match(r'\[([0-9\-]{0,}):([0-9\-]{0,})\]', word) diff --git a/manimlib/mobject/three_d_shading_utils.py b/manimlib/mobject/three_d_shading_utils.py deleted file mode 100644 index a0fe43c385..0000000000 --- a/manimlib/mobject/three_d_shading_utils.py +++ /dev/null @@ -1,56 +0,0 @@ -import numpy as np - -from manimlib.constants import ORIGIN -from manimlib.utils.space_ops import get_unit_normal - - -def get_3d_vmob_gradient_start_and_end_points(vmob): - return ( - get_3d_vmob_start_corner(vmob), - get_3d_vmob_end_corner(vmob), - ) - - -def get_3d_vmob_start_corner_index(vmob): - return 0 - - -def get_3d_vmob_end_corner_index(vmob): - return ((len(vmob.points) - 1) // 6) * 3 - - -def get_3d_vmob_start_corner(vmob): - if vmob.get_num_points() == 0: - return np.array(ORIGIN) - return vmob.points[get_3d_vmob_start_corner_index(vmob)] - - -def get_3d_vmob_end_corner(vmob): - if vmob.get_num_points() == 0: - return np.array(ORIGIN) - return vmob.points[get_3d_vmob_end_corner_index(vmob)] - - -def get_3d_vmob_unit_normal(vmob, point_index): - n_points = vmob.get_num_points() - if vmob.get_num_points() == 0: - return np.array(ORIGIN) - i = point_index - im1 = i - 1 if i > 0 else (n_points - 2) - ip1 = i + 1 if i < (n_points - 1) else 1 - return get_unit_normal( - vmob.points[ip1] - vmob.points[i], - vmob.points[im1] - vmob.points[i], - ) - - -def get_3d_vmob_start_corner_unit_normal(vmob): - return get_3d_vmob_unit_normal( - vmob, get_3d_vmob_start_corner_index(vmob) - ) - - -def get_3d_vmob_end_corner_unit_normal(vmob): - return get_3d_vmob_unit_normal( - vmob, get_3d_vmob_end_corner_index(vmob) - ) diff --git a/manimlib/mobject/three_d_utils.py b/manimlib/mobject/three_d_utils.py deleted file mode 100644 index 3138aaa1bf..0000000000 --- a/manimlib/mobject/three_d_utils.py +++ /dev/null @@ -1,61 +0,0 @@ -import numpy as np - -from manimlib.constants import ORIGIN -from manimlib.constants import UP -from manimlib.utils.space_ops import get_norm -from manimlib.utils.space_ops import get_unit_normal - - -def get_3d_vmob_gradient_start_and_end_points(vmob): - return ( - get_3d_vmob_start_corner(vmob), - get_3d_vmob_end_corner(vmob), - ) - - -def get_3d_vmob_start_corner_index(vmob): - return 0 - - -def get_3d_vmob_end_corner_index(vmob): - return ((len(vmob.points) - 1) // 6) * 3 - - -def get_3d_vmob_start_corner(vmob): - if vmob.get_num_points() == 0: - return np.array(ORIGIN) - return vmob.points[get_3d_vmob_start_corner_index(vmob)] - - -def get_3d_vmob_end_corner(vmob): - if vmob.get_num_points() == 0: - return np.array(ORIGIN) - return vmob.points[get_3d_vmob_end_corner_index(vmob)] - - -def get_3d_vmob_unit_normal(vmob, point_index): - n_points = vmob.get_num_points() - if len(vmob.get_anchors()) <= 2: - return np.array(UP) - i = point_index - im3 = i - 3 if i > 2 else (n_points - 4) - ip3 = i + 3 if i < (n_points - 3) else 3 - unit_normal = get_unit_normal( - vmob.points[ip3] - vmob.points[i], - vmob.points[im3] - vmob.points[i], - ) - if get_norm(unit_normal) == 0: - return np.array(UP) - return unit_normal - - -def get_3d_vmob_start_corner_unit_normal(vmob): - return get_3d_vmob_unit_normal( - vmob, get_3d_vmob_start_corner_index(vmob) - ) - - -def get_3d_vmob_end_corner_unit_normal(vmob): - return get_3d_vmob_unit_normal( - vmob, get_3d_vmob_end_corner_index(vmob) - ) diff --git a/manimlib/mobject/three_dimensions.py b/manimlib/mobject/three_dimensions.py index 7b49ec39fa..2918a6ac2f 100644 --- a/manimlib/mobject/three_dimensions.py +++ b/manimlib/mobject/three_dimensions.py @@ -1,148 +1,188 @@ +import math + from manimlib.constants import * -from manimlib.mobject.geometry import Square +from manimlib.mobject.types.surface import ParametricSurface +from manimlib.mobject.types.surface import SGroup from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VMobject -from manimlib.utils.iterables import tuplify +from manimlib.utils.config_ops import digest_config +from manimlib.utils.space_ops import get_norm from manimlib.utils.space_ops import z_to_vector -############## - -class ThreeDVMobject(VMobject): +class SurfaceMesh(VGroup): CONFIG = { - "shade_in_3d": True, + "resolution": (21, 21), + "stroke_width": 1, + "normal_nudge": 1e-2, + "depth_test": True, } + def __init__(self, uv_surface, **kwargs): + if not isinstance(uv_surface, ParametricSurface): + raise Exception("uv_surface must be of type ParametricSurface") + self.uv_surface = uv_surface + super().__init__(**kwargs) + + def init_points(self): + uv_surface = self.uv_surface + + full_nu, full_nv = uv_surface.resolution + part_nu, part_nv = self.resolution + u_indices = np.linspace(0, full_nu, part_nu).astype(int) + v_indices = np.linspace(0, full_nv, part_nv).astype(int) + + points, du_points, dv_points = uv_surface.get_surface_points_and_nudged_points() + normals = uv_surface.get_unit_normals() + nudge = 1e-2 + nudged_points = points + nudge * normals + + for ui in u_indices: + path = VMobject() + full_ui = full_nv * ui + path.set_points_smoothly(nudged_points[full_ui:full_ui + full_nv]) + self.add(path) + for vi in v_indices: + path = VMobject() + path.set_points_smoothly(nudged_points[vi::full_nv]) + self.add(path) + + +# Sphere, cylinder, cube, prism + +class ArglessSurface(ParametricSurface): + def __init__(self, **kwargs): + super().__init__(self.uv_func, **kwargs) + + def uv_func(self, u, v): + # To be implemented by a subclass + return [u, v, 0] -class ParametricSurface(VGroup): + +class Sphere(ArglessSurface): CONFIG = { - "u_min": 0, - "u_max": 1, - "v_min": 0, - "v_max": 1, - "resolution": 32, - "surface_piece_config": {}, - "fill_color": BLUE_D, - "fill_opacity": 1.0, - "checkerboard_colors": [BLUE_D, BLUE_E], - "stroke_color": LIGHT_GREY, - "stroke_width": 0.5, - "should_make_jagged": False, - "pre_function_handle_to_anchor_scale_factor": 0.00001, + "resolution": (101, 51), + "radius": 1, + "u_range": (0, TAU), + "v_range": (0, PI), } - def __init__(self, func, **kwargs): - VGroup.__init__(self, **kwargs) - self.func = func - self.setup_in_uv_space() - self.apply_function(lambda p: func(p[0], p[1])) - if self.should_make_jagged: - self.make_jagged() - - def get_u_values_and_v_values(self): - res = tuplify(self.resolution) - if len(res) == 1: - u_res = v_res = res[0] - else: - u_res, v_res = res - u_min = self.u_min - u_max = self.u_max - v_min = self.v_min - v_max = self.v_max - - u_values = np.linspace(u_min, u_max, u_res + 1) - v_values = np.linspace(v_min, v_max, v_res + 1) - - return u_values, v_values - - def setup_in_uv_space(self): - u_values, v_values = self.get_u_values_and_v_values() - faces = VGroup() - for i in range(len(u_values) - 1): - for j in range(len(v_values) - 1): - u1, u2 = u_values[i:i + 2] - v1, v2 = v_values[j:j + 2] - face = ThreeDVMobject() - face.set_points_as_corners([ - [u1, v1, 0], - [u2, v1, 0], - [u2, v2, 0], - [u1, v2, 0], - [u1, v1, 0], - ]) - faces.add(face) - face.u_index = i - face.v_index = j - face.u1 = u1 - face.u2 = u2 - face.v1 = v1 - face.v2 = v2 - faces.set_fill( - color=self.fill_color, - opacity=self.fill_opacity - ) - faces.set_stroke( - color=self.stroke_color, - width=self.stroke_width, - opacity=self.stroke_opacity, - ) - self.add(*faces) - if self.checkerboard_colors: - self.set_fill_by_checkerboard(*self.checkerboard_colors) + def uv_func(self, u, v): + return self.radius * np.array([ + np.cos(u) * np.sin(v), + np.sin(u) * np.sin(v), + -np.cos(v) + ]) - def set_fill_by_checkerboard(self, *colors, opacity=None): - n_colors = len(colors) - for face in self: - c_index = (face.u_index + face.v_index) % n_colors - face.set_fill(colors[c_index], opacity=opacity) +class Torus(ArglessSurface): + CONFIG = { + "u_range": (0, TAU), + "v_range": (0, TAU), + "r1": 3, + "r2": 1, + } -# Specific shapes + def uv_func(self, u, v): + P = np.array([math.cos(u), math.sin(u), 0]) + return (self.r1 - self.r2 * math.cos(v)) * P - math.sin(v) * OUT -class Sphere(ParametricSurface): +class Cylinder(ArglessSurface): CONFIG = { - "resolution": (12, 24), + "height": 2, "radius": 1, - "u_min": 0.001, - "u_max": PI - 0.001, - "v_min": 0, - "v_max": TAU, + "axis": OUT, + "u_range": (0, TAU), + "v_range": (-1, 1), + "resolution": (101, 11), } - def __init__(self, **kwargs): - ParametricSurface.__init__( - self, self.func, **kwargs + def init_points(self): + super().init_points() + self.scale(self.radius) + self.set_depth(self.height, stretch=True) + self.apply_matrix(z_to_vector(self.axis)) + return self + + def uv_func(self, u, v): + return [np.cos(u), np.sin(u), v] + + +class Line3D(Cylinder): + CONFIG = { + "width": 0.05, + "resolution": (21, 25) + } + + def __init__(self, start, end, **kwargs): + digest_config(self, kwargs) + axis = end - start + super().__init__( + height=get_norm(axis), + radius=self.width / 2, + axis=axis ) + self.shift((start + end) / 2) + + +class Disk3D(ArglessSurface): + CONFIG = { + "radius": 1, + "u_range": (0, 1), + "v_range": (0, TAU), + "resolution": (2, 25), + } + + def init_points(self): + super().init_points() self.scale(self.radius) - def func(self, u, v): - return np.array([ - np.cos(v) * np.sin(u), - np.sin(v) * np.sin(u), - np.cos(u) - ]) + def uv_func(self, u, v): + return [ + u * np.cos(v), + u * np.sin(v), + 0 + ] -class Cube(VGroup): +class Square3D(ArglessSurface): CONFIG = { - "fill_opacity": 0.75, - "fill_color": BLUE, - "stroke_width": 0, "side_length": 2, + "u_range": (-1, 1), + "v_range": (-1, 1), + "resolution": (2, 2), } - def generate_points(self): - for vect in IN, OUT, LEFT, RIGHT, UP, DOWN: - face = Square( - side_length=self.side_length, - shade_in_3d=True, - ) - face.flip() - face.shift(self.side_length * OUT / 2.0) - face.apply_matrix(z_to_vector(vect)) + def init_points(self): + super().init_points() + self.scale(self.side_length / 2) + + def uv_func(self, u, v): + return [u, v, 0] + +class Cube(SGroup): + CONFIG = { + # "fill_color": BLUE, + # "fill_opacity": 1, + # "stroke_width": 1, + # "stroke_color": BLACK, + "color": BLUE, + "opacity": 1, + "gloss": 0.5, + "square_resolution": (2, 2), + "side_length": 2, + } + + def init_points(self): + for vect in [OUT, RIGHT, UP, LEFT, DOWN, IN]: + face = Square3D(resolution=self.square_resolution) + face.shift(OUT) + face.apply_matrix(z_to_vector(vect)) self.add(face) + self.set_height(self.side_length) + # self.set_color(self.color, self.opacity, self.gloss) class Prism(Cube): @@ -150,7 +190,7 @@ class Prism(Cube): "dimensions": [3, 2, 1] } - def generate_points(self): - Cube.generate_points(self) + def init_points(self): + Cube.init_points(self) for dim, value in enumerate(self.dimensions): self.rescale_to_fit(value, dim, stretch=True) diff --git a/manimlib/mobject/types/image_mobject.py b/manimlib/mobject/types/image_mobject.py index be9cab68b7..266897c1da 100644 --- a/manimlib/mobject/types/image_mobject.py +++ b/manimlib/mobject/types/image_mobject.py @@ -4,130 +4,63 @@ from manimlib.constants import * from manimlib.mobject.mobject import Mobject -from manimlib.mobject.shape_matchers import SurroundingRectangle from manimlib.utils.bezier import interpolate -from manimlib.utils.color import color_to_int_rgb -from manimlib.utils.config_ops import digest_config from manimlib.utils.images import get_full_raster_image_path +from manimlib.utils.iterables import listify -class AbstractImageMobject(Mobject): - """ - Automatically filters out black pixels - """ +class ImageMobject(Mobject): CONFIG = { - "height": 2.0, - "pixel_array_dtype": "uint8", + "height": 4, + "opacity": 1, + "vert_shader_file": "image_vert.glsl", + "frag_shader_file": "image_frag.glsl", + "shader_dtype": [ + ('point', np.float32, (3,)), + ('im_coords', np.float32, (2,)), + ('opacity', np.float32, (1,)), + ] } - def get_pixel_array(self): - raise Exception("Not implemented") + def __init__(self, filename, **kwargs): + path = get_full_raster_image_path(filename) + self.image = Image.open(path) + self.texture_paths = {"Texture": path} + Mobject.__init__(self, **kwargs) - def set_color(self): - # Likely to be implemented in subclasses, but no obgligation - pass + def init_points(self): + self.points = np.array([UL, DL, UR, DR]) + self.im_coords = np.array([(0, 0), (0, 1), (1, 0), (1, 1)]) + size = self.image.size + self.set_width(2 * size[0] / size[1], stretch=True) + self.set_height(self.height) - def reset_points(self): - # Corresponding corners of image are fixed to these 3 points - self.points = np.array([ - UP + LEFT, - UP + RIGHT, - DOWN + LEFT, - ]) - self.center() - h, w = self.get_pixel_array().shape[:2] - self.stretch_to_fit_height(self.height) - self.stretch_to_fit_width(self.height * w / h) + def init_colors(self): + self.set_opacity(self.opacity) - def copy(self): - return self.deepcopy() + def set_opacity(self, alpha, family=True): + opacity = listify(alpha) + diff = 4 - len(opacity) + opacity += [opacity[-1]] * diff + self.opacity = np.array(opacity).reshape((4, 1)) - -class ImageMobject(AbstractImageMobject): - CONFIG = { - "invert": False, - "image_mode": "RGBA", - } - - def __init__(self, filename_or_array, **kwargs): - digest_config(self, kwargs) - if isinstance(filename_or_array, str): - path = get_full_raster_image_path(filename_or_array) - image = Image.open(path).convert(self.image_mode) - self.pixel_array = np.array(image) - else: - self.pixel_array = np.array(filename_or_array) - self.change_to_rgba_array() - if self.invert: - self.pixel_array[:, :, :3] = 255 - self.pixel_array[:, :, :3] - AbstractImageMobject.__init__(self, **kwargs) - - def change_to_rgba_array(self): - pa = self.pixel_array - if len(pa.shape) == 2: - pa = pa.reshape(list(pa.shape) + [1]) - if pa.shape[2] == 1: - pa = pa.repeat(3, axis=2) - if pa.shape[2] == 3: - alphas = 255 * np.ones( - list(pa.shape[:2]) + [1], - dtype=self.pixel_array_dtype - ) - pa = np.append(pa, alphas, axis=2) - self.pixel_array = pa - - def get_pixel_array(self): - return self.pixel_array - - def set_color(self, color, alpha=None, family=True): - rgb = color_to_int_rgb(color) - self.pixel_array[:, :, :3] = rgb - if alpha is not None: - self.pixel_array[:, :, 3] = int(255 * alpha) - for submob in self.submobjects: - submob.set_color(color, alpha, family) - self.color = color - return self - - def set_opacity(self, alpha): - self.pixel_array[:, :, 3] = int(255 * alpha) - return self + if family: + for sm in self.submobjects: + sm.set_opacity(alpha) def fade(self, darkness=0.5, family=True): - self.set_opacity(1 - darkness) - super().fade(darkness, family) + self.set_opacity(1 - darkness, family) return self def interpolate_color(self, mobject1, mobject2, alpha): - assert(mobject1.pixel_array.shape == mobject2.pixel_array.shape) - self.pixel_array = interpolate( - mobject1.pixel_array, mobject2.pixel_array, alpha - ).astype(self.pixel_array_dtype) - -# TODO, add the ability to have the dimensions/orientation of this -# mobject more strongly tied to the frame of the camera it contains, -# in the case where that's a MovingCamera - - -class ImageMobjectFromCamera(AbstractImageMobject): - CONFIG = { - "default_display_frame_config": { - "stroke_width": 3, - "stroke_color": WHITE, - "buff": 0, - } - } - - def __init__(self, camera, **kwargs): - self.camera = camera - AbstractImageMobject.__init__(self, **kwargs) - - def get_pixel_array(self): - return self.camera.get_pixel_array() - - def add_display_frame(self, **kwargs): - config = dict(self.default_display_frame_config) - config.update(kwargs) - self.display_frame = SurroundingRectangle(self, **config) - self.add(self.display_frame) - return self + # TODO, transition between actual images? + self.opacity = interpolate( + mobject1.opacity, mobject2.opacity, alpha + ) + + def get_shader_data(self): + data = self.get_blank_shader_data_array(len(self.points)) + data["point"] = self.points + data["im_coords"] = self.im_coords + data["opacity"] = self.opacity + return data diff --git a/manimlib/mobject/types/point_cloud_mobject.py b/manimlib/mobject/types/point_cloud_mobject.py index f8276382a7..7e60727480 100644 --- a/manimlib/mobject/types/point_cloud_mobject.py +++ b/manimlib/mobject/types/point_cloud_mobject.py @@ -4,9 +4,7 @@ from manimlib.utils.color import color_gradient from manimlib.utils.color import color_to_rgba from manimlib.utils.color import rgba_to_color -from manimlib.utils.config_ops import digest_config from manimlib.utils.iterables import stretch_array_to_length -from manimlib.utils.space_ops import get_norm class PMobject(Mobject): @@ -29,7 +27,7 @@ def add_points(self, points, rgbas=None, color=None, alpha=1): if not isinstance(points, np.ndarray): points = np.array(points) num_new_points = len(points) - self.points = np.append(self.points, points, axis=0) + self.points = np.vstack([self.points, points]) if rgbas is None: color = Color(color) if color else self.color rgbas = np.repeat( @@ -39,7 +37,7 @@ def add_points(self, points, rgbas=None, color=None, alpha=1): ) elif len(rgbas) != len(points): raise Exception("points and rgbas must have same shape") - self.rgbas = np.append(self.rgbas, rgbas, axis=0) + self.rgbas = np.vstack([self.rgbas, rgbas]) return self def set_color(self, color=YELLOW_C, family=True): @@ -139,7 +137,7 @@ def ingest_submobjects(self): arrays = list(map(self.get_merged_array, attrs)) for attr, array in zip(attrs, arrays): setattr(self, attr, array) - self.submobjects = [] + self.set_submobjects([]) return self def get_color(self): @@ -158,11 +156,6 @@ def align_points_with_larger(self, larger_mobject): ) ) - def get_point_mobject(self, center=None): - if center is None: - center = self.get_center() - return Point(center) - def interpolate_color(self, mobject1, mobject2, alpha): self.rgbas = interpolate( mobject1.rgbas, mobject2.rgbas, alpha @@ -185,42 +178,6 @@ def pointwise_become_partial(self, mobject, a, b): setattr(self, attr, partial_array) -# TODO, Make the two implementations bellow non-redundant -class Mobject1D(PMobject): - CONFIG = { - "density": DEFAULT_POINT_DENSITY_1D, - } - - def __init__(self, **kwargs): - digest_config(self, kwargs) - self.epsilon = 1.0 / self.density - Mobject.__init__(self, **kwargs) - - def add_line(self, start, end, color=None): - start, end = list(map(np.array, [start, end])) - length = get_norm(end - start) - if length == 0: - points = [start] - else: - epsilon = self.epsilon / length - points = [ - interpolate(start, end, t) - for t in np.arange(0, 1, epsilon) - ] - self.add_points(points, color=color) - - -class Mobject2D(PMobject): - CONFIG = { - "density": DEFAULT_POINT_DENSITY_2D, - } - - def __init__(self, **kwargs): - digest_config(self, kwargs) - self.epsilon = 1.0 / self.density - Mobject.__init__(self, **kwargs) - - class PGroup(PMobject): def __init__(self, *pmobs, **kwargs): if not all([isinstance(m, PMobject) for m in pmobs]): @@ -229,26 +186,6 @@ def __init__(self, *pmobs, **kwargs): self.add(*pmobs) -class PointCloudDot(Mobject1D): - CONFIG = { - "radius": 0.075, - "stroke_width": 2, - "density": DEFAULT_POINT_DENSITY_1D, - "color": YELLOW, - } - - def __init__(self, center=ORIGIN, **kwargs): - Mobject1D.__init__(self, **kwargs) - self.shift(center) - - def generate_points(self): - self.add_points([ - r * (np.cos(theta) * RIGHT + np.sin(theta) * UP) - for r in np.arange(0, self.radius, self.epsilon) - for theta in np.arange(0, 2 * np.pi, self.epsilon / r) - ]) - - class Point(PMobject): CONFIG = { "color": BLACK, diff --git a/manimlib/mobject/types/surface.py b/manimlib/mobject/types/surface.py new file mode 100644 index 0000000000..94455ced47 --- /dev/null +++ b/manimlib/mobject/types/surface.py @@ -0,0 +1,275 @@ +import numpy as np +import moderngl + +from manimlib.constants import * +from manimlib.mobject.mobject import Mobject +from manimlib.utils.bezier import integer_interpolate +from manimlib.utils.bezier import interpolate +from manimlib.utils.color import color_to_rgba +from manimlib.utils.color import rgb_to_color +from manimlib.utils.config_ops import digest_config +from manimlib.utils.images import get_full_raster_image_path +from manimlib.utils.space_ops import normalize_along_axis + + +class ParametricSurface(Mobject): + CONFIG = { + "u_range": (0, 1), + "v_range": (0, 1), + # Resolution counts number of points sampled, which for + # each coordinate is one more than the the number of rows/columns + # of approximating squares + "resolution": (101, 101), + "color": GREY, + "opacity": 1.0, + "gloss": 0.3, + "shadow": 0.4, + # For du and dv steps. Much smaller and numerical error + # can crop up in the shaders. + "epsilon": 1e-5, + "render_primative": moderngl.TRIANGLES, + "depth_test": True, + "vert_shader_file": "surface_vert.glsl", + "frag_shader_file": "surface_frag.glsl", + "shader_dtype": [ + ('point', np.float32, (3,)), + ('du_point', np.float32, (3,)), + ('dv_point', np.float32, (3,)), + ('color', np.float32, (4,)), + ] + } + + def __init__(self, uv_func, **kwargs): + digest_config(self, kwargs) + self.uv_func = uv_func + self.compute_triangle_indices() + super().__init__(**kwargs) + self.sort_faces_back_to_front() + + def init_points(self): + dim = self.dim + nu, nv = self.resolution + u_range = np.linspace(*self.u_range, nu) + v_range = np.linspace(*self.v_range, nv) + + # Get three lists: + # - Points generated by pure uv values + # - Those generated by values nudged by du + # - Those generated by values nudged by dv + point_lists = [] + for (du, dv) in [(0, 0), (self.epsilon, 0), (0, self.epsilon)]: + uv_grid = np.array([[[u + du, v + dv] for v in v_range] for u in u_range]) + point_grid = np.apply_along_axis(lambda p: self.uv_func(*p), 2, uv_grid) + point_lists.append(point_grid.reshape((nu * nv, dim))) + # Rather than tracking normal vectors, the points list will hold on to the + # infinitesimal nudged values alongside the original values. This way, one + # can perform all the manipulations they'd like to the surface, and normals + # are still easily recoverable. + self.points = np.vstack(point_lists) + + def compute_triangle_indices(self): + # TODO, if there is an event which changes + # the resolution of the surface, make sure + # this is called. + nu, nv = self.resolution + if nu == 0 or nv == 0: + self.triangle_indices = np.zeros(0, dtype=int) + return + index_grid = np.arange(nu * nv).reshape((nu, nv)) + indices = np.zeros(6 * (nu - 1) * (nv - 1), dtype=int) + indices[0::6] = index_grid[:-1, :-1].flatten() # Top left + indices[1::6] = index_grid[+1:, :-1].flatten() # Bottom left + indices[2::6] = index_grid[:-1, +1:].flatten() # Top right + indices[3::6] = index_grid[:-1, +1:].flatten() # Top right + indices[4::6] = index_grid[+1:, :-1].flatten() # Bottom left + indices[5::6] = index_grid[+1:, +1:].flatten() # Bottom right + self.triangle_indices = indices + + def get_triangle_indices(self): + return self.triangle_indices + + def init_colors(self): + self.rgbas = np.zeros((1, 4)) + self.set_color(self.color, self.opacity) + + def get_surface_points_and_nudged_points(self): + k = len(self.points) // 3 + return self.points[:k], self.points[k:2 * k], self.points[2 * k:] + + def get_unit_normals(self): + s_points, du_points, dv_points = self.get_surface_points_and_nudged_points() + normals = np.cross( + (du_points - s_points) / self.epsilon, + (dv_points - s_points) / self.epsilon, + ) + return normalize_along_axis(normals, 1) + + def set_color(self, color, opacity=1.0, family=True): + # TODO, allow for multiple colors + rgba = color_to_rgba(color, opacity) + mobs = self.get_family() if family else [self] + for mob in mobs: + mob.rgbas[:] = rgba + return self + + def get_color(self): + return rgb_to_color(self.rgbas[0, :3]) + + def set_opacity(self, opacity, family=True): + mobs = self.get_family() if family else [self] + for mob in mobs: + mob.rgbas[:, 3] = opacity + return self + + def interpolate_color(self, mobject1, mobject2, alpha): + self.rgbas = interpolate(mobject1.rgbas, mobject2.rgbas, alpha) + return self + + def pointwise_become_partial(self, smobject, a, b, axis=1): + assert(isinstance(smobject, ParametricSurface)) + self.points[:] = smobject.points[:] + if a <= 0 and b >= 1: + return self + + nu, nv = smobject.resolution + self.points[:] = np.vstack([ + self.get_partial_points_array(arr, a, b, (nu, nv, 3), axis=axis) + for arr in self.get_surface_points_and_nudged_points() + ]) + return self + + def get_partial_points_array(self, points, a, b, resolution, axis): + nu, nv = resolution[:2] + points = points.reshape(resolution) + max_index = resolution[axis] - 1 + lower_index, lower_residue = integer_interpolate(0, max_index, a) + upper_index, upper_residue = integer_interpolate(0, max_index, b) + if axis == 0: + points[:lower_index] = interpolate(points[lower_index], points[lower_index + 1], lower_residue) + points[upper_index:] = interpolate(points[upper_index], points[upper_index + 1], upper_residue) + else: + tuples = [ + (points[:, :lower_index], lower_index, lower_residue), + (points[:, upper_index:], upper_index, upper_residue), + ] + for to_change, index, residue in tuples: + col = interpolate(points[:, index], points[:, index + 1], residue) + to_change[:] = col.reshape((nu, 1, *resolution[2:])) + return points.reshape((nu * nv, *resolution[2:])) + + def sort_faces_back_to_front(self, vect=OUT): + tri_is = self.triangle_indices + indices = list(range(len(tri_is) // 3)) + indices.sort(key=lambda i: np.dot(self.points[tri_is[3 * i]], vect)) + for k in range(3): + tri_is[k::3] = tri_is[k::3][indices] + return self + + # For shaders + def get_shader_data(self): + s_points, du_points, dv_points = self.get_surface_points_and_nudged_points() + data = self.get_blank_shader_data_array(len(s_points)) + data["point"] = s_points + data["du_point"] = du_points + data["dv_point"] = dv_points + self.fill_in_shader_color_info(data) + return data + + def fill_in_shader_color_info(self, data): + data["color"] = self.rgbas + return data + + def get_shader_vert_indices(self): + return self.get_triangle_indices() + + +class SGroup(ParametricSurface): + CONFIG = { + "resolution": (0, 0), + } + + def __init__(self, *parametric_surfaces, **kwargs): + super().__init__(uv_func=None, **kwargs) + self.add(*parametric_surfaces) + + def init_points(self): + self.points = np.zeros((0, 3)) + + +class TexturedSurface(ParametricSurface): + CONFIG = { + "vert_shader_file": "textured_surface_vert.glsl", + "frag_shader_file": "textured_surface_frag.glsl", + "shader_dtype": [ + ('point', np.float32, (3,)), + ('du_point', np.float32, (3,)), + ('dv_point', np.float32, (3,)), + ('im_coords', np.float32, (2,)), + ('opacity', np.float32, (1,)), + ] + } + + def __init__(self, uv_surface, image_file, dark_image_file=None, **kwargs): + if not isinstance(uv_surface, ParametricSurface): + raise Exception("uv_surface must be of type ParametricSurface") + # Set texture information + if dark_image_file is None: + dark_image_file = image_file + self.num_textures = 1 + else: + self.num_textures = 2 + self.texture_paths = { + "LightTexture": get_full_raster_image_path(image_file), + "DarkTexture": get_full_raster_image_path(dark_image_file), + } + + self.uv_surface = uv_surface + self.uv_func = uv_surface.uv_func + self.u_range = uv_surface.u_range + self.v_range = uv_surface.v_range + self.resolution = uv_surface.resolution + super().__init__(self.uv_func, **kwargs) + + def init_points(self): + self.points = self.uv_surface.points + # Init im_coords + nu, nv = self.uv_surface.resolution + u_range = np.linspace(0, 1, nu) + v_range = np.linspace(1, 0, nv) # Reverse y-direction + uv_grid = np.array([[u, v] for u in u_range for v in v_range]) + self.im_coords = uv_grid + + def init_colors(self): + self.opacity = self.uv_surface.rgbas[:, 3] + self.gloss = self.uv_surface.gloss + + def interpolate_color(self, mobject1, mobject2, alpha): + # TODO, handle multiple textures + self.opacity = interpolate(mobject1.opacity, mobject2.opacity, alpha) + return self + + def set_opacity(self, opacity, family=True): + self.opacity = opacity + if family: + for sm in self.submobjects: + sm.set_opacity(opacity, family) + return self + + def pointwise_become_partial(self, tsmobject, a, b, axis=1): + super().pointwise_become_partial(tsmobject, a, b, axis) + self.im_coords[:] = tsmobject.im_coords + if a <= 0 and b >= 1: + return self + nu, nv = tsmobject.resolution + self.im_coords[:] = self.get_partial_points_array(self.im_coords, a, b, (nu, nv, 2), axis) + return self + + def get_shader_uniforms(self): + result = super().get_shader_uniforms() + result["num_textures"] = self.num_textures + return result + + def fill_in_shader_color_info(self, data): + data["im_coords"] = self.im_coords + data["opacity"] = self.opacity + return data diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 29b91e544d..a89cac3b05 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1,31 +1,34 @@ import itertools as it -import sys +import operator as op +import moderngl from colour import Color +from functools import reduce from manimlib.constants import * from manimlib.mobject.mobject import Mobject -from manimlib.mobject.three_d_utils import get_3d_vmob_gradient_start_and_end_points +from manimlib.mobject.mobject import Point from manimlib.utils.bezier import bezier -from manimlib.utils.bezier import get_smooth_handle_points +from manimlib.utils.bezier import get_smooth_quadratic_bezier_handle_points +from manimlib.utils.bezier import get_smooth_cubic_bezier_handle_points +from manimlib.utils.bezier import get_quadratic_approximation_of_cubic from manimlib.utils.bezier import interpolate +from manimlib.utils.bezier import set_array_by_interpolation from manimlib.utils.bezier import integer_interpolate -from manimlib.utils.bezier import partial_bezier_points +from manimlib.utils.bezier import partial_quadratic_bezier_points from manimlib.utils.color import color_to_rgba +from manimlib.utils.color import rgb_to_hex from manimlib.utils.iterables import make_even from manimlib.utils.iterables import stretch_array_to_length -from manimlib.utils.iterables import tuplify -from manimlib.utils.simple_functions import clip_in_place -from manimlib.utils.space_ops import rotate_vector +from manimlib.utils.iterables import stretch_array_to_length_with_interpolation +from manimlib.utils.iterables import listify +from manimlib.utils.space_ops import angle_between_vectors +from manimlib.utils.space_ops import cross2d +from manimlib.utils.space_ops import earclip_triangulation from manimlib.utils.space_ops import get_norm - -# TODO -# - Change cubic curve groups to have 4 points instead of 3 -# - Change sub_path idea accordingly -# - No more mark_paths_closed, instead have the camera test -# if last point in close to first point -# - Think about length of self.points. Always 0 or 1 mod 4? -# That's kind of weird. +from manimlib.utils.space_ops import get_unit_normal +from manimlib.utils.space_ops import z_to_vector +from manimlib.shader_wrapper import ShaderWrapper class VMobject(Mobject): @@ -35,37 +38,60 @@ class VMobject(Mobject): "stroke_color": None, "stroke_opacity": 1.0, "stroke_width": DEFAULT_STROKE_WIDTH, - # The purpose of background stroke is to have - # something that won't overlap the fill, e.g. - # For text against some textured background - "background_stroke_color": BLACK, - "background_stroke_opacity": 1.0, - "background_stroke_width": 0, - # When a color c is set, there will be a second color - # computed based on interpolating c to WHITE by with - # sheen_factor, and the display will gradient to this - # secondary color in the direction of sheen_direction. - "sheen_factor": 0.0, - "sheen_direction": UL, + "draw_stroke_behind_fill": False, # Indicates that it will not be displayed, but # that it should count in parent mobject's path - "close_new_points": False, "pre_function_handle_to_anchor_scale_factor": 0.01, "make_smooth_after_applying_functions": False, "background_image_file": None, - "shade_in_3d": False, # This is within a pixel # TODO, do we care about accounting for # varying zoom levels? - "tolerance_for_point_equality": 1e-6, - "n_points_per_cubic_curve": 4, + "tolerance_for_point_equality": 1e-8, + "n_points_per_curve": 3, + "long_lines": False, + # For shaders + "stroke_vert_shader_file": "quadratic_bezier_stroke_vert.glsl", + "stroke_geom_shader_file": "quadratic_bezier_stroke_geom.glsl", + "stroke_frag_shader_file": "quadratic_bezier_stroke_frag.glsl", + "fill_vert_shader_file": "quadratic_bezier_fill_vert.glsl", + "fill_geom_shader_file": "quadratic_bezier_fill_geom.glsl", + "fill_frag_shader_file": "quadratic_bezier_fill_frag.glsl", + # Could also be Bevel, Miter, Round + "joint_type": "auto", + "render_primative": moderngl.TRIANGLES, + "triangulation_locked": False, + "fill_dtype": [ + ('point', np.float32, (3,)), + ('unit_normal', np.float32, (3,)), + ('color', np.float32, (4,)), + # ('fill_all', np.float32, (1,)), + ('vert_index', np.float32, (1,)), + ], + "stroke_dtype": [ + ("point", np.float32, (3,)), + ("prev_point", np.float32, (3,)), + ("next_point", np.float32, (3,)), + ('unit_normal', np.float32, (3,)), + ("stroke_width", np.float32, (1,)), + ("color", np.float32, (4,)), + ] } + def __init__(self, **kwargs): + self.unit_normal_locked = False + self.triangulation_locked = False + super().__init__(**kwargs) + self.lock_unit_normal(family=False) + self.lock_triangulation(family=False) + def get_group_class(self): return VGroup # Colors def init_colors(self): + self.fill_rgbas = np.zeros((1, 4)) + self.stroke_rgbas = np.zeros((1, 4)) self.set_fill( color=self.fill_color or self.color, opacity=self.fill_opacity, @@ -74,55 +100,30 @@ def init_colors(self): color=self.stroke_color or self.color, width=self.stroke_width, opacity=self.stroke_opacity, + background=self.draw_stroke_behind_fill, ) - self.set_background_stroke( - color=self.background_stroke_color, - width=self.background_stroke_width, - opacity=self.background_stroke_opacity, - ) - self.set_sheen( - factor=self.sheen_factor, - direction=self.sheen_direction, - ) + self.set_gloss(self.gloss) return self - def generate_rgbas_array(self, color, opacity): + def generate_rgba_array(self, color, opacity): """ First arg can be either a color, or a tuple/list of colors. Likewise, opacity can either be a float, or a tuple of floats. - If self.sheen_factor is not zero, and only - one color was passed in, a second slightly light color - will automatically be added for the gradient """ - colors = list(tuplify(color)) - opacities = list(tuplify(opacity)) - rgbas = np.array([ + colors = listify(color) + opacities = listify(opacity) + return np.array([ color_to_rgba(c, o) for c, o in zip(*make_even(colors, opacities)) ]) - sheen_factor = self.get_sheen_factor() - if sheen_factor != 0 and len(rgbas) == 1: - light_rgbas = np.array(rgbas) - light_rgbas[:, :3] += sheen_factor - clip_in_place(light_rgbas, 0, 1) - rgbas = np.append(rgbas, light_rgbas, axis=0) - return rgbas - - def update_rgbas_array(self, array_name, color=None, opacity=None): - passed_color = color if (color is not None) else BLACK - passed_opacity = opacity if (opacity is not None) else 0 - rgbas = self.generate_rgbas_array(passed_color, passed_opacity) - if not hasattr(self, array_name): - setattr(self, array_name, rgbas) - return self + def update_rgbas_array(self, array_name, color, opacity): + rgbas = self.generate_rgba_array(color or BLACK, opacity or 0) # Match up current rgbas array with the newly calculated # one. 99% of the time they'll be the same. curr_rgbas = getattr(self, array_name) if len(curr_rgbas) < len(rgbas): - curr_rgbas = stretch_array_to_length( - curr_rgbas, len(rgbas) - ) + curr_rgbas = stretch_array_to_length(curr_rgbas, len(rgbas)) setattr(self, array_name, curr_rgbas) elif len(rgbas) < len(curr_rgbas): rgbas = stretch_array_to_length(rgbas, len(curr_rgbas)) @@ -136,91 +137,84 @@ def update_rgbas_array(self, array_name, color=None, opacity=None): def set_fill(self, color=None, opacity=None, family=True): if family: - for submobject in self.submobjects: - submobject.set_fill(color, opacity, family) + for sm in self.submobjects: + sm.set_fill(color, opacity, family) self.update_rgbas_array("fill_rgbas", color, opacity) return self def set_stroke(self, color=None, width=None, opacity=None, - background=False, family=True): + background=None, family=True): if family: - for submobject in self.submobjects: - submobject.set_stroke( - color, width, opacity, background, family - ) - if background: - array_name = "background_stroke_rgbas" - width_name = "background_stroke_width" - else: - array_name = "stroke_rgbas" - width_name = "stroke_width" - self.update_rgbas_array(array_name, color, opacity) + for sm in self.submobjects: + sm.set_stroke(color, width, opacity, background, family) + self.update_rgbas_array("stroke_rgbas", color, opacity) if width is not None: - setattr(self, width_name, width) - return self - - def set_background_stroke(self, **kwargs): - kwargs["background"] = True - self.set_stroke(**kwargs) + self.stroke_width = np.array(listify(width), dtype=float) + if background is not None: + self.draw_stroke_behind_fill = background return self def set_style(self, fill_color=None, fill_opacity=None, + fill_rgbas=None, stroke_color=None, - stroke_width=None, stroke_opacity=None, - background_stroke_color=None, - background_stroke_width=None, - background_stroke_opacity=None, - sheen_factor=None, - sheen_direction=None, + stroke_rgbas=None, + stroke_width=None, + gloss=None, + shadow=None, background_image_file=None, family=True): - self.set_fill( - color=fill_color, - opacity=fill_opacity, - family=family - ) - self.set_stroke( - color=stroke_color, - width=stroke_width, - opacity=stroke_opacity, - family=family, - ) - self.set_background_stroke( - color=background_stroke_color, - width=background_stroke_width, - opacity=background_stroke_opacity, - family=family, - ) - if sheen_factor: - self.set_sheen( - factor=sheen_factor, - direction=sheen_direction, + if fill_rgbas is not None: + self.fill_rgbas = np.array(fill_rgbas) + else: + self.set_fill( + color=fill_color, + opacity=fill_opacity, + family=family + ) + + if stroke_rgbas is not None: + self.stroke_rgbas = np.array(stroke_rgbas) + if stroke_width is not None: + self.stroke_width = np.array(listify(stroke_width)) + else: + self.set_stroke( + color=stroke_color, + width=stroke_width, + opacity=stroke_opacity, family=family, ) + + if gloss is not None: + self.set_gloss(gloss, family=family) + if shadow is not None: + self.set_shadow(shadow, family=family) if background_image_file: self.color_using_background_image(background_image_file) return self def get_style(self): return { - "fill_color": self.get_fill_colors(), - "fill_opacity": self.get_fill_opacities(), - "stroke_color": self.get_stroke_colors(), - "stroke_width": self.get_stroke_width(), - "stroke_opacity": self.get_stroke_opacity(), - "background_stroke_color": self.get_stroke_colors(background=True), - "background_stroke_width": self.get_stroke_width(background=True), - "background_stroke_opacity": self.get_stroke_opacity(background=True), - "sheen_factor": self.get_sheen_factor(), - "sheen_direction": self.get_sheen_direction(), + "fill_rgbas": self.get_fill_rgbas(), + "stroke_rgbas": self.get_stroke_rgbas(), + "stroke_width": self.stroke_width, + "gloss": self.get_gloss(), + "shadow": self.get_shadow(), "background_image_file": self.get_background_image_file(), } def match_style(self, vmobject, family=True): - self.set_style(**vmobject.get_style(), family=False) + for name, value in vmobject.get_style().items(): + if isinstance(value, np.ndarray): + curr = getattr(self, name) + if curr.size == value.size: + curr[:] = value[:] + else: + setattr(self, name, np.array(value)) + else: + setattr(self, name, value) if family: # Does its best to match up submobject lists, and @@ -242,7 +236,6 @@ def set_color(self, color, family=True): def set_opacity(self, opacity, family=True): self.set_fill(opacity=opacity, family=family) self.set_stroke(opacity=opacity, family=family) - self.set_stroke(opacity=opacity, family=family, background=True) return self def fade(self, darkness=0.5, family=True): @@ -255,12 +248,6 @@ def fade(self, darkness=0.5, family=True): opacity=factor * self.get_stroke_opacity(), family=False, ) - self.set_background_stroke( - opacity=factor * self.get_stroke_opacity( - background=True - ), - family=False, - ) super().fade(darkness, family) return self @@ -293,86 +280,55 @@ def get_fill_colors(self): def get_fill_opacities(self): return self.get_fill_rgbas()[:, 3] - def get_stroke_rgbas(self, background=False): + def get_stroke_rgbas(self): try: - if background: - rgbas = self.background_stroke_rgbas - else: - rgbas = self.stroke_rgbas - return rgbas + return self.stroke_rgbas except AttributeError: return np.zeros((1, 4)) - def get_stroke_color(self, background=False): - return self.get_stroke_colors(background)[0] + # TODO, it's weird for these to return the first of various lists + # rather than the full information + def get_stroke_color(self): + return self.get_stroke_colors()[0] - def get_stroke_width(self, background=False): - if background: - width = self.background_stroke_width - else: - width = self.stroke_width - return max(0, width) + def get_stroke_width(self): + return self.stroke_width[0] - def get_stroke_opacity(self, background=False): - return self.get_stroke_opacities(background)[0] + def get_stroke_opacity(self): + return self.get_stroke_opacities()[0] - def get_stroke_colors(self, background=False): + def get_stroke_colors(self): return [ - Color(rgb=rgba[:3]) - for rgba in self.get_stroke_rgbas(background) + rgb_to_hex(rgba[:3]) + for rgba in self.get_stroke_rgbas() ] - def get_stroke_opacities(self, background=False): - return self.get_stroke_rgbas(background)[:, 3] + def get_stroke_opacities(self): + return self.get_stroke_rgbas()[:, 3] def get_color(self): if np.all(self.get_fill_opacities() == 0): return self.get_stroke_color() return self.get_fill_color() - def set_sheen_direction(self, direction, family=True): - direction = np.array(direction) - if family: - for submob in self.get_family(): - submob.sheen_direction = direction - else: - self.sheen_direction = direction - return self - - def set_sheen(self, factor, direction=None, family=True): - if family: - for submob in self.submobjects: - submob.set_sheen(factor, direction, family) - self.sheen_factor = factor - if direction is not None: - # family set to false because recursion will - # already be handled above - self.set_sheen_direction(direction, family=False) - # Reset color to put sheen_factor into effect - if factor != 0: - self.set_stroke(self.get_stroke_color(), family=family) - self.set_fill(self.get_fill_color(), family=family) - return self - - def get_sheen_direction(self): - return np.array(self.sheen_direction) - - def get_sheen_factor(self): - return self.sheen_factor - - def get_gradient_start_and_end_points(self): - if self.shade_in_3d: - return get_3d_vmob_gradient_start_and_end_points(self) - else: - direction = self.get_sheen_direction() - c = self.get_center() - bases = np.array([ - self.get_edge_center(vect) - c - for vect in [RIGHT, UP, OUT] - ]).transpose() - offset = np.dot(bases, direction) - return (c - offset, c + offset) - + def has_stroke(self): + if len(self.stroke_width) == 1: + if self.stroke_width == 0: + return False + elif not self.stroke_width.any(): + return False + alphas = self.stroke_rgbas[:, 3] + if len(alphas) == 1: + return alphas[0] > 0 + return alphas.any() + + def has_fill(self): + alphas = self.fill_rgbas[:, 3] + if len(alphas) == 1: + return alphas[0] > 0 + return alphas.any() + + # TODO, this currently does nothing def color_using_background_image(self, background_image_file): self.background_image_file = background_image_file self.set_color(WHITE) @@ -387,29 +343,38 @@ def match_background_image_file(self, vmobject): self.color_using_background_image(vmobject.get_background_image_file()) return self - def set_shade_in_3d(self, value=True, z_index_as_group=False): - for submob in self.get_family(): - submob.shade_in_3d = value - if z_index_as_group: - submob.z_index_group = self - return self + def stretched_style_array_matching_points(self, array): + new_len = self.get_num_points() + long_arr = stretch_array_to_length_with_interpolation( + array, 1 + 2 * (new_len // 3) + ) + shape = array.shape + if len(shape) > 1: + result = np.zeros((new_len, shape[1])) + else: + result = np.zeros(new_len) + result[0::3] = long_arr[0:-1:2] + result[1::3] = long_arr[1::2] + result[2::3] = long_arr[2::2] + return result # Points def set_points(self, points): - self.points = np.array(points) + super().set_points(points) + self.refresh_triangulation() return self def get_points(self): + # TODO, shouldn't points always be a numpy array anyway? return np.array(self.points) - def set_anchors_and_handles(self, anchors1, handles1, handles2, anchors2): - assert(len(anchors1) == len(handles1) == len(handles2) == len(anchors2)) - nppcc = self.n_points_per_cubic_curve # 4 - total_len = nppcc * len(anchors1) - self.points = np.zeros((total_len, self.dim)) - arrays = [anchors1, handles1, handles2, anchors2] + def set_anchors_and_handles(self, anchors1, handles, anchors2): + assert(len(anchors1) == len(handles) == len(anchors2)) + nppc = self.n_points_per_curve + self.points = np.zeros((nppc * len(anchors1), self.dim)) + arrays = [anchors1, handles, anchors2] for index, array in enumerate(arrays): - self.points[index::nppcc] = array + self.points[index::nppc] = array return self def clear_points(self): @@ -419,92 +384,125 @@ def append_points(self, new_points): # TODO, check that number new points is a multiple of 4? # or else that if len(self.points) % 4 == 1, then # len(new_points) % 4 == 3? - self.points = np.append(self.points, new_points, axis=0) + self.points = np.vstack([self.points, new_points]) return self def start_new_path(self, point): - # TODO, make sure that len(self.points) % 4 == 0? + assert(len(self.points) % self.n_points_per_curve == 0) self.append_points([point]) return self def add_cubic_bezier_curve(self, anchor1, handle1, handle2, anchor2): - # TODO, check the len(self.points) % 4 == 0? - self.append_points([anchor1, handle1, handle2, anchor2]) + new_points = get_quadratic_approximation_of_cubic(anchor1, handle1, handle2, anchor2) + self.append_points(new_points) def add_cubic_bezier_curve_to(self, handle1, handle2, anchor): """ Add cubic bezier curve to the path. """ self.throw_error_if_no_points() - new_points = [handle1, handle2, anchor] + quadratic_approx = get_quadratic_approximation_of_cubic( + self.points[-1], handle1, handle2, anchor + ) if self.has_new_path_started(): - self.append_points(new_points) + self.append_points(quadratic_approx[1:]) else: - self.append_points([self.get_last_point()] + new_points) + self.append_points(quadratic_approx) - def add_line_to(self, point): - nppcc = self.n_points_per_cubic_curve - self.add_cubic_bezier_curve_to(*[ - interpolate(self.get_last_point(), point, a) - for a in np.linspace(0, 1, nppcc)[1:] - ]) - return self + def add_quadratic_bezier_curve_to(self, handle, anchor): + self.throw_error_if_no_points() + if self.has_new_path_started(): + self.append_points([handle, anchor]) + else: + self.append_points([self.points[-1], handle, anchor]) - def add_smooth_curve_to(self, *points): - """ - If two points are passed in, the first is intepretted - as a handle, the second as an anchor - """ - if len(points) == 1: - handle2 = None - new_anchor = points[0] - elif len(points) == 2: - handle2, new_anchor = points + def add_line_to(self, point): + end = self.points[-1] + alphas = np.linspace(0, 1, self.n_points_per_curve) + if self.long_lines: + halfway = interpolate(end, point, 0.5) + points = [ + interpolate(end, halfway, a) + for a in alphas + ] + [ + interpolate(halfway, point, a) + for a in alphas + ] else: - name = sys._getframe(0).f_code.co_name - raise Exception("Only call {} with 1 or 2 points".format(name)) + points = [ + interpolate(end, point, a) + for a in alphas + ] + if self.has_new_path_started(): + points = points[1:] + self.append_points(points) + return self + def add_smooth_curve_to(self, point): if self.has_new_path_started(): - self.add_line_to(new_anchor) + self.add_line_to(anchor) else: self.throw_error_if_no_points() - last_h2, last_a2 = self.points[-2:] - last_tangent = (last_a2 - last_h2) - handle1 = last_a2 + last_tangent - if handle2 is None: - to_anchor_vect = new_anchor - last_a2 - new_tangent = rotate_vector( - last_tangent, PI, axis=to_anchor_vect - ) - handle2 = new_anchor - new_tangent - self.append_points([ - last_a2, handle1, handle2, new_anchor - ]) + new_handle = self.get_reflection_of_last_handle() + self.add_quadratic_bezier_curve_to(new_handle, point) return self + def add_smooth_cubic_curve_to(self, handle, point): + self.throw_error_if_no_points() + new_handle = self.get_reflection_of_last_handle() + self.add_cubic_bezier_curve_to(new_handle, handle, point) + def has_new_path_started(self): - nppcc = self.n_points_per_cubic_curve # 4 - return len(self.points) % nppcc == 1 + return len(self.points) % self.n_points_per_curve == 1 def get_last_point(self): return self.points[-1] + def get_reflection_of_last_handle(self): + return 2 * self.points[-1] - self.points[-2] + + def close_path(self): + if not self.is_closed(): + self.add_line_to(self.get_subpaths()[-1][0]) + def is_closed(self): return self.consider_points_equals( self.points[0], self.points[-1] ) + def subdivide_sharp_curves(self, angle_threshold=30 * DEGREES, family=True): + if family: + vmobs = self.family_members_with_points() + else: + vmobs = [self] if self.has_points() else [] + + for vmob in vmobs: + new_points = [] + for tup in vmob.get_bezier_tuples(): + angle = angle_between_vectors(tup[1] - tup[0], tup[2] - tup[1]) + if angle > angle_threshold: + n = int(np.ceil(angle / angle_threshold)) + alphas = np.linspace(0, 1, n + 1) + new_points.extend([ + partial_quadratic_bezier_points(tup, a1, a2) + for a1, a2 in zip(alphas, alphas[1:]) + ]) + else: + new_points.append(tup) + vmob.points = np.vstack(new_points) + return self + def add_points_as_corners(self, points): for point in points: self.add_line_to(point) return points def set_points_as_corners(self, points): - nppcc = self.n_points_per_cubic_curve + nppc = self.n_points_per_curve points = np.array(points) self.set_anchors_and_handles(*[ interpolate(points[:-1], points[1:], a) - for a in np.linspace(0, 1, nppcc) + for a in np.linspace(0, 1, nppc) ]) return self @@ -515,39 +513,33 @@ def set_points_smoothly(self, points): def change_anchor_mode(self, mode): assert(mode in ["jagged", "smooth"]) - nppcc = self.n_points_per_cubic_curve + nppc = self.n_points_per_curve for submob in self.family_members_with_points(): subpaths = submob.get_subpaths() submob.clear_points() for subpath in subpaths: - anchors = np.append( - subpath[::nppcc], - subpath[-1:], - 0 - ) + anchors = np.vstack([subpath[::nppc], subpath[-1:]]) + new_subpath = np.array(subpath) if mode == "smooth": - h1, h2 = get_smooth_handle_points(anchors) + new_subpath[1::nppc] = get_smooth_quadratic_bezier_handle_points(anchors) + # h1, h2 = get_smooth_cubic_bezier_handle_points(anchors) + # new_subpath = get_quadratic_approximation_of_cubic(anchors[:-1], h1, h2, anchors[1:]) elif mode == "jagged": - a1 = anchors[:-1] - a2 = anchors[1:] - h1 = interpolate(a1, a2, 1.0 / 3) - h2 = interpolate(a1, a2, 2.0 / 3) - new_subpath = np.array(subpath) - new_subpath[1::nppcc] = h1 - new_subpath[2::nppcc] = h2 + new_subpath[1::nppc] = 0.5 * (anchors[:-1] + anchors[1:]) submob.append_points(new_subpath) + submob.refresh_triangulation() return self def make_smooth(self): + # TODO, Change this to not rely on a cubic-to-quadratic conversion return self.change_anchor_mode("smooth") def make_jagged(self): return self.change_anchor_mode("jagged") def add_subpath(self, points): - assert(len(points) % 4 == 0) - self.points = np.append(self.points, points, axis=0) - return self + assert(len(points) % self.n_points_per_curve == 0) + self.append_points(points) def append_vectorized_mobject(self, vectorized_mobject): new_points = list(vectorized_mobject.points) @@ -558,71 +550,50 @@ def append_vectorized_mobject(self, vectorized_mobject): self.points = self.points[:-1] self.append_points(new_points) + # TODO, how to be smart about tangents here? def apply_function(self, function): - factor = self.pre_function_handle_to_anchor_scale_factor - self.scale_handle_to_anchor_distances(factor) Mobject.apply_function(self, function) - self.scale_handle_to_anchor_distances(1. / factor) if self.make_smooth_after_applying_functions: self.make_smooth() return self - def scale_handle_to_anchor_distances(self, factor): - """ - If the distance between a given handle point H and its associated - anchor point A is d, then it changes H to be a distances factor*d - away from A, but so that the line from A to H doesn't change. - This is mostly useful in the context of applying a (differentiable) - function, to preserve tangency properties. One would pull all the - handles closer to their anchors, apply the function then push them out - again. - """ - for submob in self.family_members_with_points(): - if len(submob.points) < self.n_points_per_cubic_curve: - continue - a1, h1, h2, a2 = submob.get_anchors_and_handles() - a1_to_h1 = h1 - a1 - a2_to_h2 = h2 - a2 - new_h1 = a1 + factor * a1_to_h1 - new_h2 = a2 + factor * a2_to_h2 - submob.set_anchors_and_handles(a1, new_h1, new_h2, a2) - return self + def flip(self, *args, **kwargs): + super().flip(*args, **kwargs) + self.refresh_unit_normal() + self.refresh_triangulation() # def consider_points_equals(self, p0, p1): - return np.allclose( - p0, p1, - atol=self.tolerance_for_point_equality - ) + return get_norm(p1 - p0) < self.tolerance_for_point_equality - # Information about line - def get_cubic_bezier_tuples_from_points(self, points): - nppcc = VMobject.CONFIG["n_points_per_cubic_curve"] - remainder = len(points) % nppcc + # Information about the curve + def get_bezier_tuples_from_points(self, points): + nppc = self.n_points_per_curve + remainder = len(points) % nppc points = points[:len(points) - remainder] - return np.array([ - points[i:i + nppcc] - for i in range(0, len(points), nppcc) - ]) + return [ + points[i:i + nppc] + for i in range(0, len(points), nppc) + ] - def get_cubic_bezier_tuples(self): - return self.get_cubic_bezier_tuples_from_points( - self.get_points() - ) + def get_bezier_tuples(self): + return self.get_bezier_tuples_from_points(self.get_points()) def get_subpaths_from_points(self, points): - nppcc = self.n_points_per_cubic_curve - split_indices = filter( - lambda n: not self.consider_points_equals( - points[n - 1], points[n] - ), - range(nppcc, len(points), nppcc) - ) - split_indices = [0] + list(split_indices) + [len(points)] + nppc = self.n_points_per_curve + diffs = points[nppc - 1:-1:nppc] - points[nppc::nppc] + splits = (diffs * diffs).sum(1) > self.tolerance_for_point_equality + split_indices = np.arange(nppc, len(points), nppc, dtype=int)[splits] + + # split_indices = filter( + # lambda n: not self.consider_points_equals(points[n - 1], points[n]), + # range(nppc, len(points), nppc) + # ) + split_indices = [0, *split_indices, len(points)] return [ points[i1:i2] for i1, i2 in zip(split_indices, split_indices[1:]) - if (i2 - i1) >= nppcc + if (i2 - i1) >= nppc ] def get_subpaths(self): @@ -630,55 +601,56 @@ def get_subpaths(self): def get_nth_curve_points(self, n): assert(n < self.get_num_curves()) - nppcc = self.n_points_per_cubic_curve - return self.points[nppcc * n:nppcc * (n + 1)] + nppc = self.n_points_per_curve + return self.points[nppc * n:nppc * (n + 1)] def get_nth_curve_function(self, n): return bezier(self.get_nth_curve_points(n)) def get_num_curves(self): - nppcc = self.n_points_per_cubic_curve - return len(self.points) // nppcc + return len(self.points) // self.n_points_per_curve def point_from_proportion(self, alpha): - num_cubics = self.get_num_curves() - n, residue = integer_interpolate(0, num_cubics, alpha) - curve = self.get_nth_curve_function(n) - return curve(residue) + num_curves = self.get_num_curves() + n, residue = integer_interpolate(0, num_curves, alpha) + curve_func = self.get_nth_curve_function(n) + return curve_func(residue) def get_anchors_and_handles(self): """ - returns anchors1, handles1, handles2, anchors2, - where (anchors1[i], handles1[i], handles2[i], anchors2[i]) - will be four points defining a cubic bezier curve + returns anchors1, handles, anchors2, + where (anchors1[i], handles[i], anchors2[i]) + will be three points defining a quadratic bezier curve for any i in range(0, len(anchors1)) """ - nppcc = self.n_points_per_cubic_curve + nppc = self.n_points_per_curve return [ - self.points[i::nppcc] - for i in range(nppcc) + self.points[i::nppc] + for i in range(nppc) ] def get_start_anchors(self): - return self.points[0::self.n_points_per_cubic_curve] + return self.points[0::self.n_points_per_curve] def get_end_anchors(self): - nppcc = self.n_points_per_cubic_curve - return self.points[nppcc - 1::nppcc] + nppc = self.n_points_per_curve + return self.points[nppc - 1::nppc] def get_anchors(self): - if self.points.shape[0] == 1: + if len(self.points) == 1: return self.points return np.array(list(it.chain(*zip( self.get_start_anchors(), self.get_end_anchors(), )))) - def get_points_defining_boundary(self): - return np.array(list(it.chain(*[ - sm.get_anchors() - for sm in self.get_family() - ]))) + def get_points_without_null_curves(self, atol=1e-9): + nppc = self.n_points_per_curve + distinct_curves = reduce(op.or_, [ + (abs(self.points[i::nppc] - self.points[0::nppc]) > atol).any(1) + for i in range(1, nppc) + ]) + return self.points[distinct_curves.repeat(nppc)] def get_arc_length(self, n_sample_points=None): if n_sample_points is None: @@ -688,106 +660,156 @@ def get_arc_length(self, n_sample_points=None): for a in np.linspace(0, 1, n_sample_points) ]) diffs = points[1:] - points[:-1] - norms = np.apply_along_axis(get_norm, 1, diffs) - return np.sum(norms) + norms = np.array([get_norm(d) for d in diffs]) + return norms.sum() + + def get_area_vector(self): + # Returns a vector whose length is the area bound by + # the polygon formed by the anchor points, pointing + # in a direction perpendicular to the polygon according + # to the right hand rule. + if self.has_no_points(): + return np.zeros(3) + + nppc = self.n_points_per_curve + p0 = self.points[0::nppc] + p1 = self.points[nppc - 1::nppc] + + # Each term goes through all edges [(x1, y1, z1), (x2, y2, z2)] + return 0.5 * np.array([ + sum((p0[:, 1] + p1[:, 1]) * (p1[:, 2] - p0[:, 2])), # Add up (y1 + y2)*(z2 - z1) + sum((p0[:, 2] + p1[:, 2]) * (p1[:, 0] - p0[:, 0])), # Add up (z1 + z2)*(x2 - x1) + sum((p0[:, 0] + p1[:, 0]) * (p1[:, 1] - p0[:, 1])), # Add up (x1 + x2)*(y2 - y1) + ]) + + def get_unit_normal(self): + if self.unit_normal_locked: + return self.saved_unit_normal + + if len(self.points) < 3: + return OUT + + area_vect = self.get_area_vector() + area = get_norm(area_vect) + if area > 0: + return area_vect / area + else: + return get_unit_normal( + self.points[1] - self.points[0], + self.points[2] - self.points[1], + ) + + def lock_unit_normal(self, family=True): + mobs = self.get_family() if family else [self] + for mob in mobs: + mob.unit_normal_locked = False + mob.saved_unit_normal = mob.get_unit_normal() + mob.unit_normal_locked = True + return self + + def unlock_unit_normal(self): + for mob in self.get_family(): + self.unit_normal_locked = False + return self + + def refresh_unit_normal(self): + for mob in self.get_family(): + mob.unit_normal_locked = False + mob.saved_unit_normal = mob.get_unit_normal() + mob.unit_normal_locked = True + return self # Alignment def align_points(self, vmobject): self.align_rgbas(vmobject) - if self.get_num_points() == vmobject.get_num_points(): + if len(self.points) == len(vmobject.points): return for mob in self, vmobject: # If there are no points, add one to - # whereever the "center" is + # where the "center" is if mob.has_no_points(): mob.start_new_path(mob.get_center()) # If there's only one point, turn it into # a null curve if mob.has_new_path_started(): - mob.add_line_to(mob.get_last_point()) + mob.add_line_to(mob.points[0]) # Figure out what the subpaths are, and align subpaths1 = self.get_subpaths() subpaths2 = vmobject.get_subpaths() n_subpaths = max(len(subpaths1), len(subpaths2)) # Start building new ones - new_path1 = np.zeros((0, self.dim)) - new_path2 = np.zeros((0, self.dim)) + new_subpaths1 = [] + new_subpaths2 = [] - nppcc = self.n_points_per_cubic_curve + nppc = self.n_points_per_curve def get_nth_subpath(path_list, n): if n >= len(path_list): # Create a null path at the very end - return [path_list[-1][-1]] * nppcc + return [path_list[-1][-1]] * nppc return path_list[n] for n in range(n_subpaths): sp1 = get_nth_subpath(subpaths1, n) sp2 = get_nth_subpath(subpaths2, n) - diff1 = max(0, (len(sp2) - len(sp1)) // nppcc) - diff2 = max(0, (len(sp1) - len(sp2)) // nppcc) + diff1 = max(0, (len(sp2) - len(sp1)) // nppc) + diff2 = max(0, (len(sp1) - len(sp2)) // nppc) sp1 = self.insert_n_curves_to_point_list(diff1, sp1) sp2 = self.insert_n_curves_to_point_list(diff2, sp2) - new_path1 = np.append(new_path1, sp1, axis=0) - new_path2 = np.append(new_path2, sp2, axis=0) - self.set_points(new_path1) - vmobject.set_points(new_path2) + new_subpaths1.append(sp1) + new_subpaths2.append(sp2) + self.set_points(np.vstack(new_subpaths1)) + vmobject.set_points(np.vstack(new_subpaths2)) return self - def insert_n_curves(self, n): - new_path_point = None - if self.has_new_path_started(): - new_path_point = self.get_last_point() - - new_points = self.insert_n_curves_to_point_list( - n, self.get_points() - ) - self.set_points(new_points) - - if new_path_point is not None: - self.append_points([new_path_point]) + def insert_n_curves(self, n, family=True): + mobs = self.get_family() if family else [self] + for mob in mobs: + if mob.get_num_curves() > 0: + new_points = mob.insert_n_curves_to_point_list(n, mob.get_points()) + # TODO, this should happen in insert_n_curves_to_point_list + if mob.has_new_path_started(): + new_points = np.vstack([new_points, mob.get_last_point()]) + mob.set_points(new_points) return self def insert_n_curves_to_point_list(self, n, points): + nppc = self.n_points_per_curve if len(points) == 1: - nppcc = self.n_points_per_cubic_curve - return np.repeat(points, nppcc * n, 0) - bezier_quads = self.get_cubic_bezier_tuples_from_points(points) - curr_num = len(bezier_quads) - target_num = curr_num + n - # This is an array with values ranging from 0 - # up to curr_num, with repeats such that - # it's total length is target_num. For example, - # with curr_num = 10, target_num = 15, this would - # be [0, 0, 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 8, 9] - repeat_indices = (np.arange(target_num) * curr_num) // target_num - - # If the nth term of this list is k, it means - # that the nth curve of our path should be split - # into k pieces. In the above example, this would - # be [2, 1, 2, 1, 2, 1, 2, 1, 2, 1] - split_factors = [ - sum(repeat_indices == i) - for i in range(curr_num) - ] - new_points = np.zeros((0, self.dim)) - for quad, sf in zip(bezier_quads, split_factors): - # What was once a single cubic curve defined - # by "quad" will now be broken into sf - # smaller cubic curves - alphas = np.linspace(0, 1, sf + 1) + return np.repeat(points, nppc * n, 0) + + bezier_groups = self.get_bezier_tuples_from_points(points) + norms = np.array([ + get_norm(bg[nppc - 1] - bg[0]) + for bg in bezier_groups + ]) + total_norm = sum(norms) + # Calculate insertions per curve (ipc) + if total_norm < 1e-6: + ipc = [n] + [0] * (len(bezier_groups) - 1) + else: + ipc = np.round(n * norms / sum(norms)).astype(int) + + diff = n - sum(ipc) + for x in range(diff): + ipc[np.argmin(ipc)] += 1 + for x in range(-diff): + ipc[np.argmax(ipc)] -= 1 + + new_points = [] + for group, n_inserts in zip(bezier_groups, ipc): + # What was once a single quadratic curve defined + # by "group" will now be broken into n_inserts + 1 + # smaller quadratic curves + alphas = np.linspace(0, 1, n_inserts + 2) for a1, a2 in zip(alphas, alphas[1:]): - new_points = np.append( - new_points, - partial_bezier_points(quad, a1, a2), - axis=0 - ) - return new_points + new_points += partial_quadratic_bezier_points(group, a1, a2) + return np.vstack(new_points) def align_rgbas(self, vmobject): - attrs = ["fill_rgbas", "stroke_rgbas", "background_stroke_rgbas"] + attrs = ["fill_rgbas", "stroke_rgbas"] for attr in attrs: a1 = getattr(self, attr) a2 = getattr(vmobject, attr) @@ -799,64 +821,57 @@ def align_rgbas(self, vmobject): setattr(self, attr, new_a1) return self - def get_point_mobject(self, center=None): - if center is None: - center = self.get_center() - point = VectorizedPoint(center) - point.match_style(self) - return point - def interpolate_color(self, mobject1, mobject2, alpha): attrs = [ "fill_rgbas", "stroke_rgbas", - "background_stroke_rgbas", "stroke_width", - "background_stroke_width", - "sheen_direction", - "sheen_factor", ] for attr in attrs: - setattr(self, attr, interpolate( + set_array_by_interpolation( + getattr(self, attr), getattr(mobject1, attr), getattr(mobject2, attr), alpha - )) - if alpha == 1.0: - setattr(self, attr, getattr(mobject2, attr)) + ) def pointwise_become_partial(self, vmobject, a, b): assert(isinstance(vmobject, VMobject)) - # Partial curve includes three portions: - # - A middle section, which matches the curve exactly - # - A start, which is some ending portion of an inner cubic - # - An end, which is the starting portion of a later inner cubic + self.points[:] = vmobject.points[:] if a <= 0 and b >= 1: - self.set_points(vmobject.points) return self - bezier_quads = vmobject.get_cubic_bezier_tuples() - num_cubics = len(bezier_quads) + num_curves = self.get_num_curves() + nppc = self.n_points_per_curve - lower_index, lower_residue = integer_interpolate(0, num_cubics, a) - upper_index, upper_residue = integer_interpolate(0, num_cubics, b) - - self.clear_points() - if num_cubics == 0: + # Partial curve includes three portions: + # - A middle section, which matches the curve exactly + # - A start, which is some ending portion of an inner quadratic + # - An end, which is the starting portion of a later inner quadratic + + lower_index, lower_residue = integer_interpolate(0, num_curves, a) + upper_index, upper_residue = integer_interpolate(0, num_curves, b) + i1 = nppc * lower_index + i2 = nppc * (lower_index + 1) + i3 = nppc * upper_index + i4 = nppc * (upper_index + 1) + + if num_curves == 0: + self.points[:] = 0 return self if lower_index == upper_index: - self.append_points(partial_bezier_points( - bezier_quads[lower_index], - lower_residue, upper_residue - )) + tup = partial_quadratic_bezier_points(vmobject.points[i1:i2], lower_residue, upper_residue) + self.points[:i1] = tup[0] + self.points[i1:i4] = tup + self.points[i4:] = tup[2] + self.points[nppc:] = self.points[nppc - 1] else: - self.append_points(partial_bezier_points( - bezier_quads[lower_index], lower_residue, 1 - )) - for quad in bezier_quads[lower_index + 1:upper_index]: - self.append_points(quad) - self.append_points(partial_bezier_points( - bezier_quads[upper_index], 0, upper_residue - )) + low_tup = partial_quadratic_bezier_points(vmobject.points[i1:i2], lower_residue, 1) + high_tup = partial_quadratic_bezier_points(vmobject.points[i3:i4], 0, upper_residue) + self.points[0:i1] = low_tup[0] + self.points[i1:i2] = low_tup + # Keep points i2:i3 as they are + self.points[i3:i4] = high_tup + self.points[i4:] = high_tup[2] return self def get_subcurve(self, a, b): @@ -864,6 +879,195 @@ def get_subcurve(self, a, b): vmob.pointwise_become_partial(self, a, b) return vmob + # For shaders + def init_shader_data(self): + self.fill_data = np.zeros(len(self.points), dtype=self.fill_dtype) + self.stroke_data = np.zeros(len(self.points), dtype=self.stroke_dtype) + self.fill_shader_wrapper = ShaderWrapper( + vert_data=self.fill_data, + vert_indices=np.zeros(0, dtype='i4'), + vert_file=self.fill_vert_shader_file, + geom_file=self.fill_geom_shader_file, + frag_file=self.fill_frag_shader_file, + render_primative=self.render_primative, + ) + self.stroke_shader_wrapper = ShaderWrapper( + vert_data=self.stroke_data, + vert_file=self.stroke_vert_shader_file, + geom_file=self.stroke_geom_shader_file, + frag_file=self.stroke_frag_shader_file, + render_primative=self.render_primative, + ) + + def refresh_shader_wrapper_id(self): + for wrapper in [self.fill_shader_wrapper, self.stroke_shader_wrapper]: + wrapper.refresh_id() + return self + + def get_fill_shader_wrapper(self): + self.fill_shader_wrapper.vert_data = self.get_fill_shader_data() + self.fill_shader_wrapper.vert_indices = self.get_fill_shader_vert_indices() + self.fill_shader_wrapper.uniforms = self.get_shader_uniforms() + self.fill_shader_wrapper.depth_test = self.depth_test + return self.fill_shader_wrapper + + def get_stroke_shader_wrapper(self): + self.stroke_shader_wrapper.vert_data = self.get_stroke_shader_data() + self.stroke_shader_wrapper.uniforms = self.get_stroke_uniforms() + self.stroke_shader_wrapper.depth_test = self.depth_test + return self.stroke_shader_wrapper + + def get_shader_wrapper_list(self): + # Build up data lists + fill_shader_wrappers = [] + stroke_shader_wrappers = [] + back_stroke_shader_wrappers = [] + for submob in self.family_members_with_points(): + if submob.has_fill(): + fill_shader_wrappers.append(submob.get_fill_shader_wrapper()) + if submob.has_stroke(): + ssw = submob.get_stroke_shader_wrapper() + if submob.draw_stroke_behind_fill: + back_stroke_shader_wrappers.append(ssw) + else: + stroke_shader_wrappers.append(ssw) + + # Combine data lists + wrapper_lists = [ + back_stroke_shader_wrappers, + fill_shader_wrappers, + stroke_shader_wrappers + ] + result = [] + for wlist in wrapper_lists: + if wlist: + wrapper = wlist[0] + wrapper.combine_with(*wlist[1:]) + result.append(wrapper) + return result + + def get_stroke_uniforms(self): + j_map = { + "auto": 0, + "round": 1, + "bevel": 2, + "miter": 3, + } + result = super().get_shader_uniforms() + result["joint_type"] = j_map[self.joint_type] + return result + + def get_stroke_shader_data(self): + rgbas = self.get_stroke_rgbas() + if len(rgbas) > 1: + rgbas = self.stretched_style_array_matching_points(rgbas) + + stroke_width = self.stroke_width + if len(stroke_width) > 1: + stroke_width = self.stretched_style_array_matching_points(stroke_width) + + points = self.points + nppc = self.n_points_per_curve + + data = self.get_blank_shader_data_array(len(points), "stroke_data") + data["point"] = points + data["prev_point"][:nppc] = points[-nppc:] + data["prev_point"][nppc:] = points[:-nppc] + data["next_point"][:-nppc] = points[nppc:] + data["next_point"][-nppc:] = points[:nppc] + data["unit_normal"] = self.get_unit_normal() + data["stroke_width"][:, 0] = stroke_width + data["color"] = rgbas + return data + + def lock_triangulation(self, family=True): + mobs = self.get_family() if family else [self] + for mob in mobs: + mob.triangulation_locked = False + mob.saved_triangulation = mob.get_triangulation() + mob.triangulation_locked = True + return self + + def unlock_triangulation(self): + for sm in self.get_family(): + sm.triangulation_locked = False + + def refresh_triangulation(self): + for mob in self.get_family(): + if mob.triangulation_locked: + mob.triangulation_locked = False + mob.saved_triangulation = mob.get_triangulation() + mob.triangulation_locked = True + return self + + def get_triangulation(self, normal_vector=None): + # Figure out how to triangulate the interior to know + # how to send the points as to the vertex shader. + # First triangles come directly from the points + if normal_vector is None: + normal_vector = self.get_unit_normal() + + if self.triangulation_locked: + return self.saved_triangulation + + if len(self.points) <= 1: + return np.zeros(0, dtype='i4') + + # Rotate points such that unit normal vector is OUT + # TODO, 99% of the time this does nothing. Do a check for that? + points = np.dot(self.points, z_to_vector(normal_vector)) + indices = np.arange(len(points), dtype=int) + + b0s = points[0::3] + b1s = points[1::3] + b2s = points[2::3] + v01s = b1s - b0s + v12s = b2s - b1s + + crosses = cross2d(v01s, v12s) + convexities = np.sign(crosses) + + atol = self.tolerance_for_point_equality + end_of_loop = np.zeros(len(b0s), dtype=bool) + end_of_loop[:-1] = (np.abs(b2s[:-1] - b0s[1:]) > atol).any(1) + end_of_loop[-1] = True + + concave_parts = convexities < 0 + + # These are the vertices to which we'll apply a polygon triangulation + inner_vert_indices = np.hstack([ + indices[0::3], + indices[1::3][concave_parts], + indices[2::3][end_of_loop], + ]) + inner_vert_indices.sort() + rings = np.arange(1, len(inner_vert_indices) + 1)[inner_vert_indices % 3 == 2] + + # Triangulate + inner_verts = points[inner_vert_indices] + inner_tri_indices = inner_vert_indices[earclip_triangulation(inner_verts, rings)] + + tri_indices = np.hstack([indices, inner_tri_indices]) + return tri_indices + + def get_fill_shader_data(self): + points = self.points + n_points = len(points) + unit_normal = self.get_unit_normal() + + # TODO, best way to enable multiple colors? + rgbas = self.get_fill_rgbas()[:1] + + data = self.get_blank_shader_data_array(n_points, "fill_data") + data["point"] = points + data["unit_normal"] = unit_normal + data["color"] = rgbas + data["vert_index"][:, 0] = range(n_points) + return data + + def get_fill_shader_vert_indices(self): + return self.get_triangulation() + class VGroup(VMobject): def __init__(self, *vmobjects, **kwargs): @@ -873,7 +1077,7 @@ def __init__(self, *vmobjects, **kwargs): self.add(*vmobjects) -class VectorizedPoint(VMobject): +class VectorizedPoint(VMobject, Point): CONFIG = { "color": BLACK, "fill_opacity": 0, @@ -886,24 +1090,11 @@ def __init__(self, location=ORIGIN, **kwargs): VMobject.__init__(self, **kwargs) self.set_points(np.array([location])) - def get_width(self): - return self.artificial_width - - def get_height(self): - return self.artificial_height - - def get_location(self): - return np.array(self.points[0]) - - def set_location(self, new_loc): - self.set_points(np.array([new_loc])) - class CurvesAsSubmobjects(VGroup): def __init__(self, vmobject, **kwargs): VGroup.__init__(self, **kwargs) - tuples = vmobject.get_cubic_bezier_tuples() - for tup in tuples: + for tup in vmobject.get_bezier_tuples(): part = VMobject() part.set_points(tup) part.match_style(vmobject) diff --git a/manimlib/mobject/vector_field.py b/manimlib/mobject/vector_field.py index bfc90c2cc3..348f8aad92 100644 --- a/manimlib/mobject/vector_field.py +++ b/manimlib/mobject/vector_field.py @@ -145,7 +145,7 @@ class VectorField(VGroup): } def __init__(self, func, **kwargs): - VGroup.__init__(self, **kwargs) + super().__init__(**kwargs) self.func = func self.rgb_gradient_function = get_rgb_gradient_function( self.min_magnitude, diff --git a/manimlib/once_useful_constructs/combinatorics.py b/manimlib/once_useful_constructs/combinatorics.py index e4fdab29be..703072e12f 100644 --- a/manimlib/once_useful_constructs/combinatorics.py +++ b/manimlib/once_useful_constructs/combinatorics.py @@ -100,7 +100,7 @@ class GeneralizedPascalsTriangle(VMobject): "submob_class": combinationMobject, } - def generate_points(self): + def init_points(self): self.cell_height = float(self.height) / self.nrows self.cell_width = float(self.width) / self.nrows self.bottom_left = (self.cell_width * self.nrows / 2.0) * LEFT + \ @@ -153,7 +153,7 @@ def generate_n_choose_k_mobs(self): def fill_with_n_choose_k(self): if not hasattr(self, "coords_to_n_choose_k"): self.generate_n_choose_k_mobs() - self.submobjects = [] + self.set_submobjects([]) self.add(*[ self.coords_to_n_choose_k[n][k] for n, k in self.coords diff --git a/manimlib/once_useful_constructs/fractals.py b/manimlib/once_useful_constructs/fractals.py index 3983d3a004..88d6225c5a 100644 --- a/manimlib/once_useful_constructs/fractals.py +++ b/manimlib/once_useful_constructs/fractals.py @@ -62,11 +62,11 @@ def fractalification_iteration(vmobject, dimension=1.05, num_inserted_anchors_ra new_anchors += [p1] + inserted_points new_anchors.append(original_anchors[-1]) vmobject.set_points_as_corners(new_anchors) - vmobject.submobjects = [ + vmobject.set_submobjects([ fractalification_iteration( submob, dimension, num_inserted_anchors_range) for submob in vmobject.submobjects - ] + ]) return vmobject @@ -84,12 +84,12 @@ def init_colors(self): VMobject.init_colors(self) self.set_color_by_gradient(*self.colors) - def generate_points(self): + def init_points(self): order_n_self = self.get_order_n_self(self.order) if self.order == 0: - self.submobjects = [order_n_self] + self.set_submobjects([order_n_self]) else: - self.submobjects = order_n_self.submobjects + self.set_submobjects(order_n_self.submobjects) return self def get_order_n_self(self, order): @@ -210,7 +210,7 @@ def init_colors(self): pi.set_color(color) pi.set_stroke(color, width=0) - def generate_points(self): + def init_points(self): random.seed(self.random_seed) modes = get_all_pi_creature_modes() seed = PiCreature(mode=self.start_mode) @@ -315,7 +315,7 @@ class FractalCurve(VMobject): }, } - def generate_points(self): + def init_points(self): points = self.get_anchor_points() self.set_points_as_corners(points) if not self.monochromatic: diff --git a/manimlib/once_useful_constructs/light.py b/manimlib/once_useful_constructs/light.py index 6bf519191e..7bc15a6b48 100644 --- a/manimlib/once_useful_constructs/light.py +++ b/manimlib/once_useful_constructs/light.py @@ -67,9 +67,9 @@ def __init__(self, light, **kwargs): if (not isinstance(light, AmbientLight) and not isinstance(light, Spotlight)): raise Exception( "Only AmbientLights and Spotlights can be switched off") - light.submobjects = light.submobjects[::-1] + light.set_submobjects(light.submobjects[::-1]) LaggedStartMap.__init__(self, FadeOut, light, **kwargs) - light.submobjects = light.submobjects[::-1] + light.set_submobjects(light.submobjects[::-1]) class Lighthouse(SVGMobject): @@ -103,7 +103,7 @@ class AmbientLight(VMobject): "radius": 5.0 } - def generate_points(self): + def init_points(self): # in theory, this method is only called once, right? # so removing submobs shd not be necessary # @@ -181,8 +181,8 @@ def project(self, point): def get_source_point(self): return self.source_point.get_location() - def generate_points(self): - self.submobjects = [] + def init_points(self): + self.set_submobjects([]) self.add(self.source_point) @@ -346,7 +346,7 @@ class LightSource(VMobject): "camera_mob": None } - def generate_points(self): + def init_points(self): self.add(self.source_point) @@ -493,7 +493,7 @@ def update_ambient(self): ) new_ambient_light.apply_matrix(self.rotation_matrix()) new_ambient_light.move_source_to(self.get_source_point()) - self.ambient_light.submobjects = new_ambient_light.submobjects + self.ambient_light.set_submobjects(new_ambient_light.submobjects) def get_source_point(self): return self.source_point.get_location() diff --git a/manimlib/scene/graph_scene.py b/manimlib/scene/graph_scene.py index 7d5dcf4880..dfbec33123 100644 --- a/manimlib/scene/graph_scene.py +++ b/manimlib/scene/graph_scene.py @@ -4,7 +4,7 @@ from manimlib.animation.transform import Transform from manimlib.animation.update import UpdateFromAlphaFunc from manimlib.constants import * -from manimlib.mobject.functions import ParametricFunction +from manimlib.mobject.functions import ParametricCurve from manimlib.mobject.geometry import Line from manimlib.mobject.geometry import Rectangle from manimlib.mobject.geometry import RegularPolygon @@ -165,7 +165,7 @@ def parameterized_function(alpha): y = self.y_max return self.coords_to_point(x, y) - graph = ParametricFunction( + graph = ParametricCurve( parameterized_function, color=color, **kwargs diff --git a/manimlib/scene/moving_camera_scene.py b/manimlib/scene/moving_camera_scene.py index 4fa232684c..7ad2c16542 100644 --- a/manimlib/scene/moving_camera_scene.py +++ b/manimlib/scene/moving_camera_scene.py @@ -1,6 +1,7 @@ from manimlib.camera.moving_camera import MovingCamera from manimlib.scene.scene import Scene from manimlib.utils.iterables import list_update +from manimlib.utils.family_ops import extract_mobject_family_members class MovingCameraScene(Scene): @@ -18,7 +19,7 @@ def setup(self): def get_moving_mobjects(self, *animations): moving_mobjects = Scene.get_moving_mobjects(self, *animations) - all_moving_mobjects = self.camera.extract_mobject_family_members( + all_moving_mobjects = extract_mobject_family_members( moving_mobjects ) movement_indicators = self.camera.get_mobjects_indicating_movement() diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 014ea528e2..c7d137de8c 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -2,24 +2,29 @@ import random import warnings import platform +import itertools as it from tqdm import tqdm as ProgressDisplay import numpy as np +import time +from IPython.terminal.embed import InteractiveShellEmbed from manimlib.animation.animation import Animation -from manimlib.animation.creation import Write -from manimlib.animation.transform import MoveToTarget, ApplyMethod +from manimlib.animation.transform import MoveToTarget +from manimlib.mobject.mobject import Point from manimlib.camera.camera import Camera from manimlib.constants import * from manimlib.container.container import Container from manimlib.mobject.mobject import Mobject -from manimlib.mobject.svg.tex_mobject import TextMobject from manimlib.scene.scene_file_writer import SceneFileWriter -from manimlib.utils.iterables import list_update +from manimlib.utils.family_ops import extract_mobject_family_members +from manimlib.utils.family_ops import restructure_list_to_exclude_certain_family_members +from manimlib.window import Window class Scene(Container): CONFIG = { + "window_config": {}, "camera_class": Camera, "camera_config": {}, "file_writer_config": {}, @@ -29,33 +34,47 @@ class Scene(Container): "start_at_animation_number": None, "end_at_animation_number": None, "leave_progress_bars": False, + "preview": True, + "linger_after_completion": True, } def __init__(self, **kwargs): Container.__init__(self, **kwargs) - self.camera = self.camera_class(**self.camera_config) - self.file_writer = SceneFileWriter( - self, **self.file_writer_config, - ) + if self.preview: + self.window = Window(self, **self.window_config) + self.camera_config["ctx"] = self.window.ctx + self.virtual_animation_start_time = 0 + self.real_animation_start_time = time.time() + else: + self.window = None + self.camera = self.camera_class(**self.camera_config) + self.file_writer = SceneFileWriter(self, **self.file_writer_config) self.mobjects = [] - # TODO, remove need for foreground mobjects - self.foreground_mobjects = [] self.num_plays = 0 self.time = 0 + self.skip_time = 0 self.original_skipping_status = self.skip_animations + self.time_of_last_frame = time.time() + + # Items associated with interaction + self.mouse_point = Point() + self.mouse_drag_point = Point() + self.zoom_on_scroll = False + self.quit_interaction = False + + # Much nice to work with deterministic scenes if self.random_seed is not None: random.seed(self.random_seed) np.random.seed(self.random_seed) + def run(self): self.setup() try: self.construct() except EndSceneEarlyException: pass self.tear_down() - self.file_writer.finish() - self.print_end_message() def setup(self): """ @@ -65,84 +84,77 @@ def setup(self): """ pass - def tear_down(self): + def construct(self): + # Where all the animation happens + # To be implemented in subclasses pass - def construct(self): - pass # To be implemented in subclasses + def tear_down(self): + self.stop_skipping() + self.file_writer.finish() + if self.window and self.linger_after_completion: + self.interact() + + def interact(self): + # If there is a window, enter a loop + # which updates the frame while under + # the hood calling the pyglet event loop + self.quit_interaction = False + self.lock_static_mobject_data() + while not self.window.is_closing and not self.quit_interaction: + self.update_frame() + if self.window.is_closing: + self.window.destroy() + if self.quit_interaction: + self.unlock_mobject_data() + + def embed(self): + if not self.preview: + # If the scene is just being + # written, ignore embed calls + return + self.stop_skipping() + self.linger_after_completion = False + self.update_frame() + + shell = InteractiveShellEmbed() + # Have the frame update after each command + shell.events.register('post_run_cell', lambda *a, **kw: self.update_frame()) + # Stack depth of 2 means the shell will use + # the namespace of the caller, not this method + shell(stack_depth=2) + # End scene when exiting an embed. + raise EndSceneEarlyException() def __str__(self): return self.__class__.__name__ - def print_end_message(self): - print("Played {} animations".format(self.num_plays)) - - def set_variables_as_attrs(self, *objects, **newly_named_objects): - """ - This method is slightly hacky, making it a little easier - for certain methods (typically subroutines of construct) - to share local variables. - """ - caller_locals = inspect.currentframe().f_back.f_locals - for key, value in list(caller_locals.items()): - for o in objects: - if value is o: - setattr(self, key, value) - for key, value in list(newly_named_objects.items()): - setattr(self, key, value) - return self - - def get_attrs(self, *keys): - return [getattr(self, key) for key in keys] - # Only these methods should touch the camera - def set_camera(self, camera): - self.camera = camera - - def get_frame(self): - return np.array(self.camera.get_pixel_array()) - def get_image(self): return self.camera.get_image() - def set_camera_pixel_array(self, pixel_array): - self.camera.set_pixel_array(pixel_array) - - def set_camera_background(self, background): - self.camera.set_background(background) - - def reset_camera(self): - self.camera.reset() - - def capture_mobjects_in_camera(self, mobjects, **kwargs): - self.camera.capture_mobjects(mobjects, **kwargs) - - def update_frame( - self, - mobjects=None, - background=None, - include_submobjects=True, - ignore_skipping=True, - **kwargs): + def update_frame(self, dt=0, ignore_skipping=False): + self.increment_time(dt) + self.update_mobjects(dt) if self.skip_animations and not ignore_skipping: return - if mobjects is None: - mobjects = list_update( - self.mobjects, - self.foreground_mobjects, - ) - if background is not None: - self.set_camera_pixel_array(background) - else: - self.reset_camera() - kwargs["include_submobjects"] = include_submobjects - self.capture_mobjects_in_camera(mobjects, **kwargs) + if self.window: + self.window.clear() + self.camera.clear() + self.camera.capture(*self.mobjects) + + if self.window: + self.window.swap_buffers() + vt = self.time - self.virtual_animation_start_time + rt = time.time() - self.real_animation_start_time + if rt < vt: + self.update_frame(0) + + def emit_frame(self): + if not self.skip_animations: + self.file_writer.write_frame(self.camera) - def freeze_background(self): - self.update_frame() - self.set_camera(Camera(self.get_frame())) - self.clear() ### def update_mobjects(self, dt): @@ -151,8 +163,8 @@ def update_mobjects(self, dt): def should_update_mobjects(self): return self.always_update_mobjects or any([ - mob.has_time_based_updater() - for mob in self.get_mobject_family_members() + len(mob.get_family_updaters()) > 0 + for mob in self.mobjects ]) ### @@ -160,11 +172,10 @@ def should_update_mobjects(self): def get_time(self): return self.time - def increment_time(self, d_time): - self.time += d_time + def increment_time(self, dt): + self.time += dt ### - def get_top_level_mobjects(self): # Return only those which are not in the family # of another mobject from the scene @@ -180,16 +191,15 @@ def is_top_level(mobject): return list(filter(is_top_level, mobjects)) def get_mobject_family_members(self): - return self.camera.extract_mobject_family_members(self.mobjects) + return extract_mobject_family_members(self.mobjects) - def add(self, *mobjects): + def add(self, *new_mobjects): """ Mobjects will be displayed, from background to foreground in the order with which they are added. """ - mobjects = [*mobjects, *self.foreground_mobjects] - self.restructure_mobjects(to_remove=mobjects) - self.mobjects += mobjects + self.remove(*new_mobjects) + self.mobjects += new_mobjects return self def add_mobjects_among(self, values): @@ -204,61 +214,12 @@ def add_mobjects_among(self, values): )) return self - def remove(self, *mobjects): - for list_name in "mobjects", "foreground_mobjects": - self.restructure_mobjects(mobjects, list_name, False) - return self - - def restructure_mobjects(self, to_remove, - mobject_list_name="mobjects", - extract_families=True): - """ - In cases where the scene contains a group, e.g. Group(m1, m2, m3), but one - of its submobjects is removed, e.g. scene.remove(m1), the list of mobjects - will be editing to contain other submobjects, but not m1, e.g. it will now - insert m2 and m3 to where the group once was. - """ - if extract_families: - to_remove = self.camera.extract_mobject_family_members(to_remove) - _list = getattr(self, mobject_list_name) - new_list = self.get_restructured_mobject_list(_list, to_remove) - setattr(self, mobject_list_name, new_list) - return self - - def get_restructured_mobject_list(self, mobjects, to_remove): - new_mobjects = [] - - def add_safe_mobjects_from_list(list_to_examine, set_to_remove): - for mob in list_to_examine: - if mob in set_to_remove: - continue - intersect = set_to_remove.intersection(mob.get_family()) - if intersect: - add_safe_mobjects_from_list(mob.submobjects, intersect) - else: - new_mobjects.append(mob) - add_safe_mobjects_from_list(mobjects, set(to_remove)) - return new_mobjects - - # TODO, remove this, and calls to this - def add_foreground_mobjects(self, *mobjects): - self.foreground_mobjects = list_update( - self.foreground_mobjects, - mobjects + def remove(self, *mobjects_to_remove): + self.mobjects = restructure_list_to_exclude_certain_family_members( + self.mobjects, mobjects_to_remove ) - self.add(*mobjects) return self - def add_foreground_mobject(self, mobject): - return self.add_foreground_mobjects(mobject) - - def remove_foreground_mobjects(self, *to_remove): - self.restructure_mobjects(to_remove, "foreground_mobjects") - return self - - def remove_foreground_mobject(self, mobject): - return self.remove_foreground_mobjects(mobject) - def bring_to_front(self, *mobjects): self.add(*mobjects) return self @@ -270,7 +231,6 @@ def bring_to_back(self, *mobjects): def clear(self): self.mobjects = [] - self.foreground_mobjects = [] return self def get_mobjects(self): @@ -279,23 +239,6 @@ def get_mobjects(self): def get_mobject_copies(self): return [m.copy() for m in self.mobjects] - def get_moving_mobjects(self, *animations): - # Go through mobjects from start to end, and - # as soon as there's one that needs updating of - # some kind per frame, return the list from that - # point forward. - animation_mobjects = [anim.mobject for anim in animations] - mobjects = self.get_mobject_family_members() - for i, mob in enumerate(mobjects): - update_possibilities = [ - mob in animation_mobjects, - len(mob.get_family_updaters()) > 0, - mob in self.foreground_mobjects - ] - if any(update_possibilities): - return mobjects[i:] - return [] - def get_time_progression(self, run_time, n_iterations=None, override_skip_animations=False): if self.skip_animations and not override_skip_animations: times = [run_time] @@ -303,7 +246,8 @@ def get_time_progression(self, run_time, n_iterations=None, override_skip_animat step = 1 / self.camera.frame_rate times = np.arange(0, run_time, step) time_progression = ProgressDisplay( - times, total=n_iterations, + times, + total=n_iterations, leave=self.leave_progress_bars, ascii=False if platform.system() != 'Windows' else True ) @@ -316,13 +260,12 @@ def get_animation_time_progression(self, animations): run_time = self.get_run_time(animations) time_progression = self.get_time_progression(run_time) time_progression.set_description("".join([ - "Animation {}: ".format(self.num_plays), - str(animations[0]), - (", etc." if len(animations) > 1 else ""), + f"Animation {self.num_plays}: {animations[0]}", + ", etc." if len(animations) > 1 else "", ])) return time_progression - def compile_play_args_to_animation_list(self, *args, **kwargs): + def anims_from_play_args(self, *args, **kwargs): """ Each arg can either be an animation, or a mobject method followed by that methods arguments (and potentially follow @@ -390,42 +333,66 @@ def compile_method(state): return animations def update_skipping_status(self): - if self.start_at_animation_number: + if self.start_at_animation_number is not None: if self.num_plays == self.start_at_animation_number: - self.skip_animations = False - if self.end_at_animation_number: + self.stop_skipping() + if self.end_at_animation_number is not None: if self.num_plays >= self.end_at_animation_number: - self.skip_animations = True raise EndSceneEarlyException() + def stop_skipping(self): + if self.skip_animations: + self.skip_animations = False + self.skip_time += self.time + + # Methods associated with running animations def handle_play_like_call(func): def wrapper(self, *args, **kwargs): self.update_skipping_status() - allow_write = not self.skip_animations - self.file_writer.begin_animation(allow_write) + should_write = not self.skip_animations + if should_write: + self.file_writer.begin_animation() + + if self.window: + self.real_animation_start_time = time.time() + self.virtual_animation_start_time = self.time + func(self, *args, **kwargs) - self.file_writer.end_animation(allow_write) + + if should_write: + self.file_writer.end_animation() + self.num_plays += 1 return wrapper + def lock_static_mobject_data(self, *animations): + movers = list(it.chain(*[ + anim.mobject.get_family() + for anim in animations + ])) + for mobject in self.mobjects: + if mobject in movers: + continue + if mobject.get_family_updaters(): + continue + self.camera.set_mobjects_as_static(mobject) + + def unlock_mobject_data(self): + self.camera.release_static_mobjects() + def begin_animations(self, animations): - curr_mobjects = self.get_mobject_family_members() for animation in animations: - # Begin animation animation.begin() # Anything animated that's not already in the - # scene gets added to the scene + # scene gets added to the scene. Note, for + # animated mobjects that are in the family of + # those on screen, this can result in a restructuring + # of the scene.mobjects list, which is usually desired. mob = animation.mobject - if mob not in curr_mobjects: + if mob not in self.mobjects: self.add(mob) - curr_mobjects += mob.get_family() def progress_through_animations(self, animations): - # Paint all non-moving objects onto the screen, so they don't - # have to be rendered every frame - moving_mobjects = self.get_moving_mobjects(*animations) - self.update_frame(excluded_mobjects=moving_mobjects) - static_image = self.get_frame() last_t = 0 for t in self.get_animation_time_progression(animations): dt = t - last_t @@ -434,9 +401,8 @@ def progress_through_animations(self, animations): animation.update_mobjects(dt) alpha = t / animation.run_time animation.interpolate(alpha) - self.update_mobjects(dt) - self.update_frame(moving_mobjects, static_image) - self.add_frames(self.get_frame()) + self.update_frame(dt) + self.emit_frame() def finish_animations(self, animations): for animation in animations: @@ -446,7 +412,6 @@ def finish_animations(self, animations): anim.mobject for anim in animations ] if self.skip_animations: - # TODO, run this call in for each animation? self.update_mobjects(self.get_run_time(animations)) else: self.update_mobjects(0) @@ -456,15 +421,12 @@ def play(self, *args, **kwargs): if len(args) == 0: warnings.warn("Called Scene.play with no animations") return - animations = self.compile_play_args_to_animation_list( - *args, **kwargs - ) + animations = self.anims_from_play_args(*args, **kwargs) + self.lock_static_mobject_data(*animations) self.begin_animations(animations) self.progress_through_animations(animations) self.finish_animations(animations) - - def idle_stream(self): - self.file_writer.idle_stream() + self.unlock_mobject_data() def clean_up_animations(self, *animations): for animation in animations: @@ -497,28 +459,26 @@ def get_wait_time_progression(self, duration, stop_condition): def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): self.update_mobjects(dt=0) # Any problems with this? if self.should_update_mobjects(): + self.lock_static_mobject_data() time_progression = self.get_wait_time_progression(duration, stop_condition) - # TODO, be smart about setting a static image - # the same way Scene.play does last_t = 0 for t in time_progression: dt = t - last_t last_t = t - self.update_mobjects(dt) - self.update_frame() - self.add_frames(self.get_frame()) + self.update_frame(dt) + self.emit_frame() if stop_condition is not None and stop_condition(): time_progression.close() break + self.unlock_mobject_data() elif self.skip_animations: # Do nothing return self else: - self.update_frame() - dt = 1 / self.camera.frame_rate - n_frames = int(duration / dt) - frame = self.get_frame() - self.add_frames(*[frame] * n_frames) + self.update_frame(duration) + n_frames = int(duration * self.camera.frame_rate) + for n in range(n_frames): + self.emit_frame() return self def wait_until(self, stop_condition, max_time=60): @@ -534,34 +494,82 @@ def revert_to_original_skipping_status(self): self.skip_animations = self.original_skipping_status return self - def add_frames(self, *frames): - dt = 1 / self.camera.frame_rate - self.increment_time(len(frames) * dt) - if self.skip_animations: - return - for frame in frames: - 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) - def show_frame(self): + def show(self): self.update_frame(ignore_skipping=True) self.get_image().show() - # TODO, this doesn't belong in Scene, but should be - # part of some more specialized subclass optimized - # for livestreaming - def tex(self, latex): - eq = TextMobject(latex) - anims = [] - anims.append(Write(eq)) - for mobject in self.mobjects: - anims.append(ApplyMethod(mobject.shift, 2 * UP)) - self.play(*anims) + # Helpers for interactive development + def save_state(self): + self.saved_state = { + "mobjects": self.mobjects, + "mobject_states": [ + mob.copy() + for mob in self.mobjects + ], + } + + def restore(self): + if not hasattr(self, "saved_state"): + raise Exception("Trying to restore scene without having saved") + mobjects = self.saved_state["mobjects"] + states = self.saved_state["mobject_states"] + for mob, state in zip(mobjects, states): + mob.become(state) + self.mobjects = mobjects + + # Event handling + def on_mouse_motion(self, point, d_point): + self.mouse_point.move_to(point) + + def on_mouse_drag(self, point, d_point, buttons, modifiers): + self.mouse_drag_point.move_to(point) + # Only if 3d rotation is enabled? + self.camera.frame.increment_theta(-d_point[0]) + self.camera.frame.increment_phi(d_point[1]) + + def on_mouse_press(self, point, button, mods): + pass + + def on_mouse_release(self, point, button, mods): + pass + + def on_mouse_scroll(self, point, offset): + frame = self.camera.frame + if self.zoom_on_scroll: + factor = 1 + np.arctan(10 * offset[1]) + frame.scale(factor, about_point=point) + else: + frame.shift(-30 * offset) + + def on_key_release(self, symbol, modifiers): + if chr(symbol) == "z": + self.zoom_on_scroll = False + + def on_key_press(self, symbol, modifiers): + if chr(symbol) == "r": + self.camera.frame.to_default_state() + elif chr(symbol) == "z": + self.zoom_on_scroll = True + elif chr(symbol) == "q": + self.quit_interaction = True + + def on_resize(self, width: int, height: int): + self.camera.reset_pixel_shape(width, height) + + def on_show(self): + pass + + def on_hide(self): + pass + + def on_close(self): + pass class EndSceneEarlyException(Exception): diff --git a/manimlib/scene/scene_file_writer.py b/manimlib/scene/scene_file_writer.py index b67d97f392..4b95440093 100644 --- a/manimlib/scene/scene_file_writer.py +++ b/manimlib/scene/scene_file_writer.py @@ -1,17 +1,13 @@ import numpy as np from pydub import AudioSegment import shutil -import subprocess +import subprocess as sp import os -import _thread as thread -from time import sleep -import datetime +import sys +import platform import manimlib.constants as consts from manimlib.constants import FFMPEG_BIN -from manimlib.constants import STREAMING_IP -from manimlib.constants import STREAMING_PORT -from manimlib.constants import STREAMING_PROTOCOL from manimlib.utils.config_ops import digest_config from manimlib.utils.file_ops import guarantee_existence from manimlib.utils.file_ops import add_extension_if_not_present @@ -28,20 +24,19 @@ class SceneFileWriter(object): "save_last_frame": False, "movie_file_extension": ".mp4", "gif_file_extension": ".gif", - "livestreaming": False, - "to_twitch": False, - "twitch_key": None, # Previous output_file_name # TODO, address this in extract_scene et. al. "file_name": None, - "input_file_path": "", # ?? + "input_file_path": "", "output_directory": None, + "open_file_upon_completion": False, + "show_file_location_upon_completion": False, + "quiet": False, } def __init__(self, scene, **kwargs): digest_config(self, kwargs) self.scene = scene - self.stream_lock = False self.init_output_directories() self.init_audio() @@ -168,41 +163,24 @@ def add_sound(self, sound_file, time=None, gain=None, **kwargs): self.add_audio_segment(new_segment, time, **kwargs) # Writers - def begin_animation(self, allow_write=False): - if self.write_to_movie and allow_write: + def begin_animation(self): + if self.write_to_movie: self.open_movie_pipe() - if self.livestreaming: - self.stream_lock = False - def end_animation(self, allow_write=False): - if self.write_to_movie and allow_write: + def end_animation(self): + if self.write_to_movie: self.close_movie_pipe() - if self.livestreaming: - self.stream_lock = True - thread.start_new_thread(self.idle_stream, ()) - def write_frame(self, frame): + def write_frame(self, camera): if self.write_to_movie: - self.writing_process.stdin.write(frame.tostring()) + raw_bytes = camera.get_raw_fbo_data() + self.writing_process.stdin.write(raw_bytes) def save_final_image(self, image): file_path = self.get_image_file_path() image.save(file_path) self.print_file_ready_message(file_path) - def idle_stream(self): - while self.stream_lock: - a = datetime.datetime.now() - self.update_frame() - n_frames = 1 - frame = self.get_frame() - self.add_frames(*[frame] * n_frames) - b = datetime.datetime.now() - time_diff = (b - a).total_seconds() - frame_duration = 1 / self.scene.camera.frame_rate - if time_diff < frame_duration: - sleep(frame_duration - time_diff) - def finish(self): if self.write_to_movie: if hasattr(self, "writing_process"): @@ -211,6 +189,8 @@ def finish(self): if self.save_last_frame: self.scene.update_frame(ignore_skipping=True) self.save_final_image(self.scene.get_image()) + if self.should_open_file(): + self.open_file() def open_movie_pipe(self): file_path = self.get_next_partial_movie_path() @@ -220,17 +200,17 @@ def open_movie_pipe(self): self.temp_partial_movie_file_path = temp_file_path fps = self.scene.camera.frame_rate - height = self.scene.camera.get_pixel_height() - width = self.scene.camera.get_pixel_width() + width, height = self.scene.camera.get_pixel_shape() command = [ FFMPEG_BIN, '-y', # overwrite output file if it exists '-f', 'rawvideo', - '-s', '%dx%d' % (width, height), # size of one frame + '-s', f'{width}x{height}', # size of one frame '-pix_fmt', 'rgba', '-r', str(fps), # frames per second '-i', '-', # The imput comes from a pipe + '-vf', 'vflip', '-an', # Tells FFMPEG not to expect any audio '-loglevel', 'error', ] @@ -247,22 +227,12 @@ def open_movie_pipe(self): '-vcodec', 'libx264', '-pix_fmt', 'yuv420p', ] - if self.livestreaming: - if self.to_twitch: - command += ['-f', 'flv'] - command += ['rtmp://live.twitch.tv/app/' + self.twitch_key] - else: - command += ['-f', 'mpegts'] - command += [STREAMING_PROTOCOL + '://' + STREAMING_IP + ':' + STREAMING_PORT] - else: - command += [temp_file_path] - self.writing_process = subprocess.Popen(command, stdin=subprocess.PIPE) + command += [temp_file_path] + self.writing_process = sp.Popen(command, stdin=sp.PIPE) def close_movie_pipe(self): self.writing_process.stdin.close() self.writing_process.wait() - if self.livestreaming: - return True shutil.move( self.temp_partial_movie_file_path, self.partial_movie_file_path, @@ -304,7 +274,7 @@ def combine_movie_files(self): for pf_path in partial_movie_files: if os.name == 'nt': pf_path = pf_path.replace('\\', '/') - fp.write("file \'{}\'\n".format(pf_path)) + fp.write(f"file \'{pf_path}\'\n") movie_file_path = self.get_movie_file_path() commands = [ @@ -320,7 +290,7 @@ def combine_movie_files(self): if not self.includes_sound: commands.insert(-1, '-an') - combine_process = subprocess.Popen(commands) + combine_process = sp.Popen(commands) combine_process.wait() if self.includes_sound: @@ -350,7 +320,7 @@ def combine_movie_files(self): # "-shortest", temp_file_path, ] - subprocess.call(commands) + sp.call(commands) shutil.move(temp_file_path, movie_file_path) os.remove(sound_file_path) @@ -358,3 +328,47 @@ def combine_movie_files(self): def print_file_ready_message(self, file_path): print("\nFile ready at {}\n".format(file_path)) + + def should_open_file(self): + return any([ + self.show_file_location_upon_completion, + self.open_file_upon_completion, + ]) + + def open_file(self): + if self.quiet: + curr_stdout = sys.stdout + sys.stdout = open(os.devnull, "w") + + current_os = platform.system() + file_paths = [] + + if self.save_last_frame: + file_paths.append(self.get_image_file_path()) + if self.write_to_movie: + file_paths.append(self.get_movie_file_path()) + + for file_path in file_paths: + if current_os == "Windows": + os.startfile(file_path) + else: + commands = [] + if current_os == "Linux": + commands.append("xdg-open") + elif current_os.startswith("CYGWIN"): + commands.append("cygstart") + else: # Assume macOS + commands.append("open") + + if self.show_file_location_upon_completion: + commands.append("-R") + + commands.append(file_path) + + FNULL = open(os.devnull, 'w') + sp.call(commands, stdout=FNULL, stderr=sp.STDOUT) + FNULL.close() + + if self.quiet: + sys.stdout.close() + sys.stdout = curr_stdout diff --git a/manimlib/scene/three_d_scene.py b/manimlib/scene/three_d_scene.py index 5c2be57f1f..cdb57a661f 100644 --- a/manimlib/scene/three_d_scene.py +++ b/manimlib/scene/three_d_scene.py @@ -1,5 +1,4 @@ from manimlib.animation.transform import ApplyMethod -from manimlib.camera.three_d_camera import ThreeDCamera from manimlib.constants import DEGREES from manimlib.constants import PRODUCTION_QUALITY_CAMERA_CONFIG from manimlib.mobject.coordinate_systems import ThreeDAxes @@ -14,7 +13,17 @@ class ThreeDScene(Scene): CONFIG = { - "camera_class": ThreeDCamera, + "camera_config": { + "samples": 8, + } + } + # TODO, maybe dragging events should only be activated here? + + +# TODO, these seem deprecated. + +class OldThreeDScene(Scene): + CONFIG = { "ambient_camera_rotation": None, "default_angled_camera_orientation_kwargs": { "phi": 70 * DEGREES, @@ -100,7 +109,7 @@ def set_to_default_angled_camera_orientation(self, **kwargs): self.set_camera_orientation(**config) -class SpecialThreeDScene(ThreeDScene): +class OldSpecialThreeDScene(ThreeDScene): CONFIG = { "cut_axes_at_radius": True, "camera_config": { @@ -162,7 +171,7 @@ def get_axes(self): for piece in new_pieces: piece.shade_in_3d = True new_pieces.match_style(axis.pieces) - axis.pieces.submobjects = new_pieces.submobjects + axis.pieces.set_submobjects(new_pieces.submobjects) for tick in axis.tick_marks: tick.add(VectorizedPoint( 1.5 * tick.get_center(), diff --git a/manimlib/scene/zoomed_scene.py b/manimlib/scene/zoomed_scene.py index b0065dd51f..e5891c306e 100644 --- a/manimlib/scene/zoomed_scene.py +++ b/manimlib/scene/zoomed_scene.py @@ -2,7 +2,6 @@ from manimlib.camera.moving_camera import MovingCamera from manimlib.camera.multi_camera import MultiCamera from manimlib.constants import * -from manimlib.mobject.types.image_mobject import ImageMobjectFromCamera from manimlib.scene.moving_camera_scene import MovingCameraScene from manimlib.utils.simple_functions import fdiv @@ -10,6 +9,7 @@ # break, as it was restructured. +# TODO, this scene no longer works class ZoomedScene(MovingCameraScene): CONFIG = { "camera_class": MultiCamera, diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py new file mode 100644 index 0000000000..ea261ae191 --- /dev/null +++ b/manimlib/shader_wrapper.py @@ -0,0 +1,130 @@ +import os +import warnings +import re +import moderngl +import numpy as np +import copy + +from manimlib.constants import SHADER_DIR + +# Mobjects that should be rendered with +# the same shader will be organized and +# clumped together based on keeping track +# of a dict holding all the relevant information +# to that shader + + +class ShaderWrapper(object): + def __init__(self, + vert_data=None, + vert_indices=None, + vert_file=None, + geom_file=None, + frag_file=None, + uniforms=None, # A dictionary mapping names of uniform variables + texture_paths=None, # A dictionary mapping names to filepaths for textures. + depth_test=False, + render_primative=moderngl.TRIANGLE_STRIP, + ): + self.vert_data = vert_data + self.vert_indices = vert_indices + self.vert_attributes = vert_data.dtype.names + self.vert_file = vert_file + self.geom_file = geom_file + self.frag_file = frag_file + self.uniforms = uniforms or dict() + self.texture_paths = texture_paths or dict() + self.depth_test = depth_test + self.render_primative = str(render_primative) + self.id = self.create_id() + self.program_id = self.create_program_id() + + def copy(self): + result = copy.copy(self) + result.vert_data = np.array(self.vert_data) + if result.vert_indices is not None: + result.vert_indices = np.array(self.vert_indices) + if self.uniforms: + result.uniforms = dict(self.uniforms) + if self.texture_paths: + result.texture_paths = dict(self.texture_paths) + return result + + def is_valid(self): + return all([ + self.vert_data is not None, + self.vert_file, + self.frag_file, + ]) + + def get_id(self): + return self.id + + def get_program_id(self): + return self.program_id + + def create_id(self): + # A unique id for a shader + return "|".join(map(str, [ + self.vert_file, + self.geom_file, + self.frag_file, + self.uniforms, + self.texture_paths, + self.depth_test, + self.render_primative, + ])) + + def refresh_id(self): + self.id = self.create_id() + + def create_program_id(self): + return "|".join(map(str, [self.vert_file, self.geom_file, self.frag_file])) + + def get_program_code(self): + return { + "vertex_shader": get_shader_code_from_file(self.vert_file), + "geometry_shader": get_shader_code_from_file(self.geom_file), + "fragment_shader": get_shader_code_from_file(self.frag_file), + } + + def combine_with(self, *shader_wrappers): + # Assume they are of the same type + if len(shader_wrappers) == 0: + return + if self.vert_indices is not None: + num_verts = len(self.vert_data) + indices_list = [self.vert_indices] + data_list = [self.vert_data] + for sw in shader_wrappers: + indices_list.append(sw.vert_indices + num_verts) + data_list.append(sw.vert_data) + num_verts += len(sw.vert_data) + self.vert_indices = np.hstack(indices_list) + self.vert_data = np.hstack(data_list) + else: + self.vert_data = np.hstack([self.vert_data, *[sw.vert_data for sw in shader_wrappers]]) + return self + + +def get_shader_code_from_file(filename): + if not filename: + return None + + filepath = os.path.join(SHADER_DIR, filename) + if not os.path.exists(filepath): + warnings.warn(f"No file at {filepath}") + return + + with open(filepath, "r") as f: + result = f.read() + + # To share functionality between shaders, some functions are read in + # from other files an inserted into the relevant strings before + # passing to ctx.program for compiling + # Replace "#INSERT " lines with relevant code + insertions = re.findall(r"^#INSERT .*\.glsl$", result, flags=re.MULTILINE) + for line in insertions: + inserted_code = get_shader_code_from_file(line.replace("#INSERT ", "")) + result = result.replace(line, inserted_code) + return result diff --git a/manimlib/shaders/add_light.glsl b/manimlib/shaders/add_light.glsl new file mode 100644 index 0000000000..2e10858dde --- /dev/null +++ b/manimlib/shaders/add_light.glsl @@ -0,0 +1,22 @@ +vec4 add_light(vec4 raw_color, vec3 point, vec3 unit_normal, vec3 light_coords, float gloss, float shadow){ + if(gloss == 0.0 && shadow == 0.0) return raw_color; + + // TODO, do we actually want this? It effectively treats surfaces as two-sided + if(unit_normal.z < 0){ + unit_normal *= -1; + } + + float camera_distance = 6; // TODO, read this in as a uniform? + // Assume everything has already been rotated such that camera is in the z-direction + vec3 to_camera = vec3(0, 0, camera_distance) - point; + vec3 to_light = light_coords - point; + vec3 light_reflection = -to_light + 2 * unit_normal * dot(to_light, unit_normal); + float dot_prod = dot(normalize(light_reflection), normalize(to_camera)); + float shine = gloss * exp(-3 * pow(1 - dot_prod, 2)); + float dp2 = dot(normalize(to_light), unit_normal); + float darkening = mix(1, max(dp2, 0), shadow); + return vec4( + darkening * mix(raw_color.rgb, vec3(1.0), shine), + raw_color.a + ); +} \ No newline at end of file diff --git a/manimlib/shaders/get_gl_Position.glsl b/manimlib/shaders/get_gl_Position.glsl new file mode 100644 index 0000000000..bc7b405f38 --- /dev/null +++ b/manimlib/shaders/get_gl_Position.glsl @@ -0,0 +1,26 @@ +// Assumes the following uniforms exist in the surrounding context: +// uniform vec2 frame_shape; +// uniform float focal_distance; +// uniform float is_fixed_in_frame; + +const vec2 DEFAULT_FRAME_SHAPE = vec2(8.0 * 16.0 / 9.0, 8.0); + +vec4 get_gl_Position(vec3 point){ + vec4 result = vec4(point, 1.0); + if(!bool(is_fixed_in_frame)){ + result.x *= 2.0 / frame_shape.x; + result.y *= 2.0 / frame_shape.y; + result.z *= 2.0 / frame_shape.y; // Should we give the frame a z shape? Does that make sense? + result.z /= focal_distance; + result.xy /= max(1.0 - result.z, 0.0); + // Todo, does this discontinuity add weirdness? Theoretically, by this result, + // the z-coordiante of gl_Position only matter for z-indexing. The reason + // for thie line is to avoid agressive clipping of distant points. + if(result.z < 0) result.z *= 0.1; + } else{ + result.x *= 2.0 / DEFAULT_FRAME_SHAPE.x; + result.y *= 2.0 / DEFAULT_FRAME_SHAPE.y; + } + result.z *= -1; + return result; +} \ No newline at end of file diff --git a/manimlib/shaders/get_rotated_surface_unit_normal_vector.glsl b/manimlib/shaders/get_rotated_surface_unit_normal_vector.glsl new file mode 100644 index 0000000000..81444d9773 --- /dev/null +++ b/manimlib/shaders/get_rotated_surface_unit_normal_vector.glsl @@ -0,0 +1,17 @@ +// Only inlucde in an environment with to_screen_space defined + +vec3 get_rotated_surface_unit_normal_vector(vec3 point, vec3 du_point, vec3 dv_point){ + // normal = get_unit_normal(point, du_point, dv_point); + // return normalize((to_screen_space * vec4(normal, 0.0)).xyz); + vec3 cp = cross( + (du_point - point), + (dv_point - point) + ); + if(length(cp) == 0){ + // Instead choose a normal to just dv_point - point in the direction of point + vec3 v2 = dv_point - point; + cp = cross(cross(v2, point), v2); + } + // The zero is deliberate, as we only want to rotate and not shift + return normalize((to_screen_space * vec4(cp, 0.0)).xyz); +} \ No newline at end of file diff --git a/manimlib/shaders/get_unit_normal.glsl b/manimlib/shaders/get_unit_normal.glsl new file mode 100644 index 0000000000..d755d458f2 --- /dev/null +++ b/manimlib/shaders/get_unit_normal.glsl @@ -0,0 +1,22 @@ +vec3 get_unit_normal(in vec3[3] points){ + float tol = 1e-6; + vec3 v1 = normalize(points[1] - points[0]); + vec3 v2 = normalize(points[2] - points[0]); + vec3 cp = cross(v1, v2); + float cp_norm = length(cp); + if(cp_norm < tol){ + // Three points form a line, so find a normal vector + // to that line in the plane shared with the z-axis + vec3 k_hat = vec3(0.0, 0.0, 1.0); + vec3 new_cp = cross(cross(v2, k_hat), v2); + float new_cp_norm = length(new_cp); + if(new_cp_norm < tol){ + // We only come here if all three points line up + // on the z-axis. + return vec3(0.0, -1.0, 0.0); + // return k_hat; + } + return new_cp / new_cp_norm; + } + return cp / cp_norm; +} \ No newline at end of file diff --git a/manimlib/shaders/image_frag.glsl b/manimlib/shaders/image_frag.glsl new file mode 100644 index 0000000000..154b528eca --- /dev/null +++ b/manimlib/shaders/image_frag.glsl @@ -0,0 +1,13 @@ +#version 330 + +uniform sampler2D Texture; + +in vec2 v_im_coords; +in float v_opacity; + +out vec4 frag_color; + +void main() { + frag_color = texture(Texture, v_im_coords); + frag_color.a = v_opacity; +} \ No newline at end of file diff --git a/manimlib/shaders/image_vert.glsl b/manimlib/shaders/image_vert.glsl new file mode 100644 index 0000000000..90041b7a65 --- /dev/null +++ b/manimlib/shaders/image_vert.glsl @@ -0,0 +1,26 @@ +#version 330 + +uniform vec2 frame_shape; +uniform float anti_alias_width; +uniform mat4 to_screen_space; +uniform float is_fixed_in_frame; +uniform float focal_distance; + +uniform sampler2D Texture; + +in vec3 point; +in vec2 im_coords; +in float opacity; + +out vec2 v_im_coords; +out float v_opacity; + +// Analog of import for manim only +#INSERT get_gl_Position.glsl +#INSERT position_point_into_frame.glsl + +void main(){ + v_im_coords = im_coords; + v_opacity = opacity; + gl_Position = get_gl_Position(position_point_into_frame(point)); +} \ No newline at end of file diff --git a/manimlib/shaders/position_point_into_frame.glsl b/manimlib/shaders/position_point_into_frame.glsl new file mode 100644 index 0000000000..2438b60cda --- /dev/null +++ b/manimlib/shaders/position_point_into_frame.glsl @@ -0,0 +1,12 @@ +// Must be used in an environment with the following uniforms: +// uniform mat4 to_screen_space; +// uniform float is_fixed_in_frame; + +vec3 position_point_into_frame(vec3 point){ + if(bool(is_fixed_in_frame)){ + return point; + }else{ + // Simply apply the pre-computed to_screen_space matrix. + return (to_screen_space * vec4(point, 1)).xyz; + } +} diff --git a/manimlib/shaders/quadratic_bezier_distance.glsl b/manimlib/shaders/quadratic_bezier_distance.glsl new file mode 100644 index 0000000000..d7fd8ccd99 --- /dev/null +++ b/manimlib/shaders/quadratic_bezier_distance.glsl @@ -0,0 +1,110 @@ +// This file is not a shader, it's just a set of +// functions meant to be inserted into other shaders. + +// Must be inserted in a context with a definition for modify_distance_for_endpoints + +// All of this is with respect to a curve that's been rotated/scaled +// so that b0 = (0, 0) and b1 = (1, 0). That is, b2 entirely +// determines the shape of the curve + +vec2 bezier(float t, vec2 b2){ + // Quick returns for the 0 and 1 cases + if (t == 0) return vec2(0, 0); + else if (t == 1) return b2; + // Everything else + return vec2( + 2 * t * (1 - t) + b2.x * t*t, + b2.y * t * t + ); +} + + +float cube_root(float x){ + return sign(x) * pow(abs(x), 1.0 / 3.0); +} + + +int cubic_solve(float a, float b, float c, float d, out float roots[3]){ + // Normalize so a = 1 + b = b / a; + c = c / a; + d = d / a; + + float p = c - b*b / 3.0; + float q = b * (2.0*b*b - 9.0*c) / 27.0 + d; + float p3 = p*p*p; + float disc = q*q + 4.0*p3 / 27.0; + float offset = -b / 3.0; + if(disc >= 0.0){ + float z = sqrt(disc); + float u = (-q + z) / 2.0; + float v = (-q - z) / 2.0; + u = cube_root(u); + v = cube_root(v); + roots[0] = offset + u + v; + return 1; + } + float u = sqrt(-p / 3.0); + float v = acos(-sqrt( -27.0 / p3) * q / 2.0) / 3.0; + float m = cos(v); + float n = sin(v) * 1.732050808; + + float all_roots[3] = float[3]( + offset + u * (n - m), + offset - u * (n + m), + offset + u * (m + m) + ); + + // Only accept roots with a positive derivative + int n_valid_roots = 0; + for(int i = 0; i < 3; i++){ + float r = all_roots[i]; + if(3*r*r + 2*b*r + c > 0){ + roots[n_valid_roots] = r; + n_valid_roots++; + } + } + return n_valid_roots; +} + +float dist_to_line(vec2 p, vec2 b2){ + float t = clamp(p.x / b2.x, 0, 1); + float dist; + if(t == 0) dist = length(p); + else if(t == 1) dist = distance(p, b2); + else dist = abs(p.y); + + return modify_distance_for_endpoints(p, dist, t); +} + + +float dist_to_point_on_curve(vec2 p, float t, vec2 b2){ + t = clamp(t, 0, 1); + return modify_distance_for_endpoints( + p, length(p - bezier(t, b2)), t + ); +} + + +float min_dist_to_curve(vec2 p, vec2 b2, float degree){ + // Check if curve is really a a line + if(degree == 1) return dist_to_line(p, b2); + + // Try finding the exact sdf by solving the equation + // (d/dt) dist^2(t) = 0, which amount to the following + // cubic. + float xm2 = uv_b2.x - 2.0; + float y = uv_b2.y; + float a = xm2*xm2 + y*y; + float b = 3 * xm2; + float c = -(p.x*xm2 + p.y*y) + 2; + float d = -p.x; + + float roots[3]; + int n = cubic_solve(a, b, c, d, roots); + // At most 2 roots will have been populated. + float d0 = dist_to_point_on_curve(p, roots[0], b2); + if(n == 1) return d0; + float d1 = dist_to_point_on_curve(p, roots[1], b2); + return min(d0, d1); +} \ No newline at end of file diff --git a/manimlib/shaders/quadratic_bezier_fill_frag.glsl b/manimlib/shaders/quadratic_bezier_fill_frag.glsl new file mode 100644 index 0000000000..44c9eb036f --- /dev/null +++ b/manimlib/shaders/quadratic_bezier_fill_frag.glsl @@ -0,0 +1,67 @@ +#version 330 + +uniform mat4 to_screen_space; +uniform float is_fixed_in_frame; + +in vec4 color; +in float fill_all; // Either 0 or 1e +in float uv_anti_alias_width; + +in vec3 xyz_coords; +in float orientation; +in vec2 uv_coords; +in vec2 uv_b2; +in float bezier_degree; + +out vec4 frag_color; + +// Needed for quadratic_bezier_distance insertion below +float modify_distance_for_endpoints(vec2 p, float dist, float t){ + return dist; +} + +#INSERT quadratic_bezier_distance.glsl + + +float sdf(){ + if(bezier_degree < 2){ + return abs(uv_coords[1]); + } + float u2 = uv_b2.x; + float v2 = uv_b2.y; + // For really flat curves, just take the distance to x-axis + if(abs(v2 / u2) < 0.1 * uv_anti_alias_width){ + return abs(uv_coords[1]); + } + // For flat-ish curves, take the curve + else if(abs(v2 / u2) < 0.5 * uv_anti_alias_width){ + return min_dist_to_curve(uv_coords, uv_b2, bezier_degree); + } + // I know, I don't love this amount of arbitrary-seeming branching either, + // but a number of strange dimples and bugs pop up otherwise. + + // This converts uv_coords to yet another space where the bezier points sit on + // (0, 0), (1/2, 0) and (1, 1), so that the curve can be expressed implicityly + // as y = x^2. + mat2 to_simple_space = mat2( + v2, 0, + 2 - u2, 4 * v2 + ); + vec2 p = to_simple_space * uv_coords; + // Sign takes care of whether we should be filling the inside or outside of curve. + float sgn = orientation * sign(v2); + float Fp = (p.x * p.x - p.y); + if(sgn * Fp < 0){ + return 0; + }else{ + return min_dist_to_curve(uv_coords, uv_b2, bezier_degree); + } +} + + +void main() { + if (color.a == 0) discard; + frag_color = color; + if (fill_all == 1.0) return; + frag_color.a *= smoothstep(1, 0, sdf() / uv_anti_alias_width); +} diff --git a/manimlib/shaders/quadratic_bezier_fill_geom.glsl b/manimlib/shaders/quadratic_bezier_fill_geom.glsl new file mode 100644 index 0000000000..a28cd0bde0 --- /dev/null +++ b/manimlib/shaders/quadratic_bezier_fill_geom.glsl @@ -0,0 +1,134 @@ +#version 330 + +layout (triangles) in; +layout (triangle_strip, max_vertices = 5) out; + +uniform float anti_alias_width; +// Needed for get_gl_Position +uniform vec2 frame_shape; +uniform float focal_distance; +uniform float is_fixed_in_frame; +uniform vec3 light_source_position; +uniform float gloss; +uniform float shadow; + +in vec3 bp[3]; +in vec3 v_global_unit_normal[3]; +in vec4 v_color[3]; +// in float v_fill_all[3]; +in float v_vert_index[3]; + +out vec4 color; +out float fill_all; +out float uv_anti_alias_width; + +out vec3 xyz_coords; +out float orientation; +// uv space is where b0 = (0, 0), b1 = (1, 0), and transform is orthogonal +out vec2 uv_coords; +out vec2 uv_b2; +out float bezier_degree; + +// To my knowledge, there is no notion of #include for shaders, +// so to share functionality between this and others, the caller +// in manim replaces this line with the contents of named file +#INSERT quadratic_bezier_geometry_functions.glsl +#INSERT get_gl_Position.glsl +#INSERT get_unit_normal.glsl +#INSERT add_light.glsl + + +void emit_vertex_wrapper(vec3 point, int index){ + color = add_light( + v_color[index], + point, + v_global_unit_normal[index], + light_source_position, + gloss, + shadow + ); + xyz_coords = point; + gl_Position = get_gl_Position(xyz_coords); + EmitVertex(); +} + + +void emit_simple_triangle(){ + for(int i = 0; i < 3; i++){ + emit_vertex_wrapper(bp[i], i); + } + EndPrimitive(); +} + + +void emit_pentagon(vec3[3] points, vec3 normal){ + vec3 p0 = points[0]; + vec3 p1 = points[1]; + vec3 p2 = points[2]; + // Tangent vectors + vec3 t01 = normalize(p1 - p0); + vec3 t12 = normalize(p2 - p1); + // Vectors perpendicular to the curve in the plane of the curve pointing outside the curve + vec3 p0_perp = cross(t01, normal); + vec3 p2_perp = cross(t12, normal); + + bool fill_in = orientation > 0; + float aaw = anti_alias_width; + vec3 corners[5]; + if(fill_in){ + // Note, straight lines will also fall into this case, and since p0_perp and p2_perp + // will point to the right of the curve, it's just what we want + corners = vec3[5]( + p0 + aaw * p0_perp, + p0, + p1 + 0.5 * aaw * (p0_perp + p2_perp), + p2, + p2 + aaw * p2_perp + ); + }else{ + corners = vec3[5]( + p0, + p0 - aaw * p0_perp, + p1, + p2 - aaw * p2_perp, + p2 + ); + } + + mat4 xyz_to_uv = get_xyz_to_uv(p0, p1, normal); + uv_b2 = (xyz_to_uv * vec4(p2, 1)).xy; + uv_anti_alias_width = anti_alias_width / length(p1 - p0); + + for(int i = 0; i < 5; i++){ + vec3 corner = corners[i]; + uv_coords = (xyz_to_uv * vec4(corner, 1)).xy; + int j = int(sign(i - 1) + 1); // Maps i = [0, 1, 2, 3, 4] onto j = [0, 0, 1, 2, 2] + emit_vertex_wrapper(corner, j); + } + EndPrimitive(); +} + + +void main(){ + // If vert indices are sequential, don't fill all + fill_all = float( + (v_vert_index[1] - v_vert_index[0]) != 1.0 || + (v_vert_index[2] - v_vert_index[1]) != 1.0 + ); + + if(fill_all == 1.0){ + emit_simple_triangle(); + return; + } + + vec3 local_unit_normal = get_unit_normal(vec3[3](bp[0], bp[1], bp[2])); + orientation = sign(dot(v_global_unit_normal[0], local_unit_normal)); + + vec3 new_bp[3]; + bezier_degree = get_reduced_control_points(vec3[3](bp[0], bp[1], bp[2]), new_bp); + if(bezier_degree >= 1){ + emit_pentagon(new_bp, local_unit_normal); + } + // Don't emit any vertices for bezier_degree 0 +} + diff --git a/manimlib/shaders/quadratic_bezier_fill_vert.glsl b/manimlib/shaders/quadratic_bezier_fill_vert.glsl new file mode 100644 index 0000000000..dc426d63d8 --- /dev/null +++ b/manimlib/shaders/quadratic_bezier_fill_vert.glsl @@ -0,0 +1,28 @@ +#version 330 + +uniform mat4 to_screen_space; +uniform float is_fixed_in_frame; + +in vec3 point; +in vec3 unit_normal; +in vec4 color; +in float vert_index; + +out vec3 bp; // Bezier control point +out vec3 v_global_unit_normal; +out vec4 v_color; +// out float v_fill_all; +out float v_vert_index; + +// To my knowledge, there is no notion of #include for shaders, +// so to share functionality between this and others, the caller +// replaces this line with the contents of named file +#INSERT position_point_into_frame.glsl + +void main(){ + bp = position_point_into_frame(point); + v_global_unit_normal = normalize(to_screen_space * vec4(unit_normal, 0)).xyz; + v_color = color; + // v_fill_all = fill_all; + v_vert_index = vert_index; +} \ No newline at end of file diff --git a/manimlib/shaders/quadratic_bezier_geometry_functions.glsl b/manimlib/shaders/quadratic_bezier_geometry_functions.glsl new file mode 100644 index 0000000000..25ab9bd4c1 --- /dev/null +++ b/manimlib/shaders/quadratic_bezier_geometry_functions.glsl @@ -0,0 +1,75 @@ +// This file is not a shader, it's just a set of +// functions meant to be inserted into other shaders. + +float cross2d(vec2 v, vec2 w){ + return v.x * w.y - w.x * v.y; +} + +// Orthogonal matrix to convert to a uv space defined so that +// b0 goes to [0, 0] and b1 goes to [1, 0] +mat4 get_xyz_to_uv(vec3 b0, vec3 b1, vec3 unit_normal){ + mat4 shift = mat4( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + -b0.x, -b0.y, -b0.z, 1 + ); + + float scale_factor = length(b1 - b0); + vec3 I = (b1 - b0) / scale_factor; + vec3 K = unit_normal; + vec3 J = cross(K, I); + // Transpose (hence inverse) of matrix taking + // i-hat to I, k-hat to unit_normal, and j-hat to their cross + mat4 rotate = mat4( + I.x, J.x, K.x, 0, + I.y, J.y, K.y, 0, + I.z, J.z, K.z, 0, + 0, 0, 0 , 1 + ); + return (1 / scale_factor) * rotate * shift; +} + + +// Returns 0 for null curve, 1 for linear, 2 for quadratic. +// Populates new_points with bezier control points for the curve, +// which for quadratics will be the same, but for linear and null +// might change. The idea is to inform the caller of the degree, +// while also passing tangency information in the linear case. +// float get_reduced_control_points(vec3 b0, vec3 b1, vec3 b2, out vec3 new_points[3]){ +float get_reduced_control_points(in vec3 points[3], out vec3 new_points[3]){ + float length_threshold = 1e-6; + float angle_threshold = 1e-3; + + vec3 p0 = points[0]; + vec3 p1 = points[1]; + vec3 p2 = points[2]; + vec3 v01 = (p1 - p0); + vec3 v12 = (p2 - p1); + + float dot_prod = clamp(dot(normalize(v01), normalize(v12)), -1, 1); + bool aligned = acos(dot_prod) < angle_threshold; + bool distinct_01 = length(v01) > length_threshold; // v01 is considered nonzero + bool distinct_12 = length(v12) > length_threshold; // v12 is considered nonzero + int n_uniques = int(distinct_01) + int(distinct_12); + + bool quadratic = (n_uniques == 2) && !aligned; + bool linear = (n_uniques == 1) || ((n_uniques == 2) && aligned); + bool constant = (n_uniques == 0); + if(quadratic){ + new_points[0] = p0; + new_points[1] = p1; + new_points[2] = p2; + return 2.0; + }else if(linear){ + new_points[0] = p0; + new_points[1] = (p0 + p2) / 2.0; + new_points[2] = p2; + return 1.0; + }else{ + new_points[0] = p0; + new_points[1] = p0; + new_points[2] = p0; + return 0.0; + } +} \ No newline at end of file diff --git a/manimlib/shaders/quadratic_bezier_stroke_frag.glsl b/manimlib/shaders/quadratic_bezier_stroke_frag.glsl new file mode 100644 index 0000000000..92f66cb45c --- /dev/null +++ b/manimlib/shaders/quadratic_bezier_stroke_frag.glsl @@ -0,0 +1,97 @@ +#version 330 + +uniform mat4 to_screen_space; +uniform float is_fixed_in_frame; +uniform vec3 light_source_position; + +in vec2 uv_coords; +in vec2 uv_b2; + +in float uv_stroke_width; +in vec4 color; +in float uv_anti_alias_width; + +in float has_prev; +in float has_next; +in float bevel_start; +in float bevel_end; +in float angle_from_prev; +in float angle_to_next; + +in float bezier_degree; + +out vec4 frag_color; + + +float cross2d(vec2 v, vec2 w){ + return v.x * w.y - w.x * v.y; +} + + +float modify_distance_for_endpoints(vec2 p, float dist, float t){ + float buff = 0.5 * uv_stroke_width - uv_anti_alias_width; + // Check the beginning of the curve + if(t == 0){ + // Clip the start + if(has_prev == 0) return max(dist, -p.x + buff); + // Bevel start + if(bevel_start == 1){ + float a = angle_from_prev; + mat2 rot = mat2( + cos(a), sin(a), + -sin(a), cos(a) + ); + // Dist for intersection of two lines + float bevel_d = max(abs(p.y), abs((rot * p).y)); + // Dist for union of this intersection with the real curve + return min(dist, bevel_d); + } + // Otherwise, start will be rounded off + }else if(t == 1){ + // Check the end of the curve + // TODO, too much code repetition + vec2 v21 = (bezier_degree == 2) ? vec2(1, 0) - uv_b2 : vec2(-1, 0); + float len_v21 = length(v21); + if(len_v21 == 0){ + v21 = -uv_b2; + len_v21 = length(v21); + } + + float perp_dist = dot(p - uv_b2, v21) / len_v21; + if(has_next == 0) return max(dist, -perp_dist + buff); + // Bevel end + if(bevel_end == 1){ + float a = -angle_to_next; + mat2 rot = mat2( + cos(a), sin(a), + -sin(a), cos(a) + ); + vec2 v21_unit = v21 / length(v21); + float bevel_d = max( + abs(cross2d(p - uv_b2, v21_unit)), + abs(cross2d((rot * (p - uv_b2)), v21_unit)) + ); + return min(dist, bevel_d); + } + // Otherwise, end will be rounded off + } + return dist; +} + +// To my knowledge, there is no notion of #include for shaders, +// so to share functionality between this and others, the caller +// replaces this line with the contents of named file +#INSERT quadratic_bezier_distance.glsl + + +void main() { + if (uv_stroke_width == 0) discard; + float dist_to_curve = min_dist_to_curve(uv_coords, uv_b2, bezier_degree); + // An sdf for the region around the curve we wish to color. + float signed_dist = abs(dist_to_curve) - 0.5 * uv_stroke_width; + + frag_color = color; + frag_color.a *= smoothstep(0.5, -0.5, signed_dist / uv_anti_alias_width); + + // frag_color.a += 0.3; +} \ No newline at end of file diff --git a/manimlib/shaders/quadratic_bezier_stroke_geom.glsl b/manimlib/shaders/quadratic_bezier_stroke_geom.glsl new file mode 100644 index 0000000000..adcc6f1f69 --- /dev/null +++ b/manimlib/shaders/quadratic_bezier_stroke_geom.glsl @@ -0,0 +1,266 @@ +#version 330 + +layout (triangles) in; +layout (triangle_strip, max_vertices = 5) out; + +// Needed for get_gl_Position +uniform vec2 frame_shape; +uniform float focal_distance; +uniform float is_fixed_in_frame; +uniform float anti_alias_width; +uniform vec3 light_source_position; +uniform float joint_type; +uniform float gloss; +uniform float shadow; + +in vec3 bp[3]; +in vec3 prev_bp[3]; +in vec3 next_bp[3]; +in vec3 v_global_unit_normal[3]; + +in vec4 v_color[3]; +in float v_stroke_width[3]; + +out vec4 color; +out float uv_stroke_width; +out float uv_anti_alias_width; + +out float has_prev; +out float has_next; +out float bevel_start; +out float bevel_end; +out float angle_from_prev; +out float angle_to_next; + +out float bezier_degree; + +out vec2 uv_coords; +out vec2 uv_b2; + +// Codes for joint types +const float AUTO_JOINT = 0; +const float ROUND_JOINT = 1; +const float BEVEL_JOINT = 2; +const float MITER_JOINT = 3; + + +// To my knowledge, there is no notion of #include for shaders, +// so to share functionality between this and others, the caller +// replaces this line with the contents of named file +#INSERT quadratic_bezier_geometry_functions.glsl +#INSERT get_gl_Position.glsl +#INSERT get_unit_normal.glsl +#INSERT add_light.glsl + + +void flatten_points(in vec3[3] points, out vec3[3] flat_points){ + for(int i = 0; i < 3; i++){ + flat_points[i] = points[i]; + flat_points[i].z = 0; + } +} + + +float angle_between_vectors(vec3 v1, vec3 v2, vec3 normal){ + float v1_norm = length(v1); + float v2_norm = length(v2); + if(v1_norm == 0 || v2_norm == 0) return 0; + vec3 nv1 = v1 / v1_norm; + vec3 nv2 = v2 / v2_norm; + // float signed_area = clamp(dot(cross(nv1, nv2), normal), -1, 1); + // return asin(signed_area); + float unsigned_angle = acos(clamp(dot(nv1, nv2), -1, 1)); + float sn = sign(dot(cross(nv1, nv2), normal)); + return sn * unsigned_angle; +} + + +bool find_intersection(vec3 p0, vec3 v0, vec3 p1, vec3 v1, vec3 normal, out vec3 intersection){ + // Find the intersection of a line passing through + // p0 in the direction v0 and one passing through p1 in + // the direction p1. + // That is, find a solutoin to p0 + v0 * t = p1 + v1 * s + // float det = -v0.x * v1.y + v1.x * v0.y; + float det = dot(cross(v1, v0), normal); + if(det == 0){ + // intersection = p0; + return false; + } + float t = dot(cross(p0 - p1, v1), normal) / det; + intersection = p0 + v0 * t; + return true; +} + + +void create_joint(float angle, vec3 unit_tan, float buff, float should_bevel, + vec3 static_c0, out vec3 changing_c0, + vec3 static_c1, out vec3 changing_c1){ + float shift; + bool miter = ( + (joint_type == AUTO_JOINT && abs(angle) > 2.8 && should_bevel == 1) || + (joint_type == MITER_JOINT) + ); + if(abs(angle) < 1e-3){ + // No joint + shift = 0; + }else if(miter){ + shift = buff * (-1.0 - cos(angle)) / sin(angle); + }else{ + // For a Bevel joint + shift = buff * (1.0 - cos(angle)) / sin(angle); + } + changing_c0 = static_c0 - shift * unit_tan; + changing_c1 = static_c1 + shift * unit_tan; +} + + +// This function is responsible for finding the corners of +// a bounding region around the bezier curve, which can be +// emitted as a triangle fan +int get_corners(vec3 controls[3], vec3 normal, int degree, out vec3 corners[5]){ + vec3 p0 = controls[0]; + vec3 p1 = controls[1]; + vec3 p2 = controls[2]; + + // Unit vectors for directions between control points + vec3 v10 = normalize(p0 - p1); + vec3 v12 = normalize(p2 - p1); + vec3 v01 = -v10; + vec3 v21 = -v12; + + vec3 p0_perp = cross(normal, v01); // Pointing to the left of the curve from p0 + vec3 p2_perp = cross(normal, v12); // Pointing to the left of the curve from p2 + + // aaw is the added width given around the polygon for antialiasing. + // In case the normal is faced away from (0, 0, 1), the vector to the + // camera, this is scaled up. + float aaw = anti_alias_width; + float buff0 = 0.5 * v_stroke_width[0] + aaw; + float buff2 = 0.5 * v_stroke_width[2] + aaw; + float aaw0 = (1 - has_prev) * aaw; + float aaw2 = (1 - has_next) * aaw; + + vec3 c0 = p0 - buff0 * p0_perp + aaw0 * v10; + vec3 c1 = p0 + buff0 * p0_perp + aaw0 * v10; + vec3 c2 = p2 + buff2 * p2_perp + aaw2 * v12; + vec3 c3 = p2 - buff2 * p2_perp + aaw2 * v12; + + // Account for previous and next control points + if(has_prev > 0) create_joint(angle_from_prev, v01, buff0, bevel_start, c0, c0, c1, c1); + if(has_next > 0) create_joint(angle_to_next, v21, buff2, bevel_end, c3, c3, c2, c2); + + // Linear case is the simplest + if(degree == 1){ + // The order of corners should be for a triangle_strip. Last entry is a dummy + corners = vec3[5](c0, c1, c3, c2, vec3(0.0)); + return 4; + } + // Otherwise, form a pentagon around the curve + float orientation = sign(dot(cross(v01, v12), normal)); // Positive for ccw curves + if(orientation > 0) corners = vec3[5](c0, c1, p1, c2, c3); + else corners = vec3[5](c1, c0, p1, c3, c2); + // Replace corner[2] with convex hull point accounting for stroke width + find_intersection(corners[0], v01, corners[4], v21, normal, corners[2]); + return 5; +} + + +void set_adjascent_info(vec3 c0, vec3 tangent, + int degree, + vec3 normal, + vec3 adj[3], + out float bevel, + out float angle + ){ + vec3 new_adj[3]; + float adj_degree = get_reduced_control_points(adj, new_adj); + // Check if adj_degree is zero? + angle = angle_between_vectors(c0 - new_adj[1], tangent, normal); + // Decide on joint type + bool one_linear = (degree == 1 || adj_degree == 1.0); + bool should_bevel = ( + (joint_type == AUTO_JOINT && one_linear) || + joint_type == BEVEL_JOINT + ); + bevel = should_bevel ? 1.0 : 0.0; +} + + +void find_joint_info(vec3 controls[3], vec3 prev[3], vec3 next[3], int degree, vec3 normal){ + float tol = 1e-8; + + // Made as floats not bools so they can be passed to the frag shader + has_prev = float(distance(prev[2], controls[0]) < tol); + has_next = float(distance(next[0], controls[2]) < tol); + + if(bool(has_prev)){ + vec3 tangent = controls[1] - controls[0]; + set_adjascent_info( + controls[0], tangent, degree, normal, prev, + bevel_start, angle_from_prev + ); + } + if(bool(has_next)){ + vec3 tangent = controls[1] - controls[2]; + set_adjascent_info( + controls[2], tangent, degree, normal, next, + bevel_end, angle_to_next + ); + angle_to_next *= -1; + } +} + + +void main() { + vec3 controls[3]; + bezier_degree = get_reduced_control_points(vec3[3](bp[0], bp[1], bp[2]), controls); + int degree = int(bezier_degree); + + // Control points are projected to the xy plane before drawing, which in turn + // gets tranlated to a uv plane. The z-coordinate information will be remembered + // by what's sent out to gl_Position, and by how it affects the lighting and stroke width + vec3 flat_controls[3]; + vec3 flat_prev[3]; + vec3 flat_next[3]; + flatten_points(controls, flat_controls); + flatten_points(vec3[3](prev_bp[0], prev_bp[1], prev_bp[2]), flat_prev); + flatten_points(vec3[3](next_bp[0], next_bp[1], next_bp[2]), flat_next); + vec3 k_hat = vec3(0.0, 0.0, 1.0); + + // Null curve + if(degree == 0) return; + + find_joint_info(flat_controls, flat_prev, flat_next, degree, k_hat); + + // Find uv conversion matrix + mat4 xyz_to_uv = get_xyz_to_uv(flat_controls[0], flat_controls[1], k_hat); + float scale_factor = length(flat_controls[1] - flat_controls[0]); + uv_anti_alias_width = anti_alias_width / scale_factor; + uv_b2 = (xyz_to_uv * vec4(controls[2].xy, 0.0, 1.0)).xy; + + // Corners of a bounding region around curve + vec3 corners[5]; + int n_corners = get_corners(flat_controls, k_hat, degree, corners); + + int index_map[5] = int[5](0, 0, 1, 2, 2); + if(n_corners == 4) index_map[2] = 2; + + // Emit each corner + for(int i = 0; i < n_corners; i++){ + uv_coords = (xyz_to_uv * vec4(corners[i], 1.0)).xy; + uv_stroke_width = v_stroke_width[index_map[i]] / scale_factor; + // Apply some lighting to the color before sending out. + vec3 xyz_coords = vec3(corners[i].xy, controls[index_map[i]].z); + color = add_light( + v_color[index_map[i]], + xyz_coords, + v_global_unit_normal[index_map[i]], + light_source_position, + gloss, + shadow + ); + gl_Position = get_gl_Position(xyz_coords); + EmitVertex(); + } + EndPrimitive(); +} \ No newline at end of file diff --git a/manimlib/shaders/quadratic_bezier_stroke_vert.glsl b/manimlib/shaders/quadratic_bezier_stroke_vert.glsl new file mode 100644 index 0000000000..4b16c962bb --- /dev/null +++ b/manimlib/shaders/quadratic_bezier_stroke_vert.glsl @@ -0,0 +1,39 @@ +#version 330 + +uniform mat4 to_screen_space; +uniform float is_fixed_in_frame; +uniform float focal_distance; + +in vec3 point; +in vec3 prev_point; +in vec3 next_point; +in vec3 unit_normal; + +in float stroke_width; +in vec4 color; + +// Bezier control point +out vec3 bp; +out vec3 prev_bp; +out vec3 next_bp; +out vec3 v_global_unit_normal; + +out float v_stroke_width; +out vec4 v_color; + +const float STROKE_WIDTH_CONVERSION = 0.01; + +// To my knowledge, there is no notion of #include for shaders, +// so to share functionality between this and others, the caller +// replaces this line with the contents of named file +#INSERT position_point_into_frame.glsl + +void main(){ + bp = position_point_into_frame(point); + prev_bp = position_point_into_frame(prev_point); + next_bp = position_point_into_frame(next_point); + v_global_unit_normal = normalize(to_screen_space * vec4(unit_normal, 0)).xyz; + + v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width; + v_color = color; +} \ No newline at end of file diff --git a/manimlib/shaders/scale_and_shift_point_for_frame.glsl b/manimlib/shaders/scale_and_shift_point_for_frame.glsl new file mode 100644 index 0000000000..616ca622ba --- /dev/null +++ b/manimlib/shaders/scale_and_shift_point_for_frame.glsl @@ -0,0 +1,8 @@ +// Assumes the following uniforms exist in the surrounding context: +// uniform vec2 frame_shape; +// TODO, rename + +vec3 get_gl_Position(vec3 point){ + point.x /= aspect_ratio; + return point; +} \ No newline at end of file diff --git a/manimlib/shaders/simple_vert.glsl b/manimlib/shaders/simple_vert.glsl new file mode 100644 index 0000000000..541cd01dea --- /dev/null +++ b/manimlib/shaders/simple_vert.glsl @@ -0,0 +1,17 @@ +#version 330 + +uniform vec2 frame_shape; +uniform float anti_alias_width; +uniform mat4 to_screen_space; +uniform float is_fixed_in_frame; +uniform float focal_distance; + +in vec3 point; + +// Analog of import for manim only +#INSERT get_gl_Position.glsl +#INSERT position_point_into_frame.glsl + +void main(){ + gl_Position = get_gl_Position(position_point_into_frame(point)); +} \ No newline at end of file diff --git a/manimlib/shaders/surface_frag.glsl b/manimlib/shaders/surface_frag.glsl new file mode 100644 index 0000000000..385c0662b1 --- /dev/null +++ b/manimlib/shaders/surface_frag.glsl @@ -0,0 +1,27 @@ +#version 330 + +// in vec4 v_color; +// out vec4 frag_color; + +uniform vec3 light_source_position; +uniform float gloss; +uniform float shadow; + +in vec3 xyz_coords; +in vec3 v_normal; +in vec4 v_color; + +out vec4 frag_color; + +#INSERT add_light.glsl + +void main() { + frag_color = add_light( + v_color, + xyz_coords, + normalize(v_normal), + light_source_position, + gloss, + shadow + ); +} \ No newline at end of file diff --git a/manimlib/shaders/surface_vert.glsl b/manimlib/shaders/surface_vert.glsl new file mode 100644 index 0000000000..090a2f2d8f --- /dev/null +++ b/manimlib/shaders/surface_vert.glsl @@ -0,0 +1,28 @@ +#version 330 + +uniform vec2 frame_shape; +uniform mat4 to_screen_space; +uniform float is_fixed_in_frame; +uniform float focal_distance; +uniform vec3 light_source_position; + +in vec3 point; +in vec3 du_point; +in vec3 dv_point; +in vec4 color; + +out vec3 xyz_coords; +out vec3 v_normal; +out vec4 v_color; + +// These lines will get replaced +#INSERT position_point_into_frame.glsl +#INSERT get_gl_Position.glsl +#INSERT get_rotated_surface_unit_normal_vector.glsl + +void main(){ + xyz_coords = position_point_into_frame(point); + v_normal = get_rotated_surface_unit_normal_vector(point, du_point, dv_point); + v_color = color; + gl_Position = get_gl_Position(xyz_coords); +} \ No newline at end of file diff --git a/manimlib/shaders/textured_surface_frag.glsl b/manimlib/shaders/textured_surface_frag.glsl new file mode 100644 index 0000000000..93345c1305 --- /dev/null +++ b/manimlib/shaders/textured_surface_frag.glsl @@ -0,0 +1,42 @@ +#version 330 + +uniform sampler2D LightTexture; +uniform sampler2D DarkTexture; +uniform float num_textures; +uniform vec3 light_source_position; +uniform float gloss; +uniform float shadow; + +in vec3 xyz_coords; +in vec3 v_normal; +in vec2 v_im_coords; +in float v_opacity; + +out vec4 frag_color; + +#INSERT add_light.glsl + +const float dark_shift = 0.2; + +void main() { + vec4 color = texture(LightTexture, v_im_coords); + if(num_textures == 2.0){ + vec4 dark_color = texture(DarkTexture, v_im_coords); + float dp = dot( + normalize(light_source_position - xyz_coords), + normalize(v_normal) + ); + float alpha = smoothstep(-dark_shift, dark_shift, dp); + color = mix(dark_color, color, alpha); + } + + frag_color = add_light( + color, + xyz_coords, + normalize(v_normal), + light_source_position, + gloss, + shadow + ); + frag_color.a = v_opacity; +} \ No newline at end of file diff --git a/manimlib/shaders/textured_surface_vert.glsl b/manimlib/shaders/textured_surface_vert.glsl new file mode 100644 index 0000000000..0c1fcdf4e8 --- /dev/null +++ b/manimlib/shaders/textured_surface_vert.glsl @@ -0,0 +1,31 @@ +#version 330 + +uniform vec2 frame_shape; +uniform mat4 to_screen_space; +uniform float is_fixed_in_frame; +uniform float focal_distance; +uniform vec3 light_source_position; + +in vec3 point; +in vec3 du_point; +in vec3 dv_point; +in vec2 im_coords; +in float opacity; + +out vec3 xyz_coords; +out vec3 v_normal; +out vec2 v_im_coords; +out float v_opacity; + +// These lines will get replaced +#INSERT position_point_into_frame.glsl +#INSERT get_gl_Position.glsl +#INSERT get_rotated_surface_unit_normal_vector.glsl + +void main(){ + xyz_coords = position_point_into_frame(point); + v_normal = get_rotated_surface_unit_normal_vector(point, du_point, dv_point); + v_im_coords = im_coords; + v_opacity = opacity; + gl_Position = get_gl_Position(xyz_coords); +} \ No newline at end of file diff --git a/manimlib/stream_starter.py b/manimlib/stream_starter.py index 8a77f4ea43..4a3c5a9b7c 100644 --- a/manimlib/stream_starter.py +++ b/manimlib/stream_starter.py @@ -1,7 +1,6 @@ from time import sleep import code import os -import readline import subprocess from manimlib.scene.scene import Scene @@ -10,7 +9,6 @@ def start_livestream(to_twitch=False, twitch_key=None): class Manim(): - def __new__(cls): kwargs = { "scene_name": manimlib.constants.LIVE_STREAM_NAME, diff --git a/manimlib/tex_template.tex b/manimlib/tex_template.tex index 3b7cea4d46..b017bc7cbd 100644 --- a/manimlib/tex_template.tex +++ b/manimlib/tex_template.tex @@ -17,6 +17,7 @@ \usepackage{physics} \usepackage{xcolor} \usepackage{microtype} +\usepackage{pifont} \DisableLigatures{encoding = *, family = * } %\usepackage[UTF8]{ctex} \linespread{1} diff --git a/manimlib/utils/bezier.py b/manimlib/utils/bezier.py index 3e86562ed3..03f6f6682a 100644 --- a/manimlib/utils/bezier.py +++ b/manimlib/utils/bezier.py @@ -2,23 +2,29 @@ import numpy as np from manimlib.utils.simple_functions import choose +from manimlib.utils.space_ops import find_intersection +from manimlib.utils.space_ops import cross2d CLOSED_THRESHOLD = 0.001 def bezier(points): n = len(points) - 1 - return lambda t: sum([ - ((1 - t)**(n - k)) * (t**k) * choose(n, k) * point - for k, point in enumerate(points) - ]) + + def result(t): + return sum([ + ((1 - t)**(n - k)) * (t**k) * choose(n, k) * point + for k, point in enumerate(points) + ]) + + return result def partial_bezier_points(points, a, b): """ - Given an array of points which define + Given an list of points which define a bezier curve, and two numbers 0<=a 0 else points[0] + h2 = curve(b) if b < 1 else points[2] + h1_prime = (1 - a) * points[1] + a * points[2] + end_prop = (b - a) / (1. - a) + h1 = (1 - end_prop) * h0 + end_prop * h1_prime + return [h0, h1, h2] # Linear interpolation variants @@ -44,6 +67,11 @@ def interpolate(start, end, alpha): return (1 - alpha) * start + alpha * end +def set_array_by_interpolation(arr, arr1, arr2, alpha): + arr[:] = interpolate(arr1, arr2, alpha) + return arr + + def integer_interpolate(start, end, alpha): """ alpha is a float between 0 and 1. This returns @@ -81,9 +109,32 @@ def match_interpolate(new_start, new_end, old_start, old_end, old_value): # Figuring out which bezier curves most smoothly connect a sequence of points +def get_smooth_quadratic_bezier_handle_points(points): + n = len(points) + # Top matrix sets the constraint h_i + h_{i + 1} = 2 * P_i + top_mat = np.zeros((n - 2, n - 1)) + np.fill_diagonal(top_mat, 1) + np.fill_diagonal(top_mat[:, 1:], 1) + + # Lower matrix sets the constraint that 2(h1 - h0)= p2 - p0 and 2(h_{n-1}- h_{n-2}) = p_n - p_{n-2} + low_mat = np.zeros((2, n - 1)) + low_mat[0, :2] = [-2, 2] + low_mat[1, -2:] = [-2, 2] + + # Use the pseudoinverse to find a near solution to these constraints + full_mat = np.vstack([top_mat, low_mat]) + full_mat_pinv = np.linalg.pinv(full_mat) + + rhs = np.vstack([ + 2 * points[1:-1], + [points[2] - points[0]], + [points[-1] - points[-3]], + ]) + return np.dot(full_mat_pinv, rhs) -def get_smooth_handle_points(points): + +def get_smooth_cubic_bezier_handle_points(points): points = np.array(points) num_handles = len(points) - 1 dim = points.shape[1] @@ -116,6 +167,7 @@ def get_smooth_handle_points(points): def solve_func(b): return linalg.solve_banded((l, u), diag, b) + use_closed_solve_function = is_closed(points) if use_closed_solve_function: # Get equations to relate first and last points @@ -159,3 +211,81 @@ def diag_to_matrix(l_and_u, diag): def is_closed(points): return np.allclose(points[0], points[-1]) + + +# Given 4 control points for a cubic bezier curve (or arrays of such) +# return control points for 2 quadratics (or 2n quadratics) approximating them. +def get_quadratic_approximation_of_cubic(a0, h0, h1, a1): + a0 = np.array(a0, ndmin=2) + h0 = np.array(h0, ndmin=2) + h1 = np.array(h1, ndmin=2) + a1 = np.array(a1, ndmin=2) + # Tangent vectors at the start and end. + T0 = h0 - a0 + T1 = a1 - h1 + + # Search for inflection points. If none are found, use the + # midpoint as a cut point. + # Based on http://www.caffeineowl.com/graphics/2d/vectorial/cubic-inflexion.html + has_infl = np.ones(len(a0), dtype=bool) + + p = h0 - a0 + q = h1 - 2 * h0 + a0 + r = a1 - 3 * h1 + 3 * h0 - a0 + + a = cross2d(q, r) + b = cross2d(p, r) + c = cross2d(p, q) + + disc = b * b - 4 * a * c + has_infl &= (disc > 0) + sqrt_disc = np.sqrt(np.abs(disc)) + settings = np.seterr(all='ignore') + ti_bounds = [] + for sgn in [-1, +1]: + ti = (-b + sgn * sqrt_disc) / (2 * a) + ti[a == 0] = (-c / b)[a == 0] + ti[(a == 0) & (b == 0)] = 0 + ti_bounds.append(ti) + ti_min, ti_max = ti_bounds + np.seterr(**settings) + ti_min_in_range = has_infl & (0 < ti_min) & (ti_min < 1) + ti_max_in_range = has_infl & (0 < ti_max) & (ti_max < 1) + + # Choose a value of t which is starts as 0.5, + # but is updated to one of the inflection points + # if they lie between 0 and 1 + + t_mid = 0.5 * np.ones(len(a0)) + t_mid[ti_min_in_range] = ti_min[ti_min_in_range] + t_mid[ti_max_in_range] = ti_max[ti_max_in_range] + + m, n = a0.shape + t_mid = t_mid.repeat(n).reshape((m, n)) + + # Compute bezier point and tangent at the chosen value of t + mid = bezier([a0, h0, h1, a1])(t_mid) + Tm = bezier([h0 - a0, h1 - h0, a1 - h1])(t_mid) + + # Intersection between tangent lines at end points + # and tangent in the middle + i0 = find_intersection(a0, T0, mid, Tm) + i1 = find_intersection(a1, T1, mid, Tm) + + m, n = np.shape(a0) + result = np.zeros((6 * m, n)) + result[0::6] = a0 + result[1::6] = i0 + result[2::6] = mid + result[3::6] = mid + result[4::6] = i1 + result[5::6] = a1 + return result + + +def get_smooth_quadratic_bezier_path_through(points): + # TODO + h0, h1 = get_smooth_cubic_bezier_handle_points(points) + a0 = points[:-1] + a1 = points[1:] + return get_quadratic_approximation_of_cubic(a0, h0, h1, a1) diff --git a/manimlib/utils/color.py b/manimlib/utils/color.py index 1a5a229b71..91d9714212 100644 --- a/manimlib/utils/color.py +++ b/manimlib/utils/color.py @@ -26,7 +26,7 @@ def color_to_rgba(color, alpha=1): def rgb_to_color(rgb): try: return Color(rgb=rgb) - except: + except ValueError: return Color(WHITE) @@ -35,13 +35,13 @@ def rgba_to_color(rgba): def rgb_to_hex(rgb): - return "#" + "".join('%02x' % int(255 * x) for x in rgb) + return "#" + "".join(hex(int(255 * x))[2:] for x in rgb) def hex_to_rgb(hex_code): hex_part = hex_code[1:] if len(hex_part) == 3: - "".join([2 * c for c in hex_part]) + hex_part = "".join([2 * c for c in hex_part]) return np.array([ int(hex_part[i:i + 2], 16) / 255 for i in range(0, 6, 2) @@ -58,7 +58,7 @@ def color_to_int_rgb(color): def color_to_int_rgba(color, opacity=1.0): alpha = int(255 * opacity) - return np.append(color_to_int_rgb(color), alpha) + return np.array([*color_to_int_rgb(color), alpha]) def color_gradient(reference_colors, length_of_output): @@ -84,8 +84,7 @@ def interpolate_color(color1, color2, alpha): def average_color(*colors): rgbs = np.array(list(map(color_to_rgb, colors))) - mean_rgb = np.apply_along_axis(np.mean, 0, rgbs) - return rgb_to_color(mean_rgb) + return rgb_to_color(rgbs.mean(0)) def random_bright_color(): diff --git a/manimlib/utils/debug.py b/manimlib/utils/debug.py index 28f8f1ad77..87ef886c6d 100644 --- a/manimlib/utils/debug.py +++ b/manimlib/utils/debug.py @@ -1,3 +1,5 @@ +import time + from manimlib.constants import BLACK from manimlib.mobject.numbers import Integer from manimlib.mobject.types.vectorized_mobject import VGroup @@ -10,7 +12,7 @@ def print_family(mobject, n_tabs=0): print_family(submob, n_tabs + 1) -def get_submobject_index_labels(mobject, label_height=0.15): +def index_labels(mobject, label_height=0.15): labels = VGroup() for n, submob in enumerate(mobject): label = Integer(n) @@ -19,3 +21,9 @@ def get_submobject_index_labels(mobject, label_height=0.15): label.set_stroke(BLACK, 5, background=True) labels.add(label) return labels + + +def get_runtime(func): + now = time.time() + func() + return time.time() - now diff --git a/manimlib/utils/family_ops.py b/manimlib/utils/family_ops.py new file mode 100644 index 0000000000..e6035d3076 --- /dev/null +++ b/manimlib/utils/family_ops.py @@ -0,0 +1,37 @@ +import itertools as it + + +def extract_mobject_family_members(mobject_list, only_those_with_points=False): + result = list(it.chain(*[ + mob.get_family() + for mob in mobject_list + ])) + if only_those_with_points: + result = [mob for mob in result if mob.has_points()] + return result + + +def restructure_list_to_exclude_certain_family_members(mobject_list, to_remove): + """ + Removes anything in to_remove from mobject_list, but in the event that one of + the items to be removed is a member of the family of an item in mobject_list, + the other family members are added back into the list. + + This is useful in cases where a scene contains a group, e.g. Group(m1, m2, m3), + but one of its submobjects is removed, e.g. scene.remove(m1), it's useful + for the list of mobject_list to be edited to contain other submobjects, but not m1. + """ + new_list = [] + to_remove = extract_mobject_family_members(to_remove) + + def add_safe_mobjects_from_list(list_to_examine, set_to_remove): + for mob in list_to_examine: + if mob in set_to_remove: + continue + intersect = set_to_remove.intersection(mob.get_family()) + if intersect: + add_safe_mobjects_from_list(mob.submobjects, intersect) + else: + new_list.append(mob) + add_safe_mobjects_from_list(mobject_list, set(to_remove)) + return new_list diff --git a/manimlib/utils/iterables.py b/manimlib/utils/iterables.py index 1cd26ed9ea..8e43cec745 100644 --- a/manimlib/utils/iterables.py +++ b/manimlib/utils/iterables.py @@ -53,47 +53,54 @@ def batch_by_property(items, property_func): preserved) """ batch_prop_pairs = [] - - def add_batch_prop_pair(batch): - if len(batch) > 0: - batch_prop_pairs.append( - (batch, property_func(batch[0])) - ) curr_batch = [] curr_prop = None for item in items: prop = property_func(item) if prop != curr_prop: - add_batch_prop_pair(curr_batch) + # Add current batch + if len(curr_batch) > 0: + batch_prop_pairs.append((curr_batch, curr_prop)) + # Redefine curr curr_prop = prop curr_batch = [item] else: curr_batch.append(item) - add_batch_prop_pair(curr_batch) + if len(curr_batch) > 0: + batch_prop_pairs.append((curr_batch, curr_prop)) return batch_prop_pairs -def tuplify(obj): +def listify(obj): if isinstance(obj, str): - return (obj,) + return [obj] try: - return tuple(obj) + return list(obj) except TypeError: - return (obj,) + return [obj] def stretch_array_to_length(nparray, length): curr_len = len(nparray) if curr_len > length: - raise Warning( - "Trying to stretch array to a length shorter than its own") - indices = np.arange(length) / float(length) - indices *= curr_len - return nparray[indices.astype('int')] + raise Warning("Trying to stretch array to a length shorter than its own") + indices = np.arange(0, curr_len, curr_len / length).astype(int) + return nparray[indices] + + +def stretch_array_to_length_with_interpolation(nparray, length): + curr_len = len(nparray) + cont_indices = np.linspace(0, curr_len - 1, length) + return np.array([ + (1 - a) * nparray[lh] + a * nparray[rh] + for ci in cont_indices + for lh, rh, a in [(int(ci), int(np.ceil(ci)), ci % 1)] + ]) def make_even(iterable_1, iterable_2): - list_1, list_2 = list(iterable_1), list(iterable_2) + list_1 = list(iterable_1) + list_2 = list(iterable_2) length = max(len(list_1), len(list_2)) return ( [list_1[(n * len(list_1)) // length] for n in range(length)], diff --git a/manimlib/utils/paths.py b/manimlib/utils/paths.py index 8bc9cca1fb..b13af223da 100644 --- a/manimlib/utils/paths.py +++ b/manimlib/utils/paths.py @@ -1,9 +1,10 @@ import numpy as np +import math from manimlib.constants import OUT from manimlib.utils.bezier import interpolate from manimlib.utils.space_ops import get_norm -from manimlib.utils.space_ops import rotation_matrix +from manimlib.utils.space_ops import rotation_matrix_transpose STRAIGHT_PATH_THRESHOLD = 0.01 @@ -33,9 +34,10 @@ def path(start_points, end_points, alpha): vects = end_points - start_points centers = start_points + 0.5 * vects if arc_angle != np.pi: - centers += np.cross(unit_axis, vects / 2.0) / np.tan(arc_angle / 2) - rot_matrix = rotation_matrix(alpha * arc_angle, unit_axis) - return centers + np.dot(start_points - centers, rot_matrix.T) + centers += np.cross(unit_axis, vects / 2.0) / math.tan(arc_angle / 2) + rot_matrix_T = rotation_matrix_transpose(alpha * arc_angle, unit_axis) + return centers + np.dot(start_points - centers, rot_matrix_T) + return path diff --git a/manimlib/utils/rate_functions.py b/manimlib/utils/rate_functions.py index 39f9cd15c2..6f51bda077 100644 --- a/manimlib/utils/rate_functions.py +++ b/manimlib/utils/rate_functions.py @@ -1,27 +1,25 @@ import numpy as np from manimlib.utils.bezier import bezier -from manimlib.utils.simple_functions import sigmoid def linear(t): return t -def smooth(t, inflection=10.0): - error = sigmoid(-inflection / 2) - return np.clip( - (sigmoid(inflection * (t - 0.5)) - error) / (1 - 2 * error), - 0, 1, - ) +def smooth(t): + # Zero first and second derivatives at t=0 and t=1. + # Equivalent to bezier([0, 0, 0, 1, 1, 1]) + s = 1 - t + return (t**3) * (10 * s * s + 5 * s * t + t * t) -def rush_into(t, inflection=10.0): - return 2 * smooth(t / 2.0, inflection) +def rush_into(t): + return 2 * smooth(0.5 * t) -def rush_from(t, inflection=10.0): - return 2 * smooth(t / 2.0 + 0.5, inflection) - 1 +def rush_from(t): + return 2 * smooth(0.5 * (t + 1)) - 1 def slow_into(t): @@ -35,9 +33,9 @@ def double_smooth(t): return 0.5 * (1 + smooth(2 * t - 1)) -def there_and_back(t, inflection=10.0): +def there_and_back(t): new_t = 2 * t if t < 0.5 else 2 * (1 - t) - return smooth(new_t, inflection) + return smooth(new_t) def there_and_back_with_pause(t, pause_ratio=1. / 3): @@ -68,8 +66,7 @@ def squish_rate_func(func, a=0.4, b=0.6): def result(t): if a == b: return a - - if t < a: + elif t < a: return func(0) elif t > b: return func(1) diff --git a/manimlib/utils/simple_functions.py b/manimlib/utils/simple_functions.py index f619226726..5683db9d1c 100644 --- a/manimlib/utils/simple_functions.py +++ b/manimlib/utils/simple_functions.py @@ -45,6 +45,14 @@ def get_parameters(function): # but for now, we just allow the option to handle indeterminate 0/0. +def clip(a, min_a, max_a): + if a < min_a: + return min_a + elif a > max_a: + return max_a + return a + + def clip_in_place(array, min_val=None, max_val=None): if max_val is not None: array[array > max_val] = max_val diff --git a/manimlib/utils/space_ops.py b/manimlib/utils/space_ops.py index b0113f92f8..3ac27c9e3b 100644 --- a/manimlib/utils/space_ops.py +++ b/manimlib/utils/space_ops.py @@ -1,13 +1,14 @@ -from functools import reduce - import numpy as np +import math +import itertools as it +from mapbox_earcut import triangulate_float32 as earcut +from manimlib.constants import RIGHT +from manimlib.constants import DOWN from manimlib.constants import OUT from manimlib.constants import PI -from manimlib.constants import RIGHT from manimlib.constants import TAU from manimlib.utils.iterables import adjacent_pairs -from manimlib.utils.simple_functions import fdiv def get_norm(vect): @@ -18,28 +19,32 @@ def get_norm(vect): # TODO, implement quaternion type -def quaternion_mult(q1, q2): - w1, x1, y1, z1 = q1 - w2, x2, y2, z2 = q2 - return np.array([ - w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2, - w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2, - w1 * y2 + y1 * w2 + z1 * x2 - x1 * z2, - w1 * z2 + z1 * w2 + x1 * y2 - y1 * x2, - ]) +def quaternion_mult(*quats): + if len(quats) == 0: + return [1, 0, 0, 0] + result = quats[0] + for next_quat in quats[1:]: + w1, x1, y1, z1 = result + w2, x2, y2, z2 = next_quat + result = [ + w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2, + w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2, + w1 * y2 + y1 * w2 + z1 * x2 - x1 * z2, + w1 * z2 + z1 * w2 + x1 * y2 - y1 * x2, + ] + return result -def quaternion_from_angle_axis(angle, axis): - return np.append( - np.cos(angle / 2), - np.sin(angle / 2) * normalize(axis) - ) +def quaternion_from_angle_axis(angle, axis, axis_normalized=False): + if not axis_normalized: + axis = normalize(axis) + return [math.cos(angle / 2), *(math.sin(angle / 2) * axis)] def angle_axis_from_quaternion(quaternion): axis = normalize( quaternion[1:], - fall_back=np.array([1, 0, 0]) + fall_back=[1, 0, 0] ) angle = 2 * np.arccos(quaternion[0]) if angle > TAU / 2: @@ -48,8 +53,9 @@ def angle_axis_from_quaternion(quaternion): def quaternion_conjugate(quaternion): - result = np.array(quaternion) - result[1:] *= -1 + result = list(quaternion) + for i in range(1, len(result)): + result[i] *= -1 return result @@ -57,19 +63,20 @@ def rotate_vector(vector, angle, axis=OUT): if len(vector) == 2: # Use complex numbers...because why not z = complex(*vector) * np.exp(complex(0, angle)) - return np.array([z.real, z.imag]) + result = [z.real, z.imag] elif len(vector) == 3: # Use quaternions...because why not quat = quaternion_from_angle_axis(angle, axis) quat_inv = quaternion_conjugate(quat) - product = reduce( - quaternion_mult, - [quat, np.append(0, vector), quat_inv] - ) - return product[1:] + product = quaternion_mult(quat, [0, *vector], quat_inv) + result = product[1:] else: raise Exception("vector must be of dimension 2 or 3") + if isinstance(vector, np.ndarray): + return np.array(result) + return result + def thick_diagonal(dim, thickness=2): row_indices = np.arange(dim).repeat(dim).reshape((dim, dim)) @@ -77,20 +84,49 @@ def thick_diagonal(dim, thickness=2): return (np.abs(row_indices - col_indices) < thickness).astype('uint8') +def rotation_matrix_transpose_from_quaternion(quat): + quat_inv = quaternion_conjugate(quat) + return [ + quaternion_mult(quat, [0, *basis], quat_inv)[1:] + for basis in [ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1], + ] + ] + + +def rotation_matrix_from_quaternion(quat): + return np.transpose(rotation_matrix_transpose_from_quaternion(quat)) + + +def rotation_matrix_transpose(angle, axis): + if axis[0] == 0 and axis[1] == 0: + # axis = [0, 0, z] case is common enough it's worth + # having a shortcut + sgn = 1 if axis[2] > 0 else -1 + cos_a = math.cos(angle) + sin_a = math.sin(angle) * sgn + return [ + [cos_a, sin_a, 0], + [-sin_a, cos_a, 0], + [0, 0, 1], + ] + quat = quaternion_from_angle_axis(angle, axis) + return rotation_matrix_transpose_from_quaternion(quat) + + def rotation_matrix(angle, axis): """ Rotation in R^3 about a specified axis of rotation. """ - about_z = rotation_about_z(angle) - z_to_axis = z_to_vector(axis) - axis_to_z = np.linalg.inv(z_to_axis) - return reduce(np.dot, [z_to_axis, about_z, axis_to_z]) + return np.transpose(rotation_matrix_transpose(angle, axis)) def rotation_about_z(angle): return [ - [np.cos(angle), -np.sin(angle), 0], - [np.sin(angle), np.cos(angle), 0], + [math.cos(angle), -math.sin(angle), 0], + [math.sin(angle), math.cos(angle), 0], [0, 0, 1] ] @@ -100,41 +136,20 @@ def z_to_vector(vector): Returns some matrix in SO(3) which takes the z-axis to the (normalized) vector provided as an argument """ - norm = get_norm(vector) - if norm == 0: - return np.identity(3) - v = np.array(vector) / norm - phi = np.arccos(v[2]) - if any(v[:2]): - # projection of vector to unit circle - axis_proj = v[:2] / get_norm(v[:2]) - theta = np.arccos(axis_proj[0]) - if axis_proj[1] < 0: - theta = -theta - else: - theta = 0 - phi_down = np.array([ - [np.cos(phi), 0, np.sin(phi)], - [0, 1, 0], - [-np.sin(phi), 0, np.cos(phi)] - ]) - return np.dot(rotation_about_z(theta), phi_down) - - -def angle_between(v1, v2): - return np.arccos(np.dot( - v1 / get_norm(v1), - v2 / get_norm(v2) - )) + axis = cross(OUT, vector) + if get_norm(axis) == 0: + if vector[2] > 0: + return np.identity(3) + else: + return rotation_matrix(PI, RIGHT) + angle = np.arccos(np.dot(OUT, normalize(vector))) + return rotation_matrix(angle, axis=axis) def angle_of_vector(vector): """ Returns polar coordinate theta when vector is project on xy plane """ - z = complex(*vector[:2]) - if z == 0: - return 0 return np.angle(complex(*vector[:2])) @@ -143,10 +158,8 @@ def angle_between_vectors(v1, v2): Returns the angle between two 3D vectors. This angle will always be btw 0 and pi """ - return np.arccos(fdiv( - np.dot(v1, v2), - get_norm(v1) * get_norm(v2) - )) + diff = (angle_of_vector(v2) - angle_of_vector(v1)) % TAU + return min(diff, TAU - diff) def project_along_vector(point, vector): @@ -158,11 +171,18 @@ def normalize(vect, fall_back=None): norm = get_norm(vect) if norm > 0: return np.array(vect) / norm + elif fall_back is not None: + return fall_back else: - if fall_back is not None: - return fall_back - else: - return np.zeros(len(vect)) + return np.zeros(len(vect)) + + +def normalize_along_axis(array, axis, fall_back=None): + norms = np.sqrt((array * array).sum(axis)) + norms[norms == 0] = 1 + buffed_norms = np.repeat(norms, array.shape[axis]).reshape(array.shape) + array /= buffed_norms + return array def cross(v1, v2): @@ -173,8 +193,19 @@ def cross(v1, v2): ]) -def get_unit_normal(v1, v2): - return normalize(cross(v1, v2)) +def get_unit_normal(v1, v2, tol=1e-6): + v1 = normalize(v1) + v2 = normalize(v2) + cp = cross(v1, v2) + cp_norm = get_norm(cp) + if cp_norm < tol: + # Vectors align, so find a normal to them in the plane shared with the z-axis + new_cp = cross(cross(v1, OUT), v1) + new_cp_norm = get_norm(new_cp) + if new_cp_norm < tol: + return DOWN + return new_cp / new_cp_norm + return cp / cp_norm ### @@ -230,6 +261,35 @@ def det(a, b): return np.array([x, y, 0]) +def find_intersection(p0, v0, p1, v1, threshold=1e-5): + """ + Return the intersection of a line passing through p0 in direction v0 + with one passing through p1 in direction v1. (Or array of intersections + from arrays of such points/directions). + For 3d values, it returns the point on the ray p0 + v0 * t closest to the + ray p1 + v1 * t + """ + p0 = np.array(p0, ndmin=2) + v0 = np.array(v0, ndmin=2) + p1 = np.array(p1, ndmin=2) + v1 = np.array(v1, ndmin=2) + m, n = np.shape(p0) + assert(n in [2, 3]) + + numer = np.cross(v1, p1 - p0) + denom = np.cross(v1, v0) + if n == 3: + d = len(np.shape(numer)) + new_numer = np.multiply(numer, numer).sum(d - 1) + new_denom = np.multiply(denom, numer).sum(d - 1) + numer, denom = new_numer, new_denom + + denom[abs(denom) < threshold] = np.inf # So that ratio goes to 0 there + ratio = numer / denom + ratio = np.repeat(ratio, n).reshape((m, n)) + return p0 + ratio * v0 + + def get_winding_number(points): total_angle = 0 for p1, p2 in adjacent_pairs(points): @@ -237,3 +297,172 @@ def get_winding_number(points): d_angle = ((d_angle + PI) % TAU) - PI total_angle += d_angle return total_angle / TAU + + +## + +def cross2d(a, b): + if len(a.shape) == 2: + return a[:, 0] * b[:, 1] - a[:, 1] * b[:, 0] + else: + return a[0] * b[1] - b[0] * a[1] + + +def tri_area(a, b, c): + return 0.5 * abs( + a[0] * (b[1] - c[1]) + + b[0] * (c[1] - a[1]) + + c[0] * (a[1] - b[1]) + ) + + +def is_inside_triangle(p, a, b, c): + """ + Test if point p is inside triangle abc + """ + crosses = np.array([ + cross2d(p - a, b - p), + cross2d(p - b, c - p), + cross2d(p - c, a - p), + ]) + return np.all(crosses > 0) or np.all(crosses < 0) + + +def norm_squared(v): + return sum(v * v) + + +# TODO, fails for polygons drawn over themselves +def earclip_triangulation(verts, rings): + n = len(verts) + # Establish where loop indices should be connected + loop_connections = dict() + for e0, e1 in zip(rings, rings[1:]): + temp_i = e0 + # Find closet point in the first ring (j) to + # the first index of this ring (i) + norms = np.array([ + [j, norm_squared(verts[temp_i] - verts[j])] + for j in range(0, rings[0]) + if j not in loop_connections + ]) + j = int(norms[norms[:, 1].argmin()][0]) + # Find i closest to this j + norms = np.array([ + [i, norm_squared(verts[i] - verts[j])] + for i in range(e0, e1) + if i not in loop_connections + ]) + i = int(norms[norms[:, 1].argmin()][0]) + + loop_connections[i] = j + loop_connections[j] = i + + # Setup linked list + after = [] + e0 = 0 + for e1 in rings: + after.extend([*range(e0 + 1, e1), e0]) + e0 = e1 + + # Find an ordering of indices walking around the polygon + indices = [] + i = 0 + for x in range(n + len(rings) - 1): + # starting = False + if i in loop_connections: + j = loop_connections[i] + indices.extend([i, j]) + i = after[j] + else: + indices.append(i) + i = after[i] + if i == 0: + break + + meta_indices = earcut(verts[indices, :2], [len(indices)]) + return [indices[mi] for mi in meta_indices] + + +def old_earclip_triangulation(verts, rings, orientation): + n = len(verts) + assert(n in rings) + result = [] + + # Establish where loop indices should be connected + loop_connections = dict() + e0 = 0 + for e1 in rings: + norms = np.array([ + [i, j, get_norm(verts[i] - verts[j])] + for i in range(e0, e1) + for j in it.chain(range(0, e0), range(e1, n)) + ]) + if len(norms) == 0: + continue + i, j = norms[np.argmin(norms[:, 2])][:2].astype(int) + loop_connections[i] = j + loop_connections[j] = i + e0 = e1 + + # Setup bidirectional linked list + before = [] + after = [] + e0 = 0 + for e1 in rings: + after += [*range(e0 + 1, e1), e0] + before += [e1 - 1, *range(e0, e1 - 1)] + e0 = e1 + + # Initialize edge triangles + edge_tris = [] + i = 0 + starting = True + while (i != 0 or starting): + starting = False + if i in loop_connections: + j = loop_connections[i] + edge_tris.append([before[i], i, j]) + edge_tris.append([i, j, after[j]]) + i = after[j] + else: + edge_tris.append([before[i], i, after[i]]) + i = after[i] + + # Set up a test for whether or not three indices + # form an ear of the polygon, meaning a convex corner + # which doesn't contain any other vertices + indices = list(range(n)) + + def is_ear(*tri_indices): + tri = [verts[i] for i in tri_indices] + v1 = tri[1] - tri[0] + v2 = tri[2] - tri[1] + cross = v1[0] * v2[1] - v2[0] * v1[1] + if orientation * cross < 0: + return False + for j in indices: + if j in tri_indices: + continue + elif is_inside_triangle(verts[j], *tri): + return False + return True + + # Loop through and clip off all the ears + n_failures = 0 + i = 0 + while n_failures < len(edge_tris): + n = len(edge_tris) + edge_tri = edge_tris[i % n] + if is_ear(*edge_tri): + result.extend(edge_tri) + edge_tris[(i - 1) % n][2] = edge_tri[2] + edge_tris[(i + 1) % n][0] = edge_tri[0] + if edge_tri[1] in indices: + indices.remove(edge_tri[1]) + edge_tris.remove(edge_tri) + n_failures = 0 + else: + n_failures += 1 + i += 1 + return result diff --git a/manimlib/window.py b/manimlib/window.py new file mode 100644 index 0000000000..d39d2d7fc3 --- /dev/null +++ b/manimlib/window.py @@ -0,0 +1,86 @@ +import moderngl_window as mglw +from moderngl_window.context.pyglet.window import Window as PygletWindow +from moderngl_window.timers.clock import Timer + +from manimlib.constants import DEFAULT_PIXEL_WIDTH +from manimlib.constants import DEFAULT_PIXEL_HEIGHT +from manimlib.utils.config_ops import digest_config + + +class Window(PygletWindow): + size = (DEFAULT_PIXEL_WIDTH, DEFAULT_PIXEL_HEIGHT) + fullscreen = False + resizable = True + gl_version = (3, 3) + vsync = True + samples = 1 + cursor = True + + def __init__(self, scene, **kwargs): + digest_config(self, kwargs) + super().__init__(**kwargs) + self.scene = scene + self.title = str(scene) + # Put at the top of the screen + self.position = (self.position[0], 0) + + mglw.activate_context(window=self) + self.timer = Timer() + self.config = mglw.WindowConfig(ctx=self.ctx, wnd=self, timer=self.timer) + self.timer.start() + + # Delegate event handling to scene + def pixel_coords_to_space_coords(self, px, py, relative=False): + return self.scene.camera.pixel_coords_to_space_coords(px, py, relative) + + def on_mouse_motion(self, x, y, dx, dy): + super().on_mouse_motion(x, y, dx, dy) + point = self.pixel_coords_to_space_coords(x, y) + d_point = self.pixel_coords_to_space_coords(dx, dy, relative=True) + self.scene.on_mouse_motion(point, d_point) + + def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): + super().on_mouse_drag(x, y, dx, dy, buttons, modifiers) + point = self.pixel_coords_to_space_coords(x, y) + d_point = self.pixel_coords_to_space_coords(dx, dy, relative=True) + self.scene.on_mouse_drag(point, d_point, buttons, modifiers) + + def on_mouse_press(self, x: int, y: int, button, mods): + super().on_mouse_press(x, y, button, mods) + point = self.pixel_coords_to_space_coords(x, y) + self.scene.on_mouse_press(point, button, mods) + + def on_mouse_release(self, x: int, y: int, button, mods): + super().on_mouse_release(x, y, button, mods) + point = self.pixel_coords_to_space_coords(x, y) + self.scene.on_mouse_release(point, button, mods) + + def on_mouse_scroll(self, x, y, x_offset: float, y_offset: float): + super().on_mouse_scroll(x, y, x_offset, y_offset) + point = self.pixel_coords_to_space_coords(x, y) + offset = self.pixel_coords_to_space_coords(x_offset, y_offset, relative=True) + self.scene.on_mouse_scroll(point, offset) + + def on_key_release(self, symbol, modifiers): + super().on_key_release(symbol, modifiers) + self.scene.on_key_release(symbol, modifiers) + + def on_key_press(self, symbol, modifiers): + super().on_key_press(symbol, modifiers) + self.scene.on_key_press(symbol, modifiers) + + def on_resize(self, width: int, height: int): + super().on_resize(width, height) + self.scene.on_resize(width, height) + + def on_show(self): + super().on_show() + self.scene.on_show() + + def on_hide(self): + super().on_hide() + self.scene.on_hide() + + def on_close(self): + super().on_close() + self.scene.on_close() diff --git a/stage_scenes.py b/stage_scenes.py index 8ecf07f911..7d32712ac7 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", - "bayes", "1440p60" + "bayes/beta2", "1440p60" ) # files = os.listdir(animation_dir)