Finalize Fourier monster animations

This commit is contained in:
Grant Sanderson 2022-04-22 11:52:34 -07:00
parent 2aec2f2c7b
commit 3dd96ca2f7
3 changed files with 108 additions and 80 deletions

View file

@ -157,27 +157,31 @@ class FourierCirclesScene(Scene):
def get_drawn_path_alpha(self): def get_drawn_path_alpha(self):
return self.get_vector_time() return self.get_vector_time()
def get_drawn_path(self, vectors, stroke_width=None, **kwargs): def get_drawn_path(self, vectors, stroke_width=None, fade_rate=0.2, **kwargs):
if stroke_width is None: if stroke_width is None:
stroke_width = self.drawn_path_stroke_width stroke_width = self.drawn_path_stroke_width
path = self.get_vector_sum_path(vectors, **kwargs) path = self.get_vector_sum_path(vectors, **kwargs)
broken_path = CurvesAsSubmobjects(path) path.set_stroke(self.drawn_path_color, stroke_width)
self.add_path_fader(path, fade_rate)
return path
def update_path(path, dt): def add_path_fader(self, path, fade_rate=0.2):
stroke_width = np.max(path.get_stroke_width())
stroke_opacity = np.max(path.get_stroke_opacity())
def update_path(path_, dt):
alpha = self.get_vector_time() alpha = self.get_vector_time()
n_curves = len(path) n = path_.get_num_points()
for a, sp in zip(np.linspace(0, 1, n_curves), path): fade_factors = (np.linspace(0, 1, n) - alpha) % 1
b = alpha - a fade_factors = fade_factors**fade_rate
if b < 0: path_.set_stroke(
width = 0 width=stroke_width * fade_factors,
else: opacity=stroke_opacity * fade_factors,
width = stroke_width * (1 - (b % 1)) )
sp.set_stroke(width=width) return path_
return path
broken_path.set_color(self.drawn_path_color) path.add_updater(update_path)
broken_path.add_updater(update_path) return path
return broken_path
def get_y_component_wave(self, def get_y_component_wave(self,
vectors, vectors,

View file

@ -3,16 +3,17 @@ from _2022.piano.fourier_animations import DecomposeAudioSegment
from _2019.diffyq.part2.fourier_series import FourierCirclesScene from _2019.diffyq.part2.fourier_series import FourierCirclesScene
class DecomposeRoar(DecomposeAudioSegment): class DecomposeRoar_SlowFPS(DecomposeAudioSegment):
audio_file = "/Users/grant/Dropbox/3Blue1Brown/videos/2022/infinity/Roar.wav" audio_file = "/Users/grant/Dropbox/3Blue1Brown/videos/2022/infinity/Roar.wav"
graph_point = 0.428 graph_point = 0.428
zoom_rect_dims = (0.2, 6.0) zoom_rect_dims = (0.2, 6.0)
def construct(self): def construct(self):
self.add_full_waveform() self.add_full_waveform(run_time=2.5)
self.zoom_in_on_segment( self.zoom_in_on_segment(
self.axes, self.graph, self.axes, self.graph,
self.graph_point, self.zoom_rect_dims self.graph_point, self.zoom_rect_dims,
run_time=3,
) )
self.prepare_for_3d() self.prepare_for_3d()
self.show_all_waves() self.show_all_waves()
@ -55,7 +56,8 @@ class DecomposeRoar(DecomposeAudioSegment):
x_range=(0, t_max), x_range=(0, t_max),
) )
wave.match_y(f_axes.c2p(freq, 0)) wave.match_y(f_axes.c2p(freq, 0))
wave.set_stroke(opacity=0.75) # wave.set_stroke(opacity=0.75)
wave.set_stroke(opacity=1.0)
wave.amp = amp wave.amp = amp
wave.freq = freq wave.freq = freq
wave.phase = phase wave.phase = phase
@ -85,14 +87,14 @@ class DecomposeRoar(DecomposeAudioSegment):
), ),
lag_ratio=1 / len(sine_waves), lag_ratio=1 / len(sine_waves),
), ),
run_time=8 run_time=4
) )
self.wait(3) self.wait(3)
class CombineWavesToImage(FourierCirclesScene): class CombineWavesToImage_SlowFPS(FourierCirclesScene):
CONFIG = { CONFIG = {
"peace_sign_height": 6.0, "drawing_height": 6.0,
"n_shown_waves": 5, "n_shown_waves": 5,
"n_vectors": 500, "n_vectors": 500,
# "n_vectors": 50, # "n_vectors": 50,
@ -102,6 +104,7 @@ class CombineWavesToImage(FourierCirclesScene):
# "parametric_function_step_size": 0.01, # "parametric_function_step_size": 0.01,
"drawn_path_color": YELLOW, "drawn_path_color": YELLOW,
"remove_background_waves": False, "remove_background_waves": False,
"drawing_file": "/Users/grant/Dropbox/3Blue1Brown/videos/2022/infinity/monster-holding-piece-sign-3.svg",
} }
def construct(self): def construct(self):
@ -157,10 +160,10 @@ class CombineWavesToImage(FourierCirclesScene):
)) ))
all_axes.arrange(DOWN, buff=1.0) all_axes.arrange(DOWN, buff=1.0)
dots = VGroup(Tex("\\vdots", font_size=100), Tex("")) dots = Tex("\\vdots", font_size=100)
dots.arrange(RIGHT)
dots.next_to(all_axes, DOWN, buff=0.5) dots.next_to(all_axes, DOWN, buff=0.5)
dots.match_x(all_axes[0].get_origin()) dots.match_x(all_axes[0].get_origin())
dots.set_fill(WHITE)
frame = self.camera.frame frame = self.camera.frame
group = Group(all_axes, dots) group = Group(all_axes, dots)
@ -216,10 +219,9 @@ class CombineWavesToImage(FourierCirclesScene):
true_path = self.path true_path = self.path
fade_region = self.get_fade_region(true_path) fade_region = self.get_fade_region(true_path)
sf = self.get_slow_factor()
rt = 5 rt = 5
future_vt = self.get_vector_time() + sf * rt # sf = self.get_slow_factor()
self.wait((1.0 - (future_vt % 1.0)) / sf) # future_vt = self.get_vector_time() + sf * rt
label = VGroup(Integer(100, edge_to_fix=DR), Text("terms")) label = VGroup(Integer(100, edge_to_fix=DR), Text("terms"))
label.arrange(RIGHT, buff=0.15) label.arrange(RIGHT, buff=0.15)
@ -227,7 +229,6 @@ class CombineWavesToImage(FourierCirclesScene):
label[0].set_value(self.n_shown_waves) label[0].set_value(self.n_shown_waves)
# Animate entry # Animate entry
drawn_path = self.get_drawn_path(self.vectors[:self.n_shown_waves + 1])
terms = self.get_terms(self.n_shown_waves) terms = self.get_terms(self.n_shown_waves)
self.play( self.play(
FadeIn(fade_region, lag_ratio=0.1), FadeIn(fade_region, lag_ratio=0.1),
@ -241,16 +242,21 @@ class CombineWavesToImage(FourierCirclesScene):
), lag_ratio=0.1), ), lag_ratio=0.1),
FadeIn(label), FadeIn(label),
Write(terms), Write(terms),
# self.slow_factor_tracker.animate.set_value(0.1),
run_time=rt, run_time=rt,
) )
self.vector_clock.set_value(0)
# Set up drawn paths # Set up drawn paths
self.add(drawn_path, terms) drawn_path = self.get_drawn_path(self.vectors[:self.n_shown_waves + 1])
dot = self.add_draw_dot(self.vectors[:self.n_shown_waves + 1])
self.add(drawn_path, terms, dot)
anims = [
UpdateFromAlphaFunc(drawn_path, lambda m, a: m.set_stroke(interpolate_color(BLACK, YELLOW, a))),
UpdateFromAlphaFunc(dot, lambda m, a: m.set_opacity(a)),
]
if self.remove_background_waves: if self.remove_background_waves:
self.play( self.play(
fade_region.animate.set_height(2 * FRAME_WIDTH).set_opacity(0.05), fade_region.animate.set_height(2 * FRAME_WIDTH).set_opacity(0.05),
*anims,
run_time=2 run_time=2
) )
self.remove( self.remove(
@ -260,7 +266,7 @@ class CombineWavesToImage(FourierCirclesScene):
fade_region, fade_region,
) )
else: else:
self.wait(2) self.play(*anims, run_time=2)
# Now add on new vectors one at a time # Now add on new vectors one at a time
drawn_paths = dict() drawn_paths = dict()
@ -280,8 +286,8 @@ class CombineWavesToImage(FourierCirclesScene):
drawn_paths[n] = self.get_drawn_path(self.vectors[:n]) drawn_paths[n] = self.get_drawn_path(self.vectors[:n])
if n not in all_terms: if n not in all_terms:
all_terms[n] = self.get_terms(n) all_terms[n] = self.get_terms(n)
group[0].become(drawn_paths[n]) group.replace_submobject(0, drawn_paths[n])
group[1].become(all_terms[n]) group.replace_submobject(1, all_terms[n])
group[2][0].set_value(n) group[2][0].set_value(n)
group[3].set_submobjects(self.vectors[:n]) group[3].set_submobjects(self.vectors[:n])
group[4].set_submobjects(self.circles[:n]) group[4].set_submobjects(self.circles[:n])
@ -289,11 +295,14 @@ class CombineWavesToImage(FourierCirclesScene):
self.style_vectors(group[3]) self.style_vectors(group[3])
self.style_circles(group[4]) self.style_circles(group[4])
if n > 25:
group[4].set_stroke(opacity=(1.0 / (n - 25))**0.2)
group.update() group.update()
return group return group
self.remove(self.vectors, self.circles, drawn_path, terms) self.remove(self.vectors, self.circles, drawn_path, terms, dot)
self.add_draw_dot(drawing_group[3])
self.play(UpdateFromAlphaFunc( self.play(UpdateFromAlphaFunc(
drawing_group, update_drawing_group, drawing_group, update_drawing_group,
run_time=15, run_time=15,
@ -303,24 +312,30 @@ class CombineWavesToImage(FourierCirclesScene):
inf = Tex("\\infty") inf = Tex("\\infty")
inf.match_height(label[0]) inf.match_height(label[0])
inf.move_to(label[0], RIGHT) inf.move_to(label[0], RIGHT)
inf.set_fill(WHITE)
drawn_path = drawing_group[0]
# self.add_path_fader(true_path, fade_rate=1)
true_path.set_stroke(width=1.0)
self.add(drawn_path, true_path, self.circles, self.vectors)
self.play( self.play(
FadeIn(true_path), UpdateFromAlphaFunc(drawn_path, lambda m, a: m.set_stroke(interpolate_color(YELLOW, BLACK, a))),
VFadeOut(drawn_path), VFadeIn(true_path),
ChangeDecimalToValue(label[0], 1000), ChangeDecimalToValue(label[0], 1000),
VFadeOut(label[0]), VFadeOut(label[0]),
FadeIn(inf), FadeIn(inf),
run_time=3, run_time=3,
) )
self.remove(drawn_path)
self.wait(12) self.wait(12)
## ##
def get_drawing(self): def get_drawing(self):
# drawing = self.get_peace_sign() svg = SVGMobject(self.drawing_file)
svg = SVGMobject("/Users/grant/Dropbox/3Blue1Brown/videos/2022/infinity/monster-holding-heart.svg")
drawing = svg[1] drawing = svg[1]
drawing.set_height(self.peace_sign_height) drawing.set_height(self.drawing_height)
drawing.set_stroke(self.drawn_path_color, 2) drawing.set_stroke(self.drawn_path_color, 2)
return drawing return drawing
@ -355,47 +370,55 @@ class CombineWavesToImage(FourierCirclesScene):
for k, circle in zip(it.count(0), circles): for k, circle in zip(it.count(0), circles):
circle.set_stroke(width=mcsw / (1 + k / 4), opacity=1) circle.set_stroke(width=mcsw / (1 + k / 4), opacity=1)
if len(circles) > 25:
circles.set_stroke(opacity=(1.0 / (len(circles) - 25))**0.2)
return circles return circles
def get_fade_region(self, path, n_disks=100): def get_fade_region(self, path):
disk = Circle(radius=path.get_width() / 2) dot = GlowDot()
disks = VGroup(*( dot.set_radius(1.0 * path.get_width())
disk.copy().scale(sf) dot.set_glow_factor(0.2)
for sf in np.linspace(1.0, 2.25, n_disks) dot.set_color(BLACK, 0.95)
)) return dot
disks.set_stroke(width=0)
disks.set_fill(BLACK, opacity=3.0 / n_disks)
return disks
def get_drawn_path(self, vectors): def get_drawn_path(self, vectors):
if len(vectors) == 0: nv = len(vectors)
if nv == 0:
return VGroup() return VGroup()
return super().get_drawn_path(vectors, stroke_width=4).set_stroke(self.drawn_path_color) fade_rate = 10 * np.exp(0.01 * (5 - nv)) + 1.0
path = super().get_drawn_path(vectors, stroke_width=4, fade_rate=fade_rate)
path.set_stroke(self.drawn_path_color)
return path
def get_terms(self, n_terms, max_terms=40, max_width=12): def get_terms(self, n_terms, max_terms=100, max_width=12):
tex_str = ""
ks = list(range(-int(np.floor(n_terms / 2)), int(np.ceil(n_terms / 2)))) ks = list(range(-int(np.floor(n_terms / 2)), int(np.ceil(n_terms / 2))))
ks.sort(key=abs) ks.sort(key=abs)
ks = ks[:max_terms] ks = ks[:max_terms]
ks.sort() ks.sort()
font_size = max(180 / n_terms, 5)
terms = VGroup()
for k in ks: for k in ks:
tex_str += "c_{k} e^{k \\cdot 2 \\pi i \\cdot t} +".replace("k", str(k)) terms.add(MTex("c_{k} e^{k \\cdot 2 \\pi i \\cdot t} +".replace("k", str(k))))
if n_terms <= max_terms: if n_terms <= max_terms:
tex_str = tex_str[:-1] # Remove trailing plus terms[-1][-1].set_opacity(0)
else: else:
tex_str = "\\cdots + " + tex_str + "\\cdots" terms.add_to_back(MTex("\\cdots + "))
terms.add(MTex("\\cdots"))
font_size = 180 / n_terms terms.arrange(RIGHT, SMALL_BUFF)
result = Tex(tex_str, font_size=font_size) terms.scale(font_size / 48)
result.set_max_width(max_width) terms.set_max_width(max_width)
result.next_to(self.path, UP, MED_SMALL_BUFF) terms.next_to(self.path, UP, SMALL_BUFF)
max_light_terms = 15 max_light_terms = 15
if n_terms > max_light_terms: if n_terms > max_light_terms:
result.set_fill(opacity=max_light_terms / (n_terms - max_light_terms)) terms.set_fill(opacity=max_light_terms / (n_terms - max_light_terms))
result.set_stroke(width=0) terms.set_stroke(width=0)
return result return terms
def add_draw_dot(self, vectors):
dot = GlowDot()
dot.add_updater(lambda d: d.move_to(
vectors[-1].get_end() if len(vectors) > 0 else ORIGIN
))
self.add(dot)
return dot

View file

@ -210,7 +210,7 @@ class DecomposeAudioSegment(Scene):
self.break_down_into_fourier_components() self.break_down_into_fourier_components()
self.back_to_full_signal() self.back_to_full_signal()
def add_full_waveform(self): def add_full_waveform(self, run_time=5):
axes, graph = self.get_signal_graph() axes, graph = self.get_signal_graph()
self.add(axes) self.add(axes)
@ -224,7 +224,7 @@ class DecomposeAudioSegment(Scene):
time_width=0.1, time_width=0.1,
rate_func=linear, rate_func=linear,
), ),
run_time=5, run_time=run_time,
) )
self.axes = axes self.axes = axes
@ -261,20 +261,21 @@ class DecomposeAudioSegment(Scene):
zoom_rect.target.set_stroke(width=0) zoom_rect.target.set_stroke(width=0)
axes.target.set_stroke(opacity=0) axes.target.set_stroke(opacity=0)
self.play(Write(zoom_rect))
self.play(
*map(MoveToTarget, movers),
run_time=run_time
)
self.remove(graph, axes)
self.wait()
# Swap axes
new_axes = Axes((-2, 12), (-1, 1, 0.25), width=FRAME_WIDTH + 1) new_axes = Axes((-2, 12), (-1, 1, 0.25), width=FRAME_WIDTH + 1)
new_axes.shift(LEFT_SIDE + RIGHT - new_axes.get_origin()) new_axes.shift(LEFT_SIDE + RIGHT - new_axes.get_origin())
if fade_in_new_axes: self.play(Write(zoom_rect))
self.play(FadeIn(new_axes)) self.play(
*map(MoveToTarget, movers),
FadeIn(new_axes),
run_time=run_time,
)
self.remove(graph, axes)
# Swap axes
# if fade_in_new_axes:
# self.play(FadeIn(new_axes))
self.original_graph = graph self.original_graph = graph
self.original_axes = axes self.original_axes = axes