From 90da42e034f967d1429eacf13837042dcf8e8f18 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 15 May 2018 12:01:34 -0700 Subject: [PATCH 1/7] ZoomInOnXSquaredNearOne of alt-calc --- active_projects/alt_calc.py | 238 ++++++++++++++++++++++++++++++++++-- 1 file changed, 231 insertions(+), 7 deletions(-) diff --git a/active_projects/alt_calc.py b/active_projects/alt_calc.py index 6893da41..10695f66 100644 --- a/active_projects/alt_calc.py +++ b/active_projects/alt_calc.py @@ -41,6 +41,7 @@ class NumberlineTransformationScene(ZoomedScene): "zoomed_display_corner_buff": MED_SMALL_BUFF, "mini_line_scale_factor": 2, "default_coordinate_value_dx": 0.05, + "zoomed_camera_background_rectangle_fill_opacity": 1.0, } def setup(self): @@ -82,7 +83,7 @@ class NumberlineTransformationScene(ZoomedScene): frame = self.zoomed_camera.frame frame.next_to(self.camera.frame, UL) self.zoomed_camera_background_rectangle = BackgroundRectangle( - frame, fill_opacity=1 + frame, fill_opacity=self.zoomed_camera_background_rectangle_fill_opacity ) self.zoomed_camera_background_rectangle_anim = UpdateFromFunc( self.zoomed_camera_background_rectangle, @@ -114,14 +115,18 @@ class NumberlineTransformationScene(ZoomedScene): return dots def get_local_sample_dots(self, x, sample_radius=None, **kwargs): - if sample_radius is None: - sample_radius = self.zoomed_camera.frame.get_width() / 2 zoom_factor = self.get_zoom_factor() + delta_x = kwargs.get("delta_x", self.default_delta_x * zoom_factor) + dot_radius = kwargs.get("dot_radius", self.default_sample_dot_radius * zoom_factor) + + if sample_radius is None: + unrounded_radius = self.zoomed_camera.frame.get_width() / 2 + sample_radius = int(unrounded_radius / delta_x) * delta_x config = { "x_min": x - sample_radius, "x_max": x + sample_radius, - "delta_x": self.default_delta_x * zoom_factor, - "dot_radius": self.default_sample_dot_radius * zoom_factor, + "delta_x": delta_x, + "dot_radius": dot_radius, } config.update(kwargs) return self.get_sample_dots(**config) @@ -183,14 +188,19 @@ class NumberlineTransformationScene(ZoomedScene): ) def apply_function(self, func, + apply_function_to_number_line=True, sample_dots=None, local_sample_dots=None, - target_coordinate_values=None): + target_coordinate_values=None, + added_anims=None + ): zcbr_group = self.zoomed_camera_background_rectangle_group zcbr_anim = self.zoomed_camera_background_rectangle_anim frame = self.zoomed_camera.frame - anims = [self.get_line_mapping_animation(func)] + anims = [] + if apply_function_to_number_line: + anims.append(self.get_line_mapping_animation(func)) if hasattr(self, "mini_line"): # Test for if mini_line is in self? anims.append(self.get_mapping_animation( func, self.mini_line, @@ -218,11 +228,14 @@ class NumberlineTransformationScene(ZoomedScene): ) anims.append(FadeIn(coordinates)) zcbr_group.add(coordinates) + self.local_target_coordinates = coordinates if local_sample_dots: anims.append( self.get_sample_dots_mapping_animation(func, local_sample_dots) ) zcbr_group.add(local_sample_dots) + if added_anims: + anims += added_anims anims.append(Animation(zcbr_group)) self.play(*anims) @@ -272,6 +285,7 @@ class NumberlineTransformationScene(ZoomedScene): ) anims.append(FadeIn(local_coordinates)) zcbr_group.add(local_coordinates) + self.local_coordinates = local_coordinates # Add tiny dots if local_sample_dots is not None: @@ -839,5 +853,215 @@ class IntroduceTransformationView(NumberlineTransformationScene): class TalkThroughXSquaredExample(IntroduceTransformationView): + CONFIG = { + "func": lambda x: x**2, + "number_line_config": { + "x_min": 0, + "x_max": 5, + "unit_size": 1.25, + }, + "output_line_config": { + "x_max": 25, + }, + "default_delta_x": 0.2 + } + + def construct(self): + self.add_title() + self.show_specific_points_mapping() + + def add_title(self): + title = self.title = TextMobject("$f(x) = x^2$") + title.to_edge(UP) + self.add(title) + + def show_specific_points_mapping(self): + # First, just show integers as examples + int_dots = self.get_sample_dots(1, 6, 1) + int_dot_ghosts = int_dots.copy().fade(0.5) + int_arrows = VGroup(*[ + Arrow( + # num.get_bottom(), + self.get_input_point(x), + self.get_output_point(self.func(x)), + buff=MED_SMALL_BUFF + ) + for x, num in zip(range(1, 6), self.input_line.numbers[1:]) + ]) + point_func = self.number_func_to_point_func(self.func) + + numbers = self.input_line.numbers + numbers.next_to(self.input_line, UP, SMALL_BUFF) + self.titles[0].next_to(numbers, UP, MED_SMALL_BUFF, LEFT) + # map(TexMobject.add_background_rectangle, numbers) + # self.add_foreground_mobject(numbers) + + for dot, dot_ghost, arrow in zip(int_dots, int_dot_ghosts, int_arrows): + arrow.match_color(dot) + self.play(DrawBorderThenFill(dot, run_time=1)) + self.add(dot_ghost) + self.play( + GrowArrow(arrow), + dot.apply_function_to_position, point_func + ) + self.wait() + + # Show more sample_dots + sample_dots = self.get_sample_dots() + sample_dot_ghosts = sample_dots.copy().fade(0.5) + + self.play( + LaggedStart(DrawBorderThenFill, sample_dots), + LaggedStart(FadeOut, int_arrows), + ) + self.remove(int_dot_ghosts) + self.add(sample_dot_ghosts) + self.apply_function(self.func, sample_dots=sample_dots) + self.remove(int_dots) + self.wait() + + self.sample_dots = sample_dots + self.sample_dot_ghosts = sample_dot_ghosts + + +class ZoomInOnXSquaredNearOne(TalkThroughXSquaredExample): + def setup(self): + TalkThroughXSquaredExample.setup(self) + self.force_skipping() + self.add_title() + self.show_specific_points_mapping() + self.revert_to_original_skipping_status() + + def construct(self): + zoom_words = TextMobject("Zoomed view \\\\ near 1") + zoom_words.next_to(self.zoomed_display, DOWN) + # zoom_words.shift_onto_screen() + + x = 1 + local_sample_dots = self.get_local_sample_dots(x) + local_coords = self.get_local_coordinate_values(x, dx=0.1) + + zcbr_anim = self.zoomed_camera_background_rectangle_anim + zcbr_group = self.zoomed_camera_background_rectangle_group + frame = self.zoomed_camera.frame + + self.zoom_in_on_input(x, local_sample_dots, local_coords) + self.play(FadeIn(zoom_words)) + self.wait() + local_sample_dots.save_state() + frame.save_state() + self.mini_line.save_state() + sample_dot_ghost_copies = self.sample_dot_ghosts.copy() + self.apply_function( + self.func, + apply_function_to_number_line=False, + local_sample_dots=local_sample_dots, + target_coordinate_values=local_coords + ) + self.remove(sample_dot_ghost_copies) + self.wait() + + # Go back + self.play( + frame.restore, + self.mini_line.restore, + local_sample_dots.restore, + zcbr_anim, + Animation(zcbr_group) + ) + self.wait() + + # Zoom in even more + extra_zoom_factor = 0.3 + one_group = VGroup( + self.local_coordinates.tick_marks[1], + self.local_coordinates.numbers[1], + ) + all_other_coordinates = VGroup( + self.local_coordinates.tick_marks[::2], + self.local_coordinates.numbers[::2], + self.local_target_coordinates, + ) + self.play(frame.scale, extra_zoom_factor) + new_local_sample_dots = self.get_local_sample_dots(x, delta_x=0.005) + new_coordinate_values = self.get_local_coordinate_values(x, dx=0.02) + new_local_coordinates = self.get_local_coordinates( + self.input_line, *new_coordinate_values + ) + + self.play( + Write(new_local_coordinates), + Write(new_local_sample_dots), + one_group.scale, extra_zoom_factor, {"about_point": self.get_input_point(1)}, + FadeOut(all_other_coordinates), + *[ + ApplyMethod(dot.scale, extra_zoom_factor) + for dot in local_sample_dots + ] + ) + self.remove(one_group, local_sample_dots) + zcbr_group.remove( + self.local_coordinates, self.local_target_coordinates, + local_sample_dots + ) + + # Transform new zoomed view + stretch_by_two_words = TextMobject("Stretch by 2") + stretch_by_two_words.scale(0.5) + la, ra = TexMobject("\\leftarrow \\rightarrow") + la.next_to(stretch_by_two_words, LEFT) + ra.next_to(stretch_by_two_words, RIGHT) + stretch_by_two_words.add(la, ra) + stretch_by_two_words.next_to( + self.zoomed_display.get_top(), DOWN + ) + self.add_foreground_mobject(stretch_by_two_words) + sample_dot_ghost_copies = self.sample_dot_ghosts.copy() + self.apply_function( + self.func, + apply_function_to_number_line=False, + sample_dots=sample_dot_ghost_copies, + local_sample_dots=new_local_sample_dots, + target_coordinate_values=new_coordinate_values, + added_anims=[FadeIn(stretch_by_two_words)] + ) + self.remove(sample_dot_ghost_copies) + self.wait() + + # Write derivative + deriv_equation = self.deriv_equation = TexMobject( + "\\frac{df}{dx}(", "1", ")", "=", "2", + tex_to_color_map={"1": RED, "2": RED} + ) + deriv_equation.next_to(self.title, DOWN) + + self.play( + Write(deriv_equation), + self.title.shift, MED_SMALL_BUFF * UP + ) + self.wait() + + +class ZoomInOnXSquaredNearThree(ZoomInOnXSquaredNearOne): + def construct(self): + pass + + +class ZoomInOnXSquaredNearOneFourth(ZoomInOnXSquaredNearOne): + def construct(self): + pass + + +class ZoomInOnXSquaredNearZero(ZoomInOnXSquaredNearOne): + def construct(self): + pass + + +class XSquaredForNegativeInput(TalkThroughXSquaredExample): + CONFIG = { + "input_line_config": {}, + "output_line_config": {}, + } + def construct(self): pass From bf4bac1371536cd4428a3f74c7dd1233118a2408 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 15 May 2018 13:54:04 -0700 Subject: [PATCH 2/7] Simply added self.stem attribute to Arrow --- mobject/geometry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mobject/geometry.py b/mobject/geometry.py index 478c079d..043ee093 100644 --- a/mobject/geometry.py +++ b/mobject/geometry.py @@ -550,6 +550,7 @@ class Arrow(Line): start - perp_vect * width / 2, tip_base - perp_vect * width / 2, ]) + self.stem = self.rect # Alternate name return self def set_tip_points( From c237780f48a889a022dda60935e5bda6b1f96bab Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 15 May 2018 13:54:14 -0700 Subject: [PATCH 3/7] More zooming animations for x^2 example --- active_projects/alt_calc.py | 249 ++++++++++++++++++++++++++++++++---- 1 file changed, 222 insertions(+), 27 deletions(-) diff --git a/active_projects/alt_calc.py b/active_projects/alt_calc.py index 10695f66..dda4ac15 100644 --- a/active_projects/alt_calc.py +++ b/active_projects/alt_calc.py @@ -36,6 +36,7 @@ class NumberlineTransformationScene(ZoomedScene): "run_time": 3, # "path_arc": 30 * DEGREES, }, + "local_coordinate_num_decimal_places": 2, "zoom_factor": 0.1, "zoomed_display_height": 2.5, "zoomed_display_corner_buff": MED_SMALL_BUFF, @@ -244,6 +245,9 @@ class NumberlineTransformationScene(ZoomedScene): def zoom_in_on_input(self, x, local_sample_dots=None, local_coordinate_values=None, + pop_out=True, + first_added_anims=[], + second_added_anims=[], ): input_point = self.get_input_point(x) @@ -292,15 +296,26 @@ class NumberlineTransformationScene(ZoomedScene): anims.append(LaggedStart(GrowFromCenter, local_sample_dots)) zcbr_group.add(local_sample_dots) + if first_added_anims: + anims += first_added_anims + anims.append(Animation(zcbr_group)) + if not pop_out: + self.activate_zooming(animate=False) self.play(*anims) - if not self.zoom_activated: + if not self.zoom_activated and pop_out: self.activate_zooming(animate=False) - self.play(self.get_zoomed_display_pop_out_animation()) + added_anims = second_added_anims or [] + self.play( + self.get_zoomed_display_pop_out_animation(), + *added_anims + ) def get_local_coordinates(self, line, *x_values, **kwargs): - num_decimal_places = kwargs.get("num_decimal_places", 2) + num_decimal_places = kwargs.get( + "num_decimal_places", self.local_coordinate_num_decimal_places + ) result = VGroup() result.tick_marks = VGroup() result.numbers = VGroup() @@ -872,7 +887,7 @@ class TalkThroughXSquaredExample(IntroduceTransformationView): def add_title(self): title = self.title = TextMobject("$f(x) = x^2$") - title.to_edge(UP) + title.to_edge(UP, buff=MED_SMALL_BUFF) self.add(title) def show_specific_points_mapping(self): @@ -923,6 +938,31 @@ class TalkThroughXSquaredExample(IntroduceTransformationView): self.sample_dots = sample_dots self.sample_dot_ghosts = sample_dot_ghosts + def get_stretch_words(self, factor, color=RED, less_than_one=False): + result = TextMobject( + "Scale \\\\ by", str(factor), + tex_to_color_map={str(factor): color} + ) + result.scale(0.7) + la, ra = TexMobject("\\leftarrow \\rightarrow") + if less_than_one: + la, ra = ra, la + la.next_to(result, LEFT) + ra.next_to(result, RIGHT) + result.add(la, ra) + result.next_to( + self.zoomed_display.get_top(), DOWN, SMALL_BUFF + ) + return result + + def get_deriv_equation(self, x, rhs, color=RED): + deriv_equation = self.deriv_equation = TexMobject( + "\\frac{df}{dx}(", str(x), ")", "=", str(rhs), + tex_to_color_map={str(x): color, str(rhs): color} + ) + deriv_equation.next_to(self.title, DOWN, MED_LARGE_BUFF) + return deriv_equation + class ZoomInOnXSquaredNearOne(TalkThroughXSquaredExample): def setup(self): @@ -1006,15 +1046,7 @@ class ZoomInOnXSquaredNearOne(TalkThroughXSquaredExample): ) # Transform new zoomed view - stretch_by_two_words = TextMobject("Stretch by 2") - stretch_by_two_words.scale(0.5) - la, ra = TexMobject("\\leftarrow \\rightarrow") - la.next_to(stretch_by_two_words, LEFT) - ra.next_to(stretch_by_two_words, RIGHT) - stretch_by_two_words.add(la, ra) - stretch_by_two_words.next_to( - self.zoomed_display.get_top(), DOWN - ) + stretch_by_two_words = self.get_stretch_words(2) self.add_foreground_mobject(stretch_by_two_words) sample_dot_ghost_copies = self.sample_dot_ghosts.copy() self.apply_function( @@ -1029,37 +1061,200 @@ class ZoomInOnXSquaredNearOne(TalkThroughXSquaredExample): self.wait() # Write derivative - deriv_equation = self.deriv_equation = TexMobject( - "\\frac{df}{dx}(", "1", ")", "=", "2", - tex_to_color_map={"1": RED, "2": RED} - ) - deriv_equation.next_to(self.title, DOWN) - - self.play( - Write(deriv_equation), - self.title.shift, MED_SMALL_BUFF * UP - ) + deriv_equation = self.get_deriv_equation(1, 2, color=RED) + self.play(Write(deriv_equation)) self.wait() class ZoomInOnXSquaredNearThree(ZoomInOnXSquaredNearOne): + CONFIG = { + "zoomed_display_width": 4, + } + def construct(self): - pass + zoom_words = TextMobject("Zoomed view \\\\ near 3") + zoom_words.next_to(self.zoomed_display, DOWN) + + x = 3 + local_sample_dots = self.get_local_sample_dots(x) + local_coordinate_values = self.get_local_coordinate_values(x, dx=0.1) + target_coordinate_values = self.get_local_coordinate_values(self.func(x), dx=0.1) + + color = self.sample_dots[len(self.sample_dots) / 2].get_color() + sample_dot_ghost_copies = self.sample_dot_ghosts.copy() + stretch_words = self.get_stretch_words(2 * x, color) + deriv_equation = self.get_deriv_equation(x, 2 * x, color) + + self.add(deriv_equation) + self.zoom_in_on_input( + x, + pop_out=False, + local_sample_dots=local_sample_dots, + local_coordinate_values=local_coordinate_values + ) + self.play(Write(zoom_words, run_time=1)) + self.wait() + self.add_foreground_mobject(stretch_words) + self.apply_function( + self.func, + apply_function_to_number_line=False, + sample_dots=sample_dot_ghost_copies, + local_sample_dots=local_sample_dots, + target_coordinate_values=target_coordinate_values, + added_anims=[Write(stretch_words)] + ) + self.wait(2) class ZoomInOnXSquaredNearOneFourth(ZoomInOnXSquaredNearOne): + CONFIG = { + "zoom_factor": 0.01, + "local_coordinate_num_decimal_places": 4, + "zoomed_display_width": 4, + "default_delta_x": 0.25, + } + def construct(self): - pass + # Much copy-pasting from previous scenes. Not great, but + # the fastest way to get the ease-of-tweaking I'd like. + zoom_words = TextMobject("Zoomed view \\\\ near $1/4$") + zoom_words.next_to(self.zoomed_display, DOWN) + + x = 0.25 + local_sample_dots = self.get_local_sample_dots( + x, sample_radius=2.5 * self.zoomed_camera.frame.get_width(), + ) + local_coordinate_values = self.get_local_coordinate_values( + x, dx=0.01, + ) + target_coordinate_values = self.get_local_coordinate_values( + self.func(x), dx=0.01, + ) + + color = RED + sample_dot_ghost_copies = self.sample_dot_ghosts.copy() + stretch_words = self.get_stretch_words("1/2", color, less_than_one=True) + deriv_equation = self.get_deriv_equation("1/4", "1/2", color) + + one_fourth_point = self.get_input_point(x) + one_fourth_arrow = Vector(0.5 * UP, color=WHITE) + one_fourth_arrow.stem.stretch(0.75, 0) + one_fourth_arrow.tip.scale(0.75, about_edge=DOWN) + one_fourth_arrow.next_to(one_fourth_point, DOWN, SMALL_BUFF) + one_fourth_label = TexMobject("0.25") + one_fourth_label.match_height(self.input_line.numbers) + one_fourth_label.next_to(one_fourth_arrow, DOWN, SMALL_BUFF) + + self.add(deriv_equation) + self.zoom_in_on_input( + x, + local_sample_dots=local_sample_dots, + local_coordinate_values=local_coordinate_values, + pop_out=False, + first_added_anims=[ + FadeIn(one_fourth_label), + GrowArrow(one_fourth_arrow), + ] + ) + self.play(Write(zoom_words, run_time=1)) + self.wait() + self.add_foreground_mobject(stretch_words) + self.apply_function( + self.func, + apply_function_to_number_line=False, + sample_dots=sample_dot_ghost_copies, + local_sample_dots=local_sample_dots, + target_coordinate_values=target_coordinate_values, + added_anims=[Write(stretch_words)] + ) + self.wait(2) class ZoomInOnXSquaredNearZero(ZoomInOnXSquaredNearOne): + CONFIG = { + "zoom_factor": 0.1, + "zoomed_display_width": 4, + "scale_by_term": "???", + } + def construct(self): - pass + zoom_words = TextMobject( + "Zoomed %sx \\\\ near 0" % "{:,}".format(int(1.0 / self.zoom_factor)) + ) + zoom_words.next_to(self.zoomed_display, DOWN) + + x = 0 + local_sample_dots = self.get_local_sample_dots( + x, sample_radius=2 * self.zoomed_camera.frame.get_width() + ) + local_coordinate_values = self.get_local_coordinate_values( + x, dx=self.zoom_factor + ) + # target_coordinate_values = self.get_local_coordinate_values( + # self.func(x), dx=self.zoom_factor + # ) + + color = self.sample_dots[len(self.sample_dots) / 2].get_color() + sample_dot_ghost_copies = self.sample_dot_ghosts.copy() + stretch_words = self.get_stretch_words( + self.scale_by_term, color, less_than_one=True + ) + deriv_equation = self.get_deriv_equation(x, 2 * x, color) + + self.add(deriv_equation) + self.zoom_in_on_input( + x, + pop_out=False, + local_sample_dots=local_sample_dots, + local_coordinate_values=local_coordinate_values + ) + self.play(Write(zoom_words, run_time=1)) + self.wait() + self.add_foreground_mobject(stretch_words) + self.apply_function( + self.func, + apply_function_to_number_line=False, + sample_dots=sample_dot_ghost_copies, + local_sample_dots=local_sample_dots, + # target_coordinate_values=target_coordinate_values, + added_anims=[ + Write(stretch_words), + MaintainPositionRelativeTo( + self.local_coordinates, + self.zoomed_camera.frame + ) + ] + ) + self.wait(2) + + +class ZoomInOnXSquared100xZero(ZoomInOnXSquaredNearZero): + CONFIG = { + "zoom_factor": 0.01 + } + + +class ZoomInOnXSquared1000xZero(ZoomInOnXSquaredNearZero): + CONFIG = { + "zoom_factor": 0.001, + "local_coordinate_num_decimal_places": 3, + } + + +class ZoomInOnXSquared10000xZero(ZoomInOnXSquaredNearZero): + CONFIG = { + "zoom_factor": 0.0001, + "local_coordinate_num_decimal_places": 4, + "scale_by_term": "0", + } class XSquaredForNegativeInput(TalkThroughXSquaredExample): CONFIG = { - "input_line_config": {}, + "input_line_config": { + "x_min": -3, + "x_max": 3, + }, "output_line_config": {}, } From 8c516675cb04ce4a9b761eb5a91c37f9d5f86922 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 15 May 2018 19:49:10 -0700 Subject: [PATCH 4/7] StartingCalc101 of alt_calc --- active_projects/alt_calc.py | 429 +++++++++++++++++++++++++++++++++-- constants.py | 1 - old_projects/eoc/chapter3.py | 3 +- 3 files changed, 412 insertions(+), 21 deletions(-) diff --git a/active_projects/alt_calc.py b/active_projects/alt_calc.py index dda4ac15..4aecc459 100644 --- a/active_projects/alt_calc.py +++ b/active_projects/alt_calc.py @@ -132,6 +132,11 @@ class NumberlineTransformationScene(ZoomedScene): config.update(kwargs) return self.get_sample_dots(**config) + def add_sample_dot_ghosts(self, sample_dots, fade_factor=0.5): + self.sample_dot_ghosts = sample_dots.copy() + self.sample_dot_ghosts.fade(fade_factor) + self.add(self.sample_dot_ghosts, sample_dots) + def get_local_coordinate_values(self, x, dx=None, n_neighbors=1): dx = dx or self.default_coordinate_value_dx return [ @@ -435,24 +440,354 @@ class WriteOpeningWords(Scene): class StartingCalc101(PiCreatureScene): CONFIG = { + "camera_config": {"background_opacity": 1}, + "image_frame_width": 3.5, + "image_frame_height": 2.5, } def construct(self): - randy = self.pi_creature - deriv_equation = TexMobject( - "\\frac{df}{dx}(x) = \\lim_{\\Delta x \\to \\infty}" + - "{f(x + \\Delta x) - f(x) \\over \\Delta x}", - tex_to_color_map={"\\Delta x": BLUE} - ) - title = TextMobject("Calculus 101") - title.to_edge(UP) - h_line = Line(LEFT, RIGHT) - h_line.scale_to_fit_width(FRAME_WIDTH - LARGE_BUFF) - h_line.next_to(title, DOWN) + self.show_you() + self.show_images() + self.show_mystery_topic() - self.add(title, h_line) - self.play(randy.change, "erm", title) + def show_you(self): + randy = self.pi_creature + title = self.title = Title("Calculus 101") + you = TextMobject("You") + arrow = Vector(DL, color=WHITE) + arrow.next_to(randy, UR) + you.next_to(arrow.get_start(), UP) + + self.play( + Write(you), + GrowArrow(arrow), + randy.change, "erm", title + ) self.wait() + self.play(Write(title, run_time=1)) + self.play(FadeOut(VGroup(arrow, you))) + + def show_images(self): + randy = self.pi_creature + images = self.get_all_images() + modes = [ + "pondering", # hard_work_image + "pondering", # neat_example_image + "hesitant", # not_so_neat_example_image + "hesitant", # physics_image + "horrified", # piles_of_formulas_image + "horrified", # getting_stuck_image + "thinking", # aha_image + "thinking", # graphical_intuition_image + ] + + for i, image, mode in zip(it.count(), images, modes): + anims = [] + if hasattr(image, "fade_in_anim"): + anims.append(image.fade_in_anim) + anims.append(FadeIn(image.frame)) + else: + anims.append(FadeIn(image)) + + if i >= 3: + image_to_fade_out = images[i - 3] + if hasattr(image_to_fade_out, "fade_out_anim"): + anims.append(image_to_fade_out.fade_out_anim) + else: + anims.append(FadeOut(image_to_fade_out)) + + if hasattr(image, "continual_animations"): + self.add(*image.continual_animations) + + anims.append(ApplyMethod(randy.change, mode)) + self.play(*anims) + self.wait() + if i >= 3: + if hasattr(image_to_fade_out, "continual_animations"): + self.remove(*image_to_fade_out.continual_animations) + self.remove(image_to_fade_out.frame) + self.wait(3) + + self.remaining_images = images[-3:] + + def show_mystery_topic(self): + images = self.remaining_images + randy = self.pi_creature + + mystery_box = Rectangle( + width=self.image_frame_width, + height=self.image_frame_height, + stroke_color=YELLOW, + fill_color=DARK_GREY, + fill_opacity=0.5, + ) + mystery_box.scale(1.5) + mystery_box.next_to(self.title, DOWN, MED_LARGE_BUFF) + + rects = images[-1].rects.copy() + rects.center() + rects.scale_to_fit_height(FRAME_HEIGHT - 1) + # image = rects.get_image() + open_cv_image = cv2.imread(get_full_raster_image_path("alt_calc_hidden_image")) + blurry_iamge = cv2.blur(open_cv_image, (50, 50)) + array = np.array(blurry_iamge)[:, :, ::-1] + im_mob = ImageMobject(array) + im_mob.replace(mystery_box, stretch=True) + mystery_box.add(im_mob) + + q_marks = TexMobject("???").scale(3) + q_marks.space_out_submobjects(1.5) + q_marks.set_stroke(BLACK, 1) + q_marks.move_to(mystery_box) + mystery_box.add(q_marks) + + for image in images: + if hasattr(image, "continual_animations"): + self.remove(*image.continual_animations) + self.play( + image.shift, DOWN, + image.fade, 1, + randy.change, "erm", + run_time=1.5 + ) + self.remove(image) + self.wait() + self.play( + FadeInFromDown(mystery_box), + randy.change, "confused" + ) + self.wait(5) + + # Helpers + + def get_all_images(self): + # Images matched to narration's introductory list + images = VGroup( + self.get_hard_work_image(), + self.get_neat_example_image(), + self.get_not_so_neat_example_image(), + self.get_physics_image(), + self.get_piles_of_formulas_image(), + self.get_getting_stuck_image(), + self.get_aha_image(), + self.get_graphical_intuition_image(), + ) + colors = color_gradient([BLUE, YELLOW], len(images)) + for i, image, color in zip(it.count(), images, colors): + self.adjust_size(image) + frame = Rectangle( + width=self.image_frame_width, + height=self.image_frame_height, + color=color, + stroke_width=2, + ) + frame.move_to(image) + image.frame = frame + image.add(frame) + image.next_to(self.title, DOWN) + alt_i = (i % 3) - 1 + vect = (self.image_frame_width + LARGE_BUFF) * RIGHT + image.shift(alt_i * vect) + return images + + def adjust_size(self, group): + group.scale_to_fit_width(min( + group.get_width(), + self.image_frame_width - 2 * MED_SMALL_BUFF + )) + group.scale_to_fit_height(min( + group.get_height(), + self.image_frame_height - 2 * MED_SMALL_BUFF + )) + return group + + def get_hard_work_image(self): + new_randy = self.pi_creature.copy() + new_randy.change_mode("telepath") + bubble = new_randy.get_bubble(height=3.5, width=4) + bubble.add_content(TexMobject("\\frac{d}{dx}(\\sin(\\sqrt{x}))")) + bubble.add(bubble.content) # Remove? + + return VGroup(new_randy, bubble) + + def get_neat_example_image(self): + filled_circle = Circle( + stroke_width=0, + fill_color=BLUE_E, + fill_opacity=1 + ) + area = TexMobject("\\pi r^2") + area.move_to(filled_circle) + unfilled_circle = Circle( + stroke_width=3, + stroke_color=YELLOW, + fill_opacity=0, + ) + unfilled_circle.next_to(filled_circle, RIGHT) + circles = VGroup(filled_circle, unfilled_circle) + circumference = TexMobject("2\\pi r") + circumference.move_to(unfilled_circle) + equation = TexMobject( + "{d (\\pi r^2) \\over dx} = 2\\pi r", + tex_to_color_map={ + "\\pi r^2": BLUE_D, + "2\\pi r": YELLOW, + } + ) + equation.next_to(circles, UP) + + return VGroup( + filled_circle, area, + unfilled_circle, circumference, + equation + ) + + def get_not_so_neat_example_image(self): + return TexMobject("\\int x \\cos(x) \\, dx") + + def get_physics_image(self): + t_max = 6.5 + r = 0.2 + spring = ParametricFunction( + lambda t: op.add( + r * (np.sin(TAU * t) * RIGHT + np.cos(TAU * t) * UP), + t * DOWN, + ), + t_min=0, t_max=t_max, + color=WHITE, + stroke_width=2, + ) + spring.color_using_background_image("grey_gradient") + + weight = Square() + weight.set_stroke(width=0) + weight.set_fill(opacity=1) + weight.color_using_background_image("grey_gradient") + weight.scale_to_fit_height(0.4) + + t_tracker = ValueTracker(0) + group = VGroup(spring, weight) + group.continual_animations = [ + ContinualUpdateFromTimeFunc( + t_tracker, + lambda tracker, dt: tracker.set_value( + tracker.get_value() + dt + ) + ), + ContinualUpdateFromFunc( + spring, + lambda s: s.stretch_to_fit_height( + 1.5 + 0.5 * np.cos(3 * t_tracker.get_value()), + about_edge=UP + ) + ), + ContinualUpdateFromFunc( + weight, + lambda w: w.move_to(spring.points[-1]) + ) + ] + + def update_group_style(alpha): + spring.set_stroke(width=2 * alpha) + weight.set_fill(opacity=alpha) + + group.fade_in_anim = UpdateFromAlphaFunc( + group, + lambda g, a: update_group_style(a) + ) + group.fade_out_anim = UpdateFromAlphaFunc( + group, + lambda g, a: update_group_style(1 - a) + ) + return group + + def get_piles_of_formulas_image(self): + return TexMobject("(f/g)' = \\frac{gf' - fg'}{g^2}") + + def get_getting_stuck_image(self): + creature = self.pi_creature.copy() + creature.change_mode("angry") + equation = TexMobject("\\frac{d}{dx}(x^x)") + equation.scale_to_fit_height(creature.get_height() / 2) + equation.next_to(creature, RIGHT, aligned_edge=UP) + creature.look_at(equation) + return VGroup(creature, equation) + + def get_aha_image(self): + creature = self.pi_creature.copy() + creature.change_mode("hooray") + from old_projects.eoc.chapter3 import NudgeSideLengthOfCube + scene = NudgeSideLengthOfCube( + end_at_animation_number=7, + skip_animations=True + ) + group = VGroup( + scene.cube, scene.faces, + scene.bars, scene.corner_cube, + ) + group.scale_to_fit_height(0.75 * creature.get_height()) + group.next_to(creature, RIGHT) + creature.look_at(group) + return VGroup(creature, group) + + def get_graphical_intuition_image(self): + gs = GraphScene() + gs.setup_axes() + graph = gs.get_graph( + lambda x: 0.2 * (x - 3) * (x - 5) * (x - 6) + 4, + x_min=2, x_max=8, + ) + rects = gs.get_riemann_rectangles( + graph, x_min=2, x_max=8, + stroke_width=0.5, + dx=0.25 + ) + gs.add(graph, rects, gs.axes) + group = VGroup(*gs.mobjects) + self.adjust_size(group) + group.next_to(self.title, DOWN, MED_LARGE_BUFF) + group.rects = rects + group.continual_animations = [ + NormalAnimationAsContinualAnimation(Write(rects)), + NormalAnimationAsContinualAnimation(ShowCreation(graph)), + NormalAnimationAsContinualAnimation(FadeIn(gs.axes)), + ] + self.adjust_size(group) + return group + + +class GraphicalIntuitions(GraphScene): + CONFIG = { + "func": lambda x: 0.1 * (x - 2) * (x - 5) * (x - 7) + 4, + } + + def construct(self): + self.setup_axes() + graph = self.get_graph(self.func) + self.play( + self.get_graph_words_anim(), + Succession(Write(self.axes), ShowCreation(graph, run_time=2)) + ) + self.wait() + + def get_graph_words_anim(self): + words = VGroup( + TextMobject("Graphs,"), + TextMobject("graphs,"), + TextMobject("non-stop graphs"), + TextMobject("all day"), + TextMobject("every day"), + TextMobject("only graphs."), + ) + words.arrange_submobjects(DOWN) + words.to_edge(UP) + return LaggedStart( + FadeIn, words, + rate_func=there_and_back, + run_time=len(words) - 1, + lag_ratio=0.6, + remover=True + ) class StandardDerivativeVisual(GraphScene): @@ -939,14 +1274,26 @@ class TalkThroughXSquaredExample(IntroduceTransformationView): self.sample_dot_ghosts = sample_dot_ghosts def get_stretch_words(self, factor, color=RED, less_than_one=False): + factor_str = "$%s$" % str(factor) result = TextMobject( - "Scale \\\\ by", str(factor), - tex_to_color_map={str(factor): color} + "Scale \\\\ by", factor_str, + tex_to_color_map={factor_str: color} ) result.scale(0.7) la, ra = TexMobject("\\leftarrow \\rightarrow") if less_than_one: la, ra = ra, la + if factor < 0: + kwargs = { + "path_arc": -np.pi, + "use_rectangular_stem": False, + } + la = Arrow(DOWN, UP, **kwargs) + ra = Arrow(UP, DOWN, **kwargs) + for arrow in la, ra: + arrow.pointwise_become_partial(arrow, 0, 0.9) + arrow.tip.scale(2) + VGroup(la, ra).match_height(result) la.next_to(result, LEFT) ra.next_to(result, RIGHT) result.add(la, ra) @@ -1252,11 +1599,57 @@ class ZoomInOnXSquared10000xZero(ZoomInOnXSquaredNearZero): class XSquaredForNegativeInput(TalkThroughXSquaredExample): CONFIG = { "input_line_config": { - "x_min": -3, - "x_max": 3, + "x_min": -4, + "x_max": 4, }, + "input_line_zero_point": 0.5 * UP + 0 * LEFT, "output_line_config": {}, + "default_mapping_animation_config": { + "path_arc": 30 * DEGREES + }, + "zoomed_display_width": 4, } def construct(self): - pass + self.add_title() + self.show_full_transformation() + self.zoom_in_on_example() + + def show_full_transformation(self): + sample_dots = self.get_sample_dots() + + self.play(LaggedStart(DrawBorderThenFill, sample_dots)) + self.add_sample_dot_ghosts(sample_dots) + self.apply_function(self.func, sample_dots=sample_dots) + self.wait() + + def zoom_in_on_example(self): + x = -2 + + local_sample_dots = self.get_local_sample_dots(x) + local_coordinate_values = self.get_local_coordinate_values( + x, dx=0.1 + ) + target_coordinate_values = self.get_local_coordinate_values( + self.func(x), dx=0.1 + ) + deriv_equation = self.get_deriv_equation(x, 2 * x, color=BLUE) + sample_dot_ghost_copies = self.sample_dot_ghosts.copy() + scale_words = self.get_stretch_words(-4, color=BLUE) + + self.zoom_in_on_input( + x, + local_sample_dots=local_sample_dots, + local_coordinate_values=local_coordinate_values, + ) + self.wait() + self.play(Write(deriv_equation)) + self.add_foreground_mobject(scale_words) + self.play(Write(scale_words)) + self.apply_function( + self.func, + sample_dots=sample_dot_ghost_copies, + local_sample_dots=local_sample_dots, + target_coordinate_values=target_coordinate_values + ) + self.wait() diff --git a/constants.py b/constants.py index c8470519..888b0c31 100644 --- a/constants.py +++ b/constants.py @@ -38,7 +38,6 @@ DEFAULT_POINT_DENSITY_1D = 250 DEFAULT_POINT_THICKNESS = 4 - FRAME_HEIGHT = 8.0 FRAME_WIDTH = FRAME_HEIGHT * DEFAULT_PIXEL_WIDTH / DEFAULT_PIXEL_HEIGHT FRAME_Y_RADIUS = FRAME_HEIGHT / 2 diff --git a/old_projects/eoc/chapter3.py b/old_projects/eoc/chapter3.py index 0617770e..90d0741d 100644 --- a/old_projects/eoc/chapter3.py +++ b/old_projects/eoc/chapter3.py @@ -913,7 +913,7 @@ class NudgeSideLengthOfCube(Scene): df_equation.to_edge(UP) faces_brace = Brace(faces, DOWN) - derivative = faces_brace.get_text("$3x^2", "\\, dx$") + derivative = faces_brace.get_tex("3x^2", "\\, dx") extras_brace = Brace(VGroup(bars, corner_cube), DOWN) ignore_text = extras_brace.get_text( "Multiple \\\\ of $dx^2$" @@ -1149,7 +1149,6 @@ class NudgeSideLengthOfCube(Scene): def get_corner_cube(self): return self.get_prism(self.dx, self.dx, self.dx) - def get_prism(self, width, height, depth): color_kwargs = { "fill_color" : YELLOW, From bf1fbaba7ab9a14d2558db7397c9127fd1154190 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 16 May 2018 00:04:59 -0700 Subject: [PATCH 5/7] Added VFadeIn and VFadeOut --- animation/creation.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/animation/creation.py b/animation/creation.py index 0b32ac75..7172c0c8 100644 --- a/animation/creation.py +++ b/animation/creation.py @@ -160,6 +160,33 @@ class FadeInFromDown(FadeInAndShiftFromDirection): "direction": DOWN, } + +class VFadeIn(Animation): + """ + VFadeIn and VFadeOut only work for VMobjects, but they can be applied + to mobjects while they are being animated in some other way (e.g. shifting + then) in a way that does not work with FadeIn and FadeOut + """ + def update_submobject(self, submobject, starting_submobject, alpha): + submobject.set_stroke( + width=interpolate(0, starting_submobject.get_stroke_width(), alpha) + ) + submobject.set_fill( + opacity=interpolate(0, starting_submobject.get_fill_opacity(), alpha) + ) + + +class VFadeOut(VFadeIn): + CONFIG = { + "remover": True + } + + def update_submobject(self, submobject, starting_submobject, alpha): + VFadeIn.update_submobject( + self, submobject, starting_submobject, 1 - alpha + ) + + # Growing From 309c9f3a575688dfc8b041bf62bca3e585180c0d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 16 May 2018 00:05:19 -0700 Subject: [PATCH 6/7] Added ContinualGrowValue --- continual_animation/update.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/continual_animation/update.py b/continual_animation/update.py index bdc93e98..7e57331e 100644 --- a/continual_animation/update.py +++ b/continual_animation/update.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from continual_animation.continual_animation import ContinualAnimation from animation.update import MaintainPositionRelativeTo +from mobject.value_tracker import ValueTracker class ContinualUpdateFromFunc(ContinualAnimation): @@ -35,3 +36,21 @@ class ContinualMaintainPositionRelativeTo(ContinualAnimation): def update_mobject(self, dt): self.anim.update(0) # 0 is arbitrary + + +# TODO, maybe factor into a different file +class ContinualGrowValue(ContinualAnimation): + CONFIG = { + "rate": 1, + } + + def __init__(self, value_tracker, **kwargs): + if not isinstance(value_tracker, ValueTracker): + raise Exception("ContinualGrowValue must take a ValueTracker as its mobject") + self.value_tracker = value_tracker + ContinualAnimation.__init__(self, value_tracker, **kwargs) + + def update_mobject(self, dt): + self.value_tracker.set_value( + self.value_tracker.get_value() + self.rate * dt + ) From c3e173d2812a9a1fe7e7b81731627492689f6e41 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 16 May 2018 00:12:44 -0700 Subject: [PATCH 7/7] ChangingVectorField of alt_calc --- active_projects/alt_calc.py | 136 +++++++++++++++++++++++++++++++++++- 1 file changed, 134 insertions(+), 2 deletions(-) diff --git a/active_projects/alt_calc.py b/active_projects/alt_calc.py index 4aecc459..2ffb320e 100644 --- a/active_projects/alt_calc.py +++ b/active_projects/alt_calc.py @@ -759,14 +759,56 @@ class StartingCalc101(PiCreatureScene): class GraphicalIntuitions(GraphScene): CONFIG = { "func": lambda x: 0.1 * (x - 2) * (x - 5) * (x - 7) + 4, + "x_labeled_nums": range(1, 10), } def construct(self): self.setup_axes() + axes = self.axes graph = self.get_graph(self.func) + + ss_group = self.get_secant_slope_group( + x=2, graph=graph, dx=0.01, + secant_line_length=6, + secant_line_color=RED, + ) + rects = self.get_riemann_rectangles( + graph, x_min=2, x_max=8, dx=0.01, stroke_width=0 + ) + + deriv_text = TextMobject( + "Derivative $\\rightarrow$ slope", + tex_to_color_map={"slope": ss_group.secant_line.get_color()} + ) + deriv_text.to_edge(UP) + integral_text = TextMobject( + "Integral $\\rightarrow$ area", + tex_to_color_map={"area": rects[0].get_color()} + ) + integral_text.next_to(deriv_text, DOWN) + self.play( + Succession(Write(axes), ShowCreation(graph, run_time=2)), self.get_graph_words_anim(), - Succession(Write(self.axes), ShowCreation(graph, run_time=2)) + ) + self.animate_secant_slope_group_change( + ss_group, + target_x=8, + rate_func=there_and_back, + run_time=5, + added_anims=[ + Write(deriv_text), + VFadeIn(ss_group, run_time=2), + ] + ) + self.play(FadeIn(integral_text)) + self.play( + LaggedStart( + GrowFromEdge, rects, + lambda r: (r, DOWN) + ), + Animation(axes), + Animation(graph), ) self.wait() @@ -777,8 +819,10 @@ class GraphicalIntuitions(GraphScene): TextMobject("non-stop graphs"), TextMobject("all day"), TextMobject("every day"), - TextMobject("only graphs."), + TextMobject("as if to visualize is to graph"), ) + for word in words: + word.add_background_rectangle() words.arrange_submobjects(DOWN) words.to_edge(UP) return LaggedStart( @@ -790,6 +834,94 @@ class GraphicalIntuitions(GraphScene): ) +class Wrapper(Scene): + CONFIG = { + "title": "", + "title_kwargs": {}, + "screen_height": 6, + "wait_time": 2, + } + + def construct(self): + rect = ScreenRectangle(height=self.screen_height) + title = TextMobject(self.title, **self.title_kwargs) + title.to_edge(UP) + rect.next_to(title, DOWN) + + self.add(title) + self.play(ShowCreation(rect)) + self.wait(self.wait_time) + + +class DomainColoringWrapper(Wrapper): + CONFIG = { + "title": "Complex $\\rightarrow$ Complex", + } + + +class ChangingVectorFieldWrapper(Wrapper): + CONFIG = {"title": "$(x, y, t) \\rightarrow (x', y')$"} + + +class ChangingVectorField(Scene): + def construct(self): + plane = self.plane = NumberPlane() + plane.set_stroke(width=2) + plane.add_coordinates() + self.add(plane) + + time_tracker = self.time_tracker = ValueTracker(0) + self.add(ContinualGrowValue(time_tracker)) + + vectors = self.get_vectors() + self.add(ContinualUpdateFromFunc( + vectors, + lambda vs: self.update_vectors(vs) + )) + self.wait(15) + + def get_vectors(self): + vectors = VGroup() + x_max = int(np.ceil(FRAME_WIDTH)) + y_max = int(np.ceil(FRAME_HEIGHT)) + step = 0.5 + for x in np.arange(-x_max, x_max + 1, step): + for y in np.arange(-y_max, y_max + 1, step): + point = x * RIGHT + y * UP + vectors.add(Vector(RIGHT).shift(point)) + vectors.set_color_by_gradient(YELLOW, RED) + return vectors + + def update_vectors(self, vectors): + time = self.time_tracker.get_value() + for vector in vectors: + point = vector.get_start() + out_point = self.func(point, time) + norm = np.linalg.norm(out_point) + if norm == 0: + out_point = RIGHT # Fake it + vector.set_fill(opacity=0) + else: + alpha = sigmoid(2 * norm - 1) + out_point *= 0.4 / norm + color = interpolate_color(BLUE, RED, alpha) + vector.set_fill(color, opacity=1) + vector.set_stroke(BLACK, width=1) + new_x, new_y = out_point[:2] + vector.put_start_and_end_on( + point, point + new_x * RIGHT + new_y * UP + ) + + def func(self, point, time): + x, y, z = point + time += 5 + return np.array([ + np.sin(time) * np.sin((y * x**2 + 0.9 * x + 0.8 * y) / 10) + 0.1, + np.cos(time) * np.sin((y / (0.8 * x + 1)) / 10) + 0.1, + 0 + ]) + + class StandardDerivativeVisual(GraphScene): CONFIG = { "y_max": 8,