diff --git a/active_projects/ode/part2/fourier_series.py b/active_projects/ode/part2/fourier_series.py index 050ea2e5..6fa53bfc 100644 --- a/active_projects/ode/part2/fourier_series.py +++ b/active_projects/ode/part2/fourier_series.py @@ -15,12 +15,21 @@ class FourierCirclesScene(Scene): "circle_style": { "stroke_width": 2, }, + "use_vectors": True, "base_frequency": 1, "slow_factor": 0.25, "center_point": ORIGIN, "parametric_function_step_size": 0.001, } + def setup(self): + self.slow_factor_tracker = ValueTracker( + self.slow_factor + ) + + def get_slow_factor(self): + return self.slow_factor_tracker.get_value() + # def get_freqs(self): n = self.n_circles @@ -68,10 +77,16 @@ class FourierCirclesScene(Scene): color=color, **self.circle_style, ) - circle.radial_line = Line( + if self.use_vectors: + LineClass = Arrow + else: + LineClass = Line + circle.radial_line = LineClass( circle.get_center(), circle.get_start(), color=WHITE, + buff=0, + max_tip_length_to_length_ratio=0.1, **self.circle_style, ) circle.add(circle.radial_line) @@ -85,7 +100,7 @@ class FourierCirclesScene(Scene): def update_circle(self, circle, dt): circle.rotate( - self.slow_factor * circle.freq * dt * TAU + self.get_slow_factor() * circle.freq * dt * TAU ) circle.move_to(circle.center_func()) return circle @@ -98,9 +113,11 @@ class FourierCirclesScene(Scene): 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() + vector.add_updater(lambda v, dt: v.put_start_and_end_on( + circle.get_center(), + circle.get_start(), )) + circle.vector = vector return vector def get_circle_end_path(self, circles, color=YELLOW): @@ -123,20 +140,20 @@ class FourierCirclesScene(Scene): return path # TODO, this should be a general animated mobect - def get_drawn_path(self, circles, **kwargs): + def get_drawn_path(self, circles, stroke_width=2, **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 + alpha = path.curr_time * self.get_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)) + width = stroke_width * (1 - (b % 1)) sp.set_stroke(YELLOW, width=width) path.curr_time += dt return path @@ -168,7 +185,7 @@ class FourierCirclesScene(Scene): top_point = wave_copies.get_top() wave.creation = ShowCreation( wave, - run_time=(1 / self.slow_factor), + run_time=(1 / self.get_slow_factor()), rate_func=linear, ) cycle_animation(wave.creation) @@ -178,7 +195,7 @@ class FourierCirclesScene(Scene): def update_wave_copies(wcs): index = int( - wave.creation.total_time * self.slow_factor + wave.creation.total_time * self.get_slow_factor() ) wcs[:index].match_style(wave) wcs[index:].set_stroke(width=0) @@ -290,7 +307,6 @@ class FourierOfPiSymbol(FourierCirclesScene): 1 / np.sqrt(k), 1, )) - print(circle.freq, abs(circle.coefficient)) # approx_path = self.get_circle_end_path(circles) drawn_path = self.get_drawn_path(circles) @@ -323,36 +339,56 @@ class FourierOfTrebleClef(FourierOfPiSymbol): "n_circles": 100, "run_time": 10, "start_drawn": True, + "file_name": "TrebleClef", + "height": 7.5, } + def get_shape(self): + shape = SVGMobject(self.file_name) + return shape + def get_path(self): - path = SVGMobject("TrebleClef") - path = path.family_members_with_points()[0] - path.set_height(7.5) + shape = self.get_shape() + path = shape.family_members_with_points()[0] + path.set_height(self.height) path.set_fill(opacity=0) path.set_stroke(WHITE, 0) return path +class FourierOfEighthNote(FourierOfTrebleClef): + CONFIG = { + "file_name": "EighthNote" + } + + +class FourierOfN(FourierOfTrebleClef): + CONFIG = { + "height": 6, + "n_circles": 200, + } + + def get_shape(self): + return TexMobject("N") + + class ExplainCircleAnimations(FourierCirclesScene): CONFIG = { - # "n_circles": 100, - "n_circles": 20, + "n_circles": 100, "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.wait(8) 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): @@ -367,11 +403,9 @@ class ExplainCircleAnimations(FourierCirclesScene): 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() + top_circles = circles[:self.n_top_circles].copy() center_trackers = VGroup() for circle in top_circles: @@ -436,6 +470,9 @@ class ExplainCircleAnimations(FourierCirclesScene): ) self.wait(2) + self.freq_numbers = freq_numbers + self.freq_word = freq_word + def show_examples_for_frequencies(self): top_circles = self.top_circles c1, c2, c3 = [ @@ -479,41 +516,179 @@ class ExplainCircleAnimations(FourierCirclesScene): ]) ) self.wait(3) + self.play(FadeOut(self.freq_word)) def show_as_vectors(self): top_circles = self.top_circles top_vectors = self.get_rotating_vectors(top_circles) + top_vectors.set_color(RED) + lines = VGroup(*filter( + lambda sm: isinstance(sm, Line), + top_circles.family_members_with_points() + )) + self.add(lines, top_circles) self.play( - top_circles.set_stroke, {"width": 0.5}, FadeIn(top_vectors), + lines.fade, 1, ) self.wait(3) self.top_vectors = top_vectors def show_vector_sum(self): - top_circles = self.top_circles - top_circles = self.top_circles + # trackers = self.center_trackers.deepcopy() + trackers = self.center_trackers.copy() + trackers.sort( + submob_func=lambda t: abs(t.circle.freq) + ) + plane = self.plane = NumberPlane( + x_min=-3, + x_max=3, + y_min=-2, + y_max=2, + axis_config={ + "stroke_color": LIGHT_GREY, + } + ) + plane.set_stroke(width=1) + plane.fade(0.5) + plane.move_to(self.center_point) self.play( - FadeOut(self.path), + # FadeOut(self.path), + FadeOut(self.drawn_path), FadeOut(self.circles), + self.slow_factor_tracker.set_value, 0.05, ) + self.add(plane, self.path) + self.play(FadeIn(plane)) - def moons_of_moons_of_moons(self): - pass + new_circles = VGroup() + new_vectors = VGroup() + last_tracker = None + for tracker in trackers: + if last_tracker: + tracker.new_location_func = last_tracker.circle.get_start + else: + tracker.new_location_func = lambda: self.center_point + + original_circle = tracker.circle + original_vector = tracker.circle.vector + tracker.circle = original_circle.copy() + tracker.circle.center_func = tracker.get_location + tracker.vector = original_vector.copy() + tracker.vector.clear_updaters() + tracker.vector.circle = tracker.circle + tracker.vector.add_updater(lambda v: v.put_start_and_end_on( + v.circle.get_center(), + v.circle.get_start(), + )) + new_circles.add(tracker.circle) + new_vectors.add(tracker.vector) + + self.add(tracker, tracker.circle, tracker.vector) + start_point = tracker.get_location() + self.play( + UpdateFromAlphaFunc( + tracker, lambda t, a: t.move_to( + interpolate( + start_point, + tracker.new_location_func(), + a, + ) + ), + run_time=2 + ) + ) + tracker.add_updater(lambda t: t.move_to( + t.new_location_func() + )) + self.wait(2) + last_tracker = tracker + + self.wait(3) + + self.clear() + self.slow_factor_tracker.set_value(0.1) + self.add( + self.top_circles, + self.top_vectors, + self.freq_numbers, + self.path, + ) + self.add_circles() + for tc in self.top_circles: + for c in self.circles: + if c.freq == tc.freq: + tc.rotate( + angle_of_vector(c.get_start() - c.get_center()) - + angle_of_vector(tc.get_start() - tc.get_center()) + ) + self.wait(10) def tweak_starting_vectors(self): - pass + top_circles = self.top_circles + top_vectors = self.top_vectors + circles = self.circles + path = self.path + drawn_path = self.drawn_path + + new_path = self.get_new_path() + new_coefs = self.get_coefficients_of_path(new_path) + new_circles = self.get_circles(coefficients=new_coefs) + + new_top_circles = VGroup() + new_top_vectors = VGroup() + for top_circle in top_circles: + for circle in new_circles: + if circle.freq == top_circle.freq: + new_top_circle = circle.copy() + new_top_circle.center_func = top_circle.get_center + new_top_vector = self.get_rotating_vector( + new_top_circle + ) + new_top_circles.add(new_top_circle) + new_top_vectors.add(new_top_vector) + + self.play( + self.slow_factor_tracker.set_value, 0, + FadeOut(drawn_path) + ) + self.wait() + self.play( + ReplacementTransform(top_circles, new_top_circles), + ReplacementTransform(top_vectors, new_top_vectors), + ReplacementTransform(circles, new_circles), + # ReplacementTransform(path, new_path), + FadeOut(path), + run_time=3, + ) + new_drawn_path = self.get_drawn_path( + new_circles, stroke_width=4, + ) + self.add(new_drawn_path) + self.slow_factor_tracker.set_value(0.1) + self.wait(20) # - def get_path(self): - tex = TexMobject("f") - path = tex.family_members_with_points()[0] + def configure_path(self, path): path.set_stroke(WHITE, 1) - path.set_fill(opacity=0) + path.set_fill(BLACK, opacity=1) path.set_height(self.path_height) path.move_to(self.center_point) return path + + def get_path(self): + tex = TexMobject("f") + path = tex.family_members_with_points()[0] + self.configure_path(path) + return path # return Square().set_height(3) + + def get_new_path(self): + shape = SVGMobject("TrebleClef") + path = shape.family_members_with_points()[0] + self.configure_path(path) + path.scale(1.5, about_edge=DOWN) + return path