From 2b80c582dd1c89870f826a0e6ef03e524c0416c6 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 15 Jan 2018 15:30:15 -0800 Subject: [PATCH] BreakApartSum and UnmixMixedPaint from fourier project --- active_projects/fourier.py | 168 +++++++++++++++++++++++++++++++++++-- 1 file changed, 161 insertions(+), 7 deletions(-) diff --git a/active_projects/fourier.py b/active_projects/fourier.py index 5da34f16..443785f2 100644 --- a/active_projects/fourier.py +++ b/active_projects/fourier.py @@ -122,7 +122,6 @@ class AddingPureFrequencies(PiCreatureScene): Write(time), ShowCreation(axes.x_axis) ) - self.dither() self.broadcast( ShowCreation(graph, run_time = 4, rate_func = None), ShowCreation(equilibrium_line), @@ -133,7 +132,7 @@ class AddingPureFrequencies(PiCreatureScene): GrowFromCenter(brace), Write(words) ) - self.dither(2) + self.dither() graph.save_state() self.play( FadeOut(brace), @@ -325,7 +324,10 @@ class AddingPureFrequencies(PiCreatureScene): ]) result *= 0.5 return result + self.equilibrium_height - new_sum_graph = self.axes.get_graph(new_sum_func) + new_sum_graph = self.axes.get_graph( + new_sum_func, + num_graph_points = 200 + ) new_sum_graph.highlight(BLUE_C) self.play(*it.chain( @@ -408,10 +410,162 @@ class AddingPureFrequencies(PiCreatureScene): return UpdateFromAlphaFunc(line, update_line, **kwargs) - - - - +class BreakApartSum(Scene): + CONFIG = { + "frequencies" : [0.5, 1.5, 2, 2.5, 5], + "equilibrium_height" : 2.0, + } + def construct(self): + self.show_initial_sound() + self.decompose_sound() + self.ponder_question() + + def show_initial_sound(self): + def func(x): + return self.equilibrium_height + 0.2*np.sum([ + np.cos(2*np.pi*f*x) + for f in self.frequencies + ]) + axes = Axes( + x_min = 0, x_max = 5, + y_min = -1, y_max = 5, + x_axis_config = { + "include_tip" : False, + "unit_size" : 2.0, + }, + y_axis_config = { + "include_tip" : False, + "unit_size" : 0.5, + }, + ) + axes.stretch_to_fit_width(2*SPACE_WIDTH - 2) + axes.stretch_to_fit_height(3) + axes.center() + axes.to_edge(LEFT) + graph = axes.get_graph(func, num_graph_points = 200) + graph.highlight(YELLOW) + + v_line = Line(ORIGIN, 4*UP) + v_line.move_to(axes.coords_to_point(0, 0), DOWN) + dot = Dot(color = PINK) + dot.move_to(graph.point_from_proportion(0)) + + self.add(axes, graph) + self.play( + v_line.move_to, axes.coords_to_point(5, 0), DOWN, + MoveAlongPath(dot, graph), + run_time = 8, + rate_func = None, + ) + self.play(*map(FadeOut, [dot, v_line])) + + self.set_variables_as_attrs(axes, graph) + + def decompose_sound(self): + axes, graph = self.axes, self.graph + + pure_graphs = VGroup(*[ + axes.get_graph( + lambda x : 0.5*np.cos(2*np.pi*freq*x), + num_graph_points = 100, + ) + for freq in self.frequencies + ]) + pure_graphs.gradient_highlight(BLUE, RED) + pure_graphs.arrange_submobjects(DOWN, buff = MED_LARGE_BUFF) + h_line = DashedLine(6*LEFT, 6*RIGHT) + + self.play( + FadeOut(axes), + graph.to_edge, UP + ) + pure_graphs.next_to(graph, DOWN, LARGE_BUFF) + h_line.next_to(graph, DOWN, MED_LARGE_BUFF) + self.play(ShowCreation(h_line)) + for pure_graph in reversed(pure_graphs): + self.play(ReplacementTransform(graph.copy(), pure_graph)) + self.dither() + + self.all_graphs = VGroup(graph, h_line, pure_graphs) + self.pure_graphs = pure_graphs + + def ponder_question(self): + all_graphs = self.all_graphs + pure_graphs = self.pure_graphs + randy = Randolph() + randy.to_corner(DOWN+LEFT) + + self.play( + FadeIn(randy), + all_graphs.scale, 0.75, + all_graphs.to_corner, UP+RIGHT, + ) + self.play(randy.change, "pondering", all_graphs) + self.play(Blink(randy)) + rect = SurroundingRectangle(pure_graphs, color = WHITE) + self.play( + ShowCreation(rect), + LaggedStart( + ApplyFunction, pure_graphs, + lambda g : (lambda m : m.shift(SMALL_BUFF*UP).highlight(YELLOW), g), + rate_func = wiggle + ) + ) + self.play(FadeOut(rect)) + self.play(Blink(randy)) + self.dither() + +class Quadrant(Mobject1D): + CONFIG = { + "radius" : 2, + "stroke_width": 2, + "density" : 75, + } + def generate_points(self): + self.add_points([ + r*(np.cos(theta)*RIGHT + np.sin(theta)*UP) + for r in np.arange(0, self.radius, self.epsilon) + for theta in np.arange(0, np.pi/2, self.epsilon/r) + ]) + +class UnmixMixedPaint(Scene): + CONFIG = { + "colors" : [BLUE, RED, YELLOW, GREEN], + } + def construct(self): + angles = np.arange(4)*np.pi/2 + quadrants = PMobject(*[ + Quadrant().rotate(angle).highlight(color) + for color, angle in zip(self.colors, angles) + ]) + quadrants.ingest_submobjects() + quadrants.sort_points(lambda p : np.random.random()) + + start_rgbas = np.array(quadrants.rgbas) + target_rgbas = np.zeros(start_rgbas.shape) + mud_color = average_color(*self.colors) + target_rgbas[:,:] = color_to_rgba(mud_color) + + background_circle = Circle( + radius = 2, + stroke_width = 0, + fill_color = mud_color, + fill_opacity = 1 + ) + + def update_color(quadrants, alpha): + quadrants.rgbas = interpolate(start_rgbas, target_rgbas, alpha) + + self.add(background_circle, quadrants) + self.play( + PhaseFlow( + lambda p : rotate_vector(p, np.pi/2+0.01)/(max(np.linalg.norm(p), 0.001)), + quadrants, + virtual_time = 20, + ), + UpdateFromAlphaFunc(quadrants, update_color), + run_time = 10, + )