3b1b-manim/active_projects/fourier.py

1234 lines
40 KiB
Python
Raw Normal View History

2018-01-11 18:13:50 -08:00
from helpers import *
from animation.animation import Animation
from animation.transform import *
from animation.simple_animations import *
from animation.playground import *
from animation.continual_animation import *
from topics.geometry import *
from topics.characters import *
from topics.functions import *
from topics.fractals import *
from topics.number_line import *
from topics.combinatorics import *
from topics.numerals import *
from topics.three_dimensions import *
from topics.objects import *
from topics.probability import *
from topics.complex_numbers import *
from scene import Scene
from scene.reconfigurable_scene import ReconfigurableScene
from scene.zoomed_scene import *
from camera import Camera
from mobject import *
from mobject.image_mobject import *
from mobject.vectorized_mobject import *
from mobject.svg_mobject import *
from mobject.tex_mobject import *
from topics.graph_scene import *
#revert_to_original_skipping_status
2018-01-11 18:13:50 -08:00
class AddingPureFrequencies(PiCreatureScene):
CONFIG = {
"A_frequency" : 2.1,
"A_color" : YELLOW,
"D_color" : PINK,
"F_color" : TEAL,
"C_color" : RED,
"sum_color" : GREEN,
"equilibrium_height" : 1.5,
}
2018-01-11 18:13:50 -08:00
def construct(self):
self.add_speaker()
self.play_A440()
2018-01-11 18:13:50 -08:00
self.measure_air_pressure()
self.play_lower_pitch()
self.play_mix()
self.separate_out_parts()
self.draw_sum_at_single_point()
self.draw_full_sum()
2018-01-11 18:13:50 -08:00
self.add_more_notes()
def add_speaker(self):
speaker = SVGMobject(file_name = "speaker")
speaker.to_edge(DOWN)
self.add(speaker)
self.speaker = speaker
def play_A440(self):
2018-01-11 18:13:50 -08:00
randy = self.pi_creature
A_label = TextMobject("A440")
A_label.highlight(self.A_color)
A_label.next_to(self.speaker, UP)
2018-01-11 18:13:50 -08:00
self.broadcast(
FadeIn(A_label),
Succession(
ApplyMethod, randy.change, "pondering",
Animation, randy,
Blink, randy
)
2018-01-11 18:13:50 -08:00
)
self.set_variables_as_attrs(A_label)
2018-01-11 18:13:50 -08:00
def measure_air_pressure(self):
randy = self.pi_creature
axes = Axes(
y_min = -2, y_max = 2,
2018-01-11 18:13:50 -08:00
x_min = 0, x_max = 10,
number_line_config = {"include_tip" : False},
)
axes.stretch_to_fit_height(2)
axes.to_corner(UP+LEFT)
axes.shift(LARGE_BUFF*DOWN)
eh = self.equilibrium_height
equilibrium_line = DashedLine(
axes.coords_to_point(0, eh),
axes.coords_to_point(axes.x_max, eh),
stroke_width = 2,
stroke_color = LIGHT_GREY
)
2018-01-11 18:13:50 -08:00
frequency = self.A_frequency
graph = self.get_wave_graph(frequency, axes)
func = graph.underlying_function
graph.highlight(self.A_color)
2018-01-11 18:13:50 -08:00
pressure = TextMobject("Pressure")
time = TextMobject("Time")
for label in pressure, time:
label.scale_in_place(0.8)
pressure.next_to(axes.y_axis, UP)
pressure.to_edge(LEFT, buff = MED_SMALL_BUFF)
2018-01-11 18:13:50 -08:00
time.next_to(axes.x_axis.get_right(), DOWN+LEFT)
axes.labels = VGroup(pressure, time)
n = 10
2018-01-11 18:13:50 -08:00
brace = Brace(Line(
axes.coords_to_point(n/frequency, func(n/frequency)),
axes.coords_to_point((n+1)/frequency, func((n+1)/frequency)),
2018-01-11 18:13:50 -08:00
), UP)
words = brace.get_text("Imagine 440 per second", buff = SMALL_BUFF)
words.scale(0.8, about_point = words.get_bottom())
2018-01-11 18:13:50 -08:00
self.play(
FadeIn(pressure),
ShowCreation(axes.y_axis)
)
self.play(
Write(time),
ShowCreation(axes.x_axis)
)
self.broadcast(
ShowCreation(graph, run_time = 4, rate_func = None),
ShowCreation(equilibrium_line),
2018-01-11 18:13:50 -08:00
)
axes.add(equilibrium_line)
2018-01-11 18:13:50 -08:00
self.play(
randy.change, "erm", graph,
2018-01-11 18:13:50 -08:00
GrowFromCenter(brace),
Write(words)
)
2018-01-15 19:15:05 -08:00
self.wait()
2018-01-11 18:13:50 -08:00
graph.save_state()
self.play(
FadeOut(brace),
FadeOut(words),
VGroup(axes, graph, axes.labels).shift, 0.8*UP,
graph.fade, 0.85,
graph.shift, 0.8*UP,
2018-01-11 18:13:50 -08:00
)
graph.saved_state.move_to(graph)
self.set_variables_as_attrs(axes, A_graph = graph)
2018-01-11 18:13:50 -08:00
def play_lower_pitch(self):
axes = self.axes
randy = self.pi_creature
frequency = self.A_frequency*(2.0/3.0)
graph = self.get_wave_graph(frequency, axes)
graph.set_color(self.D_color)
D_label = TextMobject("D294")
D_label.highlight(self.D_color)
D_label.move_to(self.A_label)
self.play(
FadeOut(self.A_label),
GrowFromCenter(D_label),
)
self.broadcast(
ShowCreation(graph, run_time = 4, rate_func = None),
randy.change, "happy",
n_circles = 6,
)
self.play(randy.change, "confused", graph)
2018-01-15 19:15:05 -08:00
self.wait(2)
self.set_variables_as_attrs(
D_label,
D_graph = graph
)
2018-01-11 18:13:50 -08:00
def play_mix(self):
self.A_graph.restore()
self.broadcast(
self.get_broadcast_animation(n_circles = 6),
self.pi_creature.change, "thinking",
*[
ShowCreation(graph, run_time = 4, rate_func = None)
for graph in self.A_graph, self.D_graph
]
)
2018-01-15 19:15:05 -08:00
self.wait()
2018-01-11 18:13:50 -08:00
def separate_out_parts(self):
axes = self.axes
speaker = self.speaker
randy = self.pi_creature
A_axes = axes.deepcopy()
A_graph = self.A_graph
A_label = self.A_label
D_axes = axes.deepcopy()
D_graph = self.D_graph
D_label = self.D_label
movers = [A_axes, A_graph, A_label, D_axes, D_graph, D_label]
for mover in movers:
mover.generate_target()
D_target_group = VGroup(D_axes.target, D_graph.target)
A_target_group = VGroup(A_axes.target, A_graph.target)
D_target_group.next_to(axes, DOWN, MED_LARGE_BUFF)
A_target_group.next_to(D_target_group, DOWN, MED_LARGE_BUFF)
A_label.fade(1)
A_label.target.next_to(A_graph.target, UP)
D_label.target.next_to(D_graph.target, UP)
self.play(*it.chain(
map(MoveToTarget, movers),
[
ApplyMethod(mob.shift, SPACE_HEIGHT*DOWN, remover = True)
for mob in randy, speaker
]
))
2018-01-15 19:15:05 -08:00
self.wait()
self.set_variables_as_attrs(A_axes, D_axes)
def draw_sum_at_single_point(self):
axes = self.axes
A_axes = self.A_axes
D_axes = self.D_axes
A_graph = self.A_graph
D_graph = self.D_graph
x = 2.85
A_line = self.get_A_graph_v_line(x)
D_line = self.get_D_graph_v_line(x)
lines = VGroup(A_line, D_line)
sum_lines = lines.copy()
sum_lines.generate_target()
self.stack_v_lines(x, sum_lines.target)
top_axes_point = axes.coords_to_point(x, self.equilibrium_height)
x_point = np.array(top_axes_point)
x_point[1] = 0
v_line = Line(UP, DOWN).scale(SPACE_HEIGHT).move_to(x_point)
self.revert_to_original_skipping_status()
self.play(GrowFromCenter(v_line))
self.play(FadeOut(v_line))
self.play(*map(ShowCreation, lines))
2018-01-15 19:15:05 -08:00
self.wait()
self.play(MoveToTarget(sum_lines, path_arc = np.pi/4))
2018-01-15 19:15:05 -08:00
self.wait(2)
self.play(*[
Transform(
line,
VectorizedPoint(axes.coords_to_point(0, self.equilibrium_height)),
remover = True
)
for line, axes in [
(A_line, A_axes),
(D_line, D_axes),
(sum_lines, axes),
]
])
def draw_full_sum(self):
axes = self.axes
def new_func(x):
result = self.A_graph.underlying_function(x)
result += self.D_graph.underlying_function(x)
result -= self.equilibrium_height
return result
sum_graph = axes.get_graph(new_func)
sum_graph.highlight(self.sum_color)
##TODO
self.play(
self.get_graph_line_animation(self.A_axes, self.A_graph),
self.get_graph_line_animation(self.D_axes, self.D_graph),
self.get_graph_line_animation(axes, sum_graph.deepcopy()),
ShowCreation(sum_graph),
run_time = 15,
rate_func = None
)
2018-01-15 19:15:05 -08:00
self.wait()
2018-01-11 18:13:50 -08:00
self.sum_graph = sum_graph
2018-01-11 18:13:50 -08:00
def add_more_notes(self):
axes = self.axes
2018-01-11 18:13:50 -08:00
A_group = VGroup(self.A_axes, self.A_graph, self.A_label)
D_group = VGroup(self.D_axes, self.D_graph, self.D_label)
squish_group = VGroup(A_group, D_group)
squish_group.generate_target()
squish_group.target.stretch(0.5, 1)
squish_group.target.next_to(axes, DOWN, buff = -SMALL_BUFF)
for group in squish_group.target:
label = group[-1]
bottom = label.get_bottom()
label.stretch_in_place(0.5, 0)
label.move_to(bottom, DOWN)
self.play(MoveToTarget(squish_group))
F_axes = self.D_axes.deepcopy()
C_axes = self.A_axes.deepcopy()
VGroup(F_axes, C_axes).next_to(squish_group, DOWN)
F_graph = self.get_wave_graph(self.A_frequency*4.0/5, F_axes)
F_graph.highlight(self.F_color)
C_graph = self.get_wave_graph(self.A_frequency*6.0/5, C_axes)
C_graph.highlight(self.C_color)
F_label = TextMobject("F349")
C_label = TextMobject("C523")
for label, graph in (F_label, F_graph), (C_label, C_graph):
label.scale(0.5)
label.highlight(graph.get_stroke_color())
label.next_to(graph, UP, SMALL_BUFF)
graphs = [self.A_graph, self.D_graph, F_graph, C_graph]
def new_sum_func(x):
result = sum([
graph.underlying_function(x) - self.equilibrium_height
for graph in graphs
])
result *= 0.5
return result + self.equilibrium_height
new_sum_graph = self.axes.get_graph(
new_sum_func,
num_graph_points = 200
)
new_sum_graph.highlight(BLUE_C)
self.play(*it.chain(
map(ShowCreation, [F_axes, C_axes, F_graph, C_graph,]),
map(Write, [F_label, C_label]),
[FadeOut(self.sum_graph)]
))
kwargs = {"rate_func" : None, "run_time" : 10}
self.play(ShowCreation(new_sum_graph.copy(), **kwargs), *[
self.get_graph_line_animation(curr_axes, graph, **kwargs)
for curr_axes, graph in [
(self.A_axes, self.A_graph),
(self.D_axes, self.D_graph),
(F_axes, F_graph),
(C_axes, C_graph),
(axes, new_sum_graph),
]
])
2018-01-15 19:15:05 -08:00
self.wait()
2018-01-11 18:13:50 -08:00
####
def broadcast(self, *added_anims, **kwargs):
self.play(self.get_broadcast_animation(**kwargs), *added_anims)
def get_broadcast_animation(self, **kwargs):
2018-01-11 18:13:50 -08:00
kwargs["run_time"] = kwargs.get("run_time", 5)
kwargs["n_circles"] = kwargs.get("n_circles", 10)
return Broadcast(self.speaker[1], **kwargs)
2018-01-11 18:13:50 -08:00
def get_wave_graph(self, frequency, axes):
2018-01-11 18:13:50 -08:00
tail_len = 3.0
x_min, x_max = axes.x_min, axes.x_max
def func(x):
value = 0.7*np.cos(2*np.pi*frequency*x)
2018-01-11 18:13:50 -08:00
if x - x_min < tail_len:
value *= smooth((x-x_min)/tail_len)
if x_max - x < tail_len:
value *= smooth((x_max - x )/tail_len)
return value + self.equilibrium_height
ngp = 2*(x_max - x_min)*frequency + 1
graph = axes.get_graph(func, num_graph_points = ngp)
return graph
def get_A_graph_v_line(self, x):
return self.get_graph_v_line(x, self.A_axes, self.A_graph)
def get_D_graph_v_line(self, x):
return self.get_graph_v_line(x, self.D_axes, self.D_graph)
def get_graph_v_line(self, x, axes, graph):
result = Line(
axes.coords_to_point(x, self.equilibrium_height),
# axes.coords_to_point(x, graph.underlying_function(x)),
graph.point_from_proportion(float(x)/axes.x_max),
color = WHITE,
buff = 0,
)
return result
def stack_v_lines(self, x, lines):
point = self.axes.coords_to_point(x, self.equilibrium_height)
A_line, D_line = lines
A_line.shift(point - A_line.get_start())
D_line.shift(A_line.get_end()-D_line.get_start())
A_line.highlight(self.A_color)
D_line.highlight(self.D_color)
return lines
2018-01-11 18:13:50 -08:00
def create_pi_creature(self):
return Randolph().to_corner(DOWN+LEFT)
def get_graph_line_animation(self, axes, graph, **kwargs):
line = self.get_graph_v_line(0, axes, graph)
x_max = axes.x_max
def update_line(line, alpha):
x = alpha*x_max
Transform(line, self.get_graph_v_line(x, axes, graph)).update(1)
return line
2018-01-11 18:13:50 -08:00
return UpdateFromAlphaFunc(line, update_line, **kwargs)
class BreakApartSum(Scene):
CONFIG = {
"frequencies" : [0.5, 1.5, 2, 2.5, 5],
"equilibrium_height" : 2.0,
}
def construct(self):
self.show_initial_sound()
self.decompose_sound()
self.ponder_question()
def show_initial_sound(self):
def func(x):
return self.equilibrium_height + 0.2*np.sum([
np.cos(2*np.pi*f*x)
for f in self.frequencies
])
axes = Axes(
x_min = 0, x_max = 5,
y_min = -1, y_max = 5,
x_axis_config = {
"include_tip" : False,
"unit_size" : 2.0,
},
y_axis_config = {
"include_tip" : False,
"unit_size" : 0.5,
},
)
axes.stretch_to_fit_width(2*SPACE_WIDTH - 2)
axes.stretch_to_fit_height(3)
axes.center()
axes.to_edge(LEFT)
graph = axes.get_graph(func, num_graph_points = 200)
graph.highlight(YELLOW)
v_line = Line(ORIGIN, 4*UP)
v_line.move_to(axes.coords_to_point(0, 0), DOWN)
dot = Dot(color = PINK)
dot.move_to(graph.point_from_proportion(0))
self.add(axes, graph)
self.play(
v_line.move_to, axes.coords_to_point(5, 0), DOWN,
MoveAlongPath(dot, graph),
run_time = 8,
rate_func = None,
)
self.play(*map(FadeOut, [dot, v_line]))
self.set_variables_as_attrs(axes, graph)
def decompose_sound(self):
axes, graph = self.axes, self.graph
pure_graphs = VGroup(*[
axes.get_graph(
lambda x : 0.5*np.cos(2*np.pi*freq*x),
num_graph_points = 100,
)
for freq in self.frequencies
])
pure_graphs.gradient_highlight(BLUE, RED)
pure_graphs.arrange_submobjects(DOWN, buff = MED_LARGE_BUFF)
h_line = DashedLine(6*LEFT, 6*RIGHT)
self.play(
FadeOut(axes),
graph.to_edge, UP
)
pure_graphs.next_to(graph, DOWN, LARGE_BUFF)
h_line.next_to(graph, DOWN, MED_LARGE_BUFF)
self.play(ShowCreation(h_line))
for pure_graph in reversed(pure_graphs):
self.play(ReplacementTransform(graph.copy(), pure_graph))
2018-01-15 19:15:05 -08:00
self.wait()
self.all_graphs = VGroup(graph, h_line, pure_graphs)
self.pure_graphs = pure_graphs
def ponder_question(self):
all_graphs = self.all_graphs
pure_graphs = self.pure_graphs
randy = Randolph()
randy.to_corner(DOWN+LEFT)
2018-01-11 18:13:50 -08:00
self.play(
FadeIn(randy),
all_graphs.scale, 0.75,
all_graphs.to_corner, UP+RIGHT,
)
self.play(randy.change, "pondering", all_graphs)
self.play(Blink(randy))
rect = SurroundingRectangle(pure_graphs, color = WHITE)
self.play(
ShowCreation(rect),
LaggedStart(
ApplyFunction, pure_graphs,
lambda g : (lambda m : m.shift(SMALL_BUFF*UP).highlight(YELLOW), g),
rate_func = wiggle
)
)
self.play(FadeOut(rect))
self.play(Blink(randy))
2018-01-15 19:15:05 -08:00
self.wait()
2018-01-11 18:13:50 -08:00
class Quadrant(VMobject):
CONFIG = {
"radius" : 2,
"stroke_width": 0,
"fill_opacity" : 1,
"density" : 50,
"density_exp" : 2.0,
}
def generate_points(self):
points = [r*RIGHT for r in np.arange(0, self.radius, 1./self.density)]
points += [
self.radius*(np.cos(theta)*RIGHT + np.sin(theta)*UP)
for theta in np.arange(0, TAU/4, 1./(self.radius*self.density))
]
points += [r*UP for r in np.arange(self.radius, 0, -1./self.density)]
self.set_points_smoothly(points)
class UnmixMixedPaint(Scene):
CONFIG = {
"colors" : [BLUE, RED, YELLOW, GREEN],
}
def construct(self):
angles = np.arange(4)*np.pi/2
quadrants = VGroup(*[
Quadrant().rotate(angle).highlight(color)
for color, angle in zip(self.colors, angles)
])
quadrants.add(*it.chain(*[
quadrants.copy().rotate(angle)
for angle in np.linspace(0, 0.02*TAU, 10)
]))
quadrants.set_fill(opacity = 0.5)
mud_color = average_color(*self.colors)
mud_circle = Circle(radius = 2, stroke_width = 0)
mud_circle.set_fill(mud_color, 1)
mud_circle.save_state()
mud_circle.scale(0)
def update_quadrant(quadrant, alpha):
points = quadrant.get_anchors()
dt = 0.03 #Hmm, this has no dependency on frame rate...
norms = np.apply_along_axis(np.linalg.norm, 1, points)
points[:,0] -= dt*points[:,1]/np.clip(norms, 0.1, np.inf)
points[:,1] += dt*points[:,0]/np.clip(norms, 0.1, np.inf)
2018-01-15 18:49:58 -08:00
new_norms = np.apply_along_axis(np.linalg.norm, 1, points)
new_norms = np.clip(new_norms, 0.001, np.inf)
radius = np.max(norms)
multiplier = norms/new_norms
multiplier = multiplier.reshape((len(multiplier), 1))
multiplier.repeat(points.shape[1], axis = 1)
points *= multiplier
quadrant.set_points_smoothly(points)
2018-01-15 18:49:58 -08:00
self.add(quadrants)
run_time = 30
self.play(
*[
UpdateFromAlphaFunc(quadrant, update_quadrant)
for quadrant in quadrants
] + [
ApplyMethod(mud_circle.restore, rate_func = None)
],
run_time = run_time
)
2018-01-11 18:13:50 -08:00
2018-01-15 18:49:58 -08:00
#Incomplete, and probably not useful
class MachineThatTreatsOneFrequencyDifferently(Scene):
def construct(self):
graph = self.get_cosine_graph(0.5)
frequency_mob = DecimalNumber(220, num_decimal_points = 0)
frequency_mob.next_to(graph, UP, buff = MED_LARGE_BUFF)
self.graph = graph
self.frequency_mob = frequency_mob
self.add(graph, frequency_mob)
2018-01-11 18:13:50 -08:00
2018-01-15 18:49:58 -08:00
arrow1, q_marks, arrow2 = group = VGroup(
Vector(DOWN), TextMobject("???").scale(1.5), Vector(DOWN)
)
group.highlight(WHITE)
group.arrange_submobjects(DOWN)
group.next_to(graph, DOWN)
self.add(group)
2018-01-15 18:49:58 -08:00
self.change_graph_frequency(1)
graph.highlight(GREEN)
2018-01-15 19:15:05 -08:00
self.wait()
2018-01-15 18:49:58 -08:00
graph.highlight(YELLOW)
self.change_graph_frequency(2)
2018-01-15 19:15:05 -08:00
self.wait()
2018-01-15 18:49:58 -08:00
def change_graph_frequency(self, frequency, run_time = 2):
graph = self.graph
frequency_mob = self.frequency_mob
curr_frequency = graph.frequency
self.play(
UpdateFromAlphaFunc(
graph, self.get_signal_update_func(graph, frequency),
),
ChangingDecimal(
frequency_mob,
lambda a : 440*interpolate(curr_frequency, frequency, a)
),
run_time = run_time,
)
graph.frequency = frequency
def get_signal_update_func(self, graph, target_frequency):
curr_frequency = graph.frequency
def update(graph, alpha):
frequency = interpolate(curr_frequency, target_frequency, alpha)
new_graph = self.get_cosine_graph(frequency)
Transform(graph, new_graph).update(1)
return graph
return update
def get_cosine_graph(self, frequency, num_steps = 200, color = YELLOW):
result = FunctionGraph(
lambda x : 0.5*np.cos(2*np.pi*frequency*x),
num_steps = num_steps
)
result.frequency = frequency
result.shift(2*UP)
return result
2018-01-15 18:49:58 -08:00
class FourierMachineScene(Scene):
CONFIG = {
"time_axes_config" : {
"x_min" : 0,
"x_max" : 4.4,
"x_axis_config" : {
"unit_size" : 3,
"tick_frequency" : 0.25,
"numbers_with_elongated_ticks" : [1, 2, 3],
},
"y_min" : 0,
"y_max" : 2,
"y_axis_config" : {"unit_size" : 0.8},
},
"circle_plane_config" : {
"x_radius" : 2,
"y_radius" : 2,
},
"frequency_axes_config" : {
"x_min" : 0,
"x_max" : 5.0,
"x_axis_config" : {
"unit_size" : 1.5,
},
"y_min" : -0.59,
"y_max" : 0.59,
"y_axis_config" : {
"unit_size" : 3,
"tick_frequency" : 0.1,
"line_to_number_vect" : LEFT,
},
},
"frequency_axes_box_color" : BLUE,
"text_scale_val" : 0.75,
"default_graph_config" : {
"num_graph_points" : 100,
"color" : YELLOW,
},
"equilibrium_height" : 1,
"default_y_vector_animation_config" : {
"run_time" : 5,
"rate_func" : None,
"remover" : True,
},
"default_time_sweep_config" : {
"rate_func" : None,
"run_time" : 5,
},
2018-01-15 18:49:58 -08:00
}
def get_time_axes(self):
time_axes = Axes(**self.time_axes_config)
time_axes.x_axis.add_numbers()
time_label = TextMobject("Time")
intensity_label = TextMobject("Intensity")
labels = VGroup(time_label, intensity_label)
for label in labels:
label.scale(self.text_scale_val)
time_label.next_to(time_axes.coords_to_point(3.5,0), DOWN)
intensity_label.next_to(
time_axes.y_axis.get_top(), RIGHT,
aligned_edge = UP,
)
time_axes.labels = labels
time_axes.add(labels)
time_axes.to_corner(UP+LEFT)
self.time_axes = time_axes
return time_axes
def get_circle_plane(self):
circle_plane = NumberPlane(**self.circle_plane_config)
circle_plane.to_corner(DOWN+LEFT)
circle = DashedLine(ORIGIN, TAU*UP).apply_complex_function(np.exp)
circle.move_to(circle_plane.coords_to_point(0, 0))
circle_plane.circle = circle
circle_plane.add(circle)
circle_plane.fade()
self.circle_plane = circle_plane
return circle_plane
def get_frequency_axes(self):
frequency_axes = Axes(**self.frequency_axes_config)
frequency_axes.x_axis.add_numbers(*range(1, 6))
frequency_axes.y_axis.add_numbers(
*self.frequency_y_axis_numbers
)
box = SurroundingRectangle(
frequency_axes,
buff = MED_SMALL_BUFF,
color = self.frequency_axes_box_color,
)
frequency_axes.box = box
frequency_axes.add(box)
frequency_axes.to_corner(DOWN+RIGHT, buff = MED_SMALL_BUFF)
return frequency_axes
def get_time_graph(self, func, **kwargs):
if not hasattr(self, "time_axes"):
self.get_time_axes()
config = dict(self.default_graph_config)
config.update(kwargs)
graph = self.time_axes.get_graph(func, **config)
return graph
def get_cosine_wave(self, freq = 1):
return self.get_time_graph(lambda t : 1 + 0.5*np.cos(TAU*freq*t))
def get_polarized_mobject(self, mobject, freq = 1.0):
if not hasattr(self, "circle_plane"):
self.get_circle_plane()
polarized_mobject = mobject.copy()
polarized_mobject.apply_function(lambda p : self.polarize_point(p, freq))
# polarized_mobject.make_smooth()
mobject.polarized_mobject = polarized_mobject
polarized_mobject.frequency = freq
return polarized_mobject
def polarize_point(self, point, freq = 1.0):
t, y = self.time_axes.point_to_coords(point)
z = y*np.exp(complex(0, -2*np.pi*freq*t))
return self.circle_plane.coords_to_point(z.real, z.imag)
def get_polarized_animation(self, mobject, freq = 1.0):
p_mob = self.get_polarized_mobject(mobject, freq = freq)
def update_p_mob(p_mob):
Transform(
p_mob,
self.get_polarized_mobject(mobject, freq = freq)
).update(1)
mobject.polarized_mobject = p_mob
return p_mob
return UpdateFromFunc(p_mob, update_p_mob)
def animate_frequency_change(self, mobjects, new_freq, **kwargs):
kwargs["run_time"] = kwargs.get("run_time", 3.0)
added_anims = kwargs.get("added_anims", [])
self.play(*[
self.get_frequency_change_animation(mob, new_freq, **kwargs)
for mob in mobjects
] + added_anims)
def get_frequency_change_animation(self, mobject, new_freq, **kwargs):
if not hasattr(mobject, "polarized_mobject"):
mobject.polarized_mobject = self.get_polarized_mobject(mobject)
start_freq = mobject.polarized_mobject.frequency
def update(pm, alpha):
freq = interpolate(start_freq, new_freq, alpha)
new_pm = self.get_polarized_mobject(mobject, freq)
Transform(pm, new_pm).update(1)
mobject.polarized_mobject = pm
mobject.polarized_mobject.frequency = freq
return pm
return UpdateFromAlphaFunc(mobject.polarized_mobject, update, **kwargs)
def get_time_graph_y_vector_animation(self, graph, **kwargs):
config = dict(self.default_y_vector_animation_config)
config.update(kwargs)
vector = Vector(UP, color = WHITE)
graph_copy = graph.copy()
x_axis = self.time_axes.x_axis
x_min, x_max = x_axis.x_min, x_axis.x_max
def update_vector(vector, alpha):
x = interpolate(x_min, x_max, alpha)
vector.put_start_and_end_on(
self.time_axes.coords_to_point(x, 0),
self.time_axes.input_to_graph_point(x, graph_copy)
)
return vector
return UpdateFromAlphaFunc(vector, update_vector, **config)
def get_polarized_vector_animation(self, polarized_graph, **kwargs):
config = dict(self.default_y_vector_animation_config)
config.update(kwargs)
vector = Vector(RIGHT, color = WHITE)
origin = self.circle_plane.coords_to_point(0, 0)
graph_copy = polarized_graph.copy()
def update_vector(vector, alpha):
point = graph_copy.point_from_proportion(alpha)
vector.put_start_and_end_on_with_projection(origin, point)
return vector
return UpdateFromAlphaFunc(vector, update_vector, **config)
def get_vector_animations(self, graph, draw_polarized_graph = True, **kwargs):
config = dict(self.default_y_vector_animation_config)
config.update(kwargs)
anims = [
self.get_time_graph_y_vector_animation(graph, **config),
self.get_polarized_vector_animation(graph.polarized_mobject, **config),
]
if draw_polarized_graph:
new_config = dict(config)
new_config["remover"] = False
anims.append(ShowCreation(graph.polarized_mobject, **new_config))
return anims
def animate_time_sweep(self, freq, n_repeats = 1, t_max = None, **kwargs):
added_anims = kwargs.pop("added_anims", [])
config = dict(self.default_time_sweep_config)
config.update(kwargs)
circle_plane = self.circle_plane
time_axes = self.time_axes
ctp = time_axes.coords_to_point
t_max = t_max or time_axes.x_max
v_line = DashedLine(
ctp(0, 0), ctp(0, time_axes.y_max),
)
v_line.highlight(RED)
for x in range(n_repeats):
v_line.move_to(ctp(0, 0), DOWN)
self.play(
ApplyMethod(
v_line.move_to,
ctp(t_max, 0), DOWN
),
self.get_polarized_animation(v_line, freq = freq),
*added_anims,
**config
)
self.remove(v_line.polarized_mobject)
self.play(FadeOut(VGroup(v_line, v_line.polarized_mobject)))
def get_v_lines_indicating_periods(self, freq, n_lines = 10):
period = 1./freq
v_lines = VGroup(*[
DashedLine(ORIGIN, 1.5*UP).move_to(
self.time_axes.coords_to_point(n*period, 0),
DOWN
)
for n in range(1, n_lines + 1)
])
v_lines.set_stroke(LIGHT_GREY)
return v_lines
class WrapCosineGraphAroundCircle(FourierMachineScene):
CONFIG = {
"initial_winding_frequency" : 0.5,
"signal_frequency" : 3.0,
}
2018-01-15 18:49:58 -08:00
def construct(self):
self.show_initial_signal()
self.wrap_around_circle()
self.show_time_sweeps()
self.compare_two_frequencies()
self.change_wrapping_frequency()
def show_initial_signal(self):
axes = self.get_time_axes()
graph = self.get_cosine_wave(freq = self.signal_frequency)
self.graph = graph
braces = VGroup(*self.get_peak_braces()[3:6])
v_lines = VGroup(*[
DashedLine(
ORIGIN, 2*UP, color = RED
).move_to(axes.coords_to_point(x, 0), DOWN)
for x in 1, 2
])
words = self.get_bps_label()
self.add(axes)
self.play(ShowCreation(graph, run_time = 2, rate_func = None))
self.play(
FadeIn(words),
LaggedStart(FadeIn, braces),
*map(ShowCreation, v_lines)
)
self.wait()
self.play(FadeOut(VGroup(braces, v_lines)))
self.wait()
self.beats_per_second_label = words
self.graph = graph
def wrap_around_circle(self):
graph = self.graph
freq = self.initial_winding_frequency
low_freq = freq/3
polarized_graph = self.get_polarized_mobject(graph, low_freq)
circle_plane = self.get_circle_plane()
moving_graph = graph.copy()
self.play(ShowCreation(circle_plane, submobject_mode = "all_at_once"))
self.play(ReplacementTransform(
moving_graph,
polarized_graph,
run_time = 3,
path_arc = -TAU/2
))
self.animate_frequency_change([graph], freq)
self.wait()
pg_copy = polarized_graph.copy()
self.remove(polarized_graph)
self.play(pg_copy.fade, 0.75)
self.play(*self.get_vector_animations(graph), run_time = 15)
self.remove(pg_copy)
self.wait()
def show_time_sweeps(self):
freq = self.initial_winding_frequency
graph = self.graph
v_lines = self.get_v_lines_indicating_periods(freq)
winding_freq_label = self.get_winding_frequency_label()
self.animate_time_sweep(
freq = freq,
t_max = 4,
run_time = 6,
added_anims = [FadeIn(v_lines)]
)
self.play(
FadeIn(winding_freq_label),
*self.get_vector_animations(graph)
)
self.wait()
self.v_lines_indicating_periods = v_lines
def compare_two_frequencies(self):
bps_label = self.beats_per_second_label
wps_label = self.winding_freq_label
for label in bps_label, wps_label:
label.rect = SurroundingRectangle(
label, color = RED
)
graph = self.graph
freq = self.initial_winding_frequency
braces = self.get_peak_braces(buff = 0)
self.play(ShowCreation(bps_label.rect))
self.play(FadeOut(bps_label.rect))
self.play(LaggedStart(FadeIn, braces, run_time = 3))
self.play(FadeOut(braces))
self.play(ShowCreation(wps_label.rect))
self.play(FadeOut(wps_label.rect))
self.animate_time_sweep(freq = freq, t_max = 4)
self.wait()
def change_wrapping_frequency(self):
graph = self.graph
v_lines = self.v_lines_indicating_periods
freq_label = self.winding_freq_label[0]
count = 0
for target_freq in [1.23, 0.2, 1.55, self.signal_frequency]:
self.play(
Transform(
v_lines,
self.get_v_lines_indicating_periods(target_freq)
),
ChangeDecimalToValue(freq_label, target_freq),
self.get_frequency_change_animation(graph, target_freq),
run_time = 4,
)
self.wait()
if count == 2:
self.play(LaggedStart(
ApplyFunction, v_lines,
lambda mob : (
lambda m : m.shift(0.25*UP).highlight(YELLOW),
mob
),
rate_func = there_and_back
))
self.animate_time_sweep(target_freq, t_max = 2)
count += 1
self.wait()
self.play(
*self.get_vector_animations(graph, False),
run_time = 15
)
##
def get_winding_frequency_label(self):
freq = self.initial_winding_frequency
winding_freq_label = VGroup(
DecimalNumber(freq, num_decimal_points = 2),
TextMobject("cycles/second")
)
winding_freq_label.arrange_submobjects(RIGHT)
winding_freq_label.next_to(
self.circle_plane, RIGHT, aligned_edge = UP
)
self.winding_freq_label = winding_freq_label
return winding_freq_label
def get_peak_braces(self, **kwargs):
peak_points = [
self.time_axes.input_to_graph_point(x, self.graph)
for x in np.arange(0, 3.5, 1./self.signal_frequency)
]
return VGroup(*[
Brace(Line(p1, p2), UP, **kwargs)
for p1, p2 in zip(peak_points, peak_points[1:])
])
def get_bps_label(self):
braces = VGroup(*self.get_peak_braces()[3:6])
words = TextMobject("3 beats/second")
words.scale_to_fit_width(0.9*braces.get_width())
words.next_to(braces, UP, SMALL_BUFF)
return words
class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene):
CONFIG = {
"initial_winding_frequency" : 3.0,
"center_of_mass_color" : RED,
}
def construct(self):
self.force_skipping()
self.remove(self.pi_creature)
self.setup_graph()
# self.indicate_weight_of_wire()
self.show_center_of_mass_dot()
# self.change_to_various_frequencies()
self.introduce_frequency_plot()
self.draw_full_frequency_plot()
self.label_as_almost_fourier()
def setup_graph(self):
self.add(self.get_time_axes())
self.add(self.get_circle_plane())
self.graph = self.get_cosine_wave(self.signal_frequency)
self.add(self.graph)
self.add(self.get_polarized_mobject(
self.graph, self.initial_winding_frequency
))
self.add(self.get_winding_frequency_label())
self.beats_per_second_label = self.get_bps_label()
self.add(self.beats_per_second_label)
self.v_lines_indicating_periods = self.get_v_lines_indicating_periods(
self.initial_winding_frequency
)
self.add(self.v_lines_indicating_periods)
self.change_frequency(1.03)
self.wait()
def indicate_weight_of_wire(self):
graph = self.graph
pol_graph = graph.polarized_mobject.copy()
pol_graph.save_state()
morty = self.pi_creature
morty.change("raise_right_hand")
morty.save_state()
morty.change("plain")
morty.fade(1)
self.play(
morty.restore,
pol_graph.scale, 0.5,
pol_graph.next_to, morty.get_corner(UP+LEFT), UP, -SMALL_BUFF,
)
self.play(
morty.change, "lower_right_hand", pol_graph.get_bottom(),
pol_graph.shift, 0.45*DOWN,
rate_func = there_and_back,
run_time = 2,
)
self.wait()
metal_wire = pol_graph.copy().set_stroke(LIGHT_GREY)
self.play(
ShowCreationThenDestruction(metal_wire),
run_time = 2,
)
self.play(
pol_graph.restore,
morty.change, "pondering"
)
self.remove(pol_graph)
def show_center_of_mass_dot(self):
color = self.center_of_mass_color
dot = Dot(self.get_pol_graph_center_of_mass())
dot.highlight(color)
dot.save_state()
arrow = Vector(DOWN+2*LEFT, color = color)
arrow.next_to(dot.get_center(), UP+RIGHT, buff = SMALL_BUFF)
dot.move_to(arrow.get_start())
words = TextMobject("Center of mass")
words.next_to(arrow.get_start(), RIGHT)
words.highlight(color)
self.play(
GrowArrow(arrow),
dot.restore,
)
self.play(Write(words))
self.play(FadeOut(arrow), FadeOut(self.pi_creature))
self.wait()
self.center_of_mass_dot = dot
self.center_of_mass_dot_anim = UpdateFromFunc(
dot, lambda d : d.move_to(self.get_pol_graph_center_of_mass())
)
self.center_of_mass_label = words
def change_to_various_frequencies(self):
for new_freq in [0.5, 0.2, 1.04, 2.21, 3.0]:
self.change_frequency(new_freq)
self.wait()
self.play(
*self.get_vector_animations(self.graph),
run_time = 15
)
def introduce_frequency_plot(self):
wps_label = self.winding_freq_label
wps_label.add_to_back(BackgroundRectangle(wps_label))
com_label = self.center_of_mass_label
com_label.add_background_rectangle()
frequency_axes = self.get_frequency_axes()
self.revert_to_original_skipping_status()
self.play(
wps_label.move_to, self.circle_plane, UP,
com_label.move_to, self.circle_plane, DOWN,
)
self.play(FadeIn(frequency_axes))
self.wait()
def draw_full_frequency_plot(self):
pass
def label_as_almost_fourier(self):
pass
##
def get_pol_graph_center_of_mass(self):
pg = self.graph.polarized_mobject
result = center_of_mass([
pg.point_from_proportion(alpha)
for alpha in np.linspace(0, 1, 1000)
])
result -= self.circle_plane.get_center()
result *= 2
result += self.circle_plane.get_center()
return result
def change_frequency(self, new_freq, **kwargs):
kwargs["run_time"] = kwargs.get("run_time", 3)
freq_label = self.winding_freq_label[0]
anims = [
Transform(
self.v_lines_indicating_periods,
self.get_v_lines_indicating_periods(new_freq)
),
ChangeDecimalToValue(freq_label, new_freq),
self.get_frequency_change_animation(
self.graph, new_freq
),
]
#TODO, conditionals for center of mass
if hasattr(self, "center_of_mass_dot"):
anims.append(self.center_of_mass_dot_anim)
self.play(*anims, **kwargs)
def create_pi_creature(self):
return Mortimer().to_corner(DOWN+RIGHT)