【🌠】MANIM傅立叶频谱视频
完整项目地址:https://github.com/Remyuu/manim_video_FFT
关于3b1b的"Almost" Fourier Transform复刻的技术实现:https://remoooo.com/it/108.html
项目约1万行代码,最终视频约10分钟。
本项目摘要:
项目最终以视频呈现,视频由python的manim数学动画引擎 与 Final Cut Pro(iMovie)制作。
从音乐入手分析一个音符的震动如何拆解为一系列三角函数,认识何为频谱图、Fourier Transform。
亮点:
通过维瓦尔弟著名乐曲 四季:冬 ,进一步体会Frequency Domain。
from manim import *
import numpy as np
import scipy
from scipy import integrate
class formula_set(MathTex):
formula_01=[
MathTex(r"F\left(\omega_{k}\right) \equiv \int_{-\infty}^{\infty}",r"f(t) e^{-2 \pi i k t}",r"\mathrm{~d} t, \quad k \in(-\infty, \infty)"),
MathTex(r"f(t) e^{-2 \pi i k t}"),
MathTex(r"F\left(\omega_{k}\right) = \int_{0}^{1} f(t) e^{-2 \pi i k t} \mathrm{~d} t, \\ k \in(-\infty, \infty)"),
]
class ContinuousFourierP(Scene):
def Scene01(self):
global formula0,formula1
text1 = Tex('Fourier Transform')
formula0 = formula_set.formula_01[0]
formula1 = formula_set.formula_01[1].next_to(ORIGIN,RIGHT).shift(DOWN)
self.wait()
self.play(FadeOut(text1))
self.wait()
self.play(Write(formula0))
self.wait()
self.play(FadeOut(formula0[0],formula0[2]))
self.play(formula0[1].animate.move_to(formula1))
self.wait()
def Scene02(self):
global f,axis_c,axis_polar,graph_c
f = lambda x: .5*(np.cos(5*2*PI*x)+1)
axis_c = Axes(
x_range=[0 , 1.1 , 1],y_range=[0, 1.5, 1],
y_length=2,axis_config={"include_numbers": True},tips=False,
).to_edge(UP)
axis_label = axis_c.get_axis_labels(x_label='time',y_label='intensity')
axis_polar = PolarPlane(radius_max=1,size=4,azimuth_step = 10,
fill_opacity=0.5,
azimuth_direction='CW',
azimuth_label_font_size=36,
azimuth_units="PI radians",
background_line_style = {
"stroke_color": ORANGE,
"stroke_width": 2,
"stroke_opacity": 0.5,
}).add_coordinates().to_edge(DL)
graph_c = axis_c.plot(f,x_range=[0,1,0.01],color=TEAL_E)
##绘制坐标,高亮并展示f(t) 并加 f(t)=?sin(?t)
self.play(FadeIn(axis_c,axis_label,axis_polar))
self.play(Circumscribe(formula1[0][0:4]))
self.wait()
tex0 = MathTex(r'f(t)=\sin(?t)').move_to(graph_c)
bg_tex0 = BackgroundRectangle(tex0, color=YELLOW, fill_opacity=0.15)
self.play(ReplacementTransform(formula1[0][0:4].copy(),graph_c),Write(tex0),Write(bg_tex0),run_time=1.8)
##上下动
self.play(axis_c.get_x_axis().animate.shift(0.5*UP),graph_c.animate.shift(0.5*DOWN),run_time=.8)
self.play(axis_c.get_x_axis().animate.shift(0.5*DOWN),graph_c.animate.shift(0.5*UP),run_time=.8)
def Scene03(self):
global polar_graph,tracker_Xrange_scale,tracker
def get_labelBox():return SurroundingRectangle(VGroup(label,t), corner_radius=0.1)
def get_arc():return ArcBetweenPoints(
end= axis_polar.coords_to_point(0.3,0),
start= axis_polar.coords_to_point(0.3*np.cos(TAU*tracker_Xrange_scale.get_value()*tracker.get_value()),-0.3*np.sin(TAU*tracker_Xrange_scale.get_value()*tracker.get_value())),
stroke_color=YELLOW)
##先高亮指数部分,挪箭头过去
self.play(Circumscribe(formula1[0][4:11]))
tracker_Xrange_scale = ValueTracker(0.4)
tracker = ValueTracker(0.9)
###直角坐标的箭头 共四个组件
pointer = Vector(UP).scale(0.5).next_to(axis_c.c2p(tracker.get_value(),0),DOWN)
label = MathTex("t=").next_to(pointer, DOWN)
t = DecimalNumber(tracker.get_value()).next_to(label,RIGHT)
box_label = get_labelBox()
vec_e = axis_polar.get_vector([np.cos(2*PI*tracker_Xrange_scale.get_value()* tracker.get_value()),np.sin(-2*PI*tracker_Xrange_scale.get_value()*tracker.get_value())])
label_e = MathTex('e^{}'.format('{-2\pi '+str(round(tracker_Xrange_scale.get_value(),1))+'\cdot '+str(round(tracker.get_value(),2))+' i}')).next_to(vec_e, DOWN)
arc= get_arc()
##展示直角坐标指针、极坐标向量,标签等
self.play(
ReplacementTransform(formula1[0][4:11].copy(),vec_e),
GrowFromEdge(VGroup(pointer,label,t,box_label),DOWN),
GrowFromEdge(VGroup(label_e,arc),UP))
pointer.add_updater(lambda m: m.next_to(axis_c.c2p(tracker.get_value(),0),DOWN))
label.add_updater(lambda m: m.next_to(pointer, DOWN))
t.add_updater(lambda obj : obj.become(DecimalNumber(round(tracker.get_value(),2))).next_to(label,RIGHT))
box_label.add_updater(lambda obj : obj.become(get_labelBox()))
UP_label_e = lambda m: m.become(MathTex('e^{}'.format('{-2\pi '+str(round(tracker_Xrange_scale.get_value(),1))+'\cdot '+str(round(tracker.get_value(),2))+' i}')).next_to(vec_e, DOWN))
UP_vec = lambda obj : obj.become(axis_polar.get_vector([
np.cos( TAU*tracker_Xrange_scale.get_value()*tracker.get_value()),
np.sin(-TAU*tracker_Xrange_scale.get_value()*tracker.get_value())]))
UP_arc = lambda ob : ob.become(get_arc())
label_e.add_updater(UP_label_e)
vec_e.add_updater(UP_vec)
arc.add_updater(UP_arc)
##动动t
self.wait(2)
self.play(tracker.animate.set_value(1),run_time=0.7)
self.wait(0.5)
self.play(tracker.animate.set_value(0),run_time=2.3)
#极坐标图像
polar_graph = axis_polar.plot_parametric_curve(lambda t : [
+np.cos(TAU*tracker_Xrange_scale.get_value()*t)*(f(t)),
-np.sin(TAU*tracker_Xrange_scale.get_value()*t)*(f(t)),0
],t_range=[0,1,0.01],color=TEAL_E)
##直角坐标绕下来显示极坐标图像,移除极坐标箭头
vec_e.remove_updater(UP_vec)
label_e.remove_updater(UP_label_e)
self.play(ReplacementTransform(graph_c.copy(),polar_graph),
run_time=3.5,
path_arc = -TAU*2/3)
self.play(FadeOut(vec_e),FadeOut(label_e))
##在极坐标图像上用点代替箭头,进一步动动t,乘上f(t)
dot_polar = Dot()
updot_polar_f = lambda m : m.move_to(axis_polar.i2gp(graph=polar_graph,x=tracker.get_value()))#
dot_polar.add_updater(updot_polar_f)
vec_e = axis_polar.get_vector([f(tracker.get_value())*np.cos(TAU*tracker_Xrange_scale.get_value()* tracker.get_value()),f(tracker.get_value())*np.sin(-TAU*tracker_Xrange_scale.get_value()*tracker.get_value())])
label_e = MathTex(r'f(t) \cdot radius').next_to(vec_e, UP)
UP_vec_mult_f = lambda obj : obj.become(axis_polar.get_vector([
f(tracker.get_value())*np.cos( TAU*tracker_Xrange_scale.get_value()*tracker.get_value()),
f(tracker.get_value())*np.sin(-TAU*tracker_Xrange_scale.get_value()*tracker.get_value())]))
UP_label_e_mult_f = lambda m: m.become(MathTex(r'f(t) \cdot radius').next_to(vec_e, UP))
self.play(FadeIn(dot_polar,vec_e,label_e),run_time=0.2)
vec_e.add_updater(UP_vec_mult_f)
label_e.add_updater(UP_label_e_mult_f)
self.wait()
self.play(tracker.animate.set_value(1),run_time=10,rate_func=rate_functions.linear)
self.wait(0.5)
self.play(tracker.animate.set_value(0.4),run_time=10,rate_func=rate_functions.linear)
self.wait(0.5)
#移除f(t)·radius、箭头、弧、点。尽量清除更新器
vec_e.remove_updater(UP_vec_mult_f)
label_e.remove_updater(UP_label_e_mult_f)
arc.remove_updater(UP_arc)
dot_polar.remove_updater(updot_polar_f)
self.play(FadeOut(vec_e,label_e,arc,dot_polar,pointer,label,t,box_label))
def Scene04(self):
##公式改变
formula2 = formula_set.formula_01[2].next_to(ORIGIN,RIGHT).shift(DOWN)
self.play(ReplacementTransform(formula0[1],formula2))
self.wait()
##高亮公式,变成质点,质点已添加更新器
dot_CoM = Dot().move_to(polar_graph.get_center_of_mass()).set_color(YELLOW)
UP_dot = lambda obj : obj.move_to(polar_graph.get_center_of_mass()).set_color(YELLOW)
dot_CoM.add_updater(UP_dot)
self.play(Circumscribe(formula2))
self.wait()
self.play(ReplacementTransform(polar_graph.copy(),dot_CoM))
##indicate一下质点,动动极坐标图像
pointer_CoM = Vector(DL).scale(0.5).next_to(dot_CoM,UR)
label_pointer_CoM = Tex("Center of Mass").next_to(pointer_CoM, UP)
pointer_CoM.add_updater(lambda m: m.next_to(dot_CoM,UR))
label_pointer_CoM.add_updater(lambda m: m.next_to(pointer_CoM, UP))
upfunc_polar = lambda obj : obj.become(axis_polar.plot_parametric_curve(lambda t : [
+f(t)*np.cos(TAU*tracker.get_value()*tracker_Xrange_scale.get_value()*t),
-f(t)*np.sin(TAU*tracker.get_value()*tracker_Xrange_scale.get_value()*t),
0],t_range=[0,1,0.01],color=TEAL_E
))
self.play(FadeIn(pointer_CoM,label_pointer_CoM))
tracker_Xrange_scale.set_value(1)
tracker.set_value(.4)
polar_graph.add_updater(upfunc_polar)
self.wait()
self.play(tracker_Xrange_scale.animate.set_value(4))
self.wait()
self.play(tracker.animate.set_value(0))
##移除质点指示
self.play(FadeOut(pointer_CoM,label_pointer_CoM))
self.play(tracker_Xrange_scale.animate.set_value(1))
#freq Domain图
axis_freq = Axes(x_range=[0, 15, 1], y_range=[-0.1, 0.8],
x_length=8, y_length=3,axis_config={"include_numbers": True}
).next_to(axis_polar, RIGHT).align_to(axis_polar, DOWN)
def get_freqDomain_sample(wind):return scipy.integrate.quad(
lambda t: f(t)*np.exp(complex(0, -TAU*wind*t)),0, 1)[0].real
graph_freqD = axis_freq.plot(
get_freqDomain_sample, x_range=[0, 10, 0.1],color=YELLOW)
dot_freq = Dot().move_to(axis_freq.i2gp(tracker.get_value(),graph_freqD))
def up_dot(dot):
temp_dot = Dot(radius=0.02, color=YELLOW, fill_opacity=0.8)
temp_dot.move_to(axis_freq.i2gp(tracker.get_value(),graph_freqD))
dot.move_to(temp_dot)
dot_freq.add_updater(up_dot)
path = VMobject()
path.set_points_as_corners([dot_freq.get_center(), dot_freq.get_center()])
def update_path(path):
previous_path = path.copy()
previous_path.add_points_as_corners([dot_freq.get_center()])
path.become(previous_path)
path.set_color(YELLOW)
path.add_updater(update_path)
tex_freq = Tex(r'frequency domain').move_to(axis_freq).shift(DOWN*0.5)
bg_freqD = BackgroundRectangle(tex_freq, color=YELLOW, fill_opacity=0.15)
self.play(FadeIn(axis_freq,dot_freq,path,tex_freq,bg_freqD))
self.play(tracker.animate.set_value(14),run_time=25)
def construct(self):
self.Scene01()#开场文字+公式
self.Scene02()#绘制坐标,展示f函数
self.Scene03()#直角坐标图像绕下来,叙述极坐标
self.Scene04()#公式改变,开始跟随质点画频域图