3b1b-manim/topics/characters.py

483 lines
15 KiB
Python
Raw Normal View History

from helpers import *
2015-06-13 19:00:23 -07:00
2015-12-13 15:41:45 -08:00
from mobject import Mobject
2016-04-17 19:29:27 -07:00
from mobject.svg_mobject import SVGMobject
2016-09-07 22:03:56 -07:00
from mobject.vectorized_mobject import VMobject, VGroup
2016-08-13 18:27:02 -07:00
from mobject.tex_mobject import TextMobject, TexMobject
2016-08-09 14:07:23 -07:00
from animation import Animation
2016-08-28 18:26:08 -07:00
from animation.transform import Transform, ApplyMethod, \
FadeOut, FadeIn, ApplyPointwiseFunction
2016-07-18 11:50:26 -07:00
from animation.simple_animations import Write
from scene import Scene
2015-06-13 19:00:23 -07:00
2015-10-29 13:45:28 -07:00
PI_CREATURE_DIR = os.path.join(IMAGE_DIR, "PiCreature")
2016-08-02 15:50:32 -07:00
PI_CREATURE_SCALE_FACTOR = 0.5
2015-10-29 13:45:28 -07:00
2016-04-17 19:29:27 -07:00
MOUTH_INDEX = 5
BODY_INDEX = 4
RIGHT_PUPIL_INDEX = 3
LEFT_PUPIL_INDEX = 2
RIGHT_EYE_INDEX = 1
LEFT_EYE_INDEX = 0
2016-04-17 19:29:27 -07:00
class PiCreature(SVGMobject):
2016-02-27 16:32:53 -08:00
CONFIG = {
2016-04-17 19:29:27 -07:00
"color" : BLUE_E,
"stroke_width" : 0,
"fill_opacity" : 1.0,
2016-08-02 15:50:32 -07:00
"initial_scale_factor" : 0.01,
"corner_scale_factor" : 0.75,
"flip_at_start" : False,
2016-08-30 22:17:07 -07:00
"is_looking_direction_purposeful" : False,
2016-09-07 22:03:56 -07:00
"start_corner" : None,
}
2016-04-17 19:29:27 -07:00
def __init__(self, mode = "plain", **kwargs):
self.parts_named = False
svg_file = os.path.join(
PI_CREATURE_DIR,
"PiCreatures_%s.svg"%mode
)
digest_config(self, kwargs, locals())
SVGMobject.__init__(self, svg_file, **kwargs)
self.init_colors()
if self.flip_at_start:
self.flip()
2016-09-07 22:03:56 -07:00
if self.start_corner is not None:
self.to_corner(self.start_corner)
2016-04-17 19:29:27 -07:00
def name_parts(self):
self.mouth = self.submobjects[MOUTH_INDEX]
self.body = self.submobjects[BODY_INDEX]
self.pupils = VMobject(*[
self.submobjects[LEFT_PUPIL_INDEX],
self.submobjects[RIGHT_PUPIL_INDEX]
])
self.eyes = VMobject(*[
self.submobjects[LEFT_EYE_INDEX],
self.submobjects[RIGHT_EYE_INDEX]
])
self.submobjects = []
self.add(self.body, self.mouth, self.eyes, self.pupils)
self.parts_named = True
def init_colors(self):
2016-04-23 23:36:05 -07:00
self.set_stroke(color = BLACK, width = self.stroke_width)
2016-04-17 19:29:27 -07:00
if not self.parts_named:
self.name_parts()
2016-04-30 15:08:53 -07:00
self.mouth.set_fill(BLACK, opacity = 1)
self.body.set_fill(self.color, opacity = 1)
self.pupils.set_fill(BLACK, opacity = 1)
self.eyes.set_fill(WHITE, opacity = 1)
return self
2016-04-17 19:29:27 -07:00
def highlight(self, color):
self.body.set_fill(color)
return self
2015-06-19 08:31:02 -07:00
2016-04-17 19:29:27 -07:00
def change_mode(self, mode):
2016-09-07 22:03:56 -07:00
curr_eye_center = self.eyes.get_center()
2016-04-17 19:29:27 -07:00
curr_height = self.get_height()
2016-09-07 22:03:56 -07:00
should_be_flipped = self.is_flipped()
should_look = hasattr(self, "purposeful_looking_direction")
if should_look:
looking_direction = self.purposeful_looking_direction
self.__init__(mode)
2016-04-17 19:29:27 -07:00
self.scale_to_fit_height(curr_height)
2016-09-07 22:03:56 -07:00
self.shift(curr_eye_center - self.eyes.get_center())
if should_be_flipped ^ self.is_flipped():
2016-04-17 19:29:27 -07:00
self.flip()
2016-09-07 22:03:56 -07:00
if should_look:
2016-08-30 22:17:07 -07:00
self.look(looking_direction)
return self
2015-06-27 04:49:10 -07:00
def look(self, direction):
2016-09-07 22:03:56 -07:00
direction = direction/np.linalg.norm(direction)
self.purposeful_looking_direction = direction
for pupil, eye in zip(self.pupils.split(), self.eyes.split()):
2016-09-07 22:03:56 -07:00
pupil_radius = pupil.get_width()/2.
eye_radius = eye.get_width()/2.
pupil.move_to(eye)
if direction[1] < 0:
pupil.shift(pupil_radius*DOWN/3)
pupil.shift(direction*(eye_radius-pupil_radius))
bottom_diff = eye.get_bottom()[1] - pupil.get_bottom()[1]
if bottom_diff > 0:
pupil.shift(bottom_diff*UP)
return self
2015-06-19 08:31:02 -07:00
2016-09-07 22:03:56 -07:00
def look_at(self, point_or_mobject):
if isinstance(point_or_mobject, Mobject):
point = point_or_mobject.get_center()
else:
point = point_or_mobject
self.look(point - self.eyes.get_center())
2016-08-09 14:07:23 -07:00
def get_looking_direction(self):
return np.sign(np.round(
self.pupils.get_center() - self.eyes.get_center(),
2016-08-30 22:17:07 -07:00
decimals = 2
))
2016-04-17 19:29:27 -07:00
def is_flipped(self):
return self.eyes.submobjects[0].get_center()[0] > \
self.eyes.submobjects[1].get_center()[0]
2015-06-19 08:31:02 -07:00
2015-06-22 10:14:53 -07:00
def blink(self):
2016-04-17 19:29:27 -07:00
eye_bottom_y = self.eyes.get_bottom()[1]
for mob in self.eyes, self.pupils:
mob.apply_function(
lambda p : [p[0], eye_bottom_y, p[2]]
)
return self
2015-06-27 04:49:10 -07:00
def to_corner(self, vect = None):
if vect is not None:
SVGMobject.to_corner(self, vect)
else:
self.scale(self.corner_scale_factor)
self.to_corner(DOWN+LEFT)
return self
2016-07-12 10:34:35 -07:00
def get_bubble(self, bubble_type = "thought", **kwargs):
2016-05-03 23:14:40 -07:00
if bubble_type == "thought":
2016-07-12 10:34:35 -07:00
bubble = ThoughtBubble(**kwargs)
2016-05-03 23:14:40 -07:00
elif bubble_type == "speech":
2016-07-12 10:34:35 -07:00
bubble = SpeechBubble(**kwargs)
2016-05-03 23:14:40 -07:00
else:
raise Exception("%s is an invalid bubble type"%bubble_type)
bubble.pin_to(self)
return bubble
2015-06-13 19:00:23 -07:00
class Randolph(PiCreature):
pass #Nothing more than an alternative name
class Mortimer(PiCreature):
2016-02-27 16:32:53 -08:00
CONFIG = {
2016-07-13 22:03:31 -07:00
"color" : GREY_BROWN,
"flip_at_start" : True,
}
2016-05-03 23:14:40 -07:00
2015-06-27 04:49:10 -07:00
class Mathematician(PiCreature):
2016-02-27 16:32:53 -08:00
CONFIG = {
"color" : GREY,
}
class Blink(ApplyMethod):
CONFIG = {
"rate_func" : squish_rate_func(there_and_back)
}
def __init__(self, pi_creature, **kwargs):
ApplyMethod.__init__(self, pi_creature.blink, **kwargs)
2016-08-09 14:07:23 -07:00
class DoTheWave(Transform):
CONFIG = {
"run_time" : 2
}
def __init__(self, pi_creature, **kwargs):
start_state = pi_creature.copy()
self.target_states = [
pi_creature.copy().change_mode("wave_%d"%x)
for x in 1, 2, 3
] + [
pi_creature.copy()
]
Transform.__init__(self, pi_creature, self.target_states[0], **kwargs)
def update_mobject(self, alpha):
scaled = alpha*len(self.target_states)
try:
if scaled-1 > 0:
self.starting_mobject = self.target_states[int(scaled)-1]
self.ending_mobject = self.target_states[int(scaled)]
except IndexError:
self.ending_mobject = self.target_states[-1]
Transform.update_mobject(self, scaled%1)
return self
2016-04-17 19:29:27 -07:00
class Bubble(SVGMobject):
2016-02-27 16:32:53 -08:00
CONFIG = {
"direction" : LEFT,
"center_point" : ORIGIN,
"content_scale_factor" : 0.75,
"height" : 5,
"width" : 8,
2016-09-07 22:03:56 -07:00
"bubble_center_adjustment_factor" : 1./8,
2016-04-17 19:29:27 -07:00
"file_name" : None,
"propogate_style_to_family" : True,
}
def __init__(self, **kwargs):
2016-09-07 22:03:56 -07:00
digest_config(self, kwargs)
2016-04-17 19:29:27 -07:00
if self.file_name is None:
raise Exception("Must invoke Bubble subclass")
svg_file = os.path.join(
IMAGE_DIR, self.file_name
)
SVGMobject.__init__(self, svg_file, **kwargs)
self.center()
self.stretch_to_fit_height(self.height)
self.stretch_to_fit_width(self.width)
if self.direction[0] > 0:
2016-04-17 19:29:27 -07:00
Mobject.flip(self)
2016-08-02 12:26:15 -07:00
self.direction_was_specified = ("direction" in kwargs)
self.content = Mobject()
2015-06-19 08:31:02 -07:00
def get_tip(self):
2016-07-18 11:50:26 -07:00
#TODO, find a better way
return self.get_corner(DOWN+self.direction)-0.6*self.direction
2015-06-19 08:31:02 -07:00
def get_bubble_center(self):
2016-09-07 22:03:56 -07:00
factor = self.bubble_center_adjustment_factor
return self.get_center() + factor*self.get_height()*UP
2015-06-13 19:00:23 -07:00
def move_tip_to(self, point):
self.shift(point - self.get_tip())
return self
2015-06-13 19:00:23 -07:00
def flip(self):
2016-04-17 19:29:27 -07:00
Mobject.flip(self)
self.direction = -np.array(self.direction)
return self
2015-06-13 19:00:23 -07:00
2016-08-02 12:26:15 -07:00
def pin_to(self, mobject):
mob_center = mobject.get_center()
2016-08-02 12:26:15 -07:00
want_to_filp = np.sign(mob_center[0]) != np.sign(self.direction[0])
can_flip = not self.direction_was_specified
if want_to_filp and can_flip:
self.flip()
2016-07-18 11:50:26 -07:00
boundary_point = mobject.get_critical_point(UP-self.direction)
vector_from_center = 1.0*(boundary_point-mob_center)
self.move_tip_to(mob_center+vector_from_center)
return self
2015-06-13 19:00:23 -07:00
2016-07-12 10:34:35 -07:00
def position_mobject_inside(self, mobject):
2016-04-17 19:29:27 -07:00
scaled_width = self.content_scale_factor*self.get_width()
if mobject.get_width() > scaled_width:
2016-05-03 23:14:40 -07:00
mobject.scale_to_fit_width(scaled_width)
2016-07-12 10:34:35 -07:00
mobject.shift(
self.get_bubble_center() - mobject.get_center()
)
return mobject
def add_content(self, mobject):
self.position_mobject_inside(mobject)
self.content = mobject
return self.content
2015-06-13 19:00:23 -07:00
def write(self, text):
self.add_content(TextMobject(text))
return self
def clear(self):
self.add_content(VMobject())
return self
2015-06-13 19:00:23 -07:00
class SpeechBubble(Bubble):
2016-02-27 16:32:53 -08:00
CONFIG = {
2016-04-17 19:29:27 -07:00
"file_name" : "Bubbles_speech.svg",
"height" : 4
}
class ThoughtBubble(Bubble):
2016-02-27 16:32:53 -08:00
CONFIG = {
2016-04-17 19:29:27 -07:00
"file_name" : "Bubbles_thought.svg",
}
2015-12-13 15:41:45 -08:00
def __init__(self, **kwargs):
Bubble.__init__(self, **kwargs)
self.submobjects.sort(
lambda m1, m2 : int((m1.get_bottom()-m2.get_bottom())[1])
)
2016-07-13 22:03:31 -07:00
def make_green_screen(self):
self.submobjects[-1].set_fill(GREEN_SCREEN, opacity = 1)
return self
2016-08-17 16:14:15 -07:00
class RandolphScene(Scene):
CONFIG = {
"randy_kwargs" : {},
"randy_corner" : DOWN+LEFT
2016-08-17 16:14:15 -07:00
}
def setup(self):
self.randy = Randolph(**self.randy_kwargs)
self.randy.to_corner(self.randy_corner)
if self.randy_corner[0] > 0:
self.randy.flip()
2016-08-17 16:14:15 -07:00
self.add(self.randy)
2016-08-18 12:54:04 -07:00
def dither(self, time = 1, blink = True):
while time > 0:
if blink and time%2 == 1:
self.play(Blink(self.randy))
else:
Scene.dither(self, time)
2016-08-18 12:54:04 -07:00
time -= 1
2016-08-17 16:14:15 -07:00
return self
2016-07-18 11:50:26 -07:00
class TeacherStudentsScene(Scene):
def setup(self):
self.teacher = Mortimer()
self.teacher.to_corner(DOWN + RIGHT)
self.teacher.look(DOWN+LEFT)
2016-09-07 22:03:56 -07:00
self.students = VGroup(*[
2016-07-18 11:50:26 -07:00
Randolph(color = c)
for c in BLUE_D, BLUE_C, BLUE_E
])
self.students.arrange_submobjects(RIGHT)
self.students.scale(0.8)
self.students.to_corner(DOWN+LEFT)
2016-09-07 22:03:56 -07:00
self.teacher.look_at(self.students[-1].eyes)
for student in self.students:
student.look_at(self.teacher.eyes)
2016-07-18 11:50:26 -07:00
for pi_creature in self.get_everyone():
pi_creature.bubble = None
self.add(*self.get_everyone())
def get_teacher(self):
return self.teacher
def get_students(self):
2016-08-09 14:07:23 -07:00
return self.students
2016-07-18 11:50:26 -07:00
def get_everyone(self):
2016-09-07 22:03:56 -07:00
return [self.get_teacher()] + list(self.get_students())
2016-07-18 11:50:26 -07:00
2016-07-21 15:16:49 -07:00
def get_bubble_intro_animation(self, content, bubble_type,
pi_creature,
**bubble_kwargs):
bubble = pi_creature.get_bubble(bubble_type, **bubble_kwargs)
bubble.add_content(content)
if pi_creature.bubble:
content_intro_anims = [
Transform(pi_creature.bubble, bubble),
Transform(pi_creature.bubble.content, bubble.content)
2016-07-21 15:16:49 -07:00
]
else:
content_intro_anims = [
FadeIn(bubble),
2016-07-21 15:16:49 -07:00
Write(content),
]
pi_creature.bubble = bubble
return content_intro_anims
2016-07-18 11:50:26 -07:00
def introduce_bubble(self, content, bubble_type, pi_creature,
pi_creature_target_mode = None,
2016-07-21 15:16:49 -07:00
added_anims = [],
**bubble_kwargs):
2016-08-13 18:27:02 -07:00
if all(map(lambda s : isinstance(s, str), content)):
content = TextMobject(*content)
elif len(content) == 1 and isinstance(content[0], TexMobject):
content = content[0]
else:
raise Exception("Invalid content type")
2016-07-21 15:16:49 -07:00
content_intro_anims = self.get_bubble_intro_animation(
content, bubble_type, pi_creature, **bubble_kwargs
2016-07-18 11:50:26 -07:00
)
2016-07-21 15:16:49 -07:00
2016-07-18 11:50:26 -07:00
if not pi_creature_target_mode:
if bubble_type is "speech":
pi_creature_target_mode = "speaking"
else:
pi_creature_target_mode = "pondering"
for p in self.get_everyone():
if p.bubble and p is not pi_creature:
added_anims += [
FadeOut(p.bubble),
FadeOut(p.bubble.content)
]
2016-07-18 11:50:26 -07:00
p.bubble = None
2016-07-21 15:16:49 -07:00
added_anims.append(ApplyMethod(p.change_mode, "plain"))
anims = added_anims + content_intro_anims + [
2016-07-18 11:50:26 -07:00
ApplyMethod(
pi_creature.change_mode,
pi_creature_target_mode,
),
]
2016-07-21 15:16:49 -07:00
self.play(*anims)
return pi_creature.bubble
2016-07-18 11:50:26 -07:00
2016-08-13 18:27:02 -07:00
def teacher_says(self, *content, **kwargs):
return self.introduce_bubble(
2016-07-18 11:50:26 -07:00
content, "speech", self.get_teacher(), **kwargs
)
2016-08-13 18:27:02 -07:00
def student_says(self, *content, **kwargs):
2016-09-10 15:21:03 -07:00
if "pi_creature_target_mode" not in kwargs:
target_mode = random.choice([
"raise_right_hand",
"raise_left_hand",
])
kwargs["pi_creature_target_mode"] = target_mode
2016-08-13 18:27:02 -07:00
student = self.get_students()[kwargs.get("student_index", 1)]
return self.introduce_bubble(content, "speech", student, **kwargs)
2016-07-18 11:50:26 -07:00
2016-08-13 18:27:02 -07:00
def teacher_thinks(self, *content, **kwargs):
return self.introduce_bubble(
2016-07-18 11:50:26 -07:00
content, "thought", self.get_teacher(), **kwargs
)
2016-08-13 18:27:02 -07:00
def student_thinks(self, *content, **kwargs):
student = self.get_students()[kwargs.get("student_index", 1)]
return self.introduce_bubble(content, "thought", student, **kwargs)
2016-07-18 11:50:26 -07:00
2016-07-21 15:16:49 -07:00
def random_blink(self, num_times = 1):
for x in range(num_times):
pi_creature = random.choice(self.get_everyone())
self.play(Blink(pi_creature))
self.dither()
2016-07-18 11:50:26 -07:00
2016-08-13 18:27:02 -07:00
def change_student_modes(self, *modes):
2016-09-07 22:03:56 -07:00
pairs = zip(self.get_students(), modes)
start = VGroup(*[s for s, m in pairs])
target = VGroup(*[s.copy().change_mode(m) for s, m in pairs])
self.play(Transform(
start, target,
submobject_mode = "lagged_start",
run_time = 2
))
2016-07-18 11:50:26 -07:00
2016-08-28 18:26:08 -07:00
def zoom_in_on_thought_bubble(self, radius = SPACE_HEIGHT+SPACE_WIDTH):
bubble = None
for pi in self.get_everyone():
if hasattr(pi, "bubble") and isinstance(pi.bubble, ThoughtBubble):
bubble = pi.bubble
break
if bubble is None:
raise Exception("No pi creatures have a thought bubble")
vect = -bubble.get_bubble_center()
def func(point):
centered = point+vect
return radius*centered/np.linalg.norm(centered)
self.play(*[
ApplyPointwiseFunction(func, mob)
for mob in self.get_mobjects()
])
2016-07-18 11:50:26 -07:00
2016-07-13 22:03:31 -07:00