Merge branch 'master' of https://github.com/3b1b/manim into WindingNumber

This commit is contained in:
Sridhar Ramesh 2018-03-06 12:30:36 -08:00
commit 10d6739058
17 changed files with 9568 additions and 1956 deletions

View file

@ -24,7 +24,7 @@ pip install -r requirements.txt
``` ```
Note: pip will install the python module `aggdraw` from Note: pip will install the python module `aggdraw` from
https://github.com/scottopell/aggdraw-64bits/ and it might requires additional https://github.com/scottopell/aggdraw-64bits/ and it might have additional
dependencies. dependencies.
This doesn't install freetype, but I don't think it's required for this project This doesn't install freetype, but I don't think it's required for this project

File diff suppressed because it is too large Load diff

View file

@ -358,7 +358,6 @@ class Succession(Animation):
Each arg will either be an animation, or an animation class Each arg will either be an animation, or an animation class
followed by its arguments (and potentially a dict for followed by its arguments (and potentially a dict for
configuration). configuration).
For example, For example,
Succession( Succession(
ShowCreation(circle), ShowCreation(circle),
@ -539,4 +538,3 @@ class EmptyAnimation(Animation):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
return Animation.__init__(self, Group(), *args, **kwargs) return Animation.__init__(self, Group(), *args, **kwargs)

View file

@ -101,9 +101,15 @@ class Swap(CyclicReplace):
pass #Renaming, more understandable for two entries pass #Renaming, more understandable for two entries
class GrowFromPoint(Transform): class GrowFromPoint(Transform):
CONFIG = {
"point_color" : None,
}
def __init__(self, mobject, point, **kwargs): def __init__(self, mobject, point, **kwargs):
digest_config(self, kwargs)
target = mobject.copy() target = mobject.copy()
point_mob = Point(point) point_mob = Point(point)
if self.point_color:
point_mob.highlight(self.point_color)
mobject.replace(point_mob) mobject.replace(point_mob)
mobject.highlight(point_mob.get_color()) mobject.highlight(point_mob.get_color())
Transform.__init__(self, mobject, target, **kwargs) Transform.__init__(self, mobject, target, **kwargs)

View file

@ -18,7 +18,6 @@ from camera import Camera
HELP_MESSAGE = """ HELP_MESSAGE = """
Usage: Usage:
python extract_scene.py <module> [<scene name>] python extract_scene.py <module> [<scene name>]
-p preview in low quality -p preview in low quality
-s show and save picture of last frame -s show and save picture of last frame
-w write result to file [this is default if nothing else is stated] -w write result to file [this is default if nothing else is stated]
@ -35,7 +34,6 @@ SCENE_NOT_FOUND_MESSAGE = """
CHOOSE_NUMBER_MESSAGE = """ CHOOSE_NUMBER_MESSAGE = """
Choose number corresponding to desired scene/arguments. Choose number corresponding to desired scene/arguments.
(Use comma separated list for multiple entries) (Use comma separated list for multiple entries)
Choice(s): """ Choice(s): """
INVALID_NUMBER_MESSAGE = "Fine then, if you don't want to give a valid number I'll just quit" INVALID_NUMBER_MESSAGE = "Fine then, if you don't want to give a valid number I'll just quit"

View file

@ -389,6 +389,9 @@ class Mobject(Container):
def stretch_to_fit_height(self, height, **kwargs): def stretch_to_fit_height(self, height, **kwargs):
return self.rescale_to_fit(height, 1, stretch = True, **kwargs) return self.rescale_to_fit(height, 1, stretch = True, **kwargs)
def stretch_to_fit_depth(self, depth, **kwargs):
return self.rescale_to_fit(depth, 1, stretch = True, **kwargs)
def scale_to_fit_width(self, width, **kwargs): def scale_to_fit_width(self, width, **kwargs):
return self.rescale_to_fit(width, 0, stretch = False, **kwargs) return self.rescale_to_fit(width, 0, stretch = False, **kwargs)

View file

@ -37,9 +37,9 @@ class SVGMobject(VMobject):
if self.file_name is None: if self.file_name is None:
raise Exception("Must specify file for SVGMobject") raise Exception("Must specify file for SVGMobject")
possible_paths = [ possible_paths = [
self.file_name,
os.path.join(SVG_IMAGE_DIR, self.file_name), os.path.join(SVG_IMAGE_DIR, self.file_name),
os.path.join(SVG_IMAGE_DIR, self.file_name + ".svg"), os.path.join(SVG_IMAGE_DIR, self.file_name + ".svg"),
self.file_name,
] ]
for path in possible_paths: for path in possible_paths:
if os.path.exists(path): if os.path.exists(path):

View file

@ -247,7 +247,6 @@ class VMobject(Mobject):
a single "path", in the svg sense of the word. a single "path", in the svg sense of the word.
However, one such path may really consist of separate However, one such path may really consist of separate
continuous components if there is a move_to command. continuous components if there is a move_to command.
These other portions of the path will be treated as submobjects, These other portions of the path will be treated as submobjects,
but will be tracked in a separate special list for when but will be tracked in a separate special list for when
it comes time to display. it comes time to display.
@ -290,7 +289,6 @@ class VMobject(Mobject):
If the distance between a given handle point H and its associated If the distance between a given handle point H and its associated
anchor point A is d, then it changes H to be a distances factor*d anchor point A is d, then it changes H to be a distances factor*d
away from A, but so that the line from A to H doesn't change. away from A, but so that the line from A to H doesn't change.
This is mostly useful in the context of applying a (differentiable) This is mostly useful in the context of applying a (differentiable)
function, to preserve tangency properties. One would pull all the function, to preserve tangency properties. One would pull all the
handles closer to their anchors, apply the function then push them out handles closer to their anchors, apply the function then push them out
@ -484,4 +482,3 @@ class VectorizedPoint(VMobject):
def set_location(self,new_loc): def set_location(self,new_loc):
self.set_points(np.array([new_loc])) self.set_points(np.array([new_loc]))

4536
old_projects/basel/basel.py Normal file

File diff suppressed because it is too large Load diff

4651
old_projects/basel/basel2.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -1842,7 +1842,7 @@ class IntroduceDopplerRadar(Scene):
words = ["Original signal", "Echo"] words = ["Original signal", "Echo"]
for graph, word in zip([pulse_graph, echo_graph], words): for graph, word in zip([pulse_graph, echo_graph], words):
arrow = Vector(DOWN) arrow = Vector(DOWN)
arrow.next_to(graph.peak_point, UP, MED_SMALL_BUFF) arrow.next_to(graph.peak_point, UP, SMALL_BUFF)
arrow.match_color(graph) arrow.match_color(graph)
graph.arrow = arrow graph.arrow = arrow
label = TextMobject(word) label = TextMobject(word)
@ -2400,7 +2400,6 @@ class RadarOperatorUncertainty(Scene):
vector_gdw.move_to(plane_gdw) vector_gdw.move_to(plane_gdw)
vector_gdw.shift(2*RIGHT) vector_gdw.shift(2*RIGHT)
self.add(randy, dish, bubble, plane_cloud, pulse) self.add(randy, dish, bubble, plane_cloud, pulse)
self.play(randy.change, "confused") self.play(randy.change, "confused")
self.wait(3) self.wait(3)

View file

@ -366,7 +366,6 @@ class Scene(Container):
Each arg can either be an animation, or a mobject method Each arg can either be an animation, or a mobject method
followed by that methods arguments (and potentially follow followed by that methods arguments (and potentially follow
by a dict of kwargs for that method). by a dict of kwargs for that method).
This animation list is built by going through the args list, This animation list is built by going through the args list,
and each animation is simply added, but when a mobject method and each animation is simply added, but when a mobject method
s hit, a MoveToTarget animation is built using the args that s hit, a MoveToTarget animation is built using the args that
@ -619,4 +618,3 @@ class EndSceneEarlyException(Exception):

View file

@ -123,8 +123,11 @@ class PatreonEndScreen(PatreonThanks):
"n_patron_columns" : 3, "n_patron_columns" : 3,
"max_patron_width" : 3, "max_patron_width" : 3,
"run_time" : 20, "run_time" : 20,
"randomize_order" : True,
} }
def construct(self): def construct(self):
if self.randomize_order:
random.shuffle(self.specific_patrons)
self.add_title() self.add_title()
self.scroll_through_patrons() self.scroll_through_patrons()
@ -141,7 +144,6 @@ class PatreonEndScreen(PatreonThanks):
pi.next_to(title, vect, buff = MED_LARGE_BUFF) pi.next_to(title, vect, buff = MED_LARGE_BUFF)
self.add_foreground_mobjects(title, randy, morty) self.add_foreground_mobjects(title, randy, morty)
def scroll_through_patrons(self): def scroll_through_patrons(self):
logo_box = Square(side_length = 2.5) logo_box = Square(side_length = 2.5)
logo_box.to_corner(DOWN+LEFT, buff = MED_LARGE_BUFF) logo_box.to_corner(DOWN+LEFT, buff = MED_LARGE_BUFF)
@ -176,7 +178,7 @@ class PatreonEndScreen(PatreonThanks):
aligned_edge = UP, aligned_edge = UP,
) )
columns.scale_to_fit_width(total_width - 1) columns.scale_to_fit_width(total_width - 1)
columns.next_to(black_rect, DOWN, LARGE_BUFF) columns.next_to(black_rect, DOWN, 3*LARGE_BUFF)
columns.to_edge(RIGHT) columns.to_edge(RIGHT)
self.play( self.play(

View file

@ -96,6 +96,57 @@ class Arc(VMobject):
return self return self
class ArcBetweenPoints(Arc):
def __init__(self, start_point, end_point, angle = TAU/4, **kwargs):
if angle == 0:
raise Exception("Arc with zero curve angle: use Line instead.")
midpoint = 0.5 * (start_point + end_point)
distance_vector = end_point - start_point
normal_vector = np.array([-distance_vector[1], distance_vector[0],0])
distance = np.linalg.norm(normal_vector)
normal_vector /= distance
if angle < 0:
normal_vector *= -1
radius = distance/2 / np.sin(0.5 * np.abs(angle))
l = distance/2 / np.tan(0.5 * np.abs(angle))
arc_center = midpoint + l * normal_vector
w = start_point - arc_center
if w[0] != 0:
start_angle = np.arctan2(w[1],w[0])
else:
start_angle = np.pi/2
Arc.__init__(self, angle,
radius = radius,
start_angle = start_angle,
**kwargs)
self.move_arc_center_to(arc_center)
class CurvedArrow(ArcBetweenPoints):
def __init__(self, start_point, end_point, angle = TAU/4, **kwargs):
# I know this is in reverse, but it works
if angle >= 0:
ArcBetweenPoints.__init__(self, start_point, end_point, angle = angle, **kwargs)
self.add_tip(at_start = True, at_end = False)
else:
ArcBetweenPoints.__init__(self, end_point, start_point, angle = -angle, **kwargs)
self.add_tip(at_start = False, at_end = True)
class CurvedDoubleArrow(ArcBetweenPoints):
def __init__(self, start_point, end_point, angle = TAU/4, **kwargs):
ArcBetweenPoints.__init__(self, start_point, end_point, angle = angle, **kwargs)
self.add_tip(at_start = True, at_end = True)
class Circle(Arc): class Circle(Arc):
CONFIG = { CONFIG = {
"color" : RED, "color" : RED,

View file

@ -18,6 +18,7 @@ from mobject.svg_mobject import *
from topics.three_dimensions import * from topics.three_dimensions import *
from scipy.spatial import ConvexHull from scipy.spatial import ConvexHull
from traceback import *
LIGHT_COLOR = YELLOW LIGHT_COLOR = YELLOW
@ -28,13 +29,12 @@ NUM_LEVELS = 30
NUM_CONES = 7 # in first lighthouse scene NUM_CONES = 7 # in first lighthouse scene
NUM_VISIBLE_CONES = 5 # ibidem NUM_VISIBLE_CONES = 5 # ibidem
ARC_TIP_LENGTH = 0.2 ARC_TIP_LENGTH = 0.2
AMBIENT_FULL = 0.5 AMBIENT_FULL = 0.8
AMBIENT_DIMMED = 0.2 AMBIENT_DIMMED = 0.5
SPOTLIGHT_FULL = 0.9 SPOTLIGHT_FULL = 0.8
SPOTLIGHT_DIMMED = 0.2 SPOTLIGHT_DIMMED = 0.5
LIGHTHOUSE_HEIGHT = 0.8 LIGHTHOUSE_HEIGHT = 0.8
LIGHT_COLOR = YELLOW
DEGREES = TAU/360 DEGREES = TAU/360
inverse_power_law = lambda maxint,scale,cutoff,exponent: \ inverse_power_law = lambda maxint,scale,cutoff,exponent: \
@ -42,222 +42,6 @@ inverse_power_law = lambda maxint,scale,cutoff,exponent: \
inverse_quadratic = lambda maxint,scale,cutoff: inverse_power_law(maxint,scale,cutoff,2) inverse_quadratic = lambda maxint,scale,cutoff: inverse_power_law(maxint,scale,cutoff,2)
# 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
class LightSource(VMobject):
# combines:
# a lighthouse
# an ambient light
# a spotlight
# and a shadow
CONFIG = {
"source_point": VectorizedPoint(location = ORIGIN, stroke_width = 0, fill_opacity = 0),
"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,
"camera": None
}
def generate_points(self):
self.add(self.source_point)
self.lighthouse = Lighthouse()
self.ambient_light = AmbientLight(
source_point = VectorizedPoint(location = self.get_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 = VectorizedPoint(location = self.get_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,
camera = self.camera
)
else:
self.spotlight = Spotlight()
self.shadow = VMobject(fill_color = SHADOW_COLOR, fill_opacity = 1.0, stroke_color = BLACK)
self.lighthouse.next_to(self.get_source_point(),DOWN,buff = 0)
self.ambient_light.move_source_to(self.get_source_point())
if self.has_screen():
self.spotlight.move_source_to(self.get_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_camera(self,new_cam):
self.camera = new_cam
self.spotlight.camera = new_cam
def set_screen(self, new_screen):
if self.has_screen():
self.spotlight.screen = new_screen
else:
# Note: See below
index = self.submobjects.index(self.spotlight)
camera = self.spotlight.camera
self.remove(self.spotlight)
self.spotlight = Spotlight(
source_point = VectorizedPoint(location = self.get_source_point()),
color = self.color,
num_levels = self.num_levels,
radius = self.radius,
screen = new_screen,
camera = self.camera
)
self.spotlight.move_source_to(self.get_source_point())
# Note: This line will make spotlight show up at the end
# of the submojects list, which can make it show up on
# top of the shadow. To make it show up in the
# same spot, you could try the following line,
# where "index" is what I defined above:
self.submobjects.insert(index, self.spotlight)
#self.add(self.spotlight)
# in any case
self.screen = new_screen
def move_source_to(self,point):
apoint = np.array(point)
v = apoint - self.get_source_point()
# Note: As discussed, things stand to behave better if source
# point is a submobject, so that it automatically interpolates
# during an animation, and other updates can be defined wrt
# that source point's location
self.source_point.set_location(apoint)
#self.lighthouse.next_to(apoint,DOWN,buff = 0)
#self.ambient_light.move_source_to(apoint)
self.lighthouse.shift(v)
#self.ambient_light.shift(v)
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):
self.spotlight.update_sectors()
self.update_shadow()
def get_source_point(self):
return self.source_point.get_location()
def update_shadow(self):
point = self.get_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.get_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.get_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)
# Note: Theoretically this should not be necessary as long as we make
# sure the shadow shows up after the spotlight in the submobjects list.
#
# shift it closer to the camera so it is in front of the spotlight
self.shadow.shift(1e-5*self.spotlight.projection_direction())
self.shadow.mark_paths_closed = True
class SwitchOn(LaggedStart): class SwitchOn(LaggedStart):
CONFIG = { CONFIG = {
"lag_ratio": 0.2, "lag_ratio": 0.2,
@ -267,9 +51,9 @@ class SwitchOn(LaggedStart):
def __init__(self, light, **kwargs): def __init__(self, light, **kwargs):
if (not isinstance(light,AmbientLight) and not isinstance(light,Spotlight)): if (not isinstance(light,AmbientLight) and not isinstance(light,Spotlight)):
raise Exception("Only AmbientLights and Spotlights can be switched on") raise Exception("Only AmbientLights and Spotlights can be switched on")
LaggedStart.__init__(self, LaggedStart.__init__(
FadeIn, light, **kwargs) self, FadeIn, light, **kwargs
)
class SwitchOff(LaggedStart): class SwitchOff(LaggedStart):
CONFIG = { CONFIG = {
@ -285,19 +69,17 @@ class SwitchOff(LaggedStart):
FadeOut, light, **kwargs) FadeOut, light, **kwargs)
light.submobjects = light.submobjects[::-1] light.submobjects = light.submobjects[::-1]
class Lighthouse(SVGMobject): class Lighthouse(SVGMobject):
CONFIG = { CONFIG = {
"file_name" : "lighthouse", "file_name" : "lighthouse",
"height" : LIGHTHOUSE_HEIGHT "height" : LIGHTHOUSE_HEIGHT,
"fill_color" : WHITE,
"fill_opacity" : 1.0,
} }
def move_to(self,point): def move_to(self,point):
self.next_to(point, DOWN, buff = 0) self.next_to(point, DOWN, buff = 0)
class AmbientLight(VMobject): class AmbientLight(VMobject):
# Parameters are: # Parameters are:
@ -313,7 +95,7 @@ class AmbientLight(VMobject):
"opacity_function" : lambda r : 1.0/(r+1.0)**2, "opacity_function" : lambda r : 1.0/(r+1.0)**2,
"color" : LIGHT_COLOR, "color" : LIGHT_COLOR,
"max_opacity" : 1.0, "max_opacity" : 1.0,
"num_levels" : 10, "num_levels" : NUM_LEVELS,
"radius" : 5.0 "radius" : 5.0
} }
@ -373,31 +155,16 @@ class AmbientLight(VMobject):
new_submob_alpha = old_submob_alpha * new_alpha / old_alpha new_submob_alpha = old_submob_alpha * new_alpha / old_alpha
submob.set_fill(opacity = new_submob_alpha) submob.set_fill(opacity = new_submob_alpha)
class Spotlight(VMobject): class Spotlight(VMobject):
CONFIG = { CONFIG = {
"source_point": VectorizedPoint(location = ORIGIN, stroke_width = 0, fill_opacity = 0), "source_point": VectorizedPoint(location = ORIGIN, stroke_width = 0, fill_opacity = 0),
"opacity_function" : lambda r : 1.0/(r/2+1.0)**2, "opacity_function" : lambda r : 1.0/(r/2+1.0)**2,
"color" : LIGHT_COLOR, "color" : GREEN, # LIGHT_COLOR,
"max_opacity" : 1.0, "max_opacity" : 1.0,
"num_levels" : 10, "num_levels" : 10,
"radius" : 5.0, "radius" : 10.0,
"screen" : None, "screen" : None,
"camera": None "camera_mob": None
} }
def projection_direction(self): def projection_direction(self):
@ -405,24 +172,22 @@ class Spotlight(VMobject):
# need to be sure that any 3d scene including a spotlight # need to be sure that any 3d scene including a spotlight
# somewhere assigns that spotlights "camera" attribute # somewhere assigns that spotlights "camera" attribute
# to be the camera associated with that scene. # to be the camera associated with that scene.
if self.camera == None: if self.camera_mob == None:
return OUT return OUT
else: else:
v = self.camera.get_cartesian_coords() [phi, theta, r] = self.camera_mob.get_center()
return v/np.linalg.norm(v) 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): def project(self,point):
v = self.projection_direction() v = self.projection_direction()
w = project_along_vector(point,v) w = project_along_vector(point,v)
return w return w
def get_source_point(self): def get_source_point(self):
return self.source_point.get_location() return self.source_point.get_location()
def generate_points(self): def generate_points(self):
self.submobjects = [] self.submobjects = []
self.add(self.source_point) self.add(self.source_point)
@ -438,13 +203,7 @@ class Spotlight(VMobject):
new_sector = self.new_sector(r,dr,lower_angle,upper_angle) new_sector = self.new_sector(r,dr,lower_angle,upper_angle)
self.add(new_sector) self.add(new_sector)
def new_sector(self,r,dr,lower_angle,upper_angle): def new_sector(self,r,dr,lower_angle,upper_angle):
# Note: I'm not looking _too_ closely at the implementation
# of these updates based on viewing angles and such. It seems to
# behave as intended, but let me know if you'd like more thorough
# scrutiny
alpha = self.max_opacity * self.opacity_function(r) alpha = self.max_opacity * self.opacity_function(r)
annular_sector = AnnularSector( annular_sector = AnnularSector(
inner_radius = r, inner_radius = r,
@ -480,7 +239,6 @@ class Spotlight(VMobject):
else: else:
return -absolute_angle return -absolute_angle
def viewing_angles(self,screen): def viewing_angles(self,screen):
screen_points = screen.get_anchors() screen_points = screen.get_anchors()
@ -494,6 +252,8 @@ class Spotlight(VMobject):
lower_angle = np.min(viewing_angles) lower_angle = np.min(viewing_angles)
upper_angle = np.max(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 return lower_angle, upper_angle
def viewing_rays(self,screen): def viewing_rays(self,screen):
@ -505,7 +265,6 @@ class Spotlight(VMobject):
return lower_ray, upper_ray return lower_ray, upper_ray
def opening_angle(self): def opening_angle(self):
l,u = self.viewing_angles(self.screen) l,u = self.viewing_angles(self.screen)
return u - l return u - l
@ -525,21 +284,20 @@ class Spotlight(VMobject):
self.update_sectors() self.update_sectors()
return self return self
def update_sectors(self): def update_sectors(self):
if self.screen == None: if self.screen == None:
return return
for submob in self.submobject_family(): for submob in self.submobjects:
if type(submob) == AnnularSector: if type(submob) == AnnularSector:
lower_angle, upper_angle = self.viewing_angles(self.screen) lower_angle, upper_angle = self.viewing_angles(self.screen)
#dr = submob.outer_radius - submob.inner_radius #dr = submob.outer_radius - submob.inner_radius
dr = self.radius / self.num_levels dr = self.radius / self.num_levels
new_submob = self.new_sector(submob.inner_radius,dr,lower_angle,upper_angle) new_submob = self.new_sector(
submob.points = new_submob.points submob.inner_radius, dr, lower_angle, upper_angle
submob.set_fill(opacity = 10 * self.opacity_function(submob.outer_radius)) )
# print "new opacity:", self.opacity_function(submob.outer_radius) # 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): def dimming(self,new_alpha):
old_alpha = self.max_opacity old_alpha = self.max_opacity
@ -570,18 +328,290 @@ class Spotlight(VMobject):
alpha = self.opacity_function(r) alpha = self.opacity_function(r)
submob.set_fill(opacity = alpha) submob.set_fill(opacity = alpha)
# Warning: This class is likely quite buggy.
class LightSource(VMobject):
# combines:
# a lighthouse
# an ambient light
# a spotlight
# and a shadow
CONFIG = {
"source_point": VectorizedPoint(location = ORIGIN, stroke_width = 0, fill_opacity = 0),
"color": LIGHT_COLOR,
"num_levels": 10,
"radius": 10.0,
"screen": None,
"opacity_function": inverse_quadratic(1,2,1),
"max_opacity_ambient": AMBIENT_FULL,
"max_opacity_spotlight": SPOTLIGHT_FULL,
"camera_mob": None
}
# Note: Stylistically, I typically keep all of the implementation for an def generate_points(self):
# update inside the relevant Animation or ContinualAnimation, rather than
# in the mobject. Things are fine the way you've done it, but sometimes self.add(self.source_point)
# I personally like a nice conceptual divide between all the things that
# determine how the mobject is displayed in a single moment (implement in self.lighthouse = Lighthouse()
# the mobject's class itself) and all the things determining how that changes. self.ambient_light = AmbientLight(
# source_point = VectorizedPoint(location = self.get_source_point()),
# Up to you though. 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 = VectorizedPoint(location = self.get_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,
camera_mob = self.camera_mob
)
else:
self.spotlight = Spotlight()
self.shadow = VMobject(fill_color = SHADOW_COLOR, fill_opacity = 1.0, stroke_color = BLACK)
self.lighthouse.next_to(self.get_source_point(),DOWN,buff = 0)
self.ambient_light.move_source_to(self.get_source_point())
if self.has_screen():
self.spotlight.move_source_to(self.get_source_point())
self.update_shadow()
self.add(self.ambient_light,self.spotlight,self.lighthouse, self.shadow)
def has_screen(self):
if self.screen == None:
return False
elif np.size(self.screen.points) == 0:
return False
else:
return True
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_camera_mob(self,new_cam_mob):
self.camera_mob = new_cam_mob
self.spotlight.camera_mob = new_cam_mob
def set_screen(self, new_screen):
if self.has_screen():
self.spotlight.screen = new_screen
else:
# Note: See below
index = self.submobjects.index(self.spotlight)
camera_mob = self.spotlight.camera_mob
self.remove(self.spotlight)
self.spotlight = Spotlight(
source_point = VectorizedPoint(location = self.get_source_point()),
color = self.color,
num_levels = self.num_levels,
radius = self.radius,
screen = new_screen,
camera_mob = self.camera_mob,
opacity_function = self.opacity_function,
max_opacity = self.max_opacity_spotlight,
)
self.spotlight.move_source_to(self.get_source_point())
# Note: This line will make spotlight show up at the end
# of the submojects list, which can make it show up on
# top of the shadow. To make it show up in the
# same spot, you could try the following line,
# where "index" is what I defined above:
self.submobjects.insert(index, self.spotlight)
#self.add(self.spotlight)
# in any case
self.screen = new_screen
def move_source_to(self,point):
apoint = np.array(point)
v = apoint - self.get_source_point()
# Note: As discussed, things stand to behave better if source
# point is a submobject, so that it automatically interpolates
# during an animation, and other updates can be defined wrt
# that source point's location
self.source_point.set_location(apoint)
#self.lighthouse.next_to(apoint,DOWN,buff = 0)
#self.ambient_light.move_source_to(apoint)
self.lighthouse.shift(v)
#self.ambient_light.shift(v)
self.ambient_light.move_source_to(apoint)
if self.has_screen():
self.spotlight.move_source_to(apoint)
self.update()
return self
def change_spotlight_opacity_function(self, new_of):
self.spotlight.change_opacity_function(new_of)
def set_radius(self,new_radius):
self.radius = new_radius
self.ambient_light.radius = new_radius
self.spotlight.radius = new_radius
def update(self):
self.update_lighthouse()
self.update_ambient()
self.spotlight.update_sectors()
self.update_shadow()
def update_lighthouse(self):
self.lighthouse.move_to(self.get_source_point())
# new_lh = Lighthouse()
# new_lh.move_to(ORIGIN)
# new_lh.apply_matrix(self.rotation_matrix())
# new_lh.shift(self.get_source_point())
# self.lighthouse.submobjects = new_lh.submobjects
def update_ambient(self):
new_ambient_light = AmbientLight(
source_point = VectorizedPoint(location = ORIGIN),
color = self.color,
num_levels = self.num_levels,
radius = self.radius,
opacity_function = self.opacity_function,
max_opacity = self.max_opacity_ambient
)
new_ambient_light.apply_matrix(self.rotation_matrix())
new_ambient_light.move_source_to(self.get_source_point())
self.ambient_light.submobjects = new_ambient_light.submobjects
def get_source_point(self):
return self.source_point.get_location()
def rotation_matrix(self):
if self.camera_mob == None:
return np.eye(3)
phi = self.camera_mob.get_center()[0]
theta = self.camera_mob.get_center()[1]
R1 = np.array([
[1, 0, 0],
[0, np.cos(phi), -np.sin(phi)],
[0, np.sin(phi), np.cos(phi)]
])
R2 = np.array([
[np.cos(theta + TAU/4), -np.sin(theta + TAU/4), 0],
[np.sin(theta + TAU/4), np.cos(theta + TAU/4), 0],
[0, 0, 1]
])
R = np.dot(R2, R1)
return R
def update_shadow(self):
point = self.get_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.get_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 = self.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.get_source_point()),back_rotation_matrix.T)[:2]
index = 0
for point in point_cloud_2d[hull_2d.vertices]:
if np.all(np.abs(point - source_point_2d) < 1.0e-6):
source_index = index
index += 1
continue
point_3d = np.array([point[0], point[1], 0])
hull.append(point_3d)
index += 1
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
if np.size(anchors) == 0:
self.shadow.points = []
return
ray1 = anchors[source_index - 1] - projected_source
ray1 = ray1/np.linalg.norm(ray1) * 100
ray2 = anchors[source_index] - projected_source
ray2 = ray2/np.linalg.norm(ray2) * 100
outpoint1 = anchors[source_index - 1] + ray1
outpoint2 = anchors[source_index] + ray2
new_anchors = anchors[:source_index]
new_anchors = np.append(new_anchors,np.array([outpoint1, outpoint2]),axis = 0)
new_anchors = np.append(new_anchors,anchors[source_index:],axis = 0)
self.shadow.set_points_as_corners(new_anchors)
# shift it closer to the camera so it is in front of the spotlight
self.shadow.mark_paths_closed = True
class ScreenTracker(ContinualAnimation): class ScreenTracker(ContinualAnimation):
def __init__(self, light_source, **kwargs):
self.light_source = light_source
dummy_mob = Mobject()
ContinualAnimation.__init__(self, dummy_mob, **kwargs)
def update_mobject(self, dt): def update_mobject(self, dt):
self.mobject.update() self.light_source.update()

View file

@ -120,6 +120,9 @@ class NumberLine(VMobject):
result.add(mob) result.add(mob)
return result return result
def get_labels(self):
return self.get_number_mobjects()
def add_numbers(self, *numbers, **kwargs): def add_numbers(self, *numbers, **kwargs):
self.numbers = self.get_number_mobjects( self.numbers = self.get_number_mobjects(
*numbers, **kwargs *numbers, **kwargs
@ -241,7 +244,7 @@ class Axes(VGroup):
elif lx > x and rx < x: elif lx > x and rx < x:
lh, rh = rh, lh lh, rh = rh, lh
return points[1] return points[1]
return self.coords_to_point(x, graph.underlying_function(x))
class ThreeDAxes(Axes): class ThreeDAxes(Axes):
CONFIG = { CONFIG = {

View file

@ -163,4 +163,3 @@ class ContinualChangingDecimal(ContinualAnimation):