diff --git a/topics/light.py b/topics/light.py new file mode 100644 index 00000000..67ca3f39 --- /dev/null +++ b/topics/light.py @@ -0,0 +1,512 @@ +from helpers import * + +from mobject.tex_mobject import TexMobject +from mobject import Mobject +from mobject.vectorized_mobject import * + +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.functions import * +from scene import Scene +from camera import Camera +from mobject.svg_mobject import * +from topics.three_dimensions import * + +from scipy.spatial import ConvexHull + +import traceback + +LIGHT_COLOR = YELLOW +SWITCH_ON_RUN_TIME = 1.5 +FAST_SWITCH_ON_RUN_TIME = 0.1 +NUM_LEVELS = 30 +NUM_CONES = 7 # in first lighthouse scene +NUM_VISIBLE_CONES = 5 # ibidem +ARC_TIP_LENGTH = 0.2 +AMBIENT_FULL = 0.5 +AMBIENT_DIMMED = 0.2 +SPOTLIGHT_FULL = 0.9 +SPOTLIGHT_DIMMED = 0.2 + +LIGHT_COLOR = YELLOW +DEGREES = TAU/360 + +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) + + + +class LightSource(VMobject): + + # combines: + # a lighthouse + # an ambient light + # a spotlight + # and a shadow + + CONFIG = { + "source_point": ORIGIN, + "color": LIGHT_COLOR, + "num_levels": 10, + "radius": 5, + "screen": None, + "opacity_function": inverse_quadratic(1,2,1), + "max_opacity_ambient": AMBIENT_FULL, + "max_opacity_spotlight": SPOTLIGHT_FULL + } + + def generate_points(self): + print "LightSource.generate_points" + self.lighthouse = Lighthouse() + self.ambient_light = AmbientLight( + source_point = self.source_point, + color = self.color, + num_levels = self.num_levels, + radius = self.radius, + opacity_function = self.opacity_function, + max_opacity = self.max_opacity_ambient + ) + if self.has_screen(): + self.spotlight = Spotlight( + source_point = self.source_point, + color = self.color, + num_levels = self.num_levels, + radius = self.radius, + screen = self.screen, + opacity_function = self.opacity_function, + max_opacity = self.max_opacity_spotlight + ) + else: + self.spotlight = Spotlight() + + self.shadow = VMobject(fill_color = "BLACK", fill_opacity = 1.0, stroke_color = BLACK) + self.lighthouse.next_to(self.source_point,DOWN,buff = 0) + self.ambient_light.move_source_to(self.source_point) + + if self.has_screen(): + self.spotlight.move_source_to(self.source_point) + self.update_shadow() + + self.add(self.ambient_light,self.spotlight,self.lighthouse, self.shadow) + + def has_screen(self): + return (self.screen != None) + + def dim_ambient(self): + self.set_max_opacity_ambient(AMBIENT_DIMMED) + + def set_max_opacity_ambient(self,new_opacity): + self.max_opacity_ambient = new_opacity + self.ambient_light.dimming(new_opacity) + + def dim_spotlight(self): + self.set_max_opacity_spotlight(SPOTLIGHT_DIMMED) + + def set_max_opacity_spotlight(self,new_opacity): + self.max_opacity_spotlight = new_opacity + self.spotlight.dimming(new_opacity) + + def set_screen(self, new_screen): + if self.has_screen(): + self.spotlight.screen = new_screen + else: + self.remove(self.spotlight) + self.spotlight = Spotlight( + source_point = self.source_point, + color = self.color, + num_levels = self.num_levels, + radius = self.radius, + screen = new_screen + ) + self.spotlight.move_source_to(self.source_point) + self.add(self.spotlight) + + # in any case + self.screen = new_screen + + + + + def move_source_to(self,point): + print "LightSource.move_source_to" + apoint = np.array(point) + v = apoint - self.source_point + self.source_point = apoint + self.lighthouse.next_to(apoint,DOWN,buff = 0) + self.ambient_light.move_source_to(apoint) + #if self.has_screen(): + # self.spotlight.move_source_to(apoint) + #self.update() + return self + + + + + + + + def set_radius(self,new_radius): + self.radius = new_radius + self.ambient_light.radius = new_radius + self.spotlight.radius = new_radius + + def update(self): + print "LightSource.update" + self.spotlight.update_sectors() + self.update_shadow() + + + def update_shadow(self): + + point = self.source_point + projected_screen_points = [] + if not self.has_screen(): + return + for point in self.screen.get_anchors(): + projected_screen_points.append(self.spotlight.project(point)) + + projected_source = project_along_vector(self.source_point,self.spotlight.projection_direction()) + + projected_point_cloud_3d = np.append(projected_screen_points, + np.reshape(projected_source,(1,3)),axis = 0) + rotation_matrix = z_to_vector(self.spotlight.projection_direction()) + back_rotation_matrix = rotation_matrix.T # i. e. its inverse + + rotated_point_cloud_3d = np.dot(projected_point_cloud_3d,back_rotation_matrix.T) + # these points now should all have z = 0 + point_cloud_2d = rotated_point_cloud_3d[:,:2] + # now we can compute the convex hull + hull_2d = ConvexHull(point_cloud_2d) # guaranteed to run ccw + hull = [] + + # we also need the projected source point + source_point_2d = np.dot(self.spotlight.project(self.source_point),back_rotation_matrix.T)[:2] + + index = 0 + for point in point_cloud_2d[hull_2d.vertices]: + if np.all(point - source_point_2d < 1.0e-6): + source_index = index + continue + point_3d = np.array([point[0], point[1], 0]) + hull.append(point_3d) + index += 1 + + index = source_index + + hull_mobject = VMobject() + hull_mobject.set_points_as_corners(hull) + hull_mobject.apply_matrix(rotation_matrix) + + + anchors = hull_mobject.get_anchors() + + # add two control points for the outer cone + + + ray1 = anchors[index - 1] - projected_source + ray1 = ray1/np.linalg.norm(ray1) * 100 + ray2 = anchors[index] - projected_source + ray2 = ray2/np.linalg.norm(ray2) * 100 + outpoint1 = anchors[index - 1] + ray1 + outpoint2 = anchors[index] + ray2 + + new_anchors = anchors[:index] + new_anchors = np.append(new_anchors,np.array([outpoint1, outpoint2]),axis = 0) + new_anchors = np.append(new_anchors,anchors[index:],axis = 0) + self.shadow.set_points_as_corners(new_anchors) + + # shift it one unit closer to the camera so it is in front of the spotlight + #self.shadow.shift(-500*self.projection_direction()) + self.shadow.mark_paths_closed = True + + + +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 AmbientLights and Spotlights 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 AmbientLights and Spotlights can be switched off") + light.submobjects = light.submobjects[::-1] + LaggedStart.__init__(self, + FadeOut, light, **kwargs) + light.submobjects = light.submobjects[::-1] + + + + +class Lighthouse(SVGMobject): + CONFIG = { + "file_name" : "lighthouse", + "height" : 0.5 + } + + def move_to(self,point): + self.next_to(point, DOWN, buff = 0) + + +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): + print "AmbientLight.generate_points" + self.source_point = np.array(self.source_point) + + # 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): + + + for line in traceback.format_stack(): + print line.strip() + + print "AmbientLight.move_source_to blablub" + v = np.array(point) - self.source_point + print "test" + self.source_point = np.array(point) + self.shift(v) + return self + + + + + + + + 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, + "camera": None + } + + def projection_direction(self): + if self.camera == None: + return OUT + else: + v = self.camera.get_cartesian_coords() + return v/np.linalg.norm(v) + + def project(self,point): + v = self.projection_direction() + w = project_along_vector(point,v) + return w + + def generate_points(self): + + self.submobjects = [] + + 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 + lower_ray, upper_ray = self.viewing_rays(self.screen) + + for r in np.arange(0, self.radius, dr): + new_sector = self.new_sector(r,dr,lower_angle,upper_angle) + self.add(new_sector) + + #self.update_shadow(point = self.source_point) + #self.add_to_back(self.shadow) + + def new_sector(self,r,dr,lower_angle,upper_angle): + + 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 + ) + # rotate (not project) it into the viewing plane + rotation_matrix = z_to_vector(self.projection_direction()) + annular_sector.apply_matrix(rotation_matrix) + # now rotate it inside that plane + rotated_RIGHT = np.dot(RIGHT, rotation_matrix.T) + projected_RIGHT = self.project(RIGHT) + omega = angle_between_vectors(rotated_RIGHT,projected_RIGHT) + annular_sector.rotate(omega, axis = self.projection_direction()) + annular_sector.move_arc_center_to(self.source_point) + + return annular_sector + + def viewing_angle_of_point(self,point): + # as measured from the positive x-axis + v1 = self.project(RIGHT) + v2 = self.project(np.array(point) - self.source_point) + absolute_angle = angle_between_vectors(v1, v2) + # determine the angle's sign depending on their plane's + # choice of orientation. That choice is set by the camera + # position, i. e. projection direction + if np.dot(self.projection_direction(),np.cross(v1, v2)) > 0: + return absolute_angle + else: + return -absolute_angle + + + def viewing_angles(self,screen): + + screen_points = screen.get_anchors() + projected_screen_points = map(self.project,screen_points) + + viewing_angles = np.array(map(self.viewing_angle_of_point, + projected_screen_points)) + 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 viewing_rays(self,screen): + + lower_angle, upper_angle = self.viewing_angles(screen) + projected_RIGHT = self.project(RIGHT)/np.linalg.norm(self.project(RIGHT)) + lower_ray = rotate_vector(projected_RIGHT,lower_angle, axis = self.projection_direction()) + upper_ray = rotate_vector(projected_RIGHT,upper_angle, axis = self.projection_direction()) + + return lower_ray, upper_ray + + + 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.update_sectors() + return self + + + def update_sectors(self): + if self.screen == None: + return + for submob in self.submobject_family(): + if type(submob) == AnnularSector: + lower_angle, upper_angle = self.viewing_angles(self.screen) + dr = submob.outer_radius - submob.inner_radius + new_submob = self.new_sector(submob.inner_radius,dr,lower_angle,upper_angle) + submob.points = new_submob.points + + + + + + + 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 change_opacity_function(self,new_f): + self.opacity_function = new_f + dr = self.radius/self.num_levels + + sectors = [] + for submob in self.submobjects: + if type(submob) == AnnularSector: + sectors.append(submob) + + 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) + + + +class ScreenTracker(ContinualAnimation): + + def update_mobject(self, dt): + self.mobject.update() +