mirror of
				https://github.com/3b1b/manim.git
				synced 2025-11-01 15:08:59 +00:00 
			
		
		
		
	Finished SceneFileWriter refactor
This commit is contained in:
		
							parent
							
								
									8ae0556394
								
							
						
					
					
						commit
						e5e1fa908b
					
				
					 14 changed files with 94 additions and 111 deletions
				
			
		| 
						 | 
				
			
			@ -60,7 +60,7 @@ Set MEDIA_DIR environment variable to determine where image and animation files
 | 
			
		|||
 | 
			
		||||
Look through the old_projects folder to see the code for previous 3b1b videos.  Note, however, that developments are often made to the library without considering backwards compatibility on those old_projects.  To run them with a guarantee that they will work, you will have to go back to the commit which complete that project.
 | 
			
		||||
 | 
			
		||||
While developing a scene, the `-s` flag is helpful to just see what things look like at the end without having to generate the full animation.  It can also be helpful to use the `-n` flag to skip over some number of animations.
 | 
			
		||||
While developing a scene, the `-sp` flags are helpful to just see what things look like at the end without having to generate the full animation.  It can also be helpful to use the `-n` flag to skip over some number of animations.
 | 
			
		||||
 | 
			
		||||
### Documentation
 | 
			
		||||
Documentation is in progress at [manim.readthedocs.io](https://manim.readthedocs.io).
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| 
						 | 
				
			
			@ -1,4 +0,0 @@
 | 
			
		|||
file '/Users/grant/cs/manim/active_projects/clacks/question/480p15/partial_movie_directory/NameIntro/00000.mp4'
 | 
			
		||||
file '/Users/grant/cs/manim/active_projects/clacks/question/480p15/partial_movie_directory/NameIntro/00001.mp4'
 | 
			
		||||
file '/Users/grant/cs/manim/active_projects/clacks/question/480p15/partial_movie_directory/NameIntro/00002.mp4'
 | 
			
		||||
file '/Users/grant/cs/manim/active_projects/clacks/question/480p15/partial_movie_directory/NameIntro/00003.mp4'
 | 
			
		||||
| 
						 | 
				
			
			@ -7,8 +7,9 @@ from big_ol_pile_of_manim_imports import *
 | 
			
		|||
#
 | 
			
		||||
# Use the flat -l for a faster rendering at a lower
 | 
			
		||||
# quality.
 | 
			
		||||
# Use -s to skip to the end and just show the final frame
 | 
			
		||||
# Use the -p to have the animation pop up once done.
 | 
			
		||||
# Use -s to skip to the end and just save the final frame
 | 
			
		||||
# Use the -p to have the animation (or image, if -s was
 | 
			
		||||
# used) pop up once done.
 | 
			
		||||
# Use -n <number> to skip ahead to the n'th animation of a scene.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,7 +25,7 @@ def parse_cli():
 | 
			
		|||
        parser.add_argument(
 | 
			
		||||
            "-p", "--preview",
 | 
			
		||||
            action="store_true",
 | 
			
		||||
            help="Automatically open movie file once its done",
 | 
			
		||||
            help="Automatically open the saved file once its done",
 | 
			
		||||
        ),
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            "-w", "--write_to_movie",
 | 
			
		||||
| 
						 | 
				
			
			@ -35,7 +35,7 @@ def parse_cli():
 | 
			
		|||
        parser.add_argument(
 | 
			
		||||
            "-s", "--save_last_frame",
 | 
			
		||||
            action="store_true",
 | 
			
		||||
            help="Save the last frame and open the image file",
 | 
			
		||||
            help="Save the last frame",
 | 
			
		||||
        ),
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            "-l", "--low_quality",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,7 +33,6 @@ class Scene(Container):
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    def __init__(self, **kwargs):
 | 
			
		||||
        # Perhaps allow passing in a non-empty *mobjects parameter?
 | 
			
		||||
        Container.__init__(self, **kwargs)
 | 
			
		||||
        self.camera = self.camera_class(**self.camera_config)
 | 
			
		||||
        self.file_writer = SceneFileWriter(
 | 
			
		||||
| 
						 | 
				
			
			@ -42,6 +41,7 @@ class Scene(Container):
 | 
			
		|||
 | 
			
		||||
        self.mobjects = []
 | 
			
		||||
        self.continual_animations = []
 | 
			
		||||
        # TODO, remove need for foreground mobjects
 | 
			
		||||
        self.foreground_mobjects = []
 | 
			
		||||
        self.num_plays = 0
 | 
			
		||||
        self.time = 0
 | 
			
		||||
| 
						 | 
				
			
			@ -59,16 +59,6 @@ class Scene(Container):
 | 
			
		|||
        self.file_writer.finish()
 | 
			
		||||
        self.print_end_message()
 | 
			
		||||
 | 
			
		||||
    def handle_play_like_call(func):
 | 
			
		||||
        def wrapper(self, *args, **kwargs):
 | 
			
		||||
            self.handle_animation_skipping()
 | 
			
		||||
            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
 | 
			
		||||
 | 
			
		||||
    def setup(self):
 | 
			
		||||
        """
 | 
			
		||||
        This is meant to be implement by any scenes which
 | 
			
		||||
| 
						 | 
				
			
			@ -111,8 +101,6 @@ class Scene(Container):
 | 
			
		|||
    def get_attrs(self, *keys):
 | 
			
		||||
        return [getattr(self, key) for key in keys]
 | 
			
		||||
 | 
			
		||||
    # TODO, Scene file writer now handles sound
 | 
			
		||||
 | 
			
		||||
    # Only these methods should touch the camera
 | 
			
		||||
    def set_camera(self, camera):
 | 
			
		||||
        self.camera = camera
 | 
			
		||||
| 
						 | 
				
			
			@ -449,7 +437,7 @@ class Scene(Container):
 | 
			
		|||
        compile_method(state)
 | 
			
		||||
        return animations
 | 
			
		||||
 | 
			
		||||
    def handle_animation_skipping(self):
 | 
			
		||||
    def update_skipping_status(self):
 | 
			
		||||
        if self.start_at_animation_number:
 | 
			
		||||
            if self.num_plays == self.start_at_animation_number:
 | 
			
		||||
                self.skip_animations = False
 | 
			
		||||
| 
						 | 
				
			
			@ -458,6 +446,16 @@ class Scene(Container):
 | 
			
		|||
                self.skip_animations = True
 | 
			
		||||
                raise EndSceneEarlyException()
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
    @handle_play_like_call
 | 
			
		||||
    def play(self, *args, **kwargs):
 | 
			
		||||
        if len(args) == 0:
 | 
			
		||||
| 
						 | 
				
			
			@ -496,7 +494,6 @@ class Scene(Container):
 | 
			
		|||
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
    # TODO
 | 
			
		||||
    def idle_stream(self):
 | 
			
		||||
        self.file_writer.idle_stream()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -569,6 +566,10 @@ class Scene(Container):
 | 
			
		|||
        for frame in frames:
 | 
			
		||||
            self.file_writer.write_frame(frame)
 | 
			
		||||
 | 
			
		||||
    def add_sound(self, sound_file, time_offset=0):
 | 
			
		||||
        time = self.get_time() + time_offset
 | 
			
		||||
        self.file_writer.add_sound(sound_file, time)
 | 
			
		||||
 | 
			
		||||
    def show_frame(self):
 | 
			
		||||
        self.update_frame(ignore_skipping=True)
 | 
			
		||||
        self.get_image().show()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,6 +16,7 @@ from manimlib.utils.config_ops import digest_config
 | 
			
		|||
from manimlib.utils.file_ops import guarantee_existance
 | 
			
		||||
from manimlib.utils.file_ops import add_extension_if_not_present
 | 
			
		||||
from manimlib.utils.file_ops import get_sorted_integer_files
 | 
			
		||||
from manimlib.utils.sounds import get_full_sound_file_path
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SceneFileWriter(object):
 | 
			
		||||
| 
						 | 
				
			
			@ -38,12 +39,11 @@ class SceneFileWriter(object):
 | 
			
		|||
    def __init__(self, scene, **kwargs):
 | 
			
		||||
        digest_config(self, kwargs)
 | 
			
		||||
        self.scene = scene
 | 
			
		||||
        self.init_audio()
 | 
			
		||||
        self.init_output_directories()
 | 
			
		||||
        self.stream_lock = False
 | 
			
		||||
        self.init_output_directories()
 | 
			
		||||
        self.init_audio()
 | 
			
		||||
 | 
			
		||||
    # Output directories and files
 | 
			
		||||
 | 
			
		||||
    def init_output_directories(self):
 | 
			
		||||
        output_directory = self.output_directory or self.get_default_output_directory()
 | 
			
		||||
        file_name = self.file_name or self.get_default_file_name()
 | 
			
		||||
| 
						 | 
				
			
			@ -59,6 +59,7 @@ class SceneFileWriter(object):
 | 
			
		|||
            )
 | 
			
		||||
        if self.write_to_movie:
 | 
			
		||||
            movie_dir = guarantee_existance(os.path.join(
 | 
			
		||||
                VIDEO_DIR,
 | 
			
		||||
                output_directory,
 | 
			
		||||
                self.get_movie_directory(),
 | 
			
		||||
            ))
 | 
			
		||||
| 
						 | 
				
			
			@ -94,39 +95,6 @@ class SceneFileWriter(object):
 | 
			
		|||
    def get_partial_movie_directory(self):
 | 
			
		||||
        return "partial_movie_directory"
 | 
			
		||||
 | 
			
		||||
    # Sound
 | 
			
		||||
    # TODO, make work with Scene
 | 
			
		||||
    def init_audio(self):
 | 
			
		||||
        self.includes_sound = False
 | 
			
		||||
 | 
			
		||||
    def create_audio_segment(self):
 | 
			
		||||
        self.audio_segment = AudioSegment.silent()
 | 
			
		||||
 | 
			
		||||
    def add_audio_segment(self, new_segment, time_offset=0):
 | 
			
		||||
        if not self.includes_sound:
 | 
			
		||||
            self.includes_sound = True
 | 
			
		||||
            self.create_audio_segment()
 | 
			
		||||
        segment = self.audio_segment
 | 
			
		||||
        overly_time = self.get_time() + time_offset
 | 
			
		||||
        if overly_time < 0:
 | 
			
		||||
            raise Exception("Adding sound at timestamp < 0")
 | 
			
		||||
 | 
			
		||||
        curr_end = segment.duration_seconds
 | 
			
		||||
        new_end = overly_time + new_segment.duration_seconds
 | 
			
		||||
        diff = new_end - curr_end
 | 
			
		||||
        if diff > 0:
 | 
			
		||||
            segment = segment.append(
 | 
			
		||||
                AudioSegment.silent(int(np.ceil(diff * 1000))),
 | 
			
		||||
                crossfade=0,
 | 
			
		||||
            )
 | 
			
		||||
        self.audio_segment = segment.overlay(
 | 
			
		||||
            new_segment, position=int(1000 * overly_time)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def add_sound(self, sound_file, time_offset=0):
 | 
			
		||||
        new_segment = AudioSegment.from_file(sound_file)
 | 
			
		||||
        self.add_audio_segment(new_segment, 0)
 | 
			
		||||
 | 
			
		||||
    # Directory getters
 | 
			
		||||
    def get_image_file_path(self):
 | 
			
		||||
        return self.image_file_path
 | 
			
		||||
| 
						 | 
				
			
			@ -144,16 +112,41 @@ class SceneFileWriter(object):
 | 
			
		|||
    def get_movie_file_path(self):
 | 
			
		||||
        return self.movie_file_path
 | 
			
		||||
 | 
			
		||||
    # Sound
 | 
			
		||||
    def init_audio(self):
 | 
			
		||||
        self.includes_sound = False
 | 
			
		||||
 | 
			
		||||
    def create_audio_segment(self):
 | 
			
		||||
        self.audio_segment = AudioSegment.silent()
 | 
			
		||||
 | 
			
		||||
    def add_audio_segment(self, new_segment, time=None):
 | 
			
		||||
        if not self.includes_sound:
 | 
			
		||||
            self.includes_sound = True
 | 
			
		||||
            self.create_audio_segment()
 | 
			
		||||
        segment = self.audio_segment
 | 
			
		||||
        curr_end = segment.duration_seconds
 | 
			
		||||
        if time is None:
 | 
			
		||||
            time = curr_end
 | 
			
		||||
        if time < 0:
 | 
			
		||||
            raise Exception("Adding sound at timestamp < 0")
 | 
			
		||||
 | 
			
		||||
        new_end = time + new_segment.duration_seconds
 | 
			
		||||
        diff = new_end - curr_end
 | 
			
		||||
        if diff > 0:
 | 
			
		||||
            segment = segment.append(
 | 
			
		||||
                AudioSegment.silent(int(np.ceil(diff * 1000))),
 | 
			
		||||
                crossfade=0,
 | 
			
		||||
            )
 | 
			
		||||
        self.audio_segment = segment.overlay(
 | 
			
		||||
            new_segment, position=int(1000 * time)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def add_sound(self, sound_file, time):
 | 
			
		||||
        file_path = get_full_sound_file_path(sound_file)
 | 
			
		||||
        new_segment = AudioSegment.from_file(file_path)
 | 
			
		||||
        self.add_audio_segment(new_segment, time)
 | 
			
		||||
 | 
			
		||||
    # Writers
 | 
			
		||||
    def write_frame(self, frame):
 | 
			
		||||
        if self.write_to_movie:
 | 
			
		||||
            self.writing_process.stdin.write(frame.tostring())
 | 
			
		||||
 | 
			
		||||
    def save_image(self, image):
 | 
			
		||||
        file_path = self.get_image_file_path()
 | 
			
		||||
        image.save(file_path)
 | 
			
		||||
        self.print_file_ready_message(file_path)
 | 
			
		||||
 | 
			
		||||
    def begin_animation(self, allow_write=False):
 | 
			
		||||
        if self.write_to_movie and allow_write:
 | 
			
		||||
            self.open_movie_pipe()
 | 
			
		||||
| 
						 | 
				
			
			@ -167,6 +160,15 @@ class SceneFileWriter(object):
 | 
			
		|||
            self.stream_lock = True
 | 
			
		||||
            thread.start_new_thread(self.idle_stream, ())
 | 
			
		||||
 | 
			
		||||
    def write_frame(self, frame):
 | 
			
		||||
        if self.write_to_movie:
 | 
			
		||||
            self.writing_process.stdin.write(frame.tostring())
 | 
			
		||||
 | 
			
		||||
    def save_image(self, image):
 | 
			
		||||
        file_path = self.get_image_file_path()
 | 
			
		||||
        image.save(file_path)
 | 
			
		||||
        self.print_file_ready_message(file_path)
 | 
			
		||||
 | 
			
		||||
    def idle_stream(self):
 | 
			
		||||
        while self.stream_lock:
 | 
			
		||||
            a = datetime.datetime.now()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,38 +16,16 @@ def guarantee_existance(path):
 | 
			
		|||
    return os.path.abspath(path)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# def get_scene_output_directory(scene_class):
 | 
			
		||||
#     return guarantee_existance(os.path.join(
 | 
			
		||||
#         VIDEO_DIR,
 | 
			
		||||
#         scene_class.__module__.replace(".", os.path.sep)
 | 
			
		||||
#     ))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# def get_movie_output_directory(scene_class, camera_config, frame_duration):
 | 
			
		||||
#     directory = get_scene_output_directory(scene_class)
 | 
			
		||||
#     sub_dir = "%dp%d" % (
 | 
			
		||||
#         camera_config["pixel_height"],
 | 
			
		||||
#         int(1.0 / frame_duration)
 | 
			
		||||
#     )
 | 
			
		||||
#     return guarantee_existance(os.path.join(directory, sub_dir))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# def get_partial_movie_output_directory(scene, camera_config, frame_duration):
 | 
			
		||||
#     directory = get_movie_output_directory(
 | 
			
		||||
#         scene.__class__, camera_config, frame_duration
 | 
			
		||||
#     )
 | 
			
		||||
#     return guarantee_existance(
 | 
			
		||||
#         os.path.join(
 | 
			
		||||
#             directory,
 | 
			
		||||
#             "partial_movie_files",
 | 
			
		||||
#             scene.get_output_file_name(),
 | 
			
		||||
#         )
 | 
			
		||||
#     )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# def get_image_output_directory(scene_class, sub_dir="images"):
 | 
			
		||||
#     directory = get_scene_output_directory(scene_class)
 | 
			
		||||
#     return guarantee_existance(os.path.join(directory, sub_dir))
 | 
			
		||||
def seek_full_path_from_defaults(file_name, default_dir, extensions):
 | 
			
		||||
    possible_paths = [file_name]
 | 
			
		||||
    possible_paths += [
 | 
			
		||||
        os.path.join(default_dir, file_name + extension)
 | 
			
		||||
        for extension in ["", *extensions]
 | 
			
		||||
    ]
 | 
			
		||||
    for path in possible_paths:
 | 
			
		||||
        if os.path.exists(path):
 | 
			
		||||
            return path
 | 
			
		||||
    raise IOError("File {} not Found".format(file_name))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_sorted_integer_files(directory,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,20 +4,15 @@ import os
 | 
			
		|||
from PIL import Image
 | 
			
		||||
 | 
			
		||||
from manimlib.constants import RASTER_IMAGE_DIR
 | 
			
		||||
from manimlib.utils.file_ops import seek_full_path_from_defaults
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_full_raster_image_path(image_file_name):
 | 
			
		||||
    possible_paths = [
 | 
			
		||||
    return seek_full_path_from_defaults(
 | 
			
		||||
        image_file_name,
 | 
			
		||||
        os.path.join(RASTER_IMAGE_DIR, image_file_name),
 | 
			
		||||
        os.path.join(RASTER_IMAGE_DIR, image_file_name + ".jpg"),
 | 
			
		||||
        os.path.join(RASTER_IMAGE_DIR, image_file_name + ".png"),
 | 
			
		||||
        os.path.join(RASTER_IMAGE_DIR, image_file_name + ".gif"),
 | 
			
		||||
    ]
 | 
			
		||||
    for path in possible_paths:
 | 
			
		||||
        if os.path.exists(path):
 | 
			
		||||
            return path
 | 
			
		||||
    raise IOError("File %s not Found" % image_file_name)
 | 
			
		||||
        default_dir=RASTER_IMAGE_DIR,
 | 
			
		||||
        extensions=[".jpg", ".png", ".gif"]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def drag_pixels(frames):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,6 @@
 | 
			
		|||
import os
 | 
			
		||||
from manimlib.constants import SOUND_DIR
 | 
			
		||||
from manimlib.utils.file_ops import seek_full_path_from_defaults
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def play_chord(*nums):
 | 
			
		||||
| 
						 | 
				
			
			@ -28,3 +30,11 @@ def play_error_sound():
 | 
			
		|||
 | 
			
		||||
def play_finish_sound():
 | 
			
		||||
    play_chord(12, 9, 5, 2)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_full_sound_file_path(sound_file_name):
 | 
			
		||||
    return seek_full_path_from_defaults(
 | 
			
		||||
        sound_file_name,
 | 
			
		||||
        default_dir=SOUND_DIR,
 | 
			
		||||
        extensions=[".wav", ".mp3"]
 | 
			
		||||
    )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue