From 84a950425cae19794d66df999e2b5b19f8b559d4 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 21 Mar 2019 09:42:06 -0700 Subject: [PATCH 01/27] DashedVMobject should handle case when num_dashes=0 --- manimlib/mobject/types/vectorized_mobject.py | 26 ++++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 433f9f67..12380786 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -918,22 +918,22 @@ class DashedVMobject(VMobject): VMobject.__init__(self, **kwargs) num_dashes = self.num_dashes ps_ratio = self.positive_space_ratio + if num_dashes > 0: + # End points of the unit interval for division + alphas = np.linspace(0, 1, num_dashes + 1) - # End points of the unit interval for division - alphas = np.linspace(0, 1, num_dashes + 1) + # This determines the length of each "dash" + full_d_alpha = (1.0 / num_dashes) + partial_d_alpha = full_d_alpha * ps_ratio - # This determines the length of each "dash" - full_d_alpha = (1.0 / num_dashes) - partial_d_alpha = full_d_alpha * ps_ratio + # Rescale so that the last point of vmobject will + # be the end of the last dash + alphas /= (1 - full_d_alpha + partial_d_alpha) - # Rescale so that the last point of vmobject will - # be the end of the last dash - alphas /= (1 - full_d_alpha + partial_d_alpha) - - self.add(*[ - vmobject.get_subcurve(alpha, alpha + partial_d_alpha) - for alpha in alphas[:-1] - ]) + self.add(*[ + vmobject.get_subcurve(alpha, alpha + partial_d_alpha) + for alpha in alphas[:-1] + ]) # Family is already taken care of by get_subcurve # implementation self.match_style(vmobject, family=False) From 573abe82b8ed7197170106a3a171281fc565492c Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 21 Mar 2019 10:52:43 -0700 Subject: [PATCH 02/27] AnalyzePendulumForce --- active_projects/ode/all_part1_scenes.py | 1 + active_projects/ode/part1/pendulum.py | 254 +++++++++++++++++++++--- 2 files changed, 225 insertions(+), 30 deletions(-) diff --git a/active_projects/ode/all_part1_scenes.py b/active_projects/ode/all_part1_scenes.py index ebcb0d3d..e3ffcfe4 100644 --- a/active_projects/ode/all_part1_scenes.py +++ b/active_projects/ode/all_part1_scenes.py @@ -18,5 +18,6 @@ ALL_SCENE_CLASSES = [ StrogatzQuote, # Something... ShowGravityAcceleration, + AnalyzePendulumForce, BuildUpEquation, ] diff --git a/active_projects/ode/part1/pendulum.py b/active_projects/ode/part1/pendulum.py index 9627b3b9..35e53df2 100644 --- a/active_projects/ode/part1/pendulum.py +++ b/active_projects/ode/part1/pendulum.py @@ -110,17 +110,21 @@ class Pendulum(VGroup): return self def add_theta_label(self): - label = self.theta_label = TexMobject("\\theta") - label.set_height(self.theta_label_height) + self.theta_label = always_redraw(self.get_label) + self.add(self.theta_label) - def update_label(l): - top = self.get_fixed_point() - arc_center = self.angle_arc.point_from_proportion(0.5) - vect = arc_center - top - vect = normalize(vect) * (1 + self.theta_label_height) - l.move_to(top + vect) - label.add_updater(update_label) - self.add(label) + def get_label(self): + label = TexMobject("\\theta") + label.set_height(min( + self.theta_label_height, + self.angle_arc.get_width(), + )) + top = self.get_fixed_point() + arc_center = self.angle_arc.point_from_proportion(0.5) + vect = arc_center - top + vect = normalize(vect) * (1 + self.theta_label_height) + label.move_to(top + vect) + return label # def get_theta(self): @@ -841,12 +845,12 @@ class VeryLowAnglePendulum(LowAnglePendulum): } -class BuildUpEquation(MovingCameraScene): +class AnalyzePendulumForce(MovingCameraScene): CONFIG = { "pendulum_config": { "length": 5, - "top_point": 3 * UP, - "initial_theta": 45 * DEGREES, + "top_point": 3.5 * UP, + "initial_theta": 60 * DEGREES, }, "g_vect_config": { "length_multiple": 0.25, @@ -859,15 +863,12 @@ class BuildUpEquation(MovingCameraScene): self.add_pendulum() self.show_constraint() self.break_g_vect_into_components() - self.show_angle_geometry() self.show_gsin_formula() self.show_acceleration_at_different_angles() + self.show_angle_geometry() self.ask_about_what_to_do() - self.show_velocity_and_position() - self.show_derivatives() - self.show_equation() - self.talk_about_sine_component() - self.add_air_resistance() + # TODO, indication of theta? + # pendulum movement? Things like that? def add_pendulum(self): self.pendulum = Pendulum(**self.pendulum_config) @@ -922,9 +923,9 @@ class BuildUpEquation(MovingCameraScene): ShowCreation(arc) ) arcs.add(arc) - pendulum.clear_updaters() self.wait() - self.play(FadeOut(arc)) + + self.theta_tracker = theta_tracker def break_g_vect_into_components(self): g_vect = self.g_vect @@ -945,25 +946,218 @@ class BuildUpEquation(MovingCameraScene): color=self.perp_line_color, )) - self.play( - ShowCreation(g_vect.component_lines), - ) + self.play(ShowCreation(g_vect.component_lines)) self.play(GrowArrow(g_vect.tangent)) self.wait() self.play(GrowArrow(g_vect.perp)) self.wait() - def show_angle_geometry(self): - g_vect = self.g_vect - def show_gsin_formula(self): - pass + g_vect = self.g_vect + g_word = self.g_word + g_word.clear_updaters() + + g_term = self.g_term = TexMobject("-g") + g_term.add_updater(lambda m: m.next_to( + g_vect, + RIGHT * np.sign(self.pendulum.get_theta()), + SMALL_BUFF + )) + + def create_vect_label(vect, tex, direction): + label = TexMobject(tex) + label.set_stroke(BLACK, 5, background=True) + label.add_background_rectangle() + label.scale(0.7) + max_width = 0.9 * vect.get_length() + if label.get_width() > max_width: + label.set_width(max_width) + angle = vect.get_angle() + angle = (angle + PI / 2) % PI - PI / 2 + label.next_to(ORIGIN, direction, SMALL_BUFF) + label.rotate(angle, about_point=ORIGIN) + label.shift(vect.get_center()) + return label + + g_sin_label = always_redraw(lambda: create_vect_label( + g_vect.tangent, "-g\\sin(\\theta)", UP, + )) + g_cos_label = always_redraw(lambda: create_vect_label( + g_vect.perp, "-g\\cos(\\theta)", DOWN, + )) + + self.play(ReplacementTransform(g_word, g_term)) + self.wait() + self.play(TransformFromCopy(g_term, g_sin_label)) + self.wait() + self.play(TransformFromCopy(g_term, g_cos_label)) + self.wait() + + self.g_sin_label = g_sin_label + self.g_cos_label = g_cos_label def show_acceleration_at_different_angles(self): - pass + to_fade = VGroup( + self.g_cos_label, + self.g_vect.perp, + self.g_vect.component_lines, + ) + + self.play(to_fade.set_opacity, 0.25) + for mob in to_fade: + mob.add_updater(lambda m: m.set_opacity(0.25)) + + get_theta = self.pendulum.get_theta + theta_decimal = DecimalNumber(include_sign=True) + theta_decimal.add_updater(lambda d: d.set_value( + get_theta() + )) + theta_decimal.add_updater(lambda m: m.next_to( + self.pendulum.theta_label, DOWN + )) + theta_decimal.add_updater(lambda m: m.set_color( + GREEN if get_theta() > 0 else RED + )) + + self.set_theta(0) + self.wait(2) + self.set_theta(89.9 * DEGREES, run_time=3) + self.wait(2) + self.set_theta(60 * DEGREES, run_time=2) + self.wait() + + self.play(FadeInFrom(theta_decimal, UP)) + self.set_theta(-60 * DEGREES, run_time=4) + self.set_theta(60 * DEGREES, run_time=4) + self.play(FadeOut(theta_decimal)) + + def show_angle_geometry(self): + g_vect = self.g_vect + vectors = VGroup( + g_vect, g_vect.tangent, g_vect.perp, + ) + g_line = Line( + g_vect.get_start(), + g_vect.get_end(), + stroke_width=1, + stroke_color=WHITE, + ) + + arc = Arc( + start_angle=90 * DEGREES, + angle=self.pendulum.get_theta(), + radius=0.5, + arc_center=g_vect.get_end(), + ) + q_mark = TexMobject("?") + q_mark.next_to(arc.get_center(), UL, SMALL_BUFF) + theta_label = TexMobject("\\theta") + theta_label.move_to(q_mark) + + opposite = g_vect.component_lines[0].copy() + adjascent = g_line.copy() + opposite.set_stroke(BLUE, 5, opacity=1) + adjascent.set_stroke(YELLOW, 5) + + vectors.save_state() + vectors.clear_updaters() + self.add(g_line, g_vect) + self.play(vectors.set_opacity, 0.3) + self.play( + ShowCreation(arc), + Write(q_mark) + ) + self.play(ShowCreationThenFadeAround(q_mark)) + self.wait() + self.play(ShowCreationThenFadeAround( + self.pendulum.theta_label + )) + self.play( + TransformFromCopy( + self.pendulum.theta_label, + theta_label, + ), + FadeOut(q_mark) + ) + self.wait() + self.play(ShowCreation(opposite)) + self.play(ShowCreation(adjascent)) + self.wait() + self.play( + FadeOut(opposite), + FadeOut(adjascent), + ) + self.play( + Restore(vectors), + FadeOut(arc), + FadeOut(theta_label), + ) + + # vectors.resume_updating() def ask_about_what_to_do(self): - pass + g_vect = self.g_vect + g_sin_label = self.g_sin_label + angle = g_vect.tangent.get_angle() + angle = (angle - PI) % TAU + + randy = You() + randy.to_corner(DL) + bubble = randy.get_bubble( + height=2, + width=2, + ) + thought_term = g_sin_label.copy() + thought_term.rotate(-angle) + thought_term.move_to(bubble.get_bubble_center()) + rect = SurroundingRectangle(thought_term) + rect.rotate(angle) + rect.move_to(g_sin_label) + + randy.save_state() + randy.fade(1) + self.play(randy.restore, randy.change, "pondering") + self.play(ShowCreationThenFadeOut(rect)) + self.play( + ShowCreation(bubble), + TransformFromCopy(g_sin_label, thought_term), + randy.look_at, bubble, + ) + self.play(Blink(randy)) + self.wait() + thought_term.remove(thought_term[0]) + self.play( + ShowCreationThenDestruction( + thought_term.copy().set_style( + stroke_color=YELLOW, + stroke_width=2, + fill_opacity=0, + ), + run_time=2 + ) + ) + self.play( + randy.change, "confused", thought_term, + ) + self.play(Blink(randy)) + + # + def set_theta(self, value, *added_anims, **kwargs): + kwargs["run_time"] = kwargs.get("run_time", 2) + self.play( + self.theta_tracker.set_value, value, + *added_anims, + **kwargs, + ) + + +class BuildUpEquation(Scene): + def construct(self): + self.show_velocity_and_position() + self.show_derivatives() + self.show_equation() + self.talk_about_sine_component() + self.add_air_resistance() def show_velocity_and_position(self): pass From e567fff8553730d12435b22607b008af4d81530d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 21 Mar 2019 17:49:05 -0700 Subject: [PATCH 03/27] Experimenting with applying rate_func at the submobject level. Admittedly, I haven't thought it through that much, but I suspect this will make things work better --- manimlib/animation/animation.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/manimlib/animation/animation.py b/manimlib/animation/animation.py index 17ea65fa..dbb0f99d 100644 --- a/manimlib/animation/animation.py +++ b/manimlib/animation/animation.py @@ -108,7 +108,8 @@ class Animation(object): # Methods for interpolation, the mean of an Animation def interpolate(self, alpha): alpha = np.clip(alpha, 0, 1) - self.interpolate_mobject(self.rate_func(alpha)) + # self.interpolate_mobject(self.rate_func(alpha)) + self.interpolate_mobject(alpha) # TODO, check def update(self, alpha): """ @@ -121,7 +122,10 @@ class Animation(object): families = list(self.get_all_families_zipped()) for i, mobs in enumerate(families): sub_alpha = self.get_sub_alpha(alpha, i, len(families)) - self.interpolate_submobject(*mobs, sub_alpha) + self.interpolate_submobject( + *mobs, + self.rate_func(sub_alpha) # TODO, check + ) def interpolate_submobject(self, submobject, starting_sumobject, alpha): # Typically ipmlemented by subclass From f4c2c4642a4f1e812a9c9d0b88ce9f52387b721f Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 21 Mar 2019 17:50:04 -0700 Subject: [PATCH 04/27] Fix bug with applying function to VectorizedPoint --- manimlib/mobject/types/vectorized_mobject.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 12380786..640a40d1 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -577,6 +577,8 @@ class VMobject(Mobject): 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 From bc74e8a372f81af51383e10a5aa124e3b33b4d2f Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 21 Mar 2019 17:50:17 -0700 Subject: [PATCH 05/27] AnalyzePendulumForce --- active_projects/ode/all_part1_scenes.py | 3 + active_projects/ode/part1/pendulum.py | 570 +++++++++++++++++++++--- active_projects/ode/part1/pi_scenes.py | 2 +- active_projects/ode/part1/staging.py | 10 + 4 files changed, 530 insertions(+), 55 deletions(-) diff --git a/active_projects/ode/all_part1_scenes.py b/active_projects/ode/all_part1_scenes.py index e3ffcfe4..2ee2da52 100644 --- a/active_projects/ode/all_part1_scenes.py +++ b/active_projects/ode/all_part1_scenes.py @@ -14,10 +14,13 @@ ALL_SCENE_CLASSES = [ SomeOfYouWatching, SmallAngleApproximationTex, VeryLowAnglePendulum, + FormulasAreLies, FollowThisThread, StrogatzQuote, # Something... ShowGravityAcceleration, AnalyzePendulumForce, BuildUpEquation, + ShowDerivativeVideo, + SubtleAirCurrents, ] diff --git a/active_projects/ode/part1/pendulum.py b/active_projects/ode/part1/pendulum.py index 35e53df2..4e59a04c 100644 --- a/active_projects/ode/part1/pendulum.py +++ b/active_projects/ode/part1/pendulum.py @@ -41,6 +41,7 @@ class Pendulum(VGroup): "color": RED, }, "theta_label_height": 0.25, + "set_theta_label_height_cap": False, "n_steps_per_frame": 100, } @@ -115,10 +116,11 @@ class Pendulum(VGroup): def get_label(self): label = TexMobject("\\theta") - label.set_height(min( - self.theta_label_height, - self.angle_arc.get_width(), - )) + label.set_height(self.theta_label_height) + if self.set_theta_label_height_cap: + max_height = self.angle_arc.get_width() + if label.get_height() > max_height: + label.set_height(max_height) top = self.get_fixed_point() arc_center = self.angle_arc.point_from_proportion(0.5) vect = arc_center - top @@ -851,6 +853,7 @@ class AnalyzePendulumForce(MovingCameraScene): "length": 5, "top_point": 3.5 * UP, "initial_theta": 60 * DEGREES, + "set_theta_label_height_cap": True, }, "g_vect_config": { "length_multiple": 0.25, @@ -861,22 +864,33 @@ class AnalyzePendulumForce(MovingCameraScene): def construct(self): self.add_pendulum() + self.add_g_vect() self.show_constraint() self.break_g_vect_into_components() self.show_gsin_formula() self.show_acceleration_at_different_angles() self.show_angle_geometry() self.ask_about_what_to_do() - # TODO, indication of theta? - # pendulum movement? Things like that? + + self.emphasize_theta() + self.show_arc_length() + self.show_angular_velocity() + self.show_angular_acceleration() + self.circle_g_sin_formula() def add_pendulum(self): - self.pendulum = Pendulum(**self.pendulum_config) - self.add(self.pendulum) + pendulum = Pendulum(**self.pendulum_config) + theta_tracker = ValueTracker(pendulum.get_theta()) + pendulum.add_updater(lambda p: p.set_theta( + theta_tracker.get_value() + )) - def show_constraint(self): + self.add(pendulum) + self.pendulum = pendulum + self.theta_tracker = theta_tracker + + def add_g_vect(self): pendulum = self.pendulum - weight = pendulum.weight g_vect = self.g_vect = GravityVector( pendulum, **self.g_vect_config, @@ -888,24 +902,20 @@ class AnalyzePendulumForce(MovingCameraScene): g_vect, RIGHT, buff=-SMALL_BUFF, )) - theta_tracker = ValueTracker(pendulum.get_theta()) - - p = weight.get_center() - path = CubicBezier([p, p + 3 * DOWN, p + 3 * UP, p]) - - g_word.suspend_updating() self.play( GrowArrow(g_vect), FadeInFrom(g_word, UP, lag_ratio=0.1), ) - g_word.resume_updating() + + def show_constraint(self): + pendulum = self.pendulum + weight = pendulum.weight + p = weight.get_center() + path = CubicBezier([p, p + 3 * DOWN, p + 3 * UP, p]) self.play(MoveAlongPath(weight, path, run_time=2)) self.wait() - pendulum.add_updater(lambda p: p.set_theta( - theta_tracker.get_value() - )) arcs = VGroup() for u in [-1, 2, -1]: d_theta = 40 * DEGREES * u @@ -915,17 +925,17 @@ class AnalyzePendulumForce(MovingCameraScene): radius=pendulum.length, arc_center=pendulum.get_fixed_point(), stroke_width=2, - stroke_color=RED, + stroke_color=GREEN, stroke_opacity=0.5, ) self.play( - theta_tracker.increment_value, d_theta, + self.theta_tracker.increment_value, d_theta, ShowCreation(arc) ) arcs.add(arc) self.wait() - self.theta_tracker = theta_tracker + self.traced_arcs = arcs def break_g_vect_into_components(self): g_vect = self.g_vect @@ -960,13 +970,13 @@ class AnalyzePendulumForce(MovingCameraScene): g_term = self.g_term = TexMobject("-g") g_term.add_updater(lambda m: m.next_to( g_vect, - RIGHT * np.sign(self.pendulum.get_theta()), + RIGHT if self.pendulum.get_theta() >= 0 else LEFT, SMALL_BUFF )) def create_vect_label(vect, tex, direction): label = TexMobject(tex) - label.set_stroke(BLACK, 5, background=True) + label.set_stroke(width=0, background=True) label.add_background_rectangle() label.scale(0.7) max_width = 0.9 * vect.get_length() @@ -986,12 +996,22 @@ class AnalyzePendulumForce(MovingCameraScene): g_vect.perp, "-g\\cos(\\theta)", DOWN, )) - self.play(ReplacementTransform(g_word, g_term)) - self.wait() - self.play(TransformFromCopy(g_term, g_sin_label)) - self.wait() - self.play(TransformFromCopy(g_term, g_cos_label)) + self.play( + ReplacementTransform(g_word[0][0], g_term[0][1]), + FadeOut(g_word[0][1:]), + Write(g_term[0][0]), + ) + self.add(g_term) self.wait() + for label in g_sin_label, g_cos_label: + self.play( + GrowFromPoint(label[0], g_term.get_center()), + TransformFromCopy(g_term, label[1][:2]), + GrowFromPoint(label[1][2:], g_term.get_center()), + remover=True + ) + self.add(label) + self.wait() self.g_sin_label = g_sin_label self.g_cos_label = g_cos_label @@ -1000,10 +1020,20 @@ class AnalyzePendulumForce(MovingCameraScene): to_fade = VGroup( self.g_cos_label, self.g_vect.perp, - self.g_vect.component_lines, ) + new_comp_line_sytle = { + "stroke_width": 0.5, + "stroke_opacity": 0.25, + } - self.play(to_fade.set_opacity, 0.25) + self.play( + to_fade.set_opacity, 0.25, + self.g_vect.component_lines.set_style, + new_comp_line_sytle + ) + self.g_vect.component_lines.add_updater( + lambda m: m.set_style(**new_comp_line_sytle) + ) for mob in to_fade: mob.add_updater(lambda m: m.set_opacity(0.25)) @@ -1054,13 +1084,15 @@ class AnalyzePendulumForce(MovingCameraScene): theta_label = TexMobject("\\theta") theta_label.move_to(q_mark) - opposite = g_vect.component_lines[0].copy() + opposite = Line( + *g_vect.component_lines[0].get_start_and_end() + ) adjascent = g_line.copy() opposite.set_stroke(BLUE, 5, opacity=1) adjascent.set_stroke(YELLOW, 5) vectors.save_state() - vectors.clear_updaters() + vectors.suspend_updating() self.add(g_line, g_vect) self.play(vectors.set_opacity, 0.3) self.play( @@ -1089,11 +1121,12 @@ class AnalyzePendulumForce(MovingCameraScene): ) self.play( Restore(vectors), + FadeOut(g_line), FadeOut(arc), FadeOut(theta_label), ) - # vectors.resume_updating() + vectors.resume_updating() def ask_about_what_to_do(self): g_vect = self.g_vect @@ -1105,12 +1138,19 @@ class AnalyzePendulumForce(MovingCameraScene): randy.to_corner(DL) bubble = randy.get_bubble( height=2, - width=2, + width=3.5, ) - thought_term = g_sin_label.copy() - thought_term.rotate(-angle) + g_sin_copy = g_sin_label.copy() + g_sin_copy.remove(g_sin_copy[0]) + g_sin_copy.generate_target() + g_sin_copy.target.scale(1 / 0.75) + g_sin_copy.target.rotate(-angle) + a_eq = TexMobject("a=") + thought_term = VGroup(a_eq, g_sin_copy.target) + thought_term.arrange(RIGHT, buff=SMALL_BUFF) thought_term.move_to(bubble.get_bubble_center()) - rect = SurroundingRectangle(thought_term) + + rect = SurroundingRectangle(g_sin_copy.target) rect.rotate(angle) rect.move_to(g_sin_label) @@ -1120,12 +1160,14 @@ class AnalyzePendulumForce(MovingCameraScene): self.play(ShowCreationThenFadeOut(rect)) self.play( ShowCreation(bubble), - TransformFromCopy(g_sin_label, thought_term), + Write(a_eq), + MoveToTarget(g_sin_copy), randy.look_at, bubble, ) + thought_term.remove(g_sin_copy.target) + thought_term.add(g_sin_copy) self.play(Blink(randy)) self.wait() - thought_term.remove(thought_term[0]) self.play( ShowCreationThenDestruction( thought_term.copy().set_style( @@ -1133,13 +1175,129 @@ class AnalyzePendulumForce(MovingCameraScene): stroke_width=2, fill_opacity=0, ), - run_time=2 - ) - ) - self.play( + run_time=2, + lag_ratio=0.2, + ), randy.change, "confused", thought_term, ) self.play(Blink(randy)) + self.play( + FadeOut(randy), + FadeOut(bubble), + thought_term.next_to, self.pendulum, DOWN, LARGE_BUFF + ) + + def emphasize_theta(self): + pendulum = self.pendulum + + self.play(FocusOn(pendulum.theta_label)) + self.play(Indicate(pendulum.theta_label)) + old_updaters = pendulum.get_updaters() + pendulum.clear_updaters(recursive=False) + pendulum.start_swinging() + self.wait(5) + pendulum.clear_updaters() + for updater in old_updaters: + pendulum.add_updater(updater) + + def show_arc_length(self): + pendulum = self.pendulum + to_fade = VGroup( + self.g_vect, + self.g_term, + self.g_vect.component_lines, + self.g_vect.tangent, + self.g_vect.perp, + self.g_sin_label, + self.g_cos_label, + ) + angle = pendulum.get_theta() + height = pendulum.length + top = pendulum.get_fixed_point() + + line = Line(UP, DOWN) + line.set_height(height) + line.move_to(top, UP) + arc = Arc( + start_angle=-90 * DEGREES, + angle=angle, + arc_center=top, + radius=height, + stroke_color=GREEN, + ) + + brace = Brace(Line(ORIGIN, 5 * UP), RIGHT) + brace.point = VectorizedPoint(brace.get_right()) + brace.add(brace.point) + brace.set_height(angle) + brace.move_to(ORIGIN, DL) + brace.apply_complex_function(np.exp) + brace.scale(height) + brace.rotate(-90 * DEGREES) + brace.move_to(arc) + brace.shift(MED_SMALL_BUFF * normalize( + arc.point_from_proportion(0.5) - top + )) + x_sym = TexMobject("x") + x_sym.set_color(GREEN) + x_sym.next_to(brace.point, DR, buff=SMALL_BUFF) + + rhs = TexMobject("=", "L", "\\theta") + rhs.set_color_by_tex("\\theta", BLUE) + rhs.next_to(x_sym, RIGHT) + rhs.shift(0.7 * SMALL_BUFF * UP) + line_L = TexMobject("L") + line_L.next_to( + pendulum.rod.get_center(), UR, SMALL_BUFF, + ) + + if hasattr(self, "traced_arcs"): + self.play(FadeOut(self.traced_arcs)) + self.play(FadeOut(to_fade)) + self.play( + ShowCreation(arc), + Rotate(line, angle, about_point=top), + UpdateFromAlphaFunc( + line, lambda m, a: m.set_stroke( + width=2 * there_and_back(a) + ) + ), + GrowFromPoint( + brace, line.get_bottom(), + path_arc=angle + ), + ) + self.play(FadeInFrom(x_sym, UP)) + self.wait() + + # Show equation + line.set_stroke(BLUE, 5) + self.play( + ShowCreationThenFadeOut(line), + FadeInFromDown(line_L) + ) + self.play( + TransformFromCopy( + line_L, rhs.get_part_by_tex("L") + ), + Write(rhs.get_part_by_tex("=")) + ) + self.play( + TransformFromCopy( + pendulum.theta_label, + rhs.get_parts_by_tex("\\theta"), + ) + ) + self.add(rhs) + + def show_angular_velocity(self): + pass + + def show_angular_acceleration(self): + pass + + def circle_g_sin_formula(self): + pass # def set_theta(self, value, *added_anims, **kwargs): @@ -1152,27 +1310,331 @@ class AnalyzePendulumForce(MovingCameraScene): class BuildUpEquation(Scene): + CONFIG = { + "tex_config": { + "tex_to_color_map": { + "{a}": YELLOW, + "{v}": RED, + "{x}": GREEN, + "\\theta": BLUE, + "{L}": WHITE, + } + } + } + def construct(self): - self.show_velocity_and_position() + self.add_center_line() self.show_derivatives() - self.show_equation() + self.show_theta_double_dot_equation() self.talk_about_sine_component() self.add_air_resistance() - def show_velocity_and_position(self): - pass + def add_center_line(self): + line = Line(UP, DOWN) + line.set_height(FRAME_HEIGHT) + line.set_stroke(WHITE, 1) + self.add(line) def show_derivatives(self): - pass + a_eq = TexMobject( + "{a}", "=", "{d{v} \\over dt}", + **self.tex_config, + ) + v_eq = TexMobject( + "{v}", "=", "{d{x} \\over dt}", + **self.tex_config, + ) + x_eq = TexMobject( + "{x} = {L} \\theta", + **self.tex_config, + ) + eqs = VGroup(a_eq, v_eq, x_eq) + eqs.arrange(DOWN, buff=LARGE_BUFF) + eqs.to_corner(UL) - def show_equation(self): - pass + v_rhs = TexMobject( + "={L}{d\\theta \\over dt}", + "=", "{L}\\dot{\\theta}", + **self.tex_config, + ) + + v_rhs.next_to(v_eq, RIGHT, SMALL_BUFF) + v_rhs.shift( + UP * (v_eq[1].get_bottom()[1] - v_rhs[0].get_bottom()[1]) + ) + a_rhs = TexMobject( + "={L}{d", "\\dot{\\theta}", "\\over dt}", + "=", "{L}\\ddot{\\theta}", + **self.tex_config, + ) + a_rhs.next_to(a_eq, RIGHT, SMALL_BUFF) + a_rhs.shift( + UP * (a_eq[1].get_bottom()[1] - a_rhs[0].get_bottom()[1]) + ) + + # a_eq + self.play(Write(a_eq)) + self.wait() + + # v_eq + self.play( + TransformFromCopy( + a_eq.get_part_by_tex("{v}"), + v_eq.get_part_by_tex("{v}"), + ) + ) + self.play(TransformFromCopy(v_eq[:1], v_eq[1:])) + self.wait() + + # x_eq + self.play( + TransformFromCopy( + v_eq.get_part_by_tex("{x}"), + x_eq.get_part_by_tex("{x}"), + ) + ) + self.play(Write(x_eq[1:])) + self.wait() + for tex in "L", "\\theta": + self.play(ShowCreationThenFadeAround( + x_eq.get_part_by_tex(tex) + )) + self.wait() + + # v_rhs + self.play(*[ + TransformFromCopy( + x_eq.get_part_by_tex(tex), + v_rhs.get_part_by_tex(tex), + ) + for tex in ("=", "{L}", "\\theta") + ]) + self.play( + TransformFromCopy(v_eq[-3], v_rhs[2]), + TransformFromCopy(v_eq[-1], v_rhs[4]), + ) + self.wait() + self.play( + Write(v_rhs[-5]), + TransformFromCopy(*v_rhs.get_parts_by_tex("{L}")), + TransformFromCopy(v_rhs[3:4], v_rhs[-3:]) + ) + self.wait() + self.play(ShowCreationThenFadeAround(v_rhs[2:4])) + self.play(ShowCreationThenFadeAround(v_rhs[4])) + self.wait() + + # a_rhs + self.play(*[ + TransformFromCopy( + v_rhs.get_parts_by_tex(tex)[-1], + a_rhs.get_part_by_tex(tex), + ) + for tex in ("=", "{L}", "\\theta", "\\dot") + ]) + self.play( + TransformFromCopy(a_eq[-3], a_rhs[2]), + TransformFromCopy(a_eq[-1], a_rhs[6]), + ) + self.wait() + self.play( + Write(a_rhs[-5]), + TransformFromCopy(*a_rhs.get_parts_by_tex("{L}")), + TransformFromCopy(a_rhs[3:4], a_rhs[-3:]), + ) + self.wait() + + self.equations = VGroup( + a_eq, v_eq, x_eq, + v_rhs, a_rhs, + ) + + def show_theta_double_dot_equation(self): + equations = self.equations + a_deriv = equations[0] + a_rhs = equations[-1][-5:].copy() + + shift_vect = 1.5 * DOWN + + equals = TexMobject("=") + equals.rotate(90 * DEGREES) + equals.next_to(a_deriv[0], UP, MED_LARGE_BUFF) + g_sin_eq = TexMobject( + "-", "g", "\\sin", "(", "\\theta", ")", + **self.tex_config, + ) + g_sin_eq.next_to( + equals, UP, + buff=MED_LARGE_BUFF, + aligned_edge=LEFT, + ) + g_sin_eq.to_edge(LEFT) + g_sin_eq.shift(shift_vect) + + shift_vect += ( + g_sin_eq[1].get_center() - + a_deriv[0].get_center() + )[0] * RIGHT + + equals.shift(shift_vect) + a_rhs.shift(shift_vect) + + self.play( + equations.shift, shift_vect, + Write(equals), + GrowFromPoint( + g_sin_eq, 2 * RIGHT + 3 * DOWN + ) + ) + self.wait() + self.play( + a_rhs.next_to, g_sin_eq, RIGHT, + a_rhs.shift, SMALL_BUFF * UP, + ) + self.wait() + + # Fade equations + self.play( + FadeOut(equals), + equations.shift, DOWN, + equations.fade, 0.5, + ) + + # Rotate sides + equals, L, ddot, theta, junk = a_rhs + L_dd_theta = VGroup(L, ddot, theta) + minus, g, sin, lp, theta2, rp = g_sin_eq + m2, g2, over, L2 = frac = TexMobject("-", "{g", "\\over", "L}") + frac.next_to(equals, RIGHT) + + self.play( + L_dd_theta.next_to, equals, LEFT, + L_dd_theta.shift, SMALL_BUFF * UP, + g_sin_eq.next_to, equals, RIGHT, + path_arc=PI / 2, + ) + self.play( + ReplacementTransform(g, g2), + ReplacementTransform(minus, m2), + ReplacementTransform(L, L2), + Write(over), + g_sin_eq[2:].next_to, over, RIGHT, SMALL_BUFF, + ) + self.wait() + + # Surround + rect = SurroundingRectangle(VGroup(g_sin_eq, frac, ddot)) + rect.stretch(1.1, 0) + dashed_rect = DashedVMobject( + rect, num_dashes=50, positive_space_ratio=1, + ) + dashed_rect.shuffle() + dashed_rect.save_state() + dashed_rect.space_out_submobjects(1.1) + for piece in dashed_rect: + piece.rotate(90 * DEGREES) + dashed_rect.fade(1) + self.play(Restore(dashed_rect, lag_ratio=0.05)) + dashed_rect.generate_target() + dashed_rect.target.space_out_submobjects(0.9) + dashed_rect.target.fade(1) + for piece in dashed_rect.target: + piece.rotate(90 * DEGREES) + self.play(MoveToTarget( + dashed_rect, + lag_ratio=0.05, + remover=True + )) + self.wait() + + self.main_equation = VGroup( + ddot, theta, equals, + m2, L2, over, g2, + sin, lp, theta2, rp, + ) def talk_about_sine_component(self): - pass + main_equation = self.main_equation + gL_part = main_equation[4:7] + sin_part = main_equation[7:] + sin = sin_part[0] + + morty = Mortimer(height=1.5) + morty.next_to(sin, DR, buff=LARGE_BUFF) + morty.add_updater(lambda m: m.look_at(sin)) + + self.play(ShowCreationThenFadeAround(gL_part)) + self.wait() + self.play(ShowCreationThenFadeAround(sin_part)) + self.wait() + + self.play(FadeIn(morty)) + sin.save_state() + self.play( + morty.change, "angry", + sin.next_to, morty, LEFT, {"aligned_edge": UP}, + ) + self.play(Blink(morty)) + morty.clear_updaters() + self.play( + morty.change, "concerned_musician", + morty.look, DR, + ) + self.play(Restore(sin)) + self.play(FadeOut(morty)) + self.wait() + + # Emphasize theta as input + theta = sin_part[2] + arrow = Vector(0.5 * UP, color=WHITE) + arrow.next_to(theta, DOWN, SMALL_BUFF) + word = TextMobject("Input") + word.next_to(arrow, DOWN) + + self.play( + FadeInFrom(word, UP), + GrowArrow(arrow) + ) + self.play( + ShowCreationThenDestruction( + theta.copy().set_style( + fill_opacity=0, + stroke_width=2, + stroke_color=YELLOW, + ), + lag_ratio=0.1, + ) + ) + self.play(FadeOut(arrow), FadeOut(word)) def add_air_resistance(self): - pass + main_equation = self.main_equation + tdd_eq = main_equation[:3] + rhs = main_equation[3:] + + new_term = TexMobject( + "-", "\\mu", "\\dot{", "\\theta}", + ) + new_term.set_color_by_tex("\\theta", BLUE) + new_term.move_to(main_equation) + new_term.shift(0.5 * SMALL_BUFF * UP) + new_term[0].align_to(rhs[0], UP) + + brace = Brace(new_term, DOWN) + words = brace.get_text("Air resistance") + + self.play( + FadeInFromDown(new_term), + tdd_eq.next_to, new_term, LEFT, + tdd_eq.align_to, tdd_eq, UP, + rhs.next_to, new_term, RIGHT, + rhs.align_to, rhs, UP, + ) + self.play( + GrowFromCenter(brace), + Write(words) + ) + self.wait() class NewSceneName(Scene): diff --git a/active_projects/ode/part1/pi_scenes.py b/active_projects/ode/part1/pi_scenes.py index 4c53a9d6..bb2a6d41 100644 --- a/active_projects/ode/part1/pi_scenes.py +++ b/active_projects/ode/part1/pi_scenes.py @@ -88,7 +88,7 @@ class FormulasAreLies(PiCreatureScene): ) self.wait() self.play(you.change, "confused") - self.wait(0) + self.wait() self.play( you.change, "angry", ShowCreation(bubble), diff --git a/active_projects/ode/part1/staging.py b/active_projects/ode/part1/staging.py index 0ca51c39..2f9b4df4 100644 --- a/active_projects/ode/part1/staging.py +++ b/active_projects/ode/part1/staging.py @@ -412,3 +412,13 @@ class ShowGravityAcceleration(Scene): LaggedStartMap(FadeInFrom, amounts, lambda m: (m, LEFT)), ) self.wait() + + +class ShowDerivativeVideo(Scene): + def construct(self): + pass + + +class SubtleAirCurrents(Scene): + def construct(self): + pass From 54cd60bef715de7975461f6b06185e0d0cc47edc Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 22 Mar 2019 11:50:02 -0700 Subject: [PATCH 06/27] AnimatedBoundary. Perhaps a better name is in order, but this is meant to add a little visual interest to things like a framing rectangle. --- big_ol_pile_of_manim_imports.py | 1 + manimlib/mobject/changing.py | 60 +++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 manimlib/mobject/changing.py diff --git a/big_ol_pile_of_manim_imports.py b/big_ol_pile_of_manim_imports.py index 746936c2..0b7334ec 100644 --- a/big_ol_pile_of_manim_imports.py +++ b/big_ol_pile_of_manim_imports.py @@ -35,6 +35,7 @@ from manimlib.camera.moving_camera import * from manimlib.camera.three_d_camera import * from manimlib.mobject.coordinate_systems import * +from manimlib.mobject.changing import * from manimlib.mobject.frame import * from manimlib.mobject.functions import * from manimlib.mobject.geometry import * diff --git a/manimlib/mobject/changing.py b/manimlib/mobject/changing.py new file mode 100644 index 00000000..185026e9 --- /dev/null +++ b/manimlib/mobject/changing.py @@ -0,0 +1,60 @@ +import warnings + +from manimlib.constants import * +from manimlib.mobject.types.vectorized_mobject import VGroup +from manimlib.mobject.types.vectorized_mobject import VMobject +from manimlib.utils.rate_functions import smooth + +POINTLESS_VMOBJECT_WARNING = """ + +Calling AnimatedBoundary on a VMobject with no points. +""" + + +class AnimatedBoundary(VGroup): + CONFIG = { + "colors": [BLUE_D, BLUE_B, BLUE_E, GREY_BROWN], + "max_stroke_width": 3, + "cycle_rate": 0.5, + } + + def __init__(self, vmobject, **kwargs): + super().__init__(**kwargs) + if len(vmobject.points) == 0: + warnings.warn(POINTLESS_VMOBJECT_WARNING) + self.vmobject = vmobject + self.boundary_copies = [ + VMobject(stroke_width=0, fill_opacity=0) + for x in range(2) + ] + self.add(*self.boundary_copies) + self.total_time = 0 + self.add_updater(lambda m, dt: self.update(dt)) + + def update(self, dt): + # Not actual time, but something which passes at + # an altered rate to make the implementation below + # cleaner + time = self.total_time * self.cycle_rate + growing, fading = self.boundary_copies + colors = self.colors + msw = self.max_stroke_width + + index = int(time % len(colors)) + alpha = smooth(time % 1) + + if int(time) % 2 == 0: + bounds = (0, alpha) + else: + bounds = (1 - alpha, 1) + growing.pointwise_become_partial(self.vmobject, *bounds) + growing.set_stroke(colors[index], width=msw) + + if time > 1: + fading.pointwise_become_partial(self.vmobject, 0, 1) + fading.set_stroke( + color=colors[index - 1], + width=(1 - alpha) * msw + ) + + self.total_time += dt From 677f67cb9d14918a0ccfe3b9ddcb60670d01fd9e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 22 Mar 2019 11:50:16 -0700 Subject: [PATCH 07/27] VFadeInThenOut --- manimlib/animation/fading.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/manimlib/animation/fading.py b/manimlib/animation/fading.py index 8f900c97..9f5ea190 100644 --- a/manimlib/animation/fading.py +++ b/manimlib/animation/fading.py @@ -4,6 +4,7 @@ from manimlib.animation.transform import Transform 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 DEFAULT_FADE_LAG_RATIO = 0 @@ -149,3 +150,10 @@ class VFadeOut(VFadeIn): def interpolate_submobject(self, submob, start, alpha): super().interpolate_submobject(submob, start, 1 - alpha) + + +class VFadeInThenOut(VFadeIn): + CONFIG = { + "rate_func": there_and_back, + "remover": True, + } From 282dfeedde559c27a027605e622b53c1d56f8cbe Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 22 Mar 2019 11:50:58 -0700 Subject: [PATCH 08/27] I don't like this fix, but TexMobjects were breaking if the 'dot' part was left isolated --- manimlib/mobject/svg/tex_mobject.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manimlib/mobject/svg/tex_mobject.py b/manimlib/mobject/svg/tex_mobject.py index 3afd362e..16687c85 100644 --- a/manimlib/mobject/svg/tex_mobject.py +++ b/manimlib/mobject/svg/tex_mobject.py @@ -66,6 +66,8 @@ class SingleStringTexMobject(SVGMobject): # Need to add blank subscript or superscript tex.endswith("_"), tex.endswith("^"), + tex.endswith("\\ddot"), + tex.endswith("\\dot"), ]) if should_add_filler: filler = "{\\quad}" From 217da34de51d8ebe7443fdd71c4e94e301f10039 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 22 Mar 2019 11:51:08 -0700 Subject: [PATCH 09/27] More ode animations --- active_projects/ode/all_part1_scenes.py | 2 + active_projects/ode/part1/pendulum.py | 10 +- active_projects/ode/part1/staging.py | 350 +++++++++++++++++++++++- 3 files changed, 359 insertions(+), 3 deletions(-) diff --git a/active_projects/ode/all_part1_scenes.py b/active_projects/ode/all_part1_scenes.py index 2ee2da52..20ab09f3 100644 --- a/active_projects/ode/all_part1_scenes.py +++ b/active_projects/ode/all_part1_scenes.py @@ -23,4 +23,6 @@ ALL_SCENE_CLASSES = [ BuildUpEquation, ShowDerivativeVideo, SubtleAirCurrents, + DefineODE, + ODEvsPDEinFrames, ] diff --git a/active_projects/ode/part1/pendulum.py b/active_projects/ode/part1/pendulum.py index 4e59a04c..3ee49f99 100644 --- a/active_projects/ode/part1/pendulum.py +++ b/active_projects/ode/part1/pendulum.py @@ -1187,6 +1187,8 @@ class AnalyzePendulumForce(MovingCameraScene): thought_term.next_to, self.pendulum, DOWN, LARGE_BUFF ) + self.accleration_equation = thought_term + def emphasize_theta(self): pendulum = self.pendulum @@ -1297,7 +1299,11 @@ class AnalyzePendulumForce(MovingCameraScene): pass def circle_g_sin_formula(self): - pass + self.play( + ShowCreationThenFadeAround( + self.accleration_equation + ) + ) # def set_theta(self, value, *added_anims, **kwargs): @@ -1323,7 +1329,7 @@ class BuildUpEquation(Scene): } def construct(self): - self.add_center_line() + # self.add_center_line() self.show_derivatives() self.show_theta_double_dot_equation() self.talk_about_sine_component() diff --git a/active_projects/ode/part1/staging.py b/active_projects/ode/part1/staging.py index 2f9b4df4..70e192cd 100644 --- a/active_projects/ode/part1/staging.py +++ b/active_projects/ode/part1/staging.py @@ -1,5 +1,7 @@ from big_ol_pile_of_manim_imports import * from active_projects.ode.part1.shared_constructs import * +from active_projects.ode.part1.pendulum import Pendulum +from active_projects.ode.part1.pendulum import ThetaVsTAxes def pendulum_vector_field(point, mu=0.1, g=9.8, L=3): @@ -415,10 +417,356 @@ class ShowGravityAcceleration(Scene): class ShowDerivativeVideo(Scene): + CONFIG = { + "camera_config": {"background_color": DARKER_GREY} + } + def construct(self): - pass + title = TextMobject("Essence of Calculus") + title.scale(1.25) + title.to_edge(UP) + rect = ScreenRectangle(height=6) + rect = rect.copy() + rect.set_style( + fill_opacity=1, + fill_color=BLACK, + stroke_width=0, + ) + rect.next_to(title, DOWN) + animated_rect = AnimatedBoundary(rect) + + self.add(title, rect) + self.add(animated_rect) + self.wait(10) class SubtleAirCurrents(Scene): def construct(self): pass + + +class DefineODE(Scene): + CONFIG = { + "pendulum_config": { + "length": 2, + "top_point": 5 * RIGHT + 2 * UP, + "initial_theta": 150 * DEGREES, + "mu": 0.3, + }, + "axes_config": { + "y_axis_config": {"unit_size": 0.75}, + "y_max": PI, + "y_min": -PI, + "x_max": 10, + "x_axis_config": { + "numbers_to_show": range(2, 10, 2), + "unit_size": 1, + } + }, + } + + def construct(self): + self.add_graph() + self.write_differential_equation() + self.dont_know_the_value() + self.show_value_slope_curvature() + self.write_ode() + self.show_second_order() + self.show_changing_curvature_group() + + def add_graph(self): + pendulum = Pendulum(**self.pendulum_config) + axes = ThetaVsTAxes(**self.axes_config) + + axes.center() + axes.to_corner(DL) + graph = axes.get_live_drawn_graph(pendulum) + + pendulum.start_swinging() + self.add(axes, pendulum, graph) + + self.pendulum = pendulum + self.axes = axes + self.graph = graph + + def write_differential_equation(self): + de_word = TextMobject("Differential", "Equation") + de_word.to_edge(UP) + + equation = TexMobject( + "\\ddot \\theta({t}) = " + "-\\mu \\dot \\theta({t})" + "-{g \\over L} \\sin\\big(\\theta({t})\\big)", + tex_to_color_map={ + "\\theta": BLUE, + "{t}": WHITE + } + ) + equation.next_to(de_word, DOWN) + thetas = equation.get_parts_by_tex("\\theta") + + lines = VGroup(*[ + Line(v, 1.2 * v) + for v in compass_directions(25) + ]) + lines.replace(equation, stretch=True) + lines.scale(1.5) + lines.set_stroke(YELLOW) + lines.shuffle() + + self.add(equation) + self.wait(5) + self.play( + ShowPassingFlashWithThinningStrokeWidth( + lines, + lag_ratio=0.002, + run_time=1.5, + time_width=0.9, + n_segments=5, + ) + ) + self.play(FadeInFromDown(de_word)) + self.wait(2) + self.play( + LaggedStartMap( + ApplyMethod, thetas, + lambda m: (m.shift, 0.25 * DOWN), + rate_func=there_and_back, + ) + ) + self.wait() + + self.de_word = de_word + self.equation = equation + + def dont_know_the_value(self): + graph = self.graph + pendulum = self.pendulum + + q_marks = VGroup(*[ + TexMobject("?").move_to(graph.point_from_proportion(a)) + for a in np.linspace(0, 1, 20) + ]) + q_marks.set_stroke(width=0, background=True) + self.play( + VFadeOut(graph), + FadeOut(pendulum), + LaggedStart(*[ + UpdateFromAlphaFunc( + q_mark, + lambda m, a: m.set_height(0.5 * (1 + a)).set_fill( + opacity=there_and_back(a) + ), + ) + for q_mark in q_marks + ], lag_ratio=0.01, run_time=2) + ) + self.remove(q_marks) + + def show_value_slope_curvature(self): + axes = self.axes + p = self.pendulum + graph = axes.get_graph( + lambda t: p.initial_theta * np.cos( + np.sqrt(p.gravity / p.length) * t + ) * np.exp(-p.mu * t / 2) + ) + + tex_config = { + "tex_to_color_map": {"\\theta": BLUE}, + "height": 0.5, + } + theta, d_theta, dd_theta = [ + TexMobject(s + "\\theta(t)", **tex_config) + for s in ("", "\\dot", "\\ddot") + ] + + t_tracker = ValueTracker(2.5) + get_t = t_tracker.get_value + + def get_point(t): + return graph.point_from_proportion(t / axes.x_max) + + def get_dot(): + return Dot(get_point(get_t())).scale(0.5) + + def get_v_line(): + point = get_point(get_t()) + x_point = axes.x_axis.number_to_point( + axes.x_axis.point_to_number(point) + ) + return DashedLine( + x_point, point, + dash_length=0.025, + stroke_color=WHITE, + stroke_width=2, + ) + + def get_tangent_line(curve, alpha): + line = Line( + ORIGIN, 1.5 * RIGHT, + color=YELLOW, + stroke_width=1.5, + ) + da = 0.0001 + p0 = curve.point_from_proportion(alpha) + p1 = curve.point_from_proportion(alpha - da) + p2 = curve.point_from_proportion(alpha + da) + angle = angle_of_vector(p2 - p1) + line.rotate(angle) + line.move_to(p0) + return line + + def get_slope_line(): + return get_tangent_line( + graph, get_t() / axes.x_max + ) + + def get_curve(): + curve = VMobject() + t = get_t() + curve.set_points_smoothly([ + get_point(t + a) + for a in np.linspace(-0.5, 0.5, 11) + ]) + curve.set_stroke(RED, 1) + return curve + + v_line = always_redraw(get_v_line) + dot = always_redraw(get_dot) + slope_line = always_redraw(get_slope_line) + curve = always_redraw(get_curve) + + theta.next_to(v_line, RIGHT, SMALL_BUFF) + d_theta.next_to(slope_line.get_end(), UP, SMALL_BUFF) + dd_theta.next_to(curve.get_end(), RIGHT, SMALL_BUFF) + thetas = VGroup(theta, d_theta, dd_theta) + + words = VGroup( + TextMobject("= Height"), + TextMobject("= Slope").set_color(YELLOW), + TextMobject("= ``Curvature''").set_color(RED), + ) + words.scale(0.75) + for word, sym in zip(words, thetas): + word.next_to(sym, RIGHT, buff=2 * SMALL_BUFF) + sym.word = word + + self.play( + ShowCreation(v_line), + FadeInFromPoint(dot, v_line.get_start()), + FadeInFrom(theta, DOWN), + FadeInFrom(theta.word, DOWN), + ) + self.add(slope_line, dot) + self.play( + ShowCreation(slope_line), + FadeInFrom(d_theta, LEFT), + FadeInFrom(d_theta.word, LEFT), + ) + + a_tracker = ValueTracker(0) + curve_copy = curve.copy() + changing_slope = always_redraw( + lambda: get_tangent_line( + curve_copy, + a_tracker.get_value(), + ).set_stroke( + opacity=there_and_back(a_tracker.get_value()) + ) + ) + self.add(curve, dot) + self.play( + ShowCreation(curve), + FadeInFrom(dd_theta, LEFT), + FadeInFrom(dd_theta.word, LEFT), + ) + self.add(changing_slope) + self.play( + a_tracker.set_value, 1, + run_time=3, + ) + self.remove(changing_slope, a_tracker) + + self.t_tracker = t_tracker + self.curvature_group = VGroup( + v_line, slope_line, curve, dot + ) + self.curvature_group_labels = VGroup(thetas, words) + + def write_ode(self): + equation = self.equation + axes = self.axes + de_word = self.de_word + + ts = equation.get_parts_by_tex("{t}") + t_rects = VGroup(*map(SurroundingRectangle, ts)) # Rawr + x_axis = axes.x_axis + x_axis_line = Line( + x_axis.get_start(), x_axis.get_end(), + stroke_color=YELLOW, + stroke_width=5, + ) + + ordinary = TextMobject("Ordinary") + de_word.generate_target() + group = VGroup(ordinary, de_word.target) + group.arrange(RIGHT) + group.to_edge(UP) + ordinary_underline = Line(LEFT, RIGHT) + ordinary_underline.replace(ordinary, dim_to_match=0) + ordinary_underline.next_to(ordinary, DOWN, SMALL_BUFF) + ordinary_underline.set_color(YELLOW) + + self.play( + ShowCreationThenFadeOut( + t_rects, + lag_ratio=0.8 + ), + ShowCreationThenFadeOut(x_axis_line) + ) + self.play( + MoveToTarget(de_word), + FadeInFrom(ordinary, RIGHT), + GrowFromCenter(ordinary_underline) + ) + self.play(FadeOut(ordinary_underline)) + self.wait() + + self.remove(ordinary, de_word) + ode_word = self.ode_word = VGroup(*ordinary, *de_word) + ode_initials = VGroup(*[word[0] for word in ode_word]) + ode_initials.generate_target() + ode_initials.target.scale(1.2) + ode_initials.target.set_color(PINK) + ode_initials.target.arrange( + RIGHT, buff=0.5 * SMALL_BUFF, aligned_edge=DOWN + ) + ode_initials.target.to_edge(UP, buff=MED_SMALL_BUFF) + + ode_remaining_letters = VGroup(*it.chain(*[ + word[1:] for word in ode_word + ])) + ode_remaining_letters.generate_target() + for mob in ode_remaining_letters.target: + mob.shift(0.2 * UP) + mob.fade(1) + + self.play( + MoveToTarget(ode_initials), + MoveToTarget(ode_remaining_letters, lag_ratio=0.05), + ) + self.wait() + + self.ode_initials = ode_initials + + def show_second_order(self): + pass + + def show_changing_curvature_group(self): + pass + + +class ODEvsPDEinFrames(Scene): + def construct(self): + pass From 5ffa41f6751d5e9e9c795e293b6e2aa97f567e1d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 22 Mar 2019 15:12:21 -0700 Subject: [PATCH 10/27] Extend AnimatedBoundary to work on families --- manimlib/mobject/changing.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/manimlib/mobject/changing.py b/manimlib/mobject/changing.py index 185026e9..9387ebd1 100644 --- a/manimlib/mobject/changing.py +++ b/manimlib/mobject/changing.py @@ -1,15 +1,7 @@ -import warnings - from manimlib.constants import * from manimlib.mobject.types.vectorized_mobject import VGroup -from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.utils.rate_functions import smooth -POINTLESS_VMOBJECT_WARNING = """ - -Calling AnimatedBoundary on a VMobject with no points. -""" - class AnimatedBoundary(VGroup): CONFIG = { @@ -20,11 +12,12 @@ class AnimatedBoundary(VGroup): def __init__(self, vmobject, **kwargs): super().__init__(**kwargs) - if len(vmobject.points) == 0: - warnings.warn(POINTLESS_VMOBJECT_WARNING) self.vmobject = vmobject self.boundary_copies = [ - VMobject(stroke_width=0, fill_opacity=0) + vmobject.copy().set_style( + stroke_width=0, + fill_opacity=0 + ) for x in range(2) ] self.add(*self.boundary_copies) @@ -39,6 +32,7 @@ class AnimatedBoundary(VGroup): growing, fading = self.boundary_copies colors = self.colors msw = self.max_stroke_width + vmobject = self.vmobject index = int(time % len(colors)) alpha = smooth(time % 1) @@ -47,14 +41,21 @@ class AnimatedBoundary(VGroup): bounds = (0, alpha) else: bounds = (1 - alpha, 1) - growing.pointwise_become_partial(self.vmobject, *bounds) + self.full_family_become_partial(growing, vmobject, *bounds) growing.set_stroke(colors[index], width=msw) - if time > 1: - fading.pointwise_become_partial(self.vmobject, 0, 1) + if time >= 1: + self.full_family_become_partial(fading, vmobject, 0, 1) fading.set_stroke( color=colors[index - 1], width=(1 - alpha) * msw ) self.total_time += dt + + def full_family_become_partial(self, mob1, mob2, a, b): + family1 = mob1.family_members_with_points() + family2 = mob2.family_members_with_points() + for sm1, sm2 in zip(family1, family2): + sm1.pointwise_become_partial(sm2, a, b) + return self From 0b56d6f311faaa38c66233424db2eb524a475693 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 22 Mar 2019 15:54:16 -0700 Subject: [PATCH 11/27] Tex strings ending with 'dot' have a '{\quad}' added --- manimlib/mobject/svg/tex_mobject.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/manimlib/mobject/svg/tex_mobject.py b/manimlib/mobject/svg/tex_mobject.py index 16687c85..b8efddf5 100644 --- a/manimlib/mobject/svg/tex_mobject.py +++ b/manimlib/mobject/svg/tex_mobject.py @@ -66,8 +66,7 @@ class SingleStringTexMobject(SVGMobject): # Need to add blank subscript or superscript tex.endswith("_"), tex.endswith("^"), - tex.endswith("\\ddot"), - tex.endswith("\\dot"), + tex.endswith("dot"), ]) if should_add_filler: filler = "{\\quad}" From 9cdf35475d4e0b1cb8bec727e0e4c24db6d4dfe2 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 22 Mar 2019 15:54:45 -0700 Subject: [PATCH 12/27] Up to the pont where phase space is introduced --- active_projects/ode/all_part1_scenes.py | 4 + active_projects/ode/part1/pi_scenes.py | 93 ++++++++- .../ode/part1/shared_constructs.py | 16 ++ active_projects/ode/part1/staging.py | 191 +++++++++--------- active_projects/ode/part1/wordy_scenes.py | 141 +++++++++++++ 5 files changed, 348 insertions(+), 97 deletions(-) create mode 100644 active_projects/ode/part1/wordy_scenes.py diff --git a/active_projects/ode/all_part1_scenes.py b/active_projects/ode/all_part1_scenes.py index 20ab09f3..c1cd1a4d 100644 --- a/active_projects/ode/all_part1_scenes.py +++ b/active_projects/ode/all_part1_scenes.py @@ -1,6 +1,7 @@ from active_projects.ode.part1.pendulum import * from active_projects.ode.part1.staging import * from active_projects.ode.part1.pi_scenes import * +from active_projects.ode.part1.wordy_scenes import * OUTPUT_DIRECTORY = "ode/part1" ALL_SCENE_CLASSES = [ @@ -25,4 +26,7 @@ ALL_SCENE_CLASSES = [ SubtleAirCurrents, DefineODE, ODEvsPDEinFrames, + ProveTeacherWrong, + SetAsideSeekingSolution, + VisualizeStates, ] diff --git a/active_projects/ode/part1/pi_scenes.py b/active_projects/ode/part1/pi_scenes.py index bb2a6d41..bd181753 100644 --- a/active_projects/ode/part1/pi_scenes.py +++ b/active_projects/ode/part1/pi_scenes.py @@ -103,6 +103,95 @@ class FormulasAreLies(PiCreatureScene): return You().flip().to_corner(DR) -class NewSceneName(Scene): +class ProveTeacherWrong(TeacherStudentsScene): def construct(self): - pass + tex_config = { + "tex_to_color_map": {"{\\theta}": BLUE} + } + func = TexMobject( + "{\\theta}(t)", "=", + "\\theta_0", "\\cos(\\sqrt{g / L} \\cdot t)", + **tex_config, + ) + d_func = TexMobject( + "\\dot {\\theta}(t)", "=", + "-\\left(\\sqrt{g / L}\\right)", + "\\theta_0", "\\sin(\\sqrt{g / L} \\cdot t)", + **tex_config, + ) + dd_func = TexMobject( + "\\ddot {\\theta}(t)", "=", + "-\\left(g / L\\right)", + "\\theta_0", "\\cos(\\sqrt{g / L} \\cdot t)", + **tex_config, + ) + ode = TexMobject( + "\\ddot {\\theta}({t})", "=", + "-\\mu \\dot {\\theta}({t})", + "-{g \\over L} \\sin\\big({\\theta}({t})\\big)", + **tex_config, + ) + arrows = [TexMobject("\\Downarrow") for x in range(2)] + + VGroup(func, d_func, dd_func, ode, *arrows).scale(0.7) + + teacher = self.teacher + you = self.students[2] + + self.student_thinks(ode) + you.add_updater(lambda m: m.look_at(func)) + self.teacher_holds_up(func) + self.wait() + + group = VGroup(arrows[0], d_func, arrows[1], dd_func) + group.arrange(DOWN) + group.move_to(func, DOWN) + + arrow = Arrow( + group.get_corner(UL), + ode.get_top(), + path_arc=PI / 2, + ) + q_marks = VGroup(*[ + TexMobject("?").scale(1.5).next_to( + arrow.point_from_proportion(a), + UP + ) + for a in np.linspace(0.2, 0.8, 5) + ]) + cycle_animation(VFadeInThenOut( + q_marks, + lag_ratio=0.2, + run_time=4, + rate_func=squish_rate_func(smooth, 0, 0.5) + )) + + self.play( + func.next_to, group, UP, + LaggedStartMap( + FadeInFrom, group, + lambda m: (m, UP) + ), + teacher.change, "guilty", + you.change, "sassy", + ) + + rect = SurroundingRectangle( + VGroup(group, func) + ) + dashed_rect = DashedVMobject(rect, num_dashes=75) + animated_rect = AnimatedBoundary(dashed_rect, cycle_rate=1) + + self.wait() + self.add(animated_rect, q_marks) + self.play( + ShowCreation(arrow), + # FadeInFromDown(q_mark), + self.get_student_changes("confused", "confused") + ) + self.wait(4) + self.change_student_modes( + *3 * ["pondering"], + self.teacher.change, "maybe" + ) + self.wait(8) diff --git a/active_projects/ode/part1/shared_constructs.py b/active_projects/ode/part1/shared_constructs.py index 606c00b6..4a2f40b7 100644 --- a/active_projects/ode/part1/shared_constructs.py +++ b/active_projects/ode/part1/shared_constructs.py @@ -14,3 +14,19 @@ class You(PiCreature): CONFIG = { "color": BLUE_C, } + + +def get_ode(): + tex_config = { + "tex_to_color_map": { + "{\\theta}": BLUE, + "{t}": WHITE, + } + } + ode = TexMobject( + "\\ddot {\\theta}({t})", "=", + "-\\mu \\dot {\\theta}({t})", + "-{g \\over L} \\sin\\big({\\theta}({t})\\big)", + **tex_config, + ) + return ode diff --git a/active_projects/ode/part1/staging.py b/active_projects/ode/part1/staging.py index 70e192cd..833b5d59 100644 --- a/active_projects/ode/part1/staging.py +++ b/active_projects/ode/part1/staging.py @@ -47,39 +47,6 @@ class VectorFieldTest(Scene): self.wait(10) -class SmallAngleApproximationTex(Scene): - def construct(self): - approx = TexMobject( - "\\sin", "(", "\\theta", ") \\approx \\theta", - tex_to_color_map={"\\theta": RED}, - arg_separator="", - ) - - implies = TexMobject("\\Downarrow") - period = TexMobject( - "\\text{Period}", "\\approx", - "2\\pi \\sqrt{\\,{L} / {g}}", - **Lg_formula_config, - ) - group = VGroup(approx, implies, period) - group.arrange(DOWN) - - approx_brace = Brace(approx, UP, buff=SMALL_BUFF) - approx_words = TextMobject( - "For small $\\theta$", - tex_to_color_map={"$\\theta$": RED}, - ) - approx_words.scale(0.75) - approx_words.next_to(approx_brace, UP, SMALL_BUFF) - - self.add(approx, approx_brace, approx_words) - self.play( - Write(implies), - FadeInFrom(period, LEFT) - ) - self.wait() - - class FollowThisThread(Scene): CONFIG = { "screen_rect_style": { @@ -165,64 +132,6 @@ class FollowThisThread(Scene): self.wait() -class StrogatzQuote(Scene): - def construct(self): - law_words = "laws of physics" - language_words = "language of differential equations" - author = "-Steven Strogatz" - quote = TextMobject( - """ - \\Large - ``Since Newton, mankind has come to realize - that the laws of physics are always expressed - in the language of differential equations.''\\\\ - """ + author, - alignment="", - arg_separator=" ", - substrings_to_isolate=[law_words, language_words, author] - ) - law_part = quote.get_part_by_tex(law_words) - language_part = quote.get_part_by_tex(language_words) - author_part = quote.get_part_by_tex(author) - quote.set_width(12) - quote.to_edge(UP) - quote[-2].shift(SMALL_BUFF * LEFT) - author_part.shift(RIGHT + 0.5 * DOWN) - author_part.scale(1.2, about_edge=UL) - - movers = VGroup(*quote[:-1].family_members_with_points()) - for mover in movers: - mover.save_state() - disc = Circle(radius=0.05) - disc.set_stroke(width=0) - disc.set_fill(BLACK, 0) - disc.move_to(mover) - mover.become(disc) - self.play( - FadeInFrom(author_part, LEFT), - LaggedStartMap( - # FadeInFromLarge, - # quote[:-1].family_members_with_points(), - Restore, movers, - lag_ratio=0.005, - run_time=2, - ) - # FadeInFromDown(quote[:-1]), - # lag_ratio=0.01, - ) - self.wait() - self.play( - Write(law_part.copy().set_color(YELLOW)), - run_time=1, - ) - self.wait() - self.play( - Write(language_part.copy().set_color(BLUE)), - run_time=1.5, - ) - self.wait(2) - - class ShowGravityAcceleration(Scene): def construct(self): self.add_gravity_field() @@ -472,6 +381,7 @@ class DefineODE(Scene): self.show_value_slope_curvature() self.write_ode() self.show_second_order() + self.show_higher_order_examples() self.show_changing_curvature_group() def add_graph(self): @@ -494,8 +404,8 @@ class DefineODE(Scene): de_word.to_edge(UP) equation = TexMobject( - "\\ddot \\theta({t}) = " - "-\\mu \\dot \\theta({t})" + "\\ddot \\theta({t})", "=", + "-\\mu \\dot \\theta({t})", "-{g \\over L} \\sin\\big(\\theta({t})\\big)", tex_to_color_map={ "\\theta": BLUE, @@ -693,6 +603,7 @@ class DefineODE(Scene): v_line, slope_line, curve, dot ) self.curvature_group_labels = VGroup(thetas, words) + self.fake_graph = graph def write_ode(self): equation = self.equation @@ -761,12 +672,102 @@ class DefineODE(Scene): self.ode_initials = ode_initials def show_second_order(self): - pass + so = TextMobject("Second order") + so.scale(1.4) + ode = self.ode_initials + ode.generate_target() + group = VGroup(so, ode.target) + group.arrange(RIGHT, aligned_edge=DOWN) + group.to_edge(UP, buff=MED_SMALL_BUFF) + + second_deriv = self.equation[:5] + + self.play( + Write(so), + MoveToTarget(ode), + ) + self.wait() + self.play(FocusOn(second_deriv)) + self.play( + Indicate(second_deriv, color=RED), + ) + self.wait() + + self.second_order_word = so + + def show_higher_order_examples(self): + main_example = VGroup( + self.second_order_word, + self.ode_initials, + self.equation + ) + tex_config = {"tex_to_color_map": {"{x}": BLUE}} + example3 = VGroup( + TextMobject("Third order ODE"), + TexMobject( + "\\dddot {x}(t) + \\dot {x}(t)^2 = 0", + **tex_config, + ) + ) + example4 = VGroup( + TextMobject("Fourth order ODE"), + TexMobject( + "\\ddddot {x}(t) +", + "a\\dddot {x}(t) \\dot {x}(t) + ", + "b \\ddot {x}(t) {x}(t)", + "= 1", + **tex_config, + ) + ) + for example in [example3, example4]: + example[0].scale(1.2) + example.arrange(DOWN, buff=MED_LARGE_BUFF) + example.to_edge(UP, buff=MED_SMALL_BUFF) + + self.play( + FadeOut(main_example), + FadeIn(example3), + ) + self.wait(2) + self.play( + FadeOut(example3), + FadeIn(example4), + ) + self.wait(2) + self.play( + FadeOut(example4), + FadeIn(main_example), + ) + self.wait(2) def show_changing_curvature_group(self): - pass + t_tracker = self.t_tracker + curvature_group = self.curvature_group + labels = self.curvature_group_labels + graph = VMobject() + graph.pointwise_become_partial( + self.fake_graph, + 0.25, 1, + ) + dashed_graph = DashedVMobject(graph, num_dashes=100) + dashed_graph.set_stroke(GREEN, 1) + + self.play(FadeOut(labels)) + self.add(dashed_graph, curvature_group) + self.play( + t_tracker.set_value, 10, + ShowCreation(dashed_graph), + run_time=15, + rate_func=linear, + ) + self.wait() class ODEvsPDEinFrames(Scene): def construct(self): pass + + +class VisualizeStates(Scene): + def construct(self): + pass diff --git a/active_projects/ode/part1/wordy_scenes.py b/active_projects/ode/part1/wordy_scenes.py new file mode 100644 index 00000000..e7ffae3c --- /dev/null +++ b/active_projects/ode/part1/wordy_scenes.py @@ -0,0 +1,141 @@ +from big_ol_pile_of_manim_imports import * +from active_projects.ode.part1.shared_constructs import * + + +class SmallAngleApproximationTex(Scene): + def construct(self): + approx = TexMobject( + "\\sin", "(", "\\theta", ") \\approx \\theta", + tex_to_color_map={"\\theta": RED}, + arg_separator="", + ) + + implies = TexMobject("\\Downarrow") + period = TexMobject( + "\\text{Period}", "\\approx", + "2\\pi \\sqrt{\\,{L} / {g}}", + **Lg_formula_config, + ) + group = VGroup(approx, implies, period) + group.arrange(DOWN) + + approx_brace = Brace(approx, UP, buff=SMALL_BUFF) + approx_words = TextMobject( + "For small $\\theta$", + tex_to_color_map={"$\\theta$": RED}, + ) + approx_words.scale(0.75) + approx_words.next_to(approx_brace, UP, SMALL_BUFF) + + self.add(approx, approx_brace, approx_words) + self.play( + Write(implies), + FadeInFrom(period, LEFT) + ) + self.wait() + + +class StrogatzQuote(Scene): + def construct(self): + law_words = "laws of physics" + language_words = "language of differential equations" + author = "-Steven Strogatz" + quote = TextMobject( + """ + \\Large + ``Since Newton, mankind has come to realize + that the laws of physics are always expressed + in the language of differential equations.''\\\\ + """ + author, + alignment="", + arg_separator=" ", + substrings_to_isolate=[law_words, language_words, author] + ) + law_part = quote.get_part_by_tex(law_words) + language_part = quote.get_part_by_tex(language_words) + author_part = quote.get_part_by_tex(author) + quote.set_width(12) + quote.to_edge(UP) + quote[-2].shift(SMALL_BUFF * LEFT) + author_part.shift(RIGHT + 0.5 * DOWN) + author_part.scale(1.2, about_edge=UL) + + movers = VGroup(*quote[:-1].family_members_with_points()) + for mover in movers: + mover.save_state() + disc = Circle(radius=0.05) + disc.set_stroke(width=0) + disc.set_fill(BLACK, 0) + disc.move_to(mover) + mover.become(disc) + self.play( + FadeInFrom(author_part, LEFT), + LaggedStartMap( + # FadeInFromLarge, + # quote[:-1].family_members_with_points(), + Restore, movers, + lag_ratio=0.005, + run_time=2, + ) + # FadeInFromDown(quote[:-1]), + # lag_ratio=0.01, + ) + self.wait() + self.play( + Write(law_part.copy().set_color(YELLOW)), + run_time=1, + ) + self.wait() + self.play( + Write(language_part.copy().set_color(BLUE)), + run_time=1.5, + ) + self.wait(2) + + +class SetAsideSeekingSolution(Scene): + def construct(self): + ode = get_ode() + ode.to_edge(UP) + q1 = TextMobject("Find an exact solution") + q1.set_color(YELLOW) + q2 = TexMobject( + "\\text{What is }", "\\theta", "(t)", + "\\text{'s personality?}", + tex_to_color_map={"\\theta": BLUE}, + arg_separator="", + ) + theta = q2.get_part_by_tex("\\theta") + + for q in q1, q2: + q.scale(1.5) + q.next_to(ode, DOWN, MED_LARGE_BUFF) + eyes = Eyes(theta, height=0.1) + + self.add(ode) + self.add(q1) + self.wait() + self.play( + q1.scale, 0.3, + q1.to_corner, UR, MED_SMALL_BUFF, + ) + self.play(FadeInFrom(q2, DOWN)) + self.play( + eyes.blink, + rate_func=lambda t: smooth(1 - t), + ) + self.play(eyes.look_at, q2.get_left()) + self.play(eyes.look_at, q2.get_right()) + self.play( + eyes.blink, + rate_func=squish_rate_func(there_and_back) + ) + self.wait() + self.play( + eyes.change_mode, "confused", + eyes.look_at, ode.get_left(), + ) + self.play( + eyes.blink, + rate_func=squish_rate_func(there_and_back) + ) From d8896e299e8b200f8b2187236fe4e12fe9d59500 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 23 Mar 2019 10:51:47 -0700 Subject: [PATCH 13/27] Small bug fixes to NumberPlane --- manimlib/mobject/coordinate_systems.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/mobject/coordinate_systems.py b/manimlib/mobject/coordinate_systems.py index 6d9a5afb..b3d2cc35 100644 --- a/manimlib/mobject/coordinate_systems.py +++ b/manimlib/mobject/coordinate_systems.py @@ -288,7 +288,7 @@ class NumberPlane(Axes): def get_lines_parallel_to_axis(self, axis1, axis2, freq, ratio): line = Line(axis1.get_start(), axis1.get_end()) dense_freq = (1 + ratio) - step = 1 / dense_freq + step = (1 / dense_freq) * freq lines1 = VGroup() lines2 = VGroup() @@ -336,6 +336,7 @@ class NumberPlane(Axes): axis.get_edge_center(edge), direction, buff=buff ) + label.shift_onto_screen(buff=MED_SMALL_BUFF) return label def get_axis_labels(self, x_label_tex="x", y_label_tex="y"): From 43f214550834cdb93087dd3ebc4d65570b206417 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 23 Mar 2019 10:52:02 -0700 Subject: [PATCH 14/27] Change default max_stroke_width_to_length_ratio --- manimlib/mobject/geometry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/mobject/geometry.py b/manimlib/mobject/geometry.py index 15c0f0ce..64366422 100644 --- a/manimlib/mobject/geometry.py +++ b/manimlib/mobject/geometry.py @@ -543,7 +543,7 @@ class Arrow(Line): "buff": MED_SMALL_BUFF, "tip_width_to_length_ratio": 1, "max_tip_length_to_length_ratio": 0.25, - "max_stroke_width_to_length_ratio": 6, + "max_stroke_width_to_length_ratio": 4, "preserve_tip_size_when_scaling": True, "rectangular_stem_width": 0.05, } From 97c8ea273885476d821bd1ababe18f8253279563 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 23 Mar 2019 10:52:25 -0700 Subject: [PATCH 15/27] Small bug fix to NumberLine --- manimlib/mobject/number_line.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/number_line.py b/manimlib/mobject/number_line.py index 8128193a..653c9a19 100644 --- a/manimlib/mobject/number_line.py +++ b/manimlib/mobject/number_line.py @@ -141,8 +141,10 @@ class NumberLine(Line): self.decimal_number_config, number_config or {}, ) - scale_val = scale_val or self.number_scale_val - direction = direction or self.label_direction + 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 num_mob = DecimalNumber(number, **number_config) From 73cbb9134a8e548cb4050405cfee145ba5900bfe Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 23 Mar 2019 10:54:59 -0700 Subject: [PATCH 16/27] Ah, it turns out rate_func placement in Animation is nuanced... --- manimlib/animation/animation.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/manimlib/animation/animation.py b/manimlib/animation/animation.py index dbb0f99d..17ea65fa 100644 --- a/manimlib/animation/animation.py +++ b/manimlib/animation/animation.py @@ -108,8 +108,7 @@ class Animation(object): # Methods for interpolation, the mean of an Animation def interpolate(self, alpha): alpha = np.clip(alpha, 0, 1) - # self.interpolate_mobject(self.rate_func(alpha)) - self.interpolate_mobject(alpha) # TODO, check + self.interpolate_mobject(self.rate_func(alpha)) def update(self, alpha): """ @@ -122,10 +121,7 @@ class Animation(object): families = list(self.get_all_families_zipped()) for i, mobs in enumerate(families): sub_alpha = self.get_sub_alpha(alpha, i, len(families)) - self.interpolate_submobject( - *mobs, - self.rate_func(sub_alpha) # TODO, check - ) + self.interpolate_submobject(*mobs, sub_alpha) def interpolate_submobject(self, submobject, starting_sumobject, alpha): # Typically ipmlemented by subclass From f5420b24ca4fb070fe77ed28890b380c054dc0ed Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 23 Mar 2019 10:55:13 -0700 Subject: [PATCH 17/27] VisualizeStates Scene --- active_projects/ode/all_part1_scenes.py | 2 + active_projects/ode/part1/pendulum.py | 26 +- active_projects/ode/part1/phase_space.py | 506 +++++++++++++++++++++++ active_projects/ode/part1/staging.py | 7 +- 4 files changed, 536 insertions(+), 5 deletions(-) create mode 100644 active_projects/ode/part1/phase_space.py diff --git a/active_projects/ode/all_part1_scenes.py b/active_projects/ode/all_part1_scenes.py index c1cd1a4d..f7a90980 100644 --- a/active_projects/ode/all_part1_scenes.py +++ b/active_projects/ode/all_part1_scenes.py @@ -1,6 +1,7 @@ from active_projects.ode.part1.pendulum import * from active_projects.ode.part1.staging import * from active_projects.ode.part1.pi_scenes import * +from active_projects.ode.part1.phase_space import * from active_projects.ode.part1.wordy_scenes import * OUTPUT_DIRECTORY = "ode/part1" @@ -28,5 +29,6 @@ ALL_SCENE_CLASSES = [ ODEvsPDEinFrames, ProveTeacherWrong, SetAsideSeekingSolution, + ReferencePiCollisionStateSpaces, VisualizeStates, ] diff --git a/active_projects/ode/part1/pendulum.py b/active_projects/ode/part1/pendulum.py index 3ee49f99..39a0360d 100644 --- a/active_projects/ode/part1/pendulum.py +++ b/active_projects/ode/part1/pendulum.py @@ -43,6 +43,10 @@ class Pendulum(VGroup): "theta_label_height": 0.25, "set_theta_label_height_cap": False, "n_steps_per_frame": 100, + "include_theta_label": True, + "include_velocity_vector": False, + "velocity_vector_multiple": 0.5, + "max_velocity_vector_length_to_length_ratio": 0.5, } def __init__(self, **kwargs): @@ -53,7 +57,10 @@ class Pendulum(VGroup): self.rotating_group = VGroup(self.rod, self.weight) self.create_dashed_line() self.create_angle_arc() - self.add_theta_label() + if self.include_theta_label: + self.add_theta_label() + if self.include_velocity_vector: + self.add_velocity_vector() self.set_theta(self.initial_theta) self.update() @@ -89,17 +96,27 @@ class Pendulum(VGroup): self.angle_arc = always_redraw(lambda: Arc( arc_center=self.get_fixed_point(), start_angle=-90 * DEGREES, - angle=self.get_theta(), + angle=self.get_arc_angle_theta(), **self.angle_arc_config, )) self.add(self.angle_arc) + def get_arc_angle_theta(self): + # Might be changed in certain scenes + return self.get_theta() + def add_velocity_vector(self): def make_vector(): omega = self.get_omega() theta = self.get_theta() + mvlr = self.max_velocity_vector_length_to_length_ratio + max_len = mvlr * self.rod.get_length() + vvm = self.velocity_vector_multiple + multiple = np.clip( + vvm * omega, -max_len, max_len + ) vector = Vector( - 0.5 * omega * RIGHT, + multiple * RIGHT, **self.velocity_vector_config, ) vector.rotate(theta, about_point=ORIGIN) @@ -124,7 +141,8 @@ class Pendulum(VGroup): top = self.get_fixed_point() arc_center = self.angle_arc.point_from_proportion(0.5) vect = arc_center - top - vect = normalize(vect) * (1 + self.theta_label_height) + norm = get_norm(vect) + vect = normalize(vect) * (norm + self.theta_label_height) label.move_to(top + vect) return label diff --git a/active_projects/ode/part1/phase_space.py b/active_projects/ode/part1/phase_space.py new file mode 100644 index 00000000..62f27397 --- /dev/null +++ b/active_projects/ode/part1/phase_space.py @@ -0,0 +1,506 @@ +from big_ol_pile_of_manim_imports import * +from active_projects.ode.part1.shared_constructs import * +from active_projects.ode.part1.pendulum import Pendulum + + +class VisualizeStates(Scene): + CONFIG = { + "coordinate_plane_config": { + "y_line_frequency": PI / 2, + # "x_line_frequency": PI / 2, + "x_line_frequency": 1, + "y_axis_config": { + # "unit_size": 1.75, + "unit_size": 1, + }, + "y_max": 4, + "faded_line_ratio": 4, + "background_line_style": { + "stroke_width": 1, + }, + }, + "little_pendulum_config": { + "length": 1, + "gravity": 4.9, + "weight_diameter": 0.3, + "include_theta_label": False, + "include_velocity_vector": True, + "angle_arc_config": { + "radius": 0.2, + }, + "velocity_vector_config": { + "max_tip_length_to_length_ratio": 0.35, + "max_stroke_width_to_length_ratio": 6, + }, + "velocity_vector_multiple": 0.25, + "max_velocity_vector_length_to_length_ratio": 0.8, + }, + "big_pendulum_config": { + "length": 1.7, + "gravity": 4.9, + "damping": 0.2, + "weight_diameter": 0.3, + "include_velocity_vector": True, + "angle_arc_config": { + "radius": 0.5, + }, + "initial_theta": 80 * DEGREES, + "omega": -1, + "set_theta_label_height_cap": True, + }, + # "n_thetas": 11, + # "n_omegas": 7, + "n_thetas": 3, + "n_omegas": 5, + "initial_grid_wait_time": 15, + } + + def construct(self): + self.initialize_grid_of_states() + self.initialize_plane() + + simple = True + if simple: + self.add(self.plane) + else: + self.show_all_states_evolving() + self.show_grid_of_states_creation() + self.collapse_grid_into_points() + self.show_state_with_pair_of_numbers() + self.show_acceleration_dependence() + self.show_evolution_from_a_start_state() + + def initialize_grid_of_states(self): + pendulums = VGroup() + rects = VGroup() + state_grid = VGroup() + thetas = self.get_initial_thetas() + omegas = self.get_initial_omegas() + for omega in omegas: + row = VGroup() + for theta in thetas: + rect = Rectangle( + height=3, + width=3, + stroke_color=WHITE, + stroke_width=2, + fill_color=DARKER_GREY, + fill_opacity=1, + ) + pendulum = Pendulum( + initial_theta=theta, + omega=omega, + top_point=rect.get_center(), + **self.little_pendulum_config, + ) + pendulum.add_velocity_vector() + pendulums.add(pendulum) + rects.add(rect) + state = VGroup(rect, pendulum) + state.rect = rect + state.pendulum = pendulum + row.add(state) + row.arrange(RIGHT, buff=0) + state_grid.add(row) + state_grid.arrange(UP, buff=0) + + state_grid.set_height(FRAME_HEIGHT) + state_grid.center() + state_grid.save_state(use_deepcopy=True) + + self.state_grid = state_grid + self.pendulums = pendulums + + def initialize_plane(self): + plane = self.plane = NumberPlane( + **self.coordinate_plane_config + ) + plane.axis_labels = VGroup( + plane.get_x_axis_label( + "\\theta", RIGHT, UL, buff=SMALL_BUFF + ), + plane.get_y_axis_label( + "\\dot \\theta", UP, DR, buff=SMALL_BUFF + ).set_color(YELLOW), + ) + for label in plane.axis_labels: + label.add_background_rectangle() + plane.add(plane.axis_labels) + + plane.y_axis.add_numbers(direction=DL) + + x_axis = plane.x_axis + label_texs = ["\\pi \\over 2", "\\pi", "3\\pi \\over 2", "\\tau"] + values = [PI / 2, PI, 3 * PI / 2, TAU] + x_axis.coordinate_labels = VGroup() + x_axis.add(x_axis.coordinate_labels) + for value, label_tex in zip(values, label_texs): + for u in [-1, 1]: + tex = label_tex + if u < 0: + tex = "-" + tex + label = TexMobject(tex) + label.scale(0.5) + if label.get_height() > 0.4: + label.set_height(0.4) + point = x_axis.number_to_point(u * value) + label.next_to(point, DR, SMALL_BUFF) + x_axis.coordinate_labels.add(label) + return plane + + def show_all_states_evolving(self): + state_grid = self.state_grid + pendulums = self.pendulums + + for pendulum in pendulums: + pendulum.start_swinging() + + self.add(state_grid) + self.wait(self.initial_grid_wait_time) + + def show_grid_of_states_creation(self): + state_grid = self.state_grid + pendulums = self.pendulums + + title = TextMobject("All states") + title.to_edge(UP, buff=MED_SMALL_BUFF) + self.all_states_title = title + + self.remove(state_grid) + state_grid.restore() + for pendulum in pendulums: + pendulum.end_swinging() + + state_grid.set_height( + FRAME_HEIGHT - title.get_height() - 2 * MED_SMALL_BUFF + ) + state_grid.to_edge(DOWN, buff=0) + + def place_at_top(state): + state.set_height(3) + state.center() + state.next_to(title, DOWN) + + middle_row = state_grid[len(state_grid) // 2] + middle_row_copy = middle_row.deepcopy() + right_column_copy = VGroup(*[ + row[-1] for row in state_grid + ]).deepcopy() + + for state in it.chain(middle_row_copy, right_column_copy): + place_at_top(state) + + self.add(title) + self.play( + ShowIncreasingSubsets(middle_row), + ShowIncreasingSubsets(middle_row_copy), + run_time=2, + ) + self.wait() + self.play( + ShowIncreasingSubsets(state_grid), + ShowIncreasingSubsets(right_column_copy), + run_time=2, + ) + self.remove(middle_row_copy) + self.remove(middle_row) + self.add(state_grid) + self.remove(right_column_copy) + self.play( + ReplacementTransform( + right_column_copy[-1], + state_grid[-1][-1], + remover=True + ) + ) + self.wait() + + def collapse_grid_into_points(self): + state_grid = self.state_grid + plane = self.plane + + dots = VGroup() + for row in state_grid: + for state in row: + dot = Dot( + self.get_state_point(state.pendulum), + radius=0.05, + color=YELLOW, + background_stroke_width=3, + background_stroke_color=BLACK, + ) + dot.state = state + dots.add(dot) + + self.add(plane) + self.remove(state_grid) + self.play( + ShowCreation(plane), + LaggedStart(*[ + TransformFromCopy(m1, m2) + for m1, m2 in zip( + VGroup(*it.chain(*state_grid)), + VGroup(*it.chain(*dots)), + ) + ], lag_ratio=0.1, run_time=4) + ) + self.wait() + + self.state_dots = dots + + def show_state_with_pair_of_numbers(self): + axis_labels = self.plane.axis_labels + + state = self.get_flexible_state_picture() + dot = self.get_state_controlling_dot(state) + h_line = always_redraw( + lambda: self.get_tracking_h_line(dot.get_center()) + ) + v_line = always_redraw( + lambda: self.get_tracking_v_line(dot.get_center()) + ) + + self.add(dot) + anims = [GrowFromPoint(state, dot.get_center())] + if hasattr(self, "state_dots"): + anims.append(FadeOut(self.state_dots)) + self.play(*anims) + + for line, label in zip([h_line, v_line], axis_labels): + # self.add(line, dot) + self.play( + ShowCreation(line), + ShowCreationThenFadeAround(label), + run_time=1 + ) + for vect in LEFT, 3 * UP: + self.play( + ApplyMethod( + dot.shift, vect, + rate_func=there_and_back, + run_time=2, + ) + ) + self.wait() + for vect in 2 * LEFT, 3 * UP, 2 * DR: + self.play(dot.shift, vect) + self.wait() + + self.state = state + self.state_dot = dot + self.h_line = h_line + self.v_line = v_line + + def show_acceleration_dependence(self): + ode = get_ode() + thetas = ode.get_parts_by_tex("\\theta") + thetas[0].set_color(RED) + thetas[1].set_color(YELLOW) + ode.move_to( + FRAME_WIDTH * RIGHT / 4 + + FRAME_HEIGHT * UP / 4, + ) + ode.add_background_rectangle_to_submobjects() + + self.play(Write(ode)) + self.wait() + self.play(FadeOut(ode)) + + def show_evolution_from_a_start_state(self): + state = self.state + dot = self.state_dot + + self.play( + Rotating( + dot, + about_point=dot.get_center() + UR, + rate_func=smooth, + ) + ) + + # Show initial trajectory + state.pendulum.clear_updaters(recursive=False) + self.tie_dot_position_to_state(dot, state) + state.pendulum.start_swinging() + trajectory = self.get_evolving_trajectory(dot) + self.add(trajectory) + for x in range(20): + self.wait() + + # Talk through start + trajectory.suspend_updating() + state.pendulum.end_swinging() + dot.clear_updaters() + self.tie_state_to_dot_position(state, dot) + + # alphas = np.linspace(0, 0.1, 100) + alphas = np.linspace(0, 1, 1000) + index = np.argmin([ + trajectory.point_from_proportion(a)[1] + for a in alphas + ]) + alpha = alphas[index] + sub_traj = trajectory.copy() + sub_traj.suspend_updating() + sub_traj.pointwise_become_partial(trajectory, 0, alpha) + sub_traj.match_style(trajectory) + sub_traj.set_stroke(width=3) + + self.wait() + self.play(dot.move_to, sub_traj.get_start()) + self.wait() + self.play( + ShowCreation(sub_traj), + UpdateFromFunc( + dot, lambda d: d.move_to(sub_traj.get_end()) + ), + run_time=6, + ) + self.wait() + + # Comment on physical velocity vs. space position + v_vect = state.pendulum.velocity_vector + v_line_copy = self.v_line.copy() + v_line_copy.clear_updaters() + v_line_copy.set_stroke(PINK, 5) + td_label = self.plane.axis_labels[1] + y_axis_copy = self.plane.y_axis.copy() + y_axis_copy.submobjects = [] + y_axis_copy.match_style(v_line_copy) + + self.play(ShowCreationThenFadeAround(v_vect)) + self.play( + ShowCreationThenFadeAround(td_label), + ShowCreationThenFadeOut(y_axis_copy) + ) + self.play(ShowCreationThenFadeOut(v_line_copy)) + self.wait() + + # Abstract vs. physical + abstract = TextMobject("Abstract") + abstract.scale(2) + abstract.to_corner(UR) + physical = TextMobject("Physical") + physical.next_to(state.get_top(), DOWN) + + self.play( + ApplyMethod( + self.plane.set_stroke, YELLOW, 1, + rate_func=there_and_back, + lag_ratio=0.01, + ), + Write(abstract), + Animation(state), + ) + self.wait() + self.play(FadeInFromDown(physical)) + self.wait() + + # Continue on spiral + sub_traj.resume_updating() + state.pendulum.clear_updaters(recursive=False) + state.pendulum.start_swinging() + dot.clear_updaters() + self.tie_dot_position_to_state(dot, state) + self.wait(20) + + # + def get_initial_thetas(self): + angle = 3 * PI / 4 + return np.linspace(-angle, angle, self.n_thetas) + + def get_initial_omegas(self): + return np.linspace(-1.5, 1.5, self.n_omegas) + + def get_state(self, pendulum): + return (pendulum.get_theta(), pendulum.get_omega()) + + def get_state_point(self, pendulum): + return self.plane.coords_to_point( + *self.get_state(pendulum) + ) + + def get_flexible_state_picture(self): + height = (FRAME_HEIGHT - SMALL_BUFF) / 2 + rect = Square( + side_length=height, + stroke_color=WHITE, + stroke_width=2, + fill_color="#111111", + fill_opacity=1, + ) + rect.to_corner(UL, buff=SMALL_BUFF / 2) + pendulum = Pendulum( + top_point=rect.get_center(), + **self.big_pendulum_config + ) + + state = VGroup(rect, pendulum) + state.rect = rect + state.pendulum = pendulum + return state + + def get_state_dot(self, state): + dot = Dot(color=PINK) + dot.move_to(self.get_state_point(state.pendulum)) + return dot + + def get_state_controlling_dot(self, state): + dot = self.get_state_dot(state) + self.tie_state_to_dot_position(state, dot) + return dot + + def tie_state_to_dot_position(self, state, dot): + def update_pendulum(pend): + theta, omega = self.plane.point_to_coords( + dot.get_center() + ) + pend.set_theta(theta) + pend.set_omega(omega) + return pend + state.pendulum.add_updater(update_pendulum) + state.pendulum.get_arc_angle_theta = \ + lambda: self.plane.x_axis.point_to_number(dot.get_center()) + return self + + def tie_dot_position_to_state(self, dot, state): + dot.add_updater(lambda d: d.move_to( + self.get_state_point(state.pendulum) + )) + return dot + + def get_tracking_line(self, point, axis, color=WHITE): + number = axis.point_to_number(point) + axis_point = axis.number_to_point(number) + return DashedLine( + axis_point, point, + dash_length=0.025, + color=color, + ) + + def get_tracking_h_line(self, point): + return self.get_tracking_line( + point, self.plane.y_axis, WHITE, + ) + + def get_tracking_v_line(self, point): + return self.get_tracking_line( + point, self.plane.x_axis, YELLOW, + ) + + def get_evolving_trajectory(self, mobject): + trajectory = VMobject() + trajectory.start_new_path(mobject.get_center()) + trajectory.set_stroke(RED, 1) + + def update_trajectory(traj): + point = mobject.get_center() + if not np.all(trajectory.points[-1] == point): + traj.add_smooth_curve_to(point) + trajectory.add_updater(update_trajectory) + return trajectory + + +class NewSceneName(Scene): + def construct(self): + pass diff --git a/active_projects/ode/part1/staging.py b/active_projects/ode/part1/staging.py index 833b5d59..f1523264 100644 --- a/active_projects/ode/part1/staging.py +++ b/active_projects/ode/part1/staging.py @@ -768,6 +768,11 @@ class ODEvsPDEinFrames(Scene): pass -class VisualizeStates(Scene): +class ReferencePiCollisionStateSpaces(Scene): + def construct(self): + pass + + +class NewSceneName(Scene): def construct(self): pass From fbcc3b85d3da4a4f7ce0cd7756c93df2075555b7 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sun, 24 Mar 2019 11:32:12 -0700 Subject: [PATCH 18/27] Fix DrawBorderThenFill --- manimlib/animation/creation.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/manimlib/animation/creation.py b/manimlib/animation/creation.py index d559fada..b3e5325c 100644 --- a/manimlib/animation/creation.py +++ b/manimlib/animation/creation.py @@ -64,10 +64,11 @@ class DrawBorderThenFill(Animation): def get_outline(self): outline = self.mobject.copy() outline.set_fill(opacity=0) - outline.set_stroke( - color=self.get_stroke_color(outline), - width=self.stroke_width - ) + for sm in outline.family_members_with_points(): + sm.set_stroke( + color=self.get_stroke_color(sm), + width=self.stroke_width + ) return outline def get_stroke_color(self, vmobject): From 22f8e24d41211554318abe0ff848c3b7da4ef9fe Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sun, 24 Mar 2019 11:33:04 -0700 Subject: [PATCH 19/27] Vector.scale was broken for vectors with length 0 --- manimlib/mobject/geometry.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/manimlib/mobject/geometry.py b/manimlib/mobject/geometry.py index 64366422..db594e87 100644 --- a/manimlib/mobject/geometry.py +++ b/manimlib/mobject/geometry.py @@ -557,6 +557,9 @@ class Arrow(Line): self.set_stroke_width_from_length() def scale(self, factor, **kwargs): + if self.get_length() == 0: + return self + has_tip = self.has_tip() has_start_tip = self.has_start_tip() if has_tip or has_start_tip: From ee04922400920789c32139f33af02d45c6d80c8c Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sun, 24 Mar 2019 11:33:23 -0700 Subject: [PATCH 20/27] Add options for bracket spacing in Matrix --- manimlib/mobject/matrix.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/manimlib/mobject/matrix.py b/manimlib/mobject/matrix.py index 17847562..a689729c 100644 --- a/manimlib/mobject/matrix.py +++ b/manimlib/mobject/matrix.py @@ -56,6 +56,8 @@ class Matrix(VMobject): CONFIG = { "v_buff": 0.8, "h_buff": 1.3, + "bracket_h_buff": MED_SMALL_BUFF, + "bracket_v_buff": MED_SMALL_BUFF, "add_background_rectangles_to_entries": False, "include_background_rectangle": False, "element_to_mobject": TexMobject, @@ -101,10 +103,12 @@ class Matrix(VMobject): def add_brackets(self): bracket_pair = TexMobject("\\big[", "\\big]") bracket_pair.scale(2) - bracket_pair.stretch_to_fit_height(self.get_height() + 0.5) + bracket_pair.stretch_to_fit_height( + self.get_height() + 2 * self.bracket_v_buff + ) l_bracket, r_bracket = bracket_pair.split() - l_bracket.next_to(self, LEFT) - r_bracket.next_to(self, RIGHT) + l_bracket.next_to(self, LEFT, self.bracket_h_buff) + r_bracket.next_to(self, RIGHT, self.bracket_h_buff) self.add(l_bracket, r_bracket) self.brackets = VGroup(l_bracket, r_bracket) return self From 876f2c34196a20ee23076e3049c64d0e9f12a376 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sun, 24 Mar 2019 11:34:07 -0700 Subject: [PATCH 21/27] Make BackgroundRectangle stroke_opacity 0 --- manimlib/mobject/mobject.py | 2 +- manimlib/mobject/shape_matchers.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 02e4d2b2..6eeeec74 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -1107,7 +1107,7 @@ class Mobject(Container): # Errors def throw_error_if_no_points(self): if self.has_no_points(): - message = "Cannot call Mobject.{}" +\ + message = "Cannot call Mobject.{} " +\ "for a Mobject with no points" caller_name = sys._getframe(1).f_code.co_name raise Exception(message.format(caller_name)) diff --git a/manimlib/mobject/shape_matchers.py b/manimlib/mobject/shape_matchers.py index d3e1e089..a0f46229 100644 --- a/manimlib/mobject/shape_matchers.py +++ b/manimlib/mobject/shape_matchers.py @@ -25,6 +25,7 @@ class BackgroundRectangle(SurroundingRectangle): CONFIG = { "color": BLACK, "stroke_width": 0, + "stroke_opacity": 0, "fill_opacity": 0.75, "buff": 0 } From a166f6b214acb62427e8d7c909de12294a2f6da6 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sun, 24 Mar 2019 11:35:03 -0700 Subject: [PATCH 22/27] Up to IntroduceVectorField scene of ode chapter 1 --- active_projects/ode/all_part1_scenes.py | 2 + active_projects/ode/part1/phase_space.py | 444 ++++++++++++++++-- .../ode/part1/shared_constructs.py | 9 + active_projects/ode/part1/staging.py | 14 +- 4 files changed, 431 insertions(+), 38 deletions(-) diff --git a/active_projects/ode/all_part1_scenes.py b/active_projects/ode/all_part1_scenes.py index f7a90980..7ab7b405 100644 --- a/active_projects/ode/all_part1_scenes.py +++ b/active_projects/ode/all_part1_scenes.py @@ -31,4 +31,6 @@ ALL_SCENE_CLASSES = [ SetAsideSeekingSolution, ReferencePiCollisionStateSpaces, VisualizeStates, + IntroduceVectorField, + BreakingSecondOrderIntoTwoFirstOrder, ] diff --git a/active_projects/ode/part1/phase_space.py b/active_projects/ode/part1/phase_space.py index 62f27397..3b0cfa67 100644 --- a/active_projects/ode/part1/phase_space.py +++ b/active_projects/ode/part1/phase_space.py @@ -3,6 +3,8 @@ from active_projects.ode.part1.shared_constructs import * from active_projects.ode.part1.pendulum import Pendulum +# TODO: Arguably separate the part showing many +# configurations with the part showing just one. class VisualizeStates(Scene): CONFIG = { "coordinate_plane_config": { @@ -36,7 +38,7 @@ class VisualizeStates(Scene): "max_velocity_vector_length_to_length_ratio": 0.8, }, "big_pendulum_config": { - "length": 1.7, + "length": 1.6, "gravity": 4.9, "damping": 0.2, "weight_diameter": 0.3, @@ -48,21 +50,21 @@ class VisualizeStates(Scene): "omega": -1, "set_theta_label_height_cap": True, }, - # "n_thetas": 11, - # "n_omegas": 7, - "n_thetas": 3, - "n_omegas": 5, + "n_thetas": 11, + "n_omegas": 7, + # "n_thetas": 5, + # "n_omegas": 3, "initial_grid_wait_time": 15, } def construct(self): - self.initialize_grid_of_states() self.initialize_plane() - simple = True + simple = False if simple: self.add(self.plane) else: + self.initialize_grid_of_states() self.show_all_states_evolving() self.show_grid_of_states_creation() self.collapse_grid_into_points() @@ -159,18 +161,14 @@ class VisualizeStates(Scene): self.wait(self.initial_grid_wait_time) def show_grid_of_states_creation(self): + self.remove(self.state_grid) + self.initialize_grid_of_states() # Again state_grid = self.state_grid - pendulums = self.pendulums title = TextMobject("All states") title.to_edge(UP, buff=MED_SMALL_BUFF) self.all_states_title = title - self.remove(state_grid) - state_grid.restore() - for pendulum in pendulums: - pendulum.end_swinging() - state_grid.set_height( FRAME_HEIGHT - title.get_height() - 2 * MED_SMALL_BUFF ) @@ -195,12 +193,14 @@ class VisualizeStates(Scene): ShowIncreasingSubsets(middle_row), ShowIncreasingSubsets(middle_row_copy), run_time=2, + rate_func=linear, ) self.wait() self.play( ShowIncreasingSubsets(state_grid), ShowIncreasingSubsets(right_column_copy), run_time=2, + rate_func=linear, ) self.remove(middle_row_copy) self.remove(middle_row) @@ -234,16 +234,19 @@ class VisualizeStates(Scene): self.add(plane) self.remove(state_grid) + flat_state_group = VGroup(*it.chain(*state_grid)) + flat_dot_group = VGroup(*it.chain(*dots)) + self.clear() # The nuclear option self.play( ShowCreation(plane), + FadeOut(self.all_states_title), LaggedStart(*[ TransformFromCopy(m1, m2) - for m1, m2 in zip( - VGroup(*it.chain(*state_grid)), - VGroup(*it.chain(*dots)), - ) + for m1, m2 in zip(flat_state_group, flat_dot_group) ], lag_ratio=0.1, run_time=4) ) + self.clear() # Again, not sure why I need this + self.add(plane, dots) self.wait() self.state_dots = dots @@ -282,8 +285,8 @@ class VisualizeStates(Scene): ) ) self.wait() - for vect in 2 * LEFT, 3 * UP, 2 * DR: - self.play(dot.shift, vect) + for vect in 2 * LEFT, 3 * UP, 2 * RIGHT, 2 * DOWN: + self.play(dot.shift, vect, run_time=1.5) self.wait() self.state = state @@ -317,6 +320,7 @@ class VisualizeStates(Scene): rate_func=smooth, ) ) + self.wait() # Show initial trajectory state.pendulum.clear_updaters(recursive=False) @@ -333,8 +337,7 @@ class VisualizeStates(Scene): dot.clear_updaters() self.tie_state_to_dot_position(state, dot) - # alphas = np.linspace(0, 0.1, 100) - alphas = np.linspace(0, 1, 1000) + alphas = np.linspace(0, 0.1, 1000) index = np.argmin([ trajectory.point_from_proportion(a)[1] for a in alphas @@ -378,6 +381,7 @@ class VisualizeStates(Scene): # Abstract vs. physical abstract = TextMobject("Abstract") + abstract.add_background_rectangle() abstract.scale(2) abstract.to_corner(UR) physical = TextMobject("Physical") @@ -385,11 +389,11 @@ class VisualizeStates(Scene): self.play( ApplyMethod( - self.plane.set_stroke, YELLOW, 1, + self.plane.set_stroke, YELLOW, 0.5, rate_func=there_and_back, - lag_ratio=0.01, + lag_ratio=0.2, ), - Write(abstract), + FadeInFromDown(abstract), Animation(state), ) self.wait() @@ -421,7 +425,8 @@ class VisualizeStates(Scene): ) def get_flexible_state_picture(self): - height = (FRAME_HEIGHT - SMALL_BUFF) / 2 + buff = MED_SMALL_BUFF + height = FRAME_HEIGHT / 2 - buff rect = Square( side_length=height, stroke_color=WHITE, @@ -429,7 +434,7 @@ class VisualizeStates(Scene): fill_color="#111111", fill_opacity=1, ) - rect.to_corner(UL, buff=SMALL_BUFF / 2) + rect.to_corner(UL, buff=buff / 2) pendulum = Pendulum( top_point=rect.get_center(), **self.big_pendulum_config @@ -495,12 +500,393 @@ class VisualizeStates(Scene): def update_trajectory(traj): point = mobject.get_center() - if not np.all(trajectory.points[-1] == point): + if get_norm(trajectory.points[-1] == point) > 0.05: traj.add_smooth_curve_to(point) trajectory.add_updater(update_trajectory) return trajectory -class NewSceneName(Scene): +class IntroduceVectorField(VisualizeStates): + CONFIG = { + "vector_field_config": { + "max_magnitude": 3, + # "delta_x": 2, + # "delta_y": 2, + }, + "big_pendulum_config": { + "initial_theta": -60 * DEGREES, + "omega": 1, + } + } + def construct(self): - pass + self.initialize_plane() + self.add_flexible_state() + self.initialize_vector_field() + self.add_equation() + self.preview_vector_field() + self.write_vector_derivative() + self.interpret_first_coordinate() + self.interpret_second_coordinate() + self.show_full_vector_field() + self.show_trajectory() + + def initialize_plane(self): + super().initialize_plane() + self.add(self.plane) + + def initialize_vector_field(self): + self.vector_field = VectorField( + self.vector_field_func, + **self.vector_field_config, + ) + self.vector_field.sort(get_norm) + + def add_flexible_state(self): + self.state = self.get_flexible_state_picture() + self.add(self.state) + + def add_equation(self): + ode = get_ode() + ode.set_width(self.state.get_width() - MED_LARGE_BUFF) + ode.next_to(self.state.get_top(), DOWN, SMALL_BUFF) + thetas = ode.get_parts_by_tex("\\theta") + thetas[0].set_color(RED) + thetas[1].set_color(YELLOW) + ode_word = TextMobject("Differential equation") + ode_word.match_width(ode) + ode_word.next_to(ode, DOWN) + + self.play( + FadeInFrom(ode, 0.5 * DOWN), + FadeInFrom(ode_word, 0.5 * UP), + ) + + self.ode = ode + self.ode_word = ode_word + + def preview_vector_field(self): + vector_field = self.vector_field + + growth = LaggedStartMap( + GrowArrow, vector_field, + run_time=3, + lag_ratio=0.01, + ) + self.add( + growth.mobject, + vector_field, + self.state, self.ode, self.ode_word + ) + + self.play(growth) + self.wait() + self.play(FadeOut(vector_field)) + self.remove(growth.mobject) + + def write_vector_derivative(self): + state = self.state + plane = self.plane + + dot = self.get_state_dot(state) + + # Vector + vect = Arrow( + plane.coords_to_point(0, 0), + dot.get_center(), + buff=0, + color=dot.get_color() + ) + vect_sym, d_vect_sym = [ + self.get_vector_symbol( + "{" + a + "\\theta}(t)", + "{" + b + "\\theta}(t)", + ) + for a, b in [("", "\\dot"), ("\\dot", "\\ddot")] + ] + # vect_sym.get_entries()[1][0][1].set_color(YELLOW) + # d_vect_sym.get_entries()[0][0][1].set_color(YELLOW) + # d_vect_sym.get_entries()[1][0][1].set_color(RED) + vect_sym.next_to(vect.get_end(), UP, MED_LARGE_BUFF) + time_inputs = VGroup(*[ + e[-1][-2] for e in vect_sym.get_entries() + ]) + + # Derivative + ddt = TexMobject("d \\over dt") + ddt.set_height(0.9 * vect_sym.get_height()) + ddt.next_to(vect_sym, LEFT) + ddt.set_stroke(BLACK, 5, background=True) + equals = TexMobject("=") + equals.add_background_rectangle() + equals.next_to(vect_sym, RIGHT, SMALL_BUFF) + d_vect_sym.next_to(equals, RIGHT, SMALL_BUFF) + + # Little vector + angle_tracker = ValueTracker(0) + mag_tracker = ValueTracker(0.75) + d_vect = always_redraw( + lambda: Vector( + rotate_vector( + mag_tracker.get_value() * RIGHT, + angle_tracker.get_value(), + ), + color=WHITE + ).shift(dot.get_center()), + ) + d_vect_magnitude_factor_tracker = ValueTracker(2) + real_d_vect = always_redraw( + lambda: self.vector_field.get_vector( + dot.get_center() + ).scale( + d_vect_magnitude_factor_tracker.get_value(), + about_point=dot.get_center() + ) + ) + + # Show vector + self.play(TransformFromCopy(state[1], vect)) + self.play(FadeInFromDown(vect_sym)) + self.wait() + self.play(ReplacementTransform(vect, dot)) + self.wait() + self.play(LaggedStartMap( + ShowCreationThenFadeAround, time_inputs, + lag_ratio=0.1, + )) + self.wait() + + # Write Derivative + self.play(Write(ddt)) + self.play( + plane.y_axis.numbers.fade, 1, + FadeInFrom(equals, LEFT), + TransformFromCopy(vect_sym, d_vect_sym) + ) + self.wait() + + # Show as little vector + equation_group = VGroup( + ddt, vect_sym, equals, d_vect_sym + ) + self.play( + # equation_group.shift, 4 * DOWN, + equation_group.to_edge, RIGHT, LARGE_BUFF, + GrowArrow(d_vect), + ) + self.wait() + self.play(angle_tracker.set_value, 120 * DEGREES) + self.play(mag_tracker.set_value, 1.5) + self.wait() + + # Highlight new vector + self.play( + ShowCreationThenFadeAround(d_vect_sym), + FadeOut(d_vect) + ) + self.wait() + self.play( + TransformFromCopy(d_vect_sym, real_d_vect), + dot.set_color, WHITE, + ) + self.wait() + + # Take a walk + trajectory = VMobject() + trajectory.start_new_path(dot.get_center()) + dt = 0.01 + for x in range(130): + p = trajectory.points[-1] + dp_dt = self.vector_field_func(p) + trajectory.add_smooth_curve_to(p + dp_dt * dt) + self.tie_state_to_dot_position(state, dot) + self.play( + MoveAlongPath(dot, trajectory), + run_time=5, + rate_func=bezier([0, 0, 1, 1]), + ) + + self.state_dot = dot + self.d_vect = real_d_vect + self.equation_group = equation_group + self.d_vect_magnitude_factor_tracker = d_vect_magnitude_factor_tracker + + def interpret_first_coordinate(self): + equation = self.equation_group + ddt, vect_sym, equals, d_vect_sym = equation + dot = self.state_dot + + first_components_copy = VGroup( + vect_sym.get_entries()[0], + d_vect_sym.get_entries()[0], + ).copy() + rect = SurroundingRectangle(first_components_copy) + rect.set_stroke(YELLOW, 2) + + equation.save_state() + + self.play( + ShowCreation(rect), + equation.fade, 0.5, + Animation(first_components_copy), + ) + self.wait() + dot.save_state() + self.play(dot.shift, 2 * UP) + self.wait() + self.play(dot.shift, 6 * DOWN) + self.wait() + self.play(dot.restore) + self.wait() + + self.play( + equation.restore, + FadeOut(rect), + ) + self.remove(first_components_copy) + + def interpret_second_coordinate(self): + equation = self.equation_group + ddt, vect_sym, equals, d_vect_sym = equation + + second_components = VGroup( + vect_sym.get_entries()[1], + d_vect_sym.get_entries()[1], + ) + rect = SurroundingRectangle(second_components) + rect.set_stroke(YELLOW, 2) + + expanded_derivative = self.get_vector_symbol( + "{\\dot\\theta}(t)", + "-\\mu {\\dot\\theta}(t)" + + "-(g / L) \\sin\\big({\\theta}(t)\\big)", + ) + expanded_derivative.move_to(d_vect_sym) + expanded_derivative.to_edge(RIGHT, MED_SMALL_BUFF) + equals2 = TexMobject("=") + equals2.next_to(expanded_derivative, LEFT, SMALL_BUFF) + + equation.save_state() + self.play( + ShowCreation(rect), + ) + self.wait() + self.play( + FadeInFrom(expanded_derivative, LEFT), + FadeIn(equals2), + equation.next_to, equals2, LEFT, SMALL_BUFF, + MaintainPositionRelativeTo(rect, equation), + VFadeOut(rect), + ) + self.wait() + + self.full_equation = VGroup( + *equation, equals2, expanded_derivative, + ) + + def show_full_vector_field(self): + vector_field = self.vector_field + state = self.state + ode = self.ode + ode_word = self.ode_word + equation = self.full_equation + d_vect = self.d_vect + dot = self.state_dot + + equation.generate_target() + equation.target.scale(0.7) + equation.target.to_edge(DOWN, LARGE_BUFF) + equation.target.to_edge(LEFT, MED_SMALL_BUFF) + equation_rect = BackgroundRectangle(equation.target) + + growth = LaggedStartMap( + GrowArrow, vector_field, + run_time=3, + lag_ratio=0.01, + ) + self.add( + growth.mobject, + state, ode, ode_word, + equation_rect, equation, dot, + d_vect, + ) + self.play( + growth, + FadeIn(equation_rect), + MoveToTarget(equation), + self.d_vect_magnitude_factor_tracker.set_value, 1, + ) + + def show_trajectory(self): + state = self.state + dot = self.state_dot + + state.pendulum.clear_updaters(recursive=False) + self.tie_dot_position_to_state(dot, state) + state.pendulum.start_swinging() + + trajectory = self.get_evolving_trajectory(dot) + trajectory.set_stroke(WHITE, 3) + + self.add(trajectory, dot) + self.wait(25) + + # + def get_vector_symbol(self, tex1, tex2): + return Matrix( + [[tex1], [tex2]], + include_background_rectangle=True, + bracket_h_buff=SMALL_BUFF, + bracket_v_buff=SMALL_BUFF, + element_to_mobject_config={ + "tex_to_color_map": { + "{\\dot\\theta}": YELLOW, + "{\\ddot\\theta}": RED, + } + }, + element_alignment_corner=ORIGIN, + ).scale(0.9) + + def vector_field_func(self, point): + x, y = self.plane.point_to_coords(point) + pend = self.state.pendulum + return pendulum_vector_field_func( + x * RIGHT + y * UP, + mu=pend.damping, + g=pend.gravity, + L=pend.length, + ) + + def ask_about_change(self): + state = self.state + + dot = self.get_state_dot(state) + d_vect = Vector(0.75 * RIGHT, color=WHITE) + d_vect.shift(dot.get_center()) + q_mark = always_redraw( + lambda: TexMobject("?").move_to( + d_vect.get_end() + 0.4 * rotate_vector( + d_vect.get_vector(), 90 * DEGREES, + ), + ) + ) + + self.play(TransformFromCopy(state[1], dot)) + self.tie_state_to_dot_position(state, dot) + self.play( + GrowArrow(d_vect), + FadeInFromDown(q_mark) + ) + for x in range(4): + angle = 90 * DEGREES + self.play( + Rotate( + d_vect, angle, + about_point=d_vect.get_start(), + ) + ) + self.play( + dot.shift, + 0.3 * d_vect.get_vector(), + rate_func=there_and_back, + ) diff --git a/active_projects/ode/part1/shared_constructs.py b/active_projects/ode/part1/shared_constructs.py index 4a2f40b7..52ee9ca8 100644 --- a/active_projects/ode/part1/shared_constructs.py +++ b/active_projects/ode/part1/shared_constructs.py @@ -30,3 +30,12 @@ def get_ode(): **tex_config, ) return ode + + +def pendulum_vector_field_func(point, mu=0.1, g=9.8, L=3): + theta, omega = point[:2] + return np.array([ + omega, + -np.sqrt(g / L) * np.sin(theta) - mu * omega, + 0, + ]) diff --git a/active_projects/ode/part1/staging.py b/active_projects/ode/part1/staging.py index f1523264..da7533e4 100644 --- a/active_projects/ode/part1/staging.py +++ b/active_projects/ode/part1/staging.py @@ -4,15 +4,6 @@ from active_projects.ode.part1.pendulum import Pendulum from active_projects.ode.part1.pendulum import ThetaVsTAxes -def pendulum_vector_field(point, mu=0.1, g=9.8, L=3): - theta, omega = point[:2] - return np.array([ - omega, - -np.sqrt(g / L) * np.sin(theta) - mu * omega, - 0, - ]) - - # Scenes @@ -773,6 +764,11 @@ class ReferencePiCollisionStateSpaces(Scene): pass +class BreakingSecondOrderIntoTwoFirstOrder(Scene): + def construct(self): + pass + + class NewSceneName(Scene): def construct(self): pass From 708451964a25880daffec8f6f7d380e655967d45 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 25 Mar 2019 09:47:45 -0700 Subject: [PATCH 23/27] Up to ShowHighVelocityCase in ode part 1 --- active_projects/ode/all_part1_scenes.py | 4 + active_projects/ode/part1/pendulum.py | 3 + active_projects/ode/part1/phase_space.py | 129 +++++++++++++++++++++-- active_projects/ode/part1/staging.py | 76 ++++++++++++- 4 files changed, 204 insertions(+), 8 deletions(-) diff --git a/active_projects/ode/all_part1_scenes.py b/active_projects/ode/all_part1_scenes.py index 7ab7b405..9f4592cb 100644 --- a/active_projects/ode/all_part1_scenes.py +++ b/active_projects/ode/all_part1_scenes.py @@ -33,4 +33,8 @@ ALL_SCENE_CLASSES = [ VisualizeStates, IntroduceVectorField, BreakingSecondOrderIntoTwoFirstOrder, + ShowPendulumPhaseFlow, + ShowHighVelocityCase, + TweakMuInFormula, + TweakMuInVectorField, ] diff --git a/active_projects/ode/part1/pendulum.py b/active_projects/ode/part1/pendulum.py index 39a0360d..5feba2d6 100644 --- a/active_projects/ode/part1/pendulum.py +++ b/active_projects/ode/part1/pendulum.py @@ -90,6 +90,9 @@ class Pendulum(VGroup): self.get_fixed_point() + self.length * DOWN, **self.dashed_line_config ) + line.add_updater( + lambda l: l.move_to(self.get_fixed_point(), UP) + ) self.add_to_back(line) def create_angle_arc(self): diff --git a/active_projects/ode/part1/phase_space.py b/active_projects/ode/part1/phase_space.py index 3b0cfa67..ffd722c7 100644 --- a/active_projects/ode/part1/phase_space.py +++ b/active_projects/ode/part1/phase_space.py @@ -439,6 +439,9 @@ class VisualizeStates(Scene): top_point=rect.get_center(), **self.big_pendulum_config ) + pendulum.fixed_point_tracker.add_updater( + lambda m: m.move_to(rect.get_center()) + ) state = VGroup(rect, pendulum) state.rect = rect @@ -514,7 +517,7 @@ class IntroduceVectorField(VisualizeStates): # "delta_y": 2, }, "big_pendulum_config": { - "initial_theta": -60 * DEGREES, + "initial_theta": -80 * DEGREES, "omega": 1, } } @@ -840,21 +843,24 @@ class IntroduceVectorField(VisualizeStates): bracket_v_buff=SMALL_BUFF, element_to_mobject_config={ "tex_to_color_map": { + "{\\theta}": BLUE, "{\\dot\\theta}": YELLOW, + "{\\omega}": YELLOW, "{\\ddot\\theta}": RED, - } + }, }, element_alignment_corner=ORIGIN, ).scale(0.9) def vector_field_func(self, point): x, y = self.plane.point_to_coords(point) - pend = self.state.pendulum + mu, g, L = [ + self.big_pendulum_config.get(key) + for key in ["damping", "gravity", "length"] + ] return pendulum_vector_field_func( x * RIGHT + y * UP, - mu=pend.damping, - g=pend.gravity, - L=pend.length, + mu=mu, g=g, L=L ) def ask_about_change(self): @@ -890,3 +896,114 @@ class IntroduceVectorField(VisualizeStates): 0.3 * d_vect.get_vector(), rate_func=there_and_back, ) + + +class ShowPendulumPhaseFlow(IntroduceVectorField): + CONFIG = { + "coordinate_plane_config": { + "x_axis_config": { + "unit_size": 0.8, + }, + "x_max": 9, + "x_min": -9, + }, + "flow_time": 20, + } + + def construct(self): + self.initialize_plane() + self.initialize_vector_field() + plane = self.plane + field = self.vector_field + self.add(plane, field) + + stream_lines = StreamLines( + field.func, + delta_x=0.3, + delta_y=0.3, + ) + animated_stream_lines = AnimatedStreamLines( + stream_lines, + line_anim_class=ShowPassingFlashWithThinningStrokeWidth, + ) + + self.add(animated_stream_lines) + self.wait(self.flow_time) + + +class ShowHighVelocityCase(ShowPendulumPhaseFlow, MovingCameraScene): + CONFIG = { + "coordinate_plane_config": { + "x_max": 15, + "x_min": -15, + }, + "vector_field_config": { + "x_max": 15, + }, + "big_pendulum_config": { + "max_velocity_vector_length_to_length_ratio": 1, + }, + } + + def setup(self): + MovingCameraScene.setup(self) + + def construct(self): + self.initialize_plane() + self.add(self.plane) + self.initialize_vector_field() + self.add(self.vector_field) + self.add_flexible_state() + self.show_trajectory() + + def show_trajectory(self): + state = self.state + plane = self.plane + field = self.vector_field + frame = self.camera_frame + + state.to_edge(DOWN, buff=SMALL_BUFF), + start_point = plane.coords_to_point(0, 4) + dot = self.get_state_controlling_dot(state) + dot.move_to(start_point) + state.update() + + traj = VMobject() + traj.start_new_path(start_point) + dt = 0.01 + total_time = 25 + for x in range(int(total_time / dt)): + end = traj.points[-1] + dp_dt = field.func(end) + traj.add_smooth_curve_to(end + dp_dt * dt) + + traj.set_stroke(WHITE, 2) + + self.add(traj, dot) + self.play( + ShowCreation( + traj, + rate_func=linear, + ), + UpdateFromFunc( + dot, lambda d: d.move_to(traj.points[-1]) + ), + ApplyMethod( + frame.shift, TAU * RIGHT, + rate_func=squish_rate_func( + smooth, 0, 0.3, + ) + ), + MaintainPositionRelativeTo(state.rect, frame), + run_time=total_time, + ) + + +class TweakMuInFormula(Scene): + def construct(self): + pass + + +class TweakMuInVectorField(Scene): + def construct(self): + pass diff --git a/active_projects/ode/part1/staging.py b/active_projects/ode/part1/staging.py index da7533e4..78c4a23a 100644 --- a/active_projects/ode/part1/staging.py +++ b/active_projects/ode/part1/staging.py @@ -2,6 +2,7 @@ from big_ol_pile_of_manim_imports import * from active_projects.ode.part1.shared_constructs import * from active_projects.ode.part1.pendulum import Pendulum from active_projects.ode.part1.pendulum import ThetaVsTAxes +from active_projects.ode.part1.phase_space import IntroduceVectorField # Scenes @@ -764,9 +765,80 @@ class ReferencePiCollisionStateSpaces(Scene): pass -class BreakingSecondOrderIntoTwoFirstOrder(Scene): +class BreakingSecondOrderIntoTwoFirstOrder(IntroduceVectorField): def construct(self): - pass + ode = TexMobject( + "{\\ddot\\theta}", "(t)", "=", + "-\\mu", "{\\dot\\theta}", "(t)" + "-(g / L)\\sin\\big(", "{\\theta}", "(t)\\big)", + tex_to_color_map={ + "{\\ddot\\theta}": RED, + "{\\dot\\theta}": YELLOW, + "{\\theta}": BLUE, + # "{t}": WHITE, + } + ) + so_word = TextMobject("Second order ODE") + sys_word = TextMobject("System of two first order ODEs") + + system1 = self.get_system("{\\theta}", "{\\dot\\theta}") + system2 = self.get_system("{\\theta}", "{\\omega}") + + so_word.to_edge(UP) + ode.next_to(so_word, DOWN) + sys_word.move_to(ORIGIN) + system1.next_to(sys_word, DOWN) + system2.move_to(system1) + + self.add(ode) + self.play(FadeInFrom(so_word, 0.5 * DOWN)) + self.wait() + + self.play( + TransformFromCopy( + ode[3:], system1[3].get_entries()[1], + ), + TransformFromCopy(ode[2], system1[2]), + TransformFromCopy( + ode[:2], VGroup( + system1[0], + system1[1].get_entries()[1], + ) + ), + ) + self.play( + FadeIn(system1[1].get_brackets()), + FadeIn(system1[1].get_entries()[0]), + FadeIn(system1[3].get_brackets()), + FadeIn(system1[3].get_entries()[0]), + ) + self.play( + FadeInFromDown(sys_word) + ) + self.wait() + + self.play(ReplacementTransform(system1, system2)) + self.wait() + + def get_system(self, tex1, tex2): + system = VGroup( + TexMobject("d \\over dt"), + self.get_vector_symbol( + tex1 + "(t)", + tex2 + "(t)", + ), + TexMobject("="), + self.get_vector_symbol( + tex2 + "(t)", + "".join([ + "-\\mu", tex2, "(t)", + "-(g / L) \\sin\\big(", + tex1, "(t)", "\\big)", + ]) + ) + ) + system.arrange(RIGHT) + return system class NewSceneName(Scene): From ec1436ef5aed5c4d958e43cce48600e408647aed Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 25 Mar 2019 16:43:16 -0700 Subject: [PATCH 24/27] Changed default for Interval numbering --- manimlib/mobject/number_line.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/manimlib/mobject/number_line.py b/manimlib/mobject/number_line.py index 653c9a19..df372e3e 100644 --- a/manimlib/mobject/number_line.py +++ b/manimlib/mobject/number_line.py @@ -183,4 +183,7 @@ class UnitInterval(NumberLine): "tick_frequency": 0.1, "numbers_with_elongated_ticks": [0, 1], "number_at_center": 0.5, + "decimal_number_config": { + "num_decimal_places": 1, + } } From 9c381d15b6e66ca96b44c590391b3a0cb5eeaedb Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 25 Mar 2019 16:43:40 -0700 Subject: [PATCH 25/27] ThreeDScene was still using continual animation constructs --- manimlib/scene/three_d_scene.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/manimlib/scene/three_d_scene.py b/manimlib/scene/three_d_scene.py index ccf047d8..da120610 100644 --- a/manimlib/scene/three_d_scene.py +++ b/manimlib/scene/three_d_scene.py @@ -42,6 +42,7 @@ class ThreeDScene(Scene): def stop_ambient_camera_rotation(self): self.camera.theta_tracker.clear_updaters() + self.remove(self.camera.theta_tracker) def move_camera(self, phi=None, @@ -68,12 +69,8 @@ class ThreeDScene(Scene): self.camera.frame_center.move_to, frame_center )) - is_camera_rotating = self.ambient_camera_rotation in self.continual_animations - if is_camera_rotating: - self.remove(self.ambient_camera_rotation) + self.play(*anims + added_anims) - if is_camera_rotating: - self.add(self.ambient_camera_rotation) def get_moving_mobjects(self, *animations): moving_mobjects = Scene.get_moving_mobjects(self, *animations) From 9a452f3fee83591ea4d5763ad91b85a64a8beedf Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 25 Mar 2019 16:44:08 -0700 Subject: [PATCH 26/27] Up to 3 body problem illustration --- active_projects/ode/all_part1_scenes.py | 5 + active_projects/ode/part1/phase_space.py | 138 +++++++++++++-- active_projects/ode/part1/pi_scenes.py | 14 ++ .../ode/part1/shared_constructs.py | 19 +- active_projects/ode/part1/staging.py | 162 ++++++++++++++++++ active_projects/ode/part1/wordy_scenes.py | 121 +++++++++++++ 6 files changed, 441 insertions(+), 18 deletions(-) diff --git a/active_projects/ode/all_part1_scenes.py b/active_projects/ode/all_part1_scenes.py index 9f4592cb..2d2306b5 100644 --- a/active_projects/ode/all_part1_scenes.py +++ b/active_projects/ode/all_part1_scenes.py @@ -37,4 +37,9 @@ ALL_SCENE_CLASSES = [ ShowHighVelocityCase, TweakMuInFormula, TweakMuInVectorField, + FromODEToVectorField, + LorenzVectorField, + ThreeBodiesInSpace, + ThreeBodySymbols, + AskAboutActuallySolving, ] diff --git a/active_projects/ode/part1/phase_space.py b/active_projects/ode/part1/phase_space.py index ffd722c7..3e7a0812 100644 --- a/active_projects/ode/part1/phase_space.py +++ b/active_projects/ode/part1/phase_space.py @@ -836,20 +836,17 @@ class IntroduceVectorField(VisualizeStates): # def get_vector_symbol(self, tex1, tex2): - return Matrix( - [[tex1], [tex2]], - include_background_rectangle=True, - bracket_h_buff=SMALL_BUFF, - bracket_v_buff=SMALL_BUFF, + t2c = { + "{\\theta}": BLUE, + "{\\dot\\theta}": YELLOW, + "{\\omega}": YELLOW, + "{\\ddot\\theta}": RED, + } + return get_vector_symbol( + tex1, tex2, element_to_mobject_config={ - "tex_to_color_map": { - "{\\theta}": BLUE, - "{\\dot\\theta}": YELLOW, - "{\\omega}": YELLOW, - "{\\ddot\\theta}": RED, - }, - }, - element_alignment_corner=ORIGIN, + "tex_to_color_map": t2c, + } ).scale(0.9) def vector_field_func(self, point): @@ -1001,9 +998,118 @@ class ShowHighVelocityCase(ShowPendulumPhaseFlow, MovingCameraScene): class TweakMuInFormula(Scene): def construct(self): - pass + ode = get_ode() + ode.to_edge(DOWN, buff=LARGE_BUFF) + mu = ode.get_part_by_tex("\\mu") + lil_rect = SurroundingRectangle(mu, buff=0.5 * SMALL_BUFF) + lil_rect.stretch(1.2, 1, about_edge=DOWN) + lil_rect.set_stroke(PINK, 2) + + interval = UnitInterval() + interval.add_numbers( + *np.arange(0, 1.2, 0.2) + ) + interval.next_to(ode, UP, LARGE_BUFF) + big_rect_seed = SurroundingRectangle(interval, buff=MED_SMALL_BUFF) + big_rect_seed.stretch(1.5, 1, about_edge=DOWN) + big_rect_seed.stretch(1.2, 0) + big_rect = VGroup(*[ + DashedLine(v1, v2) + for v1, v2 in adjacent_pairs(big_rect_seed.get_vertices()) + ]) + big_rect.set_stroke(PINK, 2) + + arrow = Arrow( + lil_rect.get_top(), + big_rect_seed.point_from_proportion(0.65), + buff=SMALL_BUFF, + ) + arrow.match_color(lil_rect) + + mu_tracker = ValueTracker(0.1) + get_mu = mu_tracker.get_value + + triangle = Triangle( + start_angle=-90 * DEGREES, + stroke_width=0, + fill_opacity=1, + fill_color=WHITE, + ) + triangle.set_height(0.2) + triangle.add_updater(lambda t: t.next_to( + interval.number_to_point(get_mu()), + UP, buff=0, + )) + + equation = VGroup( + TexMobject("\\mu = "), + DecimalNumber(), + ) + equation.add_updater( + lambda e: e.arrange(RIGHT).next_to( + triangle, UP, SMALL_BUFF, + ).shift(0.4 * RIGHT) + ) + equation[-1].add_updater( + lambda d: d.set_value(get_mu()).shift(0.05 * UL) + ) + + self.add(ode) + self.play(ShowCreation(lil_rect)) + self.play( + GrowFromPoint(interval, mu.get_center()), + GrowFromPoint(triangle, mu.get_center()), + GrowFromPoint(equation, mu.get_center()), + TransformFromCopy(lil_rect, big_rect), + ShowCreation(arrow) + ) + self.wait() + self.play(mu_tracker.set_value, 0.9, run_time=5) + self.wait() -class TweakMuInVectorField(Scene): +class TweakMuInVectorField(ShowPendulumPhaseFlow): def construct(self): - pass + self.initialize_plane() + plane = self.plane + self.add(plane) + + mu_tracker = ValueTracker(0.1) + get_mu = mu_tracker.get_value + + def vector_field_func(p): + x, y = plane.point_to_coords(p) + mu = get_mu() + g = self.big_pendulum_config.get("gravity") + L = self.big_pendulum_config.get("length") + return pendulum_vector_field_func( + x * RIGHT + y * UP, + mu=mu, g=g, L=L + ) + + def get_vector_field(): + return VectorField( + vector_field_func, + **self.vector_field_config, + ) + + field = always_redraw(get_vector_field) + self.add(field) + + self.play( + mu_tracker.set_value, 0.9, + run_time=5, + ) + field.suspend_updating() + + stream_lines = StreamLines( + field.func, + delta_x=0.3, + delta_y=0.3, + ) + animated_stream_lines = AnimatedStreamLines( + stream_lines, + line_anim_class=ShowPassingFlashWithThinningStrokeWidth, + ) + self.add(animated_stream_lines) + self.wait(self.flow_time) diff --git a/active_projects/ode/part1/pi_scenes.py b/active_projects/ode/part1/pi_scenes.py index bd181753..284ff054 100644 --- a/active_projects/ode/part1/pi_scenes.py +++ b/active_projects/ode/part1/pi_scenes.py @@ -195,3 +195,17 @@ class ProveTeacherWrong(TeacherStudentsScene): self.teacher.change, "maybe" ) self.wait(8) + + +class AskAboutActuallySolving(Scene): + def construct(self): + ode = get_ode() + ode.to_corner(UL) + morty = self.teacher + + self.student_says( + "Yeah, yeah, but how do\\\\" + "you acutally \\emph{solve} it?", + target_mode="sassy", + added_anims=[morty.change, "thinking"], + ) diff --git a/active_projects/ode/part1/shared_constructs.py b/active_projects/ode/part1/shared_constructs.py index 52ee9ca8..265a4585 100644 --- a/active_projects/ode/part1/shared_constructs.py +++ b/active_projects/ode/part1/shared_constructs.py @@ -20,12 +20,15 @@ def get_ode(): tex_config = { "tex_to_color_map": { "{\\theta}": BLUE, + "{\\dot\\theta}": YELLOW, + "{\\ddot\\theta}": RED, "{t}": WHITE, + "{\\mu}": WHITE, } } ode = TexMobject( - "\\ddot {\\theta}({t})", "=", - "-\\mu \\dot {\\theta}({t})", + "{\\ddot\\theta}({t})", "=", + "-{\\mu} {\\dot\\theta}({t})", "-{g \\over L} \\sin\\big({\\theta}({t})\\big)", **tex_config, ) @@ -39,3 +42,15 @@ def pendulum_vector_field_func(point, mu=0.1, g=9.8, L=3): -np.sqrt(g / L) * np.sin(theta) - mu * omega, 0, ]) + + +def get_vector_symbol(*texs, **kwargs): + config = { + "include_background_rectangle": True, + "bracket_h_buff": SMALL_BUFF, + "bracket_v_buff": SMALL_BUFF, + "element_alignment_corner": ORIGIN, + } + config.update(kwargs) + array = [[tex] for tex in texs] + return Matrix(array, **config) diff --git a/active_projects/ode/part1/staging.py b/active_projects/ode/part1/staging.py index 78c4a23a..471586f4 100644 --- a/active_projects/ode/part1/staging.py +++ b/active_projects/ode/part1/staging.py @@ -841,6 +841,168 @@ class BreakingSecondOrderIntoTwoFirstOrder(IntroduceVectorField): return system +class FromODEToVectorField(Scene): + def construct(self): + matrix_config = { + "bracket_v_buff": 2 * SMALL_BUFF, + "element_to_mobject_config": { + "tex_to_color_map": { + "x": GREEN, + "y": RED, + "z": BLUE, + }, + } + } + vect = get_vector_symbol( + "x(t)", "y(t)", "z(t)", + **matrix_config, + ) + d_vect = get_vector_symbol( + "\\sigma\\big(y(t) - x(t)\\big)", + "x(t)\\big(\\rho - z(t)\\big) - y(t)", + "x(t)y(t) - \\beta z(t)", + **matrix_config + ) + equation = VGroup( + TexMobject("d \\over dt").scale(1.5), + vect, + TexMobject("="), + d_vect + ) + equation.scale(0.8) + equation.arrange(RIGHT) + equation.to_edge(UP) + + arrow = Vector(DOWN, color=YELLOW) + arrow.next_to(equation, DOWN) + + self.add(equation) + self.play(ShowCreation(arrow)) + self.wait() + + +class LorenzVectorField(ExternallyAnimatedScene): + pass + + +class ThreeBodiesInSpace(SpecialThreeDScene): + CONFIG = { + "masses": [1, 2, 3], + "G": 0.5, + "play_time": 60, + } + + def construct(self): + self.add_axes() + self.add_bodies() + self.add_trajectories() + self.let_play() + + def add_axes(self): + axes = self.axes = self.get_axes() + self.add(axes) + + def add_bodies(self): + bodies = self.bodies = VGroup() + velocity_vectors = VGroup() + + for mass in self.masses: + body = self.get_sphere( + checkerboard_colors=[DARK_BROWN, DARK_BROWN], + stroke_width=0.1, + ) + body.mass = mass + body.set_width(0.2 * mass) + + point = np.dot( + 2 * (np.random.random(3) - 0.5), + [RIGHT, UP, OUT] + ) + velocity = normalize(np.cross(point, OUT)) + body.move_to(point) + body.velocity = velocity + body.add_updater(self.update_body) + + vect = self.get_velocity_vector_mob(body) + + bodies.add(body) + velocity_vectors.add(vect) + + self.add(body) + # self.add(vect) + + total_mass = np.sum([body.mass for body in bodies]) + center_of_mass = reduce(op.add, [ + body.mass * body.get_center() / total_mass + for body in bodies + ]) + average_momentum = reduce(op.add, [ + body.mass * body.velocity / total_mass + for body in bodies + ]) + for body in bodies: + body.shift(-center_of_mass) + body.velocity -= average_momentum + + def add_trajectories(self): + def update_trajectory(traj, dt): + new_point = traj.body.get_center() + if get_norm(new_point - traj.points[-1]) > 0.01: + traj.add_smooth_curve_to(new_point) + + for body in self.bodies: + traj = VMobject() + traj.body = body + traj.start_new_path(body.get_center()) + traj.set_stroke(WHITE, 1) + traj.add_updater(update_trajectory) + self.add(traj, body) + + def let_play(self): + self.move_camera( + phi=70 * DEGREES, + theta=-110 * DEGREES, + run_time=3, + ) + self.begin_ambient_camera_rotation() + for x in range(6): + self.wait(self.play_time / 6) + + # + def get_velocity_vector_mob(self, body): + def draw_vector(): + center = body.get_center() + vect = Arrow( + center, + center + body.velocity, + buff=0, + color=RED, + ) + vect.set_shade_in_3d(True) + return vect + # length = vect.get_length() + # if length > 2: + # vect.scale( + # 2 / length, + # about_point=vect.get_start(), + # ) + return always_redraw(draw_vector) + + def update_body(self, body, dt): + G = self.G + acceleration = np.zeros(3) + for body2 in self.bodies: + if body2 is body: + continue + diff = body2.get_center() - body.get_center() + m2 = body2.mass + R = get_norm(diff) + acceleration += G * m2 * diff / (R**3) + + body.shift(body.velocity * dt) + body.velocity += acceleration * dt + + class NewSceneName(Scene): def construct(self): pass diff --git a/active_projects/ode/part1/wordy_scenes.py b/active_projects/ode/part1/wordy_scenes.py index e7ffae3c..6a966789 100644 --- a/active_projects/ode/part1/wordy_scenes.py +++ b/active_projects/ode/part1/wordy_scenes.py @@ -139,3 +139,124 @@ class SetAsideSeekingSolution(Scene): eyes.blink, rate_func=squish_rate_func(there_and_back) ) + + +class ThreeBodySymbols(Scene): + def construct(self): + self.init_coord_groups() + self.introduce_coord_groups() + self.count_coords() + + def init_coord_groups(self): + kwargs = { + "bracket_v_buff": 2 * SMALL_BUFF + } + positions = VGroup(*[ + get_vector_symbol(*[ + "{}_{}".format(s, i) + for s in "xyz" + ], **kwargs) + for i in range(1, 4) + ]) + velocities = VGroup(*[ + get_vector_symbol(*[ + "p^{}_{}".format(s, i) + for s in "xyz" + ], **kwargs) + for i in range(1, 4) + ]) + groups = VGroup(positions, velocities) + colors = [GREEN, RED, BLUE] + for group in groups: + for matrix in group: + matrix.coords = matrix.get_entries() + for coord, color in zip(matrix.coords, colors): + coord.set_color(color) + group.arrange(RIGHT) + groups.arrange(DOWN, buff=LARGE_BUFF) + groups.to_edge(LEFT) + + self.coord_groups = groups + + def introduce_coord_groups(self): + groups = self.coord_groups + x_group, p_group = groups + x_word = TextMobject("Positions") + p_word = TextMobject("Momenta") + words = VGroup(x_word, p_word) + for word, group in zip(words, groups): + word.next_to(group, UP) + + rect_groups = VGroup() + for group in groups: + rect_group = VGroup(*[ + SurroundingRectangle( + VGroup(*[ + tm.coords[i] + for tm in group + ]), + color=WHITE, + stroke_width=2, + ) + for i in range(3) + ]) + rect_groups.add(rect_group) + + self.play( + *[ + LaggedStartMap( + FadeInFrom, group, + lambda m: (m, UP), + run_time=1, + ) + for group in groups + ], + *map(FadeInFromDown, words), + ) + for rect_group in rect_groups: + self.play( + ShowCreationThenFadeOut( + rect_group, + lag_ratio=0.5, + ) + ) + self.wait() + + def count_coords(self): + coord_copies = VGroup() + for group in self.coord_groups: + for tex_mob in group: + for coord in tex_mob.coords: + coord_copy = coord.copy() + coord_copy.set_stroke( + WHITE, 2, background=True + ) + coord_copies.add(coord_copy) + + count = Integer() + count_word = TextMobject("18", "degrees \\\\ of freedom")[1] + count_group = VGroup(count, count_word) + count_group.arrange( + RIGHT, + aligned_edge=DOWN, + ) + count_group.scale(1.5) + count_group.next_to( + self.coord_groups, RIGHT, + aligned_edge=DOWN, + ) + count.add_updater( + lambda m: m.set_value(len(coord_copies)) + ) + count.add_updater( + lambda m: m.next_to(count_word[0][0], LEFT, aligned_edge=DOWN) + ) + + self.add(count_group) + self.play( + # ChangeDecimalToValue(count, len(coord_copies)), + ShowIncreasingSubsets(coord_copies), + run_time=1.5, + rate_func=linear, + ) + self.play(FadeOut(coord_copies)) From caf41f43d9d38c7283331a26c121caa17bc1ab03 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 26 Mar 2019 17:52:53 -0700 Subject: [PATCH 27/27] More scenes for ode1 --- active_projects/ode/all_part1_scenes.py | 8 +- active_projects/ode/part1/phase_space.py | 124 +++++++++ active_projects/ode/part1/pi_scenes.py | 177 ++++++++++++- active_projects/ode/part1/staging.py | 304 +++++++++++++++++++---- 4 files changed, 563 insertions(+), 50 deletions(-) diff --git a/active_projects/ode/all_part1_scenes.py b/active_projects/ode/all_part1_scenes.py index 2d2306b5..8065580c 100644 --- a/active_projects/ode/all_part1_scenes.py +++ b/active_projects/ode/all_part1_scenes.py @@ -17,7 +17,8 @@ ALL_SCENE_CLASSES = [ SmallAngleApproximationTex, VeryLowAnglePendulum, FormulasAreLies, - FollowThisThread, + TourOfDifferentialEquations, + # FollowThisThread, StrogatzQuote, # Something... ShowGravityAcceleration, @@ -40,6 +41,11 @@ ALL_SCENE_CLASSES = [ FromODEToVectorField, LorenzVectorField, ThreeBodiesInSpace, + AltThreeBodiesInSpace, ThreeBodySymbols, AskAboutActuallySolving, + WriteODESolvingCode, + TakeManyTinySteps, + InaccurateComputation, + HungerForExactness, ] diff --git a/active_projects/ode/part1/phase_space.py b/active_projects/ode/part1/phase_space.py index 3e7a0812..baf80d8d 100644 --- a/active_projects/ode/part1/phase_space.py +++ b/active_projects/ode/part1/phase_space.py @@ -1113,3 +1113,127 @@ class TweakMuInVectorField(ShowPendulumPhaseFlow): ) self.add(animated_stream_lines) self.wait(self.flow_time) + + +class TakeManyTinySteps(IntroduceVectorField): + CONFIG = { + "initial_theta": 60 * DEGREES, + "initial_theta_dot": 0, + } + + def construct(self): + self.initialize_plane() + self.initialize_vector_field() + field = self.vector_field + field.set_opacity(0.35) + self.add(self.plane, field) + + self.take_many_time_steps() + + def take_many_time_steps(self): + delta_t_tracker = ValueTracker(0.5) + get_delta_t = delta_t_tracker.get_value + + time_tracker = ValueTracker(10) + get_t = time_tracker.get_value + + traj = always_redraw( + lambda: self.get_time_step_trajectory( + get_delta_t(), get_t() + ) + ) + vectors = always_redraw( + lambda: self.get_path_vectors( + get_delta_t(), get_t() + ) + ) + + t_label, dt_label = labels = VGroup(*[ + VGroup( + TexMobject("{} = ".format(s)), + DecimalNumber(0) + ).arrange(RIGHT, aligned_edge=DOWN) + for s in ("t", "{\\Delta t}") + ]) + init_labels = VGroup( + TexMobject( + "\\theta_0", "= \\pi / 3", + tex_to_color_map={"\\theta": BLUE}, + ), + TexMobject( + "{\\dot\\theta}_0 = 0", + tex_to_color_map={"{\\dot\\theta}": YELLOW}, + ), + ) + for group in labels, init_labels: + for label in group: + label.scale(1.25) + label.add_background_rectangle() + group.arrange(DOWN) + group.shift(FRAME_WIDTH * RIGHT / 4) + labels.to_edge(UP) + init_labels.shift(2 * DOWN) + + dt_label[-1].add_updater( + lambda d: d.set_value(get_delta_t()) + ) + t_label[-1].add_updater( + lambda d: d.set_value( + int(get_t() / get_delta_t()) * get_delta_t() + ) + ) + + self.add(traj, vectors, init_labels, labels) + time_tracker.set_value(0) + self.play( + time_tracker.set_value, 10, + run_time=5, + rate_func=linear, + ) + self.wait() + t_label[-1].clear_updaters() + self.play( + delta_t_tracker.set_value, 0.01, + run_time=7, + ) + self.wait() + + # + def get_time_step_points(self, delta_t, total_time): + plane = self.plane + field = self.vector_field + curr_point = plane.coords_to_point( + self.initial_theta, + self.initial_theta_dot, + ) + points = [curr_point] + t = 0 + while t < total_time: + new_point = curr_point + field.func(curr_point) * delta_t + points.append(new_point) + curr_point = new_point + t += delta_t + return points + + def get_time_step_trajectory(self, delta_t, total_time): + traj = VMobject() + traj.set_points_as_corners( + self.get_time_step_points(delta_t, total_time) + ) + traj.set_stroke(WHITE, 2) + return traj + + def get_path_vectors(self, delta_t, total_time): + corners = self.get_time_step_points( + delta_t, total_time + ) + result = VGroup() + for a1, a2 in zip(corners, corners[1:]): + vector = Arrow( + a1, a2, buff=0, + ) + vector.match_style( + self.vector_field.get_vector(a1) + ) + result.add(vector) + return result diff --git a/active_projects/ode/part1/pi_scenes.py b/active_projects/ode/part1/pi_scenes.py index 284ff054..f3d304a3 100644 --- a/active_projects/ode/part1/pi_scenes.py +++ b/active_projects/ode/part1/pi_scenes.py @@ -103,6 +103,11 @@ class FormulasAreLies(PiCreatureScene): return You().flip().to_corner(DR) +# class TourOfDifferentialEquations(Scene): +# def construct(self): +# pass + + class ProveTeacherWrong(TeacherStudentsScene): def construct(self): tex_config = { @@ -197,15 +202,183 @@ class ProveTeacherWrong(TeacherStudentsScene): self.wait(8) -class AskAboutActuallySolving(Scene): +class AskAboutActuallySolving(TeacherStudentsScene): def construct(self): ode = get_ode() ode.to_corner(UL) + self.add(ode) morty = self.teacher self.student_says( - "Yeah, yeah, but how do\\\\" + "Yeah yeah, but how do\\\\" "you acutally \\emph{solve} it?", + student_index=1, target_mode="sassy", added_anims=[morty.change, "thinking"], ) + self.change_student_modes( + "confused", "sassy", "confused", + look_at_arg=ode, + ) + self.wait() + self.teacher_says( + "What do you mean\\\\ by ``solve''?", + target_mode="speaking", + added_anims=[self.get_student_changes( + *3 * ["erm"] + )] + ) + self.play(self.students[1].change, "angry") + self.wait(3) + + +class HungerForExactness(TeacherStudentsScene): + def construct(self): + students = self.students + you = students[2] + teacher = self.teacher + + ode = get_ode() + ode.to_corner(UL) + left_part = ode[:5] + friction_part = ode[5:11] + right_part = ode[11:] + self.add(ode) + frictionless_group = VGroup(left_part, right_part) + + proposed_solution = TexMobject( + "\\theta_0\\cos((\\sqrt{g/L})t)e^{-\\mu t}" + ) + proposed_solution.next_to( + you.get_corner(UL), UP, buff=0.7 + ) + proposed_solution_rect = SurroundingRectangle( + proposed_solution, buff=MED_SMALL_BUFF, + ) + proposed_solution_rect.set_color(BLUE) + proposed_solution_rect.round_corners() + + solution_p1 = TexMobject( + """ + \\theta(t) = 2\\text{am}\\left( + \\frac{\\sqrt{2g + Lc_1} (t + c_2)}{2\\sqrt{L}}, + \\frac{4g}{2g + Lc_1} + \\right) + """, + ) + solution_p1.to_corner(UL) + solution_p2 = TexMobject( + "c_1, c_2 = \\text{Constants depending on initial conditions}" + ) + solution_p2.set_color(LIGHT_GREY) + solution_p2.scale(0.75) + solution_p3 = TexMobject( + """ + \\text{am}(u, k) = + \\int_0^u \\text{dn}(v, k)\\,dv + """ + ) + solution_p3.name = TextMobject( + "(Jacobi amplitude function)" + ) + solution_p4 = TexMobject( + """ + \\text{dn}(u, k) = + \\sqrt{1 - k^2 \\sin^2(\\phi)} + """ + ) + solution_p4.name = TextMobject( + "(Jacobi elliptic function)" + ) + solution_p5 = TextMobject("Where $\\phi$ satisfies") + solution_p6 = TexMobject( + """ + u = \\int_0^\\phi \\frac{dt}{\\sqrt{1 - k^2 \\sin^2(t)}} + """ + ) + + solution = VGroup( + solution_p1, + solution_p2, + solution_p3, + solution_p4, + solution_p5, + solution_p6, + ) + solution.arrange(DOWN) + solution.scale(0.7) + solution.to_corner(UL, buff=MED_SMALL_BUFF) + solution.set_stroke(width=0, background=True) + + solution.remove(solution_p2) + solution_p1.add(solution_p2) + solution.remove(solution_p5) + solution_p6.add(solution_p5) + + for part in [solution_p3, solution_p4]: + part.name.scale(0.7 * 0.7) + part.name.set_color(LIGHT_GREY) + part.name.next_to(part, RIGHT) + part.add(part.name) + + self.student_says( + "Right, but like,\\\\" + "what \\emph{is} $\\theta(t)$?", + target_mode="sassy", + added_anims=[teacher.change, "guilty"], + ) + self.wait() + self.play( + FadeInFromDown(proposed_solution), + RemovePiCreatureBubble( + you, + target_mode="raise_left_hand", + look_at_arg=proposed_solution, + ), + teacher.change, "pondering", + students[0].change, "pondering", + students[1].change, "hesitant", + ) + self.play(ShowCreation(proposed_solution_rect)) + self.play( + proposed_solution.shift, 3 * RIGHT, + proposed_solution_rect.shift, 3 * RIGHT, + you.change, "raise_right_hand", teacher.eyes, + ) + self.wait(3) + + self.play( + FadeOut(proposed_solution), + FadeOut(proposed_solution_rect), + ode.move_to, self.hold_up_spot, DOWN, + ode.shift, LEFT, + teacher.change, "raise_right_hand", + self.get_student_changes(*3 * ["pondering"]) + ) + self.wait() + ode.save_state() + self.play( + left_part.move_to, friction_part, RIGHT, + left_part.match_y, left_part, + friction_part.to_corner, DR, + friction_part.fade, 0.5, + ) + self.wait() + + modes = ["erm", "sad", "sad", "horrified"] + for part, mode in zip(solution, modes): + self.play( + FadeInFrom(part, UP), + self.get_student_changes( + *3 * [mode], + look_at_arg=part, + ) + ) + self.wait() + self.wait(3) + self.play( + FadeOutAndShift(solution, 2 * LEFT), + Restore(ode), + self.get_student_changes(*3 * ["sick"]) + ) + self.wait(3) diff --git a/active_projects/ode/part1/staging.py b/active_projects/ode/part1/staging.py index 471586f4..8fb884da 100644 --- a/active_projects/ode/part1/staging.py +++ b/active_projects/ode/part1/staging.py @@ -39,28 +39,37 @@ class VectorFieldTest(Scene): self.wait(10) -class FollowThisThread(Scene): +class TourOfDifferentialEquations(MovingCameraScene): CONFIG = { "screen_rect_style": { "stroke_width": 2, "stroke_color": WHITE, "fill_opacity": 1, - "fill_color": DARKER_GREY, - } + "fill_color": BLACK, + }, + "camera_config": {"background_color": DARKER_GREY}, } def construct(self): + self.add_title() self.show_thumbnails() - self.show_words() + # self.show_words() + + def add_title(self): + title = TextMobject( + "A Tourist's Guide \\\\to Differential\\\\Equations" + ) + title.scale(1.5) + title.to_corner(UR) + self.add(title) def show_thumbnails(self): - # TODO, replace each of these with a picture? - thumbnails = self.thumbnails = VGroup( - ScreenRectangle(**self.screen_rect_style), - ScreenRectangle(**self.screen_rect_style), - ScreenRectangle(**self.screen_rect_style), - ScreenRectangle(**self.screen_rect_style), - ScreenRectangle(**self.screen_rect_style), + thumbnails = self.thumbnails = Group( + Group(ScreenRectangle(**self.screen_rect_style)), + Group(ScreenRectangle(**self.screen_rect_style)), + Group(ScreenRectangle(**self.screen_rect_style)), + Group(ScreenRectangle(**self.screen_rect_style)), + Group(ScreenRectangle(**self.screen_rect_style)), ) n = len(thumbnails) thumbnails.set_height(1.5) @@ -71,8 +80,16 @@ class FollowThisThread(Scene): [-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)) + dots = TexMobject("\\dots") + dots.next_to(thumbnails[-1], RIGHT) + + self.add_heat_preview(thumbnails[1]) + self.add_fourier_series(thumbnails[2]) + self.add_matrix_exponent(thumbnails[3]) + self.add_laplace_symbol(thumbnails[4]) self.play( ShowCreation( @@ -91,6 +108,14 @@ class FollowThisThread(Scene): ], lag_ratio=1), run_time=5 ) + self.play(Write(dots)) + self.wait() + self.play( + self.camera_frame.replace, + thumbnails[0], + run_time=3, + ) + self.wait() def show_words(self): words = VGroup( @@ -123,6 +148,71 @@ class FollowThisThread(Scene): ) self.wait() + # + def add_heat_preview(self, thumbnail): + image = ImageMobject("HeatSurfaceExample") + image.replace(thumbnail) + thumbnail.add(image) + + def add_matrix_exponent(self, thumbnail): + matrix = IntegerMatrix( + [[3, 1], [4, 1]], + v_buff=MED_LARGE_BUFF, + h_buff=MED_LARGE_BUFF, + bracket_h_buff=SMALL_BUFF, + bracket_v_buff=SMALL_BUFF, + ) + e = TexMobject("e") + t = TexMobject("t") + t.scale(1.5) + t.next_to(matrix, RIGHT, SMALL_BUFF) + e.scale(2) + e.move_to(matrix.get_corner(DL), UR) + group = VGroup(e, matrix, t) + group.set_height(0.7 * thumbnail.get_height()) + randy = Randolph(mode="confused", height=0.75) + randy.next_to(group, LEFT, aligned_edge=DOWN) + randy.look_at(matrix) + group.add(randy) + group.move_to(thumbnail) + thumbnail.add(group) + + def add_fourier_series(self, thumbnail): + colors = [BLUE, GREEN, YELLOW, RED, RED_E, PINK] + + waves = VGroup(*[ + self.get_square_wave_approx(N, color) + for N, color in enumerate(colors) + ]) + waves.set_stroke(width=1.5) + waves.replace(thumbnail, stretch=True) + waves.scale(0.8) + waves.move_to(thumbnail) + thumbnail.add(waves) + + def get_square_wave_approx(self, N, color): + return FunctionGraph( + lambda x: sum([ + (1 / n) * np.sin(n * PI * x) + for n in range(1, 2 * N + 3, 2) + ]), + x_min=0, + x_max=2, + color=color + ) + + def add_laplace_symbol(self, thumbnail): + mob = TexMobject( + "\\mathcal{L}\\left\\{f(t)\\right\\}" + ) + mob.set_width(0.8 * thumbnail.get_width()) + mob.move_to(thumbnail) + thumbnail.add(mob) + + +class HeatEquationPreview(ExternallyAnimatedScene): + pass + class ShowGravityAcceleration(Scene): def construct(self): @@ -395,15 +485,7 @@ class DefineODE(Scene): de_word = TextMobject("Differential", "Equation") de_word.to_edge(UP) - equation = TexMobject( - "\\ddot \\theta({t})", "=", - "-\\mu \\dot \\theta({t})", - "-{g \\over L} \\sin\\big(\\theta({t})\\big)", - tex_to_color_map={ - "\\theta": BLUE, - "{t}": WHITE - } - ) + equation = get_ode() equation.next_to(de_word, DOWN) thetas = equation.get_parts_by_tex("\\theta") @@ -475,11 +557,18 @@ class DefineODE(Scene): ) tex_config = { - "tex_to_color_map": {"\\theta": BLUE}, + "tex_to_color_map": { + "{\\theta}": BLUE, + "{\\dot\\theta}": YELLOW, + "{\\ddot\\theta}": RED, + }, "height": 0.5, } theta, d_theta, dd_theta = [ - TexMobject(s + "\\theta(t)", **tex_config) + TexMobject( + "{" + s + "\\theta}(t)", + **tex_config + ) for s in ("", "\\dot", "\\ddot") ] @@ -500,7 +589,7 @@ class DefineODE(Scene): return DashedLine( x_point, point, dash_length=0.025, - stroke_color=WHITE, + stroke_color=BLUE, stroke_width=2, ) @@ -545,7 +634,7 @@ class DefineODE(Scene): thetas = VGroup(theta, d_theta, dd_theta) words = VGroup( - TextMobject("= Height"), + TextMobject("= Height").set_color(BLUE), TextMobject("= Slope").set_color(YELLOW), TextMobject("= ``Curvature''").set_color(RED), ) @@ -887,7 +976,8 @@ class LorenzVectorField(ExternallyAnimatedScene): class ThreeBodiesInSpace(SpecialThreeDScene): CONFIG = { - "masses": [1, 2, 3], + "masses": [1, 6, 3], + "colors": [RED_E, GREEN_E, BLUE_E], "G": 0.5, "play_time": 60, } @@ -900,26 +990,48 @@ class ThreeBodiesInSpace(SpecialThreeDScene): def add_axes(self): axes = self.axes = self.get_axes() + axes.set_stroke(width=0.5) self.add(axes) def add_bodies(self): + masses = self.masses + colors = self.colors + bodies = self.bodies = VGroup() velocity_vectors = VGroup() - for mass in self.masses: - body = self.get_sphere( - checkerboard_colors=[DARK_BROWN, DARK_BROWN], - stroke_width=0.1, - ) - body.mass = mass - body.set_width(0.2 * mass) - - point = np.dot( - 2 * (np.random.random(3) - 0.5), + centers = [ + np.dot( + 4 * (np.random.random(3) - 0.5), [RIGHT, UP, OUT] ) - velocity = normalize(np.cross(point, OUT)) - body.move_to(point) + for x in range(len(masses)) + ] + + for mass, color, center in zip(masses, colors, centers): + body = self.get_sphere( + checkerboard_colors=[ + color, color + ], + color=color, + stroke_width=0.1, + ) + body.set_opacity(0.75) + body.mass = mass + body.set_width(0.15 * np.sqrt(mass)) + + body.point = center + body.move_to(center) + + to_others = [ + center - center2 + for center2 in centers + ] + velocity = 0.2 * mass * normalize(np.cross(*filter( + lambda diff: get_norm(diff) > 0, + to_others + ))) + body.velocity = velocity body.add_updater(self.update_body) @@ -946,27 +1058,28 @@ class ThreeBodiesInSpace(SpecialThreeDScene): def add_trajectories(self): def update_trajectory(traj, dt): - new_point = traj.body.get_center() + new_point = traj.body.point if get_norm(new_point - traj.points[-1]) > 0.01: traj.add_smooth_curve_to(new_point) for body in self.bodies: traj = VMobject() traj.body = body - traj.start_new_path(body.get_center()) - traj.set_stroke(WHITE, 1) + traj.start_new_path(body.point) + traj.set_stroke(body.color, 1, opacity=0.75) traj.add_updater(update_trajectory) self.add(traj, body) def let_play(self): - self.move_camera( + self.set_camera_orientation( phi=70 * DEGREES, theta=-110 * DEGREES, - run_time=3, ) self.begin_ambient_camera_rotation() - for x in range(6): - self.wait(self.play_time / 6) + # Break it up to see partial files as + # it's rendered + for x in range(int(self.play_time)): + self.wait() # def get_velocity_vector_mob(self, body): @@ -994,13 +1107,110 @@ class ThreeBodiesInSpace(SpecialThreeDScene): for body2 in self.bodies: if body2 is body: continue - diff = body2.get_center() - body.get_center() + diff = body2.point - body.point m2 = body2.mass R = get_norm(diff) acceleration += G * m2 * diff / (R**3) - body.shift(body.velocity * dt) - body.velocity += acceleration * dt + num_mid_steps = 100 + for x in range(num_mid_steps): + body.point += body.velocity * dt / num_mid_steps + body.velocity += acceleration * dt / num_mid_steps + body.move_to(body.point) + return body + + +class AltThreeBodiesInSpace(ThreeBodiesInSpace): + CONFIG = { + "random_seed": 6, + "masses": [1, 2, 6], + } + + +class DefineODECopy(DefineODE): + pass + + +class WriteODESolvingCode(ExternallyAnimatedScene): + pass + + +class InaccurateComputation(Scene): + def construct(self): + h_line = DashedLine(LEFT_SIDE, RIGHT_SIDE) + h_line.to_edge(UP, buff=1.5) + words = VGroup( + TextMobject("Real number"), + TextMobject("IEEE 754\\\\representation"), + TextMobject("Error"), + ) + for i, word in zip([-1, 0, 1], words): + word.next_to(h_line, UP) + word.shift(i * FRAME_WIDTH * RIGHT / 3) + + lines = VGroup(*[ + DashedLine(TOP, BOTTOM) + for x in range(4) + ]) + lines.arrange(RIGHT) + lines.stretch_to_fit_width(FRAME_WIDTH) + + self.add(h_line, lines[1:-1], words) + + numbers = VGroup( + TexMobject("\\pi").scale(2), + TexMobject("e^{\\sqrt{163}\\pi}").scale(1.5), + ) + numbers.set_color(YELLOW) + numbers.set_stroke(width=0, background=True) + + bit_strings = VGroup( + TexMobject( + "01000000", + "01001001", + "00001111", + "11011011", + ), + TexMobject( + "01011100", + "01101001", + "00101110", + "00011001", + ) + ) + for mob in bit_strings: + mob.arrange(DOWN, buff=SMALL_BUFF) + for word in mob: + for submob, bit in zip(word, word.get_tex_string()): + if bit == "0": + submob.set_color(LIGHT_GREY) + errors = VGroup( + TexMobject( + "\\approx 8.7422 \\times 10^{-8}" + ), + TexMobject( + "\\approx 5{,}289{,}803{,}032.00", + ), + ) + errors.set_color(RED) + + content = VGroup(numbers, bit_strings, errors) + + for group, word in zip(content, words): + group[1].shift(3 * DOWN) + group.move_to(DOWN) + group.match_x(word) + + self.play(*map(Write, numbers)) + self.wait() + self.play( + TransformFromCopy(numbers, bit_strings), + lag_ratio=0.01, + run_time=2, + ) + self.wait() + self.play(FadeInFrom(errors, 3 * LEFT)) + self.wait() class NewSceneName(Scene):