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/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/topics/geometry.py b/topics/geometry.py index 42cec5a9..2312e74b 100644 --- a/topics/geometry.py +++ b/topics/geometry.py @@ -125,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, @@ -174,6 +187,7 @@ class AnnularSector(VMobject): self.shift(v) return self + class Sector(AnnularSector): CONFIG = { "outer_radius" : 1, @@ -200,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): @@ -228,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: diff --git a/topics/numerals.py b/topics/numerals.py index 0a1337a0..1bcc61fe 100644 --- a/topics/numerals.py +++ b/topics/numerals.py @@ -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.startswith("^"): - 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()