3b1b-manim/scene/scene.py

418 lines
13 KiB
Python
Raw Normal View History

from PIL import Image
from colour import Color
import numpy as np
2015-03-26 22:49:22 -06:00
import itertools as it
import warnings
import time
import os
import copy
2015-12-31 09:25:36 -08:00
from tqdm import tqdm as ProgressDisplay
import inspect
import subprocess as sp
from helpers import *
from camera import Camera
2015-06-09 11:26:12 -07:00
from tk_scene import TkSceneRoot
2017-01-25 16:40:59 -08:00
from mobject import Mobject, VMobject
from animation import Animation
from animation.transform import MoveToTarget, Transform
class Scene(object):
2016-02-27 16:32:53 -08:00
CONFIG = {
"camera_config" : {},
"frame_duration" : LOW_QUALITY_FRAME_DURATION,
"construct_args" : [],
"skip_animations" : False,
}
def __init__(self, **kwargs):
digest_config(self, kwargs)
2016-03-07 19:07:00 -08:00
self.camera = Camera(**self.camera_config)
2015-12-15 11:31:19 -08:00
self.frames = []
2015-10-29 13:45:28 -07:00
self.mobjects = []
self.num_plays = 0
2015-10-29 13:45:28 -07:00
2016-08-10 10:26:07 -07:00
self.setup()
self.construct(*self.construct_args)
2015-06-10 22:00:35 -07:00
2016-08-10 10:26:07 -07:00
def setup(self):
pass #For any common super classes to set up.
2015-06-10 22:00:35 -07:00
def construct(self):
pass #To be implemented in subclasses
def __str__(self):
if hasattr(self, "name"):
return self.name
return self.__class__.__name__
2015-10-12 19:39:46 -07:00
2015-04-03 16:41:25 -07:00
def set_name(self, name):
self.name = name
2015-06-09 11:26:12 -07:00
return self
2015-04-03 16:41:25 -07:00
2016-03-07 19:07:00 -08:00
### 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):
return self.camera.get_image()
2015-10-29 13:45:28 -07:00
def set_camera_image(self, pixel_array):
self.camera.set_image(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, **kwargs):
if "include_submobjects" not in kwargs:
kwargs["include_submobjects"] = False
if mobjects is None:
mobjects = self.mobjects
2016-03-07 19:07:00 -08:00
if background is not None:
self.set_camera_image(background)
2016-03-07 19:07:00 -08:00
else:
2016-11-11 11:18:41 -08:00
self.reset_camera()
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 extract_mobject_family_members(self, *mobjects):
2016-11-11 11:18:41 -08:00
return remove_list_redundancies(list(
it.chain(*[
m.submobject_family()
for m in mobjects
2017-01-25 16:40:59 -08:00
if not (isinstance(m, VMobject) and m.is_subpath)
])
2016-11-11 11:18:41 -08:00
))
def add(self, *mobjects_to_add):
2015-03-26 22:49:22 -06:00
"""
Mobjects will be displayed, from background to foreground,
in the order with which they are entered.
Scene class keeps track not just of the mobject directly added,
but also of every family member therein.
2015-03-26 22:49:22 -06:00
"""
if not all_elements_are_instances(mobjects_to_add, Mobject):
2015-10-29 13:45:28 -07:00
raise Exception("Adding something which is not a mobject")
mobjects_to_add = self.extract_mobject_family_members(*mobjects_to_add)
self.mobjects = list_update(self.mobjects, mobjects_to_add)
2015-06-09 11:26:12 -07:00
return self
def add_mobjects_among(self, values):
"""
So a scene can just add all mobjects it's defined up to that point
by calling add_mobjects_among(locals().values())
"""
mobjects = filter(lambda x : isinstance(x, Mobject), values)
self.add(*mobjects)
return self
def remove(self, *mobjects_to_remove):
if not all_elements_are_instances(mobjects_to_remove, Mobject):
2015-10-29 13:45:28 -07:00
raise Exception("Removing something which is not a mobject")
mobjects_to_remove = self.extract_mobject_family_members(*mobjects_to_remove)
self.mobjects = filter(
lambda m : m not in mobjects_to_remove,
self.mobjects
)
2015-06-09 11:26:12 -07:00
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 = 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 align_run_times(self, *animations, **kwargs):
2016-07-22 11:22:31 -07:00
for animation in animations:
animation.update_config(**kwargs)
2016-07-25 16:04:54 -07:00
max_run_time = max([a.run_time for a in animations])
for animation in animations:
2016-07-22 11:22:31 -07:00
if animation.run_time != max_run_time:
new_rate_func = squish_rate_func(
animation.get_rate_func(),
2016-12-26 07:10:38 -08:00
0, float(animation.run_time)/max_run_time
2016-07-22 11:22:31 -07:00
)
animation.set_rate_func(new_rate_func)
animation.set_run_time(max_run_time)
return animations
2016-02-27 18:50:33 -08:00
def separate_moving_and_static_mobjects(self, *animations):
2016-07-19 11:07:26 -07:00
"""
"""
moving_mobjects = self.extract_mobject_family_members(
*[anim.mobject for anim in animations]
)
static_mobjects = filter(
lambda m : m not in moving_mobjects,
self.mobjects
)
return moving_mobjects, static_mobjects
def get_time_progression(self, animations):
run_time = animations[0].run_time
2015-12-31 09:25:36 -08:00
times = np.arange(0, run_time, self.frame_duration)
time_progression = ProgressDisplay(times)
2016-02-27 18:50:33 -08:00
time_progression.set_description("".join([
"Animation %d: "%self.num_plays,
2016-02-27 18:50:33 -08:00
str(animations[0]),
(", etc." if len(animations) > 1 else ""),
]))
return time_progression
def compile_play_args_to_animation_list(self, *args):
"""
Eacn arg can either be an animation, or a mobject method
followed by that methods arguments.
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"].im_self
if state["last_method"] and state["last_method"].im_self is mobject:
animations.pop()
#method should already have target then.
else:
mobject.target = mobject.copy()
state["curr_method"].im_func(
mobject.target, *state["method_args"]
)
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
you meant to pass in as a Scene.play argument
""")
else:
raise Exception("Invalid play arguments")
compile_method(state)
return animations
2016-02-27 18:50:33 -08:00
def play(self, *args, **kwargs):
if len(args) == 0:
warnings.warn("Called Scene.play with no animations")
return
if self.skip_animations:
kwargs["run_time"] = 0
animations = self.compile_play_args_to_animation_list(*args)
self.num_plays += 1
2016-07-19 11:07:26 -07:00
animations = self.align_run_times(*animations, **kwargs)
moving_mobjects, static_mobjects = \
2016-02-27 18:50:33 -08:00
self.separate_moving_and_static_mobjects(*animations)
2016-07-19 11:07:26 -07:00
self.update_frame(static_mobjects)
2016-03-07 19:07:00 -08:00
static_image = self.get_frame()
for t in self.get_time_progression(animations):
2015-04-03 16:41:25 -07:00
for animation in animations:
animation.update(t / animation.run_time)
2016-02-27 18:50:33 -08:00
self.update_frame(moving_mobjects, static_image)
self.add_frames(self.get_frame())
self.add(*moving_mobjects)
self.mobjects_from_last_animation = moving_mobjects
2016-07-19 11:07:26 -07:00
self.clean_up_animations(*animations)
return self
def clean_up_animations(self, *animations):
2015-04-03 16:41:25 -07:00
for animation in animations:
animation.clean_up()
2016-07-19 11:07:26 -07:00
if animation.is_remover():
self.remove(animation.mobject)
if isinstance(animation, Transform) :
if animation.replace_mobject_with_target_in_scene:
self.remove(animation.mobject)
self.add(animation.original_target_mobject)
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 play_over_time_range(self, t0, t1, *animations):
2015-06-27 04:49:10 -07:00
needed_scene_time = max(abs(t0), abs(t1))
existing_scene_time = len(self.frames)*self.frame_duration
if existing_scene_time < needed_scene_time:
self.dither(needed_scene_time - existing_scene_time)
existing_scene_time = needed_scene_time
#So negative values may be used
if t0 < 0:
t0 = float(t0)%existing_scene_time
if t1 < 0:
t1 = float(t1)%existing_scene_time
t0, t1 = min(t0, t1), max(t0, t1)
moving_mobjects, static_mobjects = \
self.separate_moving_and_static_mobjects(*animations)
2015-06-27 04:49:10 -07:00
for t in np.arange(t0, t1, self.frame_duration):
2015-08-17 13:19:34 -07:00
for animation in animations:
animation.update((t-t0)/(t1 - t0))
2015-06-27 04:49:10 -07:00
index = int(t/self.frame_duration)
2016-03-07 19:07:00 -08:00
self.update_frame(moving_mobjects, self.frames[index])
self.frames[index] = self.get_frame()
2015-08-17 13:19:34 -07:00
for animation in animations:
animation.clean_up()
2015-06-09 11:26:12 -07:00
return self
2015-03-26 22:49:22 -06:00
def dither(self, duration = DEFAULT_DITHER_TIME):
if self.skip_animations:
return self
self.update_frame()
self.add_frames(*[self.get_frame()]*int(duration / self.frame_duration))
2015-06-09 11:26:12 -07:00
return self
def add_frames(self, *frames):
self.frames += list(frames)
2016-01-30 14:44:45 -08:00
def repeat_frames(self, num):
self.frames = self.frames*num
return self
2016-01-30 14:44:45 -08:00
def reverse_frames(self):
self.frames.reverse()
return self
def invert_colors(self):
white_frame = 255*np.ones(self.get_frame().shape, dtype = 'uint8')
self.frames = [
white_frame-frame
for frame in self.frames
]
return self
2015-06-09 11:26:12 -07:00
def show_frame(self):
self.update_frame()
2015-04-03 16:41:25 -07:00
Image.fromarray(self.get_frame()).show()
2015-06-09 11:26:12 -07:00
def preview(self):
TkSceneRoot(self)
2015-06-19 08:31:02 -07:00
def save_image(self, directory = MOVIE_DIR, name = None):
path = os.path.join(directory, "images")
file_name = (name or str(self)) + ".png"
full_path = os.path.join(path, file_name)
if not os.path.exists(path):
os.makedirs(path)
2016-12-26 07:10:38 -08:00
self.update_frame()
Image.fromarray(self.get_frame()).save(full_path)
2015-05-17 15:08:51 -07:00
def get_movie_file_path(self, name, extension):
file_path = os.path.join(MOVIE_DIR, name)
if not file_path.endswith(extension):
file_path += extension
directory = os.path.split(file_path)[0]
if not os.path.exists(directory):
os.makedirs(directory)
return file_path
def write_to_movie(self, name = None):
if len(self.frames) == 0:
print "No frames, so I'm not writing anything"
return
if name is None:
name = str(self)
file_path = self.get_movie_file_path(name, ".mp4")
print "Writing to %s"%file_path
fps = int(1/self.frame_duration)
2016-02-27 18:50:33 -08:00
height, width = self.camera.pixel_shape
command = [
FFMPEG_BIN,
'-y', # overwrite output file if it exists
'-f', 'rawvideo',
'-vcodec','rawvideo',
2016-02-27 18:50:33 -08:00
'-s', '%dx%d'%(width, height), # size of one frame
'-pix_fmt', 'rgb24',
'-r', str(fps), # frames per second
'-i', '-', # The imput comes from a pipe
'-an', # Tells FFMPEG not to expect any audio
'-vcodec', 'mpeg',
'-c:v', 'libx264',
'-pix_fmt', 'yuv420p',
'-loglevel', 'error',
file_path,
]
process = sp.Popen(command, stdin=sp.PIPE)
for frame in self.frames:
process.stdin.write(frame.tostring())
process.stdin.close()
process.wait()
2015-08-17 13:19:34 -07:00