From 3535b39dd147976a83fca668e67e9442661c7510 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 16 Feb 2017 13:03:26 -0800 Subject: [PATCH] Sure, I *could* separate all these updates into separate reasonable commits, but where's the fun in that? --- animation/transform.py | 21 + eoc/chapter4.py | 1159 +++++++++++++++++++++++++++++++++++- eoc/graph_scene.py | 35 +- mobject/mobject.py | 2 +- mobject/tex_mobject.py | 31 +- scene/zoomed_scene.py | 12 +- topics/characters.py | 22 +- topics/geometry.py | 1 + topics/number_line.py | 15 +- topics/three_dimensions.py | 21 +- 10 files changed, 1280 insertions(+), 39 deletions(-) diff --git a/animation/transform.py b/animation/transform.py index b459c71d..66b0680e 100644 --- a/animation/transform.py +++ b/animation/transform.py @@ -9,6 +9,7 @@ from helpers import * from animation import Animation from simple_animations import DelayByOrder from mobject import Mobject, Point, VMobject, Group +from topics.geometry import Dot class Transform(Animation): CONFIG = { @@ -160,6 +161,26 @@ class ShimmerIn(DelayByOrder): mobject.sort_points(lambda p : np.dot(p, DOWN+RIGHT)) DelayByOrder.__init__(self, FadeIn(mobject, **kwargs)) +class FocusOn(Transform): + CONFIG = { + "opacity" : 0.2, + "color" : GREY, + "run_time" : 2, + "remover" : True, + } + def __init__(self, mobject_or_point, **kwargs): + digest_config(self, kwargs) + big_dot = Dot( + radius = SPACE_WIDTH+SPACE_HEIGHT, + stroke_width = 0, + fill_color = self.color, + fill_opacity = 0, + ) + little_dot = Dot(radius = 0) + little_dot.set_fill(self.color, opacity = self.opacity) + little_dot.move_to(mobject_or_point) + + Transform.__init__(self, big_dot, little_dot, **kwargs) class Rotate(ApplyMethod): CONFIG = { diff --git a/eoc/chapter4.py b/eoc/chapter4.py index c3638c99..09157955 100644 --- a/eoc/chapter4.py +++ b/eoc/chapter4.py @@ -20,6 +20,7 @@ from topics.three_dimensions import * from topics.objects import * from scene import Scene from scene.zoomed_scene import ZoomedScene +from scene.reconfigurable_scene import ReconfigurableScene from camera import Camera from mobject.svg_mobject import * from mobject.tex_mobject import * @@ -27,6 +28,10 @@ from mobject.tex_mobject import * from eoc.chapter1 import OpeningQuote, PatreonThanks from eoc.graph_scene import * +SINE_COLOR = BLUE +X_SQUARED_COLOR = GREEN +SUM_COLOR = RED +PRODUCT_COLOR = YELLOW class Chapter3OpeningQuote(OpeningQuote): CONFIG = { @@ -209,15 +214,1151 @@ class TransitionFromLastVideo(TeacherStudentsScene): ) self.dither(2) - - - - - - - - - +class SumRule(GraphScene, ZoomedScene): + CONFIG = { + "x_labeled_nums" : [], + "y_labeled_nums" : [], + "y_axis_label" : "", + "x_max" : 4, + "x_axis_width" : 2*SPACE_WIDTH, + "y_max" : 3, + "graph_origin" : 2.5*DOWN + 3.5*LEFT, + "graph_label_x_value" : 1.5, + "example_input" : 0.5, + "example_input_string" : "0.5", + "dx" : 0.02, + "zoomed_canvas_corner" : UP+LEFT, + "zoomed_canvas_corner_buff" : SMALL_BUFF, + "little_rectangle_start_position" : 2.5*LEFT, + "zoom_factor" : 10, + "tex_scale_factor" : 0.8, + } + def construct(self): + self.write_function() + self.add_graphs() + self.ask_about_derivative() + self.add_v_lines() + self.show_df_graphically() + self.show_df_algebraically() + self.show_d_sine() + self.show_d_x_squared() + self.complete_derivative() + self.revisit_ss_groups() + + def write_function(self): + func_mob = TexMobject("f(x) = ", "\\sin(x)", "+", "x^2") + func_mob.scale(self.tex_scale_factor) + func_mob.highlight_by_tex("\\sin(x)", SINE_COLOR) + func_mob.highlight_by_tex("x^2", X_SQUARED_COLOR) + func_mob.to_corner(UP+RIGHT) + self.add(func_mob) + + self.func_mob = func_mob + + def add_graphs(self): + self.setup_axes() + sine_graph = self.get_graph(np.sin, color = SINE_COLOR) + parabola = self.get_graph(lambda x : x**2, color = X_SQUARED_COLOR) + sum_graph = self.get_graph( + lambda x : np.sin(x) + x**2, + color = SUM_COLOR + ) + sine_label = self.get_graph_label( + sine_graph, "\\sin(x)", + x_val = self.graph_label_x_value, + direction = UP+RIGHT, + buff = 0, + ) + sine_label.scale(self.tex_scale_factor) + parabola_label = self.get_graph_label( + parabola, "x^2", x_val = self.graph_label_x_value, + ) + parabola_label.scale(self.tex_scale_factor) + sum_label = self.get_graph_label( + sum_graph, "f(x) = \\sin(x) + x^2", + x_val = self.graph_label_x_value, + direction = LEFT, + ) + sum_label.scale( + self.tex_scale_factor, + about_point = sum_label.get_corner(DOWN+RIGHT) + ) + #Break up + sum_label = VGroup( + VGroup(*sum_label[:11]), + VGroup(*sum_label[11:]) + ) + graphs = VGroup(sine_graph, parabola) + labels = VGroup(sine_label, parabola_label) + for label in labels: + label.add_background_rectangle() + + for graph, label in zip(graphs, labels): + self.play( + ShowCreation(graph), + Write(label) + ) + self.dither() + + sine_v_lines, parabox_v_lines = [ + self.get_vertical_lines_to_graph( + graph, x_max = 2, num_lines = 50, + stroke_width = 2 + ) + for graph in sine_graph, parabola + ] + parabox_v_lines.shift(0.02*RIGHT) + self.play(ShowCreation(sine_v_lines), Animation(labels)) + self.play(ShowCreation(parabox_v_lines), Animation(labels)) + self.dither() + self.play(*[ + ApplyMethod( + l1.shift, l2.get_end()-l1.get_start() + ) + for l1, l2, in zip(sine_v_lines, parabox_v_lines) + ] + [Animation(labels)]) + self.dither() + + # self.play(ReplacementTransform(graphs.copy(), sum_graph)) + self.play(ShowCreation(sum_graph)) + self.play(Write(sum_label, run_time = 2)) + self.dither() + + + self.sum_graph = sum_graph + self.parabola = parabola + self.sine_graph = sine_graph + self.sum_v_lines = VGroup(sine_v_lines, parabox_v_lines) + self.graph_labels = labels + + def ask_about_derivative(self): + deriv_q = TexMobject("{df ", "\\over dx}", "=", "???") + deriv_q.scale(self.tex_scale_factor) + deriv_q.next_to(self.func_mob, DOWN) + self.play(Write(deriv_q)) + self.dither() + + self.deriv_q = deriv_q + + def add_v_lines(self): + v_line, nudged_v_line = lines = VGroup(*[ + self.get_vertical_line_to_graph( + x, self.sum_graph, + line_class = DashedLine, + dashed_segment_length = 0.025, + color = WHITE + ) + for x in self.example_input, self.example_input+self.dx + ]) + dots = [ + Dot(line.get_bottom(), radius = 0.03, color = YELLOW) + for line in lines + ] + labels = [ + TexMobject( + str(self.example_input) + s + ).next_to(dot, DOWN+vect, buff = MED_LARGE_BUFF) + for s, dot, vect in zip( + ["", "+dx"], dots, [LEFT, RIGHT] + ) + ] + arrows = [ + Arrow( + label.get_corner(UP+vect), dot, + buff = SMALL_BUFF, + color = WHITE, + tip_length = 0.1 + ) + for label, dot, vect in zip(labels, dots, [RIGHT, LEFT]) + ] + + self.play( + FadeOut(self.sum_v_lines), + Animation(self.graph_labels) + ) + for line, dot, label, arrow in zip(lines, dots, labels, arrows): + self.play( + Write(label), + ShowCreation(arrow) + ) + self.play( + ShowCreation(line), + ShowCreation(dot) + ) + self.dither() + + def show_df_graphically(self): + ss_group = self.get_secant_slope_group( + self.example_input, self.sum_graph, + dx = self.dx, + dx_label = "dx", + df_label = "df", + include_secant_line = False + ) + + self.animate_activate_zooming() + self.play( + self.little_rectangle.move_to, ss_group, + ) + self.play(Write(ss_group)) + self.dither() + + self.ss_group = ss_group + + def show_df_algebraically(self): + deriv = TexMobject( + "df", "=", "d(\\sin(x))", "+", "d(x^2)" + ) + deriv.scale(self.tex_scale_factor) + deriv.next_to(self.deriv_q, DOWN, buff = MED_LARGE_BUFF) + deriv.highlight_by_tex("df", SUM_COLOR) + deriv.highlight_by_tex("d(\\sin(x))", SINE_COLOR) + deriv.highlight_by_tex("d(x^2)", X_SQUARED_COLOR) + + self.play(FocusOn(self.deriv_q)) + self.play(ReplacementTransform( + self.deriv_q[0].copy(), + deriv[0] + )) + self.play(Write(VGroup(*deriv[1:]))) + self.dither() + + self.deriv = deriv + + def show_d_sine(self): + ss_group = self.get_secant_slope_group( + self.example_input, self.sine_graph, + dx = self.dx, + dx_label = "dx", + df_label = "\\cos(0.5)dx", + include_secant_line = False + ) + for mob, vect in (ss_group.dx_label, UP), (ss_group.df_label, LEFT): + mob.scale(4, about_point = mob.get_edge_center(vect)) + + d_sine = self.deriv[2] + brace = Brace(d_sine) + cosine_dx = TexMobject("\\cos(x)", "dx") + cosine_dx.scale(self.tex_scale_factor) + cosine_dx.next_to(brace, DOWN) + cosine_dx.highlight(d_sine.get_color()) + + self.play( + GrowFromCenter(brace), + Write(cosine_dx) + ) + self.dither() + self.play( + self.little_rectangle.move_to, ss_group, + ) + self.dither() + self.play(Write(ss_group)) + self.dither() + + self.cosine = cosine_dx[0] + self.sine_ss_group = ss_group + + def show_d_x_squared(self): + ss_group = self.get_secant_slope_group( + self.example_input, self.parabola, + dx = self.dx, + dx_label = "dx", + df_label = "2(0.5)dx", + include_secant_line = False + ) + for mob, vect in (ss_group.dx_label, UP), (ss_group.df_label, LEFT): + mob.scale(3, about_point = mob.get_edge_center(vect)) + + d_x_squraed = self.deriv[4] + brace = Brace(d_x_squraed) + two_x_dx = TexMobject("2x", "\\,dx") + two_x_dx.scale(self.tex_scale_factor) + two_x_dx.next_to(brace, DOWN) + two_x_dx.highlight(d_x_squraed.get_color()) + + self.play(FocusOn(two_x_dx)) + self.play( + GrowFromCenter(brace), + Write(two_x_dx) + ) + self.dither() + self.play( + self.little_rectangle.move_to, ss_group, + ) + self.dither() + self.play(Write(ss_group)) + self.dither() + + self.two_x = two_x_dx[0] + self.x_squared_ss_group = ss_group + + def complete_derivative(self): + cosine = self.cosine.copy() + two_x = self.two_x.copy() + lhs = VGroup(*self.deriv_q[:3]) + to_fade = VGroup(*self.deriv_q[3:]) + for mob in cosine, two_x, lhs: + mob.generate_target() + lhs.target.next_to(self.func_mob, DOWN, aligned_edge = LEFT) + cosine.target.next_to(lhs.target) + plus = TexMobject("+").scale(self.tex_scale_factor) + plus.next_to(cosine.target) + two_x.target.next_to(plus) + + box = Rectangle(color = YELLOW) + box.replace(VGroup(lhs.target, two_x.target), stretch = True) + box.scale_in_place(1.2) + + self.play(FocusOn(self.deriv_q)) + self.play( + Write(plus), + FadeOut( + to_fade, + rate_func = squish_rate_func(smooth, 0, 0.5) + ), + *map(MoveToTarget, [cosine, two_x, lhs]), + run_time = 2 + ) + to_fade.highlight(BLACK) + self.play(ShowCreation(box)) + self.dither(2) + + def revisit_ss_groups(self): + for ss_group in self.sine_ss_group, self.ss_group: + self.play( + self.little_rectangle.move_to, ss_group, + run_time = 2 + ) + self.dither(2) + +class DiscussProducts(TeacherStudentsScene): + def construct(self): + wrong_product_rule = TexMobject( + "\\frac{d(\\sin(x)x^2)}{dx}", + "\\ne", + "\\left(\\frac{d(\\sin(x))}{dx}\\right)", + "\\left(\\frac{d(x^2)}{dx}\\right)", + ) + not_equals = wrong_product_rule[1] + wrong_product_rule[2].highlight(SINE_COLOR) + wrong_product_rule[3].highlight(X_SQUARED_COLOR) + wrong_product_rule.next_to( + self.get_teacher().get_corner(UP+LEFT), + UP, + buff = MED_LARGE_BUFF + ).shift_onto_screen() + + self.teacher_says( + "Products are a bit different", + target_mode = "sassy" + ) + self.dither(2) + self.play(RemovePiCreatureBubble( + self.get_teacher(), + target_mode = "raise_right_hand" + )) + self.play(Write(wrong_product_rule)) + self.change_student_modes( + "pondering", "confused", "erm", + added_anims = [ + not_equals.scale_in_place, 1.3, + not_equals.highlight, RED + ] + ) + self.dither() + self.teacher_says( + "Think about the \\\\ underlying meaning", + bubble_kwargs = {"height" : 3}, + added_anims = [ + wrong_product_rule.scale, 0.7, + wrong_product_rule.to_corner, UP+LEFT + ] + ) + self.change_student_modes(*["pondering"]*3) + self.dither(2) + +class NotGraphsForProducts(GraphScene): + CONFIG = { + "y_max" : 25, + "x_max" : 7, + } + def construct(self): + self.setup_axes() + sine_graph = self.get_graph(np.sin, color = SINE_COLOR) + sine_graph.label = self.get_graph_label( + sine_graph, "\\sin(x)", + x_val = 3*np.pi/2, + direction = DOWN + ) + parabola = self.get_graph( + lambda x : x**2, color = X_SQUARED_COLOR + ) + parabola.label = self.get_graph_label( + parabola, "x^2", + x_val = 2.5, + direction = UP+LEFT, + ) + product_graph = self.get_graph( + lambda x : np.sin(x)*(x**2), color = PRODUCT_COLOR + ) + product_graph.label = self.get_graph_label( + product_graph, "\\sin(x)x^2", + x_val = 2.8, + direction = UP+RIGHT, + buff = 0 + ) + + graphs = [sine_graph, parabola, product_graph] + for graph in graphs: + self.play( + ShowCreation(graph), + Write(graph.label, run_time = 2) + ) + self.dither() + + everything = VGroup(*filter( + lambda m : not m.is_subpath, + self.get_mobjects() + )) + words = TextMobject("Not the best visualization") + words.scale(1.5) + words.shift(SPACE_HEIGHT*UP/2) + words.add_background_rectangle() + words.highlight(RED) + self.play( + everything.fade, + Write(words) + ) + self.dither() + +class IntroduceProductAsArea(ReconfigurableScene): + CONFIG = { + "top_func" : np.sin, + "top_func_label" : "\\sin(x)", + "top_func_nudge_label" : "d(\\sin(x))", + "top_func_derivative" : "\\cos(x)", + "side_func" : lambda x : x**2, + "side_func_label" : "x^2", + "side_func_nudge_label" : "d(x^2)", + "side_func_derivative" : "2x", + "x_unit_to_space_unit" : 3, + "box_kwargs" : { + "fill_color" : YELLOW, + "fill_opacity" : 0.75, + "stroke_width" : 1, + }, + "df_box_kwargs" : { + "fill_color" : GREEN, + "fill_opacity" : 0.75, + "stroke_width" : 0, + }, + "box_corner_location" : 6*LEFT+2.5*UP, + "slider_center" : 3.5*RIGHT+2*DOWN, + "slider_width" : 6, + "slider_x_max" : 3, + "x_slider_handle_height" : 0.25, + "slider_handle_color" : BLUE, + "default_x" : .75, + "dx" : 0.1, + "tiny_dx" : 0.01, + } + def construct(self): + self.introduce_box() + self.talk_though_sine() + self.define_f_of_x() + self.nudge_x() + self.write_df() + self.show_thinner_dx() + self.expand_derivative() + self.write_derivative_abstractly() + self.write_mneumonic() + + def introduce_box(self): + box, labels = self.box_label_group = self.get_box_label_group(self.default_x) + self.x_slider = self.get_x_slider(self.default_x) + + self.play(Write(labels)) + self.play(DrawBorderThenFill(box)) + self.dither() + for mob in self.x_slider: + self.play(Write(mob, run_time = 1)) + self.dither() + for new_x in 0.5, 2, self.default_x: + self.animate_x_change( + new_x, run_time = 2 + ) + self.dither() + + def talk_though_sine(self): + x_axis = self.x_slider[0] + graph = FunctionGraph( + np.sin, x_min = 0, x_max = np.pi, + color = SINE_COLOR + ) + scale_factor = self.x_slider.get_width()/self.slider_x_max + graph.scale(scale_factor) + graph.move_to(x_axis.number_to_point(0), LEFT) + + label = TexMobject("\\sin(x)") + label.highlight(SINE_COLOR) + label.next_to(graph, UP) + + y_axis = x_axis.copy() + y_axis.remove(*y_axis.numbers) + + v_line = Line(ORIGIN, UP, color = WHITE, stroke_width = 2) + def v_line_update(v_line): + x = x_axis.point_to_number(self.x_slider[1].get_top()) + v_line.scale_to_fit_height(np.sin(x)*scale_factor) + v_line.move_to(x_axis.number_to_point(x), DOWN) + v_line_update(v_line) + + self.play( + Rotate(y_axis, np.pi/2, about_point = y_axis.get_left()), + Animation(x_axis) + ) + self.play( + ShowCreation(graph), + Write(label, run_time = 1) + ) + self.play(ShowCreation(v_line)) + for x, rt in zip([0.25, np.pi/2, 3, self.default_x], [2, 4, 4, 2]): + self.animate_x_change( + x, run_time = rt, + added_anims = [ + UpdateFromFunc(v_line, v_line_update) + ] + ) + self.dither() + self.play(*it.chain( + map(FadeOut, [y_axis, graph, label, v_line]), + [Animation(x_axis)] + )) + self.dither() + for x in 1, 0.5, self.default_x: + self.animate_x_change(x) + self.dither() + + def define_f_of_x(self): + f_def = TexMobject( + "f(x)", "=", + self.top_func_label, + self.side_func_label, + "=", + "\\text{Area}" + ) + f_def.to_corner(UP+RIGHT) + f_def[-1].highlight(self.box_kwargs["fill_color"]) + + box, labels = self.box_label_group + + self.play(Write(VGroup(*f_def[:-1]))) + self.play(Transform( + box.copy().set_fill(opacity = 0), f_def[-1], + run_time = 1.5, + )) + self.dither() + + self.f_def = f_def + + def nudge_x(self): + box, labels = self.box_label_group + nudge_label_group = self.get_nudge_label_group() + original_dx = self.dx + self.dx = self.tiny_dx + thin_df_boxes = self.get_df_boxes() + self.dx = original_dx + df_boxes = self.get_df_boxes() + right_box, corner_box, right_box = df_boxes + df_box_labels = self.get_df_box_labels(df_boxes) + + self.play(*map(GrowFromCenter, nudge_label_group)) + self.animate_x_change( + self.default_x+self.dx, + rate_func = there_and_back, + run_time = 2, + added_anims = [Animation(nudge_label_group)] + ) + self.dither() + self.play( + ReplacementTransform(thin_df_boxes, df_boxes), + VGroup(*labels[1]).shift, right_box.get_width()*RIGHT, + *map(GrowFromCenter, df_box_labels) + ) + self.play( + df_boxes.space_out_submobjects, 1.1, + df_boxes.move_to, box, UP+LEFT, + *[ + MaintainPositionRelativeTo(label, box) + for label, box in zip( + df_box_labels, [right_box, corner_box] + ) + ] + ) + self.dither() + + self.df_boxes = df_boxes + self.df_box_labels = df_box_labels + self.x_slider.add(nudge_label_group) + + def get_nudge_label_group(self): + line, triangle, x_mob = self.x_slider + dx_line = Line(*[ + line.number_to_point(self.x_slider.x_val + num) + for num in 0, self.dx, + ]) + dx_line.set_stroke( + self.df_box_kwargs["fill_color"], + width = 6 + ) + brace = Brace(dx_line, UP, buff = SMALL_BUFF) + brace.stretch_to_fit_height(0.2) + brace.next_to(dx_line, UP, buff = SMALL_BUFF) + brace.set_stroke(width = 1) + dx = TexMobject("dx") + dx.scale(0.7) + dx.next_to(brace, UP, buff = SMALL_BUFF) + dx.highlight(dx_line.get_color()) + + return VGroup(dx_line, brace, dx) + + def get_df_boxes(self): + box, labels = self.box_label_group + alt_box = self.get_box(self.x_slider.x_val + self.dx) + + h, w = box.get_height(), box.get_width() + dh, dw = alt_box.get_height()-h, alt_box.get_width()-w + heights_and_widths = [(dh, w), (dh, dw), (h, dw)] + vects = [DOWN, DOWN+RIGHT, RIGHT] + df_boxes = VGroup(*[ + Rectangle( + height = height, width = width, **self.df_box_kwargs + ).next_to(box, vect, buff = 0) + for (height, width), vect in zip( + heights_and_widths, vects + ) + ]) + return df_boxes + + def get_df_box_labels(self, df_boxes): + bottom_box, corner_box, right_box = df_boxes + result = VGroup() + quads = [ + (right_box, UP, self.top_func_nudge_label, LEFT), + (corner_box, RIGHT, self.side_func_nudge_label, ORIGIN), + ] + for box, vect, label_tex, aligned_edge in quads: + brace = Brace(box, vect) + label = TexMobject(label_tex) + label.next_to( + brace, vect, + aligned_edge = aligned_edge, + buff = SMALL_BUFF + ) + label.highlight(df_boxes[0].get_color()) + result.add(VGroup(brace, label)) + return result + + def write_df(self): + deriv = TexMobject( + "df", "=", + self.top_func_label, + self.side_func_nudge_label, + "+", + self.side_func_label, + self.top_func_nudge_label, + ) + deriv.scale(0.9) + deriv.next_to(self.f_def, DOWN, buff = LARGE_BUFF) + deriv.to_edge(RIGHT) + for submob, tex in zip(deriv, deriv.expression_parts): + if tex.startswith("d"): + submob.highlight(self.df_box_kwargs["fill_color"]) + bottom_box_area = VGroup(*deriv[2:4]) + right_box_area = VGroup(*deriv[5:7]) + + bottom_box, corner_box, right_box = self.df_boxes + plus = TexMobject("+").set_fill(opacity = 0) + df_boxes_copy = VGroup( + bottom_box.copy(), + plus, + right_box.copy(), + plus.copy(), + corner_box.copy(), + ) + + self.deriv = deriv + self.df_boxes_copy = df_boxes_copy + box, labels = self.box_label_group + self.full_box_parts = VGroup(*it.chain( + [box], self.df_boxes, labels, self.df_box_labels + )) + + self.play(Write(VGroup(*deriv[:2]))) + self.play( + df_boxes_copy.arrange_submobjects, + df_boxes_copy.set_fill, None, self.df_box_kwargs["fill_opacity"], + df_boxes_copy.next_to, deriv[1] + ) + deriv.submobjects[4] = df_boxes_copy[1] + self.dither() + + self.highlight_right_boxes() + self.highlight_bottom_boxes() + self.describe_bottom_box(bottom_box_area) + self.describe_right_box(right_box_area) + self.ignore_corner() + + # self.add(deriv) + + def highlight_boxes_and_label(self, boxes, label): + boxes.save_state() + label.save_state() + self.play( + boxes.highlight, RED, + label.highlight, RED, + ) + self.play( + label[1].scale_in_place, 1.1, + rate_func = there_and_back + ) + self.play(boxes.restore, label.restore) + self.dither() + + def highlight_right_boxes(self): + self.highlight_boxes_and_label( + VGroup(*self.df_boxes[1:]), + self.df_box_labels[0] + ) + + def highlight_bottom_boxes(self): + self.highlight_boxes_and_label( + VGroup(*self.df_boxes[:-1]), + self.df_box_labels[1] + ) + + def describe_bottom_box(self, bottom_box_area): + bottom_box = self.df_boxes[0] + bottom_box_copy = self.df_boxes_copy[0] + other_box_copies = VGroup(*self.df_boxes_copy[1:]) + top_label = self.box_label_group[1][0] + right_label = self.df_box_labels[1] + + faders = VGroup(*filter( + lambda m : m not in [bottom_box, top_label, right_label], + self.full_box_parts + )) + faders.save_state() + + self.play(faders.fade, 0.8) + self.dither() + self.play(FocusOn(bottom_box_copy)) + self.play( + ReplacementTransform(bottom_box_copy, bottom_box_area), + other_box_copies.next_to, bottom_box_area, RIGHT + ) + self.dither() + self.play(faders.restore) + + def describe_right_box(self, right_box_area): + right_box = self.df_boxes[2] + right_box_copy = self.df_boxes_copy[2] + right_box_area.next_to(self.df_boxes_copy[1]) + other_box_copies = VGroup(*self.df_boxes_copy[3:]) + top_label = self.df_box_labels[0] + right_label = self.box_label_group[1][1] + + faders = VGroup(*filter( + lambda m : m not in [right_box, top_label, right_label], + self.full_box_parts + )) + faders.save_state() + + self.play(faders.fade, 0.8) + self.dither() + self.play(FocusOn(right_box_copy)) + self.play( + ReplacementTransform(right_box_copy, right_box_area), + other_box_copies.next_to, right_box_area, DOWN, + MED_SMALL_BUFF, RIGHT, + + ) + self.dither() + self.play(faders.restore) + + def ignore_corner(self): + corner = self.df_boxes[1] + corner.save_state() + corner_copy = VGroup(*self.df_boxes_copy[-2:]) + words = TextMobject("Ignore") + words.highlight(RED) + words.next_to(corner_copy, LEFT, buff = LARGE_BUFF) + words.shift(MED_SMALL_BUFF*DOWN) + arrow = Arrow(words, corner_copy, buff = SMALL_BUFF, color = RED) + + self.play( + corner.highlight, RED, + corner_copy.highlight, RED, + ) + self.dither() + self.play(Write(words), ShowCreation(arrow)) + self.dither() + self.play(*map(FadeOut, [words, arrow, corner_copy])) + self.dither() + corner_copy.highlight(BLACK) + + def show_thinner_dx(self): + self.transition_to_alt_config(dx = self.tiny_dx) + + def expand_derivative(self): + # self.play( + # self.deriv.next_to, self.f_def, DOWN, MED_LARGE_BUFF, + # self.deriv.shift_onto_screen + # ) + # self.dither() + + expanded_deriv = TexMobject( + "df", "=", + self.top_func_label, + self.side_func_derivative, + "\\,dx", + "+", + self.side_func_label, + self.top_func_derivative, + "\\,dx" + ) + final_deriv = TexMobject( + "{df \\over ", "dx}", "=", + self.top_func_label, + self.side_func_derivative, + "+", + self.side_func_label, + self.top_func_derivative, + ) + color = self.deriv[0].get_color() + for new_deriv in expanded_deriv, final_deriv: + for submob, tex in zip(new_deriv, new_deriv.expression_parts): + for substr in "df", "dx", self.top_func_derivative, self.side_func_derivative: + if substr in tex: + submob.highlight(color) + new_deriv.scale(0.9) + new_deriv.next_to(self.deriv, DOWN, buff = MED_LARGE_BUFF) + new_deriv.shift_onto_screen() + + for index in 3, 6: + self.deriv.submobjects.insert( + index+1, self.deriv[index].copy() + ) + self.play(ReplacementTransform( + self.deriv.copy(), expanded_deriv + )) + self.dither() + self.play(*[ + Transform( + expanded_deriv[i], final_deriv[j], + path_arc = -np.pi/2 + ) + for i, j in [ + (0, 0), + (1, 2), + (2, 3), + (3, 4), + (4, 1), + (5, 5), + (6, 6), + (7, 7), + (8, 1), + ] + ]) + self.dither() + + def write_derivative_abstractly(self): + self.transition_to_alt_config( + return_to_original_configuration = False, + top_func_label = "g(x)", + top_func_nudge_label = "dg", + top_func_derivative = "\\frac{dg}{dx}", + side_func_label = "h(x)", + side_func_nudge_label = "dh", + side_func_derivative = "\\frac{dh}{dx}", + ) + self.dither() + + def write_mneumonic(self): + morty = Mortimer() + morty.scale(0.7) + morty.to_edge(DOWN) + morty.shift(2*LEFT) + words = TextMobject( + "``Left ", "d(Right) ", "+", " Right ", "d(Left)", "''", + arg_separator = "" + ) + VGroup(words[1], words[4]).highlight(self.df_boxes[0].get_color()) + words.scale(0.7) + words.next_to(morty.get_corner(UP+LEFT), UP) + words.shift_onto_screen() + + self.play(FadeIn(morty)) + self.play( + morty.change_mode, "raise_right_hand", + Write(words) + ) + self.dither() + + ############### + + def animate_x_change( + self, target_x, + box_label_group = None, + x_slider = None, + **kwargs + ): + box_label_group = box_label_group or self.box_label_group + x_slider = x_slider or self.x_slider + kwargs["run_time"] = kwargs.get("run_time", 2) + added_anims = kwargs.get("added_anims", []) + + start_x = x_slider.x_val + def update_box_label_group(box_label_group, alpha): + new_x = interpolate(start_x, target_x, alpha) + new_box_label_group = self.get_box_label_group(new_x) + Transform(box_label_group, new_box_label_group).update(1) + + def update_x_slider(x_slider, alpha): + new_x = interpolate(start_x, target_x, alpha) + new_x_slider = self.get_x_slider(new_x) + Transform(x_slider, new_x_slider).update(1) + + self.play( + UpdateFromAlphaFunc( + box_label_group, + update_box_label_group + ), + UpdateFromAlphaFunc( + x_slider, + update_x_slider + ), + *added_anims, + **kwargs + ) + x_slider.x_val = target_x + + def get_x_slider(self, x): + numbers = range(int(self.slider_x_max) + 1) + line = NumberLine( + x_min = 0, + x_max = self.slider_x_max, + space_unit_to_num = float(self.slider_width)/self.slider_x_max, + color = GREY, + numbers_with_elongated_ticks = numbers, + tick_frequency = 0.25, + ) + line.add_numbers(*numbers) + line.numbers.next_to(line, UP, buff = SMALL_BUFF) + for number in line.numbers: + number.add_background_rectangle() + line.move_to(self.slider_center) + + triangle = RegularPolygon( + 3, start_angle = np.pi/2, + fill_color = self.slider_handle_color, + fill_opacity = 0.8, + stroke_width = 0, + ) + triangle.scale_to_fit_height(self.x_slider_handle_height) + triangle.move_to(line.number_to_point(x), UP) + + x_mob = TexMobject("x") + x_mob.next_to(triangle, DOWN, buff = SMALL_BUFF) + + result = VGroup(line, triangle, x_mob) + result.x_val = x + return result + + def get_box_label_group(self, x): + box = self.get_box(x) + labels = self.get_box_labels(box) + return VGroup(box, labels) + + def get_box(self, x): + box = Rectangle( + width = self.x_unit_to_space_unit * self.top_func(x), + height = self.x_unit_to_space_unit * self.side_func(x), + **self.box_kwargs + ) + box.move_to(self.box_corner_location, UP+LEFT) + return box + + def get_box_labels(self, box): + result = VGroup() + for label_tex, vect in (self.top_func_label, UP), (self.side_func_label, RIGHT): + brace = Brace(box, vect, min_num_quads = 5) + label = TexMobject(label_tex) + label.next_to(brace, vect, buff = SMALL_BUFF) + result.add(VGroup(brace, label)) + return result + +class MneumonicExample(TeacherStudentsScene): + def construct(self): + d, left, right, rp = deriv_q = TexMobject( + "\\frac{d}{dx}(", "\\sin(x)", "x^2", ")" + ) + deriv_q.to_edge(UP) + + words = TextMobject( + "Left ", "d(Right) ", "+", " Right ", "d(Left)", + arg_separator = "" + ) + deriv = TexMobject("\\sin(x)", "2x", "+", "x^2", "\\cos(x)") + for mob in words, deriv: + VGroup(mob[1], mob[4]).highlight(GREEN) + mob.next_to(deriv_q, DOWN, buff = MED_LARGE_BUFF) + deriv.shift(words[2].get_center()-deriv[2].get_center()) + + self.add(words) + self.play( + Write(deriv_q), + self.get_teacher().change_mode, "raise_right_hand" + ) + self.change_student_modes(*["pondering"]*3) + for i, j, vect in [(0, 2, RIGHT), (2, 5, LEFT)]: + these_words = VGroup(*words[i:j]) + these_terms = VGroup(*deriv[i:j]) + self.play( + these_words.next_to, these_terms, DOWN, + MED_LARGE_BUFF, vect + ) + self.play(ReplacementTransform( + these_words.copy(), these_terms + )) + self.dither() + self.play(*it.chain(*[ + [pi.change_mode, "confused", pi.look_at, deriv_q] + for pi in self.get_pi_creatures() + ])) + self.dither() + +class ConstantMultiplication(TeacherStudentsScene): + def construct(self): + question = TextMobject("What about $\\dfrac{d}{dx}(2\\sin(x))$?") + answer = TextMobject("2\\cos(x)") + self.teacher_says(question) + self.dither() + self.student_says( + answer, target_mode = "hooray", + added_anims = [question.copy().to_edge, UP] + ) + self.play(self.get_teacher().change_mode, "happy") + self.change_student_modes("pondering", "hooray", "pondering") + self.dither(3) + +class ConstantMultiplicationFigure(IntroduceProductAsArea): + CONFIG = { + "side_func" : lambda x : 1, + "side_func_label" : "\\text{Constant}", + "side_func_nudge_label" : "", + "side_func_derivative" : "", + "x_unit_to_space_unit" : 3, + "default_x" : 0.5, + "dx" : 0.1 + } + def construct(self): + self.box_label_group = self.get_box_label_group(self.default_x) + self.x_slider = self.get_x_slider(self.default_x) + # df_boxes = self.get_df_boxes() + # df_box_labels = self.get_df_box_labels(df_boxes) + + self.add(self.box_label_group, self.x_slider) + self.nudge_x() + +class ShoveXSquaredInSine(Scene): + def construct(self): + title = TextMobject("Function composition") + title.to_edge(UP) + + sine = TexMobject("g(", "x", ")", "=", "\\sin(", "x", ")") + sine.highlight(SINE_COLOR) + x_squared = TexMobject("h(x)", "=", "x^2") + x_squared.highlight(X_SQUARED_COLOR) + group = VGroup(sine, x_squared) + group.arrange_submobjects(buff = LARGE_BUFF) + group.shift(UP) + composition = TexMobject( + "g(", "h(x)", ")", "=", "\\sin(", "x^2", ")" + ) + for i in 0, 2, 4, 6: + composition[i].highlight(SINE_COLOR) + for i in 1, 5: + composition[i].highlight(X_SQUARED_COLOR) + composition.next_to(group, DOWN, buff = LARGE_BUFF) + + brace = Brace(VGroup(*composition[-3:]), DOWN) + deriv_q = brace.get_text("Derivative?") + + self.add(group) + self.play(Write(title)) + self.dither() + triplets = [ + [sine, (0, 2), (0, 2)], + [x_squared, (0,), (1,)], + [sine, (3, 4, 6), (3, 4, 6)], + [x_squared, (2,), (5,)] + ] + for premob, pre_indices, comp_indicies in triplets: + self.play(*[ + ReplacementTransform( + premob[i].copy(), composition[j] + ) + for i, j in zip(pre_indices, comp_indicies) + ]) + self.dither() + self.dither() + self.play( + GrowFromCenter(brace), + Write(deriv_q) + ) + self.dither() + +class ThreeLinesChainRule(Scene): + CONFIG = { + "default_x" : 0.5, + "line_configs" : [ + { + "x_min" : 0, + "x_max" : 3, + "numbers_to_show" : range(4), + "tick_frequency" : 0.25, + }, + { + "x_min" : 0, + "x_max" : 9, + "numbers_to_show" : range(0, 10, 2), + "tick_frequency" : 1, + }, + { + "x_min" : -2, + "x_max" : 2, + "numbers_to_show" : range(-2, 3), + "tick_frequency" : 0.25, + }, + ], + "line_width" : 6, + } + def construct(self): + lines_group = self.get_three_lines_group() + + self.add(lines_group) + + def get_three_line_group(self, x): + number_lines = VGroup(*[ + self.get_line(x, **line_config) + for line_config in self.line_configs + ]) + number_lines.arrange_submobjects(DOWN, buff = LARGE_BUFF) + return number_lines + + def get_number_line(self, x, **line_config): + number_line = NumberLine(**line_config) diff --git a/eoc/graph_scene.py b/eoc/graph_scene.py index 306ef350..fd05ca96 100644 --- a/eoc/graph_scene.py +++ b/eoc/graph_scene.py @@ -37,10 +37,7 @@ class GraphScene(Scene): def setup_axes(self, animate = False): x_num_range = float(self.x_max - self.x_min) self.space_unit_to_x = self.x_axis_width/x_num_range - if self.x_labeled_nums is None: - self.x_labeled_nums = np.arange( - self.x_min, self.x_max, 2*self.x_tick_frequency - ) + self.x_labeled_nums = self.x_labeled_nums or [] x_axis = NumberLine( x_min = self.x_min, x_max = self.x_max, @@ -67,10 +64,7 @@ class GraphScene(Scene): y_num_range = float(self.y_max - self.y_min) self.space_unit_to_y = self.y_axis_height/y_num_range - if self.y_labeled_nums is None: - self.y_labeled_nums = np.arange( - self.y_min, self.y_max, 2*self.y_tick_frequency - ) + self.y_labeled_nums = self.y_labeled_nums or [] y_axis = NumberLine( x_min = self.y_min, x_max = self.y_max, @@ -221,7 +215,21 @@ class GraphScene(Scene): self.coords_to_point(x, 0), self.input_to_graph_point(x, graph), **line_kwargs - ) + ) + + def get_vertical_lines_to_graph( + self, graph, + x_min = None, + x_max = None, + num_lines = 20, + **kwargs + ): + x_min = x_min or self.x_min + x_max = x_max or self.x_max + return VGroup(*[ + self.get_vertical_line_to_graph(x, graph, **kwargs) + for x in np.linspace(x_min, x_max, num_lines) + ]) def get_secant_slope_group( self, @@ -236,6 +244,15 @@ class GraphScene(Scene): secant_line_color = None, secant_line_length = 10, ): + """ + Resulting group is of the form VGroup( + dx_line, + df_line, + dx_label, (if applicable) + df_label, (if applicable) + secant_line, (if applicable) + ) + """ kwargs = locals() kwargs.pop("self") group = VGroup() diff --git a/mobject/mobject.py b/mobject/mobject.py index 7c3fa438..6c1e68f0 100644 --- a/mobject/mobject.py +++ b/mobject/mobject.py @@ -310,7 +310,7 @@ class Mobject(object): def scale_to_fit_height(self, height): return self.stretch_to_fit(height, 1, stretch = False) - def space_out_submobjects(self, factor = 1.5): + def space_out_submobjects(self, factor = 1.5, **kwargs): self.scale_in_place(factor) for submob in self.submobjects: submob.scale_in_place(1./factor) diff --git a/mobject/tex_mobject.py b/mobject/tex_mobject.py index fb2672ea..fb5fdf72 100644 --- a/mobject/tex_mobject.py +++ b/mobject/tex_mobject.py @@ -1,4 +1,4 @@ -from vectorized_mobject import VMobject +from vectorized_mobject import VMobject, VGroup from svg_mobject import SVGMobject, VMobjectFromSVGPathstring from topics.geometry import BackgroundRectangle from helpers import * @@ -74,8 +74,24 @@ class TexMobject(SVGMobject): result = result.replace("\n", " \\\\ \n ") result = " ".join([self.alignment, result]) result = result.strip() + result = self.remove_stray_braces(result) return result + def remove_stray_braces(self, tex): + """ + Makes TexMobject resiliant to unmatched { at start + """ + num_lefts, num_rights = [ + tex.count(char) + for char in "{}" + ] + if tex.startswith("{") and num_lefts > num_rights: + return tex[1:] + elif tex.endswith("}") and num_rights > num_lefts: + return tex[:-1] + return tex + + def get_tex_string(self): return self.tex_string @@ -86,7 +102,7 @@ class TexMobject(SVGMobject): for expr in self.args: model = TexMobject(expr, **self.CONFIG) new_index = curr_index + len(model.submobjects) - new_submobjects.append(VMobject( + new_submobjects.append(VGroup( *self.submobjects[curr_index:new_index] )) curr_index = new_index @@ -129,8 +145,9 @@ class TextMobject(TexMobject): class Brace(TexMobject): CONFIG = { "buff" : 0.2, - "n_quads" : 3, - "tex_string" : "\\underbrace{%s}"%(3*"\\qquad"), + "width_multiplier" : 2, + "max_num_quads" : 15, + "min_num_quads" : 0, } def __init__(self, mobject, direction = DOWN, **kwargs): digest_config(self, kwargs, locals()) @@ -141,7 +158,11 @@ class Brace(TexMobject): target_width = right[0]-left[0] ## Adding int(target_width) qquads gives approximately the right width - tex_string = "\\underbrace{%s}"%(int(target_width)*"\\qquad") + num_quads = np.clip( + int(self.width_multiplier*target_width), + self.min_num_quads, self.max_num_quads + ) + tex_string = "\\underbrace{%s}"%(num_quads*"\\qquad") TexMobject.__init__(self, tex_string, **kwargs) self.stretch_to_fit_width(target_width) self.shift(left - self.get_corner(UP+LEFT) + self.buff*DOWN) diff --git a/scene/zoomed_scene.py b/scene/zoomed_scene.py index 64ef1544..216fa404 100644 --- a/scene/zoomed_scene.py +++ b/scene/zoomed_scene.py @@ -1,6 +1,7 @@ import numpy as np from scene import Scene +from animation.transform import FadeIn from mobject import Mobject from topics.geometry import Rectangle from camera import MovingCamera, Camera @@ -18,6 +19,7 @@ class ZoomedScene(Scene): "zoomed_canvas_corner" : UP+RIGHT, "zoomed_canvas_corner_buff" : DEFAULT_MOBJECT_TO_EDGE_BUFFER, "zoomed_camera_background" : None, + "little_rectangle_start_position" : ORIGIN, "zoom_factor" : 6, "square_color" : WHITE, "zoom_activated" : False, @@ -28,6 +30,12 @@ class ZoomedScene(Scene): self.setup_zoomed_camera() self.zoom_activated = True + def animate_activate_zooming(self): + self.activate_zooming() + self.play(*map(FadeIn, [ + self.little_rectangle, self.big_rectangle + ])) + def disactivate_zooming(self): self.remove(self.big_rectangle, self.little_rectangle) self.zoom_activated = False @@ -71,7 +79,9 @@ class ZoomedScene(Scene): def setup_zoomed_camera(self): self.little_rectangle = self.big_rectangle.copy() self.little_rectangle.scale(1./self.zoom_factor) - self.little_rectangle.center() + self.little_rectangle.move_to( + self.little_rectangle_start_position + ) self.zoomed_camera = MovingCamera( self.little_rectangle, pixel_shape = self.zoomed_canvas_pixel_shape, diff --git a/topics/characters.py b/topics/characters.py index 26e8797e..a15e1649 100644 --- a/topics/characters.py +++ b/topics/characters.py @@ -242,8 +242,8 @@ class PiCreatureBubbleIntroduction(AnimationGroup): } def __init__(self, pi_creature, *content, **kwargs): digest_config(self, kwargs) - if isinstance(content, Mobject): - bubble_content = content + if isinstance(content[0], Mobject): + bubble_content = content[0] else: bubble_content = TextMobject(*content) bubble = pi_creature.get_bubble( @@ -390,11 +390,13 @@ class PiCreatureScene(Scene): first mobject being animated with each .play call """ animations = Scene.compile_play_args_to_animation_list(self, *args) - if len(animations) == 0: - return animations - first_anim = animations[0] - if isinstance(first_anim, Blink): + non_pi_creature_anims = filter( + lambda anim : anim.mobject not in self.get_pi_creatures(), + animations + ) + if len(non_pi_creature_anims) == 0: return animations + first_anim = non_pi_creature_anims[0] #Look at ending state first_anim.update(1) point_of_interest = first_anim.mobject.get_center() @@ -405,10 +407,10 @@ class PiCreatureScene(Scene): continue if pi_creature in first_anim.mobject.submobject_family(): continue - anims_with_pi_creature = [ - anim for anim in animations - if pi_creature in anim.mobject.submobject_family() - ] + anims_with_pi_creature = filter( + lambda anim : pi_creature in anim.mobject.submobject_family(), + animations + ) if anims_with_pi_creature: for anim in anims_with_pi_creature: if isinstance(anim, Transform): diff --git a/topics/geometry.py b/topics/geometry.py index 5d3fda2a..0e619471 100644 --- a/topics/geometry.py +++ b/topics/geometry.py @@ -154,6 +154,7 @@ class DashedLine(Line): for p1, p2, include in zip(points, points[1:], includes): if include: self.add(Line(p1, p2, **self.init_kwargs)) + self.put_start_and_end_on(self.start, self.end) return self def get_start(self): diff --git a/topics/number_line.py b/topics/number_line.py index 5a8345d0..bf2606a0 100644 --- a/topics/number_line.py +++ b/topics/number_line.py @@ -16,6 +16,7 @@ class NumberLine(VMobject): "tick_frequency" : 1, "leftmost_tick" : None, #Defaults to ceil(x_min) "numbers_with_elongated_ticks" : [0], + "numbers_to_show" : None, "longer_tick_multiple" : 2, "number_at_center" : 0, "propogate_style_to_family" : True @@ -62,11 +63,19 @@ class NumberLine(VMobject): ) def point_to_number(self, point): - dist_from_left = float(point[0]-self.main_line.get_left()[0]) - num_dist_from_left = dist_from_left/self.space_unit_to_num - return self.x_min + num_dist_from_left + left_point, right_point = self.main_line.get_start_and_end() + full_vect = right_point-left_point + def distance_from_left(p): + return np.dot(p-left_point, full_vect)/np.linalg.norm(full_vect) + + return interpolate( + self.x_min, self.x_max, + distance_from_left(point)/distance_from_left(right_point) + ) def default_numbers_to_display(self): + if self.numbers_to_show is not None: + return self.numbers_to_show return np.arange(self.leftmost_tick, self.x_max, 1) def get_vertical_number_offset(self, direction = DOWN): diff --git a/topics/three_dimensions.py b/topics/three_dimensions.py index 94b186eb..a849e7c4 100644 --- a/topics/three_dimensions.py +++ b/topics/three_dimensions.py @@ -1,6 +1,8 @@ from helpers import * +from mobject.vectorized_mobject import VGroup +from topics.geometry import Square from scene import Scene from camera import Camera @@ -60,7 +62,24 @@ class ThreeDScene(Scene): "camera_class" : ThreeDCamera, } - +############## + +class Cube(VGroup): + CONFIG = { + "fill_opacity" : 0.75, + "fill_color" : BLUE, + "stroke_width" : 0, + "propogate_style_to_family" : True, + "side_length" : 2, + } + def generate_points(self): + faces = [ + Square(side_length = self.side_length).shift(OUT).apply_function( + lambda p : np.dot(p, z_to_vector(vect).T) + ) + for vect in IN, OUT, LEFT, RIGHT, UP, DOWN + ] + self.add(*faces)