mirror of
https://github.com/3b1b/manim.git
synced 2025-07-29 21:12:35 +08:00
Add warning about light_source
This commit is contained in:
580
topics/light.py
580
topics/light.py
@ -42,11 +42,293 @@ inverse_power_law = lambda maxint,scale,cutoff,exponent: \
|
||||
inverse_quadratic = lambda maxint,scale,cutoff: inverse_power_law(maxint,scale,cutoff,2)
|
||||
|
||||
|
||||
class SwitchOn(LaggedStart):
|
||||
CONFIG = {
|
||||
"lag_ratio": 0.2,
|
||||
"run_time": SWITCH_ON_RUN_TIME
|
||||
}
|
||||
|
||||
# Note: Overall, this class seems perfectly reasonable to me, the main
|
||||
# thing to be wary of is that calling self.add(submob) puts that submob
|
||||
# at the end of the submobjects list, and hence on top of everything else
|
||||
# which is why the shadow might sometimes end up behind the spotlight
|
||||
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" : LIGHTHOUSE_HEIGHT,
|
||||
"fill_color" : WHITE,
|
||||
"fill_opacity" : 1.0,
|
||||
}
|
||||
|
||||
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": VectorizedPoint(location = ORIGIN, stroke_width = 0, fill_opacity = 0),
|
||||
"opacity_function" : lambda r : 1.0/(r+1.0)**2,
|
||||
"color" : LIGHT_COLOR,
|
||||
"max_opacity" : 1.0,
|
||||
"num_levels" : NUM_LEVELS,
|
||||
"radius" : 5.0
|
||||
}
|
||||
|
||||
def generate_points(self):
|
||||
# in theory, this method is only called once, right?
|
||||
# so removing submobs shd not be necessary
|
||||
#
|
||||
# Note: Usually, yes, it is only called within Mobject.__init__,
|
||||
# but there is no strong guarantee of that, and you may want certain
|
||||
# update functions to regenerate points here and there.
|
||||
for submob in self.submobjects:
|
||||
self.remove(submob)
|
||||
|
||||
self.add(self.source_point)
|
||||
|
||||
# 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_to(self.get_source_point())
|
||||
self.add(annulus)
|
||||
|
||||
|
||||
|
||||
def move_source_to(self,point):
|
||||
#old_source_point = self.get_source_point()
|
||||
#self.shift(point - old_source_point)
|
||||
self.move_to(point)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def get_source_point(self):
|
||||
return self.source_point.get_location()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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": VectorizedPoint(location = ORIGIN, stroke_width = 0, fill_opacity = 0),
|
||||
"opacity_function" : lambda r : 1.0/(r/2+1.0)**2,
|
||||
"color" : GREEN, # LIGHT_COLOR,
|
||||
"max_opacity" : 1.0,
|
||||
"num_levels" : 10,
|
||||
"radius" : 10.0,
|
||||
"screen" : None,
|
||||
"camera_mob": None
|
||||
}
|
||||
|
||||
def projection_direction(self):
|
||||
# Note: This seems reasonable, though for it to work you'd
|
||||
# need to be sure that any 3d scene including a spotlight
|
||||
# somewhere assigns that spotlights "camera" attribute
|
||||
# to be the camera associated with that scene.
|
||||
if self.camera_mob == None:
|
||||
return OUT
|
||||
else:
|
||||
[phi, theta, r] = self.camera_mob.get_center()
|
||||
v = np.array([np.sin(phi)*np.cos(theta), np.sin(phi)*np.sin(theta), np.cos(phi)])
|
||||
return v #/np.linalg.norm(v)
|
||||
|
||||
def project(self,point):
|
||||
v = self.projection_direction()
|
||||
w = project_along_vector(point,v)
|
||||
return w
|
||||
|
||||
def get_source_point(self):
|
||||
return self.source_point.get_location()
|
||||
|
||||
def generate_points(self):
|
||||
self.submobjects = []
|
||||
|
||||
self.add(self.source_point)
|
||||
|
||||
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)
|
||||
|
||||
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.get_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.get_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)
|
||||
|
||||
if upper_angle - lower_angle > TAU/2:
|
||||
lower_angle, upper_angle = upper_angle, lower_angle + TAU
|
||||
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.set_location(np.array(point))
|
||||
#self.source_point.move_to(np.array(point))
|
||||
#self.move_to(point)
|
||||
self.update_sectors()
|
||||
return self
|
||||
|
||||
def update_sectors(self):
|
||||
if self.screen == None:
|
||||
return
|
||||
for submob in self.submobjects:
|
||||
if type(submob) == AnnularSector:
|
||||
lower_angle, upper_angle = self.viewing_angles(self.screen)
|
||||
#dr = submob.outer_radius - submob.inner_radius
|
||||
dr = self.radius / self.num_levels
|
||||
new_submob = self.new_sector(
|
||||
submob.inner_radius, dr, lower_angle, upper_angle
|
||||
)
|
||||
# submob.points = new_submob.points
|
||||
# submob.set_fill(opacity = 10 * self.opacity_function(submob.outer_radius))
|
||||
Transform(submob, new_submob).update(1)
|
||||
|
||||
def dimming(self,new_alpha):
|
||||
old_alpha = self.max_opacity
|
||||
self.max_opacity = new_alpha
|
||||
for submob in self.submobjects:
|
||||
# Note: Maybe it'd be best to have a Shadow class so that the
|
||||
# type can be checked directly?
|
||||
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)
|
||||
|
||||
# Warning: This class is likely quite buggy.
|
||||
class LightSource(VMobject):
|
||||
# combines:
|
||||
# a lighthouse
|
||||
@ -308,296 +590,6 @@ class LightSource(VMobject):
|
||||
# shift it closer to the camera so it is in front of the spotlight
|
||||
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" : LIGHTHOUSE_HEIGHT,
|
||||
"fill_color" : WHITE,
|
||||
"fill_opacity" : 1.0,
|
||||
}
|
||||
|
||||
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": VectorizedPoint(location = ORIGIN, stroke_width = 0, fill_opacity = 0),
|
||||
"opacity_function" : lambda r : 1.0/(r+1.0)**2,
|
||||
"color" : LIGHT_COLOR,
|
||||
"max_opacity" : 1.0,
|
||||
"num_levels" : NUM_LEVELS,
|
||||
"radius" : 5.0
|
||||
}
|
||||
|
||||
def generate_points(self):
|
||||
# in theory, this method is only called once, right?
|
||||
# so removing submobs shd not be necessary
|
||||
#
|
||||
# Note: Usually, yes, it is only called within Mobject.__init__,
|
||||
# but there is no strong guarantee of that, and you may want certain
|
||||
# update functions to regenerate points here and there.
|
||||
for submob in self.submobjects:
|
||||
self.remove(submob)
|
||||
|
||||
self.add(self.source_point)
|
||||
|
||||
# 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_to(self.get_source_point())
|
||||
self.add(annulus)
|
||||
|
||||
|
||||
|
||||
def move_source_to(self,point):
|
||||
#old_source_point = self.get_source_point()
|
||||
#self.shift(point - old_source_point)
|
||||
self.move_to(point)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def get_source_point(self):
|
||||
return self.source_point.get_location()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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": VectorizedPoint(location = ORIGIN, stroke_width = 0, fill_opacity = 0),
|
||||
"opacity_function" : lambda r : 1.0/(r/2+1.0)**2,
|
||||
"color" : GREEN, # LIGHT_COLOR,
|
||||
"max_opacity" : 1.0,
|
||||
"num_levels" : 10,
|
||||
"radius" : 10.0,
|
||||
"screen" : None,
|
||||
"camera_mob": None
|
||||
}
|
||||
|
||||
def projection_direction(self):
|
||||
# Note: This seems reasonable, though for it to work you'd
|
||||
# need to be sure that any 3d scene including a spotlight
|
||||
# somewhere assigns that spotlights "camera" attribute
|
||||
# to be the camera associated with that scene.
|
||||
if self.camera_mob == None:
|
||||
return OUT
|
||||
else:
|
||||
[phi, theta, r] = self.camera_mob.get_center()
|
||||
v = np.array([np.sin(phi)*np.cos(theta), np.sin(phi)*np.sin(theta), np.cos(phi)])
|
||||
return v #/np.linalg.norm(v)
|
||||
|
||||
def project(self,point):
|
||||
v = self.projection_direction()
|
||||
w = project_along_vector(point,v)
|
||||
return w
|
||||
|
||||
def get_source_point(self):
|
||||
return self.source_point.get_location()
|
||||
|
||||
def generate_points(self):
|
||||
self.submobjects = []
|
||||
|
||||
self.add(self.source_point)
|
||||
|
||||
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)
|
||||
|
||||
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.get_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.get_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)
|
||||
|
||||
if upper_angle - lower_angle > TAU/2:
|
||||
lower_angle, upper_angle = upper_angle, lower_angle + TAU
|
||||
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.set_location(np.array(point))
|
||||
#self.source_point.move_to(np.array(point))
|
||||
#self.move_to(point)
|
||||
self.update_sectors()
|
||||
return self
|
||||
|
||||
def update_sectors(self):
|
||||
if self.screen == None:
|
||||
return
|
||||
for submob in self.submobjects:
|
||||
if type(submob) == AnnularSector:
|
||||
lower_angle, upper_angle = self.viewing_angles(self.screen)
|
||||
#dr = submob.outer_radius - submob.inner_radius
|
||||
dr = self.radius / self.num_levels
|
||||
new_submob = self.new_sector(
|
||||
submob.inner_radius, dr, lower_angle, upper_angle
|
||||
)
|
||||
# submob.points = new_submob.points
|
||||
# submob.set_fill(opacity = 10 * self.opacity_function(submob.outer_radius))
|
||||
Transform(submob, new_submob).update(1)
|
||||
|
||||
def dimming(self,new_alpha):
|
||||
old_alpha = self.max_opacity
|
||||
self.max_opacity = new_alpha
|
||||
for submob in self.submobjects:
|
||||
# Note: Maybe it'd be best to have a Shadow class so that the
|
||||
# type can be checked directly?
|
||||
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 __init__(self, light_source, **kwargs):
|
||||
self.light_source = light_source
|
||||
|
Reference in New Issue
Block a user