mirror of
https://github.com/3b1b/manim.git
synced 2025-09-01 00:48:45 +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
|
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
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
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"]
|
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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
588
topics/light.py
588
topics/light.py
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -163,4 +163,3 @@ class ContinualChangingDecimal(ContinualAnimation):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue