diff --git a/active_projects/eola2/cramer.py b/active_projects/eola2/cramer.py index c3c7b87f..2b83c2a4 100644 --- a/active_projects/eola2/cramer.py +++ b/active_projects/eola2/cramer.py @@ -2268,7 +2268,8 @@ class Thumbnail(TransformingAreasYCoord, MovingCameraScene): ip = self.get_input_parallelogram(vect) self.add_transformable_mobject(ip) self.apply_transposed_matrix([[2, -0.5], [1, 2]]) - # self.square.set_fill(YELLOW, 0.7) + self.square.set_fill(opacity=0.7) + self.square.set_sheen(0.75, UR) self.camera_frame.shift(UP) words = TextMobject("Cramer's", "rule") diff --git a/active_projects/ode/all_part1_scenes.py b/active_projects/ode/all_part1_scenes.py index d4b6ae50..ebcb0d3d 100644 --- a/active_projects/ode/all_part1_scenes.py +++ b/active_projects/ode/all_part1_scenes.py @@ -1,6 +1,22 @@ from active_projects.ode.part1.pendulum import * +from active_projects.ode.part1.staging import * +from active_projects.ode.part1.pi_scenes import * OUTPUT_DIRECTORY = "ode/part1" ALL_SCENE_CLASSES = [ - PendulumTest + IntroducePendulum, + MultiplePendulumsOverlayed, + FormulasAreLies, + MediumAnglePendulum, + MediumHighAnglePendulum, + HighAnglePendulum, + LowAnglePendulum, + SomeOfYouWatching, + SmallAngleApproximationTex, + VeryLowAnglePendulum, + FollowThisThread, + StrogatzQuote, + # Something... + ShowGravityAcceleration, + BuildUpEquation, ] diff --git a/active_projects/ode/part1/pendulum.py b/active_projects/ode/part1/pendulum.py index 5005f73e..9627b3b9 100644 --- a/active_projects/ode/part1/pendulum.py +++ b/active_projects/ode/part1/pendulum.py @@ -1,4 +1,5 @@ from big_ol_pile_of_manim_imports import * +from active_projects.ode.part1.shared_constructs import * class Pendulum(VGroup): @@ -19,9 +20,12 @@ class Pendulum(VGroup): "weight_style": { "stroke_width": 0, "fill_opacity": 1, - "fill_color": DARK_GREY, + "fill_color": GREY_BROWN, "sheen_direction": UL, "sheen_factor": 0.5, + "background_stroke_color": BLACK, + "background_stroke_width": 3, + "background_stroke_opacity": 0.5, }, "dashed_line_config": { "num_dashes": 25, @@ -36,7 +40,8 @@ class Pendulum(VGroup): "velocity_vector_config": { "color": RED, }, - "n_steps_per_frame": 10, + "theta_label_height": 0.25, + "n_steps_per_frame": 100, } def __init__(self, **kwargs): @@ -47,12 +52,15 @@ class Pendulum(VGroup): self.rotating_group = VGroup(self.rod, self.weight) self.create_dashed_line() self.create_angle_arc() + self.add_theta_label() self.set_theta(self.initial_theta) self.update() def create_fixed_point(self): self.fixed_point_tracker = VectorizedPoint(self.top_point) + self.add(self.fixed_point_tracker) + return self def create_rod(self): rod = self.rod = Line(UP, DOWN) @@ -87,11 +95,33 @@ class Pendulum(VGroup): def add_velocity_vector(self): def make_vector(): - vector = Vector(0.5 * self.get_omega() * RIGHT) + omega = self.get_omega() + theta = self.get_theta() + vector = Vector( + 0.5 * omega * RIGHT, + **self.velocity_vector_config, + ) + vector.rotate(theta, about_point=ORIGIN) + vector.shift(self.rod.get_end()) + return vector + self.velocity_vector = always_redraw(make_vector) self.add(self.velocity_vector) return self + def add_theta_label(self): + label = self.theta_label = TexMobject("\\theta") + label.set_height(self.theta_label_height) + + 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_theta(self): theta = self.rod.get_angle() - self.dashed_line.get_angle() @@ -141,11 +171,816 @@ class Pendulum(VGroup): return self -class PendulumTest(Scene): - def construct(self): - pendulum = Pendulum( - initial_theta=150 * DEGREES, +class GravityVector(Vector): + CONFIG = { + "color": YELLOW, + "length_multiple": 1 / 9.8, + # TODO, continually update the length based + # on the pendulum's gravity? + } + + def __init__(self, pendulum, **kwargs): + super().__init__(DOWN, **kwargs) + self.pendulum = pendulum + self.scale(self.length_multiple * pendulum.gravity) + self.attach_to_pendulum(pendulum) + + def attach_to_pendulum(self, pendulum): + self.add_updater(lambda m: m.shift( + pendulum.weight.get_center() - self.get_start(), + )) + + def add_component_lines(self): + self.component_lines = always_redraw(self.create_component_lines) + self.add(self.component_lines) + + def create_component_lines(self): + theta = self.pendulum.get_theta() + x_new = rotate(RIGHT, theta) + base = self.get_start() + tip = self.get_end() + vect = tip - base + corner = base + x_new * np.dot(vect, x_new) + kw = {"dash_length": 0.025} + return VGroup( + DashedLine(base, corner, **kw), + DashedLine(corner, tip, **kw), ) - self.add(pendulum) + + +class ThetaValueDisplay(VGroup): + CONFIG = { + + } + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + +class ThetaVsTAxes(Axes): + CONFIG = { + "x_min": 0, + "x_max": 8, + "y_min": -PI / 2, + "y_max": PI / 2, + "y_axis_config": { + "tick_frequency": PI / 8, + "unit_size": 1.5, + }, + "number_line_config": { + "color": "#EEEEEE", + "stroke_width": 2, + "include_tip": False, + }, + "graph_style": { + "stroke_color": GREEN, + "stroke_width": 3, + "fill_opacity": 0, + }, + } + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.add_labels() + + def add_axes(self): + self.axes = Axes(**self.axes_config) + self.add(self.axes) + + def add_labels(self): + x_axis = self.get_x_axis() + y_axis = self.get_y_axis() + + t_label = self.t_label = TexMobject("t") + t_label.next_to(x_axis.get_right(), UP, MED_SMALL_BUFF) + x_axis.label = t_label + x_axis.add(t_label) + theta_label = self.theta_label = TexMobject("\\theta(t)") + theta_label.next_to(y_axis.get_top(), UP, SMALL_BUFF) + y_axis.label = theta_label + y_axis.add(theta_label) + + x_axis.add_numbers() + y_axis.add(self.get_y_axis_coordinates(y_axis)) + + def get_y_axis_coordinates(self, y_axis): + texs = [ + # "\\pi \\over 4", + # "\\pi \\over 2", + # "3 \\pi \\over 4", + # "\\pi", + "\\pi / 4", + "\\pi / 2", + "3 \\pi / 4", + "\\pi", + ] + values = np.arange(1, 5) * PI / 4 + labels = VGroup() + for pos_tex, pos_value in zip(texs, values): + neg_tex = "-" + pos_tex + neg_value = -1 * pos_value + for tex, value in (pos_tex, pos_value), (neg_tex, neg_value): + if value > self.y_max or value < self.y_min: + continue + symbol = TexMobject(tex) + symbol.scale(0.5) + point = y_axis.number_to_point(value) + symbol.next_to(point, LEFT, MED_SMALL_BUFF) + labels.add(symbol) + return labels + + def get_live_drawn_graph(self, pendulum, + t_max=None, + t_step=1.0 / 60, + **style): + style = merge_dicts_recursively(self.graph_style, style) + if t_max is None: + t_max = self.x_max + + graph = VMobject() + graph.set_style(**style) + + graph.all_coords = [(0, pendulum.get_theta())] + graph.time = 0 + graph.time_of_last_addition = 0 + + def update_graph(graph, dt): + graph.time += dt + if graph.time > t_max: + graph.remove_updater(update_graph) + return + new_coords = (graph.time, pendulum.get_theta()) + if graph.time - graph.time_of_last_addition >= t_step: + graph.all_coords.append(new_coords) + graph.time_of_last_addition = graph.time + points = [ + self.coords_to_point(*coords) + for coords in [*graph.all_coords, new_coords] + ] + graph.set_points_smoothly(points) + + graph.add_updater(update_graph) + return graph + + +# Scenes +class IntroducePendulum(PiCreatureScene, MovingCameraScene): + CONFIG = { + "pendulum_config": { + "length": 3, + "top_point": 4 * RIGHT, + "weight_diameter": 0.35, + }, + "theta_vs_t_axes_config": { + "y_max": PI / 4, + "y_min": -PI / 4, + "y_axis_config": { + "tick_frequency": PI / 16, + "unit_size": 2, + "tip_length": 0.3, + }, + "number_line_config": { + "stroke_width": 2, + } + }, + } + + def setup(self): + MovingCameraScene.setup(self) + PiCreatureScene.setup(self) + + def construct(self): + self.add_pendulum() + self.label_pi_creatures() + self.label_pendulum() + self.add_graph() + self.show_graph_period() + self.show_length_and_gravity() + self.tweak_length_and_gravity() + + def create_pi_creatures(self): + randy = Randolph(color=BLUE_C) + morty = Mortimer(color=MAROON_E) + creatures = VGroup(randy, morty) + creatures.scale(0.5) + creatures.arrange(RIGHT, buff=2.5) + creatures.to_corner(DR) + return creatures + + def add_pendulum(self): + pendulum = self.pendulum = Pendulum(**self.pendulum_config) pendulum.start_swinging() + frame = self.camera_frame + frame.save_state() + frame.scale(0.5) + frame.move_to(pendulum.dashed_line) + + self.add(pendulum, frame) + + def label_pi_creatures(self): + randy, morty = self.pi_creatures + randy_label = TextMobject("Physics\\\\", "student") + morty_label = TextMobject("Physics\\\\", "teacher") + labels = VGroup(randy_label, morty_label) + labels.scale(0.5) + randy_label.next_to(randy, UP, LARGE_BUFF) + morty_label.next_to(morty, UP, LARGE_BUFF) + + for label, pi in zip(labels, self.pi_creatures): + label.arrow = Arrow( + label.get_bottom(), pi.eyes.get_top() + ) + label.arrow.set_color(WHITE) + label.arrow.set_stroke(width=5) + + morty.labels = VGroup( + morty_label, + morty_label.arrow, + ) + + self.play( + FadeInFromDown(randy_label), + GrowArrow(randy_label.arrow), + randy.change, "hooray", + ) + self.play( + Animation(self.pendulum.fixed_point_tracker), + TransformFromCopy(randy_label[0], morty_label[0]), + FadeIn(morty_label[1]), + GrowArrow(morty_label.arrow), + morty.change, "raise_right_hand", + ) + self.wait() + + def label_pendulum(self): + pendulum = self.pendulum + randy, morty = self.pi_creatures + label = pendulum.theta_label + rect = SurroundingRectangle(label, buff=0.5 * SMALL_BUFF) + rect.add_updater(lambda r: r.move_to(label)) + + self.add(rect) + self.play( + ShowCreationThenFadeOut(rect), + ShowCreationThenDestruction( + label.copy().set_style( + fill_opacity=0, + stroke_color=PINK, + stroke_width=2, + ) + ), + randy.change, "pondering", + morty.change, "pondering", + ) + self.wait() + + def add_graph(self): + axes = self.axes = ThetaVsTAxes(**self.theta_vs_t_axes_config) + axes.y_axis.label.next_to(axes.y_axis, UP, buff=0) + axes.to_corner(UL) + + self.play( + Restore(self.camera_frame), + DrawBorderThenFill( + axes, + rate_func=squish_rate_func(smooth, 0.5, 1), + lag_ratio=0.9, + ), + Transform( + self.pendulum.theta_label.copy().clear_updaters(), + axes.y_axis.label.copy(), + remover=True, + rate_func=squish_rate_func(smooth, 0, 0.8), + ), + run_time=3, + ) + self.wait(2) + self.graph = axes.get_live_drawn_graph(self.pendulum) + + self.add(self.graph) + self.wait(4) + + def show_graph_period(self): + pendulum = self.pendulum + axes = self.axes + + period = self.period = TAU * np.sqrt( + pendulum.length / pendulum.gravity + ) + amplitude = pendulum.initial_theta + + line = Line( + axes.coords_to_point(0, amplitude), + axes.coords_to_point(period, amplitude), + ) + line.shift(SMALL_BUFF * RIGHT) + brace = Brace(line, UP, buff=SMALL_BUFF) + brace.add_to_back(brace.copy().set_style(BLACK, 10)) + formula = TexMobject( + "\\sqrt{\\,", "2\\pi", "L", "/", "g", "}", + tex_to_color_map={ + "L": BLUE, + "g": YELLOW, + } + ) + formula.next_to(brace, UP, SMALL_BUFF) + + self.period_formula = formula + self.period_brace = brace + + self.play( + GrowFromCenter(brace), + FadeInFromDown(formula), + ) + self.wait(2) + + def show_length_and_gravity(self): + formula = self.period_formula + L = formula.get_part_by_tex("L") + g = formula.get_part_by_tex("g") + + rod = self.pendulum.rod + new_rod = rod.copy() + new_rod.set_stroke(BLUE, 7) + new_rod.add_updater(lambda r: r.put_start_and_end_on( + *rod.get_start_and_end() + )) + + g_vect = GravityVector( + self.pendulum, + length_multiple=0.5 / 9.8, + ) + + self.play(ShowCreationThenDestructionAround(L)) + dot = Dot(fill_opacity=0.25) + dot.move_to(L) + self.play( + ShowCreation(new_rod), + dot.move_to, new_rod, + dot.fade, 1, + ) + self.remove(dot) + self.play(FadeOut(new_rod)) + self.wait() + + self.play(ShowCreationThenDestructionAround(g)) + dot.move_to(g) + dot.set_fill(opacity=0.5) + self.play( + GrowArrow(g_vect), + dot.move_to, g_vect, + dot.fade, 1, + ) + self.remove(dot) + self.wait(2) + + self.gravity_vector = g_vect + + def tweak_length_and_gravity(self): + pendulum = self.pendulum + axes = self.axes + graph = self.graph + brace = self.period_brace + formula = self.period_formula + g_vect = self.gravity_vector + randy, morty = self.pi_creatures + + graph.clear_updaters() + period2 = self.period * np.sqrt(2) + period3 = self.period / np.sqrt(2) + amplitude = pendulum.initial_theta + graph2, graph3 = [ + axes.get_graph( + lambda t: amplitude * np.cos(TAU * t / p), + color=RED, + ) + for p in (period2, period3) + ] + formula.add_updater(lambda m: m.next_to( + brace, UP, SMALL_BUFF + )) + + new_pendulum_config = dict(self.pendulum_config) + new_pendulum_config["length"] *= 2 + new_pendulum_config["top_point"] += 3.5 * UP + # new_pendulum_config["initial_theta"] = pendulum.get_theta() + new_pendulum = Pendulum(**new_pendulum_config) + + down_vectors = VGroup(*[ + Vector(0.5 * DOWN) + for x in range(10 * 150) + ]) + down_vectors.arrange_in_grid(10, 150, buff=MED_SMALL_BUFF) + down_vectors.set_color_by_gradient(BLUE, RED) + # for vect in down_vectors: + # vect.shift(0.1 * np.random.random(3)) + down_vectors.to_edge(RIGHT) + + self.play(randy.change, "happy") + self.play( + ReplacementTransform(pendulum, new_pendulum), + morty.change, "horrified", + morty.shift, 3 * RIGHT, + morty.labels.shift, 3 * RIGHT, + ) + self.remove(morty, morty.labels) + g_vect.attach_to_pendulum(new_pendulum) + new_pendulum.start_swinging() + self.play( + ReplacementTransform(graph, graph2), + brace.stretch, np.sqrt(2), 0, {"about_edge": LEFT}, + ) + self.add(g_vect) + self.wait(3) + + new_pendulum.gravity *= 4 + g_vect.scale(2) + self.play( + FadeOut(graph2), + LaggedStart(*[ + GrowArrow(v, rate_func=there_and_back) + for v in down_vectors + ], lag_ratio=0.0005, run_time=2, remover=True) + ) + self.play( + FadeIn(graph3), + brace.stretch, 0.5, 0, {"about_edge": LEFT}, + ) + self.wait(6) + + +class MultiplePendulumsOverlayed(Scene): + CONFIG = { + "initial_thetas": [ + 150 * DEGREES, + 90 * DEGREES, + 60 * DEGREES, + 30 * DEGREES, + 10 * DEGREES, + ], + "weight_colors": [ + PINK, RED, GREEN, BLUE, GREY, + ], + "pendulum_config": { + "top_point": ORIGIN, + "length": 3, + }, + } + + def construct(self): + pendulums = VGroup(*[ + Pendulum( + initial_theta=theta, + weight_style={ + "fill_color": wc, + "fill_opacity": 0.5, + }, + **self.pendulum_config, + ) + for theta, wc in zip( + self.initial_thetas, + self.weight_colors, + ) + ]) + for pendulum in pendulums: + pendulum.start_swinging() + pendulum.remove(pendulum.theta_label) + + randy = Randolph(color=BLUE_C) + randy.to_corner(DL) + randy.add_updater(lambda r: r.look_at(pendulums[0].weight)) + + axes = ThetaVsTAxes( + x_max=20, + y_axis_config={ + "unit_size": 0.5, + "tip_length": 0.3, + }, + ) + axes.to_corner(UL) + graphs = VGroup(*[ + axes.get_live_drawn_graph( + pendulum, + stroke_color=pendulum.weight.get_color(), + stroke_width=1, + ) + for pendulum in pendulums + ]) + + self.add(pendulums) + self.add(axes, *graphs) + self.play(randy.change, "sassy") + self.wait(2) + self.play(Blink(randy)) + self.wait(5) + self.play(randy.change, "angry") + self.play(Blink(randy)) self.wait(10) + + +class LowAnglePendulum(Scene): + CONFIG = { + "pendulum_config": { + "initial_theta": 20 * DEGREES, + "length": 2.0, + "damping": 0, + "top_point": ORIGIN, + }, + "axes_config": { + "y_axis_config": {"unit_size": 0.75}, + "x_axis_config": { + "unit_size": 0.5, + "numbers_to_show": range(2, 25, 2), + "number_scale_val": 0.5, + }, + "x_max": 25, + "number_line_config": { + "tip_length": 0.3, + "stroke_width": 2, + } + }, + "axes_corner": UL, + } + + def construct(self): + pendulum = Pendulum(**self.pendulum_config) + axes = ThetaVsTAxes(**self.axes_config) + axes.center() + axes.to_corner(self.axes_corner, buff=LARGE_BUFF) + graph = axes.get_live_drawn_graph(pendulum) + + L = pendulum.length + g = pendulum.gravity + theta0 = pendulum.initial_theta + prediction = axes.get_graph( + lambda t: theta0 * np.cos(t * np.sqrt(g / L)) + ) + dashed_prediction = DashedVMobject(prediction, num_dashes=300) + dashed_prediction.set_stroke(WHITE, 1) + prediction_formula = TexMobject( + "\\theta_0", "\\cos(\\sqrt{g / L} \\cdot t)" + ) + prediction_formula.scale(0.75) + prediction_formula.next_to( + dashed_prediction, UP, SMALL_BUFF, + ) + + theta0 = prediction_formula.get_part_by_tex("\\theta_0") + theta0_brace = Brace(theta0, UP, buff=SMALL_BUFF) + theta0_brace.stretch(0.5, 1, about_edge=DOWN) + theta0_label = Integer( + pendulum.initial_theta * 180 / PI, + unit="^\\circ" + ) + theta0_label.scale(0.75) + theta0_label.next_to(theta0_brace, UP, SMALL_BUFF) + + group = VGroup(theta0_brace, theta0_label, prediction_formula) + group.shift_onto_screen(buff=MED_SMALL_BUFF) + + self.add(axes, dashed_prediction, pendulum) + self.play( + ShowCreation(dashed_prediction, run_time=2), + FadeInFromDown(prediction_formula), + FadeInFromDown(theta0_brace), + FadeInFromDown(theta0_label), + ) + self.play( + ShowCreationThenFadeAround(theta0_label), + ShowCreationThenFadeAround(pendulum.theta_label), + ) + self.wait() + + pendulum.start_swinging() + self.add(graph) + self.wait(30) + + +class ApproxWordsLowAnglePendulum(Scene): + def construct(self): + period = TexMobject( + "\\text{Period}", "\\approx", + "2\\pi \\sqrt{\\,{L} / {g}}", + **Lg_formula_config + ) + checkmark = TexMobject("\\checkmark") + checkmark.set_color(GREEN) + checkmark.scale(2) + checkmark.next_to(period, RIGHT, MED_LARGE_BUFF) + + self.add(period, checkmark) + + +class MediumAnglePendulum(LowAnglePendulum): + CONFIG = { + "pendulum_config": { + "initial_theta": 50 * DEGREES, + "n_steps_per_frame": 1000, + }, + "axes_config": { + "y_axis_config": {"unit_size": 0.75}, + "y_max": PI / 2, + "y_min": -PI / 2, + "number_line_config": { + "tip_length": 0.3, + "stroke_width": 2, + } + }, + "pendulum_shift_vect": 1 * RIGHT, + } + + +class MediumHighAnglePendulum(MediumAnglePendulum): + CONFIG = { + "pendulum_config": { + "initial_theta": 90 * DEGREES, + "n_steps_per_frame": 1000, + }, + } + + +class HighAnglePendulum(LowAnglePendulum): + CONFIG = { + "pendulum_config": { + "initial_theta": 175 * DEGREES, + "n_steps_per_frame": 1000, + "top_point": 1.5 * DOWN, + "length": 2, + }, + "axes_config": { + "y_axis_config": {"unit_size": 0.5}, + "y_max": PI, + "y_min": -PI, + "number_line_config": { + "tip_length": 0.3, + "stroke_width": 2, + } + }, + "pendulum_shift_vect": 1 * RIGHT, + } + + +class VeryLowAnglePendulum(LowAnglePendulum): + CONFIG = { + "pendulum_config": { + "initial_theta": 10 * DEGREES, + "n_steps_per_frame": 1000, + "top_point": ORIGIN, + "length": 3, + }, + "axes_config": { + "y_axis_config": {"unit_size": 2}, + "y_max": PI / 4, + "y_min": -PI / 4, + "number_line_config": { + "tip_length": 0.3, + "stroke_width": 2, + } + }, + "pendulum_shift_vect": 1 * RIGHT, + } + + +class BuildUpEquation(MovingCameraScene): + CONFIG = { + "pendulum_config": { + "length": 5, + "top_point": 3 * UP, + "initial_theta": 45 * DEGREES, + }, + "g_vect_config": { + "length_multiple": 0.25, + }, + "tan_line_color": BLUE, + "perp_line_color": PINK, + } + + def construct(self): + 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.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() + + def add_pendulum(self): + self.pendulum = Pendulum(**self.pendulum_config) + self.add(self.pendulum) + + def show_constraint(self): + pendulum = self.pendulum + weight = pendulum.weight + + g_vect = self.g_vect = GravityVector( + pendulum, **self.g_vect_config, + ) + g_word = self.g_word = TextMobject("Gravity") + g_word.rotate(-90 * DEGREES) + g_word.scale(0.75) + g_word.add_updater(lambda m: m.next_to( + 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() + + 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 + arc = Arc( + start_angle=pendulum.get_theta() - 90 * DEGREES, + angle=d_theta, + radius=pendulum.length, + arc_center=pendulum.get_fixed_point(), + stroke_width=2, + stroke_color=RED, + stroke_opacity=0.5, + ) + self.play( + theta_tracker.increment_value, d_theta, + ShowCreation(arc) + ) + arcs.add(arc) + pendulum.clear_updaters() + self.wait() + self.play(FadeOut(arc)) + + def break_g_vect_into_components(self): + g_vect = self.g_vect + g_vect.component_lines = always_redraw( + g_vect.create_component_lines + ) + tan_line, perp_line = g_vect.component_lines + g_vect.tangent = always_redraw(lambda: Arrow( + tan_line.get_start(), + tan_line.get_end(), + buff=0, + color=self.tan_line_color, + )) + g_vect.perp = always_redraw(lambda: Arrow( + perp_line.get_start(), + perp_line.get_end(), + buff=0, + color=self.perp_line_color, + )) + + 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 + + def show_acceleration_at_different_angles(self): + pass + + def ask_about_what_to_do(self): + pass + + def show_velocity_and_position(self): + pass + + def show_derivatives(self): + pass + + def show_equation(self): + pass + + def talk_about_sine_component(self): + pass + + def add_air_resistance(self): + pass + + +class NewSceneName(Scene): + def construct(self): + pass diff --git a/active_projects/ode/part1/pi_scenes.py b/active_projects/ode/part1/pi_scenes.py new file mode 100644 index 00000000..4c53a9d6 --- /dev/null +++ b/active_projects/ode/part1/pi_scenes.py @@ -0,0 +1,108 @@ +from big_ol_pile_of_manim_imports import * +from active_projects.ode.part1.shared_constructs import * + + +class SomeOfYouWatching(TeacherStudentsScene): + CONFIG = { + "camera_config": { + "background_color": DARKER_GREY, + } + } + + def construct(self): + screen = self.screen + screen.scale(1.25, about_edge=UL) + screen.set_fill(BLACK, 1) + self.add(screen) + + self.teacher.change("raise_right_hand") + for student in self.students: + student.change("pondering", screen) + + self.student_says( + "Well...yeah", + target_mode="tease" + ) + self.wait(3) + + +class FormulasAreLies(PiCreatureScene): + def construct(self): + you = self.pi_creature + t2c = { + "{L}": BLUE, + "{g}": YELLOW, + "\\theta_0": WHITE, + "\\sqrt{\\,": WHITE, + } + kwargs = {"tex_to_color_map": t2c} + period_eq = TexMobject( + "\\text{Period} = 2\\pi \\sqrt{\\,{L} / {g}}", + **kwargs + ) + theta_eq = TexMobject( + "\\theta(t) = \\theta_0 \\cos\\left(" + "\\sqrt{\\,{L} / {g}} \\cdot t" + "\\right)", + **kwargs + ) + equations = VGroup(theta_eq, period_eq) + equations.arrange(DOWN, buff=LARGE_BUFF) + + for eq in period_eq, theta_eq: + i = eq.index_of_part_by_tex("\\sqrt") + eq.sqrt_part = eq[i:i + 4] + + theta0 = theta_eq.get_part_by_tex("\\theta_0") + theta0_words = TextMobject("Starting angle") + theta0_words.next_to(theta0, UL) + theta0_words.shift(UP + 0.5 * RIGHT) + arrow = Arrow( + theta0_words.get_bottom(), + theta0, + color=WHITE, + tip_length=0.25, + ) + + bubble = SpeechBubble() + bubble.pin_to(you) + bubble.write("Lies!") + bubble.content.scale(2) + bubble.resize_to_content() + + self.add(period_eq) + you.change("pondering", period_eq) + self.wait() + theta_eq.remove(*theta_eq.sqrt_part) + self.play( + TransformFromCopy( + period_eq.sqrt_part, + theta_eq.sqrt_part, + ), + FadeIn(theta_eq) + ) + theta_eq.add(*theta_eq.sqrt_part) + self.play( + FadeInFrom(theta0_words, LEFT), + GrowArrow(arrow), + ) + self.wait() + self.play(you.change, "confused") + self.wait(0) + self.play( + you.change, "angry", + ShowCreation(bubble), + FadeInFromPoint(bubble.content, you.mouth), + equations.to_edge, LEFT, + FadeOut(arrow), + FadeOut(theta0_words), + ) + self.wait() + + def create_pi_creature(self): + return You().flip().to_corner(DR) + + +class NewSceneName(Scene): + def construct(self): + pass diff --git a/active_projects/ode/part1/shared_constructs.py b/active_projects/ode/part1/shared_constructs.py new file mode 100644 index 00000000..606c00b6 --- /dev/null +++ b/active_projects/ode/part1/shared_constructs.py @@ -0,0 +1,16 @@ +from big_ol_pile_of_manim_imports import * + + +Lg_formula_config = { + "tex_to_color_map": { + "\\theta_0": WHITE, + "{L}": BLUE, + "{g}": YELLOW, + }, +} + + +class You(PiCreature): + CONFIG = { + "color": BLUE_C, + } diff --git a/active_projects/ode/part1/staging.py b/active_projects/ode/part1/staging.py new file mode 100644 index 00000000..0ca51c39 --- /dev/null +++ b/active_projects/ode/part1/staging.py @@ -0,0 +1,414 @@ +from big_ol_pile_of_manim_imports import * +from active_projects.ode.part1.shared_constructs import * + + +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 + + +class VectorFieldTest(Scene): + def construct(self): + plane = NumberPlane( + # axis_config={"unit_size": 2} + ) + mu_tracker = ValueTracker(1) + field = VectorField( + lambda p: pendulum_vector_field( + plane.point_to_coords(p), + mu=mu_tracker.get_value() + ), + delta_x=0.5, + delta_y=0.5, + max_magnitude=4, + opacity=0.5, + # length_func=lambda norm: norm, + ) + stream_lines = StreamLines( + field.func, + delta_x=0.5, + delta_y=0.5, + ) + animated_stream_lines = AnimatedStreamLines( + stream_lines, + line_anim_class=ShowPassingFlashWithThinningStrokeWidth, + ) + + self.add(plane, field, animated_stream_lines) + 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": { + "stroke_width": 2, + "stroke_color": WHITE, + "fill_opacity": 1, + "fill_color": DARKER_GREY, + } + } + + def construct(self): + self.show_thumbnails() + self.show_words() + + 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), + ) + n = len(thumbnails) + thumbnails.set_height(1.5) + + line = self.line = CubicBezier([ + [-5, 3, 0], + [3, 3, 0], + [-3, -3, 0], + [5, -3, 0], + ]) + for thumbnail, a in zip(thumbnails, np.linspace(0, 1, n)): + thumbnail.move_to(line.point_from_proportion(a)) + + self.play( + ShowCreation( + line, + rate_func=lambda t: np.clip(t * (n + 1) / n, 0, 1) + ), + LaggedStart(*[ + GrowFromCenter( + thumbnail, + rate_func=squish_rate_func( + smooth, + 0, 0.7, + ) + ) + for thumbnail in thumbnails + ], lag_ratio=1), + run_time=5 + ) + + def show_words(self): + words = VGroup( + TextMobject("Generalize"), + TextMobject("Put in context"), + TextMobject("Modify"), + ) + # words.arrange(DOWN, aligned_edge=LEFT, buff=LARGE_BUFF) + words.scale(1.5) + words.to_corner(UR) + words.add_to_back(VectorizedPoint(words.get_center())) + words.add(VectorizedPoint(words.get_center())) + + diffEq = TextMobject("Differential\\\\equations") + diffEq.scale(1.5) + diffEq.to_corner(DL, buff=LARGE_BUFF) + + for word1, word2 in zip(words, words[1:]): + self.play( + FadeInFromDown(word2), + FadeOutAndShift(word1, UP), + ) + self.wait() + self.play( + ReplacementTransform( + VGroup(self.thumbnails).copy().fade(1), + diffEq, + lag_ratio=0.01, + ) + ) + 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() + self.add_title() + self.pulse_gravity_down() + self.show_trajectory() + self.combine_v_vects() + + def add_gravity_field(self): + gravity_field = self.gravity_field = VectorField( + lambda p: DOWN, + # delta_x=2, + # delta_y=2, + ) + gravity_field.set_opacity(0.5) + gravity_field.sort_submobjects( + lambda p: -p[1], + ) + self.add(gravity_field) + + def add_title(self): + title = self.title = TextMobject("Gravitational acceleration") + title.scale(1.5) + title.to_edge(UP) + g_eq = self.g_eq = TexMobject( + "{g}", "=", "-9.8", "\\frac{\\text{m/s}}{\\text{s}}", + **Lg_formula_config + ) + g_eq.next_to(title, DOWN) + for mob in title, g_eq: + mob.add_background_rectangle_to_submobjects( + buff=0.05, + opacity=1, + ) + self.add(title, g_eq) + + def pulse_gravity_down(self): + field = self.gravity_field + self.play(LaggedStart(*[ + ApplyFunction( + lambda v: v.set_opacity(1).scale(1.2), + vector, + rate_func=there_and_back, + ) + for vector in field + ]), run_time=2, lag_ratio=0.001) + self.add(self.title, self.g_eq) + + def show_trajectory(self): + ball = Circle( + stroke_width=1, + stroke_color=WHITE, + fill_color=GREY, + fill_opacity=1, + sheen_factor=1, + sheen_direction=UL, + radius=0.25, + ) + randy = Randolph(mode="pondering") + randy.eyes.set_stroke(BLACK, 0.5) + randy.match_height(ball) + randy.scale(0.75) + randy.move_to(ball) + ball.add(randy) + + total_time = 6 + + p0 = 3 * DOWN + 5 * LEFT + v0 = 2.8 * UP + 1.5 * RIGHT + g = 0.9 * DOWN + graph = ParametricFunction( + lambda t: p0 + v0 * t + 0.5 * g * t**2, + t_min=0, + t_max=total_time, + ) + # graph.center().to_edge(DOWN) + dashed_graph = DashedVMobject(graph, num_dashes=60) + dashed_graph.set_stroke(WHITE, 1) + + ball.move_to(graph.get_start()) + randy.add_updater( + lambda m, dt: m.rotate(dt).move_to(ball) + ) + times = np.arange(0, total_time + 1) + + velocity_graph = ParametricFunction( + lambda t: v0 + g * t, + t_min=0, t_max=total_time, + ) + v_point = VectorizedPoint() + v_point.move_to(velocity_graph.get_start()) + + def get_v_vect(): + result = Vector( + v_point.get_location(), + color=RED, + tip_length=0.2, + ) + result.scale(0.5, about_point=result.get_start()) + result.shift(ball.get_center()) + result.set_stroke(width=2, family=False) + return result + v_vect = always_redraw(get_v_vect) + self.add(v_vect) + + flash_rect = FullScreenRectangle( + stroke_width=0, + fill_color=WHITE, + fill_opacity=0.2, + ) + flash = FadeOut( + flash_rect, + rate_func=squish_rate_func(smooth, 0, 0.1) + ) + + ball_copies = VGroup() + v_vect_copies = VGroup() + self.add(dashed_graph, ball) + for t1, t2 in zip(times, times[1:]): + v_vect_copy = v_vect.copy() + v_vect_copies.add(v_vect_copy) + self.add(v_vect_copy) + ball_copy = ball.copy() + ball_copy.clear_updaters() + ball_copies.add(ball_copy) + self.add(ball_copy) + + dashed_graph.save_state() + kw = { + "rate_func": lambda alpha: interpolate( + t1 / total_time, + t2 / total_time, + alpha + ) + } + self.play( + ShowCreation(dashed_graph, **kw), + MoveAlongPath(ball, graph, **kw), + MoveAlongPath(v_point, velocity_graph, **kw), + flash, + run_time=1, + ) + dashed_graph.restore() + randy.clear_updaters() + self.wait() + + self.v_vects = v_vect_copies + + def combine_v_vects(self): + v_vects = self.v_vects.copy() + v_vects.generate_target() + new_center = 2 * DOWN + 2 * LEFT + for vect in v_vects.target: + vect.scale(1.5) + vect.set_stroke(width=2) + vect.shift(new_center - vect.get_start()) + + self.play(MoveToTarget(v_vects)) + + delta_vects = VGroup(*[ + Arrow( + v1.get_end(), + v2.get_end(), + buff=0.01, + color=YELLOW, + ).set_opacity(0.5) + for v1, v2 in zip(v_vects, v_vects[1:]) + ]) + brace = Brace(Line(ORIGIN, UP), RIGHT) + braces = VGroup(*[ + brace.copy().match_height(arrow).next_to( + arrow, RIGHT, buff=0.2 * SMALL_BUFF + ) + for arrow in delta_vects + ]) + amounts = VGroup(*[ + TextMobject("9.8 m/s").scale(0.5).next_to( + brace, RIGHT, SMALL_BUFF + ) + for brace in braces + ]) + + self.play( + FadeOut(self.gravity_field), + FadeIn(delta_vects, lag_ratio=0.1), + ) + self.play( + LaggedStartMap(GrowFromCenter, braces), + LaggedStartMap(FadeInFrom, amounts, lambda m: (m, LEFT)), + ) + self.wait() diff --git a/big_ol_pile_of_manim_imports.py b/big_ol_pile_of_manim_imports.py index bc2ad705..746936c2 100644 --- a/big_ol_pile_of_manim_imports.py +++ b/big_ol_pile_of_manim_imports.py @@ -55,6 +55,7 @@ from manimlib.mobject.types.point_cloud_mobject import * from manimlib.mobject.types.vectorized_mobject import * from manimlib.mobject.mobject_update_utils import * from manimlib.mobject.value_tracker import * +from manimlib.mobject.vector_field import * from manimlib.for_3b1b_videos.common_scenes import * from manimlib.for_3b1b_videos.pi_creature import * diff --git a/manimlib/animation/fading.py b/manimlib/animation/fading.py index 5493af0c..8f900c97 100644 --- a/manimlib/animation/fading.py +++ b/manimlib/animation/fading.py @@ -97,6 +97,18 @@ class FadeOutAndShiftDown(FadeOutAndShift): } +class FadeInFromPoint(FadeIn): + def __init__(self, mobject, point, **kwargs): + self.point = point + super().__init__(mobject, **kwargs) + + def create_starting_mobject(self): + start = super().create_starting_mobject() + start.scale(0) + start.move_to(self.point) + return start + + class FadeInFromLarge(FadeIn): CONFIG = { "scale_factor": 2, diff --git a/manimlib/animation/growing.py b/manimlib/animation/growing.py index af123ab4..e2a53cb7 100644 --- a/manimlib/animation/growing.py +++ b/manimlib/animation/growing.py @@ -1,4 +1,3 @@ -from manimlib.mobject.types.vectorized_mobject import VectorizedPoint from manimlib.animation.transform import Transform # from manimlib.utils.paths import counterclockwise_path from manimlib.constants import PI diff --git a/manimlib/animation/indication.py b/manimlib/animation/indication.py index 9fb1f247..268ad5f4 100644 --- a/manimlib/animation/indication.py +++ b/manimlib/animation/indication.py @@ -1,5 +1,3 @@ -from functools import reduce - import numpy as np from manimlib.constants import * @@ -9,12 +7,8 @@ from manimlib.animation.composition import AnimationGroup from manimlib.animation.composition import Succession from manimlib.animation.creation import ShowCreation from manimlib.animation.creation import ShowPartial -from manimlib.animation.fading import FadeIn from manimlib.animation.fading import FadeOut from manimlib.animation.transform import Transform -from manimlib.animation.update import UpdateFromAlphaFunc -from manimlib.mobject.mobject_update_utils import always_redraw -from manimlib.mobject.mobject import Mobject from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.mobject.geometry import Circle from manimlib.mobject.geometry import Dot @@ -23,9 +17,6 @@ from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.geometry import Line from manimlib.utils.bezier import interpolate from manimlib.utils.config_ops import digest_config -from manimlib.utils.rate_functions import linear -from manimlib.utils.rate_functions import smooth -from manimlib.utils.rate_functions import squish_rate_func from manimlib.utils.rate_functions import there_and_back from manimlib.utils.rate_functions import wiggle diff --git a/manimlib/animation/movement.py b/manimlib/animation/movement.py index 5a2bac3b..f6a3ec05 100644 --- a/manimlib/animation/movement.py +++ b/manimlib/animation/movement.py @@ -1,5 +1,4 @@ from manimlib.animation.animation import Animation -from manimlib.utils.config_ops import digest_config from manimlib.utils.rate_functions import linear diff --git a/manimlib/constants.py b/manimlib/constants.py index 366b37af..375e6e76 100644 --- a/manimlib/constants.py +++ b/manimlib/constants.py @@ -214,6 +214,8 @@ COLOR_MAP = { "GREY": "#888888", "DARK_GREY": "#444444", "DARK_GRAY": "#444444", + "DARKER_GREY": "#222222", + "DARKER_GRAY": "#222222", "GREY_BROWN": "#736357", "PINK": "#D147BD", "GREEN_SCREEN": "#00FF00", diff --git a/manimlib/for_3b1b_videos/pi_creature_scene.py b/manimlib/for_3b1b_videos/pi_creature_scene.py index e556c4ad..bb790691 100644 --- a/manimlib/for_3b1b_videos/pi_creature_scene.py +++ b/manimlib/for_3b1b_videos/pi_creature_scene.py @@ -148,12 +148,12 @@ class PiCreatureScene(Scene): self.pi_creature_thinks( self.get_primary_pi_creature(), *content, **kwargs) - def compile_play_args_to_animation_list(self, *args): + def compile_play_args_to_animation_list(self, *args, **kwargs): """ Add animations so that all pi creatures look at the first mobject being animated with each .play call """ - animations = Scene.compile_play_args_to_animation_list(self, *args) + animations = Scene.compile_play_args_to_animation_list(self, *args, **kwargs) if not self.any_pi_creatures_on_screen(): return animations @@ -211,21 +211,24 @@ class PiCreatureScene(Scene): ]) return self - def wait(self, time=1, blink=True): + def wait(self, time=1, blink=True, **kwargs): + if "stop_condition" in kwargs: + self.non_blink_wait(time, **kwargs) + return while time >= 1: time_to_blink = self.total_wait_time % self.seconds_to_blink == 0 if blink and self.any_pi_creatures_on_screen() and time_to_blink: self.blink() else: - self.non_blink_wait() + self.non_blink_wait(**kwargs) time -= 1 self.total_wait_time += 1 if time > 0: - self.non_blink_wait(time) + self.non_blink_wait(time, **kwargs) return self - def non_blink_wait(self, time=1): - Scene.wait(self, time) + def non_blink_wait(self, time=1, **kwargs): + Scene.wait(self, time, **kwargs) return self def change_mode(self, mode): diff --git a/manimlib/mobject/geometry.py b/manimlib/mobject/geometry.py index 8ad462ce..15c0f0ce 100644 --- a/manimlib/mobject/geometry.py +++ b/manimlib/mobject/geometry.py @@ -542,7 +542,7 @@ class Arrow(Line): "stroke_width": 6, "buff": MED_SMALL_BUFF, "tip_width_to_length_ratio": 1, - "max_tip_length_to_length_ratio": 0.2, + "max_tip_length_to_length_ratio": 0.25, "max_stroke_width_to_length_ratio": 6, "preserve_tip_size_when_scaling": True, "rectangular_stem_width": 0.05, @@ -560,15 +560,17 @@ class Arrow(Line): has_tip = self.has_tip() has_start_tip = self.has_start_tip() if has_tip or has_start_tip: - self.pop_tips() + old_tips = self.pop_tips() VMobject.scale(self, factor, **kwargs) self.set_stroke_width_from_length() if has_tip: self.add_tip() + self.tip.match_style(old_tips[0]) if has_start_tip: self.add_tip(at_start=True) + self.start_tip.match_style(old_tips[1]) return self def get_normal_vector(self): @@ -588,10 +590,13 @@ class Arrow(Line): def set_stroke_width_from_length(self): max_ratio = self.max_stroke_width_to_length_ratio - self.set_stroke(width=min( - self.initial_stroke_width, - max_ratio * self.get_length(), - )) + self.set_stroke( + width=min( + self.initial_stroke_width, + max_ratio * self.get_length(), + ), + family=False, + ) return self # TODO, should this be the default for everything? diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 57d30e3a..02e4d2b2 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -4,6 +4,7 @@ import itertools as it import operator as op import os import random +import sys from colour import Color import numpy as np diff --git a/manimlib/mobject/number_line.py b/manimlib/mobject/number_line.py index cf5d1e92..8128193a 100644 --- a/manimlib/mobject/number_line.py +++ b/manimlib/mobject/number_line.py @@ -97,7 +97,7 @@ class NumberLine(Line): def get_tick_numbers(self): return np.arange( self.leftmost_tick, - self.x_max - self.tick_frequency / 2, + self.x_max + self.tick_frequency / 2, self.tick_frequency ) diff --git a/manimlib/mobject/svg/drawings.py b/manimlib/mobject/svg/drawings.py index d2a70b64..7c97adb0 100644 --- a/manimlib/mobject/svg/drawings.py +++ b/manimlib/mobject/svg/drawings.py @@ -446,7 +446,10 @@ class Bubble(SVGMobject): return self.get_center() + factor * self.get_height() * UP def move_tip_to(self, point): - VGroup(self, self.content).shift(point - self.get_tip()) + mover = VGroup(self) + if self.content is not None: + mover.add(self.content) + mover.shift(point - self.get_tip()) return self def flip(self): diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index d3c5f73e..433f9f67 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -202,6 +202,7 @@ class VMobject(Mobject): ) if background_image_file: self.color_using_background_image(background_image_file) + return self def get_style(self): return { @@ -209,8 +210,10 @@ class VMobject(Mobject): "fill_opacity": self.get_fill_opacities(), "stroke_color": self.get_stroke_colors(), "stroke_width": self.get_stroke_width(), + "stroke_opacity": self.get_stroke_opacity(), "background_stroke_color": self.get_stroke_colors(background=True), "background_stroke_width": self.get_stroke_width(background=True), + "background_stroke_opacity": self.get_stroke_opacity(background=True), "sheen_factor": self.get_sheen_factor(), "sheen_direction": self.get_sheen_direction(), "background_image_file": self.get_background_image_file(), @@ -835,6 +838,8 @@ class VMobject(Mobject): upper_index, upper_residue = integer_interpolate(0, num_cubics, b) self.clear_points() + if num_cubics == 0: + return self if lower_index == upper_index: self.append_points(partial_bezier_points( bezier_quads[lower_index], diff --git a/manimlib/mobject/vector_field.py b/manimlib/mobject/vector_field.py new file mode 100644 index 00000000..bfc90c2c --- /dev/null +++ b/manimlib/mobject/vector_field.py @@ -0,0 +1,341 @@ +import numpy as np +import os +import itertools as it +from PIL import Image +import random + +from manimlib.constants import * + +from manimlib.animation.composition import AnimationGroup +from manimlib.animation.indication import ShowPassingFlash +from manimlib.mobject.geometry import Vector +from manimlib.mobject.types.vectorized_mobject import VGroup +from manimlib.mobject.types.vectorized_mobject import VMobject +from manimlib.utils.bezier import inverse_interpolate +from manimlib.utils.bezier import interpolate +from manimlib.utils.color import color_to_rgb +from manimlib.utils.color import rgb_to_color +from manimlib.utils.config_ops import digest_config +from manimlib.utils.rate_functions import linear +from manimlib.utils.simple_functions import sigmoid +from manimlib.utils.space_ops import get_norm +# from manimlib.utils.space_ops import normalize + + +DEFAULT_SCALAR_FIELD_COLORS = [BLUE_E, GREEN, YELLOW, RED] + + +def get_colored_background_image(scalar_field_func, + number_to_rgb_func, + pixel_height=DEFAULT_PIXEL_HEIGHT, + pixel_width=DEFAULT_PIXEL_WIDTH): + ph = pixel_height + pw = pixel_width + fw = FRAME_WIDTH + fh = FRAME_HEIGHT + points_array = np.zeros((ph, pw, 3)) + x_array = np.linspace(-fw / 2, fw / 2, pw) + x_array = x_array.reshape((1, len(x_array))) + x_array = x_array.repeat(ph, axis=0) + + y_array = np.linspace(fh / 2, -fh / 2, ph) + y_array = y_array.reshape((len(y_array), 1)) + y_array.repeat(pw, axis=1) + points_array[:, :, 0] = x_array + points_array[:, :, 1] = y_array + scalars = np.apply_along_axis(scalar_field_func, 2, points_array) + rgb_array = number_to_rgb_func(scalars.flatten()).reshape((ph, pw, 3)) + return Image.fromarray((rgb_array * 255).astype('uint8')) + + +def get_rgb_gradient_function(min_value=0, max_value=1, + colors=[BLUE, RED], + flip_alphas=True, # Why? + ): + rgbs = np.array(list(map(color_to_rgb, colors))) + + def func(values): + alphas = inverse_interpolate( + min_value, max_value, np.array(values) + ) + alphas = np.clip(alphas, 0, 1) + # if flip_alphas: + # alphas = 1 - alphas + scaled_alphas = alphas * (len(rgbs) - 1) + indices = scaled_alphas.astype(int) + next_indices = np.clip(indices + 1, 0, len(rgbs) - 1) + inter_alphas = scaled_alphas % 1 + inter_alphas = inter_alphas.repeat(3).reshape((len(indices), 3)) + result = interpolate(rgbs[indices], rgbs[next_indices], inter_alphas) + return result + return func + + +def get_color_field_image_file(scalar_func, + min_value=0, max_value=2, + colors=DEFAULT_SCALAR_FIELD_COLORS + ): + # try_hash + np.random.seed(0) + sample_inputs = 5 * np.random.random(size=(10, 3)) - 10 + sample_outputs = np.apply_along_axis(scalar_func, 1, sample_inputs) + func_hash = hash( + str(min_value) + str(max_value) + str(colors) + str(sample_outputs) + ) + file_name = "%d.png" % func_hash + full_path = os.path.join(RASTER_IMAGE_DIR, file_name) + if not os.path.exists(full_path): + print("Rendering color field image " + str(func_hash)) + rgb_gradient_func = get_rgb_gradient_function( + min_value=min_value, + max_value=max_value, + colors=colors + ) + image = get_colored_background_image(scalar_func, rgb_gradient_func) + image.save(full_path) + return full_path + + +def move_along_vector_field(mobject, func): + mobject.add_updater( + lambda m, dt: m.shift( + func(m.get_center()) * dt + ) + ) + return mobject + + +def move_submobjects_along_vector_field(mobject, func): + def apply_nudge(mob, dt): + for submob in mob: + x, y = submob.get_center()[:2] + if abs(x) < FRAME_WIDTH and abs(y) < FRAME_HEIGHT: + submob.shift(func(submob.get_center()) * dt) + + mobject.add_updater(apply_nudge) + return mobject + + +def move_points_along_vector_field(mobject, func): + def apply_nudge(self, dt): + self.mobject.apply_function( + lambda p: p + func(p) * dt + ) + mobject.add_updater(apply_nudge) + return mobject + + +# Mobjects + +class VectorField(VGroup): + CONFIG = { + "delta_x": 0.5, + "delta_y": 0.5, + "x_min": int(np.floor(-FRAME_WIDTH / 2)), + "x_max": int(np.ceil(FRAME_WIDTH / 2)), + "y_min": int(np.floor(-FRAME_HEIGHT / 2)), + "y_max": int(np.ceil(FRAME_HEIGHT / 2)), + "min_magnitude": 0, + "max_magnitude": 2, + "colors": DEFAULT_SCALAR_FIELD_COLORS, + # Takes in actual norm, spits out displayed norm + "length_func": lambda norm: 0.45 * sigmoid(norm), + "opacity": 1.0, + "vector_config": {}, + } + + def __init__(self, func, **kwargs): + VGroup.__init__(self, **kwargs) + self.func = func + self.rgb_gradient_function = get_rgb_gradient_function( + self.min_magnitude, + self.max_magnitude, + self.colors, + flip_alphas=False + ) + x_range = np.arange( + self.x_min, + self.x_max + self.delta_x, + self.delta_x + ) + y_range = np.arange( + self.y_min, + self.y_max + self.delta_y, + self.delta_y + ) + for x, y in it.product(x_range, y_range): + point = x * RIGHT + y * UP + self.add(self.get_vector(point)) + self.set_opacity(self.opacity) + + def get_vector(self, point, **kwargs): + output = np.array(self.func(point)) + norm = get_norm(output) + if norm == 0: + output *= 0 + else: + output *= self.length_func(norm) / norm + vector_config = dict(self.vector_config) + vector_config.update(kwargs) + vect = Vector(output, **vector_config) + vect.shift(point) + fill_color = rgb_to_color( + self.rgb_gradient_function(np.array([norm]))[0] + ) + vect.set_color(fill_color) + return vect + + +class StreamLines(VGroup): + CONFIG = { + # TODO, this is an awkward way to inherit + # defaults to a method. + "start_points_generator_config": {}, + # Config for choosing start points + "x_min": -8, + "x_max": 8, + "y_min": -5, + "y_max": 5, + "delta_x": 0.5, + "delta_y": 0.5, + "n_repeats": 1, + "noise_factor": None, + # Config for drawing lines + "dt": 0.05, + "virtual_time": 3, + "n_anchors_per_line": 100, + "stroke_width": 1, + "stroke_color": WHITE, + "color_by_arc_length": True, + # Min and max arc lengths meant to define + # the color range, should color_by_arc_length be True + "min_arc_length": 0, + "max_arc_length": 12, + "color_by_magnitude": False, + # Min and max magnitudes meant to define + # the color range, should color_by_magnitude be True + "min_magnitude": 0.5, + "max_magnitude": 1.5, + "colors": DEFAULT_SCALAR_FIELD_COLORS, + "cutoff_norm": 15, + } + + def __init__(self, func, **kwargs): + VGroup.__init__(self, **kwargs) + self.func = func + dt = self.dt + + start_points = self.get_start_points( + **self.start_points_generator_config + ) + for point in start_points: + points = [point] + for t in np.arange(0, self.virtual_time, dt): + last_point = points[-1] + points.append(last_point + dt * func(last_point)) + if get_norm(last_point) > self.cutoff_norm: + break + line = VMobject() + step = max(1, int(len(points) / self.n_anchors_per_line)) + line.set_points_smoothly(points[::step]) + self.add(line) + + self.set_stroke(self.stroke_color, self.stroke_width) + + if self.color_by_arc_length: + len_to_rgb = get_rgb_gradient_function( + self.min_arc_length, + self.max_arc_length, + colors=self.colors, + ) + for line in self: + arc_length = line.get_arc_length() + rgb = len_to_rgb([arc_length])[0] + color = rgb_to_color(rgb) + line.set_color(color) + elif self.color_by_magnitude: + image_file = get_color_field_image_file( + lambda p: get_norm(func(p)), + min_value=self.min_magnitude, + max_value=self.max_magnitude, + colors=self.colors, + ) + self.color_using_background_image(image_file) + + def get_start_points(self): + x_min = self.x_min + x_max = self.x_max + y_min = self.y_min + y_max = self.y_max + delta_x = self.delta_x + delta_y = self.delta_y + n_repeats = self.n_repeats + noise_factor = self.noise_factor + + if noise_factor is None: + noise_factor = delta_y / 2 + return np.array([ + x * RIGHT + y * UP + noise_factor * np.random.random(3) + for n in range(n_repeats) + for x in np.arange(x_min, x_max + delta_x, delta_x) + for y in np.arange(y_min, y_max + delta_y, delta_y) + ]) + + +# TODO: Make it so that you can have a group of stream_lines +# varying in response to a changing vector field, and still +# animate the resulting flow +class ShowPassingFlashWithThinningStrokeWidth(AnimationGroup): + CONFIG = { + "n_segments": 10, + "time_width": 0.1, + "remover": True + } + + def __init__(self, vmobject, **kwargs): + digest_config(self, kwargs) + max_stroke_width = vmobject.get_stroke_width() + max_time_width = kwargs.pop("time_width", self.time_width) + AnimationGroup.__init__(self, *[ + ShowPassingFlash( + vmobject.deepcopy().set_stroke(width=stroke_width), + time_width=time_width, + **kwargs + ) + for stroke_width, time_width in zip( + np.linspace(0, max_stroke_width, self.n_segments), + np.linspace(max_time_width, 0, self.n_segments) + ) + ]) + + +# TODO, this is untested after turning it from a +# ContinualAnimation into a VGroup +class AnimatedStreamLines(VGroup): + CONFIG = { + "lag_range": 4, + "line_anim_class": ShowPassingFlash, + "line_anim_config": { + "run_time": 4, + "rate_func": linear, + "time_width": 0.3, + }, + } + + def __init__(self, stream_lines, **kwargs): + VGroup.__init__(self, **kwargs) + self.stream_lines = stream_lines + for line in stream_lines: + line.anim = self.line_anim_class(line, **self.line_anim_config) + line.anim.begin() + line.time = -self.lag_range * random.random() + self.add(line.anim.mobject) + + self.add_updater(lambda m, dt: m.update(dt)) + + def update(self, dt): + stream_lines = self.stream_lines + for line in stream_lines: + line.time += dt + adjusted_time = max(line.time, 0) % line.anim.run_time + line.anim.update(adjusted_time / line.anim.run_time) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 6776da62..2a0ca6b4 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -503,7 +503,7 @@ class Scene(Container): self.update_mobjects(dt) self.update_frame() self.add_frames(self.get_frame()) - if stop_condition and stop_condition(): + if stop_condition is not None and stop_condition(): time_progression.close() break elif self.skip_animations: diff --git a/manimlib/scene/vector_space_scene.py b/manimlib/scene/vector_space_scene.py index 8b55e733..cd1c8dd4 100644 --- a/manimlib/scene/vector_space_scene.py +++ b/manimlib/scene/vector_space_scene.py @@ -287,6 +287,9 @@ class LinearTransformationScene(VectorScene): }, "background_plane_kwargs": { "color": GREY, + "axis_config": { + "stroke_color": LIGHT_GREY, + }, "number_line_config": { "color": GREY, }, @@ -358,7 +361,7 @@ class LinearTransformationScene(VectorScene): self.add_special_mobjects(self.moving_mobjects, mobject) def get_unit_square(self, color=YELLOW, opacity=0.3, stroke_width=3): - square = Rectangle( + square = self.square = Rectangle( color=color, width=self.plane.get_x_unit_size(), height=self.plane.get_y_unit_size(), diff --git a/old_projects/div_curl.py b/old_projects/div_curl.py index ebb0cb05..8e8114a7 100644 --- a/old_projects/div_curl.py +++ b/old_projects/div_curl.py @@ -1,7 +1,6 @@ from big_ol_pile_of_manim_imports import * -DEFAULT_SCALAR_FIELD_COLORS = [BLUE_E, GREEN, YELLOW, RED] # Quick note to anyone coming to this file with the # intent of recreating animations from the video. Some @@ -24,22 +23,6 @@ RABBIT_COLOR = "#C6D6EF" # Helper functions -def get_flow_start_points(x_min=-8, x_max=8, - y_min=-5, y_max=5, - delta_x=0.5, delta_y=0.5, - n_repeats=1, - noise_factor=None - ): - if noise_factor is None: - noise_factor = delta_y / 2 - return np.array([ - x * RIGHT + y * UP + noise_factor * np.random.random(3) - for n in range(n_repeats) - for x in np.arange(x_min, x_max + delta_x, delta_x) - for y in np.arange(y_min, y_max + delta_y, delta_y) - ]) - - def joukowsky_map(z): if z == 0: return 0 @@ -99,77 +82,6 @@ def cylinder_flow_magnitude_field(point): return get_norm(cylinder_flow_vector_field(point)) -def get_colored_background_image(scalar_field_func, - number_to_rgb_func, - pixel_height=DEFAULT_PIXEL_HEIGHT, - pixel_width=DEFAULT_PIXEL_WIDTH, - ): - ph = pixel_height - pw = pixel_width - fw = FRAME_WIDTH - fh = FRAME_HEIGHT - points_array = np.zeros((ph, pw, 3)) - x_array = np.linspace(-fw / 2, fw / 2, pw) - x_array = x_array.reshape((1, len(x_array))) - x_array = x_array.repeat(ph, axis=0) - - y_array = np.linspace(fh / 2, -fh / 2, ph) - y_array = y_array.reshape((len(y_array), 1)) - y_array.repeat(pw, axis=1) - points_array[:, :, 0] = x_array - points_array[:, :, 1] = y_array - scalars = np.apply_along_axis(scalar_field_func, 2, points_array) - rgb_array = number_to_rgb_func(scalars.flatten()).reshape((ph, pw, 3)) - return Image.fromarray((rgb_array * 255).astype('uint8')) - - -def get_rgb_gradient_function(min_value=0, max_value=1, - colors=[BLUE, RED], - flip_alphas=True, # Why? - ): - rgbs = np.array(list(map(color_to_rgb, colors))) - - def func(values): - alphas = inverse_interpolate(min_value, max_value, values) - alphas = np.clip(alphas, 0, 1) - # if flip_alphas: - # alphas = 1 - alphas - scaled_alphas = alphas * (len(rgbs) - 1) - indices = scaled_alphas.astype(int) - next_indices = np.clip(indices + 1, 0, len(rgbs) - 1) - inter_alphas = scaled_alphas % 1 - inter_alphas = inter_alphas.repeat(3).reshape((len(indices), 3)) - result = interpolate(rgbs[indices], rgbs[next_indices], inter_alphas) - return result - - return func - - -def get_color_field_image_file(scalar_func, - min_value=0, max_value=2, - colors=DEFAULT_SCALAR_FIELD_COLORS - ): - # try_hash - np.random.seed(0) - sample_inputs = 5 * np.random.random(size=(10, 3)) - 10 - sample_outputs = np.apply_along_axis(scalar_func, 1, sample_inputs) - func_hash = hash( - str(min_value) + str(max_value) + str(colors) + str(sample_outputs) - ) - file_name = "%d.png" % func_hash - full_path = os.path.join(RASTER_IMAGE_DIR, file_name) - if not os.path.exists(full_path): - print("Rendering color field image " + str(func_hash)) - rgb_gradient_func = get_rgb_gradient_function( - min_value=min_value, - max_value=max_value, - colors=colors - ) - image = get_colored_background_image(scalar_func, rgb_gradient_func) - image.save(full_path) - return full_path - - def vec_tex(s): return "\\vec{\\textbf{%s}}" % s @@ -244,204 +156,6 @@ def preditor_prey_vector_field(point): # Mobjects - -class StreamLines(VGroup): - CONFIG = { - "start_points_generator": get_flow_start_points, - "start_points_generator_config": {}, - "dt": 0.05, - "virtual_time": 3, - "n_anchors_per_line": 100, - "stroke_width": 1, - "stroke_color": WHITE, - "color_lines_by_magnitude": True, - "min_magnitude": 0.5, - "max_magnitude": 1.5, - "colors": DEFAULT_SCALAR_FIELD_COLORS, - "cutoff_norm": 15, - } - - def __init__(self, func, **kwargs): - VGroup.__init__(self, **kwargs) - self.func = func - dt = self.dt - - start_points = self.start_points_generator( - **self.start_points_generator_config - ) - for point in start_points: - points = [point] - for t in np.arange(0, self.virtual_time, dt): - last_point = points[-1] - points.append(last_point + dt * func(last_point)) - if get_norm(last_point) > self.cutoff_norm: - break - line = VMobject() - step = max(1, int(len(points) / self.n_anchors_per_line)) - line.set_points_smoothly(points[::step]) - self.add(line) - - self.set_stroke(self.stroke_color, self.stroke_width) - - if self.color_lines_by_magnitude: - image_file = get_color_field_image_file( - lambda p: get_norm(func(p)), - min_value=self.min_magnitude, - max_value=self.max_magnitude, - colors=self.colors, - ) - self.color_using_background_image(image_file) - - -class VectorField(VGroup): - CONFIG = { - "delta_x": 0.5, - "delta_y": 0.5, - "x_min": int(np.floor(-FRAME_WIDTH / 2)), - "x_max": int(np.ceil(FRAME_WIDTH / 2)), - "y_min": int(np.floor(-FRAME_HEIGHT / 2)), - "y_max": int(np.ceil(FRAME_HEIGHT / 2)), - "min_magnitude": 0, - "max_magnitude": 2, - "colors": DEFAULT_SCALAR_FIELD_COLORS, - # Takes in actual norm, spits out displayed norm - "length_func": lambda norm: 0.5 * sigmoid(norm), - "stroke_color": BLACK, - "stroke_width": 0.5, - "fill_opacity": 1.0, - "vector_config": {}, - } - - def __init__(self, func, **kwargs): - VGroup.__init__(self, **kwargs) - self.func = func - self.rgb_gradient_function = get_rgb_gradient_function( - self.min_magnitude, - self.max_magnitude, - self.colors, - flip_alphas=False - ) - for x in np.arange(self.x_min, self.x_max, self.delta_x): - for y in np.arange(self.y_min, self.y_max, self.delta_y): - point = x * RIGHT + y * UP - self.add(self.get_vector(point)) - - def get_vector(self, point, **kwargs): - output = np.array(self.func(point)) - norm = get_norm(output) - if norm == 0: - output *= 0 - else: - output *= self.length_func(norm) / norm - vector_config = dict(self.vector_config) - vector_config.update(kwargs) - vect = Vector(output, **vector_config) - vect.shift(point) - fill_color = rgb_to_color( - self.rgb_gradient_function(np.array([norm]))[0] - ) - vect.set_color(fill_color) - vect.set_fill(opacity=self.fill_opacity) - vect.set_stroke( - self.stroke_color, - self.stroke_width - ) - return vect - - -# Redefining what was once a ContinualAnimation class -# as a function -def VectorFieldFlow(mobject, func): - mobject.add_updater( - lambda m, dt: m.shift( - func(m.get_center()) * dt - ) - ) - return mobject - - -# Redefining what was once a ContinualAnimation class -# as a function -def VectorFieldSubmobjectFlow(mobject, func): - def apply_nudge(mob, dt): - for submob in mob: - x, y = submob.get_center()[:2] - if abs(x) < FRAME_WIDTH and abs(y) < FRAME_HEIGHT: - submob.shift(func(submob.get_center()) * dt) - - mobject.add_updater(apply_nudge) - return mobject - - -# Redefining what was once a ContinualAnimation class -# as a function -def VectorFieldPointFlow(mobject, func): - def apply_nudge(self, dt): - self.mobject.apply_function( - lambda p: p + func(p) * dt - ) - mobject.add_updater(apply_nudge) - return mobject - - -# TODO: Make it so that you can have a group of stream_lines -# varying in response to a changing vector field, and still -# animate the resulting flow -class ShowPassingFlashWithThinningStrokeWidth(AnimationGroup): - CONFIG = { - "n_segments": 10, - "time_width": 0.1, - "remover": True - } - - def __init__(self, vmobject, **kwargs): - digest_config(self, kwargs) - max_stroke_width = vmobject.get_stroke_width() - max_time_width = kwargs.pop("time_width", self.time_width) - AnimationGroup.__init__(self, *[ - ShowPassingFlash( - vmobject.deepcopy().set_stroke(width=stroke_width), - time_width=time_width, - **kwargs - ) - for stroke_width, time_width in zip( - np.linspace(0, max_stroke_width, self.n_segments), - np.linspace(max_time_width, 0, self.n_segments) - ) - ]) - - -# TODO, this is untested after turning it from a -# ContinualAnimation into a VGroup -class AnimatedStreamLines(VGroup): - CONFIG = { - "lag_range": 4, - "line_anim_class": ShowPassingFlash, - "line_anim_config": { - "run_time": 4, - "rate_func": linear, - "time_width": 0.3, - }, - } - - def __init__(self, stream_lines, **kwargs): - VGroup.__init__(self, **kwargs) - self.stream_lines = stream_lines - for line in stream_lines: - line.anim = self.line_anim_class(line, **self.line_anim_config) - line.time = -self.lag_range * random.random() - self.add(line.anim.mobject) - - self.add_updater(lambda m, dt: m.update(dt)) - - def update(self, dt): - stream_lines = self.stream_lines - for line in stream_lines: - line.time += dt - adjusted_time = max(line.time, 0) % line.anim.run_time - line.anim.update(adjusted_time / line.anim.run_time) - - # TODO, this is untested after turning it from a # ContinualAnimation into a VGroup class JigglingSubmobjects(VGroup): @@ -3066,7 +2780,7 @@ class ShowTwoPopulations(Scene): self.start_num_rabbits * RIGHT + self.start_num_foxes * UP ) - self.add(VectorFieldFlow( + self.add(move_along_vector_field( phase_point, preditor_prey_vector_field, )) @@ -3420,7 +3134,7 @@ class PhaseSpaceOfPopulationModel(ShowTwoPopulations, PiCreatureScene, MovingCam dot_vector = new_dot_vector self.play(dot.move_to, dot_vector.get_end()) - dot_movement = VectorFieldFlow( + dot_movement = move_along_vector_field( dot, lambda p: 0.3 * vector_field.func(p) ) self.add(dot_movement) diff --git a/old_projects/turbulence.py b/old_projects/turbulence.py index 52ec382f..bd554414 100644 --- a/old_projects/turbulence.py +++ b/old_projects/turbulence.py @@ -1,7 +1,7 @@ from big_ol_pile_of_manim_imports import * from old_projects.div_curl import PureAirfoilFlow -from old_projects.div_curl import VectorFieldSubmobjectFlow -from old_projects.div_curl import VectorFieldPointFlow +from old_projects.div_curl import move_submobjects_along_vector_field +from old_projects.div_curl import move_points_along_vector_field from old_projects.div_curl import four_swirls_function from old_projects.lost_lecture import ShowWord @@ -836,7 +836,7 @@ class LaminarFlowLabel(Scene): class HighCurlFieldBreakingLayers(Scene): CONFIG = { - "flow_anim": VectorFieldSubmobjectFlow, + "flow_anim": move_submobjects_along_vector_field, } def construct(self): @@ -870,7 +870,7 @@ class HighCurlFieldBreakingLayers(Scene): class HighCurlFieldBreakingLayersLines(HighCurlFieldBreakingLayers): CONFIG = { - "flow_anim": VectorFieldPointFlow + "flow_anim": move_points_along_vector_field } def get_line(self):