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

625 lines
22 KiB
Python
Raw Normal View History

import inspect
import random
2019-07-26 16:45:52 +08:00
import platform
import itertools as it
import logging
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 DEFAULT_WAIT_TIME
from manimlib.mobject.mobject import Mobject
2019-01-24 21:47:40 -08:00
from manimlib.scene.scene_file_writer import SceneFileWriter
2021-01-03 12:29:05 -08:00
from manimlib.utils.config_ops import digest_config
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
2021-01-03 12:29:05 -08:00
class Scene(object):
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,
2020-02-22 13:20:22 -08:00
"linger_after_completion": True,
}
def __init__(self, **kwargs):
2021-01-03 12:29:05 -08:00
digest_config(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
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-14 16:26:49 -08:00
# Items associated with interaction
self.mouse_point = Point()
self.mouse_drag_point = Point()
# Much nicer to work with deterministic scenes
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):
self.virtual_animation_start_time = 0
self.real_animation_start_time = time.time()
self.file_writer.begin()
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-22 13:20:22 -08:00
# Where all the animation happens
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()
2020-02-22 13:20:22 -08:00
if self.window and self.linger_after_completion:
2020-02-13 10:42:07 -08:00
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
2020-06-18 16:30:38 -07:00
self.lock_static_mobject_data()
while not (self.window.is_closing or 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()
2020-06-18 16:30:38 -07:00
if self.quit_interaction:
self.unlock_mobject_data()
def embed(self):
if not self.preview:
# If the scene is just being
# written, ignore embed calls
return
self.stop_skipping()
2020-02-22 13:20:22 -08:00
self.linger_after_completion = False
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())
# Use the locals of the caller as the local namespace
# once embeded, and add a few custom shortcuts
local_ns = inspect.currentframe().f_back.f_locals
local_ns["touch"] = self.interact
for term in ("play", "add", "remove", "clear"):
local_ns[term] = getattr(self, term)
shell(local_ns=local_ns, stack_depth=2)
2020-04-12 09:53:29 -07:00
# End scene when exiting an embed.
raise EndSceneEarlyException()
def __str__(self):
return self.__class__.__name__
2015-04-03 16:41:25 -07:00
# 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 show(self):
self.update_frame(ignore_skipping=True)
self.get_image().show()
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()
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)
# Related to updating
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
])
# Related to time
def get_time(self):
return self.time
def increment_time(self, dt):
self.time += dt
# Related to internal mobject organization
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]
# Related to skipping
def update_skipping_status(self):
if self.start_at_animation_number is not None:
if self.num_plays == self.start_at_animation_number:
self.stop_skipping()
if self.end_at_animation_number is not None:
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
# Methods associated with running animations
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
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
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 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
def lock_static_mobject_data(self, *animations):
movers = list(it.chain(*[
anim.mobject.get_family()
for anim in animations
]))
for mobject in self.mobjects:
2021-01-13 00:35:39 -10:00
if mobject in movers or mobject.get_family_updaters():
continue
self.camera.set_mobjects_as_static(mobject)
def unlock_mobject_data(self):
self.camera.release_static_mobjects()
2019-02-08 12:51:21 -08:00
def begin_animations(self, animations):
for animation in animations:
animation.begin()
# Anything animated that's not already in the
# scene gets added to the scene. Note, for
# animated mobjects that are in the family of
# those on screen, this can result in a restructuring
# of the scene.mobjects list, which is usually desired.
if animation.mobject not in self.mobjects:
self.add(animation.mobject)
2019-02-08 11:00:04 -08:00
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)
if self.skip_animations:
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:
logging.log(
logging.WARNING,
"Called Scene.play with no animations"
)
2019-02-08 12:51:21 -08:00
return
2020-02-11 19:51:19 -08:00
animations = self.anims_from_play_args(*args, **kwargs)
self.lock_static_mobject_data(*animations)
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)
self.unlock_mobject_data()
2016-07-19 11:07:26 -07:00
@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():
self.lock_static_mobject_data()
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
self.unlock_mobject_data()
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
2020-02-22 13:20:22 -08:00
# Helpers for interactive development
def save_state(self):
self.saved_state = {
"mobjects": self.mobjects,
"mobject_states": [
mob.copy()
for mob in self.mobjects
],
}
def restore(self):
if not hasattr(self, "saved_state"):
raise Exception("Trying to restore scene without having saved")
mobjects = self.saved_state["mobjects"]
states = self.saved_state["mobject_states"]
for mob, state in zip(mobjects, states):
mob.become(state)
self.mobjects = mobjects
2020-02-11 19:51:19 -08:00
# Event handling
2021-01-28 14:02:43 +05:30
def get_event_listeners_mobjects(self):
"""
This method returns all the mobjects that listen to events
in reversed order. So the top most mobject's event is called first.
This helps in event bubbling.
"""
return filter(
lambda mob: mob.listen_to_events,
reversed(self.get_mobject_family_members())
)
2020-02-11 19:51:19 -08:00
def on_mouse_motion(self, point, d_point):
self.mouse_point.move_to(point)
2021-01-28 14:02:43 +05:30
for mob_listener in self.get_event_listeners_mobjects():
if mob_listener.is_point_touching(point):
propagate_event = mob_listener.on_mouse_motion(point, d_point)
if propagate_event is not None and propagate_event is False:
return
frame = self.camera.frame
if self.window.is_key_pressed(ord("d")):
frame.increment_theta(-d_point[0])
frame.increment_phi(d_point[1])
elif self.window.is_key_pressed(ord("s")):
2021-01-19 14:12:25 -08:00
shift = -d_point
shift[0] *= frame.get_width() / 2
shift[1] *= frame.get_height() / 2
transform = frame.get_inverse_camera_rotation_matrix()
2021-01-19 14:12:25 -08:00
shift = np.dot(np.transpose(transform), shift)
frame.shift(shift)
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
2021-01-28 14:02:43 +05:30
for mob_listener in self.get_event_listeners_mobjects():
if mob_listener.is_point_touching(point):
propagate_event = mob_listener.on_mouse_drag(point, d_point, buttons, modifiers)
if propagate_event is not None and propagate_event is False:
return
2020-02-11 19:51:19 -08:00
def on_mouse_press(self, point, button, mods):
2021-01-28 14:02:43 +05:30
for mob_listener in self.get_event_listeners_mobjects():
if mob_listener.is_point_touching(point):
propagate_event = mob_listener.on_mouse_press(point, button, mods)
if propagate_event is not None and propagate_event is False:
return
2020-02-11 19:51:19 -08:00
def on_mouse_release(self, point, button, mods):
2021-01-28 14:02:43 +05:30
for mob_listener in self.get_event_listeners_mobjects():
if mob_listener.is_point_touching(point):
propagate_event = mob_listener.on_mouse_release(point, button, mods)
if propagate_event is not None and propagate_event is False:
return
2020-02-11 19:51:19 -08:00
def on_mouse_scroll(self, point, offset):
2021-01-28 14:02:43 +05:30
for mob_listener in self.get_event_listeners_mobjects():
if mob_listener.is_point_touching(point):
propagate_event = mob_listener.on_mouse_scroll(point, offset)
if propagate_event is not None and propagate_event is False:
return
2021-01-08 22:26:14 -08:00
frame = self.camera.frame
if self.window.is_key_pressed(ord("z")):
2020-02-14 16:26:49 -08:00
factor = 1 + np.arctan(10 * offset[1])
2021-01-08 22:26:14 -08:00
frame.scale(factor, about_point=point)
2021-01-19 14:12:25 -08:00
else:
transform = frame.get_inverse_camera_rotation_matrix()
shift = np.dot(np.transpose(transform), offset)
frame.shift(-20.0 * shift)
2020-02-11 19:51:19 -08:00
def on_key_release(self, symbol, modifiers):
2021-01-28 14:02:43 +05:30
for mob_listener in self.get_event_listeners_mobjects():
propagate_event = mob_listener.on_key_release(symbol, modifiers)
if propagate_event is not None and propagate_event is False:
return
2020-02-11 19:51:19 -08:00
def on_key_press(self, symbol, modifiers):
2021-01-16 10:21:42 +08:00
try:
char = chr(symbol)
except OverflowError:
print(" Warning: The value of the pressed key is too large.")
return
2021-01-28 14:02:43 +05:30
for mob_listener in self.get_event_listeners_mobjects():
propagate_event = mob_listener.on_key_press(symbol, modifiers)
if propagate_event is not None and propagate_event is False:
return
2021-01-16 10:21:42 +08:00
if char == "r":
2020-06-01 16:21:18 -07:00
self.camera.frame.to_default_state()
2021-01-16 10:21:42 +08:00
elif char == "q":
2020-02-14 16:26:49 -08:00
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