diff --git a/docs/source/documentation/constants.rst b/docs/source/documentation/constants.rst index 81ec4937..fb0f4520 100644 --- a/docs/source/documentation/constants.rst +++ b/docs/source/documentation/constants.rst @@ -8,38 +8,35 @@ they are only used inside manim. Frame and pixel shape --------------------- +These values will be determined based on the ``camera`` configuration in default_config.yml or custom_config.yml + .. code-block:: python - ASPECT_RATIO = 16.0 / 9.0 - FRAME_HEIGHT = 8.0 - FRAME_WIDTH = FRAME_HEIGHT * ASPECT_RATIO - FRAME_Y_RADIUS = FRAME_HEIGHT / 2 - FRAME_X_RADIUS = FRAME_WIDTH / 2 + ASPECT_RATIO + FRAME_HEIGHT + FRAME_WIDTH + FRAME_Y_RADIUS + FRAME_X_RADIUS - DEFAULT_PIXEL_HEIGHT = 1080 - DEFAULT_PIXEL_WIDTH = 1920 - DEFAULT_FPS = 30 + DEFAULT_PIXEL_HEIGHT + DEFAULT_PIXEL_WIDTH + DEFAULT_FPS Buffs ----- -.. code-block:: python +These values will be determined based on the ``size`` configuration in default_config.yml or custom_config.yml - SMALL_BUFF = 0.1 - MED_SMALL_BUFF = 0.25 - MED_LARGE_BUFF = 0.5 - LARGE_BUFF = 1 - - DEFAULT_MOBJECT_TO_EDGE_BUFFER = MED_LARGE_BUFF # Distance between object and edge - DEFAULT_MOBJECT_TO_MOBJECT_BUFFER = MED_SMALL_BUFF # Distance between objects - -Run times ---------- .. code-block:: python - DEFAULT_POINTWISE_FUNCTION_RUN_TIME = 3.0 - DEFAULT_WAIT_TIME = 1.0 + SMALL_BUFF + MED_SMALL_BUFF + MED_LARGE_BUFF + LARGE_BUFF + + DEFAULT_MOBJECT_TO_EDGE_BUFF + DEFAULT_MOBJECT_TO_MOBJECT_BUFF Coordinates ----------- @@ -89,16 +86,11 @@ Text OBLIQUE = "OBLIQUE" BOLD = "BOLD" -Stroke width ------------- - -.. code-block:: python - - DEFAULT_STROKE_WIDTH = 4 - Colours ------- +Color constants are determined based on the ``color`` configuration in default_config.yml or custom_config.yml + Here are the preview of default colours. (Modified from `elteoremadebeethoven `_) diff --git a/docs/source/documentation/custom_config.rst b/docs/source/documentation/custom_config.rst index c82f8b34..271399ce 100644 --- a/docs/source/documentation/custom_config.rst +++ b/docs/source/documentation/custom_config.rst @@ -9,6 +9,10 @@ custom_config running file under the ``output`` path, and save the output (``images/`` or ``videos/``) in it. +- ``base`` + The root directory that will hold files, such as video files manim renders, + or image resources that it pulls from + - ``output`` Output file path, the videos will be saved in the ``videos/`` folder under it, and the pictures will be saved in the ``images/`` folder under it. @@ -66,34 +70,62 @@ custom_config The directory for storing sound files to be used in ``Scene.add_sound()`` ( including ``.wav`` and ``.mp3``). -- ``temporary_storage`` +- ``cache`` The directory for storing temporarily generated cache files, including ``Tex`` cache, ``Text`` cache and storage of object points. -``tex`` + +``window`` +---------- + +- ``position_string`` + The relative position of the playback window on the display (two characters, + the first character means upper(U) / middle(O) / lower(D), the second character + means left(L) / middle(O) / right(R)). + +- ``monitor_index`` + If using multiple monitors, which one should the window show up in? + +- ``full_screen`` + Should the preview window be full screen. If not, it defaults to half the screen + +- ``position`` + This is an option to more manually set the default window position, in pixel + coordinates, e.g. (500, 300) + +- ``size`` + Option to more manually set the default window size, in pixel coordinates, + e.g. (1920, 1080) + + +``camera`` +---------- + +- ``resolution`` + Resolution to render at, e.g. (1920, 1080) + +- ``background_color`` + Default background color of scenes + +- ``fps`` + Framerate + +- ``background_opacity`` + Opacity of the background + + +``file_writer`` +--------------- +Configuration specifying how files are written, e.g. what ffmpeg parameters to use + + +``scene`` ------- +Some default configuration for the Scene class -- ``executable`` - The executable program used to compile LaTeX (``latex`` or ``xelatex -no-pdf`` - is recommended) -- ``template_file`` - LaTeX template used, in ``manimlib/tex_templates`` - -- ``intermediate_filetype`` - The type of intermediate vector file generated after compilation (``dvi`` if - ``latex`` is used, ``xdv`` if ``xelatex`` is used) - -- ``text_to_replace`` - The text to be replaced in the template (needn't to change) - -``universal_import_line`` -------------------------- - -Import line that need to execute when entering interactive mode directly. - -``style`` ---------- +``text`` +------- - ``font`` Default font of Text @@ -101,57 +133,44 @@ Import line that need to execute when entering interactive mode directly. - ``text_alignment`` Default text alignment for LaTeX -- ``background_color`` - Default background color - -``window_position`` -------------------- - -The relative position of the playback window on the display (two characters, -the first character means upper(U) / middle(O) / lower(D), the second character -means left(L) / middle(O) / right(R)). - -``window_monitor`` ------------------- - -The number of the monitor you want the preview window to pop up on. (default is 0) - -``full_screen`` ---------------- - -Whether open the window in full screen. (default is false) - -``break_into_partial_movies`` ------------------------------ - -If this is set to ``True``, then many small files will be written corresponding -to each ``Scene.play`` and ``Scene.wait`` call, and these files will then be combined -to form the full scene. - -Sometimes video-editing is made easier when working with the broken up scene, which -effectively has cuts at all the places you might want. - -``camera_resolutions`` ----------------------- - -Export resolutions - -- ``low`` - Low resolutions (default is 480p) - -- ``medium`` - Medium resolutions (default is 720p) - -- ``high`` - High resolutions (default is 1080p) - -- ``ultra_high`` - Ultra high resolutions (default is 4K) - -- ``default_resolutions`` - Default resolutions (one of the above four, default is high) - -``fps`` +``tex`` ------- -Export frame rate. (default is 30) \ No newline at end of file +- ``template`` + Which configuration from the manimlib/tex_template.yml file should be used + to determine the latex compiler to use, and what preamble to include for + rendering tex. + + +``sizes`` +--------- + +Valuess for various constants used in manimm to specify distances, like the height +of the frame, the value of SMALL_BUFF, LARGE_BUFF, etc. + + +``colors`` +---------- + +Color pallete to use, determining values of color constants like RED, BLUE_E, TEAL, etc. + +``loglevel`` +------------ + +Can be DEBUG / INFO / WARNING / ERROR / CRITICAL + + +``universal_import_line`` +------------------------- + +Import line that need to execute when entering interactive mode directly. + + +``ignore_manimlib_modules_on_reload`` +------------------------------------- + +When calling ``reload`` during the interactive mode, imported modules are +by default reloaded, in case the user writing a scene which pulls from various +other files they have written. By default, modules withinn the manim library will +be ignored, but one developing manim may want to set this to be False so that +edits to the library are reloaded as well. diff --git a/docs/source/getting_started/structure.rst b/docs/source/getting_started/structure.rst index e910699b..b7a0b2a7 100644 --- a/docs/source/getting_started/structure.rst +++ b/docs/source/getting_started/structure.rst @@ -103,7 +103,6 @@ Below is the directory structure of manim: ├── family_ops.py # Process family members ├── file_ops.py # Process files and directories ├── images.py # Read image - ├── init_config.py # Configuration guide ├── iterables.py # Functions related to list/dictionary processing ├── paths.py # Curve path ├── rate_functions.py # Some defined rate_functions diff --git a/manimlib/__main__.py b/manimlib/__main__.py index b5877f8e..d2d5cf89 100644 --- a/manimlib/__main__.py +++ b/manimlib/__main__.py @@ -1,9 +1,48 @@ #!/usr/bin/env python +from addict import Dict + from manimlib import __version__ -import manimlib.config -import manimlib.logger -import manimlib.utils.init_config -from manimlib.reload_manager import ReloadManager +from manimlib.config import manim_config +from manimlib.config import parse_cli +import manimlib.extract_scene +from manimlib.window import Window + + +from IPython.terminal.embed import KillEmbedded + + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from argparse import Namespace + + +def run_scenes(): + """ + Runs the scenes in a loop and detects when a scene reload is requested. + """ + # Create a new dict to be able to upate without + # altering global configuration + scene_config = Dict(manim_config.scene) + run_config = manim_config.run + + if run_config.show_in_window: + # Create a reusable window + window = Window(**manim_config.window) + scene_config.update(window=window) + + while True: + try: + # Blocking call since a scene may init an IPython shell() + scenes = manimlib.extract_scene.main(scene_config, run_config) + for scene in scenes: + scene.run() + return + except KillEmbedded: + # Requested via the `exit_raise` IPython runline magic + # by means of the reload_scene() command + pass + except KeyboardInterrupt: + break def main(): @@ -12,18 +51,11 @@ def main(): """ print(f"ManimGL \033[32mv{__version__}\033[0m") - args = manimlib.config.parse_cli() + args = parse_cli() if args.version and args.file is None: return - if args.log_level: - manimlib.logger.log.setLevel(args.log_level) - if args.config: - manimlib.utils.init_config.init_customization() - return - - reload_manager = ReloadManager(args) - reload_manager.run() + run_scenes() if __name__ == "__main__": diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index 2074fbd1..a423465d 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -7,8 +7,7 @@ from PIL import Image from manimlib.camera.camera_frame import CameraFrame from manimlib.constants import BLACK -from manimlib.constants import DEFAULT_FPS -from manimlib.constants import DEFAULT_PIXEL_HEIGHT, DEFAULT_PIXEL_WIDTH +from manimlib.constants import DEFAULT_RESOLUTION from manimlib.constants import FRAME_HEIGHT from manimlib.constants import FRAME_WIDTH from manimlib.mobject.mobject import Mobject @@ -29,10 +28,9 @@ class Camera(object): window: Optional[Window] = None, background_image: Optional[str] = None, frame_config: dict = dict(), - pixel_width: int = DEFAULT_PIXEL_WIDTH, - pixel_height: int = DEFAULT_PIXEL_HEIGHT, - fps: int = DEFAULT_FPS, - # Note: frame height and width will be resized to match the pixel aspect ratio + # Note: frame height and width will be resized to match this resolution aspect ratio + resolution=DEFAULT_RESOLUTION, + fps: int = 30, background_color: ManimColor = BLACK, background_opacity: float = 1.0, # Points in vectorized mobjects with norm greater @@ -47,9 +45,9 @@ class Camera(object): # to set samples to be greater than 0. samples: int = 0, ): - self.background_image = background_image self.window = window - self.default_pixel_shape = (pixel_width, pixel_height) + self.background_image = background_image + self.default_pixel_shape = resolution # Rename? self.fps = fps self.max_allowable_norm = max_allowable_norm self.image_mode = image_mode diff --git a/manimlib/config.py b/manimlib/config.py index 19f731e6..29af8f21 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -5,15 +5,13 @@ import colour import importlib import inspect import os -import screeninfo import sys import yaml - -from functools import lru_cache +from ast import literal_eval +from addict import Dict from manimlib.logger import log from manimlib.utils.dict_ops import merge_dicts_recursively -from manimlib.utils.init_config import init_customization from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -21,6 +19,36 @@ if TYPE_CHECKING: from typing import Optional +def initialize_manim_config() -> Dict: + """ + Return default configuration for various classes in manim, such as + Scene, Window, Camera, and SceneFileWriter, as well as configuration + determining how the scene is run (e.g. written to file or previewed in window). + + The result is initially on the contents of default_config.yml in the manimlib directory, + which can be further updated by a custom configuration file custom_config.yml. + It is further updated based on command line argument. + """ + args = parse_cli() + global_defaults_file = os.path.join(get_manim_dir(), "manimlib", "default_config.yml") + config = merge_dicts_recursively( + load_yaml(global_defaults_file), + load_yaml("custom_config.yml"), # From current working directory + load_yaml(args.config_file) if args.config_file else dict(), + ) + + log.setLevel(args.log_level or config["log_level"]) + + update_directory_config(config) + update_window_config(config, args) + update_camera_config(config, args) + update_file_writer_config(config, args) + update_scene_config(config, args) + update_run_config(config, args) + + return Dict(config) + + def parse_cli(): try: parser = argparse.ArgumentParser() @@ -48,12 +76,12 @@ def parse_cli(): parser.add_argument( "-l", "--low_quality", action="store_true", - help="Render at a low quality (for faster rendering)", + help="Render at 480p", ) parser.add_argument( "-m", "--medium_quality", action="store_true", - help="Render at a medium quality", + help="Render at 720p", ) parser.add_argument( "--hd", @@ -119,11 +147,6 @@ def parse_cli(): action="store_true", help="Show the output file in finder", ) - parser.add_argument( - "--config", - action="store_true", - help="Guide for automatic configuration", - ) parser.add_argument( "--file_name", help="Name for the movie or image file", @@ -137,8 +160,9 @@ def parse_cli(): ) parser.add_argument( "-e", "--embed", - help="Creates a new file where the line `self.embed` is inserted " + \ - "at the corresponding line number" + metavar="LINE_NUMBER", + help="Adds a breakpoint at the inputted file dropping into an " + \ + "interactive iPython session at that point of the code." ) parser.add_argument( "-r", "--resolution", @@ -193,10 +217,101 @@ def parse_cli(): sys.exit(2) -def get_manim_dir(): - manimlib_module = importlib.import_module("manimlib") - manimlib_dir = os.path.dirname(inspect.getabsfile(manimlib_module)) - return os.path.abspath(os.path.join(manimlib_dir, "..")) +def update_directory_config(config: dict): + dir_config = config["directories"] + base = dir_config['base'] + for key, subdir in dir_config['subdirs'].items(): + dir_config[key] = os.path.join(base, subdir) + + +def update_window_config(config: dict, args: Namespace): + window_config = config["window"] + for key in "position", "size": + if window_config.get(key): + window_config[key] = literal_eval(window_config[key]) + if args.full_screen: + window_config["full_screen"] = True + + +def update_camera_config(config: dict, args: Namespace): + camera_config = config["camera"] + arg_resolution = get_resolution_from_args(args, config["resolution_options"]) + camera_config["resolution"] = arg_resolution or literal_eval(camera_config["resolution"]) + if args.fps: + camera_config["fps"] = args.fps + if args.color: + try: + camera_config["background_color"] = colour.Color(args.color) + except Exception: + log.error("Please use a valid color") + log.error(err) + sys.exit(2) + if args.transparent: + camera_config["background_opacity"] = 0.0 + + +def update_file_writer_config(config: dict, args: Namespace): + file_writer_config = config["file_writer"] + file_writer_config.update( + write_to_movie=(not args.skip_animations and args.write_file), + save_last_frame=(args.skip_animations and args.write_file), + save_pngs=args.save_pngs, + png_mode=("RGBA" if args.transparent else "RGB"), + movie_file_extension=(get_file_ext(args)), + output_directory=get_output_directory(args, config), + file_name=args.file_name, + input_file_path=args.file or "", + open_file_upon_completion=args.open, + show_file_location_upon_completion=args.finder, + quiet=args.quiet, + ) + + if args.vcodec: + file_writer_config["video_codec"] = args.vcodec + elif args.transparent: + file_writer_config["video_codec"] = 'prores_ks' + file_writer_config["pixel_format"] = '' + elif args.gif: + file_writer_config["video_codec"] = '' + + if args.pix_fmt: + file_writer_config["pixel_format"] = args.pix_fmt + + +def update_scene_config(config: dict, args: Namespace): + scene_config = config["scene"] + start, end = get_animations_numbers(args) + scene_config.update( + # Note, Scene.__init__ makes use of both manimlib.camera and + # manimlib.file_writer below, so the arguments here are just for + # any future specifications beyond what the global configuration holds + camera_config=dict(), + file_writer_config=dict(), + skip_animations=args.skip_animations, + start_at_animation_number=start, + end_at_animation_number=end, + presenter_mode=args.presenter_mode, + ) + if args.leave_progress_bars: + scene_config["leave_progress_bars"] = True + if args.show_animation_progress: + scene_config["show_animation_progress"] = True + + +def update_run_config(config: dict, args: Namespace): + config["run"] = dict( + file_name=args.file, + embed_line=(int(args.embed) if args.embed is not None else None), + is_reload=False, + prerun=args.prerun, + scene_names=args.scene_names, + quiet=args.quiet or args.write_all, + write_all=args.write_all, + show_in_window=not args.write_file + ) + + +# Helpers for the functions above def load_yaml(file_path: str): @@ -207,22 +322,24 @@ def load_yaml(file_path: str): return {} -@lru_cache -def get_global_config(): - args = parse_cli() - global_defaults_file = os.path.join(get_manim_dir(), "manimlib", "default_config.yml") - config = merge_dicts_recursively( - load_yaml(global_defaults_file), - load_yaml("custom_config.yml"), # From current working directory - load_yaml(args.config_file) if args.config_file else {}, - ) +def get_manim_dir(): + manimlib_module = importlib.import_module("manimlib") + manimlib_dir = os.path.dirname(inspect.getabsfile(manimlib_module)) + return os.path.abspath(os.path.join(manimlib_dir, "..")) - # Set the subdirectories - base = config['directories']['base'] - for key, subdir in config['directories']['subdirs'].items(): - config['directories'][key] = os.path.join(base, subdir) - return config +def get_resolution_from_args(args: Optional[Namespace], resolution_options: dict) -> Optional[tuple[int, int]]: + if args.resolution: + return tuple(map(int, args.resolution.split("x"))) + if args.low_quality: + return literal_eval(resolution_options["low"]) + if args.medium_quality: + return literal_eval(resolution_options["med"]) + if args.hd: + return literal_eval(resolution_options["high"]) + if args.uhd: + return literal_eval(resolution_options["4k"]) + return None def get_file_ext(args: Namespace) -> str: @@ -245,8 +362,8 @@ def get_animations_numbers(args: Namespace) -> tuple[int | None, int | None]: return int(stan), None -def get_output_directory(args: Namespace, global_config: dict) -> str: - dir_config = global_config["directories"] +def get_output_directory(args: Namespace, config: dict) -> str: + dir_config = config["directories"] output_directory = args.video_dir or dir_config["output"] if dir_config["mirror_module_path"] and args.file: to_cut = dir_config["removed_mirror_prefix"] @@ -258,142 +375,5 @@ def get_output_directory(args: Namespace, global_config: dict) -> str: return output_directory -def get_file_writer_config(args: Namespace, global_config: dict) -> dict: - result = { - "write_to_movie": not args.skip_animations and args.write_file, - "save_last_frame": args.skip_animations and args.write_file, - "save_pngs": args.save_pngs, - # If -t is passed in (for transparent), this will be RGBA - "png_mode": "RGBA" if args.transparent else "RGB", - "movie_file_extension": get_file_ext(args), - "output_directory": get_output_directory(args, global_config), - "file_name": args.file_name, - "input_file_path": args.file or "", - "open_file_upon_completion": args.open, - "show_file_location_upon_completion": args.finder, - "quiet": args.quiet, - **global_config["file_writer_config"], - } - - if args.vcodec: - result["video_codec"] = args.vcodec - elif args.transparent: - result["video_codec"] = 'prores_ks' - result["pixel_format"] = '' - elif args.gif: - result["video_codec"] = '' - - if args.pix_fmt: - result["pixel_format"] = args.pix_fmt - - return result - - -def get_resolution(args: Optional[Namespace] = None, global_config: Optional[dict] = None): - args = args or parse_cli() - global_config = global_config or get_global_config() - - camera_resolutions = global_config["camera_resolutions"] - if args.resolution: - resolution = args.resolution - elif args.low_quality: - resolution = camera_resolutions["low"] - elif args.medium_quality: - resolution = camera_resolutions["med"] - elif args.hd: - resolution = camera_resolutions["high"] - elif args.uhd: - resolution = camera_resolutions["4k"] - else: - resolution = camera_resolutions[camera_resolutions["default_resolution"]] - - width_str, height_str = resolution.split("x") - return int(width_str), int(height_str) - - -def get_window_config(args: Namespace, global_config: dict) -> dict: - # Default to making window half the screen size - # but make it full screen if -f is passed in - try: - monitors = screeninfo.get_monitors() - except screeninfo.ScreenInfoError: - # Default fallback - monitors = [screeninfo.Monitor(width=1920, height=1080)] - mon_index = global_config["window_monitor"] - monitor = monitors[min(mon_index, len(monitors) - 1)] - - width, height = get_resolution(args, global_config) - - aspect_ratio = width / height - window_width = monitor.width - if not (args.full_screen or global_config["full_screen"]): - window_width //= 2 - window_height = int(window_width / aspect_ratio) - return dict(size=(window_width, window_height)) - - -def get_camera_config(args: Optional[Namespace] = None, global_config: Optional[dict] = None) -> dict: - args = args or parse_cli() - global_config = global_config or get_global_config() - - width, height = get_resolution(args, global_config) - fps = int(args.fps or global_config["fps"]) - - camera_config = { - "pixel_width": width, - "pixel_height": height, - "fps": fps, - } - - try: - bg_color = args.color or global_config["style"]["background_color"] - camera_config["background_color"] = colour.Color(bg_color) - except ValueError as err: - log.error("Please use a valid color") - log.error(err) - sys.exit(2) - - # If rendering a transparent image/movie, make sure the - # scene has a background opacity of 0 - if args.transparent: - camera_config["background_opacity"] = 0 - - return camera_config - - -def get_scene_config(args: Namespace) -> dict: - """ - Returns a dictionary to be used as key word arguments for Scene - """ - global_config = get_global_config() - camera_config = get_camera_config(args, global_config) - file_writer_config = get_file_writer_config(args, global_config) - start, end = get_animations_numbers(args) - - return { - "file_writer_config": file_writer_config, - "camera_config": camera_config, - "skip_animations": args.skip_animations, - "start_at_animation_number": start, - "end_at_animation_number": end, - "presenter_mode": args.presenter_mode, - "leave_progress_bars": args.leave_progress_bars, - "show_animation_progress": args.show_animation_progress, - "embed_exception_mode": global_config["embed_exception_mode"], - "embed_error_sound": global_config["embed_error_sound"], - } - - -def get_run_config(args: Namespace): - window_config = get_window_config(args, get_global_config()) - return { - "file_name": args.file, - "embed_line": int(args.embed) if args.embed is not None else None, - "is_reload": False, - "prerun": args.prerun, - "scene_names": args.scene_names, - "quiet": args.quiet or args.write_all, - "write_all": args.write_all, - "window_config": window_config, - "show_in_window": not args.write_file - } +# Create global configuration +manim_config: Dict = initialize_manim_config() diff --git a/manimlib/constants.py b/manimlib/constants.py index 4dcdb508..5c4d1b71 100644 --- a/manimlib/constants.py +++ b/manimlib/constants.py @@ -1,44 +1,39 @@ from __future__ import annotations import numpy as np -from manimlib.config import get_resolution - from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import List from manimlib.typing import ManimColor, Vect3 +# See manimlib/default_config.yml +from manimlib.config import manim_config -# TODO, it feels a bit unprincipled to have some global constants -# depend on the output of this function, and for all that configuration -# code to be run merely upon importing from this file. -DEFAULT_RESOLUTION: tuple[int, int] = get_resolution() -DEFAULT_PIXEL_WIDTH = DEFAULT_RESOLUTION[0] -DEFAULT_PIXEL_HEIGHT = DEFAULT_RESOLUTION[1] -DEFAULT_FPS: int = 30 + +DEFAULT_RESOLUTION: tuple[int, int] = manim_config.camera.resolution +DEFAULT_PIXEL_WIDTH: int = DEFAULT_RESOLUTION[0] +DEFAULT_PIXEL_HEIGHT: int = DEFAULT_RESOLUTION[1] # Sizes relevant to default camera frame ASPECT_RATIO: float = DEFAULT_PIXEL_WIDTH / DEFAULT_PIXEL_HEIGHT -FRAME_HEIGHT: float = 8.0 +FRAME_HEIGHT: float = manim_config.sizes.frame_height FRAME_WIDTH: float = FRAME_HEIGHT * ASPECT_RATIO FRAME_SHAPE: tuple[float, float] = (FRAME_WIDTH, FRAME_HEIGHT) FRAME_Y_RADIUS: float = FRAME_HEIGHT / 2 FRAME_X_RADIUS: float = FRAME_WIDTH / 2 -SMALL_BUFF: float = 0.1 -MED_SMALL_BUFF: float = 0.25 -MED_LARGE_BUFF: float = 0.5 -LARGE_BUFF: float = 1 +# Helpful values for positioning mobjects +SMALL_BUFF: float = manim_config.sizes.small_buff +MED_SMALL_BUFF: float = manim_config.sizes.med_small_buff +MED_LARGE_BUFF: float = manim_config.sizes.med_large_buff +LARGE_BUFF: float = manim_config.sizes.large_buff -DEFAULT_MOBJECT_TO_EDGE_BUFFER: float = MED_LARGE_BUFF -DEFAULT_MOBJECT_TO_MOBJECT_BUFFER: float = MED_SMALL_BUFF - - -# In seconds -DEFAULT_WAIT_TIME: float = 1.0 +DEFAULT_MOBJECT_TO_EDGE_BUFF: float = manim_config.sizes.default_mobject_to_edge_buff +DEFAULT_MOBJECT_TO_MOBJECT_BUFF: float = manim_config.sizes.default_mobject_to_mobject_buff +# Standard vectors ORIGIN: Vect3 = np.array([0., 0., 0.]) UP: Vect3 = np.array([0., 1., 0.]) DOWN: Vect3 = np.array([0., -1., 0.]) @@ -63,6 +58,7 @@ BOTTOM: Vect3 = FRAME_Y_RADIUS * DOWN LEFT_SIDE: Vect3 = FRAME_X_RADIUS * LEFT RIGHT_SIDE: Vect3 = FRAME_X_RADIUS * RIGHT +# Angles PI: float = np.pi TAU: float = 2 * PI DEGREES: float = TAU / 360 @@ -70,100 +66,71 @@ DEGREES: float = TAU / 360 # when juxtaposed with expressions like 30 * DEGREES RADIANS: float = 1 -FFMPEG_BIN: str = "ffmpeg" - -JOINT_TYPE_MAP: dict = { - "no_joint": 0, - "auto": 1, - "bevel": 2, - "miter": 3, -} - # Related to Text NORMAL: str = "NORMAL" ITALIC: str = "ITALIC" OBLIQUE: str = "OBLIQUE" BOLD: str = "BOLD" -DEFAULT_STROKE_WIDTH: float = 4 - -# For keyboard interactions -CTRL_SYMBOL: int = 65508 -SHIFT_SYMBOL: int = 65505 -COMMAND_SYMBOL: int = 65517 -DELETE_SYMBOL: int = 65288 -ARROW_SYMBOLS: list[int] = list(range(65361, 65365)) +DEFAULT_STROKE_WIDTH: float = manim_config.vmobject.default_stroke_width # Colors +BLUE_E: ManimColor = manim_config.colors.blue_e +BLUE_D: ManimColor = manim_config.colors.blue_d +BLUE_C: ManimColor = manim_config.colors.blue_c +BLUE_B: ManimColor = manim_config.colors.blue_b +BLUE_A: ManimColor = manim_config.colors.blue_a +TEAL_E: ManimColor = manim_config.colors.teal_e +TEAL_D: ManimColor = manim_config.colors.teal_d +TEAL_C: ManimColor = manim_config.colors.teal_c +TEAL_B: ManimColor = manim_config.colors.teal_b +TEAL_A: ManimColor = manim_config.colors.teal_a +GREEN_E: ManimColor = manim_config.colors.green_e +GREEN_D: ManimColor = manim_config.colors.green_d +GREEN_C: ManimColor = manim_config.colors.green_c +GREEN_B: ManimColor = manim_config.colors.green_b +GREEN_A: ManimColor = manim_config.colors.green_a +YELLOW_E: ManimColor = manim_config.colors.yellow_e +YELLOW_D: ManimColor = manim_config.colors.yellow_d +YELLOW_C: ManimColor = manim_config.colors.yellow_c +YELLOW_B: ManimColor = manim_config.colors.yellow_b +YELLOW_A: ManimColor = manim_config.colors.yellow_a +GOLD_E: ManimColor = manim_config.colors.gold_e +GOLD_D: ManimColor = manim_config.colors.gold_d +GOLD_C: ManimColor = manim_config.colors.gold_c +GOLD_B: ManimColor = manim_config.colors.gold_b +GOLD_A: ManimColor = manim_config.colors.gold_a +RED_E: ManimColor = manim_config.colors.red_e +RED_D: ManimColor = manim_config.colors.red_d +RED_C: ManimColor = manim_config.colors.red_c +RED_B: ManimColor = manim_config.colors.red_b +RED_A: ManimColor = manim_config.colors.red_a +MAROON_E: ManimColor = manim_config.colors.maroon_e +MAROON_D: ManimColor = manim_config.colors.maroon_d +MAROON_C: ManimColor = manim_config.colors.maroon_c +MAROON_B: ManimColor = manim_config.colors.maroon_b +MAROON_A: ManimColor = manim_config.colors.maroon_a +PURPLE_E: ManimColor = manim_config.colors.purple_e +PURPLE_D: ManimColor = manim_config.colors.purple_d +PURPLE_C: ManimColor = manim_config.colors.purple_c +PURPLE_B: ManimColor = manim_config.colors.purple_b +PURPLE_A: ManimColor = manim_config.colors.purple_a +GREY_E: ManimColor = manim_config.colors.grey_e +GREY_D: ManimColor = manim_config.colors.grey_d +GREY_C: ManimColor = manim_config.colors.grey_c +GREY_B: ManimColor = manim_config.colors.grey_b +GREY_A: ManimColor = manim_config.colors.grey_a +WHITE: ManimColor = manim_config.colors.white +BLACK: ManimColor = manim_config.colors.black +GREY_BROWN: ManimColor = manim_config.colors.grey_brown +DARK_BROWN: ManimColor = manim_config.colors.dark_brown +LIGHT_BROWN: ManimColor = manim_config.colors.light_brown +PINK: ManimColor = manim_config.colors.pink +LIGHT_PINK: ManimColor = manim_config.colors.light_pink +GREEN_SCREEN: ManimColor = manim_config.colors.green_screen +ORANGE: ManimColor = manim_config.colors.orange -BLUE_E: ManimColor = "#1C758A" -BLUE_D: ManimColor = "#29ABCA" -BLUE_C: ManimColor = "#58C4DD" -BLUE_B: ManimColor = "#9CDCEB" -BLUE_A: ManimColor = "#C7E9F1" -TEAL_E: ManimColor = "#49A88F" -TEAL_D: ManimColor = "#55C1A7" -TEAL_C: ManimColor = "#5CD0B3" -TEAL_B: ManimColor = "#76DDC0" -TEAL_A: ManimColor = "#ACEAD7" -GREEN_E: ManimColor = "#699C52" -GREEN_D: ManimColor = "#77B05D" -GREEN_C: ManimColor = "#83C167" -GREEN_B: ManimColor = "#A6CF8C" -GREEN_A: ManimColor = "#C9E2AE" -YELLOW_E: ManimColor = "#E8C11C" -YELLOW_D: ManimColor = "#F4D345" -YELLOW_C: ManimColor = "#FFFF00" -YELLOW_B: ManimColor = "#FFEA94" -YELLOW_A: ManimColor = "#FFF1B6" -GOLD_E: ManimColor = "#C78D46" -GOLD_D: ManimColor = "#E1A158" -GOLD_C: ManimColor = "#F0AC5F" -GOLD_B: ManimColor = "#F9B775" -GOLD_A: ManimColor = "#F7C797" -RED_E: ManimColor = "#CF5044" -RED_D: ManimColor = "#E65A4C" -RED_C: ManimColor = "#FC6255" -RED_B: ManimColor = "#FF8080" -RED_A: ManimColor = "#F7A1A3" -MAROON_E: ManimColor = "#94424F" -MAROON_D: ManimColor = "#A24D61" -MAROON_C: ManimColor = "#C55F73" -MAROON_B: ManimColor = "#EC92AB" -MAROON_A: ManimColor = "#ECABC1" -PURPLE_E: ManimColor = "#644172" -PURPLE_D: ManimColor = "#715582" -PURPLE_C: ManimColor = "#9A72AC" -PURPLE_B: ManimColor = "#B189C6" -PURPLE_A: ManimColor = "#CAA3E8" -GREY_E: ManimColor = "#222222" -GREY_D: ManimColor = "#444444" -GREY_C: ManimColor = "#888888" -GREY_B: ManimColor = "#BBBBBB" -GREY_A: ManimColor = "#DDDDDD" -WHITE: ManimColor = "#FFFFFF" -BLACK: ManimColor = "#000000" -GREY_BROWN: ManimColor = "#736357" -DARK_BROWN: ManimColor = "#8B4513" -LIGHT_BROWN: ManimColor = "#CD853F" -PINK: ManimColor = "#D147BD" -LIGHT_PINK: ManimColor = "#DC75CD" -GREEN_SCREEN: ManimColor = "#00FF00" -ORANGE: ManimColor = "#FF862F" - -MANIM_COLORS: List[ManimColor] = [ - BLACK, GREY_E, GREY_D, GREY_C, GREY_B, GREY_A, WHITE, - BLUE_E, BLUE_D, BLUE_C, BLUE_B, BLUE_A, - TEAL_E, TEAL_D, TEAL_C, TEAL_B, TEAL_A, - GREEN_E, GREEN_D, GREEN_C, GREEN_B, GREEN_A, - YELLOW_E, YELLOW_D, YELLOW_C, YELLOW_B, YELLOW_A, - GOLD_E, GOLD_D, GOLD_C, GOLD_B, GOLD_A, - RED_E, RED_D, RED_C, RED_B, RED_A, - MAROON_E, MAROON_D, MAROON_C, MAROON_B, MAROON_A, - PURPLE_E, PURPLE_D, PURPLE_C, PURPLE_B, PURPLE_A, - GREY_BROWN, DARK_BROWN, LIGHT_BROWN, - PINK, LIGHT_PINK, -] +MANIM_COLORS: List[ManimColor] = list(manim_config.colors.values()) # Abbreviated names for the "median" colors BLUE: ManimColor = BLUE_C diff --git a/manimlib/default_config.yml b/manimlib/default_config.yml index 6f4224c5..9e340304 100644 --- a/manimlib/default_config.yml +++ b/manimlib/default_config.yml @@ -1,3 +1,13 @@ +# This file determines the default configuration for how manim is +# run, including names for directories it will write to, default +# parameters for various classes, style choices, etc. To customize +# your own, create a custom_config.yml file in whatever directory +# you are running manim. For 3blue1brown, for instance, mind is +# here: https://github.com/3b1b/videos/blob/master/custom_config.yml + +# Alternatively, you can create it whereever you like, and on running +# manim, pass in `--config_file /path/to/custom/config/file.yml` + directories: # Set this to true if you want the path to video files # to match the directory structure of the path to the @@ -25,19 +35,23 @@ directories: # it stores this saved data to whatever directory appdirs.user_cache_dir("manim") returns, # but here a user can specify a different cache location cache: "" -universal_import_line: "from manimlib import *" -style: - tex_template: "default" - font: "Consolas" - text_alignment: "LEFT" +window: + # The position of window on screen. UR -> Upper Right, and likewise DL -> Down and Left, + # UO would be upper middle, etc. + position_string: UR + # If using multiple monitors, which one should show the window + monitor_index: 0 + # If not full screen, the default to give it half the screen width + full_screen: False + # Other optional specifications that override the above include: + # position: (500, 500) # Specific position, in pixel coordiantes, for upper right corner + # size: (1920, 1080) # Specific size, in pixels +camera: + resolution: (1920, 1080) background_color: "#333333" -# Set the position of preview window, you can use directions, e.g. UL/DR/OL/OO/... -# also, you can also specify the position(pixel) of the upper left corner of -# the window on the monitor, e.g. "960,540" -window_position: UR -window_monitor: 0 -full_screen: False -file_writer_config: + fps: 30 + background_opacity: 1.0 +file_writer: # If break_into_partial_movies is set to True, then many small # files will be written corresponding to each Scene.play and # Scene.wait call, and these files will then be combined @@ -45,17 +59,106 @@ file_writer_config: # easier when working with the broken up scene, which # effectively has cuts at all the places you might want. break_into_partial_movies: False + # What command to use for ffmpeg + ffmpeg_bin: "ffmpeg" + # Parameters to pass into ffmpeg video_codec: "libx264" pixel_format: "yuv420p" saturation: 1.0 gamma: 1.0 -camera_resolutions: - low: "854x480" - med: "1280x720" - high: "1920x1080" - 4k: "3840x2160" - default_resolution: "high" -fps: 30 -embed_exception_mode: "Verbose" -embed_error_sound: False +# Most of the scene configuration will come from CLI arguments, +# but defaults can be set here +scene: + show_animation_progress: False + leave_progress_bars: False + # How long does a scene pause on Scene.wait calls + default_wait_time: 1.0 +vmobject: + default_stroke_width: 4.0 +tex: + # See tex_templates.yml + template: "default" +text: + font: "Consolas" + alignment: "LEFT" +embed: + exception_mode: "Verbose" +resolution_options: + # When the user passes in -l, -m, --hd or --uhd, these are the corresponding + # resolutions + low: (854, 480) + med: (1280, 720) + high: (1920, 1080) + 4k: (3840, 2160) +sizes: + # This determines the scale of the manim coordinate system with respect to + # the viewing frame + frame_height: 8.0 + # These determine the constants SMALL_BUFF, MED_SMALL_BUFF, etc., useful + # for nudging things around and having default spacing values + small_buff: 0.1 + med_small_buff: 0.25 + med_large_buff: 0.5 + large_buff: 1.0 + # Default buffers used in Mobject.next_to or Mobject.to_edge + default_mobject_to_edge_buff: 0.5 + default_mobject_to_mobject_buff: 0.25 +colors: + blue_e: "#1C758A" + blue_d: "#29ABCA" + blue_c: "#58C4DD" + blue_b: "#9CDCEB" + blue_a: "#C7E9F1" + teal_e: "#49A88F" + teal_d: "#55C1A7" + teal_c: "#5CD0B3" + teal_b: "#76DDC0" + teal_a: "#ACEAD7" + green_e: "#699C52" + green_d: "#77B05D" + green_c: "#83C167" + green_b: "#A6CF8C" + green_a: "#C9E2AE" + yellow_e: "#E8C11C" + yellow_d: "#F4D345" + yellow_c: "#FFFF00" + yellow_b: "#FFEA94" + yellow_a: "#FFF1B6" + gold_e: "#C78D46" + gold_d: "#E1A158" + gold_c: "#F0AC5F" + gold_b: "#F9B775" + gold_a: "#F7C797" + red_e: "#CF5044" + red_d: "#E65A4C" + red_c: "#FC6255" + red_b: "#FF8080" + red_a: "#F7A1A3" + maroon_e: "#94424F" + maroon_d: "#A24D61" + maroon_c: "#C55F73" + maroon_b: "#EC92AB" + maroon_a: "#ECABC1" + purple_e: "#644172" + purple_d: "#715582" + purple_c: "#9A72AC" + purple_b: "#B189C6" + purple_a: "#CAA3E8" + grey_e: "#222222" + grey_d: "#444444" + grey_c: "#888888" + grey_b: "#BBBBBB" + grey_a: "#DDDDDD" + white: "#FFFFFF" + black: "#000000" + grey_brown: "#736357" + dark_brown: "#8B4513" + light_brown: "#CD853F" + pink: "#D147BD" + light_pink: "#DC75CD" + green_screen: "#00FF00" + orange: "#FF862F" +# Can be DEBUG / INFO / WARNING / ERROR / CRITICAL +log_level: "INFO" +universal_import_line: "from manimlib import *" ignore_manimlib_modules_on_reload: True diff --git a/manimlib/extract_scene.py b/manimlib/extract_scene.py index 2b78c974..80c61960 100644 --- a/manimlib/extract_scene.py +++ b/manimlib/extract_scene.py @@ -6,7 +6,7 @@ import sys from manimlib.module_loader import ModuleLoader -from manimlib.config import get_global_config +from manimlib.config import manim_config from manimlib.logger import log from manimlib.scene.interactive_scene import InteractiveScene from manimlib.scene.scene import Scene @@ -19,7 +19,7 @@ if TYPE_CHECKING: class BlankScene(InteractiveScene): def construct(self): - exec(get_global_config()["universal_import_line"]) + exec(manim_config.universal_import_line) self.embed() @@ -77,13 +77,13 @@ def compute_total_frames(scene_class, scene_config): pre_scene = scene_class(**pre_config) pre_scene.run() total_time = pre_scene.time - pre_scene.skip_time - return int(total_time * scene_config["camera_config"]["fps"]) + return int(total_time * manim_config.camera.fps) def scene_from_class(scene_class, scene_config, run_config): - fw_config = scene_config["file_writer_config"] - if fw_config["write_to_movie"] and run_config["prerun"]: - fw_config["total_frames"] = compute_total_frames(scene_class, scene_config) + fw_config = manim_config.file_writer + if fw_config.write_to_movie and run_config.prerun: + scene_config.file_writer_config.total_frames = compute_total_frames(scene_class, scene_config) return scene_class(**scene_config) @@ -180,4 +180,7 @@ def main(scene_config, run_config): return [BlankScene(**scene_config)] all_scene_classes = get_scene_classes_from_module(module) - return get_scenes_to_render(all_scene_classes, scene_config, run_config) + scenes = get_scenes_to_render(all_scene_classes, scene_config, run_config) + if len(scenes) == 0: + print("No scenes found to run") + return scenes diff --git a/manimlib/logger.py b/manimlib/logger.py index 5ef12121..de6c2f9d 100644 --- a/manimlib/logger.py +++ b/manimlib/logger.py @@ -11,4 +11,3 @@ logging.basicConfig( ) log = logging.getLogger("manimgl") -log.setLevel("WARNING") diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 63089e43..36cd5c64 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -12,8 +12,8 @@ import moderngl import numbers import numpy as np -from manimlib.constants import DEFAULT_MOBJECT_TO_EDGE_BUFFER -from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFFER +from manimlib.constants import DEFAULT_MOBJECT_TO_EDGE_BUFF +from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFF from manimlib.constants import DOWN, IN, LEFT, ORIGIN, OUT, RIGHT, UP from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS from manimlib.constants import MED_SMALL_BUFF @@ -1055,7 +1055,7 @@ class Mobject(object): def align_on_border( self, direction: Vect3, - buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER + buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFF ) -> Self: """ Direction just needs to be a vector pointing towards side or @@ -1071,14 +1071,14 @@ class Mobject(object): def to_corner( self, corner: Vect3 = LEFT + DOWN, - buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER + buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFF ) -> Self: return self.align_on_border(corner, buff) def to_edge( self, edge: Vect3 = LEFT, - buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER + buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFF ) -> Self: return self.align_on_border(edge, buff) @@ -1086,7 +1086,7 @@ class Mobject(object): self, mobject_or_point: Mobject | Vect3, direction: Vect3 = RIGHT, - buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFFER, + buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFF, aligned_edge: Vect3 = ORIGIN, submobject_to_align: Mobject | None = None, index_of_submobject_to_align: int | slice | None = None, @@ -1117,7 +1117,7 @@ class Mobject(object): space_lengths = [FRAME_X_RADIUS, FRAME_Y_RADIUS] for vect in UP, DOWN, LEFT, RIGHT: dim = np.argmax(np.abs(vect)) - buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_EDGE_BUFFER) + buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_EDGE_BUFF) max_val = space_lengths[dim] - buff edge_center = self.get_edge_center(vect) if np.dot(edge_center, vect) > max_val: diff --git a/manimlib/mobject/shape_matchers.py b/manimlib/mobject/shape_matchers.py index 81911ba7..50cbca98 100644 --- a/manimlib/mobject/shape_matchers.py +++ b/manimlib/mobject/shape_matchers.py @@ -2,7 +2,7 @@ from __future__ import annotations from colour import Color -from manimlib.config import get_global_config +from manimlib.config import manim_config from manimlib.constants import BLACK, RED, YELLOW, WHITE from manimlib.constants import DL, DOWN, DR, LEFT, RIGHT, UL, UR from manimlib.constants import SMALL_BUFF @@ -57,7 +57,7 @@ class BackgroundRectangle(SurroundingRectangle): **kwargs ): if color is None: - color = get_global_config()['style']['background_color'] + color = manim_config.camera.background_color super().__init__( mobject, color=color, diff --git a/manimlib/mobject/svg/brace.py b/manimlib/mobject/svg/brace.py index 9399e73e..4078e687 100644 --- a/manimlib/mobject/svg/brace.py +++ b/manimlib/mobject/svg/brace.py @@ -5,7 +5,7 @@ import copy import numpy as np -from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFFER, SMALL_BUFF +from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFF, SMALL_BUFF from manimlib.constants import DOWN, LEFT, ORIGIN, RIGHT, DL, DR, UL from manimlib.constants import PI from manimlib.animation.composition import AnimationGroup @@ -79,7 +79,7 @@ class Brace(Tex): ) else: mob.move_to(self.get_tip()) - buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_MOBJECT_BUFFER) + buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_MOBJECT_BUFF) shift_distance = mob.get_width() / 2.0 + buff mob.shift(self.get_direction() * shift_distance) return self @@ -116,7 +116,7 @@ class BraceLabel(VMobject): text: str | Iterable[str], brace_direction: np.ndarray = DOWN, label_scale: float = 1.0, - label_buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFFER, + label_buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFF, **kwargs ) -> None: super().__init__(**kwargs) diff --git a/manimlib/mobject/svg/text_mobject.py b/manimlib/mobject/svg/text_mobject.py index 07881ab9..900a6fc5 100644 --- a/manimlib/mobject/svg/text_mobject.py +++ b/manimlib/mobject/svg/text_mobject.py @@ -12,7 +12,7 @@ import pygments import pygments.formatters import pygments.lexers -from manimlib.config import get_global_config +from manimlib.config import manim_config from manimlib.constants import DEFAULT_PIXEL_WIDTH, FRAME_WIDTH from manimlib.constants import NORMAL from manimlib.logger import log @@ -157,14 +157,14 @@ class MarkupText(StringMobject): isolate: Selector = re.compile(r"\w+", re.U), **kwargs ): - default_style = get_global_config()["style"] + text_config = manim_config.text self.text = text self.font_size = font_size self.justify = justify self.indent = indent - self.alignment = alignment or default_style["text_alignment"] + self.alignment = alignment or text_config.alignment self.line_width = line_width - self.font = font or default_style["font"] + self.font = font or text_config.font self.slant = slant self.weight = weight diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 2246420e..2c0f0213 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -8,7 +8,6 @@ from manimlib.constants import GREY_A, GREY_C, GREY_E from manimlib.constants import BLACK from manimlib.constants import DEFAULT_STROKE_WIDTH from manimlib.constants import DEGREES -from manimlib.constants import JOINT_TYPE_MAP from manimlib.constants import ORIGIN, OUT from manimlib.constants import PI from manimlib.constants import TAU @@ -72,6 +71,12 @@ class VMobject(Mobject): make_smooth_after_applying_functions: bool = False # TODO, do we care about accounting for varying zoom levels? tolerance_for_point_equality: float = 1e-8 + joint_type_map: dict = { + "no_joint": 0, + "auto": 1, + "bevel": 2, + "miter": 3, + } def __init__( self, @@ -123,7 +128,7 @@ class VMobject(Mobject): super().init_uniforms() self.uniforms.update( anti_alias_width=self.anti_alias_width, - joint_type=JOINT_TYPE_MAP[self.joint_type], + joint_type=self.joint_type_map[self.joint_type], flat_stroke=float(self.flat_stroke), scale_stroke_with_zoom=float(self.scale_stroke_with_zoom) ) @@ -406,7 +411,7 @@ class VMobject(Mobject): def set_joint_type(self, joint_type: str, recurse: bool = True) -> Self: for mob in self.get_family(recurse): - mob.uniforms["joint_type"] = JOINT_TYPE_MAP[joint_type] + mob.uniforms["joint_type"] = self.joint_type_map[joint_type] return self def get_joint_type(self) -> float: diff --git a/manimlib/module_loader.py b/manimlib/module_loader.py index 50fcf999..a623e0ad 100644 --- a/manimlib/module_loader.py +++ b/manimlib/module_loader.py @@ -6,7 +6,7 @@ import os import sys import sysconfig -from manimlib.config import get_global_config +from manimlib.config import manim_config from manimlib.logger import log Module = importlib.util.types.ModuleType @@ -142,9 +142,12 @@ class ModuleLoader: Only user-defined modules are reloaded, see `is_user_defined_module()`. """ - ignore_manimlib_modules = get_global_config()["ignore_manimlib_modules_on_reload"] + ignore_manimlib_modules = manim_config.ignore_manimlib_modules_on_reload if ignore_manimlib_modules and module.__name__.startswith("manimlib"): return + if module.__name__.startswith("manimlib.config"): + # We don't want to reload global config + return if not hasattr(module, "__dict__"): return diff --git a/manimlib/reload_manager.py b/manimlib/reload_manager.py deleted file mode 100644 index c0cff3e7..00000000 --- a/manimlib/reload_manager.py +++ /dev/null @@ -1,86 +0,0 @@ -from __future__ import annotations - -from typing import Any -from IPython.terminal.embed import KillEmbedded - - -import manimlib.config -import manimlib.extract_scene - -from manimlib.window import Window - - -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from argparse import Namespace - - -class ReloadManager: - """ - Manages the loading and running of scenes and is called directly from the - main entry point of ManimGL. - - The name "reload" comes from the fact that this class handles the - reinitialization of scenes when requested by the user via the `reload()` - command in the IPython shell. - """ - - window = None - is_reload = False - - def __init__(self, cli_args: Namespace): - self.args = cli_args - - def set_new_start_at_line(self, start_at_line): - """ - Sets/Updates the line number to load the scene from when reloading. - """ - self.args.embed = str(start_at_line) - - def run(self): - """ - Runs the scenes in a loop and detects when a scene reload is requested. - """ - while True: - try: - # blocking call since a scene will init an IPython shell() - self.retrieve_scenes_and_run() - return - except KillEmbedded: - # Requested via the `exit_raise` IPython runline magic - # by means of our scene.reload() command - self.note_reload() - except KeyboardInterrupt: - break - - def note_reload(self): - self.is_reload = True - print(" ".join([ - "Reloading interactive session for", - f"\033[96m{self.args.scene_names[0]}\033[0m", - f"at line \033[96m{self.args.embed}\033[0m" - ])) - - def retrieve_scenes_and_run(self): - """ - Creates a new configuration based on the CLI args and runs the scenes. - """ - # Args to Config - scene_config = manimlib.config.get_scene_config(self.args) - scene_config.update(reload_manager=self) - - run_config = manimlib.config.get_run_config(self.args) - run_config.update(is_reload=self.is_reload) - - # Create or reuse window - if run_config["show_in_window"] and not self.window: - self.window = Window(**run_config["window_config"]) - scene_config.update(window=self.window) - - # Scenes - scenes = manimlib.extract_scene.main(scene_config, run_config) - if len(scenes) == 0: - print("No scenes found to run") - - for scene in scenes: - scene.run() diff --git a/manimlib/scene/interactive_scene.py b/manimlib/scene/interactive_scene.py index e1a53ee0..56f620bd 100644 --- a/manimlib/scene/interactive_scene.py +++ b/manimlib/scene/interactive_scene.py @@ -7,7 +7,6 @@ from IPython.core.getipython import get_ipython from pyglet.window import key as PygletWindowKeys from manimlib.animation.fading import FadeIn -from manimlib.constants import ARROW_SYMBOLS, CTRL_SYMBOL, DELETE_SYMBOL, SHIFT_SYMBOL from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, RIGHT, UL, UP, UR from manimlib.constants import FRAME_WIDTH, FRAME_HEIGHT, SMALL_BUFF from manimlib.constants import PI @@ -49,6 +48,15 @@ INFORMATION_KEY = 'i' CURSOR_KEY = 'k' COPY_FRAME_POSITION_KEY = 'p' +# For keyboard interactions + +ARROW_SYMBOLS: list[int] = [ + PygletWindowKeys.LEFT, + PygletWindowKeys.UP, + PygletWindowKeys.RIGHT, + PygletWindowKeys.DOWN, +] + ALL_MODIFIERS = PygletWindowKeys.MOD_CTRL | PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_SHIFT # Note, a lot of the functionality here is still buggy and very much a work in progress. @@ -472,7 +480,7 @@ class InteractiveScene(Scene): self.prepare_grab() elif char == RESIZE_KEY and (modifiers & PygletWindowKeys.MOD_SHIFT): self.prepare_resizing(about_corner=((modifiers & PygletWindowKeys.MOD_SHIFT) > 0)) - elif symbol == SHIFT_SYMBOL: + elif symbol == PygletWindowKeys.LSHIFT: if self.window.is_key_pressed(ord("t")): self.prepare_resizing(about_corner=True) elif char == COLOR_KEY and (modifiers & ALL_MODIFIERS) == 0: @@ -486,7 +494,7 @@ class InteractiveScene(Scene): elif char == "x" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)): self.copy_selection() self.delete_selection() - elif symbol == DELETE_SYMBOL: + elif symbol == PygletWindowKeys.BACKSPACE: self.delete_selection() elif char == "a" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)): self.clear_selection() @@ -527,7 +535,7 @@ class InteractiveScene(Scene): self.is_grabbing = False elif chr(symbol) == INFORMATION_KEY: self.display_information(False) - elif symbol == SHIFT_SYMBOL and self.window.is_key_pressed(ord(RESIZE_KEY)): + elif symbol == PygletWindowKeys.LSHIFT and self.window.is_key_pressed(ord(RESIZE_KEY)): self.prepare_resizing(about_corner=False) # Mouse actions @@ -544,7 +552,7 @@ class InteractiveScene(Scene): if not hasattr(self, "scale_about_point"): return vect = point - self.scale_about_point - if self.window.is_key_pressed(CTRL_SYMBOL): + if self.window.is_key_pressed(PygletWindowKeys.LCTRL): for i in (0, 1): scalar = vect[i] / self.scale_ref_vect[i] self.selection.rescale_to_fit( @@ -589,7 +597,7 @@ class InteractiveScene(Scene): self.handle_grabbing(point) elif self.window.is_key_pressed(ord(RESIZE_KEY)): self.handle_resizing(point) - elif self.window.is_key_pressed(ord(SELECT_KEY)) and self.window.is_key_pressed(SHIFT_SYMBOL): + elif self.window.is_key_pressed(ord(SELECT_KEY)) and self.window.is_key_pressed(PygletWindowKeys.LSHIFT): self.handle_sweeping_selection(point) def on_mouse_drag( diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 5c008f93..9d307a25 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -6,7 +6,6 @@ import random import time from functools import wraps -from IPython.core.getipython import get_ipython from pyglet.window import key as PygletWindowKeys import numpy as np @@ -15,8 +14,7 @@ from tqdm.auto import tqdm as ProgressDisplay from manimlib.animation.animation import prepare_animation from manimlib.camera.camera import Camera from manimlib.camera.camera_frame import CameraFrame -from manimlib.constants import ARROW_SYMBOLS -from manimlib.constants import DEFAULT_WAIT_TIME +from manimlib.config import manim_config from manimlib.event_handler import EVENT_DISPATCHER from manimlib.event_handler.event_type import EventType from manimlib.logger import log @@ -29,6 +27,7 @@ from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.scene.scene_embed import interactive_scene_embed from manimlib.scene.scene_embed import CheckpointManager from manimlib.scene.scene_file_writer import SceneFileWriter +from manimlib.utils.dict_ops import merge_dicts_recursively from manimlib.utils.family_ops import extract_mobject_family_members from manimlib.utils.family_ops import recursive_mobject_remove from manimlib.utils.iterables import batch_by_property @@ -44,7 +43,6 @@ if TYPE_CHECKING: from PIL.Image import Image - from manimlib.reload_manager import ReloadManager from manimlib.animation.animation import Animation @@ -68,33 +66,37 @@ class Scene(object): def __init__( self, + window: Optional[Window] = None, camera_config: dict = dict(), file_writer_config: dict = dict(), skip_animations: bool = False, always_update_mobjects: bool = False, start_at_animation_number: int | None = None, end_at_animation_number: int | None = None, - leave_progress_bars: bool = False, - window: Optional[Window] = None, - reload_manager: Optional[ReloadManager] = None, - presenter_mode: bool = False, show_animation_progress: bool = False, - embed_exception_mode: str = "", - embed_error_sound: bool = False, + leave_progress_bars: bool = False, + presenter_mode: bool = False, + default_wait_time: float = 1.0, ): self.skip_animations = skip_animations self.always_update_mobjects = always_update_mobjects self.start_at_animation_number = start_at_animation_number self.end_at_animation_number = end_at_animation_number + self.show_animation_progress = show_animation_progress self.leave_progress_bars = leave_progress_bars self.presenter_mode = presenter_mode - self.show_animation_progress = show_animation_progress - self.embed_exception_mode = embed_exception_mode - self.embed_error_sound = embed_error_sound - self.reload_manager = reload_manager + self.default_wait_time = default_wait_time - self.camera_config = {**self.default_camera_config, **camera_config} - self.file_writer_config = {**self.default_file_writer_config, **file_writer_config} + self.camera_config = merge_dicts_recursively( + manim_config.camera, # Global default + self.default_camera_config, # Updated configuration that subclasses may specify + camera_config, # Updated configuration from instantiation + ) + self.file_writer_config = merge_dicts_recursively( + manim_config.file_writer, + self.default_file_writer_config, + file_writer_config, + ) self.window = window if self.window: @@ -589,11 +591,13 @@ class Scene(object): def wait( self, - duration: float = DEFAULT_WAIT_TIME, + duration: Optional[float] = None, stop_condition: Callable[[], bool] = None, note: str = None, ignore_presenter_mode: bool = False ): + if duration is None: + duration = self.default_wait_time self.pre_play() self.update_mobjects(dt=0) # Any problems with this? if self.presenter_mode and not self.skip_animations and not ignore_presenter_mode: @@ -839,7 +843,7 @@ class Scene(object): elif char == QUIT_KEY and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)): self.quit_interaction = True # Space or right arrow - elif char == " " or symbol == ARROW_SYMBOLS[2]: + elif char == " " or symbol == PygletWindowKeys.RIGHT: self.hold_on_wait = False def on_resize(self, width: int, height: int) -> None: @@ -854,34 +858,6 @@ class Scene(object): def on_close(self) -> None: pass - def reload(self, start_at_line: int | None = None) -> None: - """ - Reloads the scene just like the `manimgl` command would do with the - same arguments that were provided for the initial startup. This allows - for quick iteration during scene development since we don't have to exit - the IPython kernel and re-run the `manimgl` command again. The GUI stays - open during the reload. - - If `start_at_line` is provided, the scene will be reloaded at that line - number. This corresponds to the `linemarker` param of the - `extract_scene.insert_embed_line_to_module()` method. - - Before reload, the scene is cleared and the entire state is reset, such - that we can start from a clean slate. This is taken care of by the - ReloadManager, which will catch the error raised by the `exit_raise` - magic command that we invoke here. - - Note that we cannot define a custom exception class for this error, - since the IPython kernel will swallow any exception. While we can catch - such an exception in our custom exception handler registered with the - `set_custom_exc` method, we cannot break out of the IPython shell by - this means. - """ - self.reload_manager.set_new_start_at_line(start_at_line) - shell = get_ipython() - if shell: - shell.run_line_magic("exit_raise", "") - def focus(self) -> None: """ Puts focus on the ManimGL window. diff --git a/manimlib/scene/scene_embed.py b/manimlib/scene/scene_embed.py index 41b2d7b7..d5873c2c 100644 --- a/manimlib/scene/scene_embed.py +++ b/manimlib/scene/scene_embed.py @@ -1,12 +1,12 @@ import inspect import pyperclip -import os from IPython.core.getipython import get_ipython from IPython.terminal import pt_inputhooks from IPython.terminal.embed import InteractiveShellEmbed from manimlib.animation.fading import VFadeInThenOut +from manimlib.config import manim_config from manimlib.constants import RED from manimlib.mobject.mobject import Mobject from manimlib.mobject.frame import FullScreenRectangle @@ -39,11 +39,12 @@ def get_ipython_shell_for_embedded_scene(scene): module = ModuleLoader.get_module(caller_frame.f_globals["__file__"]) module.__dict__.update(caller_frame.f_locals) module.__dict__.update(get_shortcuts(scene)) + exception_mode = manim_config.embed.exception_mode return InteractiveShellEmbed( user_module=module, display_banner=False, - xmode=scene.embed_exception_mode + xmode=exception_mode ) @@ -59,12 +60,12 @@ def get_shortcuts(scene): clear=scene.clear, focus=scene.focus, save_state=scene.save_state, - reload=scene.reload, undo=scene.undo, redo=scene.redo, i2g=scene.i2g, i2m=scene.i2m, checkpoint_paste=scene.checkpoint_paste, + reload=reload_scene # Defined below ) @@ -95,8 +96,6 @@ def ensure_flash_on_error(shell, scene): def custom_exc(shell, etype, evalue, tb, tb_offset=None): # Show the error don't just swallow it shell.showtraceback((etype, evalue, tb), tb_offset=tb_offset) - if scene.embed_error_sound: - os.system("printf '\a'") rect = FullScreenRectangle().set_stroke(RED, 30).set_fill(opacity=0) rect.fix_in_frame() scene.play(VFadeInThenOut(rect, run_time=0.5)) @@ -104,6 +103,43 @@ def ensure_flash_on_error(shell, scene): shell.set_custom_exc((Exception,), custom_exc) +def reload_scene(embed_line: int | None = None) -> None: + """ + Reloads the scene just like the `manimgl` command would do with the + same arguments that were provided for the initial startup. This allows + for quick iteration during scene development since we don't have to exit + the IPython kernel and re-run the `manimgl` command again. The GUI stays + open during the reload. + + If `embed_line` is provided, the scene will be reloaded at that line + number. This corresponds to the `linemarker` param of the + `extract_scene.insert_embed_line_to_module()` method. + + Before reload, the scene is cleared and the entire state is reset, such + that we can start from a clean slate. This is taken care of by the + run_scenes function in __main__.py, which will catch the error raised by the + `exit_raise` magic command that we invoke here. + + Note that we cannot define a custom exception class for this error, + since the IPython kernel will swallow any exception. While we can catch + such an exception in our custom exception handler registered with the + `set_custom_exc` method, we cannot break out of the IPython shell by + this means. + """ + shell = get_ipython() + if not shell: + return + + # Update the global run configuration. + run_config = manim_config.run + run_config.is_reload = True + if embed_line: + run_config.embed_line = embed_line + + print("Reloading...") + shell.run_line_magic("exit_raise", "") + + class CheckpointManager: checkpoint_states: dict[str, list[tuple[Mobject, Mobject]]] = dict() diff --git a/manimlib/scene/scene_file_writer.py b/manimlib/scene/scene_file_writer.py index f98d68c8..dcbf5e2f 100644 --- a/manimlib/scene/scene_file_writer.py +++ b/manimlib/scene/scene_file_writer.py @@ -11,7 +11,6 @@ from pydub import AudioSegment from tqdm.auto import tqdm as ProgressDisplay from pathlib import Path -from manimlib.constants import FFMPEG_BIN from manimlib.logger import log from manimlib.mobject.mobject import Mobject from manimlib.utils.file_ops import add_extension_if_not_present @@ -49,6 +48,8 @@ class SceneFileWriter(object): quiet: bool = False, total_frames: int = 0, progress_description_len: int = 40, + # Name of the binary used for ffmpeg + ffmpeg_bin: str = "ffmpeg", video_codec: str = "libx264", pixel_format: str = "yuv420p", saturation: float = 1.0, @@ -70,6 +71,7 @@ class SceneFileWriter(object): self.quiet = quiet self.total_frames = total_frames self.progress_description_len = progress_description_len + self.ffmpeg_bin = ffmpeg_bin self.video_codec = video_codec self.pixel_format = pixel_format self.saturation = saturation @@ -236,7 +238,7 @@ class SceneFileWriter(object): vf_arg += f',eq=saturation={self.saturation}:gamma={self.gamma}' command = [ - FFMPEG_BIN, + self.ffmpeg_bin, '-y', # overwrite output file if it exists '-f', 'rawvideo', '-s', f'{width}x{height}', # size of one frame @@ -358,7 +360,7 @@ class SceneFileWriter(object): movie_file_path = self.get_movie_file_path() commands = [ - FFMPEG_BIN, + self.ffmpeg_bin, '-y', # overwrite output file if it exists '-f', 'concat', '-safe', '0', @@ -385,7 +387,7 @@ class SceneFileWriter(object): ) temp_file_path = stem + "_temp" + ext commands = [ - FFMPEG_BIN, + self.ffmpeg_bin, "-i", movie_file_path, "-i", sound_file_path, '-y', # overwrite output file if it exists diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index 6d4cc4ca..f9f1ce17 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -10,7 +10,7 @@ import numpy as np from functools import lru_cache from manimlib.config import parse_cli -from manimlib.config import get_camera_config +from manimlib.config import manim_config from manimlib.utils.shaders import get_shader_code_from_file from manimlib.utils.shaders import get_shader_program from manimlib.utils.shaders import image_path_to_texture @@ -410,8 +410,7 @@ class VShaderWrapper(ShaderWrapper): which can display that texture as a simple quad onto a screen, along with the rgb value which is meant to be discarded. """ - cam_config = get_camera_config() - size = (cam_config['pixel_width'], cam_config['pixel_height']) + size = manim_config.camera.resolution double_size = (2 * size[0], 2 * size[1]) # Important to make sure dtype is floating point (not fixed point) diff --git a/manimlib/utils/directories.py b/manimlib/utils/directories.py index e4a2bc03..478f7108 100644 --- a/manimlib/utils/directories.py +++ b/manimlib/utils/directories.py @@ -5,13 +5,13 @@ import tempfile import appdirs -from manimlib.config import get_global_config +from manimlib.config import manim_config from manimlib.config import get_manim_dir from manimlib.utils.file_ops import guarantee_existence def get_directories() -> dict[str, str]: - return get_global_config()["directories"] + return manim_config.directories def get_cache_dir() -> str: diff --git a/manimlib/utils/init_config.py b/manimlib/utils/init_config.py deleted file mode 100644 index c91fd7e4..00000000 --- a/manimlib/utils/init_config.py +++ /dev/null @@ -1,164 +0,0 @@ -from __future__ import annotations - -import importlib -import inspect -import os -import yaml - -from rich import box -from rich.console import Console -from rich.prompt import Confirm -from rich.prompt import Prompt -from rich.rule import Rule -from rich.table import Table - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any - - -def get_manim_dir() -> str: - manimlib_module = importlib.import_module("manimlib") - manimlib_dir = os.path.dirname(inspect.getabsfile(manimlib_module)) - return os.path.abspath(os.path.join(manimlib_dir, "..")) - - -def remove_empty_value(dictionary: dict[str, Any]) -> None: - for key in list(dictionary.keys()): - if dictionary[key] == "": - dictionary.pop(key) - elif isinstance(dictionary[key], dict): - remove_empty_value(dictionary[key]) - - -def init_customization() -> None: - configuration = { - "directories": { - "mirror_module_path": False, - "base": "", - "subdirs": { - "output": "videos", - "raster_images": "raster_images", - "vector_images": "vector_images", - "sounds": "sounds", - "data": "data", - "downloads": "downloads", - } - }, - "universal_import_line": "from manimlib import *", - "style": { - "tex_template": "", - "font": "Consolas", - "background_color": "", - }, - "window_position": "UR", - "window_monitor": 0, - "full_screen": False, - "break_into_partial_movies": False, - "camera_resolutions": { - "low": "854x480", - "medium": "1280x720", - "high": "1920x1080", - "4k": "3840x2160", - "default_resolution": "", - }, - "fps": 30, - } - - console = Console() - console.print(Rule("[bold]Configuration Guide[/bold]")) - # print("Initialize configuration") - try: - scope = Prompt.ask( - " Select the scope of the configuration", - choices=["global", "local"], - default="local" - ) - - console.print("[bold]Directories:[/bold]") - dir_config = configuration["directories"] - dir_config["base"] = Prompt.ask( - " What base directory should manim use for reading/writing video and images? [prompt.default](optional, default is none)", - default="", - show_default=False - ) - dir_config["subdirs"]["output"] = Prompt.ask( - " Within that base directory, which subdirectory should manim [bold]output[/bold] video and image files to?" + \ - " [prompt.default](optional, default is \"videos\")", - default="videos", - show_default=False - ) - dir_config["subdirs"]["raster_images"] = Prompt.ask( - " Within that base directory, which subdirectory should manim look for raster images (.png, .jpg)" + \ - " [prompt.default](optional, default is \"raster_images\")", - default="raster_images", - show_default=False - ) - dir_config["subdirs"]["vector_images"] = Prompt.ask( - " Within that base directory, which subdirectory should manim look for raster images (.svg, .xdv)" + \ - " [prompt.default](optional, default is \"vector_images\")", - default="vector_images", - show_default=False - ) - dir_config["subdirs"]["sounds"] = Prompt.ask( - " Within that base directory, which subdirectory should manim look for sound files (.mp3, .wav)" + \ - " [prompt.default](optional, default is \"sounds\")", - default="sounds", - show_default=False - ) - dir_config["subdirs"]["downloads"] = Prompt.ask( - " Within that base directory, which subdirectory should manim output downloaded files" + \ - " [prompt.default](optional, default is \"downloads\")", - default="downloads", - show_default=False - ) - - console.print("[bold]Styles:[/bold]") - style_config = configuration["style"] - tex_template = Prompt.ask( - " Select a TeX template to compile a LaTeX source file", - default="default" - ) - style_config["tex_template"] = tex_template - style_config["background_color"] = Prompt.ask( - " Which [bold]background color[/bold] do you want [italic](hex code)", - default="#333333" - ) - - console.print("[bold]Camera qualities:[/bold]") - table = Table( - "low", "medium", "high", "ultra_high", - title="Four defined qualities", - box=box.ROUNDED - ) - table.add_row("480p15", "720p30", "1080p60", "2160p60") - console.print(table) - configuration["camera_resolutions"]["default_resolution"] = Prompt.ask( - " Which one to choose as the default rendering quality", - choices=["low", "medium", "high", "ultra_high"], - default="high" - ) - - write_to_file = Confirm.ask( - "\n[bold]Are you sure to write these configs to file?[/bold]", - default=True - ) - if not write_to_file: - raise KeyboardInterrupt - - global_file_name = os.path.join(get_manim_dir(), "manimlib", "default_config.yml") - if scope == "global": - file_name = global_file_name - else: - if os.path.exists(global_file_name): - remove_empty_value(configuration) - file_name = os.path.join(os.getcwd(), "custom_config.yml") - with open(file_name, "w", encoding="utf-8") as f: - yaml.dump(configuration, f) - - console.print(f"\n:rocket: You have successfully set up a {scope} configuration file!") - console.print(f"You can manually modify it in: [cyan]`{file_name}`[/cyan]") - - except KeyboardInterrupt: - console.print("\n[green]Exit configuration guide[/green]") diff --git a/manimlib/utils/tex_file_writing.py b/manimlib/utils/tex_file_writing.py index 1f2ea4fe..7726312a 100644 --- a/manimlib/utils/tex_file_writing.py +++ b/manimlib/utils/tex_file_writing.py @@ -10,15 +10,12 @@ from pathlib import Path import tempfile from manimlib.utils.cache import cache_on_disk -from manimlib.config import get_global_config +from manimlib.config import manim_config from manimlib.config import get_manim_dir from manimlib.logger import log from manimlib.utils.simple_functions import hash_string -SAVED_TEX_CONFIG = {} - - def get_tex_template_config(template_name: str) -> dict[str, str]: name = template_name.replace(" ", "_").lower() template_path = os.path.join(get_manim_dir(), "manimlib", "tex_templates.yml") @@ -33,7 +30,8 @@ def get_tex_template_config(template_name: str) -> dict[str, str]: return templates_dict[name] -def get_tex_config() -> dict[str, str]: +@lru_cache +def get_tex_config(template: str = "") -> dict[str, str]: """ Returns a dict which should look something like this: { @@ -42,16 +40,13 @@ def get_tex_config() -> dict[str, str]: "preamble": "..." } """ - # Only load once, then save thereafter - if not SAVED_TEX_CONFIG: - template_name = get_global_config()["style"]["tex_template"] - template_config = get_tex_template_config(template_name) - SAVED_TEX_CONFIG.update({ - "template": template_name, - "compiler": template_config["compiler"], - "preamble": template_config["preamble"] - }) - return SAVED_TEX_CONFIG + template = template or manim_config.tex.template + template_config = get_tex_template_config(template) + return { + "template": template, + "compiler": template_config["compiler"], + "preamble": template_config["preamble"] + } def get_full_tex(content: str, preamble: str = ""): @@ -94,17 +89,12 @@ def latex_to_svg( message = message[:max_message_len - 3] + "..." print(message, end="\r") - tex_config = get_tex_config() - if template and template != tex_config["template"]: - tex_config = get_tex_template_config(template) - + tex_config = get_tex_config(template) compiler = tex_config["compiler"] if compiler == "latex": - program = "latex" dvi_ext = ".dvi" elif compiler == "xelatex": - program = "xelatex -no-pdf" dvi_ext = ".xdv" else: raise NotImplementedError(f"Compiler '{compiler}' is not implemented") @@ -119,18 +109,18 @@ def latex_to_svg( dvi_path = base_path + dvi_ext # Write tex file - with open(tex_path, "w", encoding="utf-8") as tex_file: - tex_file.write(full_tex) + Path(tex_path).write_text(full_tex) # Run latex compiler process = subprocess.run( [ - program.split()[0], # Split for xelatex case + compiler, + "-no-pdf", "-interaction=batchmode", "-halt-on-error", - "-output-directory=" + temp_dir, + f"-output-directory={temp_dir}", tex_path - ] + (["--no-pdf"] if compiler == "xelatex" else []), + ], capture_output=True, text=True ) diff --git a/manimlib/window.py b/manimlib/window.py index e342b83d..c711e04b 100644 --- a/manimlib/window.py +++ b/manimlib/window.py @@ -5,10 +5,10 @@ import numpy as np import moderngl_window as mglw from moderngl_window.context.pyglet.window import Window as PygletWindow from moderngl_window.timers.clock import Timer -from screeninfo import get_monitors from functools import wraps +import screeninfo -from manimlib.config import get_global_config +from manimlib.constants import ASPECT_RATIO from manimlib.constants import FRAME_SHAPE from typing import TYPE_CHECKING @@ -30,16 +30,21 @@ class Window(PygletWindow): def __init__( self, scene: Optional[Scene] = None, - size: tuple[int, int] = (1280, 720), + position_string: str = "UR", + monitor_index: int = 1, + full_screen: bool = False, + size: Optional[tuple[int, int]] = None, + position: Optional[tuple[int, int]] = None, samples: int = 0 ): - super().__init__(size=size, samples=samples) - self.scene = scene - self.default_size = size - self.default_position = self.find_initial_position(size) + self.monitor = self.get_monitor(monitor_index) + self.default_size = size or self.get_default_size(full_screen) + self.default_position = position or self.position_from_string(position_string) self.pressed_keys = set() - self.size = size + + super().__init__(samples=samples) + self.to_default_position() if self.scene: self.init_for_scene(scene) @@ -64,7 +69,31 @@ class Window(PygletWindow): mglw.activate_context(window=self, ctx=self.ctx) self.timer.start() - self.to_default_position() + self.focus() + + def get_monitor(self, index): + try: + monitors = screeninfo.get_monitors() + return monitors[min(index, len(monitors) - 1)] + except screeninfo.ScreenInfoError: + # Default fallback + return screeninfo.Monitor(width=1920, height=1080) + + def get_default_size(self, full_screen=False): + width = self.monitor.width // (1 if full_screen else 2) + height = int(width // ASPECT_RATIO) + return (width, height) + + def position_from_string(self, position_string): + # Alternatively, it might be specified with a string like + # UR, OO, DL, etc. specifying what corner it should go to + char_to_n = {"L": 0, "U": 0, "O": 1, "R": 2, "D": 2} + size = self.default_size + width_diff = self.monitor.width - size[0] + height_diff = self.monitor.height - size[1] + x_step = char_to_n[position_string[1]] * width_diff // 2 + y_step = char_to_n[position_string[0]] * height_diff // 2 + return (self.monitor.x + x_step, -self.monitor.y + y_step) def focus(self): """ @@ -77,6 +106,8 @@ class Window(PygletWindow): """ self._window.set_visible(False) self._window.set_visible(True) + # This line seems to resync the viewport + self.on_resize(*self.size) def to_default_position(self): self.position = self.default_position @@ -86,28 +117,6 @@ class Window(PygletWindow): self.size = (w - 1, h - 1) self.size = (w, h) - def find_initial_position(self, size: tuple[int, int]) -> tuple[int, int]: - global_config = get_global_config() - custom_position = global_config["window_position"] - mon_index = global_config["window_monitor"] - monitors = get_monitors() - monitor = monitors[min(mon_index, len(monitors) - 1)] - window_width, window_height = size - # Position might be specified with a string of the form - # x,y for integers x and y - if "," in custom_position: - return tuple(map(int, custom_position.split(","))) - - # Alternatively, it might be specified with a string like - # UR, OO, DL, etc. specifying what corner it should go to - char_to_n = {"L": 0, "U": 0, "O": 1, "R": 2, "D": 2} - width_diff = monitor.width - window_width - height_diff = monitor.height - window_height - return ( - monitor.x + char_to_n[custom_position[1]] * width_diff // 2, - -monitor.y + char_to_n[custom_position[0]] * height_diff // 2, - ) - # Delegate event handling to scene def pixel_coords_to_space_coords( self, diff --git a/requirements.txt b/requirements.txt index b0ebbb36..38a547fb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +addict appdirs colour diskcache @@ -22,7 +23,6 @@ screeninfo skia-pathops svgelements>=1.8.1 sympy -tempfile tqdm typing-extensions; python_version < "3.11" validators diff --git a/setup.cfg b/setup.cfg index 2aee703c..33e6991a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,6 +29,7 @@ classifiers = packages = find: include_package_data = True install_requires = + addict appdirs colour diskcache @@ -53,7 +54,6 @@ install_requires = skia-pathops svgelements>=1.8.1 sympy - tempfile tqdm typing-extensions; python_version < "3.11" validators