mirror of
https://github.com/3b1b/manim.git
synced 2025-09-19 04:41:56 +00:00
A few more Fourier series scenes
This commit is contained in:
parent
200c479759
commit
247c440d05
5 changed files with 417 additions and 47 deletions
|
@ -1,7 +1,19 @@
|
|||
from active_projects.ode.part2.staging import *
|
||||
from active_projects.ode.part2.fourier_series import *
|
||||
from active_projects.ode.part2.heat_equation import *
|
||||
|
||||
OUTPUT_DIRECTORY = "ode/part2"
|
||||
ALL_SCENE_CLASSES = [
|
||||
CirclesDrawingWave,
|
||||
# Tests
|
||||
FourierOfPiSymbol,
|
||||
FourierOfPiSymbol5,
|
||||
FourierOfTrebleClef,
|
||||
# CirclesDrawingWave,
|
||||
# Scenes for video
|
||||
ExplainCircleAnimations,
|
||||
FourierSeriesIntro,
|
||||
FourierSeriesIntroBackground4,
|
||||
FourierSeriesIntroBackground8,
|
||||
FourierSeriesIntroBackground12,
|
||||
FourierSeriesIntroBackground20,
|
||||
]
|
||||
|
|
|
@ -2012,19 +2012,25 @@ class ManyStepsFromDifferentStartingPoints(TakeManyTinySteps):
|
|||
class Thumbnail(IntroduceVectorField):
|
||||
CONFIG = {
|
||||
"vector_field_config": {
|
||||
"delta_x": 1,
|
||||
"delta_y": 1,
|
||||
"delta_x": 0.5,
|
||||
"delta_y": 0.5,
|
||||
"max_magnitude": 5,
|
||||
"length_func": lambda norm: 0.75 * sigmoid(norm),
|
||||
"length_func": lambda norm: 0.5 * sigmoid(norm),
|
||||
}
|
||||
}
|
||||
|
||||
def construct(self):
|
||||
self.initialize_plane()
|
||||
self.plane.axes.set_stroke(width=0.5)
|
||||
self.initialize_vector_field()
|
||||
|
||||
field = self.vector_field
|
||||
field.set_stroke(width=5)
|
||||
for vector in field:
|
||||
vector.set_stroke(width=3)
|
||||
vector.tip.set_stroke(width=0)
|
||||
vector.tip.scale(1.5, about_point=vector.get_last_point())
|
||||
vector.set_opacity(1)
|
||||
|
||||
title = TextMobject("Differential\\\\", "equations")
|
||||
title.space_out_submobjects(0.8)
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
from big_ol_pile_of_manim_imports import *
|
||||
# import scipy
|
||||
|
||||
|
||||
class CirclesDrawing(Scene):
|
||||
class FourierCirclesScene(Scene):
|
||||
CONFIG = {
|
||||
"n_circles": 4,
|
||||
"n_circles": 10,
|
||||
"big_radius": 2,
|
||||
"colors": [
|
||||
BLUE_D,
|
||||
|
@ -14,38 +15,54 @@ class CirclesDrawing(Scene):
|
|||
"circle_style": {
|
||||
"stroke_width": 2,
|
||||
},
|
||||
"base_frequency": 0.25 * TAU,
|
||||
"base_frequency": 1,
|
||||
"slow_factor": 0.25,
|
||||
"center_point": ORIGIN,
|
||||
"parametric_function_step_size": 0.001,
|
||||
}
|
||||
|
||||
#
|
||||
def get_freqs_and_radii(self):
|
||||
raise Exception("Not implemented")
|
||||
def get_freqs(self):
|
||||
n = self.n_circles
|
||||
all_freqs = list(range(n // 2, -n // 2, -1))
|
||||
all_freqs.sort(key=abs)
|
||||
return all_freqs
|
||||
|
||||
def get_coefficients(self):
|
||||
return [complex(0) for x in range(self.n_circles)]
|
||||
|
||||
def get_color_iterator(self):
|
||||
return it.cycle(self.colors)
|
||||
|
||||
def get_circles(self):
|
||||
def get_circles(self, freqs=None, coefficients=None):
|
||||
circles = VGroup()
|
||||
color_iterator = self.get_color_iterator()
|
||||
self.center_tracker = VectorizedPoint(self.center_point)
|
||||
|
||||
if freqs is None:
|
||||
freqs = self.get_freqs()
|
||||
if coefficients is None:
|
||||
coefficients = self.get_coefficients()
|
||||
|
||||
last_circle = None
|
||||
for freq, radius in self.get_freqs_and_radii():
|
||||
for freq, coefficient in zip(freqs, coefficients):
|
||||
if last_circle:
|
||||
center_func = last_circle.get_start
|
||||
else:
|
||||
center_func = self.center_tracker.get_location
|
||||
circle = self.get_circle(
|
||||
radius=radius,
|
||||
color=next(color_iterator),
|
||||
coefficient=coefficient,
|
||||
freq=freq,
|
||||
center_func=center_func
|
||||
color=next(color_iterator),
|
||||
center_func=center_func,
|
||||
)
|
||||
circles.add(circle)
|
||||
last_circle = circle
|
||||
return circles
|
||||
|
||||
def get_circle(self, radius, color, freq, center_func):
|
||||
def get_circle(self, coefficient, freq, color, center_func):
|
||||
radius = abs(coefficient)
|
||||
phase = np.log(coefficient).imag
|
||||
circle = Circle(
|
||||
radius=radius,
|
||||
color=color,
|
||||
|
@ -59,41 +76,60 @@ class CirclesDrawing(Scene):
|
|||
)
|
||||
circle.add(circle.radial_line)
|
||||
circle.freq = freq
|
||||
circle.phase = phase
|
||||
circle.rotate(phase)
|
||||
circle.coefficient = coefficient
|
||||
circle.center_func = center_func
|
||||
circle.add_updater(self.update_circle)
|
||||
return circle
|
||||
|
||||
def update_circle(self, circle, dt):
|
||||
circle.rotate(circle.freq * dt)
|
||||
circle.rotate(
|
||||
self.slow_factor * circle.freq * dt * TAU
|
||||
)
|
||||
circle.move_to(circle.center_func())
|
||||
return circle
|
||||
|
||||
def get_rotating_vectors(self, circles):
|
||||
return VGroup(*[
|
||||
self.get_rotating_vector(circle)
|
||||
for circle in circles
|
||||
])
|
||||
|
||||
def get_rotating_vector(self, circle):
|
||||
vector = Vector(RIGHT, color=WHITE)
|
||||
vector.add_updater(lambda v: v.put_start_and_end_on(
|
||||
*circle.radial_line.get_start_and_end()
|
||||
))
|
||||
return vector
|
||||
|
||||
def get_circle_end_path(self, circles, color=YELLOW):
|
||||
coefs = [c.coefficient for c in circles]
|
||||
freqs = [c.freq for c in circles]
|
||||
radii = [c.radius for c in circles]
|
||||
total_time = TAU / self.base_frequency
|
||||
center = circles[0].get_center()
|
||||
|
||||
return ParametricFunction(
|
||||
path = ParametricFunction(
|
||||
lambda t: center + reduce(op.add, [
|
||||
radius * rotate_vector(RIGHT, freq * t)
|
||||
for freq, radius in zip(freqs, radii)
|
||||
complex_to_R3(
|
||||
coef * np.exp(TAU * 1j * freq * t)
|
||||
)
|
||||
for coef, freq in zip(coefs, freqs)
|
||||
]),
|
||||
t_min=0,
|
||||
t_max=total_time,
|
||||
t_max=1,
|
||||
color=color,
|
||||
step_size=0.005,
|
||||
step_size=self.parametric_function_step_size,
|
||||
)
|
||||
return path
|
||||
|
||||
# TODO, this should be a general animated mobect
|
||||
def get_drawn_path(self, circles, **kwargs):
|
||||
path = self.get_circle_end_path(circles, **kwargs)
|
||||
total_time = path.t_max
|
||||
broken_path = CurvesAsSubmobjects(path)
|
||||
broken_path.curr_time = 0
|
||||
|
||||
def update_path(path, dt):
|
||||
alpha = (path.curr_time / total_time)
|
||||
alpha = path.curr_time * self.slow_factor
|
||||
n_curves = len(path)
|
||||
for a, sp in zip(np.linspace(0, 1, n_curves), path):
|
||||
b = alpha - a
|
||||
|
@ -111,9 +147,9 @@ class CirclesDrawing(Scene):
|
|||
def get_y_component_wave(self,
|
||||
circles,
|
||||
left_x=1,
|
||||
color=BLUE,
|
||||
color=PINK,
|
||||
n_copies=2,
|
||||
right_shift_rate=1.5):
|
||||
right_shift_rate=5):
|
||||
path = self.get_circle_end_path(circles)
|
||||
wave = ParametricFunction(
|
||||
lambda t: op.add(
|
||||
|
@ -132,7 +168,7 @@ class CirclesDrawing(Scene):
|
|||
top_point = wave_copies.get_top()
|
||||
wave.creation = ShowCreation(
|
||||
wave,
|
||||
run_time=wave.t_max,
|
||||
run_time=(1 / self.slow_factor),
|
||||
rate_func=linear,
|
||||
)
|
||||
cycle_animation(wave.creation)
|
||||
|
@ -141,7 +177,9 @@ class CirclesDrawing(Scene):
|
|||
))
|
||||
|
||||
def update_wave_copies(wcs):
|
||||
index = int(np.ceil(wave.creation.total_time / wave.t_max)) - 1
|
||||
index = int(
|
||||
wave.creation.total_time * self.slow_factor
|
||||
)
|
||||
wcs[:index].match_style(wave)
|
||||
wcs[index:].set_stroke(width=0)
|
||||
wcs.next_to(wave, RIGHT, buff=0)
|
||||
|
@ -154,35 +192,327 @@ class CirclesDrawing(Scene):
|
|||
return DashedLine(
|
||||
circles[-1].get_start(),
|
||||
wave[0].get_end(),
|
||||
stroke_width=1,
|
||||
dash_length=DEFAULT_DASH_LENGTH * 0.5,
|
||||
)
|
||||
|
||||
# Computing Fourier series
|
||||
def get_coefficients_of_path(self, path, n_samples=10000, freqs=None):
|
||||
if freqs is None:
|
||||
freqs = self.get_freqs()
|
||||
dt = 1 / n_samples
|
||||
ts = np.arange(0, 1, dt)
|
||||
samples = np.array([
|
||||
path.point_from_proportion(t)
|
||||
for t in ts
|
||||
])
|
||||
samples -= self.center_point
|
||||
complex_samples = samples[:, 0] + 1j * samples[:, 1]
|
||||
|
||||
class CirclesDrawingWave(CirclesDrawing):
|
||||
result = []
|
||||
for freq in freqs:
|
||||
riemann_sum = np.array([
|
||||
np.exp(-TAU * 1j * freq * t) * cs
|
||||
for t, cs in zip(ts, complex_samples)
|
||||
]).sum() * dt
|
||||
result.append(riemann_sum)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class FourierSeriesIntroBackground4(FourierCirclesScene):
|
||||
CONFIG = {
|
||||
"n_circles": 4,
|
||||
"center_point": 3 * LEFT,
|
||||
"center_point": 4 * LEFT,
|
||||
"run_time": 30,
|
||||
"big_radius": 1.5,
|
||||
}
|
||||
|
||||
def construct(self):
|
||||
circles = self.get_circles()
|
||||
path = self.get_drawn_path(circles)
|
||||
wave = self.get_y_component_wave(circles)
|
||||
|
||||
# small_path = self.get_drawn_path(circles[:1])
|
||||
wave1 = self.get_y_component_wave(circles[:1], color=PINK)
|
||||
wave2 = self.get_y_component_wave(circles[:2], color=RED)
|
||||
# Why?
|
||||
circles.update(-1 / self.camera.frame_rate)
|
||||
#
|
||||
h_line = always_redraw(
|
||||
lambda: self.get_wave_y_line(circles, wave)
|
||||
)
|
||||
self.add(circles, path, wave, h_line)
|
||||
self.add(wave1, wave2)
|
||||
self.wait(10)
|
||||
|
||||
def get_freqs_and_radii(self):
|
||||
return [
|
||||
(k * self.base_frequency, self.big_radius / k)
|
||||
for k in range(1, 2 * self.n_circles + 1, 2)
|
||||
# Why?
|
||||
circles.update(-1 / self.camera.frame_rate)
|
||||
#
|
||||
self.add(circles, path, wave, h_line)
|
||||
self.wait(self.run_time)
|
||||
|
||||
def get_ks(self):
|
||||
return np.arange(1, 2 * self.n_circles + 1, 2)
|
||||
|
||||
def get_freqs(self):
|
||||
return self.base_frequency * self.get_ks()
|
||||
|
||||
def get_coefficients(self):
|
||||
return self.big_radius / self.get_ks()
|
||||
|
||||
|
||||
class FourierSeriesIntroBackground8(FourierSeriesIntroBackground4):
|
||||
CONFIG = {
|
||||
"n_circles": 8,
|
||||
}
|
||||
|
||||
|
||||
class FourierSeriesIntroBackground12(FourierSeriesIntroBackground4):
|
||||
CONFIG = {
|
||||
"n_circles": 12,
|
||||
}
|
||||
|
||||
|
||||
class FourierSeriesIntroBackground20(FourierSeriesIntroBackground4):
|
||||
CONFIG = {
|
||||
"n_circles": 20,
|
||||
}
|
||||
|
||||
|
||||
class FourierOfPiSymbol(FourierCirclesScene):
|
||||
CONFIG = {
|
||||
"n_circles": 50,
|
||||
"center_point": ORIGIN,
|
||||
"slow_factor": 0.1,
|
||||
"run_time": 30,
|
||||
"tex": "\\pi",
|
||||
"start_drawn": False,
|
||||
}
|
||||
|
||||
def construct(self):
|
||||
path = self.get_path()
|
||||
coefs = self.get_coefficients_of_path(path)
|
||||
|
||||
circles = self.get_circles(coefficients=coefs)
|
||||
for k, circle in zip(it.count(1), circles):
|
||||
circle.set_stroke(width=max(
|
||||
1 / np.sqrt(k),
|
||||
1,
|
||||
))
|
||||
|
||||
# approx_path = self.get_circle_end_path(circles)
|
||||
drawn_path = self.get_drawn_path(circles)
|
||||
if self.start_drawn:
|
||||
drawn_path.curr_time = 1 / self.slow_factor
|
||||
|
||||
self.add(path)
|
||||
self.add(circles)
|
||||
self.add(drawn_path)
|
||||
self.wait(self.run_time)
|
||||
|
||||
def get_path(self):
|
||||
tex_mob = TexMobject(self.tex)
|
||||
tex_mob.set_height(6)
|
||||
path = tex_mob.family_members_with_points()[0]
|
||||
path.set_fill(opacity=0)
|
||||
path.set_stroke(WHITE, 1)
|
||||
return path
|
||||
|
||||
|
||||
class FourierOfPiSymbol5(FourierOfPiSymbol):
|
||||
CONFIG = {
|
||||
"n_circles": 5,
|
||||
"run_time": 10,
|
||||
}
|
||||
|
||||
|
||||
class FourierOfTrebleClef(FourierOfPiSymbol):
|
||||
CONFIG = {
|
||||
"n_circles": 100,
|
||||
"run_time": 10,
|
||||
"start_drawn": True,
|
||||
}
|
||||
|
||||
def get_path(self):
|
||||
path = SVGMobject("TrebleClef")
|
||||
path = path.family_members_with_points()[0]
|
||||
path.set_height(7.5)
|
||||
path.set_fill(opacity=0)
|
||||
path.set_stroke(WHITE, 0)
|
||||
return path
|
||||
|
||||
|
||||
class ExplainCircleAnimations(FourierCirclesScene):
|
||||
CONFIG = {
|
||||
# "n_circles": 100,
|
||||
"n_circles": 20,
|
||||
"center_point": 2 * DOWN,
|
||||
"n_top_circles": 9,
|
||||
# "slow_factor": 0.1,
|
||||
"path_height": 3,
|
||||
}
|
||||
|
||||
def construct(self):
|
||||
self.add_path()
|
||||
self.add_circles()
|
||||
self.organize_circles_in_a_row()
|
||||
self.show_frequencies()
|
||||
self.show_examples_for_frequencies()
|
||||
self.show_as_vectors()
|
||||
self.show_vector_sum()
|
||||
self.moons_of_moons_of_moons()
|
||||
self.tweak_starting_vectors()
|
||||
|
||||
def add_path(self):
|
||||
self.path = self.get_path()
|
||||
self.add(self.path)
|
||||
|
||||
def add_circles(self):
|
||||
coefs = self.get_coefficients_of_path(self.path)
|
||||
self.circles = self.get_circles(coefficients=coefs)
|
||||
|
||||
self.add(self.circles)
|
||||
self.drawn_path = self.get_drawn_path(self.circles)
|
||||
self.add(self.drawn_path)
|
||||
|
||||
self.wait(8)
|
||||
|
||||
def organize_circles_in_a_row(self):
|
||||
circles = self.circles
|
||||
top_circles = circles[:self.n_top_circles].deepcopy()
|
||||
|
||||
center_trackers = VGroup()
|
||||
for circle in top_circles:
|
||||
tracker = VectorizedPoint(circle.center_func())
|
||||
circle.center_func = tracker.get_location
|
||||
center_trackers.add(tracker)
|
||||
tracker.freq = circle.freq
|
||||
tracker.circle = circle
|
||||
|
||||
center_trackers.submobjects.sort(
|
||||
key=lambda m: m.freq
|
||||
)
|
||||
center_trackers.generate_target()
|
||||
right_buff = 1.45
|
||||
center_trackers.target.arrange(RIGHT, buff=right_buff)
|
||||
center_trackers.target.to_edge(UP, buff=1.25)
|
||||
|
||||
self.add(top_circles)
|
||||
self.play(
|
||||
MoveToTarget(center_trackers),
|
||||
run_time=2
|
||||
)
|
||||
self.wait(4)
|
||||
|
||||
self.top_circles = top_circles
|
||||
self.center_trackers = center_trackers
|
||||
|
||||
def show_frequencies(self):
|
||||
center_trackers = self.center_trackers
|
||||
|
||||
freq_numbers = VGroup()
|
||||
for ct in center_trackers:
|
||||
number = Integer(ct.freq)
|
||||
number.next_to(ct, DOWN, buff=1)
|
||||
freq_numbers.add(number)
|
||||
ct.circle.number = number
|
||||
|
||||
ld, rd = [
|
||||
TexMobject("\\dots")
|
||||
for x in range(2)
|
||||
]
|
||||
ld.next_to(freq_numbers, LEFT, MED_LARGE_BUFF)
|
||||
rd.next_to(freq_numbers, RIGHT, MED_LARGE_BUFF)
|
||||
freq_numbers.add_to_back(ld)
|
||||
freq_numbers.add(rd)
|
||||
|
||||
freq_word = TextMobject("Frequencies")
|
||||
freq_word.scale(1.5)
|
||||
freq_word.set_color(YELLOW)
|
||||
freq_word.next_to(freq_numbers, DOWN, MED_LARGE_BUFF)
|
||||
|
||||
self.play(
|
||||
LaggedStartMap(
|
||||
FadeInFromDown, freq_numbers
|
||||
)
|
||||
)
|
||||
self.play(
|
||||
Write(freq_word),
|
||||
LaggedStartMap(
|
||||
ShowCreationThenFadeAround, freq_numbers,
|
||||
)
|
||||
)
|
||||
self.wait(2)
|
||||
|
||||
def show_examples_for_frequencies(self):
|
||||
top_circles = self.top_circles
|
||||
c1, c2, c3 = [
|
||||
list(filter(
|
||||
lambda c: c.freq == k,
|
||||
top_circles
|
||||
))[0]
|
||||
for k in (1, 2, 3)
|
||||
]
|
||||
|
||||
neg_circles = VGroup(*filter(
|
||||
lambda c: c.freq < 0,
|
||||
top_circles
|
||||
))
|
||||
|
||||
for c in [c1, c2, c3, *neg_circles]:
|
||||
c.rect = SurroundingRectangle(c)
|
||||
|
||||
self.play(
|
||||
ShowCreation(c2.rect),
|
||||
WiggleOutThenIn(c2.number),
|
||||
)
|
||||
self.wait(2)
|
||||
self.play(
|
||||
ReplacementTransform(c2.rect, c1.rect),
|
||||
)
|
||||
self.play(FadeOut(c1.rect))
|
||||
self.wait()
|
||||
self.play(
|
||||
ShowCreation(c3.rect),
|
||||
WiggleOutThenIn(c3.number),
|
||||
)
|
||||
self.play(
|
||||
FadeOut(c3.rect),
|
||||
)
|
||||
self.wait(2)
|
||||
self.play(
|
||||
LaggedStart(*[
|
||||
ShowCreationThenFadeOut(c.rect)
|
||||
for c in neg_circles
|
||||
])
|
||||
)
|
||||
self.wait(3)
|
||||
|
||||
def show_as_vectors(self):
|
||||
top_circles = self.top_circles
|
||||
top_vectors = self.get_rotating_vectors(top_circles)
|
||||
|
||||
self.play(
|
||||
top_circles.set_stroke, {"width": 0.5},
|
||||
FadeIn(top_vectors),
|
||||
)
|
||||
self.wait(3)
|
||||
|
||||
self.top_vectors = top_vectors
|
||||
|
||||
def show_vector_sum(self):
|
||||
top_circles = self.top_circles
|
||||
top_vectors = self.top_vectors
|
||||
|
||||
self.play(
|
||||
FadeOut(self.path),
|
||||
FadeOut(self.circles),
|
||||
)
|
||||
|
||||
def moons_of_moons_of_moons(self):
|
||||
pass
|
||||
|
||||
def tweak_starting_vectors(self):
|
||||
pass
|
||||
|
||||
#
|
||||
def get_path(self):
|
||||
tex = TexMobject("f")
|
||||
path = tex.family_members_with_points()[0]
|
||||
path.set_stroke(WHITE, 1)
|
||||
path.set_fill(opacity=0)
|
||||
path.set_height(self.path_height)
|
||||
path.move_to(self.center_point)
|
||||
return path
|
||||
# return Square().set_height(3)
|
||||
|
|
6
active_projects/ode/part2/heat_equation.py
Normal file
6
active_projects/ode/part2/heat_equation.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from big_ol_pile_of_manim_imports import *
|
||||
|
||||
|
||||
class NewSceneName(Scene):
|
||||
def construct(self):
|
||||
pass
|
|
@ -1,6 +1,22 @@
|
|||
from big_ol_pile_of_manim_imports import *
|
||||
|
||||
|
||||
class NewSceneName(Scene):
|
||||
class FourierSeriesIntro(Scene):
|
||||
def construct(self):
|
||||
pass
|
||||
title = TextMobject(
|
||||
"Fourier ", "Series", ":",
|
||||
" An origin story",
|
||||
arg_separator="",
|
||||
)
|
||||
title.scale(2)
|
||||
title.to_edge(UP)
|
||||
image = ImageMobject("Joseph Fourier")
|
||||
image.set_height(5)
|
||||
image.next_to(title, DOWN, MED_LARGE_BUFF)
|
||||
image.to_edge(LEFT)
|
||||
name = TextMobject("Joseph", "Fourier")
|
||||
name.next_to(image, DOWN)
|
||||
|
||||
self.add(title)
|
||||
self.add(image)
|
||||
self.add(name)
|
||||
|
|
Loading…
Add table
Reference in a new issue