Merge pull request #85 from 3b1b/lighthouse

New methods for Arcs and Sectors
This commit is contained in:
Grant Sanderson 2018-01-23 12:08:36 -08:00 committed by GitHub
commit f6c9c6eec6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 277 additions and 57 deletions

View file

@ -40,9 +40,39 @@ INDICATOR_STROKE_WIDTH = 1
INDICATOR_STROKE_COLOR = WHITE INDICATOR_STROKE_COLOR = WHITE
INDICATOR_TEXT_COLOR = WHITE INDICATOR_TEXT_COLOR = WHITE
INDICATOR_UPDATE_TIME = 0.2 INDICATOR_UPDATE_TIME = 0.2
FAST_INDICATOR_UPDATE_TIME = 0.1
OPACITY_FOR_UNIT_INTENSITY = 0.2 OPACITY_FOR_UNIT_INTENSITY = 0.2
SWITCH_ON_RUN_TIME = 2.0 SWITCH_ON_RUN_TIME = 2.5
LIGHT_CONE_NUM_SECTORS = 50 FAST_SWITCH_ON_RUN_TIME = 0.1
LIGHT_CONE_NUM_SECTORS = 30
NUM_CONES = 50 # in first lighthouse scene
NUM_VISIBLE_CONES = 5 # ibidem
ARC_TIP_LENGTH = 0.2
def show_line_length(line):
v = line.points[1] - line.points[0]
print v[0]**2 + v[1]**2
class AngleUpdater(ContinualAnimation):
def __init__(self, angle_arc, lc, **kwargs):
self.angle_arc = angle_arc
self.source_point = angle_arc.get_arc_center()
self.lc = lc
#self.angle_decimal = angle_decimal
ContinualAnimation.__init__(self, self.angle_arc, **kwargs)
def update_mobject(self, dt):
# angle arc
new_arc = self.angle_arc.copy().set_bound_angles(
start = self.lc.start_angle,
stop = self.lc.stop_angle()
)
new_arc.generate_points()
new_arc.move_arc_center_to(self.source_point)
self.angle_arc.points = new_arc.points
self.angle_arc.add_tip(tip_length = ARC_TIP_LENGTH, at_start = True, at_end = True)
@ -95,14 +125,13 @@ class LightScreen(VMobject):
ray1 = self.screen.points[0] - self.light_source ray1 = self.screen.points[0] - self.light_source
ray2 = self.screen.points[-1] - self.light_source ray2 = self.screen.points[-1] - self.light_source
ray1 = ray1/np.linalg.norm(ray1) * 100 ray1 = ray1/np.linalg.norm(ray1) * 100
ray1 = rotate_vector(ray1,TAU/16) ray1 = rotate_vector(ray1,-TAU/16)
ray2 = ray2/np.linalg.norm(ray2) * 100 ray2 = ray2/np.linalg.norm(ray2) * 100
ray2 = rotate_vector(ray2,-TAU/16) ray2 = rotate_vector(ray2,TAU/16)
outpoint1 = self.screen.points[0] + ray1 outpoint1 = self.screen.points[0] + ray1
outpoint2 = self.screen.points[-1] + ray2 outpoint2 = self.screen.points[-1] + ray2
self.shadow.add_control_points([outpoint2,outpoint1,self.screen.points[0]]) self.shadow.add_control_points([outpoint2,outpoint1,self.screen.points[0]])
self.shadow.mark_paths_closed = True self.shadow.mark_paths_closed = True
class LightCone(VGroup): class LightCone(VGroup):
@ -110,6 +139,7 @@ class LightCone(VGroup):
"start_angle": 0, "start_angle": 0,
"angle" : TAU/8, "angle" : TAU/8,
"radius" : 10, "radius" : 10,
"brightness" : 1,
"opacity_function" : lambda r : 1./max(r, 0.01), "opacity_function" : lambda r : 1./max(r, 0.01),
"num_sectors" : 10, "num_sectors" : 10,
"color": LIGHT_COLOR, "color": LIGHT_COLOR,
@ -126,7 +156,7 @@ class LightCone(VGroup):
stroke_width = 0, stroke_width = 0,
stroke_color = self.color, stroke_color = self.color,
fill_color = self.color, fill_color = self.color,
fill_opacity = self.opacity_function(r1), fill_opacity = self.brightness * self.opacity_function(r1),
) )
for r1, r2 in zip(radii, radii[1:]) for r1, r2 in zip(radii, radii[1:])
] ]
@ -156,6 +186,15 @@ class LightCone(VGroup):
submob.generate_points() submob.generate_points()
submob.shift(source_point - submob.get_arc_center()) submob.shift(source_point - submob.get_arc_center())
def set_brightness(self,new_brightness):
self.brightness = new_brightness
radii = np.linspace(0, self.radius, self.num_sectors+1)
for (r1,sector) in zip(radii,self.submobjects):
sector.set_fill(opacity = self.brightness * self.opacity_function(r1))
def stop_angle(self):
return self.start_angle + self.angle
@ -165,13 +204,14 @@ class LightCone(VGroup):
class Candle(VGroup): class Candle(VGroup):
CONFIG = { CONFIG = {
"radius" : 5, "radius" : 5,
"brightness" : 1.0,
"opacity_function" : lambda r : 1./max(r, 0.01), "opacity_function" : lambda r : 1./max(r, 0.01),
"num_sectors" : 10, "num_annuli" : 10,
"color": LIGHT_COLOR, "color": LIGHT_COLOR,
} }
def generate_points(self): def generate_points(self):
radii = np.linspace(0, self.radius, self.num_sectors+1) radii = np.linspace(0, self.radius, self.num_annuli+1)
annuli = [ annuli = [
Annulus( Annulus(
inner_radius = r1, inner_radius = r1,
@ -179,7 +219,7 @@ class Candle(VGroup):
stroke_width = 0, stroke_width = 0,
stroke_color = self.color, stroke_color = self.color,
fill_color = self.color, fill_color = self.color,
fill_opacity = self.opacity_function(r1), fill_opacity = self.brightness * self.opacity_function(r1),
) )
for r1, r2 in zip(radii, radii[1:]) for r1, r2 in zip(radii, radii[1:])
] ]
@ -197,6 +237,14 @@ class Candle(VGroup):
source = self.submobjects[0].get_center() source = self.submobjects[0].get_center()
self.shift(point - source) self.shift(point - source)
def set_brightness(self,new_brightness):
self.brightness = new_brightness
radii = np.linspace(0, self.radius, self.num_annuli+1)
for (r1,annulus) in zip(radii,self.submobjects):
annulus.set_fill(opacity = self.brightness * self.opacity_function(r1))
class SwitchOn(LaggedStart): class SwitchOn(LaggedStart):
CONFIG = { CONFIG = {
@ -506,13 +554,16 @@ class FirstLightHouseScene(PiCreatureScene):
color = WHITE, color = WHITE,
number_at_center = 1.6, number_at_center = 1.6,
stroke_width = 1, stroke_width = 1,
numbers_with_elongated_ticks = [0,1,2,3], numbers_with_elongated_ticks = range(1,5),
numbers_to_show = np.arange(1,5), numbers_to_show = range(1,5),
unit_size = 2, unit_size = 2,
tick_frequency = 0.2, tick_frequency = 0.2,
line_to_number_buff = LARGE_BUFF line_to_number_buff = LARGE_BUFF,
label_direction = UP,
) )
self.number_line.label_direction = DOWN
self.number_line_labels = self.number_line.get_number_mobjects() self.number_line_labels = self.number_line.get_number_mobjects()
self.add(self.number_line,self.number_line_labels) self.add(self.number_line,self.number_line_labels)
self.wait() self.wait()
@ -549,15 +600,27 @@ class FirstLightHouseScene(PiCreatureScene):
lighthouse_pos = [] lighthouse_pos = []
light_cones = [] light_cones = []
num_cones = 6
for i in range(1,num_cones+1): euler_sum_above = TexMobject("1", "+", "{1\over 4}",
"+", "{1\over 9}", "+", "{1\over 16}", "+", "{1\over 25}", "+", "{1\over 36}")
for (i,term) in zip(range(len(euler_sum_above)),euler_sum_above):
#horizontal alignment with tick marks
term.next_to(self.number_line.number_to_point(0.5*i+1),UP,buff = 2)
# vertical alignment with light indicator
old_y = term.get_center()[1]
new_y = light_indicator.get_center()[1]
term.shift([0,new_y - old_y,0])
for i in range(1,NUM_CONES+1):
lighthouse = LightHouse() lighthouse = LightHouse()
point = self.number_line.number_to_point(i) point = self.number_line.number_to_point(i)
light_cone = Candle( light_cone = Candle(
opacity_function = inverse_quadratic(1,1), opacity_function = inverse_quadratic(1,1),
num_sectors = LIGHT_CONE_NUM_SECTORS, num_annuli = LIGHT_CONE_NUM_SECTORS,
radius = 10) radius = 12)
light_cone.move_source_to(point) light_cone.move_source_to(point)
lighthouse.next_to(point,DOWN,0) lighthouse.next_to(point,DOWN,0)
@ -571,33 +634,67 @@ class FirstLightHouseScene(PiCreatureScene):
light_indicator.set_intensity(0) light_indicator.set_intensity(0)
intensities = np.cumsum(np.array([1./n**2 for n in range(1,num_cones+1)])) intensities = np.cumsum(np.array([1./n**2 for n in range(1,NUM_CONES+1)]))
opacities = intensities * light_indicator.opacity_for_unit_intensity opacities = intensities * light_indicator.opacity_for_unit_intensity
self.remove_foreground_mobjects(light_indicator) self.remove_foreground_mobjects(light_indicator)
# slowly switch on visible light cones and increment indicator
for (i,lc) in zip(range(NUM_VISIBLE_CONES),light_cones[:NUM_VISIBLE_CONES]):
for (i,lc) in zip(range(num_cones),light_cones): indicator_start_time = 0.4 * (i+1) * SWITCH_ON_RUN_TIME/lc.radius * self.number_line.unit_size
indicator_start_time = 0.5 * (i+1) * SWITCH_ON_RUN_TIME/lc.radius * self.number_line.unit_size
indicator_stop_time = indicator_start_time + INDICATOR_UPDATE_TIME indicator_stop_time = indicator_start_time + INDICATOR_UPDATE_TIME
indicator_rate_func = squish_rate_func(#smooth, 0.8, 0.9) indicator_rate_func = squish_rate_func(
smooth,indicator_start_time,indicator_stop_time) smooth,indicator_start_time,indicator_stop_time)
self.play( self.play(
SwitchOn(lc), SwitchOn(lc),
FadeIn(euler_sum_above[2*i], run_time = SWITCH_ON_RUN_TIME,
rate_func = indicator_rate_func),
FadeIn(euler_sum_above[2*i - 1], run_time = SWITCH_ON_RUN_TIME,
rate_func = indicator_rate_func),
ChangeDecimalToValue(light_indicator.reading,intensities[i], ChangeDecimalToValue(light_indicator.reading,intensities[i],
rate_func = indicator_rate_func, run_time = SWITCH_ON_RUN_TIME), rate_func = indicator_rate_func, run_time = SWITCH_ON_RUN_TIME),
ApplyMethod(light_indicator.foreground.set_fill,None,opacities[i]) ApplyMethod(light_indicator.foreground.set_fill,None,opacities[i])
) )
if i == 0: if i == 0:
# mvoe a copy out of the thought bubble for comparison # move a copy out of the thought bubble for comparison
light_indicator_copy = light_indicator.copy() light_indicator_copy = light_indicator.copy()
old_y = light_indicator_copy.get_center()[1]
new_y = self.number_line.get_center()[1]
self.play( self.play(
light_indicator_copy.shift,[2,0,0] light_indicator_copy.shift,[0, new_y - old_y,0]
) )
# quickly switch on off-screen light cones and increment indicator
for (i,lc) in zip(range(NUM_VISIBLE_CONES,NUM_CONES),light_cones[NUM_VISIBLE_CONES:NUM_CONES]):
indicator_start_time = 0.5 * (i+1) * FAST_SWITCH_ON_RUN_TIME/lc.radius * self.number_line.unit_size
indicator_stop_time = indicator_start_time + FAST_INDICATOR_UPDATE_TIME
indicator_rate_func = squish_rate_func(#smooth, 0.8, 0.9)
smooth,indicator_start_time,indicator_stop_time)
self.play(
SwitchOn(lc, run_time = FAST_SWITCH_ON_RUN_TIME),
ChangeDecimalToValue(light_indicator.reading,intensities[i],
rate_func = indicator_rate_func, run_time = FAST_SWITCH_ON_RUN_TIME),
ApplyMethod(light_indicator.foreground.set_fill,None,opacities[i])
)
# show limit value in light indicator and an equals sign
limit_reading = TexMobject("{\pi^2 \over 6}")
limit_reading.move_to(light_indicator.reading)
equals_sign = TexMobject("=")
equals_sign.next_to(randy, UP)
old_y = equals_sign.get_center()[1]
new_y = euler_sum_above.get_center()[1]
equals_sign.shift([0,new_y - old_y,0])
self.play(
FadeOut(light_indicator.reading),
FadeIn(limit_reading),
FadeIn(equals_sign),
)
@ -622,34 +719,48 @@ class SingleLightHouseScene(PiCreatureScene):
lighthouse = LightHouse() lighthouse = LightHouse()
candle = Candle( candle = Candle(
opacity_function = inverse_quadratic(0.3,1), opacity_function = inverse_quadratic(1,1),
num_sectors = LIGHT_CONE_NUM_SECTORS, num_annuli = LIGHT_CONE_NUM_SECTORS,
radius = 10 radius = 10,
brightness = 1,
) )
lighthouse.scale(2).next_to(source_point, DOWN, buff = 0) lighthouse.scale(2).next_to(source_point, DOWN, buff = 0)
candle.move_to(source_point) candle.move_to(source_point)
morty = self.get_primary_pi_creature() morty = self.get_primary_pi_creature()
morty.scale(0.5) morty.scale(0.5)
morty.move_to(observer_point) morty.move_to(observer_point)
self.add(lighthouse, candle) self.add(lighthouse)
self.wait()
self.play( self.play(
SwitchOn(candle) SwitchOn(candle)
) )
light_cone = LightCone() light_cone = LightCone(
opacity_function = inverse_quadratic(1,1),
num_sectors = LIGHT_CONE_NUM_SECTORS,
radius = 10,
brightness = 5,
)
light_cone.move_source_to(source_point) light_cone.move_source_to(source_point)
screen = Arc(TAU/4).rotate_in_place(TAU/2).shift(3*RIGHT) screen = Line([0,-1,0],[0,1,0])
screen.radius = 4 show_line_length(screen)
screen.start_angle = -TAU/5
screen.next_to(morty, LEFT) screen.rotate_in_place(-TAU/6)
show_line_length(screen)
screen.next_to(morty, LEFT, buff = 1)
light_screen = LightScreen(light_source = source_point, light_screen = LightScreen(light_source = source_point,
screen = screen, light_cone = light_cone) screen = screen, light_cone = light_cone)
light_screen.screen.color = WHITE light_screen.screen.color = WHITE
light_screen.screen.fill_opacity = 1 light_screen.screen.fill_opacity = 1
light_screen.update_light_cone(light_cone) light_screen.update_light_cone(light_cone)
self.add(light_screen) self.play(
FadeIn(light_screen, run_time = 2),
# dim the light that misses the screen
ApplyMethod(candle.set_brightness,0.3),
ApplyMethod(light_screen.update_shadow,light_screen.shadow),
FadeIn(light_cone),
)
lc_updater = lambda lc: light_screen.update_light_cone(lc) lc_updater = lambda lc: light_screen.update_light_cone(lc)
sh_updater = lambda sh: light_screen.update_shadow(sh) sh_updater = lambda sh: light_screen.update_shadow(sh)
@ -662,9 +773,58 @@ class SingleLightHouseScene(PiCreatureScene):
self.add(ca1, ca2) self.add(ca1, ca2)
self.add_foreground_mobject(morty) self.add_foreground_mobject(morty)
moving_screen = ApplyMethod(screen.move_to, [1,0,0], run_time=3) pointing_screen_at_source = ApplyMethod(screen.rotate_in_place,TAU/6)
self.play(pointing_screen_at_source)
self.play(moving_screen) arc_angle = light_cone.angle
# draw arc arrows to show the opening angle
angle_arc = Arc(radius = 5, start_angle = light_cone.start_angle,
angle = light_cone.angle, tip_length = ARC_TIP_LENGTH)
#angle_arc.add_tip(at_start = True, at_end = True)
angle_arc.move_arc_center_to(source_point)
self.add(angle_arc)
angle_indicator = DecimalNumber(arc_angle/TAU*360,
num_decimal_points = 0,
unit = "^\\circ")
angle_indicator.next_to(angle_arc,RIGHT)
self.add_foreground_mobject(angle_indicator)
angle_update_func = lambda x: light_cone.angle/TAU*360
ca3 = ContinualChangingDecimal(angle_indicator,angle_update_func)
self.add(ca3)
#ca4 = ContinualUpdateFromFunc(angle_arc,update_angle_arc)
ca4 = AngleUpdater(angle_arc, light_screen.light_cone)
self.add(ca4)
rotating_screen = ApplyMethod(light_screen.screen.rotate_in_place, TAU/6, run_time=3, rate_func = wiggle)
self.play(rotating_screen)
#rotating_screen_back = ApplyMethod(light_screen.screen.rotate_in_place, -TAU/6) #, run_time=3, rate_func = wiggle)
#self.play(rotating_screen_back)
self.wait()
# morph into Earth scene
globe = Circle(radius = 3)
globe.move_to([2,0,0])
sun_position = [-100,0,0]
self.play(
ApplyMethod(lighthouse.move_to,sun_position),
ApplyMethod(candle.move_to,sun_position),
ApplyMethod(light_cone.move_source_to,sun_position),
FadeOut(angle_arc),
FadeOut(angle_indicator),
FadeIn(globe),
ApplyMethod(light_screen.move_to,[0,0,0]),
ApplyMethod(morty.move_to,[1,0,0])
)

View file

@ -248,9 +248,9 @@ class Mobject(object):
mob.points += about_point mob.points += about_point
return self return self
def rotate_in_place(self, angle, axis = OUT, axes = []): def rotate_in_place(self, angle, axis = OUT):
# redundant with default behavior of rotate now. # redundant with default behavior of rotate now.
return self.rotate(angle, axis = axis, axes = axes) return self.rotate(angle, axis = axis)
def scale_in_place(self, scale_factor, **kwargs): def scale_in_place(self, scale_factor, **kwargs):
#Redundant with default behavior of scale now. #Redundant with default behavior of scale now.

View file

@ -36,18 +36,70 @@ class Arc(VMobject):
) )
self.scale(self.radius, about_point = ORIGIN) self.scale(self.radius, about_point = ORIGIN)
def add_tip(self, tip_length = 0.25): def add_tip(self, tip_length = 0.25, at_start = False, at_end = True):
# clear out any old tips
for submob in self.submobjects:
if submob.mark_paths_closed == True: # is a tip
self.remove(submob)
#TODO, do this a better way #TODO, do this a better way
p1, p2 = self.points[-2:] p1 = p2 = p3 = p4 = None
arrow = Arrow( start_arrow = end_arrow = None
p1, 2*p2 - p1, if at_start:
tip_length = tip_length, p1, p2 = self.points[-3:-1]
max_tip_length_to_length_ratio = 2.0 # self.points[-2:] did overshoot
) start_arrow = Arrow(
self.add(arrow.split()[-1]) p1, 2*p2 - p1,
tip_length = tip_length,
max_tip_length_to_length_ratio = 2.0
)
self.add(start_arrow.split()[-1]) # just the tip
if at_end:
p4, p3 = self.points[1:3]
# self.points[:2] did overshoot
end_arrow = Arrow(
p3, 2*p4 - p3,
tip_length = tip_length,
max_tip_length_to_length_ratio = 2.0
)
self.add(end_arrow.split()[-1])
self.highlight(self.get_color()) self.highlight(self.get_color())
return self return self
def get_arc_center(self):
first_point = self.points[0]
radial_unit_vector = np.array([np.cos(self.start_angle),np.sin(self.start_angle),0])
arc_center = first_point - self.radius * radial_unit_vector
return arc_center
def move_arc_center_to(self,point):
v = point - self.get_arc_center()
self.shift(v)
return self
def stop_angle(self):
return self.start_angle + self.angle
def set_bound_angles(self,start=0,stop=np.pi):
self.start_angle = start
self.angle = stop - start
return self
class Circle(Arc): class Circle(Arc):
CONFIG = { CONFIG = {
"color" : RED, "color" : RED,
@ -118,14 +170,18 @@ class AnnularSector(VMobject):
v = last_point - first_point v = last_point - first_point
radial_unit_vector = v/np.linalg.norm(v) radial_unit_vector = v/np.linalg.norm(v)
arc_center = first_point - self.inner_radius * radial_unit_vector arc_center = first_point - self.inner_radius * radial_unit_vector
# radial_unit_vector = np.array([np.cos(self.start_angle),
# np.sin(self.start_angle), 0])
# arc_center = inner_arc_start_point - inner_arc.radius * radial_unit_vector
return arc_center return arc_center
<<<<<<< HEAD
def move_arc_center_to(self,point):
v = point - self.get_arc_center()
self.shift(v)
return self
=======
>>>>>>> master
class Sector(AnnularSector): class Sector(AnnularSector):
CONFIG = { CONFIG = {
@ -162,7 +218,7 @@ class Annulus(Circle):
class Line(VMobject): class Line(VMobject):
CONFIG = { CONFIG = {
"buff" : 0, "buff" : 0,
"path_arc" : None, "path_arc" : None, # angle of arc specified here
"n_arc_anchors" : 10, #Only used if path_arc is not None "n_arc_anchors" : 10, #Only used if path_arc is not None
} }
def __init__(self, start, end, **kwargs): def __init__(self, start, end, **kwargs):

View file

@ -21,11 +21,12 @@ class NumberLine(VMobject):
"longer_tick_multiple" : 2, "longer_tick_multiple" : 2,
"number_at_center" : 0, "number_at_center" : 0,
"number_scale_val" : 0.75, "number_scale_val" : 0.75,
"line_to_number_vect" : DOWN, "label_direction" : DOWN,
"line_to_number_buff" : MED_SMALL_BUFF, "line_to_number_buff" : MED_SMALL_BUFF,
"include_tip" : False, "include_tip" : False,
"propagate_style_to_family" : True, "propagate_style_to_family" : True,
} }
def __init__(self, **kwargs): def __init__(self, **kwargs):
digest_config(self, kwargs) digest_config(self, kwargs)
if self.leftmost_tick is None: if self.leftmost_tick is None:
@ -113,7 +114,7 @@ class NumberLine(VMobject):
mob.scale(self.number_scale_val) mob.scale(self.number_scale_val)
mob.next_to( mob.next_to(
self.number_to_point(number), self.number_to_point(number),
self.line_to_number_vect, self.label_direction,
self.line_to_number_buff, self.line_to_number_buff,
) )
result.add(mob) result.add(mob)
@ -135,6 +136,8 @@ class NumberLine(VMobject):
self.tip = tip self.tip = tip
self.add(tip) self.add(tip)
class UnitInterval(NumberLine): class UnitInterval(NumberLine):
CONFIG = { CONFIG = {
"x_min" : 0, "x_min" : 0,
@ -386,4 +389,3 @@ class NumberPlane(VMobject):

View file

@ -26,6 +26,7 @@ class DecimalNumber(VMobject):
if self.show_ellipsis: if self.show_ellipsis:
self.add(TexMobject("\\dots")) self.add(TexMobject("\\dots"))
if self.unit is not None: if self.unit is not None:
self.add(TexMobject(self.unit)) self.add(TexMobject(self.unit))
@ -33,6 +34,7 @@ class DecimalNumber(VMobject):
buff = self.digit_to_digit_buff, buff = self.digit_to_digit_buff,
aligned_edge = DOWN aligned_edge = DOWN
) )
if num_string.startswith("-"): if num_string.startswith("-"):
minus = self.submobjects[0] minus = self.submobjects[0]
minus.next_to( minus.next_to(