mirror of
https://github.com/3b1b/manim.git
synced 2025-08-20 21:34:05 +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)
|
ip = self.get_input_parallelogram(vect)
|
||||||
self.add_transformable_mobject(ip)
|
self.add_transformable_mobject(ip)
|
||||||
self.apply_transposed_matrix([[2, -0.5], [1, 2]])
|
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)
|
self.camera_frame.shift(UP)
|
||||||
|
|
||||||
words = TextMobject("Cramer's", "rule")
|
words = TextMobject("Cramer's", "rule")
|
||||||
|
|
|
@ -1,6 +1,22 @@
|
||||||
from active_projects.ode.part1.pendulum import *
|
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"
|
OUTPUT_DIRECTORY = "ode/part1"
|
||||||
ALL_SCENE_CLASSES = [
|
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 big_ol_pile_of_manim_imports import *
|
||||||
|
from active_projects.ode.part1.shared_constructs import *
|
||||||
|
|
||||||
|
|
||||||
class Pendulum(VGroup):
|
class Pendulum(VGroup):
|
||||||
|
@ -19,9 +20,12 @@ class Pendulum(VGroup):
|
||||||
"weight_style": {
|
"weight_style": {
|
||||||
"stroke_width": 0,
|
"stroke_width": 0,
|
||||||
"fill_opacity": 1,
|
"fill_opacity": 1,
|
||||||
"fill_color": DARK_GREY,
|
"fill_color": GREY_BROWN,
|
||||||
"sheen_direction": UL,
|
"sheen_direction": UL,
|
||||||
"sheen_factor": 0.5,
|
"sheen_factor": 0.5,
|
||||||
|
"background_stroke_color": BLACK,
|
||||||
|
"background_stroke_width": 3,
|
||||||
|
"background_stroke_opacity": 0.5,
|
||||||
},
|
},
|
||||||
"dashed_line_config": {
|
"dashed_line_config": {
|
||||||
"num_dashes": 25,
|
"num_dashes": 25,
|
||||||
|
@ -36,7 +40,8 @@ class Pendulum(VGroup):
|
||||||
"velocity_vector_config": {
|
"velocity_vector_config": {
|
||||||
"color": RED,
|
"color": RED,
|
||||||
},
|
},
|
||||||
"n_steps_per_frame": 10,
|
"theta_label_height": 0.25,
|
||||||
|
"n_steps_per_frame": 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
@ -47,12 +52,15 @@ class Pendulum(VGroup):
|
||||||
self.rotating_group = VGroup(self.rod, self.weight)
|
self.rotating_group = VGroup(self.rod, self.weight)
|
||||||
self.create_dashed_line()
|
self.create_dashed_line()
|
||||||
self.create_angle_arc()
|
self.create_angle_arc()
|
||||||
|
self.add_theta_label()
|
||||||
|
|
||||||
self.set_theta(self.initial_theta)
|
self.set_theta(self.initial_theta)
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def create_fixed_point(self):
|
def create_fixed_point(self):
|
||||||
self.fixed_point_tracker = VectorizedPoint(self.top_point)
|
self.fixed_point_tracker = VectorizedPoint(self.top_point)
|
||||||
|
self.add(self.fixed_point_tracker)
|
||||||
|
return self
|
||||||
|
|
||||||
def create_rod(self):
|
def create_rod(self):
|
||||||
rod = self.rod = Line(UP, DOWN)
|
rod = self.rod = Line(UP, DOWN)
|
||||||
|
@ -87,11 +95,33 @@ class Pendulum(VGroup):
|
||||||
|
|
||||||
def add_velocity_vector(self):
|
def add_velocity_vector(self):
|
||||||
def make_vector():
|
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.velocity_vector = always_redraw(make_vector)
|
||||||
self.add(self.velocity_vector)
|
self.add(self.velocity_vector)
|
||||||
return self
|
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):
|
def get_theta(self):
|
||||||
theta = self.rod.get_angle() - self.dashed_line.get_angle()
|
theta = self.rod.get_angle() - self.dashed_line.get_angle()
|
||||||
|
@ -141,11 +171,816 @@ class Pendulum(VGroup):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
class PendulumTest(Scene):
|
class GravityVector(Vector):
|
||||||
def construct(self):
|
CONFIG = {
|
||||||
pendulum = Pendulum(
|
"color": YELLOW,
|
||||||
initial_theta=150 * DEGREES,
|
"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()
|
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)
|
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.types.vectorized_mobject import *
|
||||||
from manimlib.mobject.mobject_update_utils import *
|
from manimlib.mobject.mobject_update_utils import *
|
||||||
from manimlib.mobject.value_tracker 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.common_scenes import *
|
||||||
from manimlib.for_3b1b_videos.pi_creature 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):
|
class FadeInFromLarge(FadeIn):
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
"scale_factor": 2,
|
"scale_factor": 2,
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
from manimlib.mobject.types.vectorized_mobject import VectorizedPoint
|
|
||||||
from manimlib.animation.transform import Transform
|
from manimlib.animation.transform import Transform
|
||||||
# from manimlib.utils.paths import counterclockwise_path
|
# from manimlib.utils.paths import counterclockwise_path
|
||||||
from manimlib.constants import PI
|
from manimlib.constants import PI
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
from functools import reduce
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from manimlib.constants import *
|
from manimlib.constants import *
|
||||||
|
@ -9,12 +7,8 @@ from manimlib.animation.composition import AnimationGroup
|
||||||
from manimlib.animation.composition import Succession
|
from manimlib.animation.composition import Succession
|
||||||
from manimlib.animation.creation import ShowCreation
|
from manimlib.animation.creation import ShowCreation
|
||||||
from manimlib.animation.creation import ShowPartial
|
from manimlib.animation.creation import ShowPartial
|
||||||
from manimlib.animation.fading import FadeIn
|
|
||||||
from manimlib.animation.fading import FadeOut
|
from manimlib.animation.fading import FadeOut
|
||||||
from manimlib.animation.transform import Transform
|
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.types.vectorized_mobject import VMobject
|
||||||
from manimlib.mobject.geometry import Circle
|
from manimlib.mobject.geometry import Circle
|
||||||
from manimlib.mobject.geometry import Dot
|
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.mobject.geometry import Line
|
||||||
from manimlib.utils.bezier import interpolate
|
from manimlib.utils.bezier import interpolate
|
||||||
from manimlib.utils.config_ops import digest_config
|
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 there_and_back
|
||||||
from manimlib.utils.rate_functions import wiggle
|
from manimlib.utils.rate_functions import wiggle
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from manimlib.animation.animation import Animation
|
from manimlib.animation.animation import Animation
|
||||||
from manimlib.utils.config_ops import digest_config
|
|
||||||
from manimlib.utils.rate_functions import linear
|
from manimlib.utils.rate_functions import linear
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -214,6 +214,8 @@ COLOR_MAP = {
|
||||||
"GREY": "#888888",
|
"GREY": "#888888",
|
||||||
"DARK_GREY": "#444444",
|
"DARK_GREY": "#444444",
|
||||||
"DARK_GRAY": "#444444",
|
"DARK_GRAY": "#444444",
|
||||||
|
"DARKER_GREY": "#222222",
|
||||||
|
"DARKER_GRAY": "#222222",
|
||||||
"GREY_BROWN": "#736357",
|
"GREY_BROWN": "#736357",
|
||||||
"PINK": "#D147BD",
|
"PINK": "#D147BD",
|
||||||
"GREEN_SCREEN": "#00FF00",
|
"GREEN_SCREEN": "#00FF00",
|
||||||
|
|
|
@ -148,12 +148,12 @@ class PiCreatureScene(Scene):
|
||||||
self.pi_creature_thinks(
|
self.pi_creature_thinks(
|
||||||
self.get_primary_pi_creature(), *content, **kwargs)
|
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
|
Add animations so that all pi creatures look at the
|
||||||
first mobject being animated with each .play call
|
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():
|
if not self.any_pi_creatures_on_screen():
|
||||||
return animations
|
return animations
|
||||||
|
|
||||||
|
@ -211,21 +211,24 @@ class PiCreatureScene(Scene):
|
||||||
])
|
])
|
||||||
return self
|
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:
|
while time >= 1:
|
||||||
time_to_blink = self.total_wait_time % self.seconds_to_blink == 0
|
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:
|
if blink and self.any_pi_creatures_on_screen() and time_to_blink:
|
||||||
self.blink()
|
self.blink()
|
||||||
else:
|
else:
|
||||||
self.non_blink_wait()
|
self.non_blink_wait(**kwargs)
|
||||||
time -= 1
|
time -= 1
|
||||||
self.total_wait_time += 1
|
self.total_wait_time += 1
|
||||||
if time > 0:
|
if time > 0:
|
||||||
self.non_blink_wait(time)
|
self.non_blink_wait(time, **kwargs)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def non_blink_wait(self, time=1):
|
def non_blink_wait(self, time=1, **kwargs):
|
||||||
Scene.wait(self, time)
|
Scene.wait(self, time, **kwargs)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def change_mode(self, mode):
|
def change_mode(self, mode):
|
||||||
|
|
|
@ -542,7 +542,7 @@ class Arrow(Line):
|
||||||
"stroke_width": 6,
|
"stroke_width": 6,
|
||||||
"buff": MED_SMALL_BUFF,
|
"buff": MED_SMALL_BUFF,
|
||||||
"tip_width_to_length_ratio": 1,
|
"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,
|
"max_stroke_width_to_length_ratio": 6,
|
||||||
"preserve_tip_size_when_scaling": True,
|
"preserve_tip_size_when_scaling": True,
|
||||||
"rectangular_stem_width": 0.05,
|
"rectangular_stem_width": 0.05,
|
||||||
|
@ -560,15 +560,17 @@ class Arrow(Line):
|
||||||
has_tip = self.has_tip()
|
has_tip = self.has_tip()
|
||||||
has_start_tip = self.has_start_tip()
|
has_start_tip = self.has_start_tip()
|
||||||
if has_tip or has_start_tip:
|
if has_tip or has_start_tip:
|
||||||
self.pop_tips()
|
old_tips = self.pop_tips()
|
||||||
|
|
||||||
VMobject.scale(self, factor, **kwargs)
|
VMobject.scale(self, factor, **kwargs)
|
||||||
self.set_stroke_width_from_length()
|
self.set_stroke_width_from_length()
|
||||||
|
|
||||||
if has_tip:
|
if has_tip:
|
||||||
self.add_tip()
|
self.add_tip()
|
||||||
|
self.tip.match_style(old_tips[0])
|
||||||
if has_start_tip:
|
if has_start_tip:
|
||||||
self.add_tip(at_start=True)
|
self.add_tip(at_start=True)
|
||||||
|
self.start_tip.match_style(old_tips[1])
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def get_normal_vector(self):
|
def get_normal_vector(self):
|
||||||
|
@ -588,10 +590,13 @@ class Arrow(Line):
|
||||||
|
|
||||||
def set_stroke_width_from_length(self):
|
def set_stroke_width_from_length(self):
|
||||||
max_ratio = self.max_stroke_width_to_length_ratio
|
max_ratio = self.max_stroke_width_to_length_ratio
|
||||||
self.set_stroke(width=min(
|
self.set_stroke(
|
||||||
|
width=min(
|
||||||
self.initial_stroke_width,
|
self.initial_stroke_width,
|
||||||
max_ratio * self.get_length(),
|
max_ratio * self.get_length(),
|
||||||
))
|
),
|
||||||
|
family=False,
|
||||||
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
# TODO, should this be the default for everything?
|
# TODO, should this be the default for everything?
|
||||||
|
|
|
@ -4,6 +4,7 @@ import itertools as it
|
||||||
import operator as op
|
import operator as op
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
import sys
|
||||||
|
|
||||||
from colour import Color
|
from colour import Color
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
|
@ -97,7 +97,7 @@ class NumberLine(Line):
|
||||||
def get_tick_numbers(self):
|
def get_tick_numbers(self):
|
||||||
return np.arange(
|
return np.arange(
|
||||||
self.leftmost_tick,
|
self.leftmost_tick,
|
||||||
self.x_max - self.tick_frequency / 2,
|
self.x_max + self.tick_frequency / 2,
|
||||||
self.tick_frequency
|
self.tick_frequency
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -446,7 +446,10 @@ class Bubble(SVGMobject):
|
||||||
return self.get_center() + factor * self.get_height() * UP
|
return self.get_center() + factor * self.get_height() * UP
|
||||||
|
|
||||||
def move_tip_to(self, point):
|
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
|
return self
|
||||||
|
|
||||||
def flip(self):
|
def flip(self):
|
||||||
|
|
|
@ -202,6 +202,7 @@ class VMobject(Mobject):
|
||||||
)
|
)
|
||||||
if background_image_file:
|
if background_image_file:
|
||||||
self.color_using_background_image(background_image_file)
|
self.color_using_background_image(background_image_file)
|
||||||
|
return self
|
||||||
|
|
||||||
def get_style(self):
|
def get_style(self):
|
||||||
return {
|
return {
|
||||||
|
@ -209,8 +210,10 @@ class VMobject(Mobject):
|
||||||
"fill_opacity": self.get_fill_opacities(),
|
"fill_opacity": self.get_fill_opacities(),
|
||||||
"stroke_color": self.get_stroke_colors(),
|
"stroke_color": self.get_stroke_colors(),
|
||||||
"stroke_width": self.get_stroke_width(),
|
"stroke_width": self.get_stroke_width(),
|
||||||
|
"stroke_opacity": self.get_stroke_opacity(),
|
||||||
"background_stroke_color": self.get_stroke_colors(background=True),
|
"background_stroke_color": self.get_stroke_colors(background=True),
|
||||||
"background_stroke_width": self.get_stroke_width(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_factor": self.get_sheen_factor(),
|
||||||
"sheen_direction": self.get_sheen_direction(),
|
"sheen_direction": self.get_sheen_direction(),
|
||||||
"background_image_file": self.get_background_image_file(),
|
"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)
|
upper_index, upper_residue = integer_interpolate(0, num_cubics, b)
|
||||||
|
|
||||||
self.clear_points()
|
self.clear_points()
|
||||||
|
if num_cubics == 0:
|
||||||
|
return self
|
||||||
if lower_index == upper_index:
|
if lower_index == upper_index:
|
||||||
self.append_points(partial_bezier_points(
|
self.append_points(partial_bezier_points(
|
||||||
bezier_quads[lower_index],
|
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_mobjects(dt)
|
||||||
self.update_frame()
|
self.update_frame()
|
||||||
self.add_frames(self.get_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()
|
time_progression.close()
|
||||||
break
|
break
|
||||||
elif self.skip_animations:
|
elif self.skip_animations:
|
||||||
|
|
|
@ -287,6 +287,9 @@ class LinearTransformationScene(VectorScene):
|
||||||
},
|
},
|
||||||
"background_plane_kwargs": {
|
"background_plane_kwargs": {
|
||||||
"color": GREY,
|
"color": GREY,
|
||||||
|
"axis_config": {
|
||||||
|
"stroke_color": LIGHT_GREY,
|
||||||
|
},
|
||||||
"number_line_config": {
|
"number_line_config": {
|
||||||
"color": GREY,
|
"color": GREY,
|
||||||
},
|
},
|
||||||
|
@ -358,7 +361,7 @@ class LinearTransformationScene(VectorScene):
|
||||||
self.add_special_mobjects(self.moving_mobjects, mobject)
|
self.add_special_mobjects(self.moving_mobjects, mobject)
|
||||||
|
|
||||||
def get_unit_square(self, color=YELLOW, opacity=0.3, stroke_width=3):
|
def get_unit_square(self, color=YELLOW, opacity=0.3, stroke_width=3):
|
||||||
square = Rectangle(
|
square = self.square = Rectangle(
|
||||||
color=color,
|
color=color,
|
||||||
width=self.plane.get_x_unit_size(),
|
width=self.plane.get_x_unit_size(),
|
||||||
height=self.plane.get_y_unit_size(),
|
height=self.plane.get_y_unit_size(),
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
|
|
||||||
from big_ol_pile_of_manim_imports import *
|
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
|
# Quick note to anyone coming to this file with the
|
||||||
# intent of recreating animations from the video. Some
|
# intent of recreating animations from the video. Some
|
||||||
|
@ -24,22 +23,6 @@ RABBIT_COLOR = "#C6D6EF"
|
||||||
|
|
||||||
|
|
||||||
# Helper functions
|
# 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):
|
def joukowsky_map(z):
|
||||||
if z == 0:
|
if z == 0:
|
||||||
return 0
|
return 0
|
||||||
|
@ -99,77 +82,6 @@ def cylinder_flow_magnitude_field(point):
|
||||||
return get_norm(cylinder_flow_vector_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):
|
def vec_tex(s):
|
||||||
return "\\vec{\\textbf{%s}}" % s
|
return "\\vec{\\textbf{%s}}" % s
|
||||||
|
|
||||||
|
@ -244,204 +156,6 @@ def preditor_prey_vector_field(point):
|
||||||
|
|
||||||
# Mobjects
|
# 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
|
# TODO, this is untested after turning it from a
|
||||||
# ContinualAnimation into a VGroup
|
# ContinualAnimation into a VGroup
|
||||||
class JigglingSubmobjects(VGroup):
|
class JigglingSubmobjects(VGroup):
|
||||||
|
@ -3066,7 +2780,7 @@ class ShowTwoPopulations(Scene):
|
||||||
self.start_num_rabbits * RIGHT +
|
self.start_num_rabbits * RIGHT +
|
||||||
self.start_num_foxes * UP
|
self.start_num_foxes * UP
|
||||||
)
|
)
|
||||||
self.add(VectorFieldFlow(
|
self.add(move_along_vector_field(
|
||||||
phase_point,
|
phase_point,
|
||||||
preditor_prey_vector_field,
|
preditor_prey_vector_field,
|
||||||
))
|
))
|
||||||
|
@ -3420,7 +3134,7 @@ class PhaseSpaceOfPopulationModel(ShowTwoPopulations, PiCreatureScene, MovingCam
|
||||||
dot_vector = new_dot_vector
|
dot_vector = new_dot_vector
|
||||||
self.play(dot.move_to, dot_vector.get_end())
|
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)
|
dot, lambda p: 0.3 * vector_field.func(p)
|
||||||
)
|
)
|
||||||
self.add(dot_movement)
|
self.add(dot_movement)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from big_ol_pile_of_manim_imports import *
|
from big_ol_pile_of_manim_imports import *
|
||||||
from old_projects.div_curl import PureAirfoilFlow
|
from old_projects.div_curl import PureAirfoilFlow
|
||||||
from old_projects.div_curl import VectorFieldSubmobjectFlow
|
from old_projects.div_curl import move_submobjects_along_vector_field
|
||||||
from old_projects.div_curl import VectorFieldPointFlow
|
from old_projects.div_curl import move_points_along_vector_field
|
||||||
from old_projects.div_curl import four_swirls_function
|
from old_projects.div_curl import four_swirls_function
|
||||||
from old_projects.lost_lecture import ShowWord
|
from old_projects.lost_lecture import ShowWord
|
||||||
|
|
||||||
|
@ -836,7 +836,7 @@ class LaminarFlowLabel(Scene):
|
||||||
|
|
||||||
class HighCurlFieldBreakingLayers(Scene):
|
class HighCurlFieldBreakingLayers(Scene):
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
"flow_anim": VectorFieldSubmobjectFlow,
|
"flow_anim": move_submobjects_along_vector_field,
|
||||||
}
|
}
|
||||||
|
|
||||||
def construct(self):
|
def construct(self):
|
||||||
|
@ -870,7 +870,7 @@ class HighCurlFieldBreakingLayers(Scene):
|
||||||
|
|
||||||
class HighCurlFieldBreakingLayersLines(HighCurlFieldBreakingLayers):
|
class HighCurlFieldBreakingLayersLines(HighCurlFieldBreakingLayers):
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
"flow_anim": VectorFieldPointFlow
|
"flow_anim": move_points_along_vector_field
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_line(self):
|
def get_line(self):
|
||||||
|
|
Loading…
Add table
Reference in a new issue