Files
manim/topics/characters.py

752 lines
24 KiB
Python

import random
import numpy as np
import itertools as it
from constants import *
from mobject.mobject import Mobject, Group
from mobject.svg_mobject import SVGMobject
from mobject.vectorized_mobject import VMobject, VGroup
from mobject.tex_mobject import TextMobject, TexMobject
from topics.objects import Bubble, ThoughtBubble, SpeechBubble
from topics.geometry import ScreenRectangle
from animation.animation import Animation
from animation.transform import Transform, ApplyMethod, MoveToTarget
from animation.transform import ReplacementTransform, FadeOut, FadeIn
from animation.simple_animations import Write, ShowCreation
from animation.compositions import AnimationGroup
from scene.scene import Scene
from utils.config_ops import digest_config
from utils.rate_functions import there_and_back, squish_rate_func
PI_CREATURE_DIR = os.path.join(MEDIA_DIR, "designs", "PiCreature")
PI_CREATURE_SCALE_FACTOR = 0.5
LEFT_EYE_INDEX = 0
RIGHT_EYE_INDEX = 1
LEFT_PUPIL_INDEX = 2
RIGHT_PUPIL_INDEX = 3
BODY_INDEX = 4
MOUTH_INDEX = 5
class PiCreature(SVGMobject):
CONFIG = {
"color" : BLUE_E,
"file_name_prefix" : "PiCreatures",
"stroke_width" : 0,
"stroke_color" : BLACK,
"fill_opacity" : 1.0,
"propagate_style_to_family" : True,
"height" : 3,
"corner_scale_factor" : 0.75,
"flip_at_start" : False,
"is_looking_direction_purposeful" : False,
"start_corner" : None,
#Range of proportions along body where arms are
"right_arm_range" : [0.55, 0.7],
"left_arm_range" : [.34, .462],
}
def __init__(self, mode = "plain", **kwargs):
digest_config(self, kwargs)
self.parts_named = False
try:
svg_file = os.path.join(
PI_CREATURE_DIR,
"%s_%s.svg"%(self.file_name_prefix, mode)
)
SVGMobject.__init__(self, file_name = svg_file, **kwargs)
except:
warnings.warn("No %s design with mode %s"%(self.file_name_prefix, mode))
svg_file = os.path.join(
FILE_DIR,
"PiCreatures_plain.svg",
)
SVGMobject.__init__(self, file_name = svg_file, **kwargs)
if self.flip_at_start:
self.flip()
if self.start_corner is not None:
self.to_corner(self.start_corner)
def name_parts(self):
self.mouth = self.submobjects[MOUTH_INDEX]
self.body = self.submobjects[BODY_INDEX]
self.pupils = VGroup(*[
self.submobjects[LEFT_PUPIL_INDEX],
self.submobjects[RIGHT_PUPIL_INDEX]
])
self.eyes = VGroup(*[
self.submobjects[LEFT_EYE_INDEX],
self.submobjects[RIGHT_EYE_INDEX]
])
self.eye_parts = VGroup(self.eyes, self.pupils)
self.parts_named = True
def init_colors(self):
SVGMobject.init_colors(self)
if not self.parts_named:
self.name_parts()
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
def copy(self):
copy_mobject = SVGMobject.copy(self)
copy_mobject.name_parts()
return copy_mobject
def set_color(self, color):
self.body.set_fill(color)
return self
def change_mode(self, mode):
new_self = self.__class__(
mode = mode,
color = self.color
)
new_self.scale_to_fit_height(self.get_height())
if self.is_flipped() ^ new_self.is_flipped():
new_self.flip()
new_self.shift(self.eyes.get_center() - new_self.eyes.get_center())
if hasattr(self, "purposeful_looking_direction"):
new_self.look(self.purposeful_looking_direction)
Transform(self, new_self).update(1)
return self
def look(self, direction):
norm = np.linalg.norm(direction)
if norm == 0:
return
direction /= norm
self.purposeful_looking_direction = direction
for pupil, eye in zip(self.pupils.split(), self.eyes.split()):
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)
#TODO, how to handle looking up...
# top_diff = eye.get_top()[1]-pupil.get_top()[1]
# if top_diff < 0:
# pupil.shift(top_diff*UP)
return self
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())
return self
def change(self, new_mode, look_at_arg = None):
self.change_mode(new_mode)
if look_at_arg is not None:
self.look_at(look_at_arg)
return self
def get_looking_direction(self):
return np.sign(np.round(
self.pupils.get_center() - self.eyes.get_center(),
decimals = 2
))
def is_flipped(self):
return self.eyes.submobjects[0].get_center()[0] > \
self.eyes.submobjects[1].get_center()[0]
def blink(self):
eye_parts = self.eye_parts
eye_bottom_y = eye_parts.get_bottom()[1]
eye_parts.apply_function(
lambda p : [p[0], eye_bottom_y, p[2]]
)
return self
def to_corner(self, vect = None, **kwargs):
if vect is not None:
SVGMobject.to_corner(self, vect, **kwargs)
else:
self.scale(self.corner_scale_factor)
self.to_corner(DOWN+LEFT, **kwargs)
return self
def get_bubble(self, *content, **kwargs):
bubble_class = kwargs.get("bubble_class", ThoughtBubble)
bubble = bubble_class(**kwargs)
if len(content) > 0:
if isinstance(content[0], str):
content_mob = TextMobject(*content)
else:
content_mob = content[0]
bubble.add_content(content_mob)
if "height" not in kwargs and "width" not in kwargs:
bubble.resize_to_content()
bubble.pin_to(self)
self.bubble = bubble
return bubble
def make_eye_contact(self, pi_creature):
self.look_at(pi_creature.eyes)
pi_creature.look_at(self.eyes)
return self
def shrug(self):
self.change_mode("shruggie")
top_mouth_point, bottom_mouth_point = [
self.mouth.points[np.argmax(self.mouth.points[:,1])],
self.mouth.points[np.argmin(self.mouth.points[:,1])]
]
self.look(top_mouth_point - bottom_mouth_point)
return self
def get_arm_copies(self):
body = self.body
return VGroup(*[
body.copy().pointwise_become_partial(body, *alpha_range)
for alpha_range in self.right_arm_range, self.left_arm_range
])
def get_all_pi_creature_modes():
result = []
prefix = "%s_"%PiCreature.CONFIG["file_name_prefix"]
suffix = ".svg"
for file in os.listdir(PI_CREATURE_DIR):
if file.startswith(prefix) and file.endswith(suffix):
result.append(
file[len(prefix):-len(suffix)]
)
return result
class Randolph(PiCreature):
pass #Nothing more than an alternative name
class Mortimer(PiCreature):
CONFIG = {
"color" : GREY_BROWN,
"flip_at_start" : True,
}
class Mathematician(PiCreature):
CONFIG = {
"color" : GREY,
}
class BabyPiCreature(PiCreature):
CONFIG = {
"scale_factor" : 0.5,
"eye_scale_factor" : 1.2,
"pupil_scale_factor" : 1.3
}
def __init__(self, *args, **kwargs):
PiCreature.__init__(self, *args, **kwargs)
self.scale(self.scale_factor)
self.shift(LEFT)
self.to_edge(DOWN, buff = LARGE_BUFF)
eyes = VGroup(self.eyes, self.pupils)
eyes_bottom = eyes.get_bottom()
eyes.scale(self.eye_scale_factor)
eyes.move_to(eyes_bottom, aligned_edge = DOWN)
looking_direction = self.get_looking_direction()
for pupil in self.pupils:
pupil.scale_in_place(self.pupil_scale_factor)
self.look(looking_direction)
class TauCreature(PiCreature):
CONFIG = {
"file_name_prefix" : "TauCreatures"
}
class ThreeLeggedPiCreature(PiCreature):
CONFIG = {
"file_name_prefix" : "ThreeLeggedPiCreatures"
}
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)
class Eyes(VMobject):
CONFIG = {
"height" : 0.3,
"thing_looked_at" : None,
"mode" : "plain",
}
def __init__(self, mobject, **kwargs):
VMobject.__init__(self, **kwargs)
self.mobject = mobject
self.submobjects = self.get_eyes().submobjects
def get_eyes(self, mode = None, thing_to_look_at = None):
mode = mode or self.mode
if thing_to_look_at is None:
thing_to_look_at = self.thing_looked_at
pi = Randolph(mode = mode)
eyes = VGroup(pi.eyes, pi.pupils)
pi.scale(self.height/eyes.get_height())
if self.submobjects:
eyes.move_to(self, DOWN)
else:
eyes.move_to(self.mobject.get_top(), DOWN)
if thing_to_look_at is not None:
pi.look_at(thing_to_look_at)
return eyes
def change_mode_anim(self, mode, **kwargs):
self.mode = mode
return Transform(self, self.get_eyes(mode = mode), **kwargs)
def look_at_anim(self, point_or_mobject, **kwargs):
self.thing_looked_at = point_or_mobject
return Transform(
self, self.get_eyes(thing_to_look_at = point_or_mobject),
**kwargs
)
def blink_anim(self, **kwargs):
target = self.copy()
bottom_y = self.get_bottom()[1]
for submob in target:
submob.apply_function(
lambda p : [p[0], bottom_y, p[2]]
)
if "rate_func" not in kwargs:
kwargs["rate_func"] = squish_rate_func(there_and_back)
return Transform(self, target, **kwargs)
#######################
class PiCreatureBubbleIntroduction(AnimationGroup):
CONFIG = {
"target_mode" : "speaking",
"bubble_class" : SpeechBubble,
"change_mode_kwargs" : {},
"bubble_creation_class" : ShowCreation,
"bubble_creation_kwargs" : {},
"bubble_kwargs" : {},
"content_introduction_class" : Write,
"content_introduction_kwargs" : {},
"look_at_arg" : None,
}
def __init__(self, pi_creature, *content, **kwargs):
digest_config(self, kwargs)
bubble = pi_creature.get_bubble(
*content,
bubble_class = self.bubble_class,
**self.bubble_kwargs
)
Group(bubble, bubble.content).shift_onto_screen()
pi_creature.generate_target()
pi_creature.target.change_mode(self.target_mode)
if self.look_at_arg is not None:
pi_creature.target.look_at(self.look_at_arg)
change_mode = MoveToTarget(pi_creature, **self.change_mode_kwargs)
bubble_creation = self.bubble_creation_class(
bubble, **self.bubble_creation_kwargs
)
content_introduction = self.content_introduction_class(
bubble.content, **self.content_introduction_kwargs
)
AnimationGroup.__init__(
self, change_mode, bubble_creation, content_introduction,
**kwargs
)
class PiCreatureSays(PiCreatureBubbleIntroduction):
CONFIG = {
"target_mode" : "speaking",
"bubble_class" : SpeechBubble,
}
class RemovePiCreatureBubble(AnimationGroup):
CONFIG = {
"target_mode" : "plain",
"look_at_arg" : None,
"remover" : True,
}
def __init__(self, pi_creature, **kwargs):
assert hasattr(pi_creature, "bubble")
digest_config(self, kwargs, locals())
pi_creature.generate_target()
pi_creature.target.change_mode(self.target_mode)
if self.look_at_arg is not None:
pi_creature.target.look_at(self.look_at_arg)
AnimationGroup.__init__(
self,
MoveToTarget(pi_creature),
FadeOut(pi_creature.bubble),
FadeOut(pi_creature.bubble.content),
)
def clean_up(self, surrounding_scene = None):
AnimationGroup.clean_up(self, surrounding_scene)
self.pi_creature.bubble = None
if surrounding_scene is not None:
surrounding_scene.add(self.pi_creature)
###########
class PiCreatureScene(Scene):
CONFIG = {
"total_wait_time" : 0,
"seconds_to_blink" : 3,
"pi_creatures_start_on_screen" : True,
"default_pi_creature_kwargs" : {
"color" : GREY_BROWN,
"flip_at_start" : True,
},
"default_pi_creature_start_corner" : DOWN+LEFT,
}
def setup(self):
self.pi_creatures = self.create_pi_creatures()
self.pi_creature = self.get_primary_pi_creature()
if self.pi_creatures_start_on_screen:
self.add(*self.pi_creatures)
def create_pi_creatures(self):
"""
Likely updated for subclasses
"""
return VGroup(self.create_pi_creature())
def create_pi_creature(self):
pi_creature = PiCreature(**self.default_pi_creature_kwargs)
pi_creature.to_corner(self.default_pi_creature_start_corner)
return pi_creature
def get_pi_creatures(self):
return self.pi_creatures
def get_primary_pi_creature(self):
return self.pi_creatures[0]
def any_pi_creatures_on_screen(self):
mobjects = self.get_mobjects()
return any([pi in mobjects for pi in self.get_pi_creatures()])
def get_on_screen_pi_creatures(self):
mobjects = self.get_mobjects()
return VGroup(*filter(
lambda pi : pi in mobjects,
self.get_pi_creatures()
))
def introduce_bubble(self, *args, **kwargs):
if isinstance(args[0], PiCreature):
pi_creature = args[0]
content = args[1:]
else:
pi_creature = self.get_primary_pi_creature()
content = args
bubble_class = kwargs.pop("bubble_class", SpeechBubble)
target_mode = kwargs.pop(
"target_mode",
"thinking" if bubble_class is ThoughtBubble else "speaking"
)
bubble_kwargs = kwargs.pop("bubble_kwargs", {})
bubble_removal_kwargs = kwargs.pop("bubble_removal_kwargs", {})
added_anims = kwargs.pop("added_anims", [])
anims = []
on_screen_mobjects = self.camera.extract_mobject_family_members(
self.get_mobjects()
)
def has_bubble(pi):
return hasattr(pi, "bubble") and \
pi.bubble is not None and \
pi.bubble in on_screen_mobjects
pi_creatures_with_bubbles = filter(has_bubble, self.get_pi_creatures())
if pi_creature in pi_creatures_with_bubbles:
pi_creatures_with_bubbles.remove(pi_creature)
old_bubble = pi_creature.bubble
bubble = pi_creature.get_bubble(
*content,
bubble_class = bubble_class,
**bubble_kwargs
)
anims += [
ReplacementTransform(old_bubble, bubble),
ReplacementTransform(old_bubble.content, bubble.content),
pi_creature.change_mode, target_mode
]
else:
anims.append(PiCreatureBubbleIntroduction(
pi_creature,
*content,
bubble_class = bubble_class,
bubble_kwargs = bubble_kwargs,
target_mode = target_mode,
**kwargs
))
anims += [
RemovePiCreatureBubble(pi, **bubble_removal_kwargs)
for pi in pi_creatures_with_bubbles
]
anims += added_anims
self.play(*anims, **kwargs)
def pi_creature_says(self, *args, **kwargs):
self.introduce_bubble(
*args,
bubble_class = SpeechBubble,
**kwargs
)
def pi_creature_thinks(self, *args, **kwargs):
self.introduce_bubble(
*args,
bubble_class = ThoughtBubble,
**kwargs
)
def say(self, *content, **kwargs):
self.pi_creature_says(self.get_primary_pi_creature(), *content, **kwargs)
def think(self, *content, **kwargs):
self.pi_creature_thinks(self.get_primary_pi_creature(), *content, **kwargs)
def compile_play_args_to_animation_list(self, *args):
"""
Add animations so that all pi creatures look at the
first mobject being animated with each .play call
"""
animations = Scene.compile_play_args_to_animation_list(self, *args)
if not self.any_pi_creatures_on_screen():
return animations
non_pi_creature_anims = filter(
lambda anim : anim.mobject not in self.get_pi_creatures(),
animations
)
if len(non_pi_creature_anims) == 0:
return animations
first_anim = non_pi_creature_anims[0]
#Look at ending state
first_anim.update(1)
point_of_interest = first_anim.mobject.get_center()
first_anim.update(0)
for pi_creature in self.get_pi_creatures():
if pi_creature not in self.get_mobjects():
continue
if pi_creature in first_anim.mobject.submobject_family():
continue
anims_with_pi_creature = filter(
lambda anim : pi_creature in anim.mobject.submobject_family(),
animations
)
for anim in anims_with_pi_creature:
if isinstance(anim, Transform):
index = anim.mobject.submobject_family().index(pi_creature)
target_family = anim.target_mobject.submobject_family()
target = target_family[index]
if isinstance(target, PiCreature):
target.look_at(point_of_interest)
if not anims_with_pi_creature:
animations.append(
ApplyMethod(pi_creature.look_at, point_of_interest)
)
return animations
def blink(self):
self.play(Blink(random.choice(self.get_on_screen_pi_creatures())))
def joint_blink(self, pi_creatures = None, shuffle = True, **kwargs):
if pi_creatures is None:
pi_creatures = self.get_on_screen_pi_creatures()
creatures_list = list(pi_creatures)
if shuffle:
random.shuffle(creatures_list)
def get_rate_func(pi):
index = creatures_list.index(pi)
proportion = float(index)/len(creatures_list)
start_time = 0.8*proportion
return squish_rate_func(
there_and_back,
start_time, start_time + 0.2
)
self.play(*[
Blink(pi, rate_func = get_rate_func(pi), **kwargs)
for pi in creatures_list
])
return self
def wait(self, time = 1, blink = True):
while time >= 1:
time_to_blink = self.total_wait_time%self.seconds_to_blink == 0
if blink and self.any_pi_creatures_on_screen() and time_to_blink:
self.blink()
self.num_plays -= 1 #This shouldn't count as an animation
else:
self.non_blink_wait()
time -= 1
self.total_wait_time += 1
if time > 0:
self.non_blink_wait(time)
return self
def non_blink_wait(self, time = 1):
Scene.wait(self, time)
return self
def change_mode(self, mode):
self.play(self.get_primary_pi_creature().change_mode, mode)
def look_at(self, thing_to_look_at, pi_creatures = None):
if pi_creatures is None:
pi_creatures = self.get_pi_creatures()
self.play(*it.chain(*[
[pi.look_at, thing_to_look_at]
for pi in pi_creatures
]))
class TeacherStudentsScene(PiCreatureScene):
CONFIG = {
"student_colors" : [BLUE_D, BLUE_E, BLUE_C],
"student_scale_factor" : 0.8,
"seconds_to_blink" : 2,
"screen_height" : 3,
}
def setup(self):
PiCreatureScene.setup(self)
self.screen = ScreenRectangle(height = self.screen_height)
self.screen.to_corner(UP+LEFT)
self.hold_up_spot = self.teacher.get_corner(UP+LEFT) + MED_LARGE_BUFF*UP
def create_pi_creatures(self):
self.teacher = Mortimer()
self.teacher.to_corner(DOWN + RIGHT)
self.teacher.look(DOWN+LEFT)
self.students = VGroup(*[
Randolph(color = c)
for c in self.student_colors
])
self.students.arrange_submobjects(RIGHT)
self.students.scale(self.student_scale_factor)
self.students.to_corner(DOWN+LEFT)
self.teacher.look_at(self.students[-1].eyes)
for student in self.students:
student.look_at(self.teacher.eyes)
return [self.teacher] + list(self.students)
def get_teacher(self):
return self.teacher
def get_students(self):
return self.students
def teacher_says(self, *content, **kwargs):
return self.pi_creature_says(
self.get_teacher(), *content, **kwargs
)
def student_says(self, *content, **kwargs):
if "target_mode" not in kwargs:
target_mode = random.choice([
"raise_right_hand",
"raise_left_hand",
])
kwargs["target_mode"] = target_mode
student = self.get_students()[kwargs.get("student_index", 1)]
return self.pi_creature_says(
student, *content, **kwargs
)
def teacher_thinks(self, *content, **kwargs):
return self.pi_creature_thinks(
self.get_teacher(), *content, **kwargs
)
def student_thinks(self, *content, **kwargs):
student = self.get_students()[kwargs.get("student_index", 1)]
return self.pi_creature_thinks(student, *content, **kwargs)
def change_all_student_modes(self, mode, **kwargs):
self.change_student_modes(*[mode]*len(self.students), **kwargs)
def change_student_modes(self, *modes, **kwargs):
added_anims = kwargs.pop("added_anims", [])
self.play(
self.get_student_changes(*modes, **kwargs),
*added_anims
)
def get_student_changes(self, *modes, **kwargs):
pairs = zip(self.get_students(), modes)
pairs = [(s, m) for s, m in pairs if m is not None]
start = VGroup(*[s for s, m in pairs])
target = VGroup(*[s.copy().change_mode(m) for s, m in pairs])
if "look_at_arg" in kwargs:
for pi in target:
pi.look_at(kwargs["look_at_arg"])
submobject_mode = kwargs.get("submobject_mode", "lagged_start")
return Transform(
start, target,
submobject_mode = submobject_mode,
run_time = 2
)
def zoom_in_on_thought_bubble(self, bubble = None, radius = FRAME_Y_RADIUS+FRAME_X_RADIUS):
if bubble is None:
for pi in self.get_pi_creatures():
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()
])
def teacher_holds_up(self, mobject, target_mode = "raise_right_hand", **kwargs):
mobject.move_to(self.hold_up_spot, DOWN)
mobject.shift_onto_screen()
mobject_copy = mobject.copy()
mobject_copy.shift(DOWN)
mobject_copy.fade(1)
self.play(
ReplacementTransform(mobject_copy, mobject),
self.teacher.change, target_mode,
)