mirror of
https://github.com/3b1b/manim.git
synced 2026-04-26 03:00:23 -04:00
Merge branch 'master' of https://github.com/3b1b/manim into WindingNumber
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,288 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from helpers import *
|
||||
|
||||
from mobject.tex_mobject import TexMobject
|
||||
from mobject import Mobject
|
||||
from mobject.image_mobject import ImageMobject
|
||||
from mobject.vectorized_mobject import *
|
||||
from mobject.point_cloud_mobject import PointCloudDot
|
||||
|
||||
from animation.animation import Animation
|
||||
from animation.transform import *
|
||||
from animation.simple_animations import *
|
||||
from animation.continual_animation import *
|
||||
from animation.playground import *
|
||||
|
||||
from topics.geometry import *
|
||||
from topics.characters import *
|
||||
from topics.functions import *
|
||||
from topics.number_line import *
|
||||
from topics.combinatorics import *
|
||||
from scene import Scene
|
||||
from camera import Camera
|
||||
from mobject.svg_mobject import *
|
||||
from mobject.tex_mobject import *
|
||||
|
||||
from mobject.vectorized_mobject import *
|
||||
|
||||
## To watch one of these scenes, run the following:
|
||||
## python extract_scene.py -p file_name <SceneName>
|
||||
|
||||
LIGHT_COLOR = YELLOW
|
||||
DEGREES = 360/TAU
|
||||
SWITCH_ON_RUN_TIME = 1.5
|
||||
|
||||
|
||||
class AmbientLight(VMobject):
|
||||
|
||||
# Parameters are:
|
||||
# * a source point
|
||||
# * an opacity function
|
||||
# * a light color
|
||||
# * a max opacity
|
||||
# * a radius (larger than the opacity's dropoff length)
|
||||
# * the number of subdivisions (levels, annuli)
|
||||
|
||||
CONFIG = {
|
||||
"source_point" : ORIGIN,
|
||||
"opacity_function" : lambda r : 1.0/(r+1.0)**2,
|
||||
"color" : LIGHT_COLOR,
|
||||
"max_opacity" : 1.0,
|
||||
"num_levels" : 10,
|
||||
"radius" : 5.0
|
||||
}
|
||||
|
||||
def generate_points(self):
|
||||
|
||||
# in theory, this method is only called once, right?
|
||||
# so removing submobs shd not be necessary
|
||||
for submob in self.submobjects:
|
||||
self.remove(submob)
|
||||
|
||||
# create annuli
|
||||
dr = self.radius / self.num_levels
|
||||
for r in np.arange(0, self.radius, dr):
|
||||
alpha = self.max_opacity * self.opacity_function(r)
|
||||
annulus = Annulus(
|
||||
inner_radius = r,
|
||||
outer_radius = r + dr,
|
||||
color = self.color,
|
||||
fill_opacity = alpha
|
||||
)
|
||||
annulus.move_arc_center_to(self.source_point)
|
||||
self.add(annulus)
|
||||
|
||||
|
||||
|
||||
def move_source_to(self,point):
|
||||
self.shift(point - self.source_point)
|
||||
self.source_point = np.array(point)
|
||||
# for submob in self.submobjects:
|
||||
# if type(submob) == Annulus:
|
||||
# submob.shift(self.source_point - submob.get_center())
|
||||
|
||||
def dimming(self,new_alpha):
|
||||
old_alpha = self.max_opacity
|
||||
self.max_opacity = new_alpha
|
||||
for submob in self.submobjects:
|
||||
old_submob_alpha = submob.fill_opacity
|
||||
new_submob_alpha = old_submob_alpha * new_alpha/old_alpha
|
||||
submob.set_fill(opacity = new_submob_alpha)
|
||||
|
||||
|
||||
class Spotlight(VMobject):
|
||||
|
||||
CONFIG = {
|
||||
"source_point" : ORIGIN,
|
||||
"opacity_function" : lambda r : 1.0/(r+1.0)**2,
|
||||
"color" : LIGHT_COLOR,
|
||||
"max_opacity" : 1.0,
|
||||
"num_levels" : 10,
|
||||
"radius" : 5.0,
|
||||
"screen" : None,
|
||||
"shadow" : VMobject(fill_color = RED, stroke_width = 0, fill_opacity = 1.0)
|
||||
}
|
||||
|
||||
def track_screen(self):
|
||||
self.generate_points()
|
||||
|
||||
def generate_points(self):
|
||||
|
||||
for submob in self.submobjects:
|
||||
self.remove(submob)
|
||||
|
||||
if self.screen != None:
|
||||
# look for the screen and create annular sectors
|
||||
lower_angle, upper_angle = self.viewing_angles(self.screen)
|
||||
dr = self.radius / self.num_levels
|
||||
for r in np.arange(0, self.radius, dr):
|
||||
alpha = self.max_opacity * self.opacity_function(r)
|
||||
annular_sector = AnnularSector(
|
||||
inner_radius = r,
|
||||
outer_radius = r + dr,
|
||||
color = self.color,
|
||||
fill_opacity = alpha,
|
||||
start_angle = lower_angle,
|
||||
angle = upper_angle - lower_angle
|
||||
)
|
||||
annular_sector.move_arc_center_to(self.source_point)
|
||||
self.add(annular_sector)
|
||||
|
||||
self.update_shadow(point = self.source_point)
|
||||
self.add(self.shadow)
|
||||
|
||||
|
||||
def viewing_angle_of_point(self,point):
|
||||
distance_vector = point - self.source_point
|
||||
angle = angle_of_vector(distance_vector)
|
||||
return angle
|
||||
|
||||
def viewing_angles(self,screen):
|
||||
|
||||
viewing_angles = np.array(map(self.viewing_angle_of_point,
|
||||
screen.get_anchors()))
|
||||
lower_angle = upper_angle = 0
|
||||
if len(viewing_angles) != 0:
|
||||
lower_angle = np.min(viewing_angles)
|
||||
upper_angle = np.max(viewing_angles)
|
||||
|
||||
return lower_angle, upper_angle
|
||||
|
||||
def move_source_to(self,point):
|
||||
self.source_point = np.array(point)
|
||||
self.recalculate_sectors(point = point, screen = self.screen)
|
||||
self.update_shadow(point = point)
|
||||
|
||||
def recalculate_sectors(self, point = ORIGIN, screen = None):
|
||||
if screen == None:
|
||||
return
|
||||
for submob in self.submobject_family():
|
||||
if type(submob) == AnnularSector:
|
||||
lower_angle, upper_angle = self.viewing_angles(screen)
|
||||
new_submob = AnnularSector(
|
||||
start_angle = lower_angle,
|
||||
angle = upper_angle - lower_angle,
|
||||
inner_radius = submob.inner_radius,
|
||||
outer_radius = submob.outer_radius
|
||||
)
|
||||
new_submob.move_arc_center_to(point)
|
||||
submob.points = new_submob.points
|
||||
|
||||
def update_shadow(self,point = ORIGIN):
|
||||
print "updating shadow"
|
||||
use_point = point #self.source_point
|
||||
self.shadow.points = self.screen.points
|
||||
ray1 = self.screen.points[0] - use_point
|
||||
ray2 = self.screen.points[-1] - use_point
|
||||
ray1 = ray1/np.linalg.norm(ray1) * 100
|
||||
ray1 = rotate_vector(ray1,-TAU/16)
|
||||
ray2 = ray2/np.linalg.norm(ray2) * 100
|
||||
ray2 = rotate_vector(ray2,TAU/16)
|
||||
outpoint1 = self.screen.points[0] + ray1
|
||||
outpoint2 = self.screen.points[-1] + ray2
|
||||
self.shadow.add_control_points([outpoint2,outpoint1,self.screen.points[0]])
|
||||
self.shadow.mark_paths_closed = True
|
||||
|
||||
|
||||
def dimming(self,new_alpha):
|
||||
old_alpha = self.max_opacity
|
||||
self.max_opacity = new_alpha
|
||||
for submob in self.submobjects:
|
||||
if type(submob) != AnnularSector:
|
||||
# it's the shadow, don't dim it
|
||||
continue
|
||||
old_submob_alpha = submob.fill_opacity
|
||||
new_submob_alpha = old_submob_alpha * new_alpha/old_alpha
|
||||
submob.set_fill(opacity = new_submob_alpha)
|
||||
|
||||
|
||||
|
||||
|
||||
class SwitchOn(LaggedStart):
|
||||
CONFIG = {
|
||||
"lag_ratio": 0.2,
|
||||
"run_time": SWITCH_ON_RUN_TIME
|
||||
}
|
||||
|
||||
def __init__(self, light, **kwargs):
|
||||
if not isinstance(light,AmbientLight) and not isinstance(light,Spotlight):
|
||||
raise Exception("Only LightCones and Candles can be switched on")
|
||||
LaggedStart.__init__(self,
|
||||
FadeIn, light, **kwargs)
|
||||
|
||||
|
||||
class SwitchOff(LaggedStart):
|
||||
CONFIG = {
|
||||
"lag_ratio": 0.2,
|
||||
"run_time": SWITCH_ON_RUN_TIME
|
||||
}
|
||||
|
||||
def __init__(self, light, **kwargs):
|
||||
if not isinstance(light,AmbientLight) and not isinstance(light,Spotlight):
|
||||
raise Exception("Only LightCones and Candles can be switched on")
|
||||
light.submobjects = light.submobjects[::-1]
|
||||
LaggedStart.__init__(self,
|
||||
FadeOut, light, **kwargs)
|
||||
light.submobjects = light.submobjects[::-1]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ScreenTracker(ContinualAnimation):
|
||||
def __init__(self, mobject, **kwargs):
|
||||
ContinualAnimation.__init__(self, mobject, **kwargs)
|
||||
|
||||
def update_mobject(self, dt):
|
||||
self.mobject.recalculate_sectors(
|
||||
point = self.mobject.source_point,
|
||||
screen = self.mobject.screen)
|
||||
self.mobject.update_shadow(self.mobject.source_point)
|
||||
|
||||
|
||||
|
||||
class IntroScene(Scene):
|
||||
def construct(self):
|
||||
|
||||
screen = Line([2,-2,0],[1,2,0]).shift([1,0,0])
|
||||
self.add(screen)
|
||||
|
||||
ambient_light = AmbientLight(
|
||||
source_point = np.array([-1,1,0]),
|
||||
max_opacity = 1.0,
|
||||
opacity_function = lambda r: 1.0/(r/2+1)**2,
|
||||
num_levels = 4,
|
||||
)
|
||||
|
||||
spotlight = Spotlight(
|
||||
source_point = np.array([-1,1,0]),
|
||||
max_opacity = 1.0,
|
||||
opacity_function = lambda r: 1.0/(r/2+1)**2,
|
||||
num_levels = 4,
|
||||
screen = screen,
|
||||
)
|
||||
|
||||
self.add(spotlight)
|
||||
|
||||
screen_updater = ScreenTracker(spotlight)
|
||||
#self.add(ca)
|
||||
|
||||
#self.play(SwitchOn(ambient_light))
|
||||
#self.play(ApplyMethod(ambient_light.move_source_to,[-3,1,0]))
|
||||
#self.play(SwitchOn(spotlight))
|
||||
|
||||
self.add(screen_updater)
|
||||
self.play(ApplyMethod(spotlight.screen.rotate,TAU/8))
|
||||
self.remove(screen_updater)
|
||||
self.play(ApplyMethod(spotlight.move_source_to,[-3,-1,0]))
|
||||
self.add(screen_updater)
|
||||
spotlight.source_point = [-3,-1,0]
|
||||
|
||||
self.play(ApplyMethod(spotlight.dimming,0.2))
|
||||
#self.play(ApplyMethod(spotlight.move_source_to,[-4,0,0]))
|
||||
|
||||
#self.wait()
|
||||
|
||||
|
||||
|
||||
@@ -141,7 +141,8 @@ class Camera(object):
|
||||
self, mobjects,
|
||||
include_submobjects = True,
|
||||
excluded_mobjects = None,
|
||||
z_buff_func = lambda m : m.get_center()[2]
|
||||
#Round z coordinate to nearest hundredth when comparring
|
||||
z_buff_func = lambda m : np.round(m.get_center()[2], 2)
|
||||
):
|
||||
if include_submobjects:
|
||||
mobjects = self.extract_mobject_family_members(
|
||||
|
||||
@@ -60,6 +60,7 @@ LEFT_SIDE = SPACE_WIDTH*LEFT
|
||||
RIGHT_SIDE = SPACE_WIDTH*RIGHT
|
||||
|
||||
TAU = 2*np.pi
|
||||
DEGREES = TAU/360
|
||||
|
||||
# Change this to point to where you want
|
||||
# animation files to output
|
||||
|
||||
@@ -6,22 +6,23 @@ from mobject.tex_mobject import TexMobject
|
||||
from mobject import Mobject
|
||||
from mobject.image_mobject import ImageMobject
|
||||
from mobject.vectorized_mobject import *
|
||||
from mobject.svg_mobject import *
|
||||
from mobject.tex_mobject import *
|
||||
|
||||
from scene import Scene
|
||||
from camera import Camera
|
||||
|
||||
from animation.animation import Animation
|
||||
from animation.transform import *
|
||||
from animation.simple_animations import *
|
||||
from animation.playground import *
|
||||
|
||||
from topics.geometry import *
|
||||
from topics.characters import *
|
||||
from topics.functions import *
|
||||
from topics.number_line import *
|
||||
from topics.combinatorics import *
|
||||
from scene import Scene
|
||||
from camera import Camera
|
||||
from mobject.svg_mobject import *
|
||||
from mobject.tex_mobject import *
|
||||
|
||||
from mobject.vectorized_mobject import *
|
||||
from topics.three_dimensions import *
|
||||
|
||||
# To watch one of these scenes, run the following:
|
||||
# python extract_scene.py file_name <SceneName> -p
|
||||
@@ -35,9 +36,9 @@ from mobject.vectorized_mobject import *
|
||||
class SquareToCircle(Scene):
|
||||
def construct(self):
|
||||
circle = Circle()
|
||||
# circle.flip(RIGHT)
|
||||
# circle.rotate(3*TAU/8)
|
||||
square = Square()
|
||||
square.flip(RIGHT)
|
||||
square.rotate(-3*TAU/8)
|
||||
|
||||
self.play(ShowCreation(square))
|
||||
self.play(Transform(square, circle))
|
||||
@@ -59,6 +60,70 @@ class WriteStuff(Scene):
|
||||
|
||||
|
||||
|
||||
class SpinAroundCube(ThreeDScene):
|
||||
# Take a look at ThreeDSCene in three_dimensions.py.
|
||||
# This has a few methods on it like set_camera_position
|
||||
# and move_camera that will be useful. The main thing to
|
||||
# know about these is that the camera position is thought
|
||||
# of as having spherical coordinates, phi and theta.
|
||||
|
||||
# In general, the nature of how this 3d camera works
|
||||
# is not always robust, you might discover little
|
||||
# quirks here or there
|
||||
def construct(self):
|
||||
axes = ThreeDAxes()
|
||||
cube = Cube(
|
||||
fill_opacity = 1,
|
||||
stroke_color = LIGHT_GREY,
|
||||
stroke_width = 1,
|
||||
)
|
||||
# The constant OUT is np.array([0, 0, 1])
|
||||
cube.next_to(ORIGIN, UP+RIGHT+OUT)
|
||||
self.add(axes, cube)
|
||||
|
||||
# The camera starts positioned with phi=0, meaning it
|
||||
# is directly above the xy-plane, and theta = -TAU/4,
|
||||
# which makes the "down" direction of the screen point
|
||||
# in the negative y direction.
|
||||
|
||||
# This animates a camera movement
|
||||
self.move_camera(
|
||||
# Tilted 20 degrees off xy plane (70 degrees off the vertical)
|
||||
phi = (70./360.)*TAU,
|
||||
# Positioned above the third quadrant of
|
||||
# the xy-plane
|
||||
theta = (-110./360.)*TAU,
|
||||
# pass in animation config just like a .play call
|
||||
run_time = 3
|
||||
)
|
||||
self.wait()
|
||||
# If you want the camera to slowly rotate about
|
||||
# the z-axis
|
||||
self.begin_ambient_camera_rotation()
|
||||
self.wait(4)
|
||||
self.play(FadeOut(cube))
|
||||
|
||||
|
||||
|
||||
text = TextMobject("Your ad here")
|
||||
text.rotate(TAU/4, axis = RIGHT)
|
||||
text.next_to(cube, OUT)
|
||||
self.play(Write(text))
|
||||
# If you want to play animations while moving the camera,
|
||||
# include them in an "added_anims" list to move_camera
|
||||
self.move_camera(
|
||||
theta = -0.2*TAU,
|
||||
added_anims = [
|
||||
text.shift, 3*OUT,
|
||||
text.set_fill, {"opacity" : 1},
|
||||
]
|
||||
)
|
||||
self.wait(4)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
23
helpers.py
23
helpers.py
@@ -199,14 +199,16 @@ def bezier(points):
|
||||
def remove_list_redundancies(l):
|
||||
"""
|
||||
Used instead of list(set(l)) to maintain order
|
||||
Keeps the last occurance of each element
|
||||
"""
|
||||
result = []
|
||||
reversed_result = []
|
||||
used = set()
|
||||
for x in l:
|
||||
for x in reversed(l):
|
||||
if not x in used:
|
||||
result.append(x)
|
||||
reversed_result.append(x)
|
||||
used.add(x)
|
||||
return result
|
||||
reversed_result.reverse()
|
||||
return reversed_result
|
||||
|
||||
def list_update(l1, l2):
|
||||
"""
|
||||
@@ -643,6 +645,19 @@ def angle_of_vector(vector):
|
||||
return 0
|
||||
return np.angle(complex(*vector[:2]))
|
||||
|
||||
def angle_between_vectors(v1, v2):
|
||||
"""
|
||||
Returns the angle between two 3D vectors.
|
||||
This angle will always be btw 0 and TAU/2.
|
||||
"""
|
||||
l1 = np.linalg.norm(v1)
|
||||
l2 = np.linalg.norm(v2)
|
||||
return np.arccos(np.dot(v1,v2)/(l1*l2))
|
||||
|
||||
def project_along_vector(point, vector):
|
||||
matrix = np.identity(3) - np.outer(vector, vector)
|
||||
return np.dot(point, matrix.T)
|
||||
|
||||
def concatenate_lists(*list_of_lists):
|
||||
return [item for l in list_of_lists for item in l]
|
||||
|
||||
|
||||
@@ -367,7 +367,7 @@ class Scene(Container):
|
||||
animations.pop()
|
||||
#method should already have target then.
|
||||
else:
|
||||
mobject.target = mobject.copy()
|
||||
mobject.target = mobject.deepcopy()
|
||||
#
|
||||
if len(state["method_args"]) > 0 and isinstance(state["method_args"][-1], dict):
|
||||
method_kwargs = state["method_args"].pop()
|
||||
|
||||
506
topics/light.py
Normal file
506
topics/light.py
Normal file
@@ -0,0 +1,506 @@
|
||||
from helpers import *
|
||||
|
||||
from mobject.tex_mobject import TexMobject
|
||||
from mobject import Mobject
|
||||
from mobject.vectorized_mobject import *
|
||||
|
||||
from animation.animation import Animation
|
||||
from animation.transform import *
|
||||
from animation.simple_animations import *
|
||||
from animation.continual_animation import *
|
||||
|
||||
from animation.playground import *
|
||||
from topics.geometry import *
|
||||
from topics.functions import *
|
||||
from scene import Scene
|
||||
from camera import Camera
|
||||
from mobject.svg_mobject import *
|
||||
from topics.three_dimensions import *
|
||||
|
||||
from scipy.spatial import ConvexHull
|
||||
|
||||
|
||||
LIGHT_COLOR = YELLOW
|
||||
SHADOW_COLOR = BLACK
|
||||
SWITCH_ON_RUN_TIME = 1.5
|
||||
FAST_SWITCH_ON_RUN_TIME = 0.1
|
||||
NUM_LEVELS = 30
|
||||
NUM_CONES = 7 # in first lighthouse scene
|
||||
NUM_VISIBLE_CONES = 5 # ibidem
|
||||
ARC_TIP_LENGTH = 0.2
|
||||
AMBIENT_FULL = 0.5
|
||||
AMBIENT_DIMMED = 0.2
|
||||
SPOTLIGHT_FULL = 0.9
|
||||
SPOTLIGHT_DIMMED = 0.2
|
||||
|
||||
LIGHT_COLOR = YELLOW
|
||||
DEGREES = TAU/360
|
||||
|
||||
inverse_power_law = lambda maxint,scale,cutoff,exponent: \
|
||||
(lambda r: maxint * (cutoff/(r/scale+cutoff))**exponent)
|
||||
inverse_quadratic = lambda maxint,scale,cutoff: inverse_power_law(maxint,scale,cutoff,2)
|
||||
|
||||
|
||||
|
||||
class LightSource(VMobject):
|
||||
|
||||
# combines:
|
||||
# a lighthouse
|
||||
# an ambient light
|
||||
# a spotlight
|
||||
# and a shadow
|
||||
|
||||
CONFIG = {
|
||||
"source_point": ORIGIN,
|
||||
"color": LIGHT_COLOR,
|
||||
"num_levels": 10,
|
||||
"radius": 5,
|
||||
"screen": None,
|
||||
"opacity_function": inverse_quadratic(1,2,1),
|
||||
"max_opacity_ambient": AMBIENT_FULL,
|
||||
"max_opacity_spotlight": SPOTLIGHT_FULL
|
||||
}
|
||||
|
||||
def generate_points(self):
|
||||
self.lighthouse = Lighthouse()
|
||||
self.ambient_light = AmbientLight(
|
||||
source_point = self.source_point,
|
||||
color = self.color,
|
||||
num_levels = self.num_levels,
|
||||
radius = self.radius,
|
||||
opacity_function = self.opacity_function,
|
||||
max_opacity = self.max_opacity_ambient
|
||||
)
|
||||
if self.has_screen():
|
||||
self.spotlight = Spotlight(
|
||||
source_point = self.source_point,
|
||||
color = self.color,
|
||||
num_levels = self.num_levels,
|
||||
radius = self.radius,
|
||||
screen = self.screen,
|
||||
opacity_function = self.opacity_function,
|
||||
max_opacity = self.max_opacity_spotlight
|
||||
)
|
||||
else:
|
||||
self.spotlight = Spotlight()
|
||||
|
||||
self.shadow = VMobject(fill_color = SHADOW_COLOR, fill_opacity = 1.0, stroke_color = BLACK)
|
||||
self.lighthouse.next_to(self.source_point,DOWN,buff = 0)
|
||||
self.ambient_light.move_source_to(self.source_point)
|
||||
|
||||
if self.has_screen():
|
||||
self.spotlight.move_source_to(self.source_point)
|
||||
self.update_shadow()
|
||||
|
||||
self.add(self.ambient_light,self.spotlight,self.lighthouse, self.shadow)
|
||||
|
||||
def has_screen(self):
|
||||
return (self.screen != None)
|
||||
|
||||
def dim_ambient(self):
|
||||
self.set_max_opacity_ambient(AMBIENT_DIMMED)
|
||||
|
||||
def set_max_opacity_ambient(self,new_opacity):
|
||||
self.max_opacity_ambient = new_opacity
|
||||
self.ambient_light.dimming(new_opacity)
|
||||
|
||||
def dim_spotlight(self):
|
||||
self.set_max_opacity_spotlight(SPOTLIGHT_DIMMED)
|
||||
|
||||
def set_max_opacity_spotlight(self,new_opacity):
|
||||
self.max_opacity_spotlight = new_opacity
|
||||
self.spotlight.dimming(new_opacity)
|
||||
|
||||
def set_screen(self, new_screen):
|
||||
if self.has_screen():
|
||||
self.spotlight.screen = new_screen
|
||||
else:
|
||||
self.remove(self.spotlight)
|
||||
self.spotlight = Spotlight(
|
||||
source_point = self.source_point,
|
||||
color = self.color,
|
||||
num_levels = self.num_levels,
|
||||
radius = self.radius,
|
||||
screen = new_screen
|
||||
)
|
||||
self.spotlight.move_source_to(self.source_point)
|
||||
self.add(self.spotlight)
|
||||
|
||||
# in any case
|
||||
self.screen = new_screen
|
||||
|
||||
|
||||
|
||||
|
||||
def move_source_to(self,point):
|
||||
|
||||
apoint = np.array(point)
|
||||
v = apoint - self.source_point
|
||||
self.source_point = apoint
|
||||
self.lighthouse.next_to(apoint,DOWN,buff = 0)
|
||||
self.ambient_light.move_source_to(apoint)
|
||||
if self.has_screen():
|
||||
self.spotlight.move_source_to(apoint)
|
||||
self.update()
|
||||
return self
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def set_radius(self,new_radius):
|
||||
self.radius = new_radius
|
||||
self.ambient_light.radius = new_radius
|
||||
self.spotlight.radius = new_radius
|
||||
|
||||
def update(self):
|
||||
self.spotlight.update_sectors()
|
||||
self.update_shadow()
|
||||
|
||||
|
||||
def update_shadow(self):
|
||||
|
||||
point = self.source_point
|
||||
projected_screen_points = []
|
||||
if not self.has_screen():
|
||||
return
|
||||
for point in self.screen.get_anchors():
|
||||
projected_screen_points.append(self.spotlight.project(point))
|
||||
|
||||
print "projected", self.screen.get_anchors(), "onto", projected_screen_points
|
||||
|
||||
|
||||
|
||||
projected_source = project_along_vector(self.source_point,self.spotlight.projection_direction())
|
||||
|
||||
projected_point_cloud_3d = np.append(projected_screen_points,
|
||||
np.reshape(projected_source,(1,3)),axis = 0)
|
||||
rotation_matrix = z_to_vector(self.spotlight.projection_direction())
|
||||
back_rotation_matrix = rotation_matrix.T # i. e. its inverse
|
||||
|
||||
rotated_point_cloud_3d = np.dot(projected_point_cloud_3d,back_rotation_matrix.T)
|
||||
# these points now should all have z = 0
|
||||
point_cloud_2d = rotated_point_cloud_3d[:,:2]
|
||||
# now we can compute the convex hull
|
||||
hull_2d = ConvexHull(point_cloud_2d) # guaranteed to run ccw
|
||||
hull = []
|
||||
|
||||
# we also need the projected source point
|
||||
source_point_2d = np.dot(self.spotlight.project(self.source_point),back_rotation_matrix.T)[:2]
|
||||
|
||||
index = 0
|
||||
for point in point_cloud_2d[hull_2d.vertices]:
|
||||
if np.all(point - source_point_2d < 1.0e-6):
|
||||
source_index = index
|
||||
continue
|
||||
point_3d = np.array([point[0], point[1], 0])
|
||||
hull.append(point_3d)
|
||||
index += 1
|
||||
|
||||
index = source_index
|
||||
|
||||
hull_mobject = VMobject()
|
||||
hull_mobject.set_points_as_corners(hull)
|
||||
hull_mobject.apply_matrix(rotation_matrix)
|
||||
|
||||
|
||||
anchors = hull_mobject.get_anchors()
|
||||
|
||||
# add two control points for the outer cone
|
||||
|
||||
|
||||
ray1 = anchors[index - 1] - projected_source
|
||||
ray1 = ray1/np.linalg.norm(ray1) * 100
|
||||
ray2 = anchors[index] - projected_source
|
||||
ray2 = ray2/np.linalg.norm(ray2) * 100
|
||||
outpoint1 = anchors[index - 1] + ray1
|
||||
outpoint2 = anchors[index] + ray2
|
||||
|
||||
new_anchors = anchors[:index]
|
||||
new_anchors = np.append(new_anchors,np.array([outpoint1, outpoint2]),axis = 0)
|
||||
new_anchors = np.append(new_anchors,anchors[index:],axis = 0)
|
||||
self.shadow.set_points_as_corners(new_anchors)
|
||||
|
||||
# shift it 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,
|
||||
"run_time": SWITCH_ON_RUN_TIME
|
||||
}
|
||||
|
||||
def __init__(self, light, **kwargs):
|
||||
if (not isinstance(light,AmbientLight) and not isinstance(light,Spotlight)):
|
||||
raise Exception("Only AmbientLights and Spotlights can be switched on")
|
||||
LaggedStart.__init__(self,
|
||||
FadeIn, light, **kwargs)
|
||||
|
||||
|
||||
class SwitchOff(LaggedStart):
|
||||
CONFIG = {
|
||||
"lag_ratio": 0.2,
|
||||
"run_time": SWITCH_ON_RUN_TIME
|
||||
}
|
||||
|
||||
def __init__(self, light, **kwargs):
|
||||
if (not isinstance(light,AmbientLight) and not isinstance(light,Spotlight)):
|
||||
raise Exception("Only AmbientLights and Spotlights can be switched off")
|
||||
light.submobjects = light.submobjects[::-1]
|
||||
LaggedStart.__init__(self,
|
||||
FadeOut, light, **kwargs)
|
||||
light.submobjects = light.submobjects[::-1]
|
||||
|
||||
|
||||
|
||||
|
||||
class Lighthouse(SVGMobject):
|
||||
CONFIG = {
|
||||
"file_name" : "lighthouse",
|
||||
"height" : 0.5
|
||||
}
|
||||
|
||||
def move_to(self,point):
|
||||
self.next_to(point, DOWN, buff = 0)
|
||||
|
||||
|
||||
class AmbientLight(VMobject):
|
||||
|
||||
# Parameters are:
|
||||
# * a source point
|
||||
# * an opacity function
|
||||
# * a light color
|
||||
# * a max opacity
|
||||
# * a radius (larger than the opacity's dropoff length)
|
||||
# * the number of subdivisions (levels, annuli)
|
||||
|
||||
CONFIG = {
|
||||
"source_point" : ORIGIN,
|
||||
"opacity_function" : lambda r : 1.0/(r+1.0)**2,
|
||||
"color" : LIGHT_COLOR,
|
||||
"max_opacity" : 1.0,
|
||||
"num_levels" : 10,
|
||||
"radius" : 5.0
|
||||
}
|
||||
|
||||
def generate_points(self):
|
||||
self.source_point = np.array(self.source_point)
|
||||
|
||||
# in theory, this method is only called once, right?
|
||||
# so removing submobs shd not be necessary
|
||||
for submob in self.submobjects:
|
||||
self.remove(submob)
|
||||
|
||||
# create annuli
|
||||
self.radius = float(self.radius)
|
||||
dr = self.radius / self.num_levels
|
||||
for r in np.arange(0, self.radius, dr):
|
||||
alpha = self.max_opacity * self.opacity_function(r)
|
||||
annulus = Annulus(
|
||||
inner_radius = r,
|
||||
outer_radius = r + dr,
|
||||
color = self.color,
|
||||
fill_opacity = alpha
|
||||
)
|
||||
annulus.move_arc_center_to(self.source_point)
|
||||
self.add(annulus)
|
||||
|
||||
|
||||
|
||||
def move_source_to(self,point):
|
||||
|
||||
v = np.array(point) - self.source_point
|
||||
self.source_point = np.array(point)
|
||||
self.shift(v)
|
||||
return self
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def dimming(self,new_alpha):
|
||||
old_alpha = self.max_opacity
|
||||
self.max_opacity = new_alpha
|
||||
for submob in self.submobjects:
|
||||
old_submob_alpha = submob.fill_opacity
|
||||
new_submob_alpha = old_submob_alpha * new_alpha / old_alpha
|
||||
submob.set_fill(opacity = new_submob_alpha)
|
||||
|
||||
|
||||
class Spotlight(VMobject):
|
||||
|
||||
CONFIG = {
|
||||
"source_point" : ORIGIN,
|
||||
"opacity_function" : lambda r : 1.0/(r/2+1.0)**2,
|
||||
"color" : LIGHT_COLOR,
|
||||
"max_opacity" : 1.0,
|
||||
"num_levels" : 10,
|
||||
"radius" : 5.0,
|
||||
"screen" : None,
|
||||
"camera": None
|
||||
}
|
||||
|
||||
def projection_direction(self):
|
||||
if self.camera == None:
|
||||
return OUT
|
||||
else:
|
||||
v = self.camera.get_cartesian_coords()
|
||||
return v/np.linalg.norm(v)
|
||||
|
||||
def project(self,point):
|
||||
v = self.projection_direction()
|
||||
w = project_along_vector(point,v)
|
||||
return w
|
||||
|
||||
def generate_points(self):
|
||||
|
||||
self.submobjects = []
|
||||
|
||||
if self.screen != None:
|
||||
# look for the screen and create annular sectors
|
||||
lower_angle, upper_angle = self.viewing_angles(self.screen)
|
||||
self.radius = float(self.radius)
|
||||
dr = self.radius / self.num_levels
|
||||
lower_ray, upper_ray = self.viewing_rays(self.screen)
|
||||
|
||||
for r in np.arange(0, self.radius, dr):
|
||||
new_sector = self.new_sector(r,dr,lower_angle,upper_angle)
|
||||
self.add(new_sector)
|
||||
|
||||
|
||||
def new_sector(self,r,dr,lower_angle,upper_angle):
|
||||
|
||||
alpha = self.max_opacity * self.opacity_function(r)
|
||||
annular_sector = AnnularSector(
|
||||
inner_radius = r,
|
||||
outer_radius = r + dr,
|
||||
color = self.color,
|
||||
fill_opacity = alpha,
|
||||
start_angle = lower_angle,
|
||||
angle = upper_angle - lower_angle
|
||||
)
|
||||
# rotate (not project) it into the viewing plane
|
||||
rotation_matrix = z_to_vector(self.projection_direction())
|
||||
annular_sector.apply_matrix(rotation_matrix)
|
||||
# now rotate it inside that plane
|
||||
rotated_RIGHT = np.dot(RIGHT, rotation_matrix.T)
|
||||
projected_RIGHT = self.project(RIGHT)
|
||||
omega = angle_between_vectors(rotated_RIGHT,projected_RIGHT)
|
||||
annular_sector.rotate(omega, axis = self.projection_direction())
|
||||
annular_sector.move_arc_center_to(self.source_point)
|
||||
|
||||
return annular_sector
|
||||
|
||||
def viewing_angle_of_point(self,point):
|
||||
# as measured from the positive x-axis
|
||||
v1 = self.project(RIGHT)
|
||||
v2 = self.project(np.array(point) - self.source_point)
|
||||
absolute_angle = angle_between_vectors(v1, v2)
|
||||
# determine the angle's sign depending on their plane's
|
||||
# choice of orientation. That choice is set by the camera
|
||||
# position, i. e. projection direction
|
||||
if np.dot(self.projection_direction(),np.cross(v1, v2)) > 0:
|
||||
return absolute_angle
|
||||
else:
|
||||
return -absolute_angle
|
||||
|
||||
|
||||
def viewing_angles(self,screen):
|
||||
|
||||
screen_points = screen.get_anchors()
|
||||
projected_screen_points = map(self.project,screen_points)
|
||||
|
||||
viewing_angles = np.array(map(self.viewing_angle_of_point,
|
||||
projected_screen_points))
|
||||
|
||||
lower_angle = upper_angle = 0
|
||||
if len(viewing_angles) != 0:
|
||||
lower_angle = np.min(viewing_angles)
|
||||
upper_angle = np.max(viewing_angles)
|
||||
|
||||
return lower_angle, upper_angle
|
||||
|
||||
def viewing_rays(self,screen):
|
||||
|
||||
lower_angle, upper_angle = self.viewing_angles(screen)
|
||||
projected_RIGHT = self.project(RIGHT)/np.linalg.norm(self.project(RIGHT))
|
||||
lower_ray = rotate_vector(projected_RIGHT,lower_angle, axis = self.projection_direction())
|
||||
upper_ray = rotate_vector(projected_RIGHT,upper_angle, axis = self.projection_direction())
|
||||
|
||||
return lower_ray, upper_ray
|
||||
|
||||
|
||||
def opening_angle(self):
|
||||
l,u = self.viewing_angles(self.screen)
|
||||
return u - l
|
||||
|
||||
def start_angle(self):
|
||||
l,u = self.viewing_angles(self.screen)
|
||||
return l
|
||||
|
||||
def stop_angle(self):
|
||||
l,u = self.viewing_angles(self.screen)
|
||||
return u
|
||||
|
||||
def move_source_to(self,point):
|
||||
self.source_point = np.array(point)
|
||||
self.update_sectors()
|
||||
return self
|
||||
|
||||
|
||||
def update_sectors(self):
|
||||
if self.screen == None:
|
||||
return
|
||||
for submob in self.submobject_family():
|
||||
if type(submob) == AnnularSector:
|
||||
lower_angle, upper_angle = self.viewing_angles(self.screen)
|
||||
dr = submob.outer_radius - submob.inner_radius
|
||||
new_submob = self.new_sector(submob.inner_radius,dr,lower_angle,upper_angle)
|
||||
submob.points = new_submob.points
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def dimming(self,new_alpha):
|
||||
old_alpha = self.max_opacity
|
||||
self.max_opacity = new_alpha
|
||||
for submob in self.submobjects:
|
||||
if type(submob) != AnnularSector:
|
||||
# it's the shadow, don't dim it
|
||||
continue
|
||||
old_submob_alpha = submob.fill_opacity
|
||||
new_submob_alpha = old_submob_alpha * new_alpha/old_alpha
|
||||
submob.set_fill(opacity = new_submob_alpha)
|
||||
|
||||
def change_opacity_function(self,new_f):
|
||||
self.opacity_function = new_f
|
||||
dr = self.radius/self.num_levels
|
||||
|
||||
sectors = []
|
||||
for submob in self.submobjects:
|
||||
if type(submob) == AnnularSector:
|
||||
sectors.append(submob)
|
||||
|
||||
for (r,submob) in zip(np.arange(0,self.radius,dr),sectors):
|
||||
if type(submob) != AnnularSector:
|
||||
# it's the shadow, don't dim it
|
||||
continue
|
||||
alpha = self.opacity_function(r)
|
||||
submob.set_fill(opacity = alpha)
|
||||
|
||||
|
||||
|
||||
class ScreenTracker(ContinualAnimation):
|
||||
|
||||
def update_mobject(self, dt):
|
||||
self.mobject.update()
|
||||
|
||||
@@ -83,22 +83,22 @@ class ThreeDCamera(CameraWithPerspective):
|
||||
*self.get_spherical_coords()
|
||||
)
|
||||
def z_cmp(*vmobs):
|
||||
#Compare to three dimensional mobjects based on
|
||||
#how close they are to the camera
|
||||
return cmp(*[
|
||||
-np.linalg.norm(vm.get_center()-camera_point)
|
||||
for vm in vmobs
|
||||
])
|
||||
# three_d_status = map(should_shade_in_3d, vmobs)
|
||||
# has_points = [vm.get_num_points() > 0 for vm in vmobs]
|
||||
# if all(three_d_status) and all(has_points):
|
||||
# cmp_vect = self.get_unit_normal_vect(vmobs[1])
|
||||
# return cmp(*[
|
||||
# np.dot(vm.get_center(), cmp_vect)
|
||||
# for vm in vmobs
|
||||
# ])
|
||||
# else:
|
||||
# return 0
|
||||
# Compare to three dimensional mobjects based on
|
||||
# how close they are to the camera
|
||||
# return cmp(*[
|
||||
# -np.linalg.norm(vm.get_center()-camera_point)
|
||||
# for vm in vmobs
|
||||
# ])
|
||||
three_d_status = map(should_shade_in_3d, vmobs)
|
||||
has_points = [vm.get_num_points() > 0 for vm in vmobs]
|
||||
if all(three_d_status) and all(has_points):
|
||||
cmp_vect = self.get_unit_normal_vect(vmobs[1])
|
||||
return cmp(*[
|
||||
np.dot(vm.get_center(), cmp_vect)
|
||||
for vm in vmobs
|
||||
])
|
||||
else:
|
||||
return 0
|
||||
Camera.display_multiple_vectorized_mobjects(
|
||||
self, sorted(vmobjects, cmp = z_cmp)
|
||||
)
|
||||
@@ -110,6 +110,13 @@ class ThreeDCamera(CameraWithPerspective):
|
||||
if distance is None: distance = curr_d
|
||||
return np.array([phi, theta, distance])
|
||||
|
||||
def get_cartesian_coords(self, phi = None, theta = None, distance = None):
|
||||
spherical_coords_array = self.get_spherical_coords(phi,theta,distance)
|
||||
phi2 = spherical_coords_array[0]
|
||||
theta2 = spherical_coords_array[1]
|
||||
d2 = spherical_coords_array[2]
|
||||
return self.spherical_coords_to_point(phi2,theta2,d2)
|
||||
|
||||
def get_phi(self):
|
||||
return self.get_spherical_coords()[0]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user