diff --git a/active_projects/basel.py b/active_projects/basel.py index 909fd1cc..a29a14bb 100644 --- a/active_projects/basel.py +++ b/active_projects/basel.py @@ -30,9 +30,9 @@ from mobject.vectorized_mobject import * ## To watch one of these scenes, run the following: ## python extract_scene.py -p file_name -inverse_power_law = lambda maxint,cutoff,exponent: \ - (lambda r: maxint * (cutoff/(r+cutoff))**exponent) -inverse_quadratic = lambda maxint,cutoff: inverse_power_law(maxint,cutoff,2) +inverse_power_law = lambda maxint,scale,cutoff,exponent: \ + (lambda r: maxint * (cutoff/(r/scale+cutoff))**exponent) +inverse_quadratic = lambda maxint,scale,cutoff: inverse_power_law(maxint,scale,cutoff,2) LIGHT_COLOR = YELLOW INDICATOR_RADIUS = 0.7 @@ -44,15 +44,15 @@ FAST_INDICATOR_UPDATE_TIME = 0.1 OPACITY_FOR_UNIT_INTENSITY = 0.2 SWITCH_ON_RUN_TIME = 2.5 FAST_SWITCH_ON_RUN_TIME = 0.1 -LIGHT_CONE_NUM_SECTORS = 30 +NUM_LEVELS = 30 NUM_CONES = 50 # in first lighthouse scene NUM_VISIBLE_CONES = 5 # ibidem ARC_TIP_LENGTH = 0.2 - - -def show_line_length(line): - v = line.points[1] - line.points[0] - print v[0]**2 + v[1]**2 +AMBIENT_FULL = 1.0 +AMBIENT_DIMMED = 0.2 +LIGHT_COLOR = YELLOW +DEGREES = TAU/360 +SWITCH_ON_RUN_TIME = 1.5 class AngleUpdater(ContinualAnimation): @@ -66,7 +66,7 @@ class AngleUpdater(ContinualAnimation): def update_mobject(self, dt): # angle arc new_arc = self.angle_arc.copy().set_bound_angles( - start = self.lc.start_angle, + start = self.lc.start_angle(), stop = self.lc.stop_angle() ) new_arc.generate_points() @@ -77,171 +77,213 @@ class AngleUpdater(ContinualAnimation): -class LightScreen(VMobject): - # A light screen is composed of a VMobject and a light cone. - # It has knowledge of the light source point. - # As the screen changes, it calculates the viewing angle from - # the source and updates the light cone. - def __init__(self, light_source = ORIGIN, screen = None, light_cone = None): - Mobject.__init__(self) - self.light_cone = light_cone - self.light_source = light_source - self.screen = screen - self.light_cone.move_source_to(self.light_source) - self.shadow = VMobject(fill_color = BLACK, stroke_width = 0, fill_opacity = 1.0) - self.add(self.light_cone, self.screen, self.shadow) - self.update_shadow(self.shadow) - def update_light_cone(self,lc): - lower_angle, upper_angle = self.viewing_angles() - self.light_cone.update_opening(start_angle = lower_angle, - stop_angle = upper_angle) - return self + + + + + + + + + + + +class AmbientLight(VMobject): + + # Parameters are: + # * a source point + # * an opacity function + # * a light color + # * a max opacity + # * a radius (larger than the opacity's dropoff length) + # * the number of subdivisions (levels, annuli) + + CONFIG = { + "source_point" : ORIGIN, + "opacity_function" : lambda r : 1.0/(r+1.0)**2, + "color" : LIGHT_COLOR, + "max_opacity" : 1.0, + "num_levels" : 10, + "radius" : 5.0 + } + + def generate_points(self): + + # in theory, this method is only called once, right? + # so removing submobs shd not be necessary + for submob in self.submobjects: + self.remove(submob) + + # create annuli + self.radius = float(self.radius) + dr = self.radius / self.num_levels + for r in np.arange(0, self.radius, dr): + alpha = self.max_opacity * self.opacity_function(r) + annulus = Annulus( + inner_radius = r, + outer_radius = r + dr, + color = self.color, + fill_opacity = alpha + ) + annulus.move_arc_center_to(self.source_point) + self.add(annulus) + + + + def move_source_to(self,point): + self.shift(point - self.source_point) + self.source_point = np.array(point) + # for submob in self.submobjects: + # if type(submob) == Annulus: + # submob.shift(self.source_point - submob.get_center()) + + def dimming(self,new_alpha): + old_alpha = self.max_opacity + self.max_opacity = new_alpha + for submob in self.submobjects: + old_submob_alpha = submob.fill_opacity + new_submob_alpha = old_submob_alpha * new_alpha/old_alpha + submob.set_fill(opacity = new_submob_alpha) + + +class Spotlight(VMobject): + + CONFIG = { + "source_point" : ORIGIN, + "opacity_function" : lambda r : 1.0/(r/2+1.0)**2, + "color" : LIGHT_COLOR, + "max_opacity" : 1.0, + "num_levels" : 10, + "radius" : 5.0, + "screen" : None, + "shadow" : VMobject(fill_color = BLACK, stroke_width = 0, fill_opacity = 1.0) + } + + def track_screen(self): + self.generate_points() + + def generate_points(self): + + for submob in self.submobjects: + self.remove(submob) + + if self.screen != None: + # look for the screen and create annular sectors + lower_angle, upper_angle = self.viewing_angles(self.screen) + self.radius = float(self.radius) + dr = self.radius / self.num_levels + for r in np.arange(0, self.radius, dr): + alpha = self.max_opacity * self.opacity_function(r) + annular_sector = AnnularSector( + inner_radius = r, + outer_radius = r + dr, + color = self.color, + fill_opacity = alpha, + start_angle = lower_angle, + angle = upper_angle - lower_angle + ) + annular_sector.move_arc_center_to(self.source_point) + self.add(annular_sector) + + self.update_shadow(point = self.source_point) + self.add(self.shadow) + def viewing_angle_of_point(self,point): - distance_vector = point - self.light_source + distance_vector = point - self.source_point angle = angle_of_vector(distance_vector) return angle - def viewing_angles(self): - all_points = [] + def viewing_angles(self,screen): - for submob in self.family_members_with_points(): - all_points.extend(submob.get_anchors()) - - viewing_angles = np.array(map(self.viewing_angle_of_point, self.screen.get_anchors())) - - if len(viewing_angles) == 0: - lower_angle = upper_angle = 0 - else: + viewing_angles = np.array(map(self.viewing_angle_of_point, + screen.get_anchors())) + lower_angle = upper_angle = 0 + if len(viewing_angles) != 0: lower_angle = np.min(viewing_angles) upper_angle = np.max(viewing_angles) - + return lower_angle, upper_angle - def update_shadow(self,sh): + def opening_angle(self): + l,u = self.viewing_angles(self.screen) + return u - l + + def start_angle(self): + l,u = self.viewing_angles(self.screen) + return l + + def stop_angle(self): + l,u = self.viewing_angles(self.screen) + return u + + def move_source_to(self,point): + self.source_point = np.array(point) + self.recalculate_sectors(point = point, screen = self.screen) + self.update_shadow(point = point) + + + def recalculate_sectors(self, point = ORIGIN, screen = None): + if screen == None: + return + for submob in self.submobject_family(): + if type(submob) == AnnularSector: + lower_angle, upper_angle = self.viewing_angles(screen) + new_submob = AnnularSector( + start_angle = lower_angle, + angle = upper_angle - lower_angle, + inner_radius = submob.inner_radius, + outer_radius = submob.outer_radius + ) + new_submob.move_arc_center_to(point) + submob.points = new_submob.points + + def update_shadow(self,point = ORIGIN): + use_point = point #self.source_point self.shadow.points = self.screen.points - ray1 = self.screen.points[0] - self.light_source - ray2 = self.screen.points[-1] - self.light_source + ray1 = self.screen.points[0] - use_point + ray2 = self.screen.points[-1] - use_point ray1 = ray1/np.linalg.norm(ray1) * 100 - ray1 = rotate_vector(ray1,-TAU/16) + ray1 = rotate_vector(ray1,-TAU/100) ray2 = ray2/np.linalg.norm(ray2) * 100 - ray2 = rotate_vector(ray2,TAU/16) + ray2 = rotate_vector(ray2,TAU/100) outpoint1 = self.screen.points[0] + ray1 outpoint2 = self.screen.points[-1] + ray2 self.shadow.add_control_points([outpoint2,outpoint1,self.screen.points[0]]) self.shadow.mark_paths_closed = True -class LightCone(VGroup): - CONFIG = { - "start_angle": 0, - "angle" : TAU/8, - "radius" : 10, - "brightness" : 1, - "opacity_function" : lambda r : 1./max(r, 0.01), - "num_sectors" : 10, - "color": LIGHT_COLOR, - } + def dimming(self,new_alpha): + old_alpha = self.max_opacity + self.max_opacity = new_alpha + for submob in self.submobjects: + if type(submob) != AnnularSector: + # it's the shadow, don't dim it + continue + old_submob_alpha = submob.fill_opacity + new_submob_alpha = old_submob_alpha * new_alpha/old_alpha + submob.set_fill(opacity = new_submob_alpha) - def generate_points(self): - radii = np.linspace(0, self.radius, self.num_sectors+1) - sectors = [ - AnnularSector( - start_angle = self.start_angle, - angle = self.angle, - inner_radius = r1, - outer_radius = r2, - stroke_width = 0, - stroke_color = self.color, - fill_color = self.color, - fill_opacity = self.brightness * self.opacity_function(r1), - ) - for r1, r2 in zip(radii, radii[1:]) - ] - self.add(*sectors) + def change_opacity_function(self,new_f): + self.radius = 120 + self.opacity_function = new_f + dr = self.radius/self.num_levels - def get_source_point(self): - if len(self.submobjects) == 0: - return None - source = self.submobjects[0].get_arc_center() - return source - - def move_source_to(self,point): - if len(self.submobjects) == 0: - return - source = self.submobjects[0].get_arc_center() - self.shift(point - source) - - def update_opening(self, start_angle, stop_angle): - self.start_angle = start_angle - self.angle = stop_angle - start_angle - source_point = self.get_source_point() + sectors = [] for submob in self.submobjects: if type(submob) == AnnularSector: + sectors.append(submob) - submob.start_angle = self.start_angle - submob.angle = self.angle - submob.generate_points() - submob.shift(source_point - submob.get_arc_center()) + print self.num_levels, len(sectors) + for (r,submob) in zip(np.arange(0,self.radius,dr),sectors): + if type(submob) != AnnularSector: + # it's the shadow, don't dim it + continue + alpha = self.opacity_function(r) + submob.set_fill(opacity = alpha) - def set_brightness(self,new_brightness): - self.brightness = new_brightness - radii = np.linspace(0, self.radius, self.num_sectors+1) - for (r1,sector) in zip(radii,self.submobjects): - sector.set_fill(opacity = self.brightness * self.opacity_function(r1)) - - def stop_angle(self): - return self.start_angle + self.angle - - - - - - - -class Candle(VGroup): - CONFIG = { - "radius" : 5, - "brightness" : 1.0, - "opacity_function" : lambda r : 1./max(r, 0.01), - "num_annuli" : 10, - "color": LIGHT_COLOR, - } - - def generate_points(self): - radii = np.linspace(0, self.radius, self.num_annuli+1) - annuli = [ - Annulus( - inner_radius = r1, - outer_radius = r2, - stroke_width = 0, - stroke_color = self.color, - fill_color = self.color, - fill_opacity = self.brightness * self.opacity_function(r1), - ) - for r1, r2 in zip(radii, radii[1:]) - ] - self.add(*annuli) - - def get_source_point(self): - if len(self.submobjects) == 0: - return None - source = self.submobjects[0].get_center() - return source - - def move_source_to(self,point): - if len(self.submobjects) == 0: - return - source = self.submobjects[0].get_center() - self.shift(point - source) - - def set_brightness(self,new_brightness): - self.brightness = new_brightness - radii = np.linspace(0, self.radius, self.num_annuli+1) - for (r1,annulus) in zip(radii,self.submobjects): - annulus.set_fill(opacity = self.brightness * self.opacity_function(r1)) @@ -253,11 +295,42 @@ class SwitchOn(LaggedStart): } def __init__(self, light, **kwargs): - if not isinstance(light,LightCone) and not isinstance(light,Candle): + if not isinstance(light,AmbientLight) and not isinstance(light,Spotlight): raise Exception("Only LightCones and Candles can be switched on") LaggedStart.__init__(self, FadeIn, light, **kwargs) + +class SwitchOff(LaggedStart): + CONFIG = { + "lag_ratio": 0.2, + "run_time": SWITCH_ON_RUN_TIME + } + + def __init__(self, light, **kwargs): + if not isinstance(light,AmbientLight) and not isinstance(light,Spotlight): + raise Exception("Only LightCones and Candles can be switched on") + light.submobjects = light.submobjects[::-1] + LaggedStart.__init__(self, + FadeOut, light, **kwargs) + light.submobjects = light.submobjects[::-1] + + + + + +class ScreenTracker(ContinualAnimation): + def __init__(self, mobject, **kwargs): + ContinualAnimation.__init__(self, mobject, **kwargs) + + def update_mobject(self, dt): + self.mobject.recalculate_sectors( + point = self.mobject.source_point, + screen = self.mobject.screen) + self.mobject.update_shadow(self.mobject.source_point) + + + class LightHouse(SVGMobject): CONFIG = { "file_name" : "lighthouse", @@ -268,7 +341,8 @@ class LightIndicator(Mobject): CONFIG = { "radius": 0.5, "intensity": 0, - "opacity_for_unit_intensity": 1 + "opacity_for_unit_intensity": 1, + "precision": 3 } def generate_points(self): @@ -278,7 +352,7 @@ class LightIndicator(Mobject): self.foreground.set_stroke(color=INDICATOR_STROKE_COLOR,width=INDICATOR_STROKE_WIDTH) self.add(self.background, self.foreground) - self.reading = DecimalNumber(self.intensity,num_decimal_points = 3) + self.reading = DecimalNumber(self.intensity,num_decimal_points = self.precision) self.reading.set_fill(color=INDICATOR_TEXT_COLOR) self.reading.move_to(self.get_center()) self.add(self.reading) @@ -306,6 +380,28 @@ class UpdateLightIndicator(AnimationGroup): self.mobject = indicator + + + + + + + + + + + + + + + + + + + + + + class IntroScene(PiCreatureScene): CONFIG = { @@ -319,16 +415,10 @@ class IntroScene(PiCreatureScene): randy = self.get_primary_pi_creature() randy.scale(0.7).to_corner(DOWN+RIGHT) - self.force_skipping() - self.build_up_euler_sum() self.build_up_sum_on_number_line() self.show_pi_answer() self.other_pi_formulas() - - self.revert_to_original_skipping_status() - - self.refocus_on_euler_sum() @@ -526,20 +616,41 @@ class IntroScene(PiCreatureScene): ScaleInPlace(pi_squared,2,rate_func = wiggle) ) - q_circle = Circle(color=WHITE,radius=0.8) - q_mark = TexMobject("?") - q_mark.next_to(q_circle) - thought = Group(q_circle, q_mark) - q_mark.height *= 2 - self.pi_creature_thinks(thought,target_mode = "confused", - bubble_kwargs = { "height" : 1.5, "width" : 2 }) - self.wait() + # Morty thinks of a circle + + # q_circle = Circle(color=WHITE,radius=0.8) + # q_mark = TexMobject("?") + # q_mark.next_to(q_circle) + + # thought = Group(q_circle, q_mark) + # q_mark.height *= 2 + # self.pi_creature_thinks(thought,target_mode = "confused", + # bubble_kwargs = { "height" : 1.5, "width" : 2 }) + + # self.wait() -class FirstLightHouseScene(PiCreatureScene): + + + + + + + + + + + + + + + + + +class FirstLighthouseScene(PiCreatureScene): def construct(self): self.remove(self.get_primary_pi_creature()) @@ -598,7 +709,7 @@ class FirstLightHouseScene(PiCreatureScene): lighthouses = [] lighthouse_pos = [] - light_cones = [] + ambient_lights = [] euler_sum_above = TexMobject("1", "+", "{1\over 4}", @@ -617,15 +728,15 @@ class FirstLightHouseScene(PiCreatureScene): for i in range(1,NUM_CONES+1): lighthouse = LightHouse() point = self.number_line.number_to_point(i) - light_cone = Candle( - opacity_function = inverse_quadratic(1,1), - num_annuli = LIGHT_CONE_NUM_SECTORS, - radius = 12) + ambient_light = AmbientLight( + opacity_function = inverse_quadratic(1,2,1), + num_levels = NUM_LEVELS, + radius = 12.0) - light_cone.move_source_to(point) + ambient_light.move_source_to(point) lighthouse.next_to(point,DOWN,0) lighthouses.append(lighthouse) - light_cones.append(light_cone) + ambient_lights.append(ambient_light) lighthouse_pos.append(point) @@ -641,13 +752,13 @@ class FirstLightHouseScene(PiCreatureScene): # slowly switch on visible light cones and increment indicator - for (i,lc) in zip(range(NUM_VISIBLE_CONES),light_cones[:NUM_VISIBLE_CONES]): - indicator_start_time = 0.4 * (i+1) * SWITCH_ON_RUN_TIME/lc.radius * self.number_line.unit_size + for (i,ambient_light) in zip(range(NUM_VISIBLE_CONES),ambient_lights[:NUM_VISIBLE_CONES]): + indicator_start_time = 0.4 * (i+1) * SWITCH_ON_RUN_TIME/ambient_light.radius * self.number_line.unit_size indicator_stop_time = indicator_start_time + INDICATOR_UPDATE_TIME indicator_rate_func = squish_rate_func( smooth,indicator_start_time,indicator_stop_time) self.play( - SwitchOn(lc), + SwitchOn(ambient_light), FadeIn(euler_sum_above[2*i], run_time = SWITCH_ON_RUN_TIME, rate_func = indicator_rate_func), FadeIn(euler_sum_above[2*i - 1], run_time = SWITCH_ON_RUN_TIME, @@ -667,13 +778,13 @@ class FirstLightHouseScene(PiCreatureScene): ) # quickly switch on off-screen light cones and increment indicator - for (i,lc) in zip(range(NUM_VISIBLE_CONES,NUM_CONES),light_cones[NUM_VISIBLE_CONES:NUM_CONES]): - indicator_start_time = 0.5 * (i+1) * FAST_SWITCH_ON_RUN_TIME/lc.radius * self.number_line.unit_size + for (i,ambient_light) in zip(range(NUM_VISIBLE_CONES,NUM_CONES),ambient_lights[NUM_VISIBLE_CONES:NUM_CONES]): + indicator_start_time = 0.5 * (i+1) * FAST_SWITCH_ON_RUN_TIME/ambient_light.radius * self.number_line.unit_size indicator_stop_time = indicator_start_time + FAST_INDICATOR_UPDATE_TIME indicator_rate_func = squish_rate_func(#smooth, 0.8, 0.9) smooth,indicator_start_time,indicator_stop_time) self.play( - SwitchOn(lc, run_time = FAST_SWITCH_ON_RUN_TIME), + SwitchOn(ambient_light, run_time = FAST_SWITCH_ON_RUN_TIME), ChangeDecimalToValue(light_indicator.reading,intensities[i], rate_func = indicator_rate_func, run_time = FAST_SWITCH_ON_RUN_TIME), ApplyMethod(light_indicator.foreground.set_fill,None,opacities[i]) @@ -702,128 +813,259 @@ class FirstLightHouseScene(PiCreatureScene): -class SingleLightHouseScene(PiCreatureScene): + + + + + + + + + + + + + + + + + + +class SingleLighthouseScene(PiCreatureScene): def construct(self): - self.create_light_source_and_creature() + self.setup_elements() + self.setup_trackers() # spotlight and angle msmt change when screen rotates + self.rotate_screen() - def create_light_source_and_creature(self): + def setup_elements(self): SCREEN_SIZE = 3.0 DISTANCE_FROM_LIGHTHOUSE = 10.0 source_point = [-DISTANCE_FROM_LIGHTHOUSE/2,0,0] observer_point = [DISTANCE_FROM_LIGHTHOUSE/2,0,0] + # Lighthouse + lighthouse = LightHouse() - candle = Candle( - opacity_function = inverse_quadratic(1,1), - num_annuli = LIGHT_CONE_NUM_SECTORS, + ambient_light = AmbientLight( + opacity_function = inverse_quadratic(AMBIENT_FULL,2,1), + num_levels = NUM_LEVELS, radius = 10, brightness = 1, ) lighthouse.scale(2).next_to(source_point, DOWN, buff = 0) - candle.move_to(source_point) + ambient_light.move_to(source_point) + + # Pi Creature + morty = self.get_primary_pi_creature() morty.scale(0.5) morty.move_to(observer_point) self.add(lighthouse) self.play( - SwitchOn(candle) + SwitchOn(ambient_light) ) - light_cone = LightCone( - opacity_function = inverse_quadratic(1,1), - num_sectors = LIGHT_CONE_NUM_SECTORS, + # Screen + + self.screen = Line([0,-1,0],[0,1,0]) + self.screen.rotate(-TAU/6) + self.screen.next_to(morty, LEFT, buff = 1) + + # Spotlight + + self.spotlight = Spotlight( + opacity_function = inverse_quadratic(1,2,1), + num_levels = NUM_LEVELS, radius = 10, brightness = 5, + screen = self.screen ) - light_cone.move_source_to(source_point) - screen = Line([0,-1,0],[0,1,0]) - show_line_length(screen) + self.spotlight.move_source_to(source_point) - screen.rotate_in_place(-TAU/6) - show_line_length(screen) - screen.next_to(morty, LEFT, buff = 1) - - light_screen = LightScreen(light_source = source_point, - screen = screen, light_cone = light_cone) - light_screen.screen.color = WHITE - light_screen.screen.fill_opacity = 1 - light_screen.update_light_cone(light_cone) + # Animations + self.play( - FadeIn(light_screen, run_time = 2), - # dim the light that misses the screen - ApplyMethod(candle.set_brightness,0.3), - ApplyMethod(light_screen.update_shadow,light_screen.shadow), - FadeIn(light_cone), + ApplyMethod(ambient_light.dimming,AMBIENT_DIMMED), + FadeIn(self.spotlight) ) + self.add(self.spotlight.shadow) - lc_updater = lambda lc: light_screen.update_light_cone(lc) - sh_updater = lambda sh: light_screen.update_shadow(sh) - - ca1 = ContinualUpdateFromFunc(light_screen.light_cone, - lc_updater) - ca2 = ContinualUpdateFromFunc(light_screen.shadow, - sh_updater) - - self.add(ca1, ca2) self.add_foreground_mobject(morty) - pointing_screen_at_source = ApplyMethod(screen.rotate_in_place,TAU/6) + + + + def setup_trackers(self): + + # Make spotlight follow the screen + + screen_tracker = ScreenTracker(self.spotlight) + + self.add(screen_tracker) + pointing_screen_at_source = Rotate(self.screen,TAU/6) self.play(pointing_screen_at_source) - arc_angle = light_cone.angle + + # angle msmt (arc) + + arc_angle = self.spotlight.opening_angle() # draw arc arrows to show the opening angle - angle_arc = Arc(radius = 5, start_angle = light_cone.start_angle, - angle = light_cone.angle, tip_length = ARC_TIP_LENGTH) + angle_arc = Arc(radius = 5, start_angle = self.spotlight.start_angle(), + angle = self.spotlight.opening_angle(), tip_length = ARC_TIP_LENGTH) #angle_arc.add_tip(at_start = True, at_end = True) - angle_arc.move_arc_center_to(source_point) + angle_arc.move_arc_center_to(self.spotlight.source_point) self.add(angle_arc) + # angle msmt (decimal number) + angle_indicator = DecimalNumber(arc_angle/TAU*360, num_decimal_points = 0, unit = "^\\circ") angle_indicator.next_to(angle_arc,RIGHT) self.add_foreground_mobject(angle_indicator) - angle_update_func = lambda x: light_cone.angle/TAU*360 - ca3 = ContinualChangingDecimal(angle_indicator,angle_update_func) - self.add(ca3) + angle_update_func = lambda x: self.spotlight.opening_angle()/TAU * 360 + ca1 = ContinualChangingDecimal(angle_indicator,angle_update_func) + self.add(ca1) - #ca4 = ContinualUpdateFromFunc(angle_arc,update_angle_arc) - ca4 = AngleUpdater(angle_arc, light_screen.light_cone) - self.add(ca4) + ca2 = AngleUpdater(angle_arc, self.spotlight) + self.add(ca2) + + + def rotate_screen(self): + + # rotating_screen_1 = Rotate(self.screen, + # TAU/8, run_time=1.5) + # #self.wait(2) + # rotating_screen_2 = Rotate(self.screen, + # -TAU/4, run_time=3) + # #self.wait(2) + # rotating_screen_3 = Rotate(self.screen, + # TAU/8, run_time=1.5) + + # self.play(rotating_screen_1) + # self.play(rotating_screen_2) + # self.play(rotating_screen_3) + + rotating_screen_1 = Rotate(self.screen, TAU/8, rate_func = there_and_back) + rotating_screen_2 = Rotate(self.screen, -TAU/8, rate_func = there_and_back) + self.play(rotating_screen_1) + self.play(rotating_screen_2) - rotating_screen = ApplyMethod(light_screen.screen.rotate_in_place, TAU/6, run_time=3, rate_func = wiggle) - self.play(rotating_screen) - #rotating_screen_back = ApplyMethod(light_screen.screen.rotate_in_place, -TAU/6) #, run_time=3, rate_func = wiggle) - #self.play(rotating_screen_back) - self.wait() + #self.wait() + + +### The following is supposed to morph the scene into the Earth scene, +### but it doesn't work + + + # # morph into Earth scene + + # earth = Circle(radius = 3) + # earth.move_to([2,0,0]) + # sun_position = [-100,0,0] + # #self.add(screen_tracker) + # print "tuet" + # self.remove(screen_tracker) + # new_opacity_function = lambda r: 0.5 + # self.play( + # ApplyMethod(lighthouse.move_to,sun_position), + # ApplyMethod(ambient_light.move_to,sun_position), + # ApplyMethod(spotlight.move_source_to,sun_position), + + # ) + # self.play( + # ApplyMethod(spotlight.change_opacity_function,new_opacity_function)) + + # self.add(screen_tracker) - # morph into Earth scene - globe = Circle(radius = 3) - globe.move_to([2,0,0]) - sun_position = [-100,0,0] + + + + + + + + + + + + + + + + + + +class EarthScene(Scene): + + def construct(self): + + radius = 2.5 + center_x = 3 + theta0 = 70 * DEGREES + dtheta = 10 * DEGREES + theta1 = theta0 + dtheta + + + # screen + + screen = Line([center_x - radius * np.cos(theta0),radius * np.sin(theta0),0], + [center_x - radius * np.cos(theta1),radius * np.sin(theta1),0]) + screen.set_stroke(color = RED, width = 5) + + # Earth + + earth = Circle(radius = radius, stroke_width = 0) + earth.move_to([center_x,0,0]) + foreground_earth = earth.copy() # above the shadow + foreground_earth.radius -= 0.2 + foreground_earth.set_stroke(color = WHITE, width = 1) + self.add_foreground_mobject(foreground_earth) + earth.add(screen) + + # Morty + + morty = Mortimer().scale(0.3).next_to(screen, RIGHT, buff = 0.5) + self.add_foreground_mobject(morty) + + + # Light source (far-away Sun) + + sun = Spotlight( + opacity_function = lambda r : 0.5, + num_levels = NUM_LEVELS, + radius = 1100, + brightness = 5, + screen = screen + ) + + sun.move_source_to([-1000,0,0]) + + # Add elements to scene + + self.add(earth,sun,screen) + screen_tracker = ScreenTracker(sun) + self.add(screen_tracker) + + + # move screen to equator + self.play( - ApplyMethod(lighthouse.move_to,sun_position), - ApplyMethod(candle.move_to,sun_position), - ApplyMethod(light_cone.move_source_to,sun_position), - FadeOut(angle_arc), - FadeOut(angle_indicator), - FadeIn(globe), - ApplyMethod(light_screen.move_to,[0,0,0]), - ApplyMethod(morty.move_to,[1,0,0]) - + Rotate(earth,theta0 + dtheta/2,run_time = 3), + ApplyMethod(morty.move_to,[1.5,0,0], run_time = 3), ) @@ -849,6 +1091,281 @@ class SingleLightHouseScene(PiCreatureScene): + +class ScreenShapingScene(Scene): + + def construct(self): + + self.setup_elements() + self.deform_screen() + self.create_brightness_rect() + self.slant_screen() + self.unslant_screen() + self.left_shift_screen_while_showing_light_indicator() + self.add_distance_arrow() + self.right_shift_screen_while_showing_light_indicator_and_distance_arrow() + self.left_shift_again() + self.morph_into_faux_3d() + + + def setup_elements(self): + + self.screen_height = 1.0 + self.brightness_rect_height = 1.0 + + # screen + self.screen = Line([3,-self.screen_height/2,0],[3,self.screen_height/2,0], + path_arc = 0, num_arc_anchors = 10) + + # spotlight + self.spotlight = Spotlight( + opacity_function = inverse_quadratic(1,5,1), + num_levels = NUM_LEVELS, + radius = 10, + brightness = 5, + screen = self.screen + ) + + self.spotlight.move_source_to([-5,0,0]) + screen_tracker = ScreenTracker(self.spotlight) + + # lighthouse + lighthouse = LightHouse() + lighthouse.scale(2).next_to(self.spotlight.source_point,DOWN,buff=0) + + # ambient light + ambient_light = AmbientLight( + opacity_function = inverse_quadratic(AMBIENT_DIMMED,1,1), + num_levels = NUM_LEVELS, + radius = 10, + brightness = 1, + ) + ambient_light.move_source_to(self.spotlight.source_point) + + # Morty + self.morty = Mortimer().scale(0.3).next_to(self.screen, RIGHT, buff = 0.5) + + # Add everything to the scene + self.add(lighthouse, ambient_light,self.spotlight,self.screen) + self.add_foreground_mobject(self.morty) + self.add(screen_tracker) + + + + + def deform_screen(self): + + self.wait() + + self.play(ApplyMethod(self.screen.set_path_arc, 45 * DEGREES)) + self.play(ApplyMethod(self.screen.set_path_arc, -90 * DEGREES)) + self.play(ApplyMethod(self.screen.set_path_arc, 0)) + + + + + def create_brightness_rect(self): + + # in preparation for the slanting, create a rectangle that shows the brightness + + # a rect a zero width overlaying the screen + # so we can morph it into the brightness rect above + brightness_rect0 = Rectangle(width = 0, + height = self.screen_height).move_to(self.screen.get_center()) + self.add_foreground_mobject(brightness_rect0) + + self.brightness_rect = Rectangle(width = self.brightness_rect_height, + height = self.brightness_rect_height, fill_color = YELLOW, fill_opacity = 0.5) + + self.brightness_rect.next_to(self.screen, UP, buff = 1) + + self.play( + ReplacementTransform(brightness_rect0,self.brightness_rect) + ) + + self.original_screen = self.screen.copy() + self.original_brightness_rect = self.brightness_rect.copy() + # for unslanting the screen later + + + def slant_screen(self): + + lower_screen_point, upper_screen_point = self.screen.get_start_and_end() + + lower_slanted_screen_point = interpolate( + lower_screen_point, self.spotlight.source_point, 0.2 + ) + upper_slanted_screen_point = interpolate( + upper_screen_point, self.spotlight.source_point, -0.2 + ) + + self.slanted_brightness_rect = self.brightness_rect.copy() + self.slanted_brightness_rect.width *= 2 + self.slanted_brightness_rect.generate_points() + self.slanted_brightness_rect.set_fill(opacity = 0.25) + + self.slanted_screen = Line(lower_slanted_screen_point,upper_slanted_screen_point, + path_arc = 0, num_arc_anchors = 10) + self.slanted_brightness_rect.move_to(self.brightness_rect.get_center()) + + self.play( + ReplacementTransform(self.screen,self.slanted_screen), + ReplacementTransform(self.brightness_rect,self.slanted_brightness_rect), + ) + + + + def unslant_screen(self): + + self.wait() + + + self.remove(self.slanted_screen) + self.remove(self.slanted_brightness_rect) + + + self.play( + ReplacementTransform(self.screen,self.original_screen), + ReplacementTransform(self.slanted_brightness_rect,self.original_brightness_rect), + ) + + + #self.remove(self.original_brightness_rect) + + + def left_shift_screen_while_showing_light_indicator(self): + + # Scene 5: constant screen size, changing opening angle + + # let's use an actual light indicator instead of just rects + + self.indicator_intensity = 0.25 + indicator_height = 1.25 * self.screen_height + + # indicator0 = Ellipse(width = 0, height = screen_height).move_to(self.screen.get_center()) + # indicator0.set_fill(opacity = 0) + # self.add_foreground_mobject(indicator0) + + self.indicator = LightIndicator(radius = indicator_height/2, + opacity_for_unit_intensity = OPACITY_FOR_UNIT_INTENSITY, + color = LIGHT_COLOR, + precision = 2) + self.indicator.set_intensity(self.indicator_intensity) + + + + self.indicator.move_to(self.original_brightness_rect.get_center()) + + + self.play( + FadeOut(self.original_brightness_rect), + FadeIn(self.indicator) + ) + + self.add_foreground_mobject(self.indicator.reading) + + self.unit_indicator_intensity = 1.0 + + self.left_shift = (self.screen.get_center()[0] - self.spotlight.source_point[0])/2 + + self.play( + ApplyMethod(self.screen.shift,[-self.left_shift,0,0]), + ApplyMethod(self.morty.shift,[-self.left_shift,0,0]), + #ApplyMethod(self.indicator.shift,[-self.left_shift,0,0]), + ApplyMethod(self.indicator.set_intensity,self.unit_indicator_intensity), + ) + + self.remove(self.original_screen) # was still hiding behind the shadow + + + + def add_distance_arrow(self): + + # distance arrow (length 1) + left_x = self.spotlight.source_point[0] + right_x = self.screen.get_center()[0] + arrow_y = -2 + arrow1 = Arrow([left_x,arrow_y,0],[right_x,arrow_y,0]) + arrow2 = Arrow([right_x,arrow_y,0],[left_x,arrow_y,0]) + arrow1.set_fill(color = WHITE) + arrow2.set_fill(color = WHITE) + distance_decimal = Integer(1).next_to(arrow1,DOWN) + self.arrow = VGroup(arrow1, arrow2,distance_decimal) + self.add(self.arrow) + + + # distance arrow (length 2) + # will be morphed into + self.distance_to_source = right_x - left_x + new_right_x = left_x + 2 * self.distance_to_source + new_arrow1 = Arrow([left_x,arrow_y,0],[new_right_x,arrow_y,0]) + new_arrow2 = Arrow([new_right_x,arrow_y,0],[left_x,arrow_y,0]) + new_arrow1.set_fill(color = WHITE) + new_arrow2.set_fill(color = WHITE) + new_distance_decimal = Integer(2).next_to(new_arrow1,DOWN) + self.new_arrow = VGroup(new_arrow1, new_arrow2, new_distance_decimal) + # don't add it yet + + + def right_shift_screen_while_showing_light_indicator_and_distance_arrow(self): + + self.wait() + + self.play( + ReplacementTransform(self.arrow,self.new_arrow), + ApplyMethod(self.screen.shift,[self.distance_to_source,0,0]), + #ApplyMethod(self.indicator.shift,[self.left_shift,0,0]), + + ApplyMethod(self.indicator.set_intensity,self.indicator_intensity), + # this should trigger ChangingDecimal, but it doesn't + # maybe bc it's an anim within an anim? + + ApplyMethod(self.morty.shift,[self.distance_to_source,0,0]), + ) + + + def left_shift_again(self): + + self.wait() + + self.play( + ReplacementTransform(self.new_arrow,self.arrow), + ApplyMethod(self.screen.shift,[-self.distance_to_source,0,0]), + #ApplyMethod(self.indicator.shift,[-self.left_shift,0,0]), + ApplyMethod(self.indicator.set_intensity,self.unit_indicator_intensity), + ApplyMethod(self.morty.shift,[-self.distance_to_source,0,0]), + ) + + def morph_into_faux_3d(self): + + p0_lower, p0_upper = self.screen.get_start_and_end() + + p01 = p0_lower + p02 = p0_lower + p03 = p0_upper + p04 = p0_upper + + screen3d0 = Polygon(p01,p02,p03,p04) + screen3d0.set_stroke(width = 1, color = WHITE) + + screen3d0_edge = VMobject() + screen3d0_edge.set_anchor_points([p03,p04,p01]) + + perspective_v = 0.5 * np.array([1,-1,0]) + p1 = p01 + 0.5 * perspective_v + p2 = p02 - 0.5 * perspective_v + p3 = p03 - 0.5 * perspective_v + p4 = p04 + 0.5 * perspective_v + + screen3d = Polygon(p1,p2,p3,p4) + screen3d.set_fill(color = WHITE, opacity = 0.5) + + screen3d_edge = VMobject() + screen3d_edge.set_anchor_points([p3,p4,p1]) + + self.spotlight.screen = screen3d0_edge + + self.play(Transform(self.spotlight.screen,screen3d_edge)) diff --git a/active_projects/fourier.py b/active_projects/fourier.py index f7c1d745..429b5041 100644 --- a/active_projects/fourier.py +++ b/active_projects/fourier.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from helpers import * import scipy @@ -47,6 +48,87 @@ def get_fourier_transform( ## +class Introduction(TeacherStudentsScene): + def construct(self): + title = TextMobject("Fourier Transform") + title.scale(1.2) + title.to_edge(UP, buff = MED_SMALL_BUFF) + + func = lambda t : np.cos(2*TAU*t) + np.cos(3*TAU*t) + graph = FunctionGraph(func, x_min = 0, x_max = 5) + graph.stretch(0.25, 1) + graph.next_to(title, DOWN) + graph.to_edge(LEFT) + graph.highlight(BLUE) + fourier_graph = FunctionGraph( + get_fourier_transform(func, 0, 5), + x_min = 0, x_max = 5 + ) + fourier_graph.move_to(graph) + fourier_graph.to_edge(RIGHT) + fourier_graph.highlight(RED) + arrow = Arrow(graph, fourier_graph, color = WHITE) + self.add(title, graph) + + self.student_thinks( + "What's that?", + look_at_arg = title, + target_mode = "confused", + student_index = 1, + ) + self.play( + GrowArrow(arrow), + ReplacementTransform(graph.copy(), fourier_graph) + ) + self.wait(2) + self.student_thinks( + "Pssht, I got this", + target_mode = "tease", + student_index = 2, + added_anims = [RemovePiCreatureBubble(self.students[1])] + ) + self.play(self.teacher.change, "hesitant") + self.wait(2) + +class TODOInsertUnmixingSound(TODOStub): + CONFIG = { + "message" : "Show unmixing sound" + } + +class OtherContexts(PiCreatureScene): + def construct(self): + items = VGroup(*map(TextMobject, [ + "Extracting frequencies from sound", + "Uncertainty principle", + "Riemann Zeta function and primes", + "Differential equations", + ])) + items.arrange_submobjects( + DOWN, buff = MED_LARGE_BUFF, + aligned_edge = LEFT + ) + items.to_corner(UP+LEFT) + items[1:].set_fill(opacity = 0.2) + + morty = self.pi_creature + morty.to_corner(UP+RIGHT) + + self.add(items) + for item in items[1:]: + self.play( + LaggedStart( + ApplyMethod, item, + lambda m : (m.set_fill, {"opacity" : 1}), + ), + morty.change, "thinking", + ) + self.wait() + +class TODOInsertCosineWrappingAroundCircle(TODOStub): + CONFIG = { + "message" : "Give a picture-in-picture \\\\ of cosine wrapping around circle", + } + class AddingPureFrequencies(PiCreatureScene): CONFIG = { "A_frequency" : 2.1, @@ -721,13 +803,14 @@ class FourierMachineScene(Scene): "x_max" : 5.0, "x_axis_config" : { "unit_size" : 1.4, + "numbers_to_show" : range(1, 6), }, "y_min" : -1.0, "y_max" : 1.0, "y_axis_config" : { "unit_size" : 1.8, "tick_frequency" : 0.5, - "line_to_number_vect" : LEFT, + "label_direction" : LEFT, }, "color" : TEAL, }, @@ -783,7 +866,7 @@ class FourierMachineScene(Scene): def get_frequency_axes(self): frequency_axes = Axes(**self.frequency_axes_config) - frequency_axes.x_axis.add_numbers(*range(1, 6)) + frequency_axes.x_axis.add_numbers() frequency_axes.y_axis.add_numbers( *frequency_axes.y_axis.get_tick_numbers() ) @@ -981,6 +1064,7 @@ class WrapCosineGraphAroundCircle(FourierMachineScene): } def construct(self): self.show_initial_signal() + self.show_finite_interval() self.wrap_around_circle() self.show_time_sweeps() self.compare_two_frequencies() @@ -1018,6 +1102,38 @@ class WrapCosineGraphAroundCircle(FourierMachineScene): self.beats_per_second_label = words self.graph = graph + def show_finite_interval(self): + axes = self.time_axes + v_line = DashedLine( + axes.coords_to_point(0, 0), + axes.coords_to_point(0, axes.y_max), + color = RED, + stroke_width = 6, + ) + h_line = Line( + axes.coords_to_point(0, 0), + axes.coords_to_point(axes.x_max, 0), + ) + rect = Rectangle( + stroke_width = 0, + fill_color = TEAL, + fill_opacity = 0.5, + ) + rect.match_height(v_line) + rect.match_width(h_line, stretch = True) + rect.move_to(v_line, DOWN+LEFT) + right_v_line = v_line.copy() + right_v_line.move_to(rect, RIGHT) + + rect.save_state() + rect.stretch(0, 0, about_edge = ORIGIN) + self.play(rect.restore, run_time = 2) + self.play(FadeOut(rect)) + for line in v_line, right_v_line: + self.play(ShowCreation(line)) + self.play(FadeOut(line)) + self.wait() + def wrap_around_circle(self): graph = self.graph freq = self.initial_winding_frequency @@ -1089,7 +1205,7 @@ class WrapCosineGraphAroundCircle(FourierMachineScene): freq_label = self.winding_freq_label[0] count = 0 - for target_freq in [1.23, 0.2, 1.55, self.signal_frequency]: + for target_freq in [1.23, 0.2, 0.79, 1.55, self.signal_frequency]: self.play( Transform( v_lines, @@ -1241,9 +1357,11 @@ class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene): self.center_of_mass_label = words def change_to_various_frequencies(self): - for new_freq in [0.5, 0.2, 1.04, 2.21, 3.0]: - self.change_frequency(new_freq) - self.wait() + self.change_frequency( + 3.0, run_time = 30, + rate_func = bezier([0, 0, 1, 1]) + ) + self.wait() self.play( *self.get_vector_animations(self.graph), run_time = 15 @@ -1255,7 +1373,7 @@ class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene): com_label = self.center_of_mass_label com_label.add_background_rectangle() frequency_axes = self.get_frequency_axes() - x_coord_label = TextMobject("$x$-coordiante for center of mass") + x_coord_label = TextMobject("$x$-coordinate for center of mass") x_coord_label.highlight(self.center_of_mass_color) x_coord_label.scale(self.text_scale_val) x_coord_label.next_to( @@ -1466,25 +1584,29 @@ class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene): ) return self.fourier_graph_drawing_update_anim - def generate_center_of_mass_dot_update_anim(self): + def generate_center_of_mass_dot_update_anim(self, multiplier = 1): + origin = self.circle_plane.coords_to_point(0, 0) + com = self.get_pol_graph_center_of_mass self.center_of_mass_dot_anim = UpdateFromFunc( self.center_of_mass_dot, - lambda d : d.move_to(self.get_pol_graph_center_of_mass()) + lambda d : d.move_to( + multiplier*(com()-origin)+origin + ) ) def change_frequency(self, new_freq, **kwargs): kwargs["run_time"] = kwargs.get("run_time", 3) + kwargs["rate_func"] = kwargs.get( + "rate_func", bezier([0, 0, 1, 1]) + ) added_anims = kwargs.get("added_anims", []) - freq_label = filter( - lambda sm : isinstance(sm, DecimalNumber), - self.winding_freq_label - )[0] - anims = [ - ChangeDecimalToValue(freq_label, new_freq), - self.get_frequency_change_animation( - self.graph, new_freq - ) - ] + anims = [self.get_frequency_change_animation(self.graph, new_freq)] + if hasattr(self, "winding_freq_label"): + freq_label = filter( + lambda sm : isinstance(sm, DecimalNumber), + self.winding_freq_label + )[0] + anims.append(ChangeDecimalToValue(freq_label, new_freq)) if hasattr(self, "v_lines_indicating_periods"): anims.append(self.get_period_v_lines_update_anim()) if hasattr(self, "center_of_mass_dot"): @@ -1507,6 +1629,20 @@ class StudentsHorrifiedAtScene(TeacherStudentsScene): ) self.wait(4) +class AskAboutAlmostFouierName(TeacherStudentsScene): + def construct(self): + self.student_says( + "``Almost'' Fourier transform?", + target_mode = "sassy" + ) + self.change_student_modes("confused", "sassy", "confused") + self.wait() + self.teacher_says( + "We'll get to the real \\\\ one in a few minutes", + added_anims = [self.get_student_changes(*["plain"]*3)] + ) + self.wait(2) + class ShowLowerFrequency(DrawFrequencyPlot): CONFIG = { "signal_frequency" : 2.0, @@ -2039,6 +2175,15 @@ class ShowCommutativeDiagram(ShowLinearity): spike_rect.set_fill(YELLOW, 0.5) return spike_rect +class PauseAndPonder(TeacherStudentsScene): + def construct(self): + self.teacher_says( + "Pause and \\\\ ponder!", + target_mode = "hooray" + ) + self.change_student_modes(*["thinking"]*3) + self.wait(4) + class BeforeGettingToTheFullMath(TeacherStudentsScene): def construct(self): formula = TexMobject( @@ -2398,6 +2543,11 @@ class WriteComplexExponentialExpression(DrawFrequencyPlot): "default_num_v_lines_indicating_periods" : 0, "time_axes_scale_val" : 0.7, "initial_winding_frequency" : 0.1, + "circle_plane_config" : { + "unit_size" : 2, + "y_radius" : SPACE_HEIGHT+LARGE_BUFF, + "x_radius" : SPACE_WIDTH+LARGE_BUFF + } } def construct(self): self.remove(self.pi_creature) @@ -2410,11 +2560,7 @@ class WriteComplexExponentialExpression(DrawFrequencyPlot): self.find_center_of_mass() def setup_plane(self): - circle_plane = ComplexPlane( - unit_size = 2, - y_radius = SPACE_HEIGHT+LARGE_BUFF, - x_radius = SPACE_WIDTH+LARGE_BUFF - ) + circle_plane = ComplexPlane(**self.circle_plane_config) circle_plane.shift(DOWN+LEFT) circle = DashedLine(ORIGIN, TAU*UP) circle.apply_complex_function( @@ -2425,11 +2571,12 @@ class WriteComplexExponentialExpression(DrawFrequencyPlot): circle_plane.add(circle) time_axes = self.get_time_axes() - time_axes.add_to_back(BackgroundRectangle( + time_axes.background_rectangle = BackgroundRectangle( time_axes, fill_opacity = 0.9, buff = MED_SMALL_BUFF, - )) + ) + time_axes.add_to_back(time_axes.background_rectangle) time_axes.scale(self.time_axes_scale_val) time_axes.to_corner(UP+LEFT, buff = 0) time_axes.set_stroke(color = WHITE, width = 1) @@ -2697,7 +2844,7 @@ class WriteComplexExponentialExpression(DrawFrequencyPlot): Write(time_label), GrowArrow(time_label.arrow), ) - self.wait(6.5) #Leave time to say let's slow down + self.wait(12.5) #Leave time to say let's slow down self.remove(ambient_ghost_dot_movement) self.play( FadeOut(time_label), @@ -2956,6 +3103,15 @@ class WriteComplexExponentialExpression(DrawFrequencyPlot): ) return VGroup(time_graph.dots, pol_graph.dots) +class EulersFormulaViaGroupTheoryWrapper(Scene): + def construct(self): + title = TextMobject("Euler's formula with introductory group theory") + 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(2) class WhyAreYouTellingUsThis(TeacherStudentsScene): def construct(self): @@ -2967,7 +3123,7 @@ class BuildUpExpressionStepByStep(TeacherStudentsScene): def construct(self): expression = TexMobject( "\\frac{1}{t_2 - t_1}", "\\int_{t_1}^{t_2}", - "g(t)", "e", "^{2\\pi i", "f", "t}", "dt" + "g(t)", "e", "^{-2\\pi i", "f", "t}", "dt" ) frac, integral, g, e, two_pi_i, f, t, dt = expression expression.next_to(self.teacher, UP+LEFT) @@ -3010,24 +3166,28 @@ class BuildUpExpressionStepByStep(TeacherStudentsScene): class ScaleUpCenterOfMass(WriteComplexExponentialExpression): CONFIG = { "time_axes_scale_val" : 0.6, - "initial_winding_frequency" : 1.95 + "initial_winding_frequency" : 2.05 } def construct(self): self.remove(self.pi_creature) self.setup_plane() self.setup_graph() - self.add_expression() self.add_center_of_mass_dot() + self.add_expression() self.cross_out_denominator() self.scale_up_center_of_mass() - self.what_this_means_for_various_winding_frequencies() + self.comment_on_current_signal() + def add_center_of_mass_dot(self): + self.center_of_mass_dot = self.get_center_of_mass_dot() + self.generate_center_of_mass_dot_update_anim() + self.add(self.center_of_mass_dot) def add_expression(self): expression = TexMobject( "\\frac{1}{t_2 - t_1}", "\\int_{t_1}^{t_2}", - "g(t)", "e", "^{2\\pi i", "f", "t}", "dt" + "g(t)", "e", "^{-2\\pi i", "f", "t}", "dt" ) frac, integral, g, e, two_pi_i, f, t, dt = expression expression.to_corner(UP+RIGHT) @@ -3035,30 +3195,860 @@ class ScaleUpCenterOfMass(WriteComplexExponentialExpression): g[2].highlight(YELLOW) dt[1].highlight(YELLOW) f.highlight(GREEN) - expression.add_background_rectangle() self.expression = expression self.add(expression) self.winding_freq_label.to_edge(RIGHT) self.winding_freq_label[1].match_color(f) - - def add_center_of_mass_dot(self): - self.center_of_mass_dot = self.get_center_of_mass_dot() - self.generate_center_of_mass_dot_update_anim() - self.add(self.center_of_mass_dot) - + self.winding_freq_label.align_to( + self.circle_plane.coords_to_point(0, 0.1), DOWN + ) def cross_out_denominator(self): frac = self.expression[0] - integral = VGroup(*self.expression[1:]) - + integral = self.expression[1:] + for mob in frac, integral: + mob.add_to_back(BackgroundRectangle(mob)) + self.add(mob) + cross = Cross(frac) + brace = Brace(integral, DOWN) + label = brace.get_text("The actual \\\\ Fourier transform") + label.add_background_rectangle() + label.shift_onto_screen() + rect = SurroundingRectangle(integral) + + self.play(ShowCreation(cross)) + self.wait() + self.play(ShowCreation(rect)) + self.play( + GrowFromCenter(brace), + FadeIn(label) + ) + self.wait(2) + + self.integral = integral + self.frac = frac + self.frac_cross = cross + self.integral_rect = rect + self.integral_brace = brace + self.integral_label = label def scale_up_center_of_mass(self): - pass + plane = self.circle_plane + origin = plane.coords_to_point(0, 0) + com_dot = self.center_of_mass_dot + com_vector = Arrow( + origin, com_dot.get_center(), + buff = 0 + ) + com_vector.match_style(com_dot) + vector_to_scale = com_vector.copy() + def get_com_vector_copies(n): + com_vector_copies = VGroup(*[ + com_vector.copy().shift(x*com_vector.get_vector()) + for x in range(1, n+1) + ]) + com_vector_copies.highlight(TEAL) + return com_vector_copies + com_vector_update = UpdateFromFunc( + com_vector, + lambda v : v.put_start_and_end_on(origin, com_dot.get_center()) + ) + + circle = Circle(color = TEAL) + circle.surround(com_dot, buffer_factor = 1.2) + + time_span = Rectangle( + stroke_width = 0, + fill_color = TEAL, + fill_opacity = 0.4 + ) + axes = self.time_axes + time_span.replace( + Line(axes.coords_to_point(0, 0), axes.coords_to_point(3, 1.5)), + stretch = True + ) + time_span.save_state() + time_span.stretch(0, 0, about_edge = LEFT) + + graph = self.graph + short_graph, long_graph = [ + axes.get_graph( + graph.underlying_function, x_min = 0, x_max = t_max, + ).match_style(graph) + for t_max in 3, 6 + ] + for g in short_graph, long_graph: + self.get_polarized_mobject(g, freq = self.initial_winding_frequency) + + self.play( + FocusOn(circle, run_time = 2), + Succession( + ShowCreation, circle, + FadeOut, circle, + ), + ) + self.play( + com_dot.fade, 0.5, + FadeIn(vector_to_scale) + ) + self.wait() + self.play(vector_to_scale.scale, 4, {"about_point" : origin}) + self.wait() + self.play( + FadeOut(vector_to_scale), + FadeIn(com_vector), + ) + self.remove(graph.polarized_mobject) + self.play( + com_dot.move_to, + center_of_mass(short_graph.polarized_mobject.points), + com_vector_update, + time_span.restore, + ShowCreation(short_graph.polarized_mobject), + ) + self.wait() + # dot = Dot(fill_opacity = 0.5).move_to(time_span) + # self.play( + # dot.move_to, com_vector, + # dot.set_fill, {"opacity" : 0}, + # remover = True + # ) + com_vector_copies = get_com_vector_copies(2) + self.play(*[ + ReplacementTransform( + com_vector.copy(), cvc, + path_arc = -TAU/10 + ) + for cvc in com_vector_copies + ]) + self.wait() + + #Squish_graph + to_squish = VGroup( + axes, graph, + time_span, + ) + to_squish.generate_target() + squish_factor = 0.75 + to_squish.target.stretch(squish_factor, 0, about_edge = LEFT) + pairs = zip( + to_squish.family_members_with_points(), + to_squish.target.family_members_with_points() + ) + to_unsquish = list(axes.x_axis.numbers) + list(axes.labels) + for sm, tsm in pairs: + if sm in to_unsquish: + tsm.stretch(1/squish_factor, 0) + if sm is axes.background_rectangle: + tsm.stretch(1/squish_factor, 0, about_edge = LEFT) + + long_graph.stretch(squish_factor, 0) + self.play( + MoveToTarget(to_squish), + FadeOut(com_vector_copies) + ) + long_graph.move_to(graph, LEFT) + self.play( + com_dot.move_to, + center_of_mass(long_graph.polarized_mobject.points), + com_vector_update, + time_span.stretch, 2, 0, {"about_edge" : LEFT}, + *[ + ShowCreation( + mob, + rate_func = lambda a : interpolate( + 0.5, 1, smooth(a) + ) + ) + for mob in long_graph, long_graph.polarized_mobject + ], + run_time = 2 + ) + self.remove(graph, short_graph.polarized_mobject) + self.graph = long_graph + self.wait() + self.play(FocusOn(com_dot)) + com_vector_copies = get_com_vector_copies(5) + self.play(*[ + ReplacementTransform( + com_vector.copy(), cvc, + path_arc = -TAU/10 + ) + for cvc in com_vector_copies + ]) + self.wait() + + # Scale graph out even longer + to_shift = VGroup(self.integral, self.integral_rect) + to_fade = VGroup( + self.integral_brace, self.integral_label, + self.frac, self.frac_cross + ) + self.play( + to_shift.shift, 2*DOWN, + FadeOut(to_fade), + axes.background_rectangle.stretch, 2, 0, {"about_edge" : LEFT}, + Animation(axes), + Animation(self.graph), + FadeOut(com_vector_copies), + ) + self.change_frequency(2.0, added_anims = [com_vector_update]) + very_long_graph = axes.get_graph( + graph.underlying_function, + x_min = 0, x_max = 12, + ) + very_long_graph.match_style(graph) + self.get_polarized_mobject(very_long_graph, freq = 2.0) + self.play( + com_dot.move_to, + center_of_mass(very_long_graph.polarized_mobject.points), + com_vector_update, + ShowCreation( + very_long_graph, + rate_func = lambda a : interpolate(0.5, 1, a) + ), + ShowCreation(very_long_graph.polarized_mobject) + ) + self.remove(graph, graph.polarized_mobject) + self.graph = very_long_graph + self.wait() + self.play( + com_vector.scale, 12, {"about_point" : origin}, + run_time = 2 + ) + # com_vector_copies = get_com_vector_copies(11) + # self.play(ReplacementTransform( + # VGroup(com_vector.copy()), + # com_vector_copies, + # path_arc = TAU/10, + # run_time = 1.5, + # submobject_mode = "lagged_start" + # )) + self.wait() + + self.com_vector = com_vector + self.com_vector_update = com_vector_update + self.com_vector_copies = com_vector_copies + + def comment_on_current_signal(self): + graph = self.graph + com_dot = self.center_of_mass_dot + com_vector = self.com_vector + com_vector_update = self.com_vector_update + axes = self.time_axes + origin = self.circle_plane.coords_to_point(0, 0) + wps_label = self.winding_freq_label + + new_com_vector_update = UpdateFromFunc( + com_vector, lambda v : v.put_start_and_end_on( + origin, com_dot.get_center() + ).scale(12, about_point = origin) + ) + + v_lines = self.get_v_lines_indicating_periods( + freq = 1.0, n_lines = 3 + )[:2] + graph_portion = axes.get_graph( + graph.underlying_function, x_min = 1, x_max = 2 + ) + graph_portion.highlight(TEAL) + bps_label = TextMobject("2 beats per second") + bps_label.scale(0.75) + bps_label.next_to(graph_portion, UP, aligned_edge = LEFT) + bps_label.shift(SMALL_BUFF*RIGHT) + bps_label.add_background_rectangle() + + self.play( + ShowCreation(v_lines, submobject_mode = "all_at_once"), + ShowCreation(graph_portion), + FadeIn(bps_label), + ) + self.wait() + self.play(ReplacementTransform( + bps_label[1][0].copy(), wps_label[1] + )) + self.wait() + self.play( + com_vector.scale, 0.5, {"about_point" : origin}, + rate_func = there_and_back, + run_time = 2 + ) + self.wait(2) + self.change_frequency(2.5, + added_anims = [new_com_vector_update], + run_time = 20, + rate_func = None, + ) + self.wait() + +class TakeAStepBack(TeacherStudentsScene): + def construct(self): + self.student_says( + "Hang on, go over \\\\ that again?", + target_mode = "confused" + ), + self.change_student_modes(*["confused"]*3) + self.play(self.teacher.change, "happy") + self.wait(3) + +class SimpleCosineWrappingAroundCircle(WriteComplexExponentialExpression): + CONFIG = { + "initial_winding_frequency" : 0, + "circle_plane_config" : { + "unit_size" : 3, + }, + } + def construct(self): + self.setup_plane() + self.setup_graph() + self.remove(self.pi_creature) + self.winding_freq_label.shift(7*LEFT) + VGroup(self.time_axes, self.graph).shift(4*UP) + VGroup( + self.circle_plane, + self.graph.polarized_mobject + ).move_to(ORIGIN) + self.add(self.get_center_of_mass_dot()) + self.generate_center_of_mass_dot_update_anim() + + self.change_frequency( + 2.0, + rate_func = None, + run_time = 30 + ) + self.wait() + +class SummarizeTheFullTransform(DrawFrequencyPlot): + CONFIG = { + "time_axes_config" : { + "x_max" : 4.5, + "x_axis_config" : { + "unit_size" : 1.2, + "tick_frequency" : 0.5, + # "numbers_with_elongated_ticks" : range(0, 10, 2), + # "numbers_to_show" : range(0, 10, 2), + } + }, + "frequency_axes_config" : { + "x_max" : 5, + "x_axis_config" : { + "unit_size" : 1, + "numbers_to_show" : range(1, 5), + }, + "y_max" : 2, + "y_min" : -2, + "y_axis_config" : { + "unit_size" : 0.75, + "tick_frequency" : 1, + }, + }, + } + def construct(self): + self.setup_all_axes() + self.show_transform_function() + self.show_winding() + + def setup_all_axes(self): + time_axes = self.get_time_axes() + time_label, intensity_label = time_axes.labels + time_label.next_to( + time_axes.x_axis.get_right(), + DOWN, SMALL_BUFF + ) + intensity_label.next_to(time_axes.y_axis, UP, buff = SMALL_BUFF) + intensity_label.to_edge(LEFT) + + frequency_axes = self.get_frequency_axes() + frequency_axes.to_corner(UP+RIGHT) + frequency_axes.shift(RIGHT) + fy_axis = frequency_axes.y_axis + for number in fy_axis.numbers: + number.add_background_rectangle() + fy_axis.remove(*fy_axis.numbers[1::2]) + frequency_axes.remove(frequency_axes.box) + frequency_axes.label.shift_onto_screen() + + circle_plane = self.get_circle_plane() + + self.set_variables_as_attrs(time_axes, frequency_axes, circle_plane) + self.add(time_axes) + + def show_transform_function(self): + time_axes = self.time_axes + frequency_axes = self.frequency_axes + def func(t): + return 0.5*(2+np.cos(2*TAU*t) + np.cos(3*TAU*t)) + fourier_func = get_fourier_transform( + func, + t_min = time_axes.x_min, + t_max = time_axes.x_max, + use_almost_fourier = False, + ) + + graph = time_axes.get_graph(func) + graph.highlight(GREEN) + fourier_graph = frequency_axes.get_graph(fourier_func) + fourier_graph.highlight(RED) + + g_t = TexMobject("g(t)") + g_t[-2].match_color(graph) + g_t.next_to(graph, UP) + g_hat_f = TexMobject("\\hat g(f)") + g_hat_f[-2].match_color(fourier_graph) + g_hat_f.next_to( + frequency_axes.input_to_graph_point(2, fourier_graph), + UP + ) + + morty = self.pi_creature + + time_label = time_axes.labels[0] + frequency_label = frequency_axes.label + for label in time_label, frequency_label: + label.rect = SurroundingRectangle(label) + time_label.rect.match_style(graph) + frequency_label.rect.match_style(fourier_graph) + + self.add(graph) + g_t.save_state() + g_t.move_to(morty, UP+LEFT) + g_t.fade(1) + self.play( + morty.change, "raise_right_hand", + g_t.restore, + ) + self.wait() + self.play(Write(frequency_axes, run_time = 1)) + self.play( + ReplacementTransform(graph.copy(), fourier_graph), + ReplacementTransform(g_t.copy(), g_hat_f), + ) + self.wait(2) + for label in time_label, frequency_label: + self.play( + ShowCreation(label.rect), + morty.change, "thinking" + ) + self.play(FadeOut(label.rect)) + self.wait() + + self.set_variables_as_attrs( + graph, fourier_graph, + g_t, g_hat_f + ) + + def show_winding(self): + plane = self.circle_plane + graph = self.graph + fourier_graph = self.fourier_graph + morty = self.pi_creature + g_hat_f = self.g_hat_f + g_hat_f_rect = SurroundingRectangle(g_hat_f) + g_hat_f_rect.highlight(TEAL) + g_hat_rect = SurroundingRectangle(g_hat_f[0]) + g_hat_rect.match_style(g_hat_f_rect) + + g_hat_f.generate_target() + g_hat_f.target.next_to(plane, RIGHT) + g_hat_f.target.shift(UP) + arrow = Arrow( + g_hat_f.target.get_left(), + plane.coords_to_point(0, 0), + color = self.center_of_mass_color, + ) + + frequency_axes = self.frequency_axes + imaginary_fourier_graph = frequency_axes.get_graph( + get_fourier_transform( + graph.underlying_function, + t_min = self.time_axes.x_min, + t_max = self.time_axes.x_max, + real_part = False, + use_almost_fourier = False, + ) + ) + imaginary_fourier_graph.highlight(BLUE) + imaginary_fourier_graph.shift( + frequency_axes.x_axis.main_line.get_right() - \ + imaginary_fourier_graph.points[-1], + ) + + real_part = TextMobject( + "Real part of", "$\\hat g(f)$" + ) + real_part[1].match_style(g_hat_f) + real_part.move_to(g_hat_f) + real_part.to_edge(RIGHT) + + self.get_polarized_mobject(graph, freq = 0) + update_pol_graph = UpdateFromFunc( + graph.polarized_mobject, + lambda m : m.set_stroke(width = 2) + ) + com_dot = self.get_center_of_mass_dot() + + winding_run_time = 40.0 + g_hat_f_indication = Succession( + Animation, Mobject(), {"run_time" : 4}, + FocusOn, g_hat_f, + ShowCreation, g_hat_f_rect, + Animation, Mobject(), + Transform, g_hat_f_rect, g_hat_rect, + Animation, Mobject(), + FadeOut, g_hat_f_rect, + Animation, Mobject(), + MoveToTarget, g_hat_f, + UpdateFromAlphaFunc, com_dot, lambda m, a : m.set_fill(opacity = a), + Animation, Mobject(), {"run_time" : 2}, + GrowArrow, arrow, + FadeOut, arrow, + Animation, Mobject(), {"run_time" : 5}, + Write, real_part, {"run_time" : 2}, + Animation, Mobject(), {"run_time" : 3}, + ShowCreation, imaginary_fourier_graph, {"run_time" : 3}, + rate_func = squish_rate_func( + lambda x : x, 0, 31./winding_run_time + ), + run_time = winding_run_time + ) + + self.play( + FadeIn(plane), + ReplacementTransform( + graph.copy(), graph.polarized_mobject + ), + morty.change, "happy", + ) + self.generate_center_of_mass_dot_update_anim(multiplier = 4.5) + self.generate_fourier_dot_transform(fourier_graph) + self.change_frequency( + 5.0, + rate_func = None, + run_time = winding_run_time, + added_anims = [ + g_hat_f_indication, + update_pol_graph, + Animation(frequency_axes.x_axis.numbers), + Animation(self.fourier_graph_dot), + ] + ) + self.wait() + +class SummarizeFormula(Scene): + def construct(self): + expression = self.get_expression() + screen_rect = ScreenRectangle(height = 5) + screen_rect.to_edge(DOWN) + + exp_rect, g_exp_rect, int_rect = [ + SurroundingRectangle(VGroup( + expression.get_part_by_tex(p1), + expression.get_part_by_tex(p2), + )) + for p1, p2 in ("e", "t}"), ("g({}", "t}"), ("\\int", "dt") + ] + + self.add(expression) + self.wait() + self.play( + ShowCreation(screen_rect), + ShowCreation(exp_rect), + ) + self.wait(2) + self.play(Transform(exp_rect, g_exp_rect)) + self.wait(2) + self.play(Transform(exp_rect, int_rect)) + self.wait(2) + + def get_expression(self): + expression = TexMobject( + "\\hat g(", "f", ")", "=", "\\int", "_{t_1}", "^{t_2}", + "g({}", "t", ")", "e", "^{-2\\pi i", "f", "t}", "dt" + ) + expression.highlight_by_tex( + "t", YELLOW, substring = False, + ) + expression.highlight_by_tex("t}", YELLOW) + expression.highlight_by_tex( + "f", RED, substring = False, + ) + expression.scale(1.2) + expression.to_edge(UP) + return expression + +class OneSmallNote(TeacherStudentsScene): + def construct(self): + self.teacher_says( + "Just one \\\\ small note...", + # target_mode = + ) + self.change_student_modes("erm", "happy", "sassy") + self.wait(2) + +class BoundsAtInfinity(SummarizeFormula): + def construct(self): + expression = self.get_expression() + self.add(expression) + self.add_graph() + axes = self.axes + graph = self.graph + + time_interval = self.get_time_interval(-2, 2) + wide_interval = self.get_time_interval(-SPACE_WIDTH, SPACE_WIDTH) + bounds = VGroup(*reversed(expression.get_parts_by_tex("t_"))) + bound_rects = VGroup(*[ + SurroundingRectangle(b, buff = 0.5*SMALL_BUFF) + for b in bounds + ]) + bound_rects.highlight(TEAL) + inf_bounds = VGroup(*[ + VGroup(TexMobject(s + "\\infty")) + for s in "-", "+" + ]) + decimal_bounds = VGroup(*[DecimalNumber(0) for x in range(2)]) + for bound, inf_bound, d_bound in zip(bounds, inf_bounds, decimal_bounds): + for new_bound in inf_bound, d_bound: + new_bound.scale(0.7) + new_bound.move_to(bound, LEFT) + new_bound.bound = bound + def get_db_num_update(vect): + return lambda a : axes.x_axis.point_to_number( + time_interval.get_edge_center(vect) + ) + decimal_updates = [ + ChangingDecimal( + db, get_db_num_update(vect), + position_update_func = lambda m : m.move_to( + m.bound, LEFT + ) + ) + for db, vect in zip(decimal_bounds, [LEFT, RIGHT]) + ] + for update in decimal_updates: + update.update(1) + + time_interval.save_state() + self.wait() + self.play(ReplacementTransform( + self.get_time_interval(0, 0.01), time_interval + )) + self.play(LaggedStart(ShowCreation, bound_rects)) + self.wait() + self.play(FadeOut(bound_rects)) + self.play(ReplacementTransform(bounds, inf_bounds)) + self.play(Transform( + time_interval, wide_interval, + run_time = 4, + rate_func = there_and_back + )) + self.play( + ReplacementTransform(inf_bounds, decimal_bounds), + time_interval.restore, + ) + self.play( + VGroup(axes, graph).stretch, 0.05, 0, + Transform(time_interval, wide_interval), + UpdateFromAlphaFunc( + axes.x_axis.numbers, + lambda m, a : m.set_fill(opacity = 1-a) + ), + *decimal_updates, + run_time = 12, + rate_func = bezier([0, 0, 1, 1]) + ) + self.wait() - def what_this_means_for_various_winding_frequencies(self): - pass + def add_graph(self): + axes = Axes( + x_min = -140, + x_max = 140, + y_min = -2, + y_max = 2, + number_line_config = { + "include_tip" : False, + }, + default_num_graph_points = 1000, + ) + axes.x_axis.add_numbers(*filter( + lambda x : x != 0, + range(-8, 10, 2), + )) + axes.shift(DOWN) + self.add(axes) + + def func(x): + return np.exp(-0.1*x**2)*(1 + np.cos(TAU*x)) + graph = axes.get_graph(func) + self.add(graph) + graph.highlight(YELLOW) + + self.set_variables_as_attrs(axes, graph) + + def get_time_interval(self, t1, t2): + line = Line(*[ + self.axes.coords_to_point(t, 0) + for t in t1, t2 + ]) + rect = Rectangle( + stroke_width = 0, + fill_color = TEAL, + fill_opacity = 0.5, + ) + rect.match_width(line) + rect.stretch_to_fit_height(2.5) + rect.move_to(line, DOWN) + return rect + +class MoreToCover(TeacherStudentsScene): + def construct(self): + self.teacher_says( + "Much more to say...", + target_mode = "hooray", + run_time = 1, + ) + self.wait() + self.teacher_says( + "SO MUCH!", + target_mode = "surprised", + added_anims = [self.get_student_changes(*3*["happy"])], + run_time = 0.5 + ) + self.wait(2) + +class ShowUncertaintyPrinciple(Scene): + def construct(self): + title = TextMobject("Uncertainty principle") + self.add(title) + top_axes = Axes( + x_min = -SPACE_WIDTH, + x_max = SPACE_WIDTH, + y_min = 0, + y_max = 3, + y_axis_config = { + "unit_size" : 0.6, + "include_tip" : False, + } + ) + bottom_axes = top_axes.deepcopy() + arrow = Vector(DOWN, color = WHITE) + group = VGroup( + title, top_axes, arrow, bottom_axes + ) + group.arrange_submobjects(DOWN) + title.shift(MED_SMALL_BUFF*UP) + group.to_edge(UP) + fourier_word = TextMobject("Fourier transform") + fourier_word.next_to(arrow, RIGHT) + self.add(group, fourier_word) + + ghost_dot = Dot(RIGHT, fill_opacity = 0) + def get_bell_func(factor = 1): + return lambda x : 2*np.exp(-factor*x**2) + top_graph = top_axes.get_graph(get_bell_func()) + top_graph.highlight(YELLOW) + bottom_graph = bottom_axes.get_graph(get_bell_func()) + bottom_graph.highlight(RED) + def get_update_func(axes): + def update_graph(graph): + f = ghost_dot.get_center()[0] + if axes == bottom_axes: + f = 1./f + new_graph = axes.get_graph(get_bell_func(f)) + graph.points = new_graph.points + return update_graph + + factors = [0.3, 0.1, 2, 10, 100, 0.01, 0.5] + + self.play(ShowCreation(top_graph)) + self.play(ReplacementTransform( + top_graph.copy(), + bottom_graph, + )) + self.wait(2) + self.add(*[ + ContinualUpdateFromFunc(graph, get_update_func(axes)) + for graph, axes in (top_graph, top_axes), (bottom_graph, bottom_axes) + ]) + for factor in factors: + self.play( + ghost_dot.move_to, factor*RIGHT, + run_time = 2 + ) + self.wait() + +class XCoordinateLabelTypoFix(Scene): + def construct(self): + words = TextMobject("$x$-coordinate for center of mass") + words.highlight(RED) + self.add(words) + +class NextVideoWrapper(Scene): + def construct(self): + title = TextMobject("Next video") + 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(2) + +class SubscribeOrBinge(PiCreatureScene): + def construct(self): + morty = self.pi_creature + morty.center().to_edge(DOWN, LARGE_BUFF) + subscribe = TextMobject("Subscribe") + subscribe.highlight(RED) + subscribe.next_to(morty, UP+RIGHT) + binge = TextMobject("Binge") + binge.highlight(BLUE) + binge.next_to(morty, UP+LEFT) + + videos = VGroup(*[VideoIcon() for x in range(30)]) + colors = it.cycle([BLUE_D, BLUE_E, BLUE_C, GREY_BROWN]) + for video, color in zip(videos, colors): + video.highlight(color) + videos.move_to(binge.get_bottom(), UP) + video_anim = LaggedStart( + Succession, videos, + lambda v : ( + FadeIn, v, + ApplyMethod, v.shift, 5*DOWN, {"run_time" : 6}, + ), + run_time = 10 + ) + sub_arrow = Arrow( + subscribe.get_bottom(), + Dot().to_corner(DOWN+RIGHT, buff = LARGE_BUFF), + color = RED + ) + + for word in subscribe, binge: + word.save_state() + word.shift(DOWN) + word.set_fill(opacity = 0) + + self.play( + subscribe.restore, + morty.change, "raise_left_hand" + ) + self.play(GrowArrow(sub_arrow)) + self.wait() + self.play( + video_anim, + Succession( + AnimationGroup( + ApplyMethod(binge.restore), + ApplyMethod(morty.change, "raise_right_hand", binge), + ), + Blink, morty, + ApplyMethod, morty.change, "shruggie", videos, + Animation, Mobject(), {"run_time" : 2}, + Blink, morty, + Animation, Mobject(), {"run_time" : 4} + ) + ) class CloseWithAPuzzle(TeacherStudentsScene): @@ -3113,14 +4103,139 @@ class SponsorScreenGrab(PiCreatureScene): self.play(morty.change, mode, screen) self.wait(4) - - - - - - - - +class FourierEndScreen(PatreonEndScreen): + CONFIG = { + "specific_patrons" : [ + "CrypticSwarm", + "Ali Yahya", + "Juan Benet", + "Markus Persson", + "Damion Kistler", + "Burt Humburg", + "Yu Jun", + "Dave Nicponski", + "Kaustuv DeBiswas", + "Joseph John Cox", + "Luc Ritchie", + "Achille Brighton", + "Rish Kundalia", + "Yana Chernobilsky", + "Shìmín Kuang", + "Mathew Bramson", + "Jerry Ling", + "Mustafa Mahdi", + "Meshal Alshammari", + "Mayank M. Mehrotra", + "Lukas Biewald", + "Robert Teed", + "One on Epsilon", + "Samantha D. Suplee", + "Mark Govea", + "John Haley", + "Julian Pulgarin", + "Jeff Linse", + "Cooper Jones", + "Boris Veselinovich", + "Ryan Dahl", + "Ripta Pasay", + "Eric Lavault", + "Mads Elvheim", + "Andrew Busey", + "Randall Hunt", + "Desmos", + "Tianyu Ge", + "Awoo", + "Dr David G. Stork", + "Linh Tran", + "Jason Hise", + "Bernd Sing", + "Ankalagon", + "Mathias Jansson", + "David Clark", + "Ted Suzman", + "Eric Chow", + "Michael Gardner", + "Jonathan Eppele", + "Clark Gaebel", + "David Kedmey", + "Jordan Scales", + "Ryan Atallah", + "supershabam", + "1stViewMaths", + "Jacob Magnuson", + "Thomas Tarler", + "Isak Hietala", + "James Thornton", + "Egor Gumenuk", + "Waleed Hamied", + "Oliver Steele", + "Yaw Etse", + "David B", + "Julio Cesar Campo Neto", + "Delton Ding", + "George Chiesa", + "Chloe Zhou", + "Alexander Nye", + "Ross Garber", + "Wang HaoRan", + "Felix Tripier", + "Arthur Zey", + "Norton", + "Kevin Le", + "Alexander Feldman", + "David MacCumber", + ], + } + +class Thumbnail(Scene): + def construct(self): + title = TextMobject("Fourier\\\\", "Visualized") + title.highlight(YELLOW) + title.set_stroke(RED, 2) + title.scale(2.5) + title.add_background_rectangle() + + def func(t): + return np.cos(2*TAU*t) + np.cos(3*TAU*t) + np.cos(5*t) + fourier = get_fourier_transform(func, -5, 5) + + graph = FunctionGraph(func, x_min = -5, x_max = 5) + graph.highlight(BLUE) + fourier_graph = FunctionGraph(fourier, x_min = 0, x_max = 6) + fourier_graph.highlight(YELLOW) + for g in graph, fourier_graph: + g.stretch_to_fit_height(2) + g.stretch_to_fit_width(10) + g.set_stroke(width = 8) + + pol_graphs = VGroup() + for f in np.linspace(1.98, 2.02, 7): + pol_graph = ParametricFunction( + lambda t : complex_to_R3( + (2+np.cos(2*TAU*t)+np.cos(3*TAU*t))*np.exp(-complex(0, TAU*f*t)) + ), + t_min = -5, + t_max = 5, + num_graph_points = 200, + ) + pol_graph.match_color(graph) + pol_graph.scale_to_fit_height(2) + pol_graphs.add(pol_graph) + pol_graphs.arrange_submobjects(RIGHT, buff = LARGE_BUFF) + pol_graphs.gradient_highlight(BLUE_C, YELLOW) + pol_graphs.match_width(graph) + pol_graphs.set_stroke(width = 2) + + + self.clear() + title.center().to_edge(UP) + pol_graphs.scale_to_fit_width(2*SPACE_WIDTH - 1) + pol_graphs.center() + title.move_to(pol_graphs) + title.shift(SMALL_BUFF*LEFT) + graph.next_to(title, UP) + fourier_graph.next_to(title, DOWN) + self.add(pol_graphs, title, graph, fourier_graph) diff --git a/ben_playground.py b/ben_playground.py new file mode 100644 index 00000000..c03c8abe --- /dev/null +++ b/ben_playground.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python + +from helpers import * + +from mobject.tex_mobject import TexMobject +from mobject import Mobject +from mobject.image_mobject import ImageMobject +from mobject.vectorized_mobject import * +from mobject.point_cloud_mobject import PointCloudDot + +from animation.animation import Animation +from animation.transform import * +from animation.simple_animations import * +from animation.continual_animation import * +from animation.playground import * + +from topics.geometry import * +from topics.characters import * +from topics.functions import * +from topics.number_line import * +from topics.combinatorics import * +from scene import Scene +from camera import Camera +from mobject.svg_mobject import * +from mobject.tex_mobject import * + +from mobject.vectorized_mobject import * + +## To watch one of these scenes, run the following: +## python extract_scene.py -p file_name + +LIGHT_COLOR = YELLOW +DEGREES = 360/TAU +SWITCH_ON_RUN_TIME = 1.5 + + +class AmbientLight(VMobject): + + # Parameters are: + # * a source point + # * an opacity function + # * a light color + # * a max opacity + # * a radius (larger than the opacity's dropoff length) + # * the number of subdivisions (levels, annuli) + + CONFIG = { + "source_point" : ORIGIN, + "opacity_function" : lambda r : 1.0/(r+1.0)**2, + "color" : LIGHT_COLOR, + "max_opacity" : 1.0, + "num_levels" : 10, + "radius" : 5.0 + } + + def generate_points(self): + + # in theory, this method is only called once, right? + # so removing submobs shd not be necessary + for submob in self.submobjects: + self.remove(submob) + + # create annuli + dr = self.radius / self.num_levels + for r in np.arange(0, self.radius, dr): + alpha = self.max_opacity * self.opacity_function(r) + annulus = Annulus( + inner_radius = r, + outer_radius = r + dr, + color = self.color, + fill_opacity = alpha + ) + annulus.move_arc_center_to(self.source_point) + self.add(annulus) + + + + def move_source_to(self,point): + self.shift(point - self.source_point) + self.source_point = np.array(point) + # for submob in self.submobjects: + # if type(submob) == Annulus: + # submob.shift(self.source_point - submob.get_center()) + + def dimming(self,new_alpha): + old_alpha = self.max_opacity + self.max_opacity = new_alpha + for submob in self.submobjects: + old_submob_alpha = submob.fill_opacity + new_submob_alpha = old_submob_alpha * new_alpha/old_alpha + submob.set_fill(opacity = new_submob_alpha) + + +class Spotlight(VMobject): + + CONFIG = { + "source_point" : ORIGIN, + "opacity_function" : lambda r : 1.0/(r+1.0)**2, + "color" : LIGHT_COLOR, + "max_opacity" : 1.0, + "num_levels" : 10, + "radius" : 5.0, + "screen" : None, + "shadow" : VMobject(fill_color = RED, stroke_width = 0, fill_opacity = 1.0) + } + + def track_screen(self): + self.generate_points() + + def generate_points(self): + + for submob in self.submobjects: + self.remove(submob) + + if self.screen != None: + # look for the screen and create annular sectors + lower_angle, upper_angle = self.viewing_angles(self.screen) + dr = self.radius / self.num_levels + for r in np.arange(0, self.radius, dr): + alpha = self.max_opacity * self.opacity_function(r) + annular_sector = AnnularSector( + inner_radius = r, + outer_radius = r + dr, + color = self.color, + fill_opacity = alpha, + start_angle = lower_angle, + angle = upper_angle - lower_angle + ) + annular_sector.move_arc_center_to(self.source_point) + self.add(annular_sector) + + self.update_shadow(point = self.source_point) + self.add(self.shadow) + + + def viewing_angle_of_point(self,point): + distance_vector = point - self.source_point + angle = angle_of_vector(distance_vector) + return angle + + def viewing_angles(self,screen): + + viewing_angles = np.array(map(self.viewing_angle_of_point, + screen.get_anchors())) + lower_angle = upper_angle = 0 + if len(viewing_angles) != 0: + lower_angle = np.min(viewing_angles) + upper_angle = np.max(viewing_angles) + + return lower_angle, upper_angle + + def move_source_to(self,point): + self.source_point = np.array(point) + self.recalculate_sectors(point = point, screen = self.screen) + self.update_shadow(point = point) + + def recalculate_sectors(self, point = ORIGIN, screen = None): + if screen == None: + return + for submob in self.submobject_family(): + if type(submob) == AnnularSector: + lower_angle, upper_angle = self.viewing_angles(screen) + new_submob = AnnularSector( + start_angle = lower_angle, + angle = upper_angle - lower_angle, + inner_radius = submob.inner_radius, + outer_radius = submob.outer_radius + ) + new_submob.move_arc_center_to(point) + submob.points = new_submob.points + + def update_shadow(self,point = ORIGIN): + print "updating shadow" + use_point = point #self.source_point + self.shadow.points = self.screen.points + ray1 = self.screen.points[0] - use_point + ray2 = self.screen.points[-1] - use_point + ray1 = ray1/np.linalg.norm(ray1) * 100 + ray1 = rotate_vector(ray1,-TAU/16) + ray2 = ray2/np.linalg.norm(ray2) * 100 + ray2 = rotate_vector(ray2,TAU/16) + outpoint1 = self.screen.points[0] + ray1 + outpoint2 = self.screen.points[-1] + ray2 + self.shadow.add_control_points([outpoint2,outpoint1,self.screen.points[0]]) + self.shadow.mark_paths_closed = True + + + def dimming(self,new_alpha): + old_alpha = self.max_opacity + self.max_opacity = new_alpha + for submob in self.submobjects: + if type(submob) != AnnularSector: + # it's the shadow, don't dim it + continue + old_submob_alpha = submob.fill_opacity + new_submob_alpha = old_submob_alpha * new_alpha/old_alpha + submob.set_fill(opacity = new_submob_alpha) + + + + +class SwitchOn(LaggedStart): + CONFIG = { + "lag_ratio": 0.2, + "run_time": SWITCH_ON_RUN_TIME + } + + def __init__(self, light, **kwargs): + if not isinstance(light,AmbientLight) and not isinstance(light,Spotlight): + raise Exception("Only LightCones and Candles can be switched on") + LaggedStart.__init__(self, + FadeIn, light, **kwargs) + + +class SwitchOff(LaggedStart): + CONFIG = { + "lag_ratio": 0.2, + "run_time": SWITCH_ON_RUN_TIME + } + + def __init__(self, light, **kwargs): + if not isinstance(light,AmbientLight) and not isinstance(light,Spotlight): + raise Exception("Only LightCones and Candles can be switched on") + light.submobjects = light.submobjects[::-1] + LaggedStart.__init__(self, + FadeOut, light, **kwargs) + light.submobjects = light.submobjects[::-1] + + + + + +class ScreenTracker(ContinualAnimation): + def __init__(self, mobject, **kwargs): + ContinualAnimation.__init__(self, mobject, **kwargs) + + def update_mobject(self, dt): + self.mobject.recalculate_sectors( + point = self.mobject.source_point, + screen = self.mobject.screen) + self.mobject.update_shadow(self.mobject.source_point) + + + +class IntroScene(Scene): + def construct(self): + + screen = Line([2,-2,0],[1,2,0]).shift([1,0,0]) + self.add(screen) + + ambient_light = AmbientLight( + source_point = np.array([-1,1,0]), + max_opacity = 1.0, + opacity_function = lambda r: 1.0/(r/2+1)**2, + num_levels = 4, + ) + + spotlight = Spotlight( + source_point = np.array([-1,1,0]), + max_opacity = 1.0, + opacity_function = lambda r: 1.0/(r/2+1)**2, + num_levels = 4, + screen = screen, + ) + + self.add(spotlight) + + screen_updater = ScreenTracker(spotlight) + #self.add(ca) + + #self.play(SwitchOn(ambient_light)) + #self.play(ApplyMethod(ambient_light.move_source_to,[-3,1,0])) + #self.play(SwitchOn(spotlight)) + + self.add(screen_updater) + self.play(ApplyMethod(spotlight.screen.rotate,TAU/8)) + self.remove(screen_updater) + self.play(ApplyMethod(spotlight.move_source_to,[-3,-1,0])) + self.add(screen_updater) + spotlight.source_point = [-3,-1,0] + + self.play(ApplyMethod(spotlight.dimming,0.2)) + #self.play(ApplyMethod(spotlight.move_source_to,[-4,0,0])) + + #self.wait() + + + diff --git a/mobject/mobject.py b/mobject/mobject.py index b824d7c7..53509d0e 100644 --- a/mobject/mobject.py +++ b/mobject/mobject.py @@ -455,14 +455,14 @@ class Mobject(Container): **kwargs ) - def match_width(self, mobject): - return self.match_dim(mobject, 0) + def match_width(self, mobject, **kwargs): + return self.match_dim(mobject, 0, **kwargs) - def match_height(self, mobject): - return self.match_dim(mobject, 1) + def match_height(self, mobject, **kwargs): + return self.match_dim(mobject, 1, **kwargs) - def match_depth(self, mobject): - return self.match_dim(mobject, 2) + def match_depth(self, mobject, **kwargs): + return self.match_dim(mobject, 2, **kwargs) ## Color functions diff --git a/mobject/vectorized_mobject.py b/mobject/vectorized_mobject.py index 4b1a37e6..f663367e 100644 --- a/mobject/vectorized_mobject.py +++ b/mobject/vectorized_mobject.py @@ -100,12 +100,12 @@ class VMobject(Mobject): #match styles accordingly submobs1, submobs2 = self.submobjects, vmobject.submobjects if len(submobs1) == 0: - return + return self elif len(submobs2) == 0: submobs2 = [vmobject] for sm1, sm2 in zip(*make_even(submobs1, submobs2)): sm1.match_style(sm2) - return + return self def fade(self, darkness = 0.5): for submob in self.submobject_family(): @@ -353,7 +353,8 @@ class VMobject(Mobject): for index in range(num_curves): curr_bezier_points = self.points[3*index:3*index+4] num_inter_curves = sum(index_allocation == index) - alphas = np.arange(0, num_inter_curves+1)/float(num_inter_curves) + alphas = np.linspace(0, 1, num_inter_curves+1) + # alphas = np.arange(0, num_inter_curves+1)/float(num_inter_curves) for a, b in zip(alphas, alphas[1:]): new_points = partial_bezier_points( curr_bezier_points, a, b diff --git a/stage_scenes.py b/stage_scenes.py index e79d341c..7177d614 100644 --- a/stage_scenes.py +++ b/stage_scenes.py @@ -18,9 +18,7 @@ def get_sorted_scene_names(module_name): for line_no in sorted(line_to_scene.keys()) ] - - -def stage_animaions(module_name): +def stage_animations(module_name): scene_names = get_sorted_scene_names(module_name) animation_dir = os.path.join( ANIMATIONS_DIR, module_name.replace(".py", "") @@ -32,19 +30,27 @@ def stage_animaions(module_name): sorted_files.append( os.path.join(animation_dir, clip) ) - for f in os.listdir(STAGED_SCENES_DIR): - os.remove(os.path.join(STAGED_SCENES_DIR, f)) - for f, count in zip(sorted_files, it.count()): + staged_scenes_dir = os.path.join(animation_dir, "staged_scenes") + count = 0 + while True: + staged_scenes_dir = os.path.join( + animation_dir, "staged_scenes_%d"%count + ) + if not os.path.exists(staged_scenes_dir): + os.makedirs(staged_scenes_dir) + break + #Otherwise, keep trying new names until + #there is a free one + count += 1 + for count, f in enumerate(sorted_files): symlink_name = os.path.join( - STAGED_SCENES_DIR, + staged_scenes_dir, "Scene_%03d"%count + f.split(os.sep)[-1] ) os.symlink(f, symlink_name) - - if __name__ == "__main__": if len(sys.argv) < 2: raise Exception("No module given.") module_name = sys.argv[1] - stage_animaions(module_name) \ No newline at end of file + stage_animations(module_name) \ No newline at end of file diff --git a/topics/characters.py b/topics/characters.py index 3e2214a2..5f454c00 100644 --- a/topics/characters.py +++ b/topics/characters.py @@ -443,7 +443,9 @@ class PiCreatureScene(Scene): added_anims = kwargs.pop("added_anims", []) anims = [] - on_screen_mobjects = self.get_mobjects() + on_screen_mobjects = self.camera.extract_mobject_family_members( + self.get_mobjects() + ) def has_bubble(pi): return hasattr(pi, "bubble") and \ pi.bubble is not None and \ @@ -478,7 +480,7 @@ class PiCreatureScene(Scene): ] anims += added_anims - self.play(*anims) + self.play(*anims, **kwargs) def pi_creature_says(self, *args, **kwargs): self.introduce_bubble( diff --git a/topics/geometry.py b/topics/geometry.py index bbbd12a5..2312e74b 100644 --- a/topics/geometry.py +++ b/topics/geometry.py @@ -97,9 +97,6 @@ class Arc(VMobject): return self - - - class Circle(Arc): CONFIG = { "color" : RED, @@ -128,6 +125,19 @@ class Dot(Circle): self.shift(point) self.init_colors() +class Ellipse(VMobject): + CONFIG = { + "width" : 2, + "height" : 1 + } + + def generate_points(self): + circle = Circle(radius = 1) + circle = circle.stretch_to_fit_width(self.width) + circle = circle.stretch_to_fit_height(self.height) + self.points = circle.points + + class AnnularSector(VMobject): CONFIG = { "inner_radius" : 1, @@ -177,6 +187,7 @@ class AnnularSector(VMobject): self.shift(v) return self + class Sector(AnnularSector): CONFIG = { "outer_radius" : 1, @@ -203,10 +214,12 @@ class Annulus(Circle): } def generate_points(self): + self.points = [] self.radius = self.outer_radius - Circle.generate_points(self) + outer_circle = Circle(radius = self.outer_radius) inner_circle = Circle(radius=self.inner_radius) inner_circle.flip() + self.points = outer_circle.points self.add_subpath(inner_circle.points) class Line(VMobject): @@ -231,6 +244,10 @@ class Line(VMobject): ]) self.account_for_buff() + def set_path_arc(self,new_value): + self.path_arc = new_value + self.generate_points() + def account_for_buff(self): length = self.get_arc_length() if length < 2*self.buff or self.buff == 0: @@ -336,6 +353,17 @@ class Line(VMobject): self.shift(new_start - self.get_start()) return self + def insert_n_anchor_points(self, n): + if not self.path_arc: + n_anchors = self.get_num_anchor_points() + new_num_points = 3*(n_anchors + n)+1 + self.points = np.array([ + self.point_from_proportion(alpha) + for alpha in np.linspace(0, 1, new_num_points) + ]) + else: + VMobject.insert_n_anchor_points(self, n) + class DashedLine(Line): CONFIG = { "dashed_segment_length" : 0.05 @@ -511,6 +539,7 @@ class Arrow(Line): Line.put_start_and_end_on(self, *args, **kwargs) self.set_tip_points(self.tip, preserve_normal = False) self.set_rectangular_stem_points() + return self def scale(self, scale_factor, **kwargs): Line.scale(self, scale_factor, **kwargs) @@ -520,6 +549,9 @@ class Arrow(Line): self.set_rectangular_stem_points() return self + def copy(self): + return self.deepcopy() + class Vector(Arrow): CONFIG = { "color" : YELLOW, diff --git a/topics/number_line.py b/topics/number_line.py index 7aa5fa9b..623abf4e 100644 --- a/topics/number_line.py +++ b/topics/number_line.py @@ -200,14 +200,21 @@ class Axes(VGroup): self.y_axis.point_to_number(point), ) - def get_graph(self, function, num_graph_points = None, **kwargs): + def get_graph( + self, function, num_graph_points = None, + x_min = None, + x_max = None, + **kwargs + ): kwargs["fill_opacity"] = kwargs.get("fill_opacity", 0) kwargs["num_anchor_points"] = \ num_graph_points or self.default_num_graph_points + x_min = x_min or self.x_min + x_max = x_max or self.x_max graph = ParametricFunction( lambda t : self.coords_to_point(t, function(t)), - t_min = self.x_min, - t_max = self.x_max, + t_min = x_min, + t_max = x_max, **kwargs ) graph.underlying_function = function diff --git a/topics/numerals.py b/topics/numerals.py index 44e28323..1bcc61fe 100644 --- a/topics/numerals.py +++ b/topics/numerals.py @@ -12,7 +12,7 @@ class DecimalNumber(VMobject): "num_decimal_points" : 2, "digit_to_digit_buff" : 0.05, "show_ellipsis" : False, - "unit" : None, + "unit" : None, #Aligned to bottom unless it starts with "^" "include_background_rectangle" : False, } def __init__(self, number, **kwargs): @@ -41,8 +41,17 @@ class DecimalNumber(VMobject): if self.show_ellipsis: self.add(TexMobject("\\dots")) - if self.unit is not None: - self.add(TexMobject(self.unit)) + + if num_string.startswith("-"): + minus = self.submobjects[0] + minus.next_to( + self.submobjects[1], LEFT, + buff = self.digit_to_digit_buff + ) + + if self.unit != None: + self.unit_sign = TexMobject(self.unit) + self.add(self.unit_sign) self.arrange_submobjects( buff = self.digit_to_digit_buff, @@ -54,8 +63,8 @@ class DecimalNumber(VMobject): for i, c in enumerate(num_string): if c == "-" and len(num_string) > i+1: self[i].align_to(self[i+1], alignment_vect = UP) - if self.unit == "\\circ": - self[-1].align_to(self, UP) + if self.unit and self.unit.startswith("^"): + self.unit_sign.align_to(self, UP) # if self.include_background_rectangle: self.add_background_rectangle() @@ -80,7 +89,7 @@ class ChangingDecimal(Animation): "num_decimal_points" : None, "show_ellipsis" : None, "position_update_func" : None, - "tracked_mobject" : None + "tracked_mobject" : None, } def __init__(self, decimal_number_mobject, number_update_func, **kwargs): digest_config(self, kwargs, locals())