3b1b-manim/manimlib/scene/scene.py

511 lines
17 KiB
Python
Raw Normal View History

import inspect
import random
import warnings
2019-07-26 16:45:52 +08:00
import platform
from tqdm import tqdm as ProgressDisplay
import numpy as np
from manimlib.animation.animation import Animation
from manimlib.animation.creation import Write
from manimlib.animation.transform import MoveToTarget, ApplyMethod
from manimlib.camera.camera import Camera
from manimlib.constants import *
from manimlib.container.container import Container
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.svg.tex_mobject import TextMobject
2019-01-24 21:47:40 -08:00
from manimlib.scene.scene_file_writer import SceneFileWriter
from manimlib.utils.family_ops import extract_mobject_family_members
from manimlib.utils.family_ops import restructure_list_to_exclude_certain_family_members
class Scene(Container):
2016-02-27 16:32:53 -08:00
CONFIG = {
"camera_class": Camera,
"camera_config": {},
2019-01-24 21:47:40 -08:00
"file_writer_config": {},
"skip_animations": False,
"always_update_mobjects": False,
"random_seed": 0,
"start_at_animation_number": None,
"end_at_animation_number": None,
"leave_progress_bars": False,
}
def __init__(self, **kwargs):
Container.__init__(self, **kwargs)
2017-02-02 15:36:24 -08:00
self.camera = self.camera_class(**self.camera_config)
2019-01-24 21:47:40 -08:00
self.file_writer = SceneFileWriter(
self, **self.file_writer_config,
)
2015-10-29 13:45:28 -07:00
self.mobjects = []
self.num_plays = 0
self.time = 0
self.original_skipping_status = self.skip_animations
if self.random_seed is not None:
random.seed(self.random_seed)
np.random.seed(self.random_seed)
2015-10-29 13:45:28 -07:00
2016-08-10 10:26:07 -07:00
self.setup()
try:
2019-01-24 21:47:40 -08:00
self.construct()
except EndSceneEarlyException:
2019-01-24 21:47:40 -08:00
pass
self.tear_down()
2019-01-24 21:47:40 -08:00
self.file_writer.finish()
self.print_end_message()
2016-08-10 10:26:07 -07:00
def setup(self):
2017-04-21 17:40:49 -07:00
"""
This is meant to be implement by any scenes which
are comonly subclassed, and have some common setup
2017-04-21 17:40:49 -07:00
involved before the construct method is called.
"""
pass
2016-08-10 10:26:07 -07:00
def tear_down(self):
pass
2015-06-10 22:00:35 -07:00
def construct(self):
pass # To be implemented in subclasses
def __str__(self):
return self.__class__.__name__
2015-04-03 16:41:25 -07:00
def print_end_message(self):
print("Played {} animations".format(self.num_plays))
2017-10-20 16:29:30 -07:00
def set_variables_as_attrs(self, *objects, **newly_named_objects):
2017-05-10 17:22:26 -07:00
"""
2017-10-20 16:29:30 -07:00
This method is slightly hacky, making it a little easier
for certain methods (typically subroutines of construct)
to share local variables.
2017-05-10 17:22:26 -07:00
"""
caller_locals = inspect.currentframe().f_back.f_locals
2018-08-09 17:56:05 -07:00
for key, value in list(caller_locals.items()):
2017-10-24 13:41:28 -07:00
for o in objects:
if value is o:
setattr(self, key, value)
2018-08-09 17:56:05 -07:00
for key, value in list(newly_named_objects.items()):
2017-10-20 16:29:30 -07:00
setattr(self, key, value)
2017-05-10 17:22:26 -07:00
return self
2017-10-20 16:29:30 -07:00
def get_attrs(self, *keys):
return [getattr(self, key) for key in keys]
# Only these methods should touch the camera
2016-02-27 16:29:11 -08:00
def set_camera(self, camera):
self.camera = camera
2015-10-29 13:45:28 -07:00
def get_frame(self):
2017-09-26 17:41:45 -07:00
return np.array(self.camera.get_pixel_array())
def get_image(self):
return self.camera.get_image()
2015-10-29 13:45:28 -07:00
2017-09-26 17:41:45 -07:00
def set_camera_pixel_array(self, pixel_array):
self.camera.set_pixel_array(pixel_array)
2016-11-11 11:18:41 -08:00
def set_camera_background(self, background):
2016-11-23 17:50:25 -08:00
self.camera.set_background(background)
2016-11-11 11:18:41 -08:00
def reset_camera(self):
self.camera.reset()
def capture_mobjects_in_camera(self, mobjects, **kwargs):
self.camera.capture_mobjects(mobjects, **kwargs)
def update_frame(
self,
mobjects=None,
background=None,
include_submobjects=True,
2019-01-24 21:47:40 -08:00
ignore_skipping=True,
**kwargs):
2019-01-24 21:47:40 -08:00
if self.skip_animations and not ignore_skipping:
return
if mobjects is None:
2020-01-15 18:01:28 -08:00
mobjects = self.mobjects
2016-03-07 19:07:00 -08:00
if background is not None:
2017-09-26 17:41:45 -07:00
self.set_camera_pixel_array(background)
2016-03-07 19:07:00 -08:00
else:
2016-11-11 11:18:41 -08:00
self.reset_camera()
2017-08-24 11:43:38 -07:00
kwargs["include_submobjects"] = include_submobjects
2016-11-11 11:18:41 -08:00
self.capture_mobjects_in_camera(mobjects, **kwargs)
2016-03-17 23:53:59 -07:00
def freeze_background(self):
self.update_frame()
self.set_camera(Camera(self.get_frame()))
self.clear()
2016-03-07 19:07:00 -08:00
###
def update_mobjects(self, dt):
for mobject in self.mobjects:
mobject.update(dt)
2017-08-24 11:43:38 -07:00
def should_update_mobjects(self):
return self.always_update_mobjects or any([
mob.has_time_based_updater()
for mob in self.get_mobject_family_members()
])
###
2017-08-24 11:43:38 -07:00
def get_time(self):
return self.time
def increment_time(self, d_time):
self.time += d_time
###
2017-02-09 21:09:51 -08:00
def get_top_level_mobjects(self):
# Return only those which are not in the family
# of another mobject from the scene
mobjects = self.get_mobjects()
2018-08-21 19:15:16 -07:00
families = [m.get_family() for m in mobjects]
2017-02-09 21:09:51 -08:00
def is_top_level(mobject):
num_families = sum([
(mobject in family)
2017-02-09 21:09:51 -08:00
for family in families
])
return num_families == 1
2018-08-09 17:56:05 -07:00
return list(filter(is_top_level, mobjects))
def get_mobject_family_members(self):
return extract_mobject_family_members(self.mobjects)
def add(self, *new_mobjects):
2015-03-26 22:49:22 -06:00
"""
Mobjects will be displayed, from background to
foreground in the order with which they are added.
2015-03-26 22:49:22 -06:00
"""
self.remove(*new_mobjects)
self.mobjects += new_mobjects
2015-06-09 11:26:12 -07:00
return self
def add_mobjects_among(self, values):
"""
This is meant mostly for quick prototyping,
e.g. to add all mobjects defined up to a point,
call self.add_mobjects_among(locals().values())
"""
self.add(*filter(
lambda m: isinstance(m, Mobject),
values
))
return self
def remove(self, *mobjects_to_remove):
self.mobjects = restructure_list_to_exclude_certain_family_members(
self.mobjects, mobjects_to_remove
)
return self
2016-08-02 12:26:15 -07:00
def bring_to_front(self, *mobjects):
self.add(*mobjects)
2015-10-12 19:39:46 -07:00
return self
2016-08-02 12:26:15 -07:00
def bring_to_back(self, *mobjects):
self.remove(*mobjects)
self.mobjects = list(mobjects) + self.mobjects
2015-10-12 19:39:46 -07:00
return self
2015-06-10 22:00:35 -07:00
def clear(self):
2015-10-29 13:45:28 -07:00
self.mobjects = []
2015-06-10 22:00:35 -07:00
return self
2016-07-18 11:50:26 -07:00
def get_mobjects(self):
return list(self.mobjects)
def get_mobject_copies(self):
return [m.copy() for m in self.mobjects]
def get_moving_mobjects(self, *animations):
# Go through mobjects from start to end, and
# as soon as there's one that needs updating of
# some kind per frame, return the list from that
# point forward.
animation_mobjects = [anim.mobject for anim in animations]
2018-08-23 14:46:22 -07:00
mobjects = self.get_mobject_family_members()
for i, mob in enumerate(mobjects):
2020-01-15 18:01:28 -08:00
animated = (mob in animation_mobjects)
updated = (len(mob.get_family_updaters()) > 0)
if animated or updated:
return mobjects[i:]
return []
def get_time_progression(self, run_time, n_iterations=None, override_skip_animations=False):
if self.skip_animations and not override_skip_animations:
times = [run_time]
else:
step = 1 / self.camera.frame_rate
times = np.arange(0, run_time, step)
2019-01-14 13:26:58 -08:00
time_progression = ProgressDisplay(
times, total=n_iterations,
leave=self.leave_progress_bars,
2019-07-26 16:45:52 +08:00
ascii=False if platform.system() != 'Windows' else True
2019-01-14 13:26:58 -08:00
)
2017-08-24 11:43:38 -07:00
return time_progression
def get_run_time(self, animations):
return np.max([animation.run_time for animation in animations])
2017-08-24 11:43:38 -07:00
def get_animation_time_progression(self, animations):
run_time = self.get_run_time(animations)
2017-08-24 11:43:38 -07:00
time_progression = self.get_time_progression(run_time)
2016-02-27 18:50:33 -08:00
time_progression.set_description("".join([
"Animation {}: ".format(self.num_plays),
2016-02-27 18:50:33 -08:00
str(animations[0]),
(", etc." if len(animations) > 1 else ""),
]))
return time_progression
2019-02-08 12:51:21 -08:00
def compile_play_args_to_animation_list(self, *args, **kwargs):
"""
Each arg can either be an animation, or a mobject method
followed by that methods arguments (and potentially follow
by a dict of kwargs for that method).
This animation list is built by going through the args list,
and each animation is simply added, but when a mobject method
s hit, a MoveToTarget animation is built using the args that
follow up until either another animation is hit, another method
is hit, or the args list runs out.
"""
animations = []
state = {
"curr_method": None,
"last_method": None,
"method_args": [],
}
def compile_method(state):
if state["curr_method"] is None:
return
mobject = state["curr_method"].__self__
if state["last_method"] and state["last_method"].__self__ is mobject:
animations.pop()
# method should already have target then.
else:
mobject.generate_target()
#
if len(state["method_args"]) > 0 and isinstance(state["method_args"][-1], dict):
method_kwargs = state["method_args"].pop()
else:
method_kwargs = {}
state["curr_method"].__func__(
mobject.target,
*state["method_args"],
**method_kwargs
)
animations.append(MoveToTarget(mobject))
state["last_method"] = state["curr_method"]
state["curr_method"] = None
state["method_args"] = []
for arg in args:
if isinstance(arg, Animation):
compile_method(state)
animations.append(arg)
elif inspect.ismethod(arg):
compile_method(state)
state["curr_method"] = arg
elif state["curr_method"] is not None:
state["method_args"].append(arg)
2016-09-07 22:04:24 -07:00
elif isinstance(arg, Mobject):
raise Exception("""
I think you may have invoked a method
2016-09-07 22:04:24 -07:00
you meant to pass in as a Scene.play argument
""")
else:
raise Exception("Invalid play arguments")
compile_method(state)
2019-02-08 12:51:21 -08:00
for animation in animations:
# This is where kwargs to play like run_time and rate_func
# get applied to all animations
animation.update_config(**kwargs)
return animations
2016-02-27 18:50:33 -08:00
2019-01-24 22:24:01 -08:00
def update_skipping_status(self):
2018-02-10 22:45:46 -08:00
if self.start_at_animation_number:
if self.num_plays == self.start_at_animation_number:
self.skip_animations = False
2018-02-10 22:45:46 -08:00
if self.end_at_animation_number:
if self.num_plays >= self.end_at_animation_number:
self.skip_animations = True
raise EndSceneEarlyException()
2019-01-24 22:24:01 -08:00
def handle_play_like_call(func):
def wrapper(self, *args, **kwargs):
self.update_skipping_status()
allow_write = not self.skip_animations
self.file_writer.begin_animation(allow_write)
func(self, *args, **kwargs)
self.file_writer.end_animation(allow_write)
self.num_plays += 1
return wrapper
2019-02-08 12:51:21 -08:00
def begin_animations(self, animations):
2019-02-08 11:00:04 -08:00
curr_mobjects = self.get_mobject_family_members()
for animation in animations:
# Begin animation
animation.begin()
# Anything animated that's not already in the
# scene gets added to the scene
2019-02-08 11:00:04 -08:00
mob = animation.mobject
if mob not in curr_mobjects:
self.add(mob)
curr_mobjects += mob.get_family()
2019-02-08 12:51:21 -08:00
def progress_through_animations(self, animations):
# Paint all non-moving objects onto the screen, so they don't
# have to be rendered every frame
2019-02-08 12:51:21 -08:00
moving_mobjects = self.get_moving_mobjects(*animations)
self.update_frame(excluded_mobjects=moving_mobjects)
2016-03-07 19:07:00 -08:00
static_image = self.get_frame()
2019-02-08 12:51:21 -08:00
last_t = 0
2017-08-24 11:43:38 -07:00
for t in self.get_animation_time_progression(animations):
2019-02-08 12:51:21 -08:00
dt = t - last_t
last_t = t
2015-04-03 16:41:25 -07:00
for animation in animations:
animation.update_mobjects(dt)
2019-02-08 12:51:21 -08:00
alpha = t / animation.run_time
animation.interpolate(alpha)
self.update_mobjects(dt)
2016-02-27 18:50:33 -08:00
self.update_frame(moving_mobjects, static_image)
self.add_frames(self.get_frame())
2019-02-08 11:00:04 -08:00
2019-02-08 12:51:21 -08:00
def finish_animations(self, animations):
2019-02-08 11:00:04 -08:00
for animation in animations:
animation.finish()
animation.clean_up_from_scene(self)
2019-02-08 12:51:21 -08:00
self.mobjects_from_last_animation = [
anim.mobject for anim in animations
]
if self.skip_animations:
# TODO, run this call in for each animation?
self.update_mobjects(self.get_run_time(animations))
else:
self.update_mobjects(0)
2019-02-08 12:51:21 -08:00
@handle_play_like_call
def play(self, *args, **kwargs):
if len(args) == 0:
warnings.warn("Called Scene.play with no animations")
return
animations = self.compile_play_args_to_animation_list(
*args, **kwargs
)
2019-02-08 15:26:30 -08:00
self.begin_animations(animations)
2019-02-08 12:51:21 -08:00
self.progress_through_animations(animations)
self.finish_animations(animations)
2016-07-19 11:07:26 -07:00
def idle_stream(self):
2019-01-24 21:47:40 -08:00
self.file_writer.idle_stream()
2016-07-19 11:07:26 -07:00
def clean_up_animations(self, *animations):
2015-04-03 16:41:25 -07:00
for animation in animations:
2019-02-08 11:00:04 -08:00
animation.clean_up_from_scene(self)
2015-06-09 11:26:12 -07:00
return self
2016-07-19 11:07:26 -07:00
def get_mobjects_from_last_animation(self):
if hasattr(self, "mobjects_from_last_animation"):
return self.mobjects_from_last_animation
return []
def get_wait_time_progression(self, duration, stop_condition):
if stop_condition is not None:
time_progression = self.get_time_progression(
duration,
n_iterations=-1, # So it doesn't show % progress
override_skip_animations=True
)
time_progression.set_description(
"Waiting for {}".format(stop_condition.__name__)
)
else:
time_progression = self.get_time_progression(duration)
time_progression.set_description(
"Waiting {}".format(self.num_plays)
)
return time_progression
@handle_play_like_call
2019-01-14 13:26:58 -08:00
def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None):
self.update_mobjects(dt=0) # Any problems with this?
if self.should_update_mobjects():
time_progression = self.get_wait_time_progression(duration, stop_condition)
# TODO, be smart about setting a static image
# the same way Scene.play does
last_t = 0
2019-01-14 13:26:58 -08:00
for t in time_progression:
dt = t - last_t
last_t = t
self.update_mobjects(dt)
self.update_frame()
self.add_frames(self.get_frame())
2019-03-19 17:31:03 -07:00
if stop_condition is not None and stop_condition():
time_progression.close()
2019-01-14 13:26:58 -08:00
break
elif self.skip_animations:
# Do nothing
return self
else:
2017-08-24 11:43:38 -07:00
self.update_frame()
dt = 1 / self.camera.frame_rate
n_frames = int(duration / dt)
frame = self.get_frame()
self.add_frames(*[frame] * n_frames)
2015-06-09 11:26:12 -07:00
return self
2019-01-14 13:26:58 -08:00
def wait_until(self, stop_condition, max_time=60):
self.wait(max_time, stop_condition=stop_condition)
2018-01-29 21:29:36 -08:00
2017-03-20 14:37:51 -07:00
def force_skipping(self):
self.original_skipping_status = self.skip_animations
self.skip_animations = True
return self
def revert_to_original_skipping_status(self):
if hasattr(self, "original_skipping_status"):
self.skip_animations = self.original_skipping_status
return self
def add_frames(self, *frames):
dt = 1 / self.camera.frame_rate
self.increment_time(len(frames) * dt)
if self.skip_animations:
return
2019-01-24 21:47:40 -08:00
for frame in frames:
self.file_writer.write_frame(frame)
2016-01-30 14:44:45 -08:00
def add_sound(self, sound_file, time_offset=0, gain=None, **kwargs):
if self.skip_animations:
return
2019-01-24 22:24:01 -08:00
time = self.get_time() + time_offset
self.file_writer.add_sound(sound_file, time, gain, **kwargs)
2019-01-24 22:24:01 -08:00
2015-06-09 11:26:12 -07:00
def show_frame(self):
2019-01-24 21:47:40 -08:00
self.update_frame(ignore_skipping=True)
2017-09-26 17:41:45 -07:00
self.get_image().show()
2015-04-03 16:41:25 -07:00
2019-01-12 14:43:44 -08:00
# TODO, this doesn't belong in Scene, but should be
# part of some more specialized subclass optimized
2019-01-12 14:43:44 -08:00
# for livestreaming
def tex(self, latex):
eq = TextMobject(latex)
anims = []
anims.append(Write(eq))
for mobject in self.mobjects:
anims.append(ApplyMethod(mobject.shift, 2 * UP))
self.play(*anims)
class EndSceneEarlyException(Exception):
pass