diff --git a/active_projects/fourier.py b/active_projects/fourier.py index 83857a08..386f9bf0 100644 --- a/active_projects/fourier.py +++ b/active_projects/fourier.py @@ -32,6 +32,7 @@ from topics.graph_scene import * USE_ALMOST_FOURIER_BY_DEFAULT = True NUM_SAMPLES_FOR_FFT = 1000 +DEFAULT_COMPLEX_TO_REAL_FUNC = lambda z : z.real def get_fourier_graph( @@ -53,7 +54,7 @@ def get_fourier_graph( graph = VMobject() graph.set_points_smoothly([ axes.coords_to_point( - x, 200.0*complex_to_real_func(y)/n_samples, + x, complex_to_real_func(y)/n_samples, ) for x, y in zip(frequencies, fft_output[:n_samples//2]) ]) @@ -62,18 +63,17 @@ def get_fourier_graph( def get_fourier_transform( func, t_min, t_max, - complex_to_real_func = lambda z : z.real, + complex_to_real_func = DEFAULT_COMPLEX_TO_REAL_FUNC, use_almost_fourier = USE_ALMOST_FOURIER_BY_DEFAULT, + **kwargs ##Just eats these ): scalar = 1./(t_max - t_min) if use_almost_fourier else 1.0 def fourier_transform(f): - return scalar*scipy.integrate.quad( - lambda t : complex_to_real_func( - # f(t) e^{-TAU*i*f*t} - func(t)*np.exp(complex(0, -TAU*f*t)) - ), + z = scalar*scipy.integrate.quad( + lambda t : func(t)*np.exp(complex(0, -TAU*f*t)), t_min, t_max )[0] + return complex_to_real_func(z) return fourier_transform ## @@ -939,11 +939,28 @@ class FourierMachineScene(Scene): if not hasattr(self, "frequency_axes"): self.get_frequency_axes() func = time_graph.underlying_function - t_min = self.time_axes.x_min - t_max = self.time_axes.x_max + t_axis = self.time_axes.x_axis + t_min = t_axis.point_to_number(time_graph.points[0]) + t_max = t_axis.point_to_number(time_graph.points[-1]) + f_max = self.frequency_axes.x_max + # result = get_fourier_graph( + # self.frequency_axes, func, t_min, t_max, + # **kwargs + # ) + # too_far_right_point_indices = [ + # i + # for i, point in enumerate(result.points) + # if self.frequency_axes.x_axis.point_to_number(point) > f_max + # ] + # if too_far_right_point_indices: + # i = min(too_far_right_point_indices) + # prop = float(i)/len(result.points) + # result.pointwise_become_partial(result, 0, prop) + # return result return self.frequency_axes.get_graph( get_fourier_transform(func, t_min, t_max, **kwargs), color = self.center_of_mass_color, + **kwargs ) def get_polarized_mobject(self, mobject, freq = 1.0): @@ -999,7 +1016,8 @@ class FourierMachineScene(Scene): vector = Vector(UP, color = WHITE) graph_copy = graph.copy() x_axis = self.time_axes.x_axis - x_min, x_max = x_axis.x_min, x_axis.x_max + x_min = x_axis.point_to_number(graph.points[0]) + x_max = x_axis.point_to_number(graph.points[-1]) def update_vector(vector, alpha): x = interpolate(x_min, x_max, alpha) vector.put_start_and_end_on( @@ -1016,7 +1034,13 @@ class FourierMachineScene(Scene): origin = self.circle_plane.coords_to_point(0, 0) graph_copy = polarized_graph.copy() def update_vector(vector, alpha): - point = graph_copy.point_from_proportion(alpha) + # Not sure why this is needed, but without smoothing + # out the alpha like this, the vector would occasionally + # jump around + point = center_of_mass([ + graph_copy.point_from_proportion(alpha+d) + for d in np.linspace(-0.001, 0.001, 5) + ]) vector.put_start_and_end_on_with_projection(origin, point) return vector return UpdateFromAlphaFunc(vector, update_vector, **config) @@ -1299,6 +1323,7 @@ class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene): CONFIG = { "initial_winding_frequency" : 3.0, "center_of_mass_color" : RED, + "center_of_mass_multiple" : 1, } def construct(self): self.remove(self.pi_creature) @@ -1581,10 +1606,11 @@ class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene): def get_pol_graph_center_of_mass(self): pg = self.graph.polarized_mobject - result = center_of_mass([ - pg.point_from_proportion(alpha) - for alpha in np.linspace(0, 1, 1000) - ]) + result = center_of_mass(pg.get_anchors()) + if self.center_of_mass_multiple != 1: + mult = self.center_of_mass_multiple + origin = self.circle_plane.coords_to_point(0, 0) + result = mult*(result - origin) + origin return result def generate_fourier_dot_transform(self, fourier_graph): @@ -1626,7 +1652,7 @@ class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene): def change_frequency(self, new_freq, **kwargs): kwargs["run_time"] = kwargs.get("run_time", 3) - kwargs["rate_func"] = kwargs.get( + rate_func = kwargs.pop( "rate_func", bezier([0, 0, 1, 1]) ) added_anims = kwargs.get("added_anims", []) @@ -1645,6 +1671,8 @@ class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene): anims.append(self.fourier_graph_dot_anim) if hasattr(self, "fourier_graph_drawing_update_anim"): anims.append(self.fourier_graph_drawing_update_anim) + for anim in anims: + anim.rate_func = rate_func anims += added_anims self.play(*anims, **kwargs) diff --git a/active_projects/uncertainty.py b/active_projects/uncertainty.py index fcbe9b22..bb3be45b 100644 --- a/active_projects/uncertainty.py +++ b/active_projects/uncertainty.py @@ -28,6 +28,7 @@ from mobject.vectorized_mobject import * from mobject.svg_mobject import * from mobject.tex_mobject import * from topics.graph_scene import * +from topics.light import * from active_projects.fourier import * @@ -238,6 +239,82 @@ class RadarPulse(ContinualAnimation): def is_finished(self): return all([ps.is_finished() for ps in self.pulse_singletons]) +class Flash(AnimationGroup): + CONFIG = { + "line_length" : 0.2, + "num_lines" : 12, + "flash_radius" : 0.3, + "line_stroke_width" : 3, + } + def __init__(self, mobject, color = YELLOW, **kwargs): + digest_config(self, kwargs) + original_color = mobject.get_color() + on_and_off = UpdateFromAlphaFunc( + mobject.copy(), lambda m, a : m.highlight( + color if a < 0.5 else original_color + ), + remover = True + ) + lines = VGroup() + for angle in np.arange(0, TAU, TAU/self.num_lines): + line = Line(ORIGIN, self.line_length*RIGHT) + line.shift((self.flash_radius - self.line_length)*RIGHT) + line.rotate(angle, about_point = ORIGIN) + lines.add(line) + lines.move_to(mobject) + lines.highlight(color) + line_anims = [ + ShowCreationThenDestruction( + line, rate_func = squish_rate_func(smooth, 0, 0.5) + ) + for line in lines + ] + fade_anims = [ + UpdateFromAlphaFunc( + line, lambda m, a : m.set_stroke( + width = self.line_stroke_width*(1-a) + ), + rate_func = squish_rate_func(smooth, 0, 0.75) + ) + for line in lines + ] + + AnimationGroup.__init__( + self, on_and_off, *line_anims+fade_anims, **kwargs + ) + +class MultipleFlashes(Succession): + CONFIG = { + "run_time_per_flash" : 1.0, + "num_flashes" : 3, + } + def __init__(self, *args, **kwargs): + digest_config(self, kwargs) + kwargs["run_time"] = self.run_time_per_flash + Succession.__init__(self, *[ + Flash(*args, **kwargs) + for x in range(self.num_flashes) + ]) + +class TrafficLight(SVGMobject): + CONFIG = { + "file_name" : "traffic_light", + "height" : 0.7, + "post_height" : 2, + "post_width" : 0.05, + } + def __init__(self, **kwargs): + SVGMobject.__init__(self, **kwargs) + post = Rectangle( + height = self.post_height, + width = self.post_width, + stroke_width = 0, + fill_color = WHITE, + fill_opacity = 1, + ) + self.move_to(post.get_top(), DOWN) + self.add_to_back(post) + ################### class MentionUncertaintyPrinciple(TeacherStudentsScene): @@ -327,6 +404,10 @@ class MentionUncertaintyPrinciple(TeacherStudentsScene): self.wait() class FourierTradeoff(Scene): + CONFIG = { + "show_text" : True, + "complex_to_real_func" : abs, + } def construct(self): #Setup axes time_mean = 4 @@ -339,8 +420,9 @@ class FourierTradeoff(Scene): y_axis_config = {"unit_size" : 0.5} ) time_label = TextMobject("Time") + time_label.scale(1.5) time_label.next_to( - time_axes.x_axis.get_right(), UP, + time_axes.x_axis.get_right(), UP+LEFT, buff = MED_SMALL_BUFF, ) time_axes.add(time_label) @@ -352,16 +434,17 @@ class FourierTradeoff(Scene): x_max = 8, x_axis_config = {"unit_size" : 1.5}, y_min = 0, - y_max = 15, + y_max = 0.1, y_axis_config = { - "unit_size" : 0.15, - "tick_frequency" : 5, + "unit_size" : 30, + "tick_frequency" : 0.025, }, color = TEAL, ) frequency_label = TextMobject("Frequency") + frequency_label.scale(1.5) frequency_label.next_to( - frequency_axes.x_axis.get_right(), UP, + frequency_axes.x_axis.get_right(), UP+LEFT, buff = MED_SMALL_BUFF, ) frequency_label.highlight(FREQUENCY_COLOR) @@ -392,12 +475,12 @@ class FourierTradeoff(Scene): time_radius = 10 def get_wave_packet_fourier_transform(): return get_fourier_graph( - frequency_axes, get_wave_packet_function(), + frequency_axes, + get_wave_packet_function(), t_min = time_mean - time_radius, t_max = time_mean + time_radius, n_samples = 2*time_radius*17, - # complex_to_real_func = abs, - complex_to_real_func = lambda z : z.real, + complex_to_real_func = self.complex_to_real_func, color = FREQUENCY_COLOR, ) @@ -413,11 +496,13 @@ class FourierTradeoff(Scene): ) arrow = Arrow( - wave_packet, frequency_axes.coords_to_point(4, 10), + wave_packet, frequency_axes.coords_to_point( + 4, frequency_axes.y_max/2, + ), color = FREQUENCY_COLOR, ) - fourier_words = TextMobject("Fourier Transform") - fourier_words.next_to(arrow, RIGHT, buff = MED_LARGE_BUFF) + fourier_words = TextMobject("$|$Fourier Transform$|$") + fourier_words.next_to(arrow, LEFT, buff = MED_LARGE_BUFF) sub_words = TextMobject("(To be explained shortly)") sub_words.highlight(BLUE) sub_words.scale(0.75) @@ -426,24 +511,25 @@ class FourierTradeoff(Scene): #Draw items self.add(time_axes, frequency_axes) self.play(ShowCreation(wave_packet, rate_func = double_smooth)) - self.play( - ReplacementTransform( - wave_packet.copy(), - fourier_graph, - ), - GrowArrow(arrow), - Write(fourier_words, run_time = 1) - ) + anims = [ReplacementTransform( + wave_packet.copy(), fourier_graph + )] + if self.show_text: + anims += [ + GrowArrow(arrow), + Write(fourier_words, run_time = 1) + ] + self.play(*anims) # self.play(FadeOut(arrow)) self.wait() - for width in 6, 0.1, 1: + for width in 6, 0.02, 1: self.play( width_tracker.move_to, width*RIGHT, wave_packet_update, fourier_graph_update, run_time = 3 ) - if sub_words not in self.mobjects: + if sub_words not in self.mobjects and self.show_text: self.play(FadeIn(sub_words)) else: self.wait() @@ -454,8 +540,8 @@ class ShowPlan(PiCreatureScene): self.add_title() words = self.get_words() self.play_sound_anims(words[0]) - self.play_doppler_anims(words[1], words[0]) - self.play_quantum_anims(words[2], words[1]) + self.play_doppler_anims(words[1]) + self.play_quantum_anims(words[2]) def add_title(self): title = TextMobject("The plan") @@ -466,14 +552,19 @@ class ShowPlan(PiCreatureScene): self.add(title, h_line) def get_words(self): - colors = [YELLOW, GREEN, BLUE] - topics = ["sound waves", "Doppler radar", "quantum particles"] + trips = [ + ("sound waves", "(time vs. frequency)", YELLOW), + ("Doppler radar", "(distance vs. velocity)", GREEN), + ("quantum particles", "(position vs. momentum)", BLUE), + ] words = VGroup() - for topic, color in zip(topics, colors): - word = TextMobject("Uncertainty for", topic) - word[1].highlight(color) + for topic, tradeoff, color in trips: + word = TextMobject("Uncertainty for", topic, tradeoff) + word[1:].highlight(color) + word[2].scale(0.75) + word[2].next_to(word[1], DOWN, buff = 1.5*SMALL_BUFF) words.add(word) - words.arrange_submobjects(DOWN, aligned_edge = LEFT, buff = LARGE_BUFF) + words.arrange_submobjects(DOWN, aligned_edge = LEFT, buff = MED_LARGE_BUFF) words.to_edge(LEFT) return words @@ -517,7 +608,7 @@ class ShowPlan(PiCreatureScene): self.add(word) self.wait() - def play_doppler_anims(self, word, to_fade): + def play_doppler_anims(self, word): morty = self.pi_creature radar_dish = RadarDish() @@ -533,7 +624,6 @@ class ShowPlan(PiCreatureScene): self.add(target_movement) self.play( - to_fade.fade, 0.5, Write(word), DrawBorderThenFill(radar_dish), UpdateFromAlphaFunc( @@ -542,7 +632,6 @@ class ShowPlan(PiCreatureScene): morty.change, "pondering", run_time = 1 ) - self.wait() self.add(pulse) count = it.count() #TODO, this is not a great hack... while not pulse.is_finished() and count.next() < 15: @@ -560,25 +649,1152 @@ class ShowPlan(PiCreatureScene): ) self.wait() + def play_quantum_anims(self, word): + morty = self.pi_creature + dot_cloud = ProbabalisticDotCloud() + gdw = dot_cloud.gaussian_distribution_wrapper + gdw.next_to(word, DOWN, MED_LARGE_BUFF) + gdw.rotate(5*DEGREES) + gdw.save_state() + gdw.scale(0) + checkmark = self.get_checkmark(word) + ish = TextMobject("$\\dots$ish") + ish.next_to(checkmark, RIGHT, -SMALL_BUFF, DOWN) + + self.add(dot_cloud) + self.play( + Write(word), + FadeIn(dot_cloud.mobject), + morty.change, "confused", + ) + self.play(gdw.restore, run_time = 2) + self.play(Write(checkmark)) + self.wait() + self.play( + Write(ish), + morty.change, 'maybe' + ) + self.wait(6) - def play_quantum_anims(self, word, to_fade): - pass ## def get_checkmark(self, word): checkmark = TexMobject("\\checkmark") checkmark.highlight(GREEN) - checkmark.scale(1.5) - checkmark.next_to(word, UP+RIGHT, buff = 0) + checkmark.scale(1.25) + checkmark.next_to(word[1], UP+RIGHT, buff = 0) return checkmark - - - - +class StartWithIntuition(TeacherStudentsScene): + def construct(self): + self.teacher_says( + "You already \\\\ have this \\\\ intuition", + bubble_kwargs = { + "height" : 3.5, + "width" : 3, + }, + ) + self.change_student_modes("pondering", "erm", "maybe") + self.look_at(VectorizedPoint(4*LEFT + 2*UP)) + self.wait(5) + +class TwoCarsAtRedLight(Scene): + CONFIG = { + "text_scale_val" : 0.75, + } + def construct(self): + self.pull_up_behind() + self.flash_in_sync_short_time() + self.show_low_confidence() + self.flash_in_sync_long_time() + self.show_high_confidence() + + def pull_up_behind(self): + #Setup Traffic light + traffic_light = TrafficLight() + traffic_light.move_to(6*RIGHT + 2.5*DOWN, DOWN) + source_point = VectorizedPoint( + traffic_light[2].get_right() + ) + screen = Line(ORIGIN, UP) + screen.next_to(source_point, RIGHT, LARGE_BUFF) + red_light = Spotlight( + color = RED, + source_point = source_point, + radius = 0.5, + screen = screen, + num_levels = 20, + opacity_function = lambda r : 1/(10*r**2+1) + ) + red_light.fade(0.5) + red_light.rotate(TAU/2, about_edge = LEFT) + self.add(red_light, traffic_light) + + #Setup cars + car1, car2 = cars = self.cars = VGroup(*[ + Car() for x in range(2) + ]) + cars.arrange_submobjects(RIGHT, buff = LARGE_BUFF) + cars.next_to( + traffic_light, LEFT, + buff = LARGE_BUFF, aligned_edge = DOWN + ) + car2.pi_creature.highlight(GREY_BROWN) + car1.start_point = car1.get_corner(DOWN+RIGHT) + car1.shift(SPACE_WIDTH*LEFT) + + #Pull up car + self.add(cars) + self.play( + SwitchOn( + red_light, + rate_func = squish_rate_func(smooth, 0, 0.3), + ), + Animation(traffic_light), + self.get_flashes(car2, num_flashes = 3), + MoveCar( + car1, car1.start_point, + run_time = 3, + rate_func = rush_from, + ) + ) + + def flash_in_sync_short_time(self): + car1, car2 = cars = self.cars + + #Setup axes + axes = Axes( + x_min = 0, + x_max = 5, + y_min = 0, + y_max = 2, + y_axis_config = { + "tick_frequency" : 0.5, + }, + ) + axes.x_axis.add_numbers(1, 2, 3) + time_label = TextMobject("Time") + time_label.scale(self.text_scale_val) + time_label.next_to(axes.x_axis.get_right(), DOWN) + y_title = TextMobject("Signal") + y_title.scale(self.text_scale_val) + y_title.next_to(axes.y_axis, UP, SMALL_BUFF) + axes.add(time_label, y_title) + axes.to_corner(UP+LEFT, buff = MED_SMALL_BUFF) + graph = axes.get_graph( + self.get_multispike_function(range(1, 4)), + x_min = 0.8, + x_max = 3.8, + ) + graph.highlight(YELLOW) + + #Label short duration + brace = Brace(Line( + axes.input_to_graph_point(1, graph), + axes.input_to_graph_point(3, graph), + ), UP) + text = TextMobject("Short duration observation") + text.scale(self.text_scale_val) + text.next_to(brace, UP, SMALL_BUFF) + text.align_to( + axes.coords_to_point(0.25, 0), LEFT + ) + + + self.play( + self.get_flashes(car1, num_flashes = 2), + self.get_flashes(car2, num_flashes = 2), + LaggedStart(FadeIn, VGroup( + axes, time_label, y_title, + )) + ) + self.play( + self.get_flashes(car1, num_flashes = 3), + self.get_flashes(car2, num_flashes = 3), + ShowCreation(graph, rate_func = None, run_time = 3) + ) + self.play( + self.get_flashes(car1, num_flashes = 10), + self.get_flashes(car2, num_flashes = 10, run_time_per_flash = 0.98), + GrowFromCenter(brace), + Write(text), + ) + + self.time_axes = axes + self.time_graph = graph + self.time_graph_label = VGroup( + brace, text + ) + + def show_low_confidence(self): + car1, car2 = cars = self.cars + time_axes = self.time_axes + + #Setup axes + frequency_axes = Axes( + x_min = 0, + x_max = 3, + y_min = 0, + y_max = 1.5, + y_axis_config = { + "tick_frequency" : 0.5, + } + ) + frequency_axes.next_to(time_axes, DOWN, LARGE_BUFF) + frequency_axes.highlight(LIGHT_GREY) + frequency_label = TextMobject("Frequency") + frequency_label.scale(self.text_scale_val) + frequency_label.next_to(frequency_axes.x_axis.get_right(), DOWN) + frequency_axes.add( + frequency_label, + VectorizedPoint(frequency_axes.y_axis.get_top()) + ) + frequency_axes.x_axis.add_numbers(1, 2) + frequency_graph = frequency_axes.get_graph( + lambda x : np.exp(-4*(x-1)**2), + x_min = 0, + x_max = 2, + ) + frequency_graph.highlight(RED) + peak_point = frequency_axes.input_to_graph_point( + 1, frequency_graph + ) + + #Setup label + label = TextMobject("Low confidence") + label.scale(self.text_scale_val) + label.move_to(peak_point + UP+RIGHT, DOWN) + label.match_color(frequency_graph) + arrow = Arrow(label.get_bottom(), peak_point, buff = 2*SMALL_BUFF) + arrow.match_color(frequency_graph) + + self.play( + ReplacementTransform( + self.time_axes.copy(), frequency_axes + ), + ReplacementTransform( + self.time_graph.copy(), frequency_graph + ), + ) + self.play( + Write(label), + GrowArrow(arrow) + ) + self.wait() + + self.frequency_axes = frequency_axes + self.frequency_graph = frequency_graph + self.frequency_graph_label = VGroup( + label, arrow + ) + + def flash_in_sync_long_time(self): + time_graph = self.time_graph + time_axes = self.time_axes + frequency_graph = self.frequency_graph + frequency_axes = self.frequency_axes + + n_spikes = 12 + new_time_graph = time_axes.get_graph( + self.get_multispike_function(range(1, n_spikes+1)), + x_min = 0.8, + x_max = n_spikes + 0.8, + ) + new_time_graph.match_color(time_graph) + + new_frequency_graph = frequency_axes.get_graph( + lambda x : np.exp(-500*(x-1)**2), + x_min = 0, + x_max = 2, + num_anchors = 500, + ) + new_frequency_graph.match_color(self.frequency_graph) + + def pin_freq_graph_end_points(freq_graph): + freq_graph.points[0] = frequency_axes.coords_to_point(0, 0) + freq_graph.points[-1] = frequency_axes.coords_to_point(2, 0) + + self.play(LaggedStart( + FadeOut, VGroup( + self.time_graph_label, + self.frequency_graph_label, + self.time_graph, + ) + )) + self.play( + ApplyMethod( + self.time_axes.x_axis.main_line.stretch, 2.5, 0, + {"about_edge" : LEFT}, + run_time = 4, + rate_func = squish_rate_func(smooth, 0.3, 0.6), + ), + UpdateFromFunc( + self.time_axes.x_axis.tip, + lambda m : m.move_to( + self.time_axes.x_axis.main_line.get_right(), + LEFT + ) + ), + ShowCreation( + new_time_graph, + run_time = n_spikes, + rate_func = None, + ), + ApplyMethod( + frequency_graph.stretch, 0.1, 0, + run_time = n_spikes, + ), + UpdateFromFunc(frequency_graph, pin_freq_graph_end_points), + *[ + self.get_flashes(car, num_flashes = n_spikes) + for car in self.cars + ] + ) + + self.new_time_graph = new_time_graph + self.new_frequency_graph = new_frequency_graph + + def show_high_confidence(self): + #Frequency stuff + arrow = self.frequency_graph_label[1] + label = TextMobject("High confidence") + label.scale(self.text_scale_val) + label.next_to(arrow.get_start(), UP, SMALL_BUFF) + label.match_color(arrow) + + frequency_axes = self.frequency_axes + + #Time stuff + new_time_graph = self.new_time_graph + brace = Brace(new_time_graph, UP, buff = SMALL_BUFF) + text = TextMobject("Long duration observation") + text.scale(self.text_scale_val) + text.next_to(brace, UP, buff = SMALL_BUFF) + + self.play( + FadeIn(label), + GrowArrow(arrow), + *map(self.get_flashes, self.cars) + ) + self.play( + GrowFromCenter(brace), + Write(text, run_time = 1), + *map(self.get_flashes, self.cars) + ) + self.play(*[ + self.get_flashes(car, num_flashes = 10) + for car in self.cars + ]) + + ### + + def get_flashes(self, car, colors = [YELLOW, RED], num_flashes = 1, **kwargs): + return AnimationGroup(*[ + MultipleFlashes(light, color, num_flashes = num_flashes, **kwargs) + for light, color in zip(car.get_lights(), colors) + ]) + + def get_multispike_function(self, spike_times): + return lambda x : sum([ + 1.25*np.exp(-100*(x-m)**2) + for m in spike_times + ]) + +class VariousMusicalNotes(Scene): + def construct(self): + freq = 20 + # x-coordinate of this point represents log(a) + # where the bell curve component of the signal + # is exp(-a*(x**2)) + graph_width_tracker = VectorizedPoint() + graph_width_tracker.move_to(np.log(2)*RIGHT) + def get_graph(): + a = np.exp(graph_width_tracker.get_center()[0]) + return FunctionGraph( + lambda x : np.exp(-a*x**2)*np.sin(freq*x)-0.5, + num_anchor_points = 500, + ) + graph = get_graph() + def graph_update(graph): + graph.points = get_graph().points + graph_update_anim = UpdateFromFunc(graph, graph_update) + def change_width_anim(width, **kwargs): + a = 2.0/(width**2) + return AnimationGroup( + ApplyMethod( + graph_width_tracker.move_to, + np.log(a)*RIGHT + ), + graph_update_anim, + **kwargs + ) + + phrases = [ + TextMobject(*words.split(" ")) + for words in [ + "Less clear frequency", + "Extremely unclear frequency", + "Very clear frequency", + ] + ] + + + #Show graphs and phrases + widths = [1, 0.2, SPACE_WIDTH] + for width, phrase in zip(widths, phrases): + brace = Brace(Line(LEFT, RIGHT), UP) + brace.stretch(width, 0) + brace.next_to(graph.get_center(), UP, buff = 1.2) + phrase.next_to(brace, UP) + + if width is widths[0]: + self.play(ShowCreation(graph, rate_func = None)), + self.play( + GrowFromCenter(brace), + Write(phrase, run_time = 1) + ) + else: + self.play( + change_width_anim(width), + ReplacementTransform( + VGroup(last_phrase, last_brace), + VGroup(phrase, brace), + rate_func = squish_rate_func(smooth, 0.5, 1), + ), + run_time = 2 + ) + self.wait() + # self.play(*map(FadeOut, [graph, brace, phrase])) + last_phrase = phrase + last_brace = brace + + #Talk about correlations + short_signal_words = TextMobject( + "Short", "signal", "correlates", + "with", "wide range", "of frequencies" + ) + long_signal_words = TextMobject( + "Only", "wide", "signals", "correlate", + "with a", "short range", "of frequencies" + ) + phrases = VGroup(short_signal_words, long_signal_words) + for phrase in phrases: + phrase.scale(0.8) + phrase.highlight_by_tex_to_color_map({ + "short" : RED, + "long" : GREEN, + "wide" : GREEN, + }, case_sensitive = False) + phrases.arrange_submobjects(DOWN) + phrases.to_edge(UP) + + long_graph = FunctionGraph( + lambda x : 0.5*np.sin(freq*x), + x_min = -2*SPACE_WIDTH, + x_max = 2*SPACE_WIDTH, + num_anchor_points = 1000 + ) + long_graph.highlight(BLUE) + long_graph.next_to(graph, UP, MED_LARGE_BUFF) + + self.play( + ShowCreation(long_graph), + *map(FadeOut, [last_brace, last_phrase]) + ) + self.play( + Write(short_signal_words), + change_width_anim(widths[1]) + ) + self.play( + long_graph.stretch, 0.35, 0, + long_graph.highlight, GREEN, + run_time = 5, + rate_func = wiggle + ) + self.wait() + self.play( + Write(long_signal_words), + change_width_anim(widths[2]), + ) + self.play( + long_graph.stretch, 0.95, 0, + long_graph.highlight, average_color(GREEN, BLUE), + run_time = 4, + rate_func = wiggle + ) + self.wait() + +class BringInFourierTranform(TeacherStudentsScene): + def construct(self): + fourier = TextMobject("Fourier") + fourier.scale(1.5) + fourier.next_to(self.teacher.get_corner(UP+LEFT), UP, LARGE_BUFF) + fourier.save_state() + fourier.shift(DOWN) + fourier.fade(1) + + self.play( + self.teacher.change, "raise_right_hand", + fourier.restore + ) + self.change_student_modes("happy", "erm", "confused") + self.look_at(3*LEFT + 2*UP) + self.wait(3) + +class LastVideoWrapper(Scene): + def construct(self): + title = TextMobject("Visualizing the Fourier Transform") + title.to_edge(UP) + screen_rect = ScreenRectangle(height = 6) + screen_rect.next_to(title, DOWN) + + self.add(title) + self.play(ShowCreation(screen_rect)) + self.wait() + +class FourierRecapScene(DrawFrequencyPlot): + CONFIG = { + "frequency_axes_config" : { + "x_max" : 10.0, + "x_axis_config" : { + "unit_size" : 0.7, + "numbers_to_show" : range(1, 10, 1), + } + }, + "initial_winding_frequency" : 0.1, + } + def construct(self): + self.setup_axes() + self.preview_fourier_plot() + self.wrap_signal_around_circle() + self.match_winding_to_beat_frequency() + self.follow_center_of_mass() + self.draw_fourier_plot() + self.highlight_spike() + + def setup_axes(self): + self.remove(self.pi_creature) + time_axes = self.get_time_axes() + time_axes.to_edge(UP, buff = MED_SMALL_BUFF) + time_axes.scale(0.9, about_edge = UP) + frequency_axes = self.get_frequency_axes() + circle_plane = self.get_circle_plane() + + self.add(time_axes) + + self.set_variables_as_attrs( + time_axes, frequency_axes, + circle_plane + ) + + def preview_fourier_plot(self): + time_graph = self.graph = self.get_time_graph( + width = 2, + num_graph_points = 200, + ) + fourier_graph = self.get_fourier_transform_graph( + time_graph + ) + fourier_graph.pointwise_become_partial(fourier_graph, 0.1, 1) + + #labels + signal_label = TextMobject("Signal") + fourier_label = TextMobject("Fourier transform") + signal_label.next_to(time_graph, UP, buff = SMALL_BUFF) + fourier_label.next_to(fourier_graph, UP) + fourier_label.match_color(fourier_graph) + + self.play( + ShowCreation(time_graph, run_time = 2), + Write(signal_label), + ) + self.wait() + self.play( + LaggedStart(FadeIn, self.frequency_axes), + ReplacementTransform( + time_graph.copy(), + fourier_graph, + run_time = 2 + ), + ReplacementTransform( + signal_label.copy(), + fourier_label, + run_time = 2, + rate_func = squish_rate_func(smooth, 0.5, 1) + ), + ) + self.wait() + self.play(LaggedStart( + Indicate, self.frequency_axes.x_axis.numbers, + run_time = 4, + rate_func = wiggle, + )) + self.wait() + self.play(*map(FadeOut, [ + self.frequency_axes, fourier_graph, + signal_label, fourier_label, + ])) + + self.time_graph = time_graph + self.set_variables_as_attrs(time_graph, fourier_label) + + def wrap_signal_around_circle(self): + time_graph = self.time_graph + circle_plane = self.circle_plane + freq = self.initial_winding_frequency + pol_graph = self.get_polarized_mobject(time_graph, freq) + winding_freq_label = self.get_winding_frequency_label() + winding_freq_label.add_to_back(BackgroundRectangle(winding_freq_label)) + winding_freq_label.move_to(circle_plane.get_top(), DOWN) + + self.add_foreground_mobjects(winding_freq_label) + self.play( + Write(circle_plane, run_time = 1), + ReplacementTransform( + time_graph.copy(), pol_graph, + path_arc = -TAU/4, + run_time_per_flash = 2, + run_time = 2, + ), + FadeIn(winding_freq_label), + ) + freq = 0.3 + self.change_frequency(freq, run_time = 2) + ghost_pol_graph = pol_graph.copy() + self.remove(pol_graph) + self.play(ghost_pol_graph.set_stroke, {"width" : 0.5}) + self.play( + *self.get_vector_animations(time_graph), + run_time = 15 + ) + self.remove(ghost_pol_graph) + self.wait() + + def match_winding_to_beat_frequency(self): + self.v_lines_indicating_periods = self.get_v_lines_indicating_periods(0.3) + self.add(self.v_lines_indicating_periods) + for freq in range(1, 6): + self.change_frequency(freq, run_time = 5) + self.play( + *self.get_vector_animations( + self.time_graph, + draw_polarized_graph = False + ), + rate_func = lambda t : 0.3*t, + run_time = 5 + ) + self.wait() + + def follow_center_of_mass(self): + com_dot = self.get_center_of_mass_dot() + self.generate_center_of_mass_dot_update_anim() + com_arrow = Arrow(UP+3*RIGHT, ORIGIN) + com_arrow.shift(com_dot.get_center()) + com_arrow.match_color(com_dot) + com_words = TextMobject("Center of mass") + com_words.next_to(com_arrow.get_start(), UP) + com_words.match_color(com_arrow) + com_words.add_background_rectangle() + + com_dot.save_state() + com_dot.move_to(com_arrow.get_start()) + com_dot.fade(1) + + self.play( + com_dot.restore, + GrowArrow(com_arrow, rate_func = squish_rate_func(smooth, 0.2, 1)), + Write(com_words), + ) + self.wait() + squished_func = squish_rate_func(smooth, 0, 0.2) + self.change_frequency( + 4, + added_anims = [ + FadeOut(com_arrow, rate_func = squished_func), + FadeOut(com_words, rate_func = squished_func), + ], + run_time = 5 + ) + + def draw_fourier_plot(self): + frequency_axes = self.frequency_axes + fourier_label = self.fourier_label + + self.change_frequency(0, run_time = 2) + self.play( + FadeIn(frequency_axes), + FadeIn(fourier_label), + ) + + fourier_graph = self.get_fourier_transform_graph(self.time_graph) + self.get_fourier_graph_drawing_update_anim(fourier_graph) + self.generate_fourier_dot_transform(fourier_graph) + + self.change_frequency(5, run_time = 20) + self.wait() + self.change_frequency(7.5, run_time = 10) + self.fourier_graph_drawing_update_anim = Animation(Mobject()) + self.fourier_graph = fourier_graph + + def highlight_spike(self): + spike_point = self.frequency_axes.input_to_graph_point( + 5, self.fourier_graph + ) + circle = Circle(color = YELLOW, radius = 0.25) + circle.move_to(spike_point) + circle.save_state() + circle.scale(5) + circle.fade(1) + + self.change_frequency(5) + self.play(circle.restore) + self.play(FadeOut(circle)) + self.wait() + for x in range(2): + self.change_frequency(5.2, run_time = 3) + self.change_frequency(4.8, run_time = 3) + self.change_frequency(5, run_time = 1.5) + self.wait() + + + ######### + + def get_time_graph(self, frequency = 5, width = 2, **kwargs): + # low_x = center-width/2 + # high_x = center+width/2 + # new_smooth = lambda x : np.clip(smooth((x+0.5)), 0, 1) + # def func(x): + # pure_signal = 0.9*np.cos(TAU*frequency*x) + # factor = new_smooth(x - low_x) - new_smooth(x-high_x) + # return 1 + factor*pure_signal + graph = self.time_axes.get_graph( + lambda x : 1+0.9*np.cos(TAU*frequency*x), + x_min = 0, x_max = width, + **kwargs + ) + graph.highlight(YELLOW) + return graph + +class CenterOfMassDescription(FourierRecapScene): + def construct(self): + self.remove(self.pi_creature) + circle_plane = self.get_circle_plane() + circle_plane.save_state() + circle_plane.generate_target() + circle_plane.target.scale_to_fit_height(2*SPACE_HEIGHT) + circle_plane.target.center() + circle_plane.target.axes.set_stroke(width = 2) + circle_plane.target.main_lines.set_stroke(width = 2) + circle_plane.target.secondary_lines.set_stroke(width = 1) + + start_coords = (0.5, 0.5) + alt_coords = (0.8, 0.8) + + com_dot = Dot(color = self.center_of_mass_color) + com_dot.move_to(circle_plane.coords_to_point(*start_coords)) + + self.add(circle_plane, com_dot) + self.wait() + self.play( + MoveToTarget(circle_plane), + com_dot.move_to, + circle_plane.target.coords_to_point(*start_coords) + ) + self.wait() + + alt_com_dot = com_dot.copy().move_to( + circle_plane.coords_to_point(*alt_coords) + ) + + for dot in com_dot, alt_com_dot: + line = Line(ORIGIN, dot.get_center()) + line.match_color(com_dot) + angle = line.get_angle() + line.rotate(-angle, about_point = ORIGIN) + brace = Brace(line, UP) + words = brace.get_text("Strength of frequency") + words.add_background_rectangle() + dot.length_label_group = VGroup(line, brace, words) + dot.length_label_group.rotate(angle, about_point = ORIGIN) + + line, brace, words = com_dot.length_label_group + self.play( + GrowFromCenter(line), + GrowFromCenter(brace), + FadeIn(words), + ) + self.wait() + self.play( + Transform( + com_dot.length_label_group, + alt_com_dot.length_label_group, + ), + Transform(com_dot, alt_com_dot), + rate_func = there_and_back, + run_time = 4, + ) + + #Do rotation + line = com_dot.length_label_group[0] + com_dot.length_label_group.remove(line) + angle = line.get_angle() + arc, alt_arc = [ + Arc( + start_angle = 0, + angle = factor*angle, + radius = 0.5, + ) + for factor in 1, 2 + ] + theta = TexMobject("\\theta") + theta.shift(1.5*arc.point_from_proportion(0.5)) + + self.play( + FadeOut(com_dot.length_label_group), + Animation(line), + ShowCreation(arc), + Write(theta) + ) + self.play( + Rotate( + VGroup(line, com_dot), + angle, about_point = ORIGIN + ), + Transform(arc, alt_arc), + theta.move_to, 1.5*alt_arc.point_from_proportion(0.5), + rate_func = there_and_back, + run_time = 4 + ) + self.wait() + +class AskAboutLongVsShort(TeacherStudentsScene): + def construct(self): + self.student_says( + "What happens if we \\\\ change the length of \\\\ the signal?", + student_index = 2, + ) + self.play( + self.teacher.change, "happy", + self.get_student_changes("pondering", "confused", "raise_right_hand") + ) + self.wait(5) + +class LongAndShortSignalsInWindingMachine(FourierRecapScene): + CONFIG = { + "num_fourier_graph_points" : 1000, + } + def construct(self): + self.setup_axes() + self.extend_for_long_time() + self.note_sharp_fourier_peak() + self.very_short_signal() + self.note_wide_fourier_peak() + + def setup_axes(self): + FourierRecapScene.setup_axes(self) + self.add(self.circle_plane) + self.add(self.frequency_axes) + self.time_graph = self.graph = self.get_time_graph(width = 2) + self.add(self.time_graph) + + self.force_skipping() + self.wrap_signal_around_circle() + + fourier_graph = self.get_fourier_transform_graph(self.time_graph) + self.fourier_graph = fourier_graph + self.add(fourier_graph) + self.change_frequency(5) + + self.revert_to_original_skipping_status() + + def extend_for_long_time(self): + short_time_graph = self.time_graph + long_time_graph = self.get_time_graph( + width = 10, + num_graph_points = 500, + ) + long_time_graph.set_stroke(width = 2) + new_freq = 5.1 + long_pol_graph = self.get_polarized_mobject( + long_time_graph, + freq = new_freq + ) + fourier_graph = self.fourier_graph + + self.change_frequency(new_freq) + self.play( + FadeOut(self.graph), + FadeOut(self.graph.polarized_mobject), + FadeOut(fourier_graph) + ) + self.play( + ShowCreation(long_time_graph, rate_func = None), + ShowCreation(long_pol_graph, rate_func = None), + run_time = 5 + ) + self.wait() + + self.time_graph = self.graph = long_time_graph + + def note_sharp_fourier_peak(self): + fourier_graph = self.get_fourier_transform_graph( + self.time_graph, + num_graph_points = self.num_fourier_graph_points + ) + self.fourier_graph = fourier_graph + self.note_fourier_peak(fourier_graph, 5, 5.1) + + def very_short_signal(self): + time_graph = self.time_graph + fourier_graph = self.fourier_graph + short_time_graph = self.get_time_graph(width = 0.6) + new_freq = 5.1 + short_pol_graph = self.get_polarized_mobject( + short_time_graph, + freq = new_freq + ) + + self.play( + FadeOut(fourier_graph), + FadeOut(time_graph), + FadeOut(time_graph.polarized_mobject), + ) + self.play( + ShowCreation(short_time_graph), + ShowCreation(short_time_graph.polarized_mobject), + ) + self.graph = self.time_graph = short_time_graph + self.change_frequency(6.66, run_time = 5) + + def note_wide_fourier_peak(self): + fourier_graph = self.get_fourier_transform_graph( + self.graph, + num_graph_points = self.num_fourier_graph_points + ) + self.fourier_graph = fourier_graph + self.note_fourier_peak(fourier_graph, 5, 6.66) + + + ### + + def note_fourier_peak(self, fourier_graph, freq1, freq2): + fourier_graph = self.fourier_graph + dots = self.get_fourier_graph_dots(fourier_graph, freq1, freq2) + self.get_center_of_mass_dot() + self.generate_center_of_mass_dot_update_anim() + self.generate_fourier_dot_transform(fourier_graph) + dot = self.fourier_graph_dot + arrow = Arrow(UP, ORIGIN, buff = SMALL_BUFF) + arrow.next_to(dot, UP, buff = SMALL_BUFF) + + self.play(ShowCreation(fourier_graph)) + self.change_frequency(freq1, + added_anims = [ + MaintainPositionRelativeTo(arrow, dot), + UpdateFromAlphaFunc( + arrow, + lambda m, a : m.set_fill(opacity = a) + ), + ], + run_time = 3, + ) + self.wait() + self.change_frequency(freq2, + added_anims = [ + MaintainPositionRelativeTo(arrow, dot) + ], + run_time = 3 + ) + self.wait() + self.play(*map(FadeOut, [ + dot, arrow, self.center_of_mass_dot + ])) + #This is not great... + for attr in "center_of_mass_dot", "fourier_graph_dot": + self.__dict__.pop(attr) + + + def get_fourier_graph_dots(self, fourier_graph, *freqs): + axis_point = self.frequency_axes.coords_to_point(4.5, -0.25) + dots = VGroup() + for freq in freqs: + point = self.frequency_axes.input_to_graph_point(freq, fourier_graph) + dot = Dot(point) + dot.scale(0.5) + dots.add(dot) + vect = point - axis_point + vect *= 1.3/np.linalg.norm(vect) + arrow = Arrow(vect, ORIGIN, buff = SMALL_BUFF) + arrow.highlight(YELLOW) + arrow.shift(point) + dot.arrow = arrow + return dots + +class CleanerFourierTradeoff(FourierTradeoff): + CONFIG = { + "show_text" : False, + "complex_to_real_func" : lambda z : z.real, + } + +class MentionDopplerRadar(TeacherStudentsScene): + def construct(self): + words = TextMobject("Doppler Radar") + words.next_to(self.teacher, UP) + words.save_state() + words.shift(DOWN).fade(1) + dish = RadarDish() + dish.next_to(self.students, UP, buff = 2, aligned_edge = LEFT) + plane = Plane() + plane.to_edge(RIGHT) + plane.align_to(dish) + plane_flight = AmbientMovement( + plane, + direction = LEFT, + rate = 1, + ) + plane.flip() + pulse = RadarPulse(dish, plane) + look_at_anims = [ + ContinualUpdateFromFunc( + pi, lambda pi : pi.look_at(pulse.mobject) + ) + for pi in self.get_pi_creatures() + ] + + self.add(dish, plane_flight, pulse, *look_at_anims) + self.play( + self.teacher.change, "hooray", + words.restore + ) + self.change_student_modes("pondering", "erm", "sassy") + self.wait(2) + self.play( + self.teacher.change, "happy", + self.get_student_changes(*["thinking"]*3) + ) + self.wait() + dish.set_stroke(width = 0) + self.play(UpdateFromAlphaFunc( + VGroup(plane, dish), + lambda m, a : m.set_fill(opacity = 1 - a) + )) + +class IntroduceDopplerRadar(Scene): + def construct(self): + self.setup_axes() + self.measure_distance_with_time() + self.measure_velocity_with_frequency() + + def setup_axes(self): + self.dish = RadarDish() + self.dish.to_corner(UP+LEFT) + axes = Axes( + x_min = 0, + x_max = 10, + y_min = -2, + y_max = 2 + ) + axes.move_to(DOWN) + time_label = TextMobject("Time") + time_label.next_to(axes.x_axis.get_right(), UP) + axes.time_label = time_label + axes.add(time_label) + self.axes = axes + + self.add(self.dish) + self.add(axes) + + def measure_distance_with_time(self): + dish = self.dish + axes = self.axes + distance = 5 + time_diff = 5 + speed = (2*distance)/time_diff + randy = Randolph().flip() + randy.match_height(dish) + randy.move_to(dish.get_right(), LEFT) + randy.shift(distance*RIGHT) + + pulse_graph = self.get_single_pulse_graph(1, color = BLUE) + echo_graph = self.get_single_pulse_graph(1+time_diff, color = YELLOW) + sum_graph = axes.get_graph( + lambda x : sum([ + pulse_graph.underlying_function(x), + echo_graph.underlying_function(x), + ]), + color = WHITE + ) + sum_graph.background_image_file = "blue_yellow_gradient" + words = ["Original signal", "Echo"] + for graph, word in zip([pulse_graph, echo_graph], words): + arrow = Vector(DOWN+LEFT) + arrow.next_to(graph.peak_point, UP+RIGHT, SMALL_BUFF) + arrow.match_color(graph) + graph.arrow = arrow + label = TextMobject(word) + label.next_to(arrow.get_start(), UP, SMALL_BUFF) + label.match_color(graph) + graph.label = label + + #v_line anim? + + pulse = RadarPulseSingleton(dish, randy, speed = speed) + graph_draw = NormalAnimationAsContinualAnimation( + ShowCreation( + sum_graph, + rate_func = None, + run_time = axes.x_max + ) + ) + randy_look_at = ContinualUpdateFromFunc( + randy, lambda pi : pi.look_at(pulse.mobject) + ) + + self.add(randy_look_at, ContinualAnimation(axes), graph_draw) + self.wait() + self.add(pulse) + self.play( + Write(pulse_graph.label), + GrowArrow(pulse_graph.arrow), + run_time = 1, + ) + self.play(randy.change, "pondering") + self.wait(time_diff - 2) + self.play( + Write(echo_graph.label), + GrowArrow(echo_graph.arrow), + run_time = 1 + ) + self.wait(3) + graph_draw.update(10) + self.add(sum_graph) + self.wait() + + + + def measure_velocity_with_frequency(self): + pass + + + ### + + def get_single_pulse_graph(self, x, **kwargs): + graph = self.axes.get_graph( + self.get_single_pulse_function(x), + **kwargs + ) + graph.peak_point = self.get_peak_point(graph) + return graph + + def get_single_pulse_function(self, x): + return lambda t : -2*np.sin(10*(t-x))*np.exp(-100*(t-x)**2) + + def get_peak_point(self, graph): + anchors = graph.get_anchors() + return anchors[np.argmax([p[1] for p in anchors])] diff --git a/camera/camera.py b/camera/camera.py index 7c6d5477..1217f86f 100644 --- a/camera/camera.py +++ b/camera/camera.py @@ -30,7 +30,7 @@ class Camera(object): "max_allowable_norm" : 2*SPACE_WIDTH, "image_mode" : "RGBA", "n_rgb_coords" : 4, - "background_alpha" : 0, #Out of color_max_val + "background_alpha" : 0, #Out of rgb_max_val "pixel_array_dtype" : 'uint8', "use_z_coordinate_for_display_order" : False, # z_buff_func is only used if the flag above is set to True. @@ -40,7 +40,7 @@ class Camera(object): def __init__(self, background = None, **kwargs): digest_config(self, kwargs, locals()) - self.color_max_val = np.iinfo(self.pixel_array_dtype).max + self.rgb_max_val = np.iinfo(self.pixel_array_dtype).max self.init_background() self.resize_space_shape() self.reset() @@ -92,7 +92,7 @@ class Camera(object): retval = np.array(pixel_array) if convert_from_floats: retval = np.apply_along_axis( - lambda f : (f * self.color_max_val).astype(self.pixel_array_dtype), + lambda f : (f * self.rgb_max_val).astype(self.pixel_array_dtype), 2, retval) return retval @@ -177,29 +177,30 @@ class Camera(object): def capture_mobjects(self, mobjects, **kwargs): mobjects = self.get_mobjects_to_display(mobjects, **kwargs) - vmobjects = [] - for mobject in mobjects: - if isinstance(mobject, VMobject): - vmobjects.append(mobject) - elif len(vmobjects) > 0: - self.display_multiple_vectorized_mobjects(vmobjects) - vmobjects = [] - - if isinstance(mobject, PMobject): - self.display_point_cloud( - mobject.points, mobject.rgbas, - self.adjusted_thickness(mobject.stroke_width) - ) - elif isinstance(mobject, ImageMobject): - self.display_image_mobject(mobject) - elif isinstance(mobject, Mobject): - pass #Remainder of loop will handle submobjects - else: - raise Exception( - "Unknown mobject type: " + mobject.__class__.__name__ - ) - #TODO, more? Call out if it's unknown? - self.display_multiple_vectorized_mobjects(vmobjects) + + # Organize this list into batches of the same type, and + # apply corresponding function to those batches + type_func_pairs = [ + (VMobject, self.display_multiple_vectorized_mobjects), + (PMobject, self.display_multiple_point_cloud_mobjects), + (ImageMobject, self.display_multiple_image_mobjects), + (Mobject, lambda batch : batch), #Do nothing + ] + def get_mobject_type(mobject): + for mobject_type, func in type_func_pairs: + if isinstance(mobject, mobject_type): + return mobject_type + raise Exception( + "Trying to display something which is not of type Mobject" + ) + batch_type_pairs = batch_by_property(mobjects, get_mobject_type) + + #Display in these batches + for batch, batch_type in batch_type_pairs: + #check what the type is, and call the appropriate function + for mobject_type, func in type_func_pairs: + if batch_type == mobject_type: + func(batch) ## Methods associated with svg rendering @@ -215,12 +216,12 @@ class Camera(object): def display_multiple_vectorized_mobjects(self, vmobjects): if len(vmobjects) == 0: return - batches = batch_by_property( + batch_file_pairs = batch_by_property( vmobjects, lambda vm : vm.get_background_image_file() ) - for batch in batches: - if batch[0].get_background_image_file(): + for batch, file_name in batch_file_pairs: + if file_name: self.display_multiple_background_colored_vmobject(batch) else: self.display_multiple_non_background_colored_vmobjects(batch) @@ -252,7 +253,7 @@ class Camera(object): stroke_hex = rgb_to_hex(stroke_rgb) pen = aggdraw.Pen(stroke_hex, stroke_width) - fill_opacity = int(self.color_max_val*vmobject.get_fill_opacity()) + fill_opacity = int(self.rgb_max_val*vmobject.get_fill_opacity()) if fill_opacity == 0: fill = None else: @@ -305,13 +306,19 @@ class Camera(object): def display_multiple_background_colored_vmobject(self, cvmobjects): displayer = self.get_background_colored_vmobject_displayer() cvmobject_pixel_array = displayer.display(*cvmobjects) - self.pixel_array[:,:] = np.maximum( - self.pixel_array, cvmobject_pixel_array - ) + self.overlay_rgba_array(cvmobject_pixel_array) return self ## Methods for other rendering + def display_multiple_point_cloud_mobjects(self, pmobjects): + for pmobject in pmobjects: + self.display_point_cloud( + pmobject.points, + pmobject.rgbas, + self.adjusted_thickness(pmobject.stroke_width) + ) + def display_point_cloud(self, points, rgbas, thickness): if len(points) == 0: return @@ -322,7 +329,7 @@ class Camera(object): ) rgba_len = self.pixel_array.shape[2] - rgbas = (self.color_max_val*rgbas).astype(self.pixel_array_dtype) + rgbas = (self.rgb_max_val*rgbas).astype(self.pixel_array_dtype) target_len = len(pixel_coords) factor = target_len/len(rgbas) rgbas = np.array([rgbas]*factor).reshape((target_len, rgba_len)) @@ -342,6 +349,10 @@ class Camera(object): new_pa[indices] = rgbas self.pixel_array = new_pa.reshape((ph, pw, rgba_len)) + def display_multiple_image_mobjects(self, image_mobjects): + for image_mobject in image_mobjects: + self.display_image_mobject(image_mobject) + def display_image_mobject(self, image_mobject): corner_coords = self.points_to_pixel_coords(image_mobject.points) ul_coords, ur_coords, dl_coords = corner_coords @@ -410,17 +421,23 @@ class Camera(object): self.overlay_rgba_array(image) def overlay_rgba_array(self, arr): - # """ Overlays arr onto self.pixel_array with relevant alphas""" - bg, fg = fdiv(self.pixel_array, self.color_max_val), fdiv(arr, self.color_max_val) - bga, fga = [arr[:,:,3:] for arr in bg, fg] - alpha_sum = fga + (1-fga)*bga - with np.errstate(divide = 'ignore', invalid='ignore'): - bg[:,:,:3] = reduce(op.add, [ - np.divide(fg[:,:,:3]*fga, alpha_sum), - np.divide(bg[:,:,:3]*(1-fga)*bga, alpha_sum), - ]) - bg[:,:,3:] = 1 - (1 - bga)*(1 - fga) - self.pixel_array = (self.color_max_val*bg).astype(self.pixel_array_dtype) + fg = arr + bg = self.pixel_array + # rgba_max_val = self.rgb_max_val + src_rgb, src_a, dst_rgb, dst_a = [ + a.astype(np.float32)/self.rgb_max_val + for a in fg[...,:3], fg[...,3], bg[...,:3], bg[...,3] + ] + + out_a = src_a + dst_a*(1.0-src_a) + out_rgb = fdiv( + src_rgb*src_a[..., None] + \ + dst_rgb*dst_a[..., None]*(1.0-src_a[..., None]), + out_a[..., None] + ) + + self.pixel_array[..., :3] = out_rgb*self.rgb_max_val + self.pixel_array[..., 3] = out_a*self.rgb_max_val def align_points_to_camera(self, points): ## This is where projection should live @@ -537,8 +554,7 @@ class BackgroundColoredVMobjectDisplayer(object): mode = "RGBA" if pixel_array.shape[2] == 4 else "RGB" return self.resize_background_array(background_array, width, height, mode) - def get_background_array(self, cvmobject): - file_name = cvmobject.get_background_image_file() + def get_background_array(self, file_name): if file_name in self.file_name_to_pixel_array_map: return self.file_name_to_pixel_array_map[file_name] full_path = get_full_raster_image_path(file_name) @@ -553,12 +569,12 @@ class BackgroundColoredVMobjectDisplayer(object): return array def display(self, *cvmobjects): - batches = batch_by_property( + batch_image_file_pairs = batch_by_property( cvmobjects, lambda cv : cv.get_background_image_file() ) curr_array = None - for batch in batches: - background_array = self.get_background_array(batch[0]) + for batch, image_file in batch_image_file_pairs: + background_array = self.get_background_array(image_file) for cvmobject in batch: self.camera.display_vectorized(cvmobject, self.canvas) self.canvas.flush() diff --git a/helpers.py b/helpers.py index 80ee9e33..848f0adc 100644 --- a/helpers.py +++ b/helpers.py @@ -227,22 +227,30 @@ def adjacent_pairs(objects): return zip(objects, list(objects[1:])+[objects[0]]) def batch_by_property(items, property_func): - batches = [] - def add_batch(batch): + """ + Takes in a list, and returns a list of tuples, (batch, prop) + such that all items in a batch have the same output when + put into property_func, and such that chaining all these + batches together would give the original list. + """ + batch_prop_pairs = [] + def add_batch_prop_pair(batch): if len(batch) > 0: - batches.append(batch) + batch_prop_pairs.append( + (batch, property_func(batch[0])) + ) curr_batch = [] curr_prop = None for item in items: prop = property_func(item) if prop != curr_prop: - add_batch(curr_batch) + add_batch_prop_pair(curr_batch) curr_prop = prop curr_batch = [item] else: curr_batch.append(item) - add_batch(curr_batch) - return batches + add_batch_prop_pair(curr_batch) + return batch_prop_pairs def complex_to_R3(complex_num): return np.array((complex_num.real, complex_num.imag, 0)) diff --git a/mobject/tex_mobject.py b/mobject/tex_mobject.py index 8401f5b7..650362f0 100644 --- a/mobject/tex_mobject.py +++ b/mobject/tex_mobject.py @@ -141,9 +141,15 @@ class TexMobject(SVGMobject): self.submobjects = new_submobjects return self - def get_parts_by_tex(self, tex, substring = True): + def get_parts_by_tex(self, tex, substring = True, case_sensitive = True): def test(tex1, tex2): - return tex1 == tex2 or (substring and tex1 in tex2) + if not case_sensitive: + tex1 = tex1.lower() + tex2 = tex2.lower() + if substring: + return tex1 in tex2 + else: + return tex1 == tex2 tex_submobjects = filter( lambda m : isinstance(m, TexMobject), diff --git a/mobject/vectorized_mobject.py b/mobject/vectorized_mobject.py index e732cc5b..a3ac3372 100644 --- a/mobject/vectorized_mobject.py +++ b/mobject/vectorized_mobject.py @@ -122,7 +122,7 @@ class VMobject(Mobject): return self def get_fill_rgb(self): - return self.fill_rgb + return np.clip(self.fill_rgb, 0, 1) def get_fill_color(self): try: @@ -135,7 +135,7 @@ class VMobject(Mobject): return np.clip(self.fill_opacity, 0, 1) def get_stroke_rgb(self): - return self.stroke_rgb + return np.clip(self.stroke_rgb, 0, 1) def get_stroke_color(self): try: diff --git a/old_projects/eoc/chapter2.py b/old_projects/eoc/chapter2.py index 0202b9e9..385a3d56 100644 --- a/old_projects/eoc/chapter2.py +++ b/old_projects/eoc/chapter2.py @@ -35,90 +35,6 @@ VELOCITY_COLOR = GREEN #### Warning, scenes here not updated based on most recent GraphScene changes ####### -class Car(SVGMobject): - CONFIG = { - "file_name" : "Car", - "height" : 1, - "color" : "#BBBBBB", - } - def __init__(self, **kwargs): - SVGMobject.__init__(self, **kwargs) - self.scale_to_fit_height(self.height) - self.set_stroke(color = WHITE, width = 0) - self.set_fill(self.color, opacity = 1) - - randy = Randolph(mode = "happy") - randy.scale_to_fit_height(0.6*self.get_height()) - randy.stretch(0.8, 0) - randy.look(RIGHT) - randy.move_to(self) - randy.shift(0.07*self.height*(RIGHT+UP)) - self.randy = randy - self.add_to_back(randy) - - orientation_line = Line(self.get_left(), self.get_right()) - orientation_line.set_stroke(width = 0) - self.add(orientation_line) - self.orientation_line = orientation_line - - self.add_treds_to_tires() - - def move_to(self, point_or_mobject): - vect = rotate_vector( - UP+LEFT, self.orientation_line.get_angle() - ) - self.next_to(point_or_mobject, vect, buff = 0) - return self - - def get_front_line(self): - return DashedLine( - self.get_corner(UP+RIGHT), - self.get_corner(DOWN+RIGHT), - color = DISTANCE_COLOR, - dashed_segment_length = 0.05, - ) - - def add_treds_to_tires(self): - for tire in self.get_tires(): - radius = tire.get_width()/2 - center = tire.get_center() - tred = Line( - 0.9*radius*RIGHT, 1.4*radius*RIGHT, - stroke_width = 2, - color = BLACK - ) - tred.rotate_in_place(np.pi/4) - for theta in np.arange(0, 2*np.pi, np.pi/4): - new_tred = tred.copy() - new_tred.rotate(theta) - new_tred.shift(center) - tire.add(new_tred) - return self - - def get_tires(self): - return VGroup(self[1][1], self[1][3]) - -class MoveCar(ApplyMethod): - CONFIG = { - "moving_forward" : True, - } - def __init__(self, car, target_point, **kwargs): - ApplyMethod.__init__(self, car.move_to, target_point, **kwargs) - displacement = self.target_mobject.get_right()-self.starting_mobject.get_right() - distance = np.linalg.norm(displacement) - if not self.moving_forward: - distance *= -1 - tire_radius = car.get_tires()[0].get_width()/2 - self.total_tire_radians = -distance/tire_radius - - def update_mobject(self, alpha): - ApplyMethod.update_mobject(self, alpha) - if alpha == 0: - return - radians = alpha*self.total_tire_radians - for tire in self.mobject.get_tires(): - tire.rotate_in_place(radians) - class IncrementNumber(Succession): CONFIG = { "start_num" : 0, diff --git a/topics/number_line.py b/topics/number_line.py index 30ad693a..9d02286c 100644 --- a/topics/number_line.py +++ b/topics/number_line.py @@ -219,7 +219,29 @@ class Axes(VGroup): return graph def input_to_graph_point(self, x, graph): - return self.coords_to_point(x, graph.underlying_function(x)) + if hasattr(graph, "underlying_function"): + return self.coords_to_point(x, graph.underlying_function(x)) + else: + #binary search + lh, rh = 0, 1 + while abs(lh - rh) > 0.001: + mh = np.mean([lh, rh]) + hands = [lh, mh, rh] + points = map(graph.point_from_proportion, hands) + lx, mx, rx = map(self.x_axis.point_to_number, points) + if lx <= x and rx >= x: + if mx > x: + rh = mh + else: + lh = mh + elif lx <= x and rx <= x: + return points[2] + elif lx >= x and rx >= x: + return points[0] + elif lx > x and rx < x: + lh, rh = rh, lh + return points[1] + class ThreeDAxes(Axes): CONFIG = { diff --git a/topics/objects.py b/topics/objects.py index 2ef461fe..af9607c6 100644 --- a/topics/objects.py +++ b/topics/objects.py @@ -486,6 +486,104 @@ class ThoughtBubble(Bubble): self.submobjects[-1].set_fill(GREEN_SCREEN, opacity = 1) return self +class Car(SVGMobject): + CONFIG = { + "file_name" : "Car", + "height" : 1, + "color" : LIGHT_GREY, + "light_colors" : [BLACK, BLACK], + } + def __init__(self, **kwargs): + SVGMobject.__init__(self, **kwargs) + self.scale_to_fit_height(self.height) + self.set_stroke(color = WHITE, width = 0) + self.set_fill(self.color, opacity = 1) + + from topics.characters import Randolph + randy = Randolph(mode = "happy") + randy.scale_to_fit_height(0.6*self.get_height()) + randy.stretch(0.8, 0) + randy.look(RIGHT) + randy.move_to(self) + randy.shift(0.07*self.height*(RIGHT+UP)) + self.randy = self.pi_creature = randy + self.add_to_back(randy) + + orientation_line = Line(self.get_left(), self.get_right()) + orientation_line.set_stroke(width = 0) + self.add(orientation_line) + self.orientation_line = orientation_line + + for light, color in zip(self.get_lights(), self.light_colors): + light.set_fill(color, 1) + light.is_subpath = False + + self.add_treds_to_tires() + + def move_to(self, point_or_mobject): + vect = rotate_vector( + UP+LEFT, self.orientation_line.get_angle() + ) + self.next_to(point_or_mobject, vect, buff = 0) + return self + + def get_front_line(self): + return DashedLine( + self.get_corner(UP+RIGHT), + self.get_corner(DOWN+RIGHT), + color = DISTANCE_COLOR, + dashed_segment_length = 0.05, + ) + + def add_treds_to_tires(self): + for tire in self.get_tires(): + radius = tire.get_width()/2 + center = tire.get_center() + tred = Line( + 0.9*radius*RIGHT, 1.4*radius*RIGHT, + stroke_width = 2, + color = BLACK + ) + tred.rotate_in_place(np.pi/4) + for theta in np.arange(0, 2*np.pi, np.pi/4): + new_tred = tred.copy() + new_tred.rotate(theta, about_point = ORIGIN) + new_tred.shift(center) + tire.add(new_tred) + return self + + def get_tires(self): + return VGroup(self[1][1], self[1][3]) + + def get_lights(self): + return VGroup(self.get_front_light(), self.get_rear_light()) + + def get_front_light(self): + return self[1][5] + + def get_rear_light(self): + return self[1][8] + +class MoveCar(ApplyMethod): + CONFIG = { + "moving_forward" : True, + } + def __init__(self, car, target_point, **kwargs): + ApplyMethod.__init__(self, car.move_to, target_point, **kwargs) + displacement = self.target_mobject.get_right()-self.starting_mobject.get_right() + distance = np.linalg.norm(displacement) + if not self.moving_forward: + distance *= -1 + tire_radius = car.get_tires()[0].get_width()/2 + self.total_tire_radians = -distance/tire_radius + + def update_mobject(self, alpha): + ApplyMethod.update_mobject(self, alpha) + if alpha == 0: + return + radians = alpha*self.total_tire_radians + for tire in self.mobject.get_tires(): + tire.rotate_in_place(radians) #TODO: Where should this live? class Broadcast(LaggedStart):