mirror of
https://github.com/3b1b/manim.git
synced 2025-08-02 02:35:22 +08:00
519 lines
14 KiB
Python
519 lines
14 KiB
Python
from big_ol_pile_of_manim_imports import *
|
|
# import scipy
|
|
|
|
|
|
class FourierCirclesScene(Scene):
|
|
CONFIG = {
|
|
"n_circles": 10,
|
|
"big_radius": 2,
|
|
"colors": [
|
|
BLUE_D,
|
|
BLUE_C,
|
|
BLUE_E,
|
|
GREY_BROWN,
|
|
],
|
|
"circle_style": {
|
|
"stroke_width": 2,
|
|
},
|
|
"base_frequency": 1,
|
|
"slow_factor": 0.25,
|
|
"center_point": ORIGIN,
|
|
"parametric_function_step_size": 0.001,
|
|
}
|
|
|
|
#
|
|
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, 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, 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(
|
|
coefficient=coefficient,
|
|
freq=freq,
|
|
color=next(color_iterator),
|
|
center_func=center_func,
|
|
)
|
|
circles.add(circle)
|
|
last_circle = circle
|
|
return circles
|
|
|
|
def get_circle(self, coefficient, freq, color, center_func):
|
|
radius = abs(coefficient)
|
|
phase = np.log(coefficient).imag
|
|
circle = Circle(
|
|
radius=radius,
|
|
color=color,
|
|
**self.circle_style,
|
|
)
|
|
circle.radial_line = Line(
|
|
circle.get_center(),
|
|
circle.get_start(),
|
|
color=WHITE,
|
|
**self.circle_style,
|
|
)
|
|
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(
|
|
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]
|
|
center = circles[0].get_center()
|
|
|
|
path = ParametricFunction(
|
|
lambda t: center + reduce(op.add, [
|
|
complex_to_R3(
|
|
coef * np.exp(TAU * 1j * freq * t)
|
|
)
|
|
for coef, freq in zip(coefs, freqs)
|
|
]),
|
|
t_min=0,
|
|
t_max=1,
|
|
color=color,
|
|
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)
|
|
broken_path = CurvesAsSubmobjects(path)
|
|
broken_path.curr_time = 0
|
|
|
|
def update_path(path, dt):
|
|
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
|
|
if b < 0:
|
|
width = 0
|
|
else:
|
|
width = 2 * (1 - (b % 1))
|
|
sp.set_stroke(YELLOW, width=width)
|
|
path.curr_time += dt
|
|
return path
|
|
|
|
broken_path.add_updater(update_path)
|
|
return broken_path
|
|
|
|
def get_y_component_wave(self,
|
|
circles,
|
|
left_x=1,
|
|
color=PINK,
|
|
n_copies=2,
|
|
right_shift_rate=5):
|
|
path = self.get_circle_end_path(circles)
|
|
wave = ParametricFunction(
|
|
lambda t: op.add(
|
|
right_shift_rate * t * LEFT,
|
|
path.function(t)[1] * UP
|
|
),
|
|
t_min=path.t_min,
|
|
t_max=path.t_max,
|
|
color=color,
|
|
)
|
|
wave_copies = VGroup(*[
|
|
wave.copy()
|
|
for x in range(n_copies)
|
|
])
|
|
wave_copies.arrange(RIGHT, buff=0)
|
|
top_point = wave_copies.get_top()
|
|
wave.creation = ShowCreation(
|
|
wave,
|
|
run_time=(1 / self.slow_factor),
|
|
rate_func=linear,
|
|
)
|
|
cycle_animation(wave.creation)
|
|
wave.add_updater(lambda m: m.shift(
|
|
(m.get_left()[0] - left_x) * LEFT
|
|
))
|
|
|
|
def update_wave_copies(wcs):
|
|
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)
|
|
wcs.align_to(top_point, UP)
|
|
wave_copies.add_updater(update_wave_copies)
|
|
|
|
return VGroup(wave, wave_copies)
|
|
|
|
def get_wave_y_line(self, circles, wave):
|
|
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]
|
|
|
|
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": 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)
|
|
h_line = always_redraw(
|
|
lambda: self.get_wave_y_line(circles, wave)
|
|
)
|
|
|
|
# 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)
|