努力载入中...

【🌠】MANIM傅立叶频谱视频 - Remoooo

完整项目地址: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。

2022-11-16T13:09:00.png


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()#公式改变,开始跟随质点画频域图

添加新评论

🚥
☕️
⬆️