mirror of
https://github.com/3b1b/manim.git
synced 2025-08-20 05:14:12 +00:00
commit
b2f973dce6
23 changed files with 1799 additions and 330 deletions
|
@ -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")
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
108
active_projects/ode/part1/pi_scenes.py
Normal file
108
active_projects/ode/part1/pi_scenes.py
Normal file
|
@ -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
|
16
active_projects/ode/part1/shared_constructs.py
Normal file
16
active_projects/ode/part1/shared_constructs.py
Normal file
|
@ -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,
|
||||
}
|
414
active_projects/ode/part1/staging.py
Normal file
414
active_projects/ode/part1/staging.py
Normal file
|
@ -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()
|
|
@ -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 *
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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.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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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],
|
||||
|
|
341
manimlib/mobject/vector_field.py
Normal file
341
manimlib/mobject/vector_field.py
Normal file
|
@ -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)
|
|
@ -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:
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Add table
Reference in a new issue