mirror of
https://github.com/3b1b/manim.git
synced 2025-08-31 23:58:32 +00:00
Merge branch 'master' of https://github.com/3b1b/manim into WindingNumber
This commit is contained in:
commit
10d6739058
17 changed files with 9568 additions and 1956 deletions
|
@ -24,7 +24,7 @@ pip install -r requirements.txt
|
|||
```
|
||||
|
||||
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.
|
||||
|
||||
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
|
@ -358,7 +358,6 @@ class Succession(Animation):
|
|||
Each arg will either be an animation, or an animation class
|
||||
followed by its arguments (and potentially a dict for
|
||||
configuration).
|
||||
|
||||
For example,
|
||||
Succession(
|
||||
ShowCreation(circle),
|
||||
|
@ -539,4 +538,3 @@ class EmptyAnimation(Animation):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
return Animation.__init__(self, Group(), *args, **kwargs)
|
||||
|
||||
|
|
|
@ -101,9 +101,15 @@ class Swap(CyclicReplace):
|
|||
pass #Renaming, more understandable for two entries
|
||||
|
||||
class GrowFromPoint(Transform):
|
||||
CONFIG = {
|
||||
"point_color" : None,
|
||||
}
|
||||
def __init__(self, mobject, point, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
target = mobject.copy()
|
||||
point_mob = Point(point)
|
||||
if self.point_color:
|
||||
point_mob.highlight(self.point_color)
|
||||
mobject.replace(point_mob)
|
||||
mobject.highlight(point_mob.get_color())
|
||||
Transform.__init__(self, mobject, target, **kwargs)
|
||||
|
|
|
@ -18,7 +18,6 @@ from camera import Camera
|
|||
HELP_MESSAGE = """
|
||||
Usage:
|
||||
python extract_scene.py <module> [<scene name>]
|
||||
|
||||
-p preview in low quality
|
||||
-s show and save picture of last frame
|
||||
-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 corresponding to desired scene/arguments.
|
||||
(Use comma separated list for multiple entries)
|
||||
|
||||
Choice(s): """
|
||||
INVALID_NUMBER_MESSAGE = "Fine then, if you don't want to give a valid number I'll just quit"
|
||||
|
||||
|
@ -262,4 +260,4 @@ def main():
|
|||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
|
@ -389,6 +389,9 @@ class Mobject(Container):
|
|||
def stretch_to_fit_height(self, height, **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):
|
||||
return self.rescale_to_fit(width, 0, stretch = False, **kwargs)
|
||||
|
||||
|
|
|
@ -37,9 +37,9 @@ class SVGMobject(VMobject):
|
|||
if self.file_name is None:
|
||||
raise Exception("Must specify file for SVGMobject")
|
||||
possible_paths = [
|
||||
self.file_name,
|
||||
os.path.join(SVG_IMAGE_DIR, self.file_name),
|
||||
os.path.join(SVG_IMAGE_DIR, self.file_name + ".svg"),
|
||||
self.file_name,
|
||||
]
|
||||
for path in possible_paths:
|
||||
if os.path.exists(path):
|
||||
|
|
|
@ -247,7 +247,6 @@ class VMobject(Mobject):
|
|||
a single "path", in the svg sense of the word.
|
||||
However, one such path may really consist of separate
|
||||
continuous components if there is a move_to command.
|
||||
|
||||
These other portions of the path will be treated as submobjects,
|
||||
but will be tracked in a separate special list for when
|
||||
it comes time to display.
|
||||
|
@ -290,7 +289,6 @@ class VMobject(Mobject):
|
|||
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
|
||||
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)
|
||||
function, to preserve tangency properties. One would pull all the
|
||||
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):
|
||||
self.set_points(np.array([new_loc]))
|
||||
|
||||
|
||||
|
|
4536
old_projects/basel/basel.py
Normal file
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
4651
old_projects/basel/basel2.py
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1842,7 +1842,7 @@ class IntroduceDopplerRadar(Scene):
|
|||
words = ["Original signal", "Echo"]
|
||||
for graph, word in zip([pulse_graph, echo_graph], words):
|
||||
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)
|
||||
graph.arrow = arrow
|
||||
label = TextMobject(word)
|
||||
|
@ -2400,7 +2400,6 @@ class RadarOperatorUncertainty(Scene):
|
|||
vector_gdw.move_to(plane_gdw)
|
||||
vector_gdw.shift(2*RIGHT)
|
||||
|
||||
|
||||
self.add(randy, dish, bubble, plane_cloud, pulse)
|
||||
self.play(randy.change, "confused")
|
||||
self.wait(3)
|
||||
|
|
|
@ -366,7 +366,6 @@ class Scene(Container):
|
|||
Each arg can either be an animation, or a mobject method
|
||||
followed by that methods arguments (and potentially follow
|
||||
by a dict of kwargs for that method).
|
||||
|
||||
This animation list is built by going through the args list,
|
||||
and each animation is simply added, but when a mobject method
|
||||
s hit, a MoveToTarget animation is built using the args that
|
||||
|
@ -619,4 +618,3 @@ class EndSceneEarlyException(Exception):
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -123,8 +123,11 @@ class PatreonEndScreen(PatreonThanks):
|
|||
"n_patron_columns" : 3,
|
||||
"max_patron_width" : 3,
|
||||
"run_time" : 20,
|
||||
"randomize_order" : True,
|
||||
}
|
||||
def construct(self):
|
||||
if self.randomize_order:
|
||||
random.shuffle(self.specific_patrons)
|
||||
self.add_title()
|
||||
self.scroll_through_patrons()
|
||||
|
||||
|
@ -141,7 +144,6 @@ class PatreonEndScreen(PatreonThanks):
|
|||
pi.next_to(title, vect, buff = MED_LARGE_BUFF)
|
||||
self.add_foreground_mobjects(title, randy, morty)
|
||||
|
||||
|
||||
def scroll_through_patrons(self):
|
||||
logo_box = Square(side_length = 2.5)
|
||||
logo_box.to_corner(DOWN+LEFT, buff = MED_LARGE_BUFF)
|
||||
|
@ -176,7 +178,7 @@ class PatreonEndScreen(PatreonThanks):
|
|||
aligned_edge = UP,
|
||||
)
|
||||
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)
|
||||
|
||||
self.play(
|
||||
|
|
|
@ -96,6 +96,57 @@ class Arc(VMobject):
|
|||
|
||||
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):
|
||||
CONFIG = {
|
||||
"color" : RED,
|
||||
|
|
588
topics/light.py
588
topics/light.py
|
@ -18,6 +18,7 @@ from mobject.svg_mobject import *
|
|||
from topics.three_dimensions import *
|
||||
|
||||
from scipy.spatial import ConvexHull
|
||||
from traceback import *
|
||||
|
||||
|
||||
LIGHT_COLOR = YELLOW
|
||||
|
@ -28,13 +29,12 @@ 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
|
||||
AMBIENT_FULL = 0.8
|
||||
AMBIENT_DIMMED = 0.5
|
||||
SPOTLIGHT_FULL = 0.8
|
||||
SPOTLIGHT_DIMMED = 0.5
|
||||
LIGHTHOUSE_HEIGHT = 0.8
|
||||
|
||||
LIGHT_COLOR = YELLOW
|
||||
DEGREES = TAU/360
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
||||
# 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):
|
||||
CONFIG = {
|
||||
"lag_ratio": 0.2,
|
||||
|
@ -267,9 +51,9 @@ class SwitchOn(LaggedStart):
|
|||
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)
|
||||
|
||||
LaggedStart.__init__(
|
||||
self, FadeIn, light, **kwargs
|
||||
)
|
||||
|
||||
class SwitchOff(LaggedStart):
|
||||
CONFIG = {
|
||||
|
@ -285,19 +69,17 @@ class SwitchOff(LaggedStart):
|
|||
FadeOut, light, **kwargs)
|
||||
light.submobjects = light.submobjects[::-1]
|
||||
|
||||
|
||||
|
||||
|
||||
class Lighthouse(SVGMobject):
|
||||
CONFIG = {
|
||||
"file_name" : "lighthouse",
|
||||
"height" : LIGHTHOUSE_HEIGHT
|
||||
"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:
|
||||
|
@ -313,7 +95,7 @@ class AmbientLight(VMobject):
|
|||
"opacity_function" : lambda r : 1.0/(r+1.0)**2,
|
||||
"color" : LIGHT_COLOR,
|
||||
"max_opacity" : 1.0,
|
||||
"num_levels" : 10,
|
||||
"num_levels" : NUM_LEVELS,
|
||||
"radius" : 5.0
|
||||
}
|
||||
|
||||
|
@ -373,31 +155,16 @@ class AmbientLight(VMobject):
|
|||
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" : LIGHT_COLOR,
|
||||
"color" : GREEN, # LIGHT_COLOR,
|
||||
"max_opacity" : 1.0,
|
||||
"num_levels" : 10,
|
||||
"radius" : 5.0,
|
||||
"radius" : 10.0,
|
||||
"screen" : None,
|
||||
"camera": None
|
||||
"camera_mob": None
|
||||
}
|
||||
|
||||
def projection_direction(self):
|
||||
|
@ -405,24 +172,22 @@ class Spotlight(VMobject):
|
|||
# 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 == None:
|
||||
if self.camera_mob == None:
|
||||
return OUT
|
||||
else:
|
||||
v = self.camera.get_cartesian_coords()
|
||||
return v/np.linalg.norm(v)
|
||||
[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)
|
||||
|
@ -438,13 +203,7 @@ class Spotlight(VMobject):
|
|||
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):
|
||||
# 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)
|
||||
annular_sector = AnnularSector(
|
||||
inner_radius = r,
|
||||
|
@ -480,7 +239,6 @@ class Spotlight(VMobject):
|
|||
else:
|
||||
return -absolute_angle
|
||||
|
||||
|
||||
def viewing_angles(self,screen):
|
||||
|
||||
screen_points = screen.get_anchors()
|
||||
|
@ -494,6 +252,8 @@ class Spotlight(VMobject):
|
|||
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):
|
||||
|
@ -505,7 +265,6 @@ class Spotlight(VMobject):
|
|||
|
||||
return lower_ray, upper_ray
|
||||
|
||||
|
||||
def opening_angle(self):
|
||||
l,u = self.viewing_angles(self.screen)
|
||||
return u - l
|
||||
|
@ -525,21 +284,20 @@ class Spotlight(VMobject):
|
|||
self.update_sectors()
|
||||
return self
|
||||
|
||||
|
||||
def update_sectors(self):
|
||||
if self.screen == None:
|
||||
return
|
||||
for submob in self.submobject_family():
|
||||
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))
|
||||
# print "new opacity:", self.opacity_function(submob.outer_radius)
|
||||
|
||||
|
||||
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
|
||||
|
@ -570,18 +328,290 @@ class Spotlight(VMobject):
|
|||
alpha = self.opacity_function(r)
|
||||
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
|
||||
# update inside the relevant Animation or ContinualAnimation, rather than
|
||||
# in the mobject. Things are fine the way you've done it, but sometimes
|
||||
# I personally like a nice conceptual divide between all the things that
|
||||
# determine how the mobject is displayed in a single moment (implement in
|
||||
# the mobject's class itself) and all the things determining how that changes.
|
||||
#
|
||||
# Up to you though.
|
||||
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_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):
|
||||
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):
|
||||
self.mobject.update()
|
||||
self.light_source.update()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -120,6 +120,9 @@ class NumberLine(VMobject):
|
|||
result.add(mob)
|
||||
return result
|
||||
|
||||
def get_labels(self):
|
||||
return self.get_number_mobjects()
|
||||
|
||||
def add_numbers(self, *numbers, **kwargs):
|
||||
self.numbers = self.get_number_mobjects(
|
||||
*numbers, **kwargs
|
||||
|
@ -241,7 +244,7 @@ class Axes(VGroup):
|
|||
elif lx > x and rx < x:
|
||||
lh, rh = rh, lh
|
||||
return points[1]
|
||||
|
||||
return self.coords_to_point(x, graph.underlying_function(x))
|
||||
|
||||
class ThreeDAxes(Axes):
|
||||
CONFIG = {
|
||||
|
|
|
@ -144,7 +144,6 @@ class ContinualChangingDecimal(ContinualAnimation):
|
|||
|
||||
def update_mobject(self, dt):
|
||||
self.anim.update(self.internal_time)
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue