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

563 lines
18 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
2020-02-11 19:51:19 -08:00
import time
from IPython.terminal.embed import InteractiveShellEmbed
from manimlib.animation.animation import Animation
from manimlib.animation.transform import MoveToTarget
from manimlib.mobject.mobject import Point
from manimlib.camera.camera import Camera
from manimlib.constants import *
from manimlib.container.container import Container
from manimlib.mobject.mobject import Mobject
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
2020-02-11 19:51:19 -08:00
from manimlib.window import Window
class Scene(Container):
2016-02-27 16:32:53 -08:00
CONFIG = {
2020-02-11 19:51:19 -08:00
"window_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,
2020-02-11 19:51:19 -08:00
"preview": True,
}
def __init__(self, **kwargs):
Container.__init__(self, **kwargs)
2020-02-11 19:51:19 -08:00
if self.preview:
self.window = Window(self, **self.window_config)
2020-02-13 10:42:07 -08:00
self.camera_config["ctx"] = self.window.ctx
self.virtual_animation_start_time = 0
self.real_animation_start_time = time.time()
2020-02-13 10:42:07 -08:00
else:
self.window = None
2020-02-11 19:51:19 -08:00
2017-02-02 15:36:24 -08:00
self.camera = self.camera_class(**self.camera_config)
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.skip_time = 0
self.original_skipping_status = self.skip_animations
2020-02-11 19:51:19 -08:00
self.time_of_last_frame = time.time()
2020-02-14 16:26:49 -08:00
# Items associated with interaction
self.mouse_point = Point()
self.mouse_drag_point = Point()
2020-02-14 16:26:49 -08:00
self.zoom_on_scroll = False
self.quit_interaction = False
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
2020-02-11 19:51:19 -08:00
def run(self):
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()
2020-02-11 19:51:19 -08:00
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
2015-06-10 22:00:35 -07:00
def construct(self):
2020-02-11 19:51:19 -08:00
# To be implemented in subclasses
pass
def tear_down(self):
self.stop_skipping()
2020-02-11 19:51:19 -08:00
self.file_writer.finish()
self.print_end_message()
2020-02-13 10:42:07 -08:00
if self.window:
self.interact()
def interact(self):
# If there is a window, enter a loop
# which updates the frame while under
# the hood calling the pyglet event loop
2020-02-14 16:26:49 -08:00
self.quit_interaction = False
while not self.window.is_closing and not self.quit_interaction:
2020-02-11 19:51:19 -08:00
self.update_frame()
2020-02-14 16:26:49 -08:00
if self.window.is_closing:
self.window.destroy()
def embed(self):
self.stop_skipping()
self.update_frame()
shell = InteractiveShellEmbed()
2020-02-14 16:26:49 -08:00
# Have the frame update after each command
shell.events.register('post_run_cell', lambda *a, **kw: self.update_frame())
# Stack depth of 2 means the shell will use
# the namespace of the caller, not this method
shell(stack_depth=2)
def __str__(self):
return self.__class__.__name__
2015-04-03 16:41:25 -07:00
def print_end_message(self):
2020-02-13 10:42:07 -08:00
print(f"Played {self.num_plays} animations")
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
2017-09-26 17:41:45 -07:00
def get_image(self):
return self.camera.get_image()
2015-10-29 13:45:28 -07:00
def update_frame(self, dt=0, ignore_skipping=False):
self.increment_time(dt)
self.update_mobjects(dt)
2019-01-24 21:47:40 -08:00
if self.skip_animations and not ignore_skipping:
return
2020-02-11 19:51:19 -08:00
2020-02-13 10:42:07 -08:00
if self.window:
self.window.clear()
self.camera.clear()
self.camera.capture(*self.mobjects)
2020-02-13 10:42:07 -08:00
if self.window:
self.window.swap_buffers()
# win_time, win_dt = self.window.timer.next_frame()
# while (self.time - self.skip_time - win_time) > 0:
vt = self.time - self.virtual_animation_start_time
rt = time.time() - self.real_animation_start_time
if rt < vt:
self.update_frame(0)
def emit_frame(self):
2020-02-11 19:51:19 -08:00
if not self.skip_animations:
self.file_writer.write_frame(self.camera)
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([
len(mob.get_family_updaters()) > 0
for mob in self.mobjects
])
###
2017-08-24 11:43:38 -07:00
def get_time(self):
return self.time
def increment_time(self, dt):
self.time += dt
###
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):
is_animated = (mob in animation_mobjects)
is_updated = (len(mob.get_family_updaters()) > 0)
if is_animated or is_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([
f"Animation {self.num_plays}: {animations[0]}",
", etc." if len(animations) > 1 else "",
2016-02-27 18:50:33 -08:00
]))
return time_progression
2020-02-11 19:51:19 -08:00
def anims_from_play_args(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):
if self.start_at_animation_number is not None:
2018-02-10 22:45:46 -08:00
if self.num_plays == self.start_at_animation_number:
self.stop_skipping()
if self.end_at_animation_number is not None:
2018-02-10 22:45:46 -08:00
if self.num_plays >= self.end_at_animation_number:
raise EndSceneEarlyException()
def stop_skipping(self):
if self.skip_animations:
self.skip_animations = False
self.skip_time += self.time
2020-02-11 19:51:19 -08:00
# Methods associated with running animations
2019-01-24 22:24:01 -08:00
def handle_play_like_call(func):
def wrapper(self, *args, **kwargs):
self.update_skipping_status()
should_write = not self.skip_animations
if should_write:
2020-02-11 19:51:19 -08:00
self.file_writer.begin_animation()
if self.window:
self.real_animation_start_time = time.time()
self.virtual_animation_start_time = self.time
func(self, *args, **kwargs)
if should_write:
2020-02-11 19:51:19 -08:00
self.file_writer.end_animation()
2019-01-24 22:24:01 -08:00
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):
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_frame(dt)
self.emit_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
2020-02-11 19:51:19 -08:00
animations = self.anims_from_play_args(*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 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)
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_frame(dt)
self.emit_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:
self.update_frame(duration)
n_frames = int(duration * self.camera.frame_rate)
for n in range(n_frames):
self.emit_frame()
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_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
def show(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
2020-02-11 19:51:19 -08:00
# Event handling
def on_mouse_motion(self, point, d_point):
self.mouse_point.move_to(point)
2020-02-11 19:51:19 -08:00
def on_mouse_drag(self, point, d_point, buttons, modifiers):
self.mouse_drag_point.move_to(point)
2020-02-11 19:51:19 -08:00
def on_mouse_press(self, point, button, mods):
pass
def on_mouse_release(self, point, button, mods):
pass
def on_mouse_scroll(self, point, offset):
frame = self.camera.frame
2020-02-14 16:26:49 -08:00
if self.zoom_on_scroll:
factor = 1 + np.arctan(10 * offset[1])
frame.scale(factor, about_point=point)
else:
frame.shift(-30 * offset)
2020-02-11 19:51:19 -08:00
self.camera.refresh_shader_uniforms()
def on_key_release(self, symbol, modifiers):
2020-02-14 16:26:49 -08:00
if chr(symbol) == "z":
self.zoom_on_scroll = False
2020-02-11 19:51:19 -08:00
def on_key_press(self, symbol, modifiers):
if chr(symbol) == "r":
self.camera.frame.restore()
self.camera.refresh_shader_uniforms()
2020-02-14 16:26:49 -08:00
elif chr(symbol) == "z":
self.zoom_on_scroll = True
elif chr(symbol) == "q":
self.quit_interaction = True
2020-02-11 19:51:19 -08:00
def on_resize(self, width: int, height: int):
self.camera.reset_pixel_shape(width, height)
def on_show(self):
pass
def on_hide(self):
pass
def on_close(self):
pass
class EndSceneEarlyException(Exception):
pass