From 9cba5f7d3a52d898b8e570b92471d059d84e96bf Mon Sep 17 00:00:00 2001 From: leotrs Date: Mon, 25 May 2020 15:58:58 -0400 Subject: [PATCH 01/38] move a bunch of variables from constants.py into the config.cfg file, where the user can change them if needed --- manim/config.py | 13 ++++- manim/constants.py | 129 +++++++++++-------------------------------- manim/dirs.py | 4 -- manim/utils/color.py | 2 +- 4 files changed, 43 insertions(+), 105 deletions(-) delete mode 100644 manim/dirs.py diff --git a/manim/config.py b/manim/config.py index b511fe342d..02660950e4 100644 --- a/manim/config.py +++ b/manim/config.py @@ -1,3 +1,12 @@ +""" +config.py +--------- + +Process the files constants.py, config.cfg, and the command line arguments +into a single config object. + +""" + import argparse import colour import os @@ -36,7 +45,7 @@ def parse_cli(): help="Save the last frame", ) parser.add_argument( - "--dry_run", + "--dry_run", action="store_true", help= "Do a dry run (render scenes but generate no output files)", ) @@ -269,7 +278,7 @@ def initialize_directories(config): if not config["video_dir"] or dirs.VIDEO_DIR: dir_config["video_dir"] = os.path.join(dir_config["media_dir"], "videos") - + for folder in [dir_config["video_dir"], dir_config["tex_dir"], dir_config["text_dir"]]: if folder != "" and not os.path.exists(folder): os.makedirs(folder) diff --git a/manim/constants.py b/manim/constants.py index c1b2e8687b..086899062f 100644 --- a/manim/constants.py +++ b/manim/constants.py @@ -1,7 +1,7 @@ import numpy as np import os -from .logger import logger +# Messages NOT_SETTING_FONT_MSG=''' You haven't set font. If you are not using English, this may cause text rendering problem. @@ -13,26 +13,6 @@ class MyText(Text): 'font': 'My Font' } ''' -START_X = 30 -START_Y = 20 -NORMAL = 'NORMAL' -ITALIC = 'ITALIC' -OBLIQUE = 'OBLIQUE' -BOLD = 'BOLD' - -TEX_USE_CTEX = False -TEX_TEXT_TO_REPLACE = "YourTextHere" -TEMPLATE_TEX_FILE = os.path.join( - os.path.dirname(os.path.realpath(__file__)), - "tex_template.tex" if not TEX_USE_CTEX else "ctex_template.tex" -) -with open(TEMPLATE_TEX_FILE, "r") as infile: - TEMPLATE_TEXT_FILE_BODY = infile.read() - TEMPLATE_TEX_FILE_BODY = TEMPLATE_TEXT_FILE_BODY.replace( - TEX_TEXT_TO_REPLACE, - "\\begin{align*}\n" + TEX_TEXT_TO_REPLACE + "\n\\end{align*}", - ) - SCENE_NOT_FOUND_MESSAGE = """ {} is not in the script """ @@ -41,70 +21,17 @@ class MyText(Text): (Use comma separated list for multiple entries) Choice(s): """ INVALID_NUMBER_MESSAGE = "Invalid scene numbers have been specified. Aborting." - NO_SCENE_MESSAGE = """ There are no scenes inside that module """ -# There might be other configuration than pixel shape later... -FOURK_CAMERA_CONFIG = { - "pixel_height": 2160, - "pixel_width": 3840, - "frame_rate": 60, -} - -PRODUCTION_QUALITY_CAMERA_CONFIG = { - "pixel_height": 1440, - "pixel_width": 2560, - "frame_rate": 60, -} - -HIGH_QUALITY_CAMERA_CONFIG = { - "pixel_height": 1080, - "pixel_width": 1920, - "frame_rate": 60, -} - -MEDIUM_QUALITY_CAMERA_CONFIG = { - "pixel_height": 720, - "pixel_width": 1280, - "frame_rate": 30, -} - -LOW_QUALITY_CAMERA_CONFIG = { - "pixel_height": 480, - "pixel_width": 854, - "frame_rate": 15, -} - -DEFAULT_PIXEL_HEIGHT = PRODUCTION_QUALITY_CAMERA_CONFIG["pixel_height"] -DEFAULT_PIXEL_WIDTH = PRODUCTION_QUALITY_CAMERA_CONFIG["pixel_width"] -DEFAULT_FRAME_RATE = 60 - -DEFAULT_POINT_DENSITY_2D = 25 -DEFAULT_POINT_DENSITY_1D = 250 - -DEFAULT_STROKE_WIDTH = 4 - -FRAME_HEIGHT = 8.0 -FRAME_WIDTH = FRAME_HEIGHT * DEFAULT_PIXEL_WIDTH / DEFAULT_PIXEL_HEIGHT -FRAME_Y_RADIUS = FRAME_HEIGHT / 2 -FRAME_X_RADIUS = FRAME_WIDTH / 2 - -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 -DEFAULT_MOBJECT_TO_MOBJECT_BUFFER = MED_SMALL_BUFF - - -# All in seconds -DEFAULT_POINTWISE_FUNCTION_RUN_TIME = 3.0 -DEFAULT_WAIT_TIME = 1.0 - +# Cairo stuff +NORMAL = 'NORMAL' +ITALIC = 'ITALIC' +OBLIQUE = 'OBLIQUE' +BOLD = 'BOLD' +# Geometry: directions ORIGIN = np.array((0., 0., 0.)) UP = np.array((0., 1., 0.)) DOWN = np.array((0., -1., 0.)) @@ -112,25 +39,44 @@ class MyText(Text): LEFT = np.array((-1., 0., 0.)) IN = np.array((0., 0., -1.)) OUT = np.array((0., 0., 1.)) + +# Geometry: axes X_AXIS = np.array((1., 0., 0.)) Y_AXIS = np.array((0., 1., 0.)) Z_AXIS = np.array((0., 0., 1.)) -# Useful abbreviations for diagonals +# Geometry: useful abbreviations for diagonals UL = UP + LEFT UR = UP + RIGHT DL = DOWN + LEFT DR = DOWN + RIGHT +# Geometry: sides TOP = FRAME_Y_RADIUS * UP BOTTOM = FRAME_Y_RADIUS * DOWN LEFT_SIDE = FRAME_X_RADIUS * LEFT RIGHT_SIDE = FRAME_X_RADIUS * RIGHT +# Tex stuff +TEX_USE_CTEX = False +TEX_TEXT_TO_REPLACE = "YourTextHere" +TEMPLATE_TEX_FILE = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + "tex_template.tex" if not TEX_USE_CTEX else "ctex_template.tex" +) +with open(TEMPLATE_TEX_FILE, "r") as infile: + TEMPLATE_TEXT_FILE_BODY = infile.read() + TEMPLATE_TEX_FILE_BODY = TEMPLATE_TEXT_FILE_BODY.replace( + TEX_TEXT_TO_REPLACE, + "\\begin{align*}\n" + TEX_TEXT_TO_REPLACE + "\n\\end{align*}", + ) + +# Mathematical constants PI = np.pi TAU = 2 * PI DEGREES = TAU / 360 +# ffmpeg stuff FFMPEG_BIN = "ffmpeg" # Colors @@ -194,25 +140,13 @@ class MyText(Text): "GREEN_SCREEN": "#00FF00", "ORANGE": "#FF862F", } +COLOR_MAP.update({name.replace("_C", ""): COLOR_MAP[name] + for name in COLOR_MAP + if name.endswith("_C")}) PALETTE = list(COLOR_MAP.values()) locals().update(COLOR_MAP) -for name in [s for s in list(COLOR_MAP.keys()) if s.endswith("_C")]: - locals()[name.replace("_C", "")] = locals()[name] -# Streaming related configuration -LIVE_STREAM_NAME = "LiveStream" -TWITCH_STREAM_KEY = "YOUR_STREAM_KEY" -STREAMING_PROTOCOL = "tcp" -STREAMING_IP = "127.0.0.1" -STREAMING_PORT = "2000" -STREAMING_CLIENT = "ffplay" -STREAMING_URL = f"{STREAMING_PROTOCOL}://{STREAMING_IP}:{STREAMING_PORT}?listen" -STREAMING_CONSOLE_BANNER = """ -Manim is now running in streaming mode. Stream animations by passing -them to manim.play(), e.g. ->>> c = Circle() ->>> manim.play(ShowCreation(c)) -""" +# Settings for CodeMobject code_languages_list = {"abap": "abap", "as": "as", "as3": "as3", "ada": "ada", "antlr": "antlr", "antlr_as": "antlr-as", "antlr_csharp": "antlr-csharp", "antlr_cpp": "antlr-cpp", "antlr_java": "antlr-java", @@ -313,7 +247,6 @@ class MyText(Text): "xml_smarty": "xml+smarty", "xml_velocity": "xml+velocity", "xquery": "xquery", "xslt": "xslt", "xtend": "xtend", "yaml": "yaml"} - code_styles_list = {0: "autumn", 1: "borland", 2: "bw", 3: "colorful", 4: "default", 5: "emacs", 6: "friendly", 7: "fruity", 8: "manni", 9: "monokai", 10: "murphy", 11: "native", 12: "pastie", 13: "perldoc", 14: "rrt", 15: "tango", 16: "trac", 17: "vim", 18: "vs"} diff --git a/manim/dirs.py b/manim/dirs.py deleted file mode 100644 index 9d9d5cf3d0..0000000000 --- a/manim/dirs.py +++ /dev/null @@ -1,4 +0,0 @@ -MEDIA_DIR = r"" -VIDEO_DIR = r"" -TEX_DIR = r"" -TEXT_DIR = r"" diff --git a/manim/utils/color.py b/manim/utils/color.py index 48f9325862..eae9ca2e4a 100644 --- a/manim/utils/color.py +++ b/manim/utils/color.py @@ -108,4 +108,4 @@ def get_shaded_rgb(rgb, point, unit_normal_vect, light_source): factor *= 0.5 result = rgb + factor clip_in_place(rgb + factor, 0, 1) - return result \ No newline at end of file + return result From b22135f335f92cef49b9ae2e820f8daa09d8ac51 Mon Sep 17 00:00:00 2001 From: leotrs Date: Mon, 25 May 2020 15:59:16 -0400 Subject: [PATCH 02/38] Prefer the use of COLOR_MAP over PALETTE --- manim/utils/color.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manim/utils/color.py b/manim/utils/color.py index eae9ca2e4a..5b06292e9d 100644 --- a/manim/utils/color.py +++ b/manim/utils/color.py @@ -3,7 +3,7 @@ from colour import Color import numpy as np -from ..constants import PALETTE +from ..constants import COLOR_MAP from ..constants import WHITE from ..utils.bezier import interpolate from ..utils.simple_functions import clip_in_place @@ -98,7 +98,7 @@ def random_bright_color(): def random_color(): - return random.choice(PALETTE) + return random.choice(list(COLOR_MAP.values())) def get_shaded_rgb(rgb, point, unit_normal_vect, light_source): From 97431fc098a69d8ad376c6bfeb73ed3f45e8b02e Mon Sep 17 00:00:00 2001 From: leotrs Date: Mon, 25 May 2020 15:59:25 -0400 Subject: [PATCH 03/38] add initial config.cfg file --- manim/config.cfg | 83 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 manim/config.cfg diff --git a/manim/config.cfg b/manim/config.cfg new file mode 100644 index 0000000000..7c199022f3 --- /dev/null +++ b/manim/config.cfg @@ -0,0 +1,83 @@ +[DEFAULT] + +# Geometry +START_X = 30 +START_Y = 20 + +# Frame config +FRAME_HEIGHT = 8.0 +FRAME_WIDTH = FRAME_HEIGHT * DEFAULT_PIXEL_WIDTH / DEFAULT_PIXEL_HEIGHT +FRAME_Y_RADIUS = FRAME_HEIGHT / 2 +FRAME_X_RADIUS = FRAME_WIDTH / 2 + +# Default buffers (padding) +SMALL_BUFF = 0.1 +MED_SMALL_BUFF = 0.25 +MED_LARGE_BUFF = 0.5 +LARGE_BUFF = 1 +MOBJECT_TO_EDGE_BUFFER = MED_LARGE_BUFF +MOBJECT_TO_MOBJECT_BUFFER = MED_SMALL_BUFF + +# Times in seconds +POINTWISE_FUNCTION_RUN_TIME = 3.0 +WAIT_TIME = 1.0 + +# Misc +POINT_DENSITY_2D = 25 +POINT_DENSITY_1D = 250 +STROKE_WIDTH = 4 + +# Default quality config +FRAME_RATE = 60 +PIXEL_HEIGHT = 1440 +PIXEL_WIDTH = 2560 + + +[production-quality] + PIXEL_HEIGHT = 1440 + PIXEL_WIDTH = 2560 + FRAME_RATE = 60 + + +[fourk-quality] + PIXEL_HEIGHT = 2160 + PIXEL_WIDTH = 3840 + FRAME_RATE = 60 + + +[high-quality] + PIXEL_HEIGHT = 1080 + PIXEL_WIDTH = 1920 + FRAME_RATE = 60 + + +[medium-quality] + PIXEL_HEIGHT = 720 + PIXEL_WIDTH = 1280 + FRAME_RATE = 30 + + +[low-quality] + PIXEL_HEIGHT = 480 + PIXEL_WIDTH = 854 + FRAME_RATE = 15 + + +[streaming] +LIVE_STREAM_NAME = LiveStream +TWITCH_STREAM_KEY = YOUR_STREAM_KEY +STREAMING_PROTOCOL = tcp +STREAMING_IP = 127.0.0.1 +STREAMING_PORT = 2000 +STREAMING_CLIENT = ffplay +STREAMING_URL = f"{STREAMING_PROTOCOL}://{STREAMING_IP}:{STREAMING_PORT}?listen" +STREAMING_CONSOLE_BANNER = Manim is now running in streaming mode. + Stream animations by passing them to manim.play(), e.g. + >>> c = Circle() + >>> manim.play(ShowCreation(c)) + +[dirs] +MEDIA_DIR = +VIDEO_DIR = +TEX_DIR = +TEXT_DIR = From 6d0d7a3afd9200a8fe20f1902aa3a635edf97c4c Mon Sep 17 00:00:00 2001 From: leotrs Date: Wed, 27 May 2020 09:25:53 -0400 Subject: [PATCH 04/38] WIP setup of configparser --- manim/__init__.py | 3 + manim/__main__.py | 7 +- manim/animation/transform.py | 2 +- manim/camera/camera.py | 10 +- manim/config.cfg | 83 --- manim/config.py | 610 +++++++++++++--------- manim/constants.py | 139 ++--- manim/container/container.py | 2 +- manim/extract_scene.py | 57 +- manim/manim.cfg | 141 +++++ manim/mobject/mobject.py | 4 +- manim/mobject/svg/svg_mobject.py | 2 +- manim/mobject/svg/text_mobject.py | 6 +- manim/mobject/types/vectorized_mobject.py | 2 +- manim/mobject/vector_field.py | 4 +- manim/scene/scene.py | 52 +- manim/scene/scene_file_writer.py | 103 ++-- manim/scene/three_d_scene.py | 6 +- manim/scene/zoomed_scene.py | 2 +- manim/utils/tex_file_writing.py | 6 +- setup.py | 1 + 21 files changed, 668 insertions(+), 574 deletions(-) delete mode 100644 manim/config.cfg create mode 100644 manim/manim.cfg diff --git a/manim/__init__.py b/manim/__init__.py index 2c56030b44..0f7d596607 100644 --- a/manim/__init__.py +++ b/manim/__init__.py @@ -1,4 +1,7 @@ #!/usr/bin/env python + +# Importing config should be the first thing since other modules use it +from .config import config from .constants import * from .animation.animation import * diff --git a/manim/__main__.py b/manim/__main__.py index 08dc1d325b..e6e30ff842 100644 --- a/manim/__main__.py +++ b/manim/__main__.py @@ -1,13 +1,10 @@ +from .config import config from . import extract_scene -from . import config from . import constants def main(): - args = config.parse_cli() - cfg = config.get_configuration(args) - config.initialize_directories(cfg) - extract_scene.main(cfg) + extract_scene.main(config) if __name__ == "__main__": diff --git a/manim/animation/transform.py b/manim/animation/transform.py index 9bf330a18e..13f58a91f6 100644 --- a/manim/animation/transform.py +++ b/manim/animation/transform.py @@ -321,4 +321,4 @@ def __init__(self, start_anim, end_anim, **kwargs): def interpolate(self, alpha): self.start_anim.interpolate(alpha) self.end_anim.interpolate(alpha) - Transform.interpolate(self, alpha) \ No newline at end of file + Transform.interpolate(self, alpha) diff --git a/manim/camera/camera.py b/manim/camera/camera.py index 5a76684b98..619f914e6b 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -36,14 +36,14 @@ class Camera(object): self.background_image : str, optional The path to an image that should be the background image. If not set, the background is filled with `self.background_color` - + self.pixel_height """ CONFIG = { "background_image": None, - "pixel_height": DEFAULT_PIXEL_HEIGHT, - "pixel_width": DEFAULT_PIXEL_WIDTH, - "frame_rate": DEFAULT_FRAME_RATE, + "pixel_height": PIXEL_HEIGHT, + "pixel_width": PIXEL_WIDTH, + "frame_rate": FRAME_RATE, # Note: frame height and width will be resized to match # the pixel aspect ratio "frame_height": FRAME_HEIGHT, @@ -1328,4 +1328,4 @@ def display(self, *cvmobjects): else: curr_array = np.maximum(curr_array, new_array) self.reset_pixel_array() - return curr_array \ No newline at end of file + return curr_array diff --git a/manim/config.cfg b/manim/config.cfg deleted file mode 100644 index 7c199022f3..0000000000 --- a/manim/config.cfg +++ /dev/null @@ -1,83 +0,0 @@ -[DEFAULT] - -# Geometry -START_X = 30 -START_Y = 20 - -# Frame config -FRAME_HEIGHT = 8.0 -FRAME_WIDTH = FRAME_HEIGHT * DEFAULT_PIXEL_WIDTH / DEFAULT_PIXEL_HEIGHT -FRAME_Y_RADIUS = FRAME_HEIGHT / 2 -FRAME_X_RADIUS = FRAME_WIDTH / 2 - -# Default buffers (padding) -SMALL_BUFF = 0.1 -MED_SMALL_BUFF = 0.25 -MED_LARGE_BUFF = 0.5 -LARGE_BUFF = 1 -MOBJECT_TO_EDGE_BUFFER = MED_LARGE_BUFF -MOBJECT_TO_MOBJECT_BUFFER = MED_SMALL_BUFF - -# Times in seconds -POINTWISE_FUNCTION_RUN_TIME = 3.0 -WAIT_TIME = 1.0 - -# Misc -POINT_DENSITY_2D = 25 -POINT_DENSITY_1D = 250 -STROKE_WIDTH = 4 - -# Default quality config -FRAME_RATE = 60 -PIXEL_HEIGHT = 1440 -PIXEL_WIDTH = 2560 - - -[production-quality] - PIXEL_HEIGHT = 1440 - PIXEL_WIDTH = 2560 - FRAME_RATE = 60 - - -[fourk-quality] - PIXEL_HEIGHT = 2160 - PIXEL_WIDTH = 3840 - FRAME_RATE = 60 - - -[high-quality] - PIXEL_HEIGHT = 1080 - PIXEL_WIDTH = 1920 - FRAME_RATE = 60 - - -[medium-quality] - PIXEL_HEIGHT = 720 - PIXEL_WIDTH = 1280 - FRAME_RATE = 30 - - -[low-quality] - PIXEL_HEIGHT = 480 - PIXEL_WIDTH = 854 - FRAME_RATE = 15 - - -[streaming] -LIVE_STREAM_NAME = LiveStream -TWITCH_STREAM_KEY = YOUR_STREAM_KEY -STREAMING_PROTOCOL = tcp -STREAMING_IP = 127.0.0.1 -STREAMING_PORT = 2000 -STREAMING_CLIENT = ffplay -STREAMING_URL = f"{STREAMING_PROTOCOL}://{STREAMING_IP}:{STREAMING_PORT}?listen" -STREAMING_CONSOLE_BANNER = Manim is now running in streaming mode. - Stream animations by passing them to manim.play(), e.g. - >>> c = Circle() - >>> manim.play(ShowCreation(c)) - -[dirs] -MEDIA_DIR = -VIDEO_DIR = -TEX_DIR = -TEXT_DIR = diff --git a/manim/config.py b/manim/config.py index 8ce22b62d9..f4fbfd5bff 100644 --- a/manim/config.py +++ b/manim/config.py @@ -2,226 +2,329 @@ config.py --------- -Process the files constants.py, config.cfg, and the command line arguments -into a single config object. +Process the manim.cfg file and the command line arguments into a single +config object. """ -import argparse -import colour import os import sys -import types +import argparse +import configparser +import colour -from . import constants -from . import dirs from .logger import logger -__all__ = ["parse_cli", "get_configuration", "initialize_directories"] - - -def parse_cli(): - try: - parser = argparse.ArgumentParser() - parser.add_argument( - "file", - help="path to file holding the python code for the scene", - ) - parser.add_argument( - "scene_names", - nargs="*", - help="Name of the Scene class you want to see", - ) - parser.add_argument( - "-p", "--preview", - action="store_true", - help="Automatically open the saved file once its done", - ) - parser.add_argument( - "-w", "--write_to_movie", - action="store_true", - help="Render the scene as a movie file", - ) - parser.add_argument( - "-s", "--save_last_frame", - action="store_true", - help="Save the last frame", - ) - parser.add_argument( - "--dry_run", - action="store_true", - help= "Do a dry run (render scenes but generate no output files)", - ) - parser.add_argument( - "-l", "--low_quality", - action="store_true", - help="Render at low quality (for fastest rendering)", - ), - parser.add_argument( - "-m", "--medium_quality", - action="store_true", - help="Render at medium quality (for much faster rendering)", - ), - parser.add_argument( - "-e", "--high_quality", - action="store_true", - help="Render at high quality (for slightly faster rendering)", - ), - parser.add_argument( - "-k", "--four_k", - action="store_true", - help="Render at 4K quality (slower rendering)", - ), - parser.add_argument( - "-g", "--save_pngs", - action="store_true", - help="Save each frame as a png", - ) - parser.add_argument( - "-i", "--save_as_gif", - action="store_true", - help="Save the video as gif", - ) - parser.add_argument( - "-f", "--show_file_in_finder", - action="store_true", - help="Show the output file in finder", - ) - parser.add_argument( - "-t", "--transparent", - action="store_true", - help="Render to a movie file with an alpha channel", - ) - parser.add_argument( - "-q", "--quiet", - action="store_true", - help="", - ) - parser.add_argument( - "-a", "--write_all", - action="store_true", - help="Write all the scenes from a file", - ) - parser.add_argument( - "-o", "--file_name", - help="Specify the name of the output file, if" - "it should be different from the scene class name", - ) - parser.add_argument( - "-n", "--start_at_animation_number", - help="Start rendering not from the first animation, but" - "from another, specified by its index. If you pass" - "in two comma separated values, e.g. \"3,6\", it will end" - "the rendering at the second value", - ) - parser.add_argument( - "-r", "--resolution", - help="Resolution, passed as \"height,width\"", - ) - parser.add_argument( - "-c", "--color", - help="Background color", - ) - parser.add_argument( - "--sound", - action="store_true", - help="Play a success/failure sound", - ) - parser.add_argument( - "--leave_progress_bars", - action="store_true", - help="Leave progress bars displayed in terminal", - ) - parser.add_argument( - "--media_dir", - help="directory to write media", - ) - video_group = parser.add_mutually_exclusive_group() - video_group.add_argument( - "--video_dir", - help="directory to write file tree for video", - ) - parser.add_argument( - "--tex_dir", - help="directory to write tex", - ) - parser.add_argument( - "--text_dir", - help="directory to write text", - ) - return parser.parse_args() - except argparse.ArgumentError as err: - logger.error(str(err)) - sys.exit(2) - - -def get_configuration(args): - file_writer_config = { - # By default, write to file - "write_to_movie": (args.write_to_movie or not args.save_last_frame) and not args.dry_run, - "save_last_frame": args.save_last_frame and not args.dry_run, - "save_pngs": args.save_pngs, - "save_as_gif": args.save_as_gif, - # If -t is passed in (for transparent), this will be RGBA - "png_mode": "RGBA" if args.transparent else "RGB", - "movie_file_extension": ".mov" if args.transparent else ".mp4", - "file_name": args.file_name, - "input_file_path": args.file, - } - config = { - "file": args.file, - "scene_names": args.scene_names, - "open_video_upon_completion": args.preview, - "show_file_in_finder": args.show_file_in_finder, - "file_writer_config": file_writer_config, - "quiet": args.quiet or args.write_all, - "ignore_waits": args.preview, - "write_all": args.write_all, - "start_at_animation_number": args.start_at_animation_number, - "end_at_animation_number": None, - "sound": args.sound, - "leave_progress_bars": args.leave_progress_bars, - "media_dir": args.media_dir, - "video_dir": args.video_dir, - "tex_dir": args.tex_dir, - "text_dir": args.text_dir, - } - - # Camera configuration - config["camera_config"] = get_camera_configuration(args) - - # Arguments related to skipping - stan = config["start_at_animation_number"] - if stan is not None: - if "," in stan: - start, end = stan.split(",") - config["start_at_animation_number"] = int(start) - config["end_at_animation_number"] = int(end) - else: - config["start_at_animation_number"] = int(stan) +__all__ = ['config'] + + +def _parse_config(input_file, config_files): + # This only loads the [CLI] section of the manim.cfg file. If using + # CLI arguments, the _update_config_with_args function will take care + # of overriding any of these defaults. + config_parser = configparser.ConfigParser() + successfully_read_files = config_parser.read(config_files) + if not successfully_read_files: + raise FileNotFoundError('Config file could not be read') + + # Put everything in a dict + config = {} + + # Make sure we have an input file + config['INPUT_FILE'] = input_file + + # ConfigParser options are all strings, so need to convert to the + # appropriate type + + # booleans + for opt in ['PREVIEW', 'SHOW_FILE_IN_FINDER', 'QUIET', 'SOUND', + 'LEAVE_PROGRESS_BARS', 'WRITE_ALL', 'WRITE_TO_MOVIE', + 'SAVE_LAST_FRAME', 'DRY_RUN', 'SAVE_PNGS', 'SAVE_AS_GIF']: + config[opt] = config_parser['CLI'].getboolean(opt) + + # numbers + for opt in ['FROM_ANIMATION_NUMBER', 'UPTO_ANIMATION_NUMBER', + 'BACKGROUND_OPACITY']: + config[opt] = config_parser['CLI'].getint(opt) + + # UPTO_ANIMATION_NUMBER is special because -1 actually means np.inf + if config['UPTO_ANIMATION_NUMBER'] == -1: + import numpy as np + config['UPTO_ANIMATION_NUMBER'] = np.inf - config["skip_animations"] = any([ - file_writer_config["save_last_frame"], - config["start_at_animation_number"], + # strings + for opt in ['PNG_MODE', 'MOVIE_FILE_EXTENSION', 'MEDIA_DIR', + 'OUTPUT_FILE', 'VIDEO_DIR', 'TEX_DIR', 'TEXT_DIR', + 'BACKGROUND_COLOR']: + config[opt] = config_parser['CLI'][opt] + + # streaming section -- all values are strings + config['STREAMING'] = {} + for opt in ['LIVE_STREAM_NAME', 'TWITCH_STREAM_KEY', + 'STREAMING_PROTOCOL', 'STREAMING_PROTOCOL', 'STREAMING_IP', + 'STREAMING_PORT', 'STREAMING_PORT', 'STREAMING_CLIENT', + 'STREAMING_CONSOLE_BANNER']: + config['STREAMING'][opt] = config_parser['streaming'][opt] + + # for internal use (no CLI flag) + config['SKIP_ANIMATIONS'] = any([ + config['SAVE_LAST_FRAME'], + config['FROM_ANIMATION_NUMBER'], ]) - return config + # camera config -- all happen to be integers + config['CAMERA_CONFIG'] = {} + for opt in ['FRAME_RATE', 'PIXEL_HEIGHT', 'PIXEL_WIDTH']: + config['CAMERA_CONFIG'][opt] = config_parser['CLI'].getint(opt) -def get_camera_configuration(args): - camera_config = {} - if args.low_quality: - camera_config.update(constants.LOW_QUALITY_CAMERA_CONFIG) - elif args.medium_quality: - camera_config.update(constants.MEDIUM_QUALITY_CAMERA_CONFIG) - elif args.high_quality: - camera_config.update(constants.HIGH_QUALITY_CAMERA_CONFIG) - elif args.four_k: - camera_config.update(constants.FOURK_CAMERA_CONFIG) - else: - camera_config.update(constants.PRODUCTION_QUALITY_CAMERA_CONFIG) + # file writer config -- just pull them from the overall config for now + config['FILE_WRITER_CONFIG'] = {key: config[key] for key in [ + "WRITE_TO_MOVIE", "SAVE_LAST_FRAME", "SAVE_PNGS", + "SAVE_AS_GIF", "PNG_MODE", "MOVIE_FILE_EXTENSION", + "OUTPUT_FILE", "INPUT_FILE"]} + + return config, config_parser + + +def _parse_cli(): + parser = argparse.ArgumentParser( + description='Animation engine for explanatory math videos', + epilog='Made with <3 by the manim community devs' + ) + parser.add_argument( + "file", + help="path to file holding the python code for the scene", + ) + parser.add_argument( + "--scene_names", + nargs="*", + help="Name of the Scene class you want to see", + ) + parser.add_argument( + "-o", "--output_file", + help="Specify the name of the output file, if" + "it should be different from the scene class name", + ) + + # Note the following use (action='store_const', const=True), + # instead of using the built-in (action='store_true'). The reason + # is that these two are not equivalent. The latter is equivalent + # to (action='store_const', const=True, default=False), while the + # former sets no default value. We do not want to set the default + # here, but in the manim.cfg file. Therefore we use the latter, + # (action='store_const', const=True). + parser.add_argument( + "-p", "--preview", + action="store_const", + const=True, + help="Automatically open the saved file once its done", + ) + parser.add_argument( + "-f", "--show_file_in_finder", + action="store_const", + const=True, + help="Show the output file in finder", + ) + parser.add_argument( + "-q", "--quiet", + action="store_const", + const=True, + help="Quiet mode", + ) + parser.add_argument( + "--sound", + action="store_const", + const=True, + help="Play a success/failure sound", + ) + parser.add_argument( + "--leave_progress_bars", + action="store_const", + const=True, + help="Leave progress bars displayed in terminal", + ) + parser.add_argument( + "-a", "--write_all", + action="store_const", + const=True, + help="Write all the scenes from a file", + ) + parser.add_argument( + "-w", "--write_to_movie", + action="store_const", + const=True, + help="Render the scene as a movie file", + ) + parser.add_argument( + "-s", "--save_last_frame", + action="store_const", + const=True, + help="Save the last frame", + ) + parser.add_argument( + "-g", "--save_pngs", + action="store_const", + const=True, + help="Save each frame as a png", + ) + parser.add_argument( + "-i", "--save_as_gif", + action="store_const", + const=True, + help="Save the video as gif", + ) + + # The default value of the following is set in manim.cfg + parser.add_argument( + "-c", "--color", + help="Background color", + ) + parser.add_argument( + "--background_opacity", + help="Background opacity", + ) + parser.add_argument( + "--media_dir", + help="directory to write media", + ) + video_group = parser.add_mutually_exclusive_group() + video_group.add_argument( + "--video_dir", + help="directory to write file tree for video", + ) + parser.add_argument( + "--tex_dir", + help="directory to write tex", + ) + parser.add_argument( + "--text_dir", + help="directory to write text", + ) + + # All of the following use (action="store_true"). This means that + # they are by default False. In contrast to the previous ones that + # used (action="store_const", const=True), the following do not + # correspond to a single configuration option. Rather, they + # override several options at the same time. + + # The following overrides -w, -a, -g, and -i + parser.add_argument( + "--dry_run", + action="store_true", + help="Do a dry run (render scenes but generate no output files)", + ) + + # The following overrides PNG_MODE, MOVIE_FILE_EXTENSION, and + # BACKGROUND_OPACITY + parser.add_argument( + "-t", "--transparent", + action="store_true", + help="Render to a movie file with an alpha channel", + ) + + # The following are mutually exclusive and each overrides + # FRAME_RATE, PIXEL_HEIGHT, and PIXEL_WIDTH, + parser.add_argument( + "-l", "--low_quality", + action="store_true", + help="Render at low quality (for fastest rendering)", + ) + parser.add_argument( + "-m", "--medium_quality", + action="store_true", + help="Render at medium quality (for much faster rendering)", + ) + parser.add_argument( + "-e", "--high_quality", + action="store_true", + help="Render at high quality (for slightly faster rendering)", + ) + parser.add_argument( + "-k", "--fourk_quality", + action="store_true", + help="Render at 4K quality (slower rendering)", + ) - # If the resolution was passed in via -r - if args.resolution: + # This overrides any of the above + parser.add_argument( + "-r", "--resolution", + help="Resolution, passed as \"height,width\"", + ) + + # This sets FROM_ANIMATION_NUMBER and UPTO_ANIMATION_NUMBER + parser.add_argument( + "-n", "--from_animation_number", + help="Start rendering not from the first animation, but" + "from another, specified by its index. If you pass" + "in two comma separated values, e.g. \"3,6\", it will end" + "the rendering at the second value", + ) + + # Specify the manim.cfg file + parser.add_argument( + "--config_file", + help="Specify the configuration file", + ) + + return parser.parse_args() + + +def _update_config_with_args(config, config_parser, args): + # Take care of options that do not have defaults in manim.cfg + config['FILE'] = args.file + config['SCENE_NAMES'] = (args.scene_names + if args.scene_names is not None else []) + + # Flags that directly override config defaults. + for opt in ['PREVIEW', 'SHOW_FILE_IN_FINDER', 'QUIET', 'SOUND', + 'LEAVE_PROGRESS_BARS', 'WRITE_ALL', 'WRITE_TO_MOVIE', + 'SAVE_LAST_FRAME', 'SAVE_PNGS', 'SAVE_AS_GIF', 'MEDIA_DIR', + 'VIDEO_DIR', 'TEX_DIR', 'TEXT_DIR', 'BACKGROUND_OPACITY', + 'OUTPUT_FILE']: + if getattr(args, opt.lower()) is not None: + config[opt] = getattr(args, opt.lower()) + + # Parse the -n flag. + nflag = args.from_animation_number + if nflag is not None: + if ',' in nflag: + start, end = nflag.split(',') + config['FROM_ANIMATION_NUMBER'] = int(start) + config['UPTO_ANIMATION_NUMBER'] = int(end) + else: + config['FROM_ANIMATION_NUMBER'] = int(nflag) + + # The following flags use the options in the corresponding manim.cfg + # sections to override default options. For example, passing the -t + # (--transparent) flag takes all of the options defined in the + # [transparent] section of manim.cfg and uses their values to override + # the values of those options defined in CLI. + + # -t, --transparent + if args.transparent: + config.update({ + 'PNG_MODE': config_parser['transparent']['PNG_MODE'], + 'MOVIE_FILE_EXTENSION': config_parser['transparent']['MOVIE_FILE_EXTENSION'], + 'BACKGROUND_OPACITY': config_parser['transparent'].getfloat('BACKGROUND_OPACITY') + }) + + # --dry_run happens to override options that are all booleans + if args.dry_run: + config.update({opt.upper(): config_parser['dry_run'].getboolean(opt) + for opt in config_parser['dry_run']}) + + # the *_quality arguments happen to override options that are all ints + for flag in ['fourk_quality', 'high_quality', 'medium_quality', + 'low_quality']: + if getattr(args, flag) is not None: + config['CAMERA_CONFIG'].update( + {opt.upper(): config_parser[flag].getint(opt) + for opt in config_parser[flag]}) + + # Parse the -r (--resolution) flag. Note the -r flag does not + # correspond to any section in manim.cfg, but overrides the same + # options as the *_quality sections. + if args.resolution is not None: if "," in args.resolution: height_str, width_str = args.resolution.split(",") height = int(height_str) @@ -229,64 +332,75 @@ def get_camera_configuration(args): else: height = int(args.resolution) width = int(16 * height / 9) - camera_config.update({ - "pixel_height": height, - "pixel_width": width, - }) + config['CAMERA_CONFIG'].update({'pixel_height': height, + 'pixel_width': width}) - if args.color: + # Parse the -c (--color) flag + if args.color is not None: try: - camera_config["background_color"] = colour.Color(args.color) + config['CAMERA_CONFIG']['BACKGROUND_COLOR'] = colour.Color(args.color) except AttributeError as err: - logger.warning("Please use a valid color") + logger.warning('Please use a valid color') logger.error(err) sys.exit(2) - # If rendering a transparent image/move, make sure the - # scene has a background opacity of 0 - if args.transparent: - camera_config["background_opacity"] = 0 - - return camera_config + # As before, make FILE_WRITER_CONFIG by pulling form the overall + config['FILE_WRITER_CONFIG'] = {key: config[key] for key in [ + "WRITE_TO_MOVIE", "SAVE_LAST_FRAME", "SAVE_PNGS", + "SAVE_AS_GIF", "PNG_MODE", "MOVIE_FILE_EXTENSION", + "OUTPUT_FILE", "INPUT_FILE"]} + return config -def initialize_directories(config): - dir_config = {} - dir_config["media_dir"] = config["media_dir"] or dirs.MEDIA_DIR - dir_config["video_dir"] = config["video_dir"] or dirs.VIDEO_DIR - if not (config["video_dir"] and config["tex_dir"]): - if config["media_dir"]: - if not os.path.isdir(dir_config["media_dir"]): - os.makedirs(dir_config["media_dir"]) - if not os.path.isdir(dir_config["media_dir"]): - dir_config["media_dir"] = "./media" +def _init_dirs(config): + if not (config["VIDEO_DIR"] and config["TEX_DIR"]): + if config["MEDIA_DIR"]: + if not os.path.isdir(config["MEDIA_DIR"]): + os.makedirs(config["MEDIA_DIR"]) + if not os.path.isdir(config["MEDIA_DIR"]): + config["MEDIA_DIR"] = "./media" else: print( - f"Media will be written to {dir_config['media_dir'] + os.sep}. You can change " - "this behavior with the --media_dir flag, or by adjusting dirs.py.," + f"Media will be written to {config['media_dir'] + os.sep}. You can change " + "this behavior with the --media_dir flag, or by adjusting manim.cfg." ) else: - if config["media_dir"]: + if config["MEDIA_DIR"]: print( "Ignoring --media_dir, since both --tex_dir and --video_dir were passed." ) - dir_config["tex_dir"] = (config["tex_dir"] - or dirs.TEX_DIR - or os.path.join(dir_config["media_dir"], "Tex")) - dir_config["text_dir"] = (config["text_dir"] - or dirs.TEXT_DIR - or os.path.join(dir_config["media_dir"], "texts")) + # Make sure all folders exist + for folder in [config["VIDEO_DIR"], config["TEX_DIR"], config["TEXT_DIR"]]: + if not os.path.exists(folder): + os.makedirs(folder) - if not config["video_dir"] or dirs.VIDEO_DIR: - dir_config["video_dir"] = os.path.join(dir_config["media_dir"], "videos") - for folder in [dir_config["video_dir"], dir_config["tex_dir"], dir_config["text_dir"]]: - if folder != "" and not os.path.exists(folder): - os.makedirs(folder) +config_filename = 'manim.cfg' + +foobar = os.path.join(os.path.dirname(__file__), config_filename) +print(foobar) +config_files = [ + foobar, + os.path.expanduser('~/.{}'.format(config_filename)), + os.path.join(os.getcwd(), config_filename), + ] + +prog = os.path.split(sys.argv[0])[-1] +if prog in ['manim', 'manimcm']: + # If called as entrypoint, set default config, and override the + # defaults using CLI arguments + args = _parse_cli() + + print(args) + + if args.config_file is not None: + config_files.append(args.config_file) + config, config_parser = _parse_config(args.file, config_files) + _update_config_with_args(config, config_parser, args) + +else: + config, _ = _parse_config('', config_files) - dirs.MEDIA_DIR = dir_config["media_dir"] - dirs.VIDEO_DIR = dir_config["video_dir"] - dirs.TEX_DIR = dir_config["tex_dir"] - dirs.TEXT_DIR = dir_config["text_dir"] +_init_dirs(config) diff --git a/manim/constants.py b/manim/constants.py index 086899062f..205d976559 100644 --- a/manim/constants.py +++ b/manim/constants.py @@ -1,8 +1,10 @@ import numpy as np import os +from .config import config + # Messages -NOT_SETTING_FONT_MSG=''' +NOT_SETTING_FONT_MSG = ''' You haven't set font. If you are not using English, this may cause text rendering problem. You set font like: @@ -51,12 +53,42 @@ class MyText(Text): DL = DOWN + LEFT DR = DOWN + RIGHT +# Geometry +START_X = 30 +START_Y = 20 + +# Geometry: frame +FRAME_HEIGHT = 8.0 +PIXEL_WIDTH = config['CAMERA_CONFIG']['PIXEL_WIDTH'] +PIXEL_HEIGHT = config['CAMERA_CONFIG']['PIXEL_HEIGHT'] +FRAME_RATE = config['CAMERA_CONFIG']['FRAME_RATE'] +FRAME_WIDTH = FRAME_HEIGHT * PIXEL_WIDTH / PIXEL_HEIGHT +FRAME_Y_RADIUS = FRAME_HEIGHT / 2 +FRAME_X_RADIUS = FRAME_WIDTH / 2 + # Geometry: sides TOP = FRAME_Y_RADIUS * UP BOTTOM = FRAME_Y_RADIUS * DOWN LEFT_SIDE = FRAME_X_RADIUS * LEFT RIGHT_SIDE = FRAME_X_RADIUS * RIGHT +# Default buffers (padding) +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 +DEFAULT_MOBJECT_TO_MOBJECT_BUFFER = MED_SMALL_BUFF + +# Times in seconds +DEFAULT_POINTWISE_FUNCTION_RUN_TIME = 3.0 +DEFAULT_WAIT_TIME = 1.0 + +# Misc +DEFAULT_POINT_DENSITY_2D = 25 +DEFAULT_POINT_DENSITY_1D = 250 +DEFAULT_STROKE_WIDTH = 4 + # Tex stuff TEX_USE_CTEX = False TEX_TEXT_TO_REPLACE = "YourTextHere" @@ -145,108 +177,3 @@ class MyText(Text): if name.endswith("_C")}) PALETTE = list(COLOR_MAP.values()) locals().update(COLOR_MAP) - -# Settings for CodeMobject -code_languages_list = {"abap": "abap", "as": "as", "as3": "as3", "ada": "ada", "antlr": "antlr", - "antlr_as": "antlr-as", - "antlr_csharp": "antlr-csharp", "antlr_cpp": "antlr-cpp", "antlr_java": "antlr-java", - "antlr_objc": "antlr-objc", "antlr_perl": "antlr-perl", "antlr_python": "antlr-python", - "antlr_ruby": "antlr-ruby", "apacheconf": "apacheconf", "applescript": "applescript", - "aspectj": "aspectj", - "aspx_cs": "aspx-cs", "aspx_vb": "aspx-vb", "asy": "asy", "ahk": "ahk", "autoit": "autoit", - "awk": "awk", - "basemake": "basemake", "bash": "bash", "console": "console", "bat": "bat", - "bbcode": "bbcode", - "befunge": "befunge", "blitzmax": "blitzmax", "boo": "boo", "brainfuck": "brainfuck", - "bro": "bro", - "bugs": "bugs", "c": "c", "csharp": "csharp", "cpp": "cpp", "c_objdump": "c-objdump", - "ca65": "ca65", - "cbmbas": "cbmbas", "ceylon": "ceylon", "cfengine3": "cfengine3", "cfs": "cfs", - "cheetah": "cheetah", - "clojure": "clojure", "cmake": "cmake", "cobol": "cobol", "cobolfree": "cobolfree", - "coffee_script": "coffee-script", "cfm": "cfm", "common_lisp": "common-lisp", "coq": "coq", - "cpp_objdump": "cpp-objdump", "croc": "croc", "css": "css", "css_django": "css+django", - "css_genshitext": "css+genshitext", "css_lasso": "css+lasso", "css_mako": "css+mako", - "css_myghty": "css+myghty", "css_php": "css+php", "css_erb": "css+erb", - "css_smarty": "css+smarty", - "cuda": "cuda", "cython": "cython", "d": "d", "d_objdump": "d-objdump", "dpatch": "dpatch", - "dart": "dart", - "control": "control", "sourceslist": "sourceslist", "delphi": "delphi", "dg": "dg", - "diff": "diff", - "django": "django", "dtd": "dtd", "duel": "duel", "dylan": "dylan", - "dylan_console": "dylan-console", - "dylan_lid": "dylan-lid", "ec": "ec", "ecl": "ecl", "elixir": "elixir", "iex": "iex", - "ragel_em": "ragel-em", - "erb": "erb", "erlang": "erlang", "erl": "erl", "evoque": "evoque", "factor": "factor", - "fancy": "fancy", - "fan": "fan", "felix": "felix", "fortran": "fortran", "Clipper": "Clipper", - "fsharp": "fsharp", "gas": "gas", - "genshi": "genshi", "genshitext": "genshitext", "pot": "pot", "Cucumber": "Cucumber", - "glsl": "glsl", - "gnuplot": "gnuplot", "go": "go", "gooddata_cl": "gooddata-cl", "gosu": "gosu", "gst": "gst", - "groff": "groff", - "groovy": "groovy", "haml": "haml", "haskell": "haskell", "hx": "hx", "html": "html", - "html_cheetah": "html+cheetah", "html_django": "html+django", "html_evoque": "html+evoque", - "html_genshi": "html+genshi", "html_lasso": "html+lasso", "html_mako": "html+mako", - "html_myghty": "html+myghty", "html_php": "html+php", "html_smarty": "html+smarty", - "html_velocity": "html+velocity", "http": "http", "haxeml": "haxeml", "hybris": "hybris", - "idl": "idl", - "ini": "ini", "io": "io", "ioke": "ioke", "irc": "irc", "jade": "jade", "jags": "jags", - "java": "java", - "jsp": "jsp", "js": "js", "js_cheetah": "js+cheetah", "js_django": "js+django", - "js_genshitext": "js+genshitext", "js_lasso": "js+lasso", "js_mako": "js+mako", - "js_myghty": "js+myghty", - "js_php": "js+php", "js_erb": "js+erb", "js_smarty": "js+smarty", "json": "json", - "julia": "julia", - "jlcon": "jlcon", "kconfig": "kconfig", "koka": "koka", "kotlin": "kotlin", "lasso": "lasso", - "lighty": "lighty", "lhs": "lhs", "live_script": "live-script", "llvm": "llvm", - "logos": "logos", - "logtalk": "logtalk", "lua": "lua", "make": "make", "mako": "mako", "maql": "maql", - "mason": "mason", - "matlab": "matlab", "matlabsession": "matlabsession", "minid": "minid", - "modelica": "modelica", - "modula2": "modula2", "trac_wiki": "trac-wiki", "monkey": "monkey", "moocode": "moocode", - "moon": "moon", - "mscgen": "mscgen", "mupad": "mupad", "mxml": "mxml", "myghty": "myghty", "mysql": "mysql", - "nasm": "nasm", - "nemerle": "nemerle", "newlisp": "newlisp", "newspeak": "newspeak", "nginx": "nginx", - "nimrod": "nimrod", - "nsis": "nsis", "numpy": "numpy", "objdump": "objdump", "objective_c": "objective-c", - "objective_c_+": "objective-c++", "objective_j": "objective-j", "ocaml": "ocaml", - "octave": "octave", - "ooc": "ooc", "opa": "opa", "openedge": "openedge", "perl": "perl", "php": "php", - "plpgsql": "plpgsql", - "psql": "psql", "postgresql": "postgresql", "postscript": "postscript", "pov": "pov", - "powershell": "powershell", "prolog": "prolog", "properties": "properties", - "protobuf": "protobuf", - "puppet": "puppet", "pypylog": "pypylog", "python": "python", "python3": "python3", - "py3tb": "py3tb", - "pycon": "pycon", "pytb": "pytb", "qml": "qml", "racket": "racket", "ragel": "ragel", - "ragel_c": "ragel-c", - "ragel_cpp": "ragel-cpp", "ragel_d": "ragel-d", "ragel_java": "ragel-java", - "ragel_objc": "ragel-objc", - "ragel_ruby": "ragel-ruby", "raw": "raw", "rconsole": "rconsole", "rd": "rd", - "rebol": "rebol", - "redcode": "redcode", "registry": "registry", "rst": "rst", "rhtml": "rhtml", - "RobotFramework": "RobotFramework", "spec": "spec", "rb": "rb", "rbcon": "rbcon", - "rust": "rust", - "splus": "splus", "sass": "sass", "scala": "scala", "ssp": "ssp", "scaml": "scaml", - "scheme": "scheme", - "scilab": "scilab", "scss": "scss", "shell_session": "shell-session", "smali": "smali", - "smalltalk": "smalltalk", "smarty": "smarty", "snobol": "snobol", "sp": "sp", "sql": "sql", - "sqlite3": "sqlite3", "squidconf": "squidconf", "stan": "stan", "sml": "sml", - "systemverilog": "systemverilog", - "tcl": "tcl", "tcsh": "tcsh", "tea": "tea", "tex": "tex", "text": "text", - "treetop": "treetop", "ts": "ts", - "urbiscript": "urbiscript", "vala": "vala", "vb.net": "vb.net", "velocity": "velocity", - "verilog": "verilog", - "vgl": "vgl", "vhdl": "vhdl", "vim": "vim", "xml": "xml", "xml_cheetah": "xml+cheetah", - "xml_django": "xml+django", "xml_evoque": "xml+evoque", "xml_lasso": "xml+lasso", - "xml_mako": "xml+mako", - "xml_myghty": "xml+myghty", "xml_php": "xml+php", "xml_erb": "xml+erb", - "xml_smarty": "xml+smarty", - "xml_velocity": "xml+velocity", "xquery": "xquery", "xslt": "xslt", "xtend": "xtend", - "yaml": "yaml"} -code_styles_list = {0: "autumn", 1: "borland", 2: "bw", 3: "colorful", 4: "default", 5: "emacs", - 6: "friendly", 7: "fruity", 8: "manni", 9: "monokai", 10: "murphy", 11: "native", - 12: "pastie", 13: "perldoc", 14: "rrt", 15: "tango", 16: "trac", 17: "vim", 18: "vs"} diff --git a/manim/container/container.py b/manim/container/container.py index 5f1274f03b..8d84f2729d 100644 --- a/manim/container/container.py +++ b/manim/container/container.py @@ -28,4 +28,4 @@ def remove(self, *items): Must be implemented by subclasses. """ raise Exception( - "Container.remove is not implemented; it is up to derived classes to implement") \ No newline at end of file + "Container.remove is not implemented; it is up to derived classes to implement") diff --git a/manim/extract_scene.py b/manim/extract_scene.py index b8a0ad96de..323833b3a6 100644 --- a/manim/extract_scene.py +++ b/manim/extract_scene.py @@ -15,21 +15,21 @@ def open_file_if_needed(file_writer, **config): - if config["quiet"]: + if config["QUIET"]: curr_stdout = sys.stdout sys.stdout = open(os.devnull, "w") open_file = any([ - config["open_video_upon_completion"], - config["show_file_in_finder"] + config["PREVIEW"], + config["SHOW_FILE_IN_FINDER"] ]) if open_file: current_os = platform.system() file_paths = [] - if config["file_writer_config"]["save_last_frame"]: + if config["FILE_WRITER_CONFIG"]["SAVE_LAST_FRAME"]: file_paths.append(file_writer.get_image_file_path()) - if config["file_writer_config"]["write_to_movie"]: + if config["FILE_WRITER_CONFIG"]["WRITE_TO_MOVIE"]: file_paths.append(file_writer.get_movie_file_path()) for file_path in file_paths: @@ -44,7 +44,7 @@ def open_file_if_needed(file_writer, **config): else: # Assume macOS commands.append("open") - if config["show_file_in_finder"]: + if config["SHOW_FILE_IN_FINDER"]: commands.append("-R") commands.append(file_path) @@ -54,7 +54,7 @@ def open_file_if_needed(file_writer, **config): sp.call(commands, stdout=FNULL, stderr=sp.STDOUT) FNULL.close() - if config["quiet"]: + if config["QUIET"]: sys.stdout.close() sys.stdout = curr_stdout @@ -96,13 +96,13 @@ def prompt_user_for_choice(scene_classes): def get_scenes_to_render(scene_classes, config): - if len(scene_classes) == 0: + if not scene_classes: logger.error(constants.NO_SCENE_MESSAGE) return [] - if config["write_all"]: + if config["WRITE_ALL"]: return scene_classes result = [] - for scene_name in config["scene_names"]: + for scene_name in config["SCENE_NAMES"]: found = False for scene_class in scene_classes: if scene_class.__name__ == scene_name: @@ -144,40 +144,41 @@ def get_module(file_name): logger.error(f"Failed to render scene: {str(e)}") sys.exit(2) else: - module_name = file_name.replace(os.sep, ".").replace(".py", "") - spec = importlib.util.spec_from_file_location(module_name, file_name) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module + if os.path.exists(file_name): + module_name = file_name.replace(os.sep, ".").replace(".py", "") + spec = importlib.util.spec_from_file_location(module_name, file_name) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + else: + raise FileNotFoundError(f'{file_name} not found') def main(config): - module = get_module(config["file"]) + module = get_module(config["FILE"]) all_scene_classes = get_scene_classes_from_module(module) scene_classes_to_render = get_scenes_to_render(all_scene_classes, config) - scene_kwargs = dict([ - (key, config[key]) - for key in [ - "camera_config", - "file_writer_config", - "skip_animations", - "start_at_animation_number", - "end_at_animation_number", - "leave_progress_bars", + keys = [ + "CAMERA_CONFIG", + "FILE_WRITER_CONFIG", + "SKIP_ANIMATIONS", + "FROM_ANIMATION_NUMBER", + "UPTO_ANIMATION_NUMBER", + "LEAVE_PROGRESS_BARS", ] - ]) + scene_kwargs = {key: config[key] for key in keys} for SceneClass in scene_classes_to_render: try: # By invoking, this renders the full scene scene = SceneClass(**scene_kwargs) open_file_if_needed(scene.file_writer, **config) - if config["sound"]: + if config["SOUND"]: play_finish_sound() except Exception: print("\n\n") traceback.print_exc() print("\n\n") - if config["sound"]: + if config["SOUND"]: play_error_sound() diff --git a/manim/manim.cfg b/manim/manim.cfg new file mode 100644 index 0000000000..472df34d0c --- /dev/null +++ b/manim/manim.cfg @@ -0,0 +1,141 @@ +# manim.cfg +# Default configuration for manim + +# Configure how manim behaves when called from the command line without +# specifying any flags +[CLI] + +# Each of the following will be set to True if the corresponding CLI flag +# is present when executing manim. If the flag is not present, they will +# be set to the value found here. For example, running manim with the -w +# flag will set WRITE_TO_MOVIE to True. However, since the default value +# of WRITE_TO_MOVIE defined in this file is also True, running manim +# without the -w value will also output a movie file. To change that, set +# WRITE_TO_MOVIE = False so that running manim without the -w flag will not +# generate a movie file. Note all of the following accept boolean values. + +# -w, --write_to_movie +WRITE_TO_MOVIE = True + +# -a, --write_all +WRITE_ALL = False + +# -s, --save_last_frame +SAVE_LAST_FRAME = False + +# -g, --save_pngs +SAVE_PNGS = False + +# -i, --save_as_gif +SAVE_AS_GIF = False + +# -p, --preview +PREVIEW = False + +# -f, --show_file_in_finder +SHOW_FILE_IN_FINDER = False + +# -q, --quiet +QUIET = False + +# --sound +SOUND = False + +# -o, --output_file +OUTPUT_FILE = + +# --leave_progress_bars +LEAVE_PROGRESS_BARS = False + +# -c, --color +BACKGROUND_COLOR = BLACK + +# --background_opacity +BACKGROUND_OPACITY = 1 + +# The following two are both set by the -n (or --from_animation_number) +# flag. See manim -h for more information. +FROM_ANIMATION_NUMBER = 0 + +# Use -1 to render all animations +UPTO_ANIMATION_NUMBER = -1 + +# The following are meant to be paths relative to the point of execution. +# Set them at the CLI with the corresponding flag, or set their default +# values here. +# --media_dir +MEDIA_DIR = ./media + +# --video_dir +VIDEO_DIR = %(MEDIA_DIR)s/videos + +# --tex_dir +TEX_DIR = %(MEDIA_DIR)s/Tex + +# --text_dir +TEXT_DIR = %(MEDIA_DIR)s/texts + +# If the -t (--transparent) flag is used, these will be replaced with the +# values specified in the [TRANSPARENT] section later in this file. +PNG_MODE = RGB +MOVIE_FILE_EXTENSION = .mp4 + +# These can be overriden with any of -l (--low_quality), -m +# (--medium_quality), -e (--high_quality), or -k (--fourk_quality). The +# overriding values are found in the corresponding sections. +FRAME_RATE = 60 +PIXEL_HEIGHT = 1440 +PIXEL_WIDTH = 2560 + +# These override the previous by using -t, --transparent +[transparent] +PNG_MODE = RGBA +MOVIE_FILE_EXTENSION = .mov +BACKGROUND_OPACITY = 0 + +# These override the previous by using -k, --four_k +[fourk_quality] +PIXEL_HEIGHT = 2160 +PIXEL_WIDTH = 3840 +FRAME_RATE = 60 + +# These override the previous by using -e, --high_quality +[high_quality] +PIXEL_HEIGHT = 1080 +PIXEL_WIDTH = 1920 +FRAME_RATE = 60 + +# These override the previous by using -m, --medium_quality +[medium_quality] +PIXEL_HEIGHT = 720 +PIXEL_WIDTH = 1280 +FRAME_RATE = 30 + +# These override the previous by usnig -l, --low_quality +[low_quality] +PIXEL_HEIGHT = 480 +PIXEL_WIDTH = 854 +FRAME_RATE = 15 + +# These override the previous by using --dry_run +# Note --dry_run overrides all of -w, -a, -s, -g, -i +[dry_run] +WRITE_TO_MOVIE = False +WRITE_ALL = False +SAVE_LAST_FRAME = False +SAVE_PNGS = False +SAVE_AS_GIF = False + +# Streaming settings +[streaming] +LIVE_STREAM_NAME = LiveStream +TWITCH_STREAM_KEY = YOUR_STREAM_KEY +STREAMING_PROTOCOL = tcp +STREAMING_IP = 127.0.0.1 +STREAMING_PORT = 2000 +STREAMING_CLIENT = ffplay +STREAMING_URL = %(STREAMING_PROTOCOL)s://%(STREAMING_IP)s:%(STREAMING_PORT)s?listen +STREAMING_CONSOLE_BANNER = Manim is now running in streaming mode. + Stream animations by passing them to manim.play(), e.g. + >>> c = Circle() + >>> manim.play(ShowCreation(c)) diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py index edd22af14d..e460b8431c 100644 --- a/manim/mobject/mobject.py +++ b/manim/mobject/mobject.py @@ -10,7 +10,7 @@ import numpy as np from ..constants import * -from .. import dirs +from ..config import config from ..container.container import Container from ..utils.color import color_gradient from ..utils.color import interpolate_color @@ -110,7 +110,7 @@ def show(self, camera=None): def save_image(self, name=None): self.get_image().save( - os.path.join(dirs.VIDEO_DIR, (name or str(self)) + ".png") + os.path.join(config['VIDEO_DIR'], (name or str(self)) + ".png") ) def copy(self): diff --git a/manim/mobject/svg/svg_mobject.py b/manim/mobject/svg/svg_mobject.py index 9d569d4098..321b689b70 100644 --- a/manim/mobject/svg/svg_mobject.py +++ b/manim/mobject/svg/svg_mobject.py @@ -428,4 +428,4 @@ def string_to_points(self, coord_string): return result def get_original_path_string(self): - return self.path_string \ No newline at end of file + return self.path_string diff --git a/manim/mobject/svg/text_mobject.py b/manim/mobject/svg/text_mobject.py index f229a95ba1..359271790b 100644 --- a/manim/mobject/svg/text_mobject.py +++ b/manim/mobject/svg/text_mobject.py @@ -5,7 +5,7 @@ import cairo from ...constants import * -from ... import dirs +from ...config import config from ...container.container import Container from ...logger import logger from ...mobject.geometry import Dot, Rectangle @@ -96,7 +96,7 @@ def __init__(self, text, **config): def get_space_width(self): size = self.size * 10 - dir_name = dirs.TEXT_DIR + dir_name = config['TEXT_DIR'] file_name = os.path.join(dir_name, "space") + '.svg' surface = cairo.SVGSurface(file_name, 600, 400) @@ -292,7 +292,7 @@ def text2svg(self): if NOT_SETTING_FONT_MSG != '': logger.warning(NOT_SETTING_FONT_MSG) - dir_name = dirs.TEXT_DIR + dir_name = config['TEXT_DIR'] hash_name = self.text2hash() file_name = os.path.join(dir_name, hash_name)+'.svg' if os.path.exists(file_name): diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index e354d9ce38..81335047e2 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -973,4 +973,4 @@ def __init__(self, vmobject, **kwargs): ]) # Family is already taken care of by get_subcurve # implementation - self.match_style(vmobject, family=False) \ No newline at end of file + self.match_style(vmobject, family=False) diff --git a/manim/mobject/vector_field.py b/manim/mobject/vector_field.py index 774b753eee..cc77353702 100644 --- a/manim/mobject/vector_field.py +++ b/manim/mobject/vector_field.py @@ -27,8 +27,8 @@ def get_colored_background_image(scalar_field_func, number_to_rgb_func, - pixel_height=DEFAULT_PIXEL_HEIGHT, - pixel_width=DEFAULT_PIXEL_WIDTH): + pixel_height=PIXEL_HEIGHT, + pixel_width=PIXEL_WIDTH): ph = pixel_height pw = pixel_width fw = FRAME_WIDTH diff --git a/manim/scene/scene.py b/manim/scene/scene.py index ce24579c48..b69e2c3aa0 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -44,21 +44,21 @@ def construct(self): """ CONFIG = { "camera_class": Camera, - "camera_config": {}, - "file_writer_config": {}, - "skip_animations": False, + "CAMERA_CONFIG": {}, + "FILE_WRITER_CONFIG": {}, + "SKIP_ANIMATIONS": False, "always_update_mobjects": False, "random_seed": 0, - "start_at_animation_number": None, - "end_at_animation_number": None, - "leave_progress_bars": False, + "FROM_ANIMATION_NUMBER": None, + "UPTO_ANIMATION_NUMBER": None, + "LEAVE_PROGRESS_BARS": False, } def __init__(self, **kwargs): Container.__init__(self, **kwargs) - self.camera = self.camera_class(**self.camera_config) + self.camera = self.camera_class(**self.CAMERA_CONFIG) self.file_writer = SceneFileWriter( - self, **self.file_writer_config, + self, **self.FILE_WRITER_CONFIG, ) self.mobjects = [] @@ -66,7 +66,7 @@ def __init__(self, **kwargs): self.foreground_mobjects = [] self.num_plays = 0 self.time = 0 - self.original_skipping_status = self.skip_animations + self.original_skipping_status = self.SKIP_ANIMATIONS if self.random_seed is not None: random.seed(self.random_seed) np.random.seed(self.random_seed) @@ -238,7 +238,7 @@ def update_frame( #TODO Description in Docstring **kwargs """ - if self.skip_animations and not ignore_skipping: + if self.SKIP_ANIMATIONS and not ignore_skipping: return if mobjects is None: mobjects = list_update( @@ -679,14 +679,14 @@ def get_time_progression(self, run_time, n_iterations=None, override_skip_animat ProgressDisplay The CommandLine Progress Bar. """ - if self.skip_animations and not override_skip_animations: + if self.SKIP_ANIMATIONS and not override_skip_animations: times = [run_time] else: step = 1 / self.camera.frame_rate times = np.arange(0, run_time, step) time_progression = ProgressDisplay( times, total=n_iterations, - leave=self.leave_progress_bars, + leave=self.LEAVE_PROGRESS_BARS, ascii=False if platform.system() != 'Windows' else True ) return time_progression @@ -824,12 +824,12 @@ def update_skipping_status(self): raises an EndSceneEarlyException if they don't correspond. """ - if self.start_at_animation_number: - if self.num_plays == self.start_at_animation_number: - self.skip_animations = False - if self.end_at_animation_number: - if self.num_plays >= self.end_at_animation_number: - self.skip_animations = True + if self.FROM_ANIMATION_NUMBER: + if self.num_plays == self.FROM_ANIMATION_NUMBER: + self.SKIP_ANIMATIONS = False + if self.UPTO_ANIMATION_NUMBER: + if self.num_plays >= self.UPTO_ANIMATION_NUMBER: + self.SKIP_ANIMATIONS = True raise EndSceneEarlyException() def handle_play_like_call(func): @@ -854,7 +854,7 @@ def handle_play_like_call(func): """ def wrapper(self, *args, **kwargs): self.update_skipping_status() - allow_write = not self.skip_animations + allow_write = not self.SKIP_ANIMATIONS self.file_writer.begin_animation(allow_write) func(self, *args, **kwargs) self.file_writer.end_animation(allow_write) @@ -927,7 +927,7 @@ def finish_animations(self, animations): self.mobjects_from_last_animation = [ anim.mobject for anim in animations ] - if self.skip_animations: + if self.SKIP_ANIMATIONS: # TODO, run this call in for each animation? self.update_mobjects(self.get_run_time(animations)) else: @@ -1070,7 +1070,7 @@ def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): if stop_condition is not None and stop_condition(): time_progression.close() break - elif self.skip_animations: + elif self.SKIP_ANIMATIONS: # Do nothing return self else: @@ -1109,8 +1109,8 @@ def force_skipping(self): Scene The Scene, with skipping turned on. """ - self.original_skipping_status = self.skip_animations - self.skip_animations = True + self.original_skipping_status = self.SKIP_ANIMATIONS + self.SKIP_ANIMATIONS = True return self def revert_to_original_skipping_status(self): @@ -1125,7 +1125,7 @@ def revert_to_original_skipping_status(self): The Scene, with the original skipping status. """ if hasattr(self, "original_skipping_status"): - self.skip_animations = self.original_skipping_status + self.SKIP_ANIMATIONS = self.original_skipping_status return self def add_frames(self, *frames): @@ -1139,7 +1139,7 @@ def add_frames(self, *frames): """ dt = 1 / self.camera.frame_rate self.increment_time(len(frames) * dt) - if self.skip_animations: + if self.SKIP_ANIMATIONS: return for frame in frames: self.file_writer.write_frame(frame) @@ -1160,7 +1160,7 @@ def add_sound(self, sound_file, time_offset=0, gain=None, **kwargs): gain : """ - if self.skip_animations: + if self.SKIP_ANIMATIONS: return time = self.get_time() + time_offset self.file_writer.add_sound(sound_file, time, gain, **kwargs) diff --git a/manim/scene/scene_file_writer.py b/manim/scene/scene_file_writer.py index 820e25ea8c..3444fa1626 100644 --- a/manim/scene/scene_file_writer.py +++ b/manim/scene/scene_file_writer.py @@ -9,10 +9,7 @@ from PIL import Image from ..constants import FFMPEG_BIN -from ..constants import STREAMING_IP -from ..constants import STREAMING_PORT -from ..constants import STREAMING_PROTOCOL -from .. import dirs +from ..config import config from ..logger import logger from ..utils.config_ops import digest_config from ..utils.file_ops import guarantee_existence @@ -38,17 +35,15 @@ class SceneFileWriter(object): The file-type extension of the outputted video. """ CONFIG = { - "write_to_movie": False, - "save_pngs": False, - "png_mode": "RGBA", - "save_last_frame": False, - "movie_file_extension": ".mp4", - "gif_file_extension": ".gif", - # Previous output_file_name - # TODO, address this in extract_scene et. al. - "file_name": None, - "input_file_path": "", # ?? - "output_directory": None, + "WRITE_TO_MOVIE": False, + "SAVE_PNGS": False, + "PNG_MODE": "RGBA", + "SAVE_LAST_FRAME": False, + "MOVIE_FILE_EXTENSION": ".mp4", + "GIF_FILE_EXTENSION": ".gif", + "OUTPUT_FILE": None, + "INPUT_FILE": "", + "OUTPUT_DIRECTORY": None, } def __init__(self, scene, **kwargs): @@ -66,14 +61,12 @@ def init_output_directories(self): files will be written to and read from (within MEDIA_DIR). If they don't already exist, they will be created. """ - module_directory = self.output_directory or self.get_default_module_directory() - scene_name = self.file_name or self.get_default_scene_name() - #print("1") - #print(dirs.MEDIA_DIR) - if self.save_last_frame or self.save_pngs: - if dirs.MEDIA_DIR != "": + module_directory = self.OUTPUT_DIRECTORY or self.get_default_module_directory() + scene_name = self.OUTPUT_FILE or self.get_default_scene_name() + if self.SAVE_LAST_FRAME or self.SAVE_PNGS: + if config['MEDIA_DIR'] != "": image_dir = guarantee_existence(os.path.join( - dirs.MEDIA_DIR, + config['MEDIA_DIR'], "images", module_directory, )) @@ -81,23 +74,24 @@ def init_output_directories(self): image_dir, add_extension_if_not_present(scene_name, ".png") ) - if self.write_to_movie: - if dirs.VIDEO_DIR != "": + + if config['WRITE_TO_MOVIE']: + if config['VIDEO_DIR']: movie_dir = guarantee_existence(os.path.join( - dirs.VIDEO_DIR, + config['VIDEO_DIR'], module_directory, self.get_resolution_directory(), )) self.movie_file_path = os.path.join( movie_dir, add_extension_if_not_present( - scene_name, self.movie_file_extension + scene_name, self.MOVIE_FILE_EXTENSION ) ) self.gif_file_path = os.path.join( movie_dir, add_extension_if_not_present( - scene_name, self.gif_file_extension + scene_name, self.GIF_FILE_EXTENSION ) ) self.partial_movie_directory = guarantee_existence(os.path.join( @@ -116,7 +110,7 @@ def get_default_module_directory(self): str The name of the directory. """ - filename = os.path.basename(self.input_file_path) + filename = os.path.basename(self.INPUT_FILE) root, _ = os.path.splitext(filename) return root @@ -132,10 +126,10 @@ def get_default_scene_name(self): str The default scene name. """ - if self.file_name is None: - return self.scene.__class__.__name__ + if self.OUTPUT_FILE: + return self.OUTPUT_FILE else: - return self.file_name + return self.scene.__class__.__name__ def get_resolution_directory(self): """ @@ -146,7 +140,7 @@ def get_resolution_directory(self): that immediately contains the video file will be 480p15. The file structure should look something like: - + MEDIA_DIR |--Tex |--texts @@ -172,7 +166,7 @@ def get_image_file_path(self): written to. It is usually named "images", but can be changed by changing "image_file_path". - + Returns ------- str @@ -183,7 +177,7 @@ def get_image_file_path(self): def get_next_partial_movie_path(self): """ Manim renders each play-like call in a short partial - video file. All such files are then concatenated with + video file. All such files are then concatenated with the help of FFMPEG. This method returns the path of the next partial movie. @@ -197,7 +191,7 @@ def get_next_partial_movie_path(self): self.partial_movie_directory, "{:05}{}".format( self.scene.num_plays, - self.movie_file_extension, + self.MOVIE_FILE_EXTENSION, ) ) return result @@ -230,18 +224,18 @@ def add_audio_segment(self, new_segment, time=None, gain_to_background=None): """ - This method adds an audio segment from an + This method adds an audio segment from an AudioSegment type object and suitable parameters. - + Parameters ---------- new_segment : AudioSegment The audio segment to add - + time : int, float, optional the timestamp at which the sound should be added. - + gain_to_background : optional The gain of the segment from the background. """ @@ -276,7 +270,7 @@ def add_sound(self, sound_file, time=None, gain=None, **kwargs): ---------- sound_file : str The path to the sound file. - + time : float or int, optional The timestamp at which the audio should be added. @@ -305,7 +299,7 @@ def begin_animation(self, allow_write=False): allow_write : bool, optional Whether or not to write to a video file. """ - if self.write_to_movie and allow_write: + if self.WRITE_TO_MOVIE and allow_write: self.open_movie_pipe() def end_animation(self, allow_write=False): @@ -318,7 +312,7 @@ def end_animation(self, allow_write=False): allow_write : bool, optional Whether or not to write to a video file. """ - if self.write_to_movie and allow_write: + if self.WRITE_TO_MOVIE and allow_write: self.close_movie_pipe() def write_frame(self, frame): @@ -331,9 +325,9 @@ def write_frame(self, frame): frame : np.array Pixel array of the frame. """ - if self.write_to_movie: + if self.WRITE_TO_MOVIE: self.writing_process.stdin.write(frame.tostring()) - if self.save_pngs: + if self.SAVE_PNGS: path, extension = os.path.splitext(self.image_file_path) Image.fromarray(frame).save(f'{path}{self.frame_count}{extension}') self.frame_count += 1 @@ -376,11 +370,11 @@ def finish(self): If save_last_frame is True, saves the last frame in the default image directory. """ - if self.write_to_movie: + if self.WRITE_TO_MOVIE: if hasattr(self, "writing_process"): self.writing_process.terminate() self.combine_movie_files() - if self.save_last_frame: + if self.SAVE_LAST_FRAME: self.scene.update_frame(ignore_skipping=True) self.save_final_image(self.scene.get_image()) @@ -391,8 +385,7 @@ def open_movie_pipe(self): buffer. """ file_path = self.get_next_partial_movie_path() - temp_file_path = os.path.splitext(file_path)[0] + '_temp' + self.movie_file_extension - + temp_file_path = os.path.splitext(file_path)[0] + '_temp' + self.MOVIE_FILE_EXTENSION self.partial_movie_file_path = file_path self.temp_partial_movie_file_path = temp_file_path @@ -413,7 +406,7 @@ def open_movie_pipe(self): ] # TODO, the test for a transparent background should not be based on # the file extension. - if self.movie_file_extension == ".mov": + if self.MOVIE_FILE_EXTENSION == ".mov": # This is if the background of the exported # video should be transparent. command += [ @@ -455,12 +448,12 @@ def combine_movie_files(self): # single piece. kwargs = { "remove_non_integer_files": True, - "extension": self.movie_file_extension, + "extension": self.MOVIE_FILE_EXTENSION, } - if self.scene.start_at_animation_number is not None: - kwargs["min_index"] = self.scene.start_at_animation_number - if self.scene.end_at_animation_number is not None: - kwargs["max_index"] = self.scene.end_at_animation_number + if self.scene.FROM_ANIMATION_NUMBER is not None: + kwargs["min_index"] = self.scene.FROM_ANIMATION_NUMBER + if self.scene.UPTO_ANIMATION_NUMBER is not None: + kwargs["max_index"] = self.scene.UPTO_ANIMATION_NUMBER else: kwargs["remove_indices_greater_than"] = self.scene.num_plays - 1 partial_movie_files = get_sorted_integer_files( @@ -502,7 +495,7 @@ def combine_movie_files(self): if self.includes_sound: sound_file_path = movie_file_path.replace( - self.movie_file_extension, ".wav" + self.MOVIE_FILE_EXTENSION, ".wav" ) # Makes sure sound file length will match video file self.add_audio_segment(AudioSegment.silent(0)) diff --git a/manim/scene/three_d_scene.py b/manim/scene/three_d_scene.py index 6cfeff671d..bbb226d59d 100644 --- a/manim/scene/three_d_scene.py +++ b/manim/scene/three_d_scene.py @@ -1,7 +1,7 @@ from ..animation.transform import ApplyMethod from ..camera.three_d_camera import ThreeDCamera from ..constants import DEGREES -from ..constants import PRODUCTION_QUALITY_CAMERA_CONFIG +from ..config import config from ..mobject.coordinate_systems import ThreeDAxes from ..mobject.geometry import Line from ..mobject.three_dimensions import Sphere @@ -278,7 +278,7 @@ class SpecialThreeDScene(ThreeDScene): def __init__(self, **kwargs): digest_config(self, kwargs) - if self.camera_config["pixel_width"] == PRODUCTION_QUALITY_CAMERA_CONFIG["pixel_width"]: + if self.camera_config["pixel_width"] == config['CAMERA_CONFIG']['PIXEL_WIDTH']: config = {} else: config = self.low_quality_config @@ -349,4 +349,4 @@ def set_camera_to_default_position(self): """ self.set_camera_orientation( **self.default_angled_camera_position - ) \ No newline at end of file + ) diff --git a/manim/scene/zoomed_scene.py b/manim/scene/zoomed_scene.py index 94d6120f6e..5c88794b1b 100644 --- a/manim/scene/zoomed_scene.py +++ b/manim/scene/zoomed_scene.py @@ -141,4 +141,4 @@ def get_zoom_factor(self): return fdiv( self.zoomed_camera.frame.get_height(), self.zoomed_display.get_height() - ) \ No newline at end of file + ) diff --git a/manim/utils/tex_file_writing.py b/manim/utils/tex_file_writing.py index e3951187d6..72b3f3a202 100644 --- a/manim/utils/tex_file_writing.py +++ b/manim/utils/tex_file_writing.py @@ -3,9 +3,9 @@ from pathlib import Path +from ..config import config from ..constants import TEX_TEXT_TO_REPLACE from ..constants import TEX_USE_CTEX -from .. import dirs from ..logger import logger def tex_hash(expression, template_tex_file_body): @@ -24,7 +24,7 @@ def tex_to_svg_file(expression, template_tex_file_body): def generate_tex_file(expression, template_tex_file_body): result = os.path.join( - dirs.TEX_DIR, + config['TEX_DIR'], tex_hash(expression, template_tex_file_body) ) + ".tex" if not os.path.exists(result): @@ -43,7 +43,7 @@ def tex_to_dvi(tex_file): result = tex_file.replace(".tex", ".dvi" if not TEX_USE_CTEX else ".xdv") result = Path(result).as_posix() tex_file = Path(tex_file).as_posix() - tex_dir = Path(dirs.TEX_DIR).as_posix() + tex_dir = Path(config['TEX_DIR']).as_posix() if not os.path.exists(result): commands = [ "latex", diff --git a/setup.py b/setup.py index 15a0c90a3a..ea2d3cf6c1 100755 --- a/setup.py +++ b/setup.py @@ -28,4 +28,5 @@ "pyreadline; sys_platform == 'win32'", "rich", ], + data_files=[('manim/', ['manim/manim.cfg'])], ) From b86f35890d068a4355045f2cc3b5c7d6a4b1e06e Mon Sep 17 00:00:00 2001 From: leotrs Date: Fri, 29 May 2020 12:51:52 -0400 Subject: [PATCH 05/38] answers some of the requested changes --- manim/config.py | 26 ++++++++++++++------------ manim/{manim.cfg => default.cfg} | 0 setup.py | 2 +- 3 files changed, 15 insertions(+), 13 deletions(-) rename manim/{manim.cfg => default.cfg} (100%) diff --git a/manim/config.py b/manim/config.py index f4fbfd5bff..bb405bd365 100644 --- a/manim/config.py +++ b/manim/config.py @@ -361,13 +361,13 @@ def _init_dirs(config): if not os.path.isdir(config["MEDIA_DIR"]): config["MEDIA_DIR"] = "./media" else: - print( + logger.warning( f"Media will be written to {config['media_dir'] + os.sep}. You can change " "this behavior with the --media_dir flag, or by adjusting manim.cfg." ) else: if config["MEDIA_DIR"]: - print( + logger.warning( "Ignoring --media_dir, since both --tex_dir and --video_dir were passed." ) @@ -377,14 +377,15 @@ def _init_dirs(config): os.makedirs(folder) -config_filename = 'manim.cfg' - -foobar = os.path.join(os.path.dirname(__file__), config_filename) -print(foobar) +# Config files to be parsed, in ascending priority +library_wide = os.path.join(os.path.dirname(__file__), 'default.cfg'), config_files = [ - foobar, - os.path.expanduser('~/.{}'.format(config_filename)), - os.path.join(os.getcwd(), config_filename), + # Lowest priority: library-wide defaults + library_wide, + # Medium priority: look for a 'manim.cfg' in the user home + os.path.expanduser('~/.manim.cfg'), + # Highest priority: look for a 'manim.cfg' in the current dir + os.path.join(os.getcwd(), 'manim.cfg'), ] prog = os.path.split(sys.argv[0])[-1] @@ -393,10 +394,11 @@ def _init_dirs(config): # defaults using CLI arguments args = _parse_cli() - print(args) - + # If the user specified a config file, only use that one and the + # library-wide defaults. if args.config_file is not None: - config_files.append(args.config_file) + config_files = [library_wide, args.config_file] + config, config_parser = _parse_config(args.file, config_files) _update_config_with_args(config, config_parser, args) diff --git a/manim/manim.cfg b/manim/default.cfg similarity index 100% rename from manim/manim.cfg rename to manim/default.cfg diff --git a/setup.py b/setup.py index ea2d3cf6c1..7eab2ca5ea 100755 --- a/setup.py +++ b/setup.py @@ -28,5 +28,5 @@ "pyreadline; sys_platform == 'win32'", "rich", ], - data_files=[('manim/', ['manim/manim.cfg'])], + data_files=[('manim/', ['manim/default.cfg'])], ) From 32f72a0d59312de51000c37da2b3ce418bae8b31 Mon Sep 17 00:00:00 2001 From: leotrs Date: Thu, 4 Jun 2020 23:25:55 -0400 Subject: [PATCH 06/38] split config into two dicts --- example_scenes/basic.py | 2 +- manim/__main__.py | 174 ++++++++++- manim/camera/camera.py | 3 +- manim/camera/moving_camera.py | 12 +- manim/camera/three_d_camera.py | 5 +- manim/config.py | 434 +++++++++++++--------------- manim/constants.py | 22 -- manim/default.cfg | 107 +++---- manim/extract_scene.py | 184 ------------ manim/mobject/coordinate_systems.py | 11 +- manim/mobject/frame.py | 5 +- manim/mobject/functions.py | 7 +- manim/mobject/mobject.py | 2 +- manim/mobject/number_line.py | 7 +- manim/mobject/svg/drawings.py | 3 +- manim/mobject/svg/svg_mobject.py | 1 + manim/mobject/svg/tex_mobject.py | 8 +- manim/mobject/vector_field.py | 23 +- manim/scene/scene.py | 1 + manim/scene/scene_file_writer.py | 8 +- manim/scene/vector_space_scene.py | 15 +- manim/utils/tex_file_writing.py | 23 +- 22 files changed, 504 insertions(+), 553 deletions(-) delete mode 100644 manim/extract_scene.py diff --git a/example_scenes/basic.py b/example_scenes/basic.py index 84f1e2b537..7d6f04c43e 100644 --- a/example_scenes/basic.py +++ b/example_scenes/basic.py @@ -106,7 +106,7 @@ def construct(self): ) group = VGroup(example_text, example_tex) group.arrange(DOWN) - group.set_width(FRAME_WIDTH - 2 * LARGE_BUFF) + group.set_width(config['frame_width'] - 2 * LARGE_BUFF) self.play(Write(example_text)) self.play(Write(example_tex)) diff --git a/manim/__main__.py b/manim/__main__.py index b14c2cad6b..59d1529d39 100644 --- a/manim/__main__.py +++ b/manim/__main__.py @@ -1,12 +1,178 @@ -from .config import initialize_tex, config -from . import extract_scene +import inspect +import itertools as it +import os +import platform +import subprocess as sp +import sys +import traceback +import importlib.util + +from .config import file_writer_config +from .scene.scene import Scene +from .utils.sounds import play_error_sound +from .utils.sounds import play_finish_sound from . import constants +from .logger import logger + + +def open_file_if_needed(file_writer): + if file_writer_config["quiet"]: + curr_stdout = sys.stdout + sys.stdout = open(os.devnull, "w") + + open_file = any([ + file_writer_config["preview"], + file_writer_config["show_file_in_finder"] + ]) + if open_file: + current_os = platform.system() + file_paths = [] + + if file_writer_config["save_last_frame"]: + file_paths.append(file_writer.get_image_file_path()) + if file_writer_config["write_to_movie"]: + file_paths.append(file_writer.get_movie_file_path()) + + for file_path in file_paths: + if current_os == "Windows": + os.startfile(file_path) + else: + commands = [] + if current_os == "Linux": + commands.append("xdg-open") + elif current_os.startswith("CYGWIN"): + commands.append("cygstart") + else: # Assume macOS + commands.append("open") + + if file_writer_config["show_file_in_finder"]: + commands.append("-R") + + commands.append(file_path) + + # commands.append("-g") + FNULL = open(os.devnull, 'w') + sp.call(commands, stdout=FNULL, stderr=sp.STDOUT) + FNULL.close() + + if file_writer_config["quiet"]: + sys.stdout.close() + sys.stdout = curr_stdout + + +def is_child_scene(obj, module): + if not inspect.isclass(obj): + return False + if not issubclass(obj, Scene): + return False + if obj == Scene: + return False + if not obj.__module__.startswith(module.__name__): + return False + return True + + +def prompt_user_for_choice(scene_classes): + num_to_class = {} + for count, scene_class in zip(it.count(1), scene_classes): + name = scene_class.__name__ + print("%d: %s" % (count, name)) + num_to_class[count] = scene_class + try: + user_input = input(constants.CHOOSE_NUMBER_MESSAGE) + return [ + num_to_class[int(num_str)] + for num_str in user_input.split(",") + ] + except KeyError: + logger.error(constants.INVALID_NUMBER_MESSAGE) + sys.exit(2) + user_input = input(constants.CHOOSE_NUMBER_MESSAGE) + return [ + num_to_class[int(num_str)] + for num_str in user_input.split(",") + ] + except EOFError: + sys.exit(1) + + +def get_scenes_to_render(scene_classes): + if not scene_classes: + logger.error(constants.NO_SCENE_MESSAGE) + return [] + if file_writer_config["write_all"]: + return scene_classes + result = [] + for scene_name in file_writer_config["scene_names"]: + found = False + for scene_class in scene_classes: + if scene_class.__name__ == scene_name: + result.append(scene_class) + found = True + break + if not found and (scene_name != ""): + logger.error( + constants.SCENE_NOT_FOUND_MESSAGE.format( + scene_name + ) + ) + if result: + return result + return [scene_classes[0]] if len(scene_classes) == 1 else prompt_user_for_choice(scene_classes) + + +def get_scene_classes_from_module(module): + if hasattr(module, "SCENES_IN_ORDER"): + return module.SCENES_IN_ORDER + else: + return [ + member[1] + for member in inspect.getmembers( + module, + lambda x: is_child_scene(x, module) + ) + ] + + +def get_module(file_name): + if file_name == "-": + module = types.ModuleType("input_scenes") + code = sys.stdin.read() + try: + exec(code, module.__dict__) + return module + except Exception as e: + logger.error(f"Failed to render scene: {str(e)}") + sys.exit(2) + else: + if os.path.exists(file_name): + module_name = file_name.replace(os.sep, ".").replace(".py", "") + spec = importlib.util.spec_from_file_location(module_name, file_name) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + else: + raise FileNotFoundError(f'{file_name} not found') def main(): - initialize_tex(config) - extract_scene.main(config) + module = get_module(file_writer_config["input_file"]) + all_scene_classes = get_scene_classes_from_module(module) + scene_classes_to_render = get_scenes_to_render(all_scene_classes) + for SceneClass in scene_classes_to_render: + try: + # By invoking, this renders the full scene + scene = SceneClass(**{}) + open_file_if_needed(scene.file_writer) + if file_writer_config["sound"]: + play_finish_sound() + except Exception: + print("\n\n") + traceback.print_exc() + print("\n\n") + if file_writer_config["sound"]: + play_error_sound() if __name__ == "__main__": diff --git a/manim/camera/camera.py b/manim/camera/camera.py index 405890f224..0216b5c71a 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -10,6 +10,7 @@ import numpy as np from ..constants import * +from ..config import config from ..logger import logger from ..mobject.types.image_mobject import AbstractImageMobject from ..mobject.mobject import Mobject @@ -767,7 +768,7 @@ def apply_stroke(self, ctx, vmobject, background=False): width * self.cairo_line_width_multiple * # This ensures lines have constant width # as you zoom in on them. - (self.get_frame_width() / FRAME_WIDTH) + (self.get_frame_width() / self.frame_width) ) ctx.stroke_preserve() return self diff --git a/manim/camera/moving_camera.py b/manim/camera/moving_camera.py index 9bc928c8aa..a092db1dab 100644 --- a/manim/camera/moving_camera.py +++ b/manim/camera/moving_camera.py @@ -1,8 +1,6 @@ from ..camera.camera import Camera -from ..constants import FRAME_HEIGHT -from ..constants import FRAME_WIDTH -from ..constants import ORIGIN -from ..constants import WHITE +from ..config import config +from ..constants import ORIGIN, WHITE from ..mobject.frame import ScreenRectangle from ..mobject.types.vectorized_mobject import VGroup from ..utils.config_ops import digest_config @@ -11,8 +9,8 @@ # TODO, think about how to incorporate perspective class CameraFrame(VGroup): CONFIG = { - "width": FRAME_WIDTH, - "height": FRAME_HEIGHT, + "width": config['frame_width'], + "height": config['frame_height'], "center": ORIGIN, } @@ -153,4 +151,4 @@ def get_mobjects_indicating_movement(self): ------- list """ - return [self.frame] \ No newline at end of file + return [self.frame] diff --git a/manim/camera/three_d_camera.py b/manim/camera/three_d_camera.py index c5e7dfc1ca..339555c3a8 100644 --- a/manim/camera/three_d_camera.py +++ b/manim/camera/three_d_camera.py @@ -2,6 +2,7 @@ from ..camera.camera import Camera from ..constants import * +from ..config import config from ..mobject.three_d_utils import get_3d_vmob_end_corner from ..mobject.three_d_utils import get_3d_vmob_end_corner_unit_normal from ..mobject.three_d_utils import get_3d_vmob_start_corner @@ -26,7 +27,7 @@ class ThreeDCamera(Camera): "frame_center": ORIGIN, "should_apply_shading": True, "exponential_projection": False, - "max_allowable_norm": 3 * FRAME_WIDTH, + "max_allowable_norm": 3 * config['frame_width'], } def __init__(self, *args, **kwargs): @@ -406,4 +407,4 @@ def remove_fixed_in_frame_mobjects(self, *mobjects): """ for mobject in self.extract_mobject_family_members(mobjects): if mobject in self.fixed_in_frame_mobjects: - self.fixed_in_frame_mobjects.remove(mobject) \ No newline at end of file + self.fixed_in_frame_mobjects.remove(mobject) diff --git a/manim/config.py b/manim/config.py index e49c29976c..9221314691 100644 --- a/manim/config.py +++ b/manim/config.py @@ -14,97 +14,180 @@ import colour from .utils.tex import TexTemplateFromFile, TexTemplate from .logger import logger +from . import constants __all__ = ["config", "register_tex_template", "initialize_tex"] -def _parse_config(input_file, config_files): - # This only loads the [CLI] section of the manim.cfg file. If using - # CLI arguments, the _update_config_with_args function will take care - # of overriding any of these defaults. - config_parser = configparser.ConfigParser() - successfully_read_files = config_parser.read(config_files) - if not successfully_read_files: - raise FileNotFoundError('Config file could not be read') +def _parse_config(config_parser, args): + """Parse config files and CLI arguments into a single dictionary.""" + # By default, use the CLI section of the digested .cfg files + default = config_parser['CLI'] - # Put everything in a dict + # This will be the final config dict exposed to the user config = {} - # Make sure we have an input file - config['INPUT_FILE'] = input_file - - # ConfigParser options are all strings, so need to convert to the - # appropriate type - - # booleans - for opt in ['PREVIEW', 'SHOW_FILE_IN_FINDER', 'QUIET', 'SOUND', - 'LEAVE_PROGRESS_BARS', 'WRITE_ALL', 'WRITE_TO_MOVIE', - 'SAVE_LAST_FRAME', 'DRY_RUN', 'SAVE_PNGS', 'SAVE_AS_GIF']: - config[opt] = config_parser['CLI'].getboolean(opt) - - # numbers - for opt in ['FROM_ANIMATION_NUMBER', 'UPTO_ANIMATION_NUMBER', - 'BACKGROUND_OPACITY']: - config[opt] = config_parser['CLI'].getint(opt) - - # UPTO_ANIMATION_NUMBER is special because -1 actually means np.inf - if config['UPTO_ANIMATION_NUMBER'] == -1: - import numpy as np - config['UPTO_ANIMATION_NUMBER'] = np.inf - - # strings - for opt in ['PNG_MODE', 'MOVIE_FILE_EXTENSION', 'MEDIA_DIR', - 'OUTPUT_FILE', 'VIDEO_DIR', 'TEX_DIR', 'TEXT_DIR', - 'BACKGROUND_COLOR']: - config[opt] = config_parser['CLI'][opt] - - # streaming section -- all values are strings - config['STREAMING'] = {} - for opt in ['LIVE_STREAM_NAME', 'TWITCH_STREAM_KEY', - 'STREAMING_PROTOCOL', 'STREAMING_PROTOCOL', 'STREAMING_IP', - 'STREAMING_PORT', 'STREAMING_PORT', 'STREAMING_CLIENT', - 'STREAMING_CONSOLE_BANNER']: - config['STREAMING'][opt] = config_parser['streaming'][opt] - - # for internal use (no CLI flag) - config['SKIP_ANIMATIONS'] = any([ - config['SAVE_LAST_FRAME'], - config['FROM_ANIMATION_NUMBER'], - ]) - - # camera config -- all happen to be integers - config['CAMERA_CONFIG'] = {} - for opt in ['FRAME_RATE', 'PIXEL_HEIGHT', 'PIXEL_WIDTH']: - config['CAMERA_CONFIG'][opt] = config_parser['CLI'].getint(opt) - - # file writer config -- just pull them from the overall config for now - config['FILE_WRITER_CONFIG'] = {key: config[key] for key in [ - "WRITE_TO_MOVIE", "SAVE_LAST_FRAME", "SAVE_PNGS", - "SAVE_AS_GIF", "PNG_MODE", "MOVIE_FILE_EXTENSION", - "OUTPUT_FILE", "INPUT_FILE"]} - - return config, config_parser - - -def _parse_cli(): + # Handle the *_quality flags. These determine the section to read + # and are stored in 'camera_config'. Note the highest resolution + # passed as argument will be used. + for flag in ['fourk_quality', 'high_quality', 'medium_quality', 'low_quality']: + if getattr(args, flag): + section = config_parser[flag] + break + else: + section = config_parser['CLI'] + config = {opt: section.getint(opt) for opt in config_parser[flag]} + + # The -r, --resolution flag overrides the *_quality flags + if args.resolution is not None: + if "," in args.resolution: + height_str, width_str = args.resolution.split(",") + height, width = int(height_str), int(width_str) + else: + height, width = int(args.resolution), int(16 * height / 9) + config['camera_config'].update({'pixel_height': height, + 'pixel_width': width}) + + # Handle the -c (--color) flag + if args.color is not None: + try: + background_color = colour.Color(args.color) + except AttributeError as err: + logger.warning('Please use a valid color.') + logger.error(err) + sys.exit(2) + else: + background_color = colour.Color(default['background_color']) + config['background_color'] = background_color + + # Set the rest of the frame properties + config['frame_height'] = 8.0 + config['frame_width'] = (config['frame_height'] + * config['pixel_width'] + / config['pixel_height']) + config['frame_y_radius'] = config['frame_height'] / 2 + config['frame_x_radius'] = config['frame_width'] / 2 + config['top'] = config['frame_y_radius'] * constants.UP + config['bottom'] = config['frame_y_radius'] * constants.DOWN + config['left_side'] = config['frame_x_radius'] * constants.LEFT + config['right_side'] = config['frame_x_radius'] * constants.RIGHT + + # Hangle the --tex_template flag. Note we accept None if the flag is absent + filename = (os.path.expanduser(args.tex_template) + if args.tex_template is not None + else None) + + if filename is not None and not os.access(filename, os.R_OK): + # custom template not available, fallback to default + logger.warning( + f"Custom TeX template {filename} not found or not readable. " + "Falling back to the default template." + ) + filename = None + config['tex_template_file'] = filename + config['tex_template'] = (TexTemplateFromFile(filename=filename) + if filename is not None + else TexTemplate()) + + return config + + +def _parse_file_writer_config(config_parser, args): + """Parse config files and CLI arguments into a single dictionary.""" + # By default, use the CLI section of the digested .cfg files + default = config_parser['CLI'] + + # This will be the final config dict exposed to the user + config = {} + + # Handle input files and scenes. Note these cannot be set from + # the .cfg files, only from CLI arguments + config['input_file'] = args.file + config['scene_names'] = (args.scene_names + if args.scene_names is not None else []) + + # Read some options that cannot be overriden by CLI arguments + for opt in ['gif_file_extension']: + config[opt] = default[opt] + + # Handle all options that are directly overridden by CLI + # arguments. Note ConfigParser options are all strings and each + # needs to be converted to the appropriate type. Thus, we do this + # in batches, depending on their type: booleans and strings + for boolean_opt in ['preview', 'show_file_in_finder', 'quiet', + 'sound', 'leave_progress_bars']: + config[boolean_opt] = (default.getboolean(boolean_opt) + if getattr(args, boolean_opt) is None + else getattr(args, boolean_opt)) + for str_opt in ['media_dir', 'video_dir', 'tex_dir', + 'text_dir']: + config[str_opt] = (default[str_opt] + if getattr(args, str_opt) is None + else getattr(args, str_opt)) + + # Handle the -t (--transparent) flag. This flag determines which + # section to use from the .cfg file. + section = config_parser['transparent'] if args.transparent else default + for opt in ['png_mode', 'movie_file_extension', + 'background_opacity']: + config[opt] = section[opt] + + # Handle the -n flag. Read first from the cfg and then override with CLI. + # These two are integers -- use getint() + for opt in ['from_animation_number', 'upto_animation_number']: + config[opt] = default.getint(opt) + if config['upto_animation_number'] == -1: + config['upto_animation_number'] = float('inf') + nflag = args.from_animation_number + if nflag is not None: + if ',' in nflag: + start, end = nflag.split(',') + config['from_animation_number'] = int(start) + config['upto_animation_number'] = int(end) + else: + config['from_animation_number'] = int(nflag) + + # Handle the --dry_run flag. This flag determines which section + # to use from the .cfg file. All options involved are boolean. + section = config_parser['dry_run'] if args.dry_run else default + for opt in ['write_to_movie', 'write_all', 'save_last_frame', 'save_pngs', 'save_as_gif']: + config[opt] = section.getboolean(opt) + + # Read in the streaming section -- all values are strings + config['streaming'] = {opt: config_parser['streaming'][opt] + for opt in ['live_stream_name', 'twitch_stream_key', + 'streaming_protocol', 'streaming_ip', + 'streaming_protocol', 'streaming_client', + 'streaming_port', 'streaming_port', + 'streaming_console_banner']} + + # For internal use (no CLI flag) + config['skip_animations'] = any([config['save_last_frame'], + config['from_animation_number']]) + + return config + + +def _parse_cli(arg_list, input=True): parser = argparse.ArgumentParser( description='Animation engine for explanatory math videos', epilog='Made with <3 by the manim community devs' ) - parser.add_argument( - "file", - help="path to file holding the python code for the scene", - ) - parser.add_argument( - "--scene_names", - nargs="*", - help="Name of the Scene class you want to see", - ) - parser.add_argument( - "-o", "--output_file", - help="Specify the name of the output file, if" - "it should be different from the scene class name", - ) + if input: + parser.add_argument( + "file", + help="path to file holding the python code for the scene", + ) + parser.add_argument( + "scene_names", + nargs="*", + help="Name of the Scene class you want to see", + ) + parser.add_argument( + "-o", "--output_file", + help="Specify the name of the output file, if" + "it should be different from the scene class name", + ) # Note the following use (action='store_const', const=True), # instead of using the built-in (action='store_true'). The reason @@ -270,184 +353,77 @@ def _parse_cli(): help="Specify the configuration file", ) - return parser.parse_args() - - -def _update_config_with_args(config, config_parser, args): - # Take care of options that do not have defaults in manim.cfg - config['FILE'] = args.file - config['SCENE_NAMES'] = (args.scene_names - if args.scene_names is not None else []) - - # Flags that directly override config defaults. - for opt in ['PREVIEW', 'SHOW_FILE_IN_FINDER', 'QUIET', 'SOUND', - 'LEAVE_PROGRESS_BARS', 'WRITE_ALL', 'WRITE_TO_MOVIE', - 'SAVE_LAST_FRAME', 'SAVE_PNGS', 'SAVE_AS_GIF', 'MEDIA_DIR', - 'VIDEO_DIR', 'TEX_DIR', 'TEXT_DIR', 'BACKGROUND_OPACITY', - 'OUTPUT_FILE']: - if getattr(args, opt.lower()) is not None: - config[opt] = getattr(args, opt.lower()) - - # Parse the -n flag. - nflag = args.from_animation_number - if nflag is not None: - if ',' in nflag: - start, end = nflag.split(',') - config['FROM_ANIMATION_NUMBER'] = int(start) - config['UPTO_ANIMATION_NUMBER'] = int(end) - else: - config['FROM_ANIMATION_NUMBER'] = int(nflag) - - # The following flags use the options in the corresponding manim.cfg - # sections to override default options. For example, passing the -t - # (--transparent) flag takes all of the options defined in the - # [transparent] section of manim.cfg and uses their values to override - # the values of those options defined in CLI. - - # -t, --transparent - if args.transparent: - config.update({ - 'PNG_MODE': config_parser['transparent']['PNG_MODE'], - 'MOVIE_FILE_EXTENSION': config_parser['transparent']['MOVIE_FILE_EXTENSION'], - 'BACKGROUND_OPACITY': config_parser['transparent'].getfloat('BACKGROUND_OPACITY') - }) - - # --dry_run happens to override options that are all booleans - if args.dry_run: - config.update({opt.upper(): config_parser['dry_run'].getboolean(opt) - for opt in config_parser['dry_run']}) - - # the *_quality arguments happen to override options that are all ints - for flag in ['fourk_quality', 'high_quality', 'medium_quality', - 'low_quality']: - if getattr(args, flag) is not None: - config['CAMERA_CONFIG'].update( - {opt.upper(): config_parser[flag].getint(opt) - for opt in config_parser[flag]}) - - # Parse the -r (--resolution) flag. Note the -r flag does not - # correspond to any section in manim.cfg, but overrides the same - # options as the *_quality sections. - if args.resolution is not None: - if "," in args.resolution: - height_str, width_str = args.resolution.split(",") - height = int(height_str) - width = int(width_str) - else: - height = int(args.resolution) - width = int(16 * height / 9) - config['CAMERA_CONFIG'].update({'pixel_height': height, - 'pixel_width': width}) - - # Parse the -c (--color) flag - if args.color is not None: - try: - config['CAMERA_CONFIG']['BACKGROUND_COLOR'] = colour.Color(args.color) - except AttributeError as err: - logger.warning('Please use a valid color') - logger.error(err) - sys.exit(2) - - # As before, make FILE_WRITER_CONFIG by pulling form the overall - config['FILE_WRITER_CONFIG'] = {key: config[key] for key in [ - "WRITE_TO_MOVIE", "SAVE_LAST_FRAME", "SAVE_PNGS", - "SAVE_AS_GIF", "PNG_MODE", "MOVIE_FILE_EXTENSION", - "OUTPUT_FILE", "INPUT_FILE"]} - - return config + return parser.parse_args(arg_list) def _init_dirs(config): - if not (config["VIDEO_DIR"] and config["TEX_DIR"]): - if config["MEDIA_DIR"]: - if not os.path.isdir(config["MEDIA_DIR"]): - os.makedirs(config["MEDIA_DIR"]) - if not os.path.isdir(config["MEDIA_DIR"]): - config["MEDIA_DIR"] = "./media" + if not (config["video_dir"] and config["tex_dir"]): + if config["media_dir"]: + if not os.path.isdir(config["media_dir"]): + os.makedirs(config["media_dir"]) + if not os.path.isdir(config["media_dir"]): + config["media_dir"] = "./media" else: logger.warning( f"Media will be written to {config['media_dir'] + os.sep}. You can change " "this behavior with the --media_dir flag, or by adjusting manim.cfg." ) else: - if config["MEDIA_DIR"]: + if config["media_dir"]: logger.warning( "Ignoring --media_dir, since both --tex_dir and --video_dir were passed." ) # Make sure all folders exist - for folder in [config["VIDEO_DIR"], config["TEX_DIR"], config["TEXT_DIR"]]: + for folder in [config["video_dir"], config["tex_dir"], config["text_dir"]]: if not os.path.exists(folder): os.makedirs(folder) -def register_tex_template(tpl): - """Register the given LaTeX template for later use. - - Parameters - ---------- - tpl : :class:`~.TexTemplate` - The LaTeX template to register. - """ - config['tex_template'] = tpl +def _from_command_line(): + """Determine if manim was called from the command line.""" + # Manim can be called from the command line in three different + # ways. The first two involve using the manim or manimcm commands + prog = os.path.split(sys.argv[0])[-1] + from_cli_command = prog in ['manim', 'manimcm'] + # The third way involves using `python -m manim ...`. In this + # case, the CLI arguments passed to manim do not include 'manim', + # 'manimcm', or even 'python'. However, the -m flag will always + # be the first argument. + from_python_m = sys.argv[0] == '-m' -def initialize_tex(config): - """Safely create a LaTeX template object from a file. - If file is not readable, the default template file is used. - - Parameters - ---------- - filename : :class:`str` - The name of the file with the LaTeX template. - """ - filename = "" - if config["tex_template"]: - filename = os.path.expanduser(config["tex_template"]) - if filename and not os.access(filename, os.R_OK): - # custom template not available, fallback to default - logger.warning( - f"Custom TeX template {filename} not found or not readable. " - "Falling back to the default template." - ) - filename = "" - if filename: - # still having a filename -> use the file - config['tex_template'] = TexTemplateFromFile(filename=filename) - else: - # use the default template - config['tex_template'] = TexTemplate() + return from_cli_command or from_python_m # Config files to be parsed, in ascending priority -library_wide = os.path.join(os.path.dirname(__file__), 'default.cfg'), +library_wide = os.path.join(os.path.dirname(__file__), 'default.cfg') config_files = [ - # Lowest priority: library-wide defaults library_wide, - # Medium priority: look for a 'manim.cfg' in the user home os.path.expanduser('~/.manim.cfg'), - # Highest priority: look for a 'manim.cfg' in the current dir os.path.join(os.getcwd(), 'manim.cfg'), - ] +] -prog = os.path.split(sys.argv[0])[-1] -if prog in ['manim', 'manimcm']: - # If called as entrypoint, set default config, and override the - # defaults using CLI arguments - args = _parse_cli() - - # If the user specified a config file, only use that one and the - # library-wide defaults. +if _from_command_line(): + args = _parse_cli(sys.argv) if args.config_file is not None: - config_files = [library_wide, args.config_file] - - config, config_parser = _parse_config(args.file, config_files) - _update_config_with_args(config, config_parser, args) + config_files.append(args.config_file) else: - config, _ = _parse_config('', config_files) + # In this case, we still need an empty args object. + args = _parse_cli([], input=False) + # Need to populate the options left out + args.file, args.scene_names, args.output_file = '', '', '' + +config_parser = configparser.ConfigParser() +successfully_read_files = config_parser.read(config_files) +logger.info(f'Read configuration files: {successfully_read_files}') + +# this is for internal use when writing output files +file_writer_config = _parse_file_writer_config(config_parser, args) + +# this is for the user +config = _parse_config(config_parser, args) +camera_config = config -######### ########## ########## -# make sure to set dirs.TEX_DIR somewhere -######### ########## ########## -_init_dirs(config) +_init_dirs(file_writer_config) diff --git a/manim/constants.py b/manim/constants.py index f73f74a6e2..45fe51bfed 100644 --- a/manim/constants.py +++ b/manim/constants.py @@ -1,7 +1,4 @@ import numpy as np -import os -from .config import config - # Messages NOT_SETTING_FONT_MSG = ''' @@ -15,7 +12,6 @@ class MyText(Text): 'font': 'My Font' } ''' - SCENE_NOT_FOUND_MESSAGE = """ {} is not in the script """ @@ -58,21 +54,6 @@ class MyText(Text): START_X = 30 START_Y = 20 -# Geometry: frame -FRAME_HEIGHT = 8.0 -PIXEL_WIDTH = config['CAMERA_CONFIG']['PIXEL_WIDTH'] -PIXEL_HEIGHT = config['CAMERA_CONFIG']['PIXEL_HEIGHT'] -FRAME_RATE = config['CAMERA_CONFIG']['FRAME_RATE'] -FRAME_WIDTH = FRAME_HEIGHT * PIXEL_WIDTH / PIXEL_HEIGHT -FRAME_Y_RADIUS = FRAME_HEIGHT / 2 -FRAME_X_RADIUS = FRAME_WIDTH / 2 - -# Geometry: sides -TOP = FRAME_Y_RADIUS * UP -BOTTOM = FRAME_Y_RADIUS * DOWN -LEFT_SIDE = FRAME_X_RADIUS * LEFT -RIGHT_SIDE = FRAME_X_RADIUS * RIGHT - # Default buffers (padding) SMALL_BUFF = 0.1 MED_SMALL_BUFF = 0.25 @@ -90,9 +71,6 @@ class MyText(Text): DEFAULT_POINT_DENSITY_1D = 250 DEFAULT_STROKE_WIDTH = 4 -# Tex stuff -TEX_TEMPLATE = None - # Mathematical constants PI = np.pi TAU = 2 * PI diff --git a/manim/default.cfg b/manim/default.cfg index 472df34d0c..68996ad611 100644 --- a/manim/default.cfg +++ b/manim/default.cfg @@ -15,127 +15,130 @@ # generate a movie file. Note all of the following accept boolean values. # -w, --write_to_movie -WRITE_TO_MOVIE = True +write_to_movie = True # -a, --write_all -WRITE_ALL = False +write_all = False # -s, --save_last_frame -SAVE_LAST_FRAME = False +save_last_frame = False # -g, --save_pngs -SAVE_PNGS = False +save_pngs = False # -i, --save_as_gif -SAVE_AS_GIF = False +save_as_gif = False # -p, --preview -PREVIEW = False +preview = False # -f, --show_file_in_finder -SHOW_FILE_IN_FINDER = False +show_file_in_finder = False # -q, --quiet -QUIET = False +quiet = False # --sound -SOUND = False +sound = False # -o, --output_file -OUTPUT_FILE = +output_file = # --leave_progress_bars -LEAVE_PROGRESS_BARS = False +leave_progress_bars = False # -c, --color -BACKGROUND_COLOR = BLACK +background_color = BLACK # --background_opacity -BACKGROUND_OPACITY = 1 +background_opacity = 1 # The following two are both set by the -n (or --from_animation_number) # flag. See manim -h for more information. -FROM_ANIMATION_NUMBER = 0 +from_animation_number = 0 # Use -1 to render all animations -UPTO_ANIMATION_NUMBER = -1 +upto_animation_number = -1 # The following are meant to be paths relative to the point of execution. # Set them at the CLI with the corresponding flag, or set their default # values here. # --media_dir -MEDIA_DIR = ./media +media_dir = ./media # --video_dir -VIDEO_DIR = %(MEDIA_DIR)s/videos +video_dir = %(MEDIA_DIR)s/videos # --tex_dir -TEX_DIR = %(MEDIA_DIR)s/Tex +tex_dir = %(MEDIA_DIR)s/Tex # --text_dir -TEXT_DIR = %(MEDIA_DIR)s/texts +text_dir = %(MEDIA_DIR)s/texts + +# This value cannot be changed through CLI +gif_file_extension = .gif # If the -t (--transparent) flag is used, these will be replaced with the # values specified in the [TRANSPARENT] section later in this file. -PNG_MODE = RGB -MOVIE_FILE_EXTENSION = .mp4 +png_mode = RGB +movie_file_extension = .mp4 # These can be overriden with any of -l (--low_quality), -m # (--medium_quality), -e (--high_quality), or -k (--fourk_quality). The # overriding values are found in the corresponding sections. -FRAME_RATE = 60 -PIXEL_HEIGHT = 1440 -PIXEL_WIDTH = 2560 +frame_rate = 60 +pixel_height = 1440 +pixel_width = 2560 # These override the previous by using -t, --transparent [transparent] -PNG_MODE = RGBA -MOVIE_FILE_EXTENSION = .mov -BACKGROUND_OPACITY = 0 +png_mode = RGBA +movie_file_extension = .mov +background_opacity = 0 # These override the previous by using -k, --four_k [fourk_quality] -PIXEL_HEIGHT = 2160 -PIXEL_WIDTH = 3840 -FRAME_RATE = 60 +pixel_height = 2160 +pixel_width = 3840 +frame_rate = 60 # These override the previous by using -e, --high_quality [high_quality] -PIXEL_HEIGHT = 1080 -PIXEL_WIDTH = 1920 -FRAME_RATE = 60 +pixel_height = 1080 +pixel_width = 1920 +frame_rate = 60 # These override the previous by using -m, --medium_quality [medium_quality] -PIXEL_HEIGHT = 720 -PIXEL_WIDTH = 1280 -FRAME_RATE = 30 +pixel_height = 720 +pixel_width = 1280 +frame_rate = 30 # These override the previous by usnig -l, --low_quality [low_quality] -PIXEL_HEIGHT = 480 -PIXEL_WIDTH = 854 -FRAME_RATE = 15 +pixel_height = 480 +pixel_width = 854 +frame_rate = 15 # These override the previous by using --dry_run # Note --dry_run overrides all of -w, -a, -s, -g, -i [dry_run] -WRITE_TO_MOVIE = False -WRITE_ALL = False -SAVE_LAST_FRAME = False -SAVE_PNGS = False -SAVE_AS_GIF = False +write_to_movie = False +write_all = False +save_last_frame = False +save_pngs = False +save_as_gif = False # Streaming settings [streaming] -LIVE_STREAM_NAME = LiveStream -TWITCH_STREAM_KEY = YOUR_STREAM_KEY -STREAMING_PROTOCOL = tcp -STREAMING_IP = 127.0.0.1 -STREAMING_PORT = 2000 -STREAMING_CLIENT = ffplay -STREAMING_URL = %(STREAMING_PROTOCOL)s://%(STREAMING_IP)s:%(STREAMING_PORT)s?listen -STREAMING_CONSOLE_BANNER = Manim is now running in streaming mode. +live_stream_name = LiveStream +twitch_stream_key = YOUR_STREAM_KEY +streaming_protocol = tcp +streaming_ip = 127.0.0.1 +streaming_port = 2000 +streaming_client = ffplay +streaming_url = %(STREAMING_PROTOCOL)s://%(STREAMING_IP)s:%(STREAMING_PORT)s?listen +streaming_console_banner = Manim is now running in streaming mode. Stream animations by passing them to manim.play(), e.g. >>> c = Circle() >>> manim.play(ShowCreation(c)) diff --git a/manim/extract_scene.py b/manim/extract_scene.py deleted file mode 100644 index 323833b3a6..0000000000 --- a/manim/extract_scene.py +++ /dev/null @@ -1,184 +0,0 @@ -import inspect -import itertools as it -import os -import platform -import subprocess as sp -import sys -import traceback -import importlib.util - -from .scene.scene import Scene -from .utils.sounds import play_error_sound -from .utils.sounds import play_finish_sound -from . import constants -from .logger import logger - - -def open_file_if_needed(file_writer, **config): - if config["QUIET"]: - curr_stdout = sys.stdout - sys.stdout = open(os.devnull, "w") - - open_file = any([ - config["PREVIEW"], - config["SHOW_FILE_IN_FINDER"] - ]) - if open_file: - current_os = platform.system() - file_paths = [] - - if config["FILE_WRITER_CONFIG"]["SAVE_LAST_FRAME"]: - file_paths.append(file_writer.get_image_file_path()) - if config["FILE_WRITER_CONFIG"]["WRITE_TO_MOVIE"]: - file_paths.append(file_writer.get_movie_file_path()) - - for file_path in file_paths: - if current_os == "Windows": - os.startfile(file_path) - else: - commands = [] - if current_os == "Linux": - commands.append("xdg-open") - elif current_os.startswith("CYGWIN"): - commands.append("cygstart") - else: # Assume macOS - commands.append("open") - - if config["SHOW_FILE_IN_FINDER"]: - commands.append("-R") - - commands.append(file_path) - - # commands.append("-g") - FNULL = open(os.devnull, 'w') - sp.call(commands, stdout=FNULL, stderr=sp.STDOUT) - FNULL.close() - - if config["QUIET"]: - sys.stdout.close() - sys.stdout = curr_stdout - - -def is_child_scene(obj, module): - if not inspect.isclass(obj): - return False - if not issubclass(obj, Scene): - return False - if obj == Scene: - return False - if not obj.__module__.startswith(module.__name__): - return False - return True - - -def prompt_user_for_choice(scene_classes): - num_to_class = {} - for count, scene_class in zip(it.count(1), scene_classes): - name = scene_class.__name__ - print("%d: %s" % (count, name)) - num_to_class[count] = scene_class - try: - user_input = input(constants.CHOOSE_NUMBER_MESSAGE) - return [ - num_to_class[int(num_str)] - for num_str in user_input.split(",") - ] - except KeyError: - logger.error(constants.INVALID_NUMBER_MESSAGE) - sys.exit(2) - user_input = input(constants.CHOOSE_NUMBER_MESSAGE) - return [ - num_to_class[int(num_str)] - for num_str in user_input.split(",") - ] - except EOFError: - sys.exit(1) - - -def get_scenes_to_render(scene_classes, config): - if not scene_classes: - logger.error(constants.NO_SCENE_MESSAGE) - return [] - if config["WRITE_ALL"]: - return scene_classes - result = [] - for scene_name in config["SCENE_NAMES"]: - found = False - for scene_class in scene_classes: - if scene_class.__name__ == scene_name: - result.append(scene_class) - found = True - break - if not found and (scene_name != ""): - logger.error( - constants.SCENE_NOT_FOUND_MESSAGE.format( - scene_name - ) - ) - if result: - return result - return [scene_classes[0]] if len(scene_classes) == 1 else prompt_user_for_choice(scene_classes) - - -def get_scene_classes_from_module(module): - if hasattr(module, "SCENES_IN_ORDER"): - return module.SCENES_IN_ORDER - else: - return [ - member[1] - for member in inspect.getmembers( - module, - lambda x: is_child_scene(x, module) - ) - ] - - -def get_module(file_name): - if file_name == "-": - module = types.ModuleType("input_scenes") - code = sys.stdin.read() - try: - exec(code, module.__dict__) - return module - except Exception as e: - logger.error(f"Failed to render scene: {str(e)}") - sys.exit(2) - else: - if os.path.exists(file_name): - module_name = file_name.replace(os.sep, ".").replace(".py", "") - spec = importlib.util.spec_from_file_location(module_name, file_name) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - else: - raise FileNotFoundError(f'{file_name} not found') - - -def main(config): - module = get_module(config["FILE"]) - all_scene_classes = get_scene_classes_from_module(module) - scene_classes_to_render = get_scenes_to_render(all_scene_classes, config) - - keys = [ - "CAMERA_CONFIG", - "FILE_WRITER_CONFIG", - "SKIP_ANIMATIONS", - "FROM_ANIMATION_NUMBER", - "UPTO_ANIMATION_NUMBER", - "LEAVE_PROGRESS_BARS", - ] - scene_kwargs = {key: config[key] for key in keys} - - for SceneClass in scene_classes_to_render: - try: - # By invoking, this renders the full scene - scene = SceneClass(**scene_kwargs) - open_file_if_needed(scene.file_writer, **config) - if config["SOUND"]: - play_finish_sound() - except Exception: - print("\n\n") - traceback.print_exc() - print("\n\n") - if config["SOUND"]: - play_error_sound() diff --git a/manim/mobject/coordinate_systems.py b/manim/mobject/coordinate_systems.py index 10b2e2dca8..a509984059 100644 --- a/manim/mobject/coordinate_systems.py +++ b/manim/mobject/coordinate_systems.py @@ -2,6 +2,7 @@ import numbers from ..constants import * +from ..config import config from ..mobject.functions import ParametricFunction from ..mobject.geometry import Arrow from ..mobject.geometry import Line @@ -22,10 +23,10 @@ class CoordinateSystem(): """ CONFIG = { "dimension": 2, - "x_min": -FRAME_X_RADIUS, - "x_max": FRAME_X_RADIUS, - "y_min": -FRAME_Y_RADIUS, - "y_max": FRAME_Y_RADIUS, + "x_min": -config['frame_x_radius'], + "x_max": config['frame_x_radius'], + "y_min": -config['frame_y_radius'], + "y_max": config['frame_y_radius'], } def coords_to_point(self, *coords): @@ -429,4 +430,4 @@ def get_coordinate_labels(self, *numbers, **kwargs): def add_coordinates(self, *numbers): self.add(self.get_coordinate_labels(*numbers)) - return self \ No newline at end of file + return self diff --git a/manim/mobject/frame.py b/manim/mobject/frame.py index 7b45722d69..dfa096bd73 100644 --- a/manim/mobject/frame.py +++ b/manim/mobject/frame.py @@ -1,4 +1,5 @@ from ..constants import * +from ..config import config from ..mobject.geometry import Rectangle from ..utils.config_ops import digest_config @@ -19,7 +20,7 @@ def __init__(self, **kwargs): class FullScreenRectangle(ScreenRectangle): CONFIG = { - "height": FRAME_HEIGHT, + "height": config['frame_height'], } @@ -44,4 +45,4 @@ def __init__(self, **kwargs): width=self.aspect_ratio * self.height, height=self.height, **kwargs - ) \ No newline at end of file + ) diff --git a/manim/mobject/functions.py b/manim/mobject/functions.py index f31ad0d63e..d6a5fd04c7 100644 --- a/manim/mobject/functions.py +++ b/manim/mobject/functions.py @@ -1,4 +1,5 @@ from ..constants import * +from ..config import config from ..mobject.types.vectorized_mobject import VMobject from ..utils.config_ops import digest_config import math @@ -80,8 +81,8 @@ def generate_points(self): class FunctionGraph(ParametricFunction): CONFIG = { "color": YELLOW, - "x_min": -FRAME_X_RADIUS, - "x_max": FRAME_X_RADIUS, + "x_min": -config['frame_x_radius'], + "x_max": config['frame_x_radius'], } def __init__(self, function, **kwargs): @@ -101,4 +102,4 @@ def get_function(self): return self.function def get_point_from_function(self, x): - return self.parametric_function(x) \ No newline at end of file + return self.parametric_function(x) diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py index 52f6336bc2..b992de86a9 100644 --- a/manim/mobject/mobject.py +++ b/manim/mobject/mobject.py @@ -389,7 +389,7 @@ def align_on_border(self, direction, buff=DEFAULT_MOBJECT_TO_EDGE_BUFFER): Direction just needs to be a vector pointing towards side or corner in the 2d plane. """ - target_point = np.sign(direction) * (FRAME_X_RADIUS, FRAME_Y_RADIUS, 0) + target_point = np.sign(direction) * (config['frame_x_radius'], config['frame_y_radius'], 0) point_to_align = self.get_critical_point(direction) shift_val = target_point - point_to_align - buff * np.array(direction) shift_val = shift_val * abs(np.sign(direction)) diff --git a/manim/mobject/number_line.py b/manim/mobject/number_line.py index 1ae4dab2e7..d2fe005038 100644 --- a/manim/mobject/number_line.py +++ b/manim/mobject/number_line.py @@ -1,6 +1,7 @@ import operator as op from ..constants import * +from ..config import config from ..mobject.geometry import Line from ..mobject.numbers import DecimalNumber from ..mobject.types.vectorized_mobject import VGroup @@ -14,8 +15,8 @@ class NumberLine(Line): CONFIG = { "color": LIGHT_GREY, - "x_min": -FRAME_X_RADIUS, - "x_max": FRAME_X_RADIUS, + "x_min": -config['frame_x_radius'], + "x_max": config['frame_x_radius'], "unit_size": 1, "include_ticks": True, "tick_size": 0.1, @@ -198,4 +199,4 @@ class UnitInterval(NumberLine): "decimal_number_config": { "num_decimal_places": 1, } - } \ No newline at end of file + } diff --git a/manim/mobject/svg/drawings.py b/manim/mobject/svg/drawings.py index a816399108..22e25e5004 100644 --- a/manim/mobject/svg/drawings.py +++ b/manim/mobject/svg/drawings.py @@ -4,6 +4,7 @@ from ...animation.animation import Animation from ...animation.rotation import Rotating from ...constants import * +from ...config import config from ...mobject.geometry import AnnularSector from ...mobject.geometry import Arc from ...mobject.geometry import Circle @@ -279,7 +280,7 @@ def __init__(self, **kwargs): class VideoIcon(SVGMobject): CONFIG = { "file_name": "video_icon", - "width": FRAME_WIDTH / 12., + "width": config['frame_width'] / 12., } def __init__(self, **kwargs): diff --git a/manim/mobject/svg/svg_mobject.py b/manim/mobject/svg/svg_mobject.py index 321b689b70..adc3e61953 100644 --- a/manim/mobject/svg/svg_mobject.py +++ b/manim/mobject/svg/svg_mobject.py @@ -1,5 +1,6 @@ import itertools as it import re +import os import string import warnings diff --git a/manim/mobject/svg/tex_mobject.py b/manim/mobject/svg/tex_mobject.py index f016cba804..242f7bd41e 100644 --- a/manim/mobject/svg/tex_mobject.py +++ b/manim/mobject/svg/tex_mobject.py @@ -2,6 +2,7 @@ import operator as op from ...constants import * +from ...config import config from ...mobject.geometry import Line from ...mobject.svg.svg_mobject import SVGMobject from ...mobject.svg.svg_mobject import VMobjectFromSVGPathstring @@ -15,11 +16,10 @@ class TexSymbol(VMobjectFromSVGPathstring): - """ - Purely a renaming of VMobjectFromSVGPathstring - """ + """Purely a renaming of VMobjectFromSVGPathstring.""" pass + class SingleStringTexMobject(SVGMobject): CONFIG = { "stroke_width": 0, @@ -304,7 +304,7 @@ class Title(TextMobject): CONFIG = { "scale_factor": 1, "include_underline": True, - "underline_width": FRAME_WIDTH - 2, + "underline_width": config['frame_width'] - 2, # This will override underline_width "match_underline_width_to_text": False, "underline_buff": MED_SMALL_BUFF, diff --git a/manim/mobject/vector_field.py b/manim/mobject/vector_field.py index cc77353702..0903e54c54 100644 --- a/manim/mobject/vector_field.py +++ b/manim/mobject/vector_field.py @@ -5,6 +5,7 @@ import random from ..constants import * +from ..config import config from ..logger import logger from ..animation.composition import AnimationGroup from ..animation.indication import ShowPassingFlash @@ -27,12 +28,12 @@ def get_colored_background_image(scalar_field_func, number_to_rgb_func, - pixel_height=PIXEL_HEIGHT, - pixel_width=PIXEL_WIDTH): - ph = pixel_height - pw = pixel_width - fw = FRAME_WIDTH - fh = FRAME_HEIGHT + pixel_height=config['pixel_height'], + pixel_width=config['pixel_width']): + ph = config['pixel_height'] + pw = config['pixel_width'] + fw = config['frame_width'] + fh = config['frame_height'] points_array = np.zeros((ph, pw, 3)) x_array = np.linspace(-fw / 2, fw / 2, pw) x_array = x_array.reshape((1, len(x_array))) @@ -109,7 +110,7 @@ def move_submobjects_along_vector_field(mobject, func): def apply_nudge(mob, dt): for submob in mob: x, y = submob.get_center()[:2] - if abs(x) < FRAME_WIDTH and abs(y) < FRAME_HEIGHT: + if abs(x) < config['frame_width'] and abs(y) < config['frame_height']: submob.shift(func(submob.get_center()) * dt) mobject.add_updater(apply_nudge) @@ -131,10 +132,10 @@ class VectorField(VGroup): CONFIG = { "delta_x": 0.5, "delta_y": 0.5, - "x_min": int(np.floor(-FRAME_WIDTH / 2)), - "x_max": int(np.ceil(FRAME_WIDTH / 2)), - "y_min": int(np.floor(-FRAME_HEIGHT / 2)), - "y_max": int(np.ceil(FRAME_HEIGHT / 2)), + "x_min": int(np.floor(-config['frame_width'] / 2)), + "x_max": int(np.ceil(config['frame_width'] / 2)), + "y_min": int(np.floor(-config['frame_height'] / 2)), + "y_max": int(np.ceil(config['frame_height'] / 2)), "min_magnitude": 0, "max_magnitude": 2, "colors": DEFAULT_SCALAR_FIELD_COLORS, diff --git a/manim/scene/scene.py b/manim/scene/scene.py index b69e2c3aa0..7b6bd25742 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -10,6 +10,7 @@ from ..animation.transform import MoveToTarget, ApplyMethod from ..camera.camera import Camera from ..constants import * +from ..config import camera_config, file_writer_config from ..container.container import Container from ..logger import logger from ..mobject.mobject import Mobject diff --git a/manim/scene/scene_file_writer.py b/manim/scene/scene_file_writer.py index 3444fa1626..1bfa7ed0ee 100644 --- a/manim/scene/scene_file_writer.py +++ b/manim/scene/scene_file_writer.py @@ -9,7 +9,7 @@ from PIL import Image from ..constants import FFMPEG_BIN -from ..config import config +from ..config import file_writer_config from ..logger import logger from ..utils.config_ops import digest_config from ..utils.file_ops import guarantee_existence @@ -75,10 +75,10 @@ def init_output_directories(self): add_extension_if_not_present(scene_name, ".png") ) - if config['WRITE_TO_MOVIE']: - if config['VIDEO_DIR']: + if file_writer_config['write_to_movie']: + if file_writer_config['video_dir']: movie_dir = guarantee_existence(os.path.join( - config['VIDEO_DIR'], + file_writer_config['video_dir'], module_directory, self.get_resolution_directory(), )) diff --git a/manim/scene/vector_space_scene.py b/manim/scene/vector_space_scene.py index 7839366bd2..ac860857b0 100644 --- a/manim/scene/vector_space_scene.py +++ b/manim/scene/vector_space_scene.py @@ -9,6 +9,7 @@ from ..animation.transform import ApplyPointwiseFunction from ..animation.transform import Transform from ..constants import * +from ..config import config from ..mobject.coordinate_systems import Axes from ..mobject.coordinate_systems import NumberPlane from ..mobject.geometry import Arrow @@ -488,8 +489,8 @@ def show_ghost_movement(self, vector): vector = vector.get_end() - vector.get_start() elif len(vector) == 2: vector = np.append(np.array(vector), 0.0) - x_max = int(FRAME_X_RADIUS + abs(vector[0])) - y_max = int(FRAME_Y_RADIUS + abs(vector[1])) + x_max = int(config['frame_x_radius'] + abs(vector[0])) + y_max = int(config['frame_y_radius'] + abs(vector[1])) dots = VMobject(*[ Dot(x * RIGHT + y * UP) for x in range(-x_max, x_max) @@ -517,10 +518,10 @@ class LinearTransformationScene(VectorScene): "include_background_plane": True, "include_foreground_plane": True, "foreground_plane_kwargs": { - "x_max": FRAME_WIDTH / 2, - "x_min": -FRAME_WIDTH / 2, - "y_max": FRAME_WIDTH / 2, - "y_min": -FRAME_WIDTH / 2, + "x_max": config['frame_width'] / 2, + "x_min": -config['frame_width'] / 2, + "y_max": config['frame_width'] / 2, + "y_min": -config['frame_width'] / 2, "faded_line_ratio": 0 }, "background_plane_kwargs": { @@ -1083,4 +1084,4 @@ def apply_function(self, function, added_anims=[], **kwargs): Animation(f_mob) for f_mob in self.foreground_mobjects ] + added_anims - self.play(*anims, **kwargs) \ No newline at end of file + self.play(*anims, **kwargs) diff --git a/manim/utils/tex_file_writing.py b/manim/utils/tex_file_writing.py index a51f4b9315..7b574f380f 100644 --- a/manim/utils/tex_file_writing.py +++ b/manim/utils/tex_file_writing.py @@ -1,12 +1,12 @@ import os import hashlib - from pathlib import Path -from ..config import config -from ..constants import TEX_TEXT_TO_REPLACE, TEX_USE_CTEX +from .. import constants +from ..config import file_writer_config, config from ..logger import logger + def tex_hash(expression): id_str = str(expression) hasher = hashlib.sha256() @@ -14,12 +14,14 @@ def tex_hash(expression): # Truncating at 16 bytes for cleanliness return hasher.hexdigest()[:16] + def tex_to_svg_file(expression, source_type): - tex_template = constants.TEX_TEMPLATE + tex_template = config['tex_template'] tex_file = generate_tex_file(expression, tex_template, source_type) dvi_file = tex_to_dvi(tex_file, tex_template.use_ctex) return dvi_to_svg(dvi_file, use_ctex=tex_template.use_ctex) + def generate_tex_file(expression, tex_template, source_type): if source_type == "text": output = tex_template.get_text_for_text_mode(expression) @@ -27,7 +29,7 @@ def generate_tex_file(expression, tex_template, source_type): output = tex_template.get_text_for_tex_mode(expression) result = os.path.join( - config['TEX_DIR'], + file_writer_config['tex_dir'], tex_hash(output) ) + ".tex" if not os.path.exists(result): @@ -39,11 +41,11 @@ def generate_tex_file(expression, tex_template, source_type): return result -def tex_to_dvi(tex_file, use_ctex = False): +def tex_to_dvi(tex_file, use_ctex=False): result = tex_file.replace(".tex", ".dvi" if not use_ctex else ".xdv") result = Path(result).as_posix() tex_file = Path(tex_file).as_posix() - tex_dir = Path(config['TEX_DIR']).as_posix() + tex_dir = Path(file_writer_config['tex_dir']).as_posix() if not os.path.exists(result): commands = [ "latex", @@ -67,9 +69,10 @@ def tex_to_dvi(tex_file, use_ctex = False): if exit_code != 0: log_file = tex_file.replace(".tex", ".log") raise Exception( - ("LaTeX error converting to dvi. " if not use_ctex - else "XeLaTeX error converting to xdv. ") + - "See log output above or the log file: %s" % log_file) + ("LaTeX error converting to dvi. " + if not use_ctex + else "XeLaTeX error converting to xdv. ") + + f"See log output above or the log file: {log_file}") return result From b1a7475b7b1922c3950db0411bef69d2d0898c83 Mon Sep 17 00:00:00 2001 From: leotrs Date: Thu, 4 Jun 2020 23:37:31 -0400 Subject: [PATCH 07/38] fix usage of CONFIG and file_writer_config --- manim/camera/camera.py | 12 +++--- manim/config.py | 2 +- manim/scene/scene.py | 39 +++++++++---------- manim/scene/scene_file_writer.py | 64 +++++++++++++------------------- 4 files changed, 50 insertions(+), 67 deletions(-) diff --git a/manim/camera/camera.py b/manim/camera/camera.py index 0216b5c71a..bb0145bca1 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -42,19 +42,19 @@ class Camera(object): """ CONFIG = { "background_image": None, - "pixel_height": PIXEL_HEIGHT, - "pixel_width": PIXEL_WIDTH, - "frame_rate": FRAME_RATE, + "pixel_height": config['pixel_height'], + "pixel_width": config['pixel_width'], + "frame_rate": config['frame_rate'], # Note: frame height and width will be resized to match # the pixel aspect ratio - "frame_height": FRAME_HEIGHT, - "frame_width": FRAME_WIDTH, + "frame_height": config['frame_height'], + "frame_width": config['frame_width'], "frame_center": ORIGIN, "background_color": BLACK, "background_opacity": 1, # Points in vectorized mobjects with norm greater # than this value will be rescaled. - "max_allowable_norm": FRAME_WIDTH, + "max_allowable_norm": config['frame_width'], "image_mode": "RGBA", "n_channels": 4, "pixel_array_dtype": 'uint8', diff --git a/manim/config.py b/manim/config.py index 9221314691..ef8c007822 100644 --- a/manim/config.py +++ b/manim/config.py @@ -16,7 +16,7 @@ from .logger import logger from . import constants -__all__ = ["config", "register_tex_template", "initialize_tex"] +__all__ = ["file_writer_config", "config", "camera_config"] def _parse_config(config_parser, args): diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 7b6bd25742..766a4e13d6 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -45,21 +45,16 @@ def construct(self): """ CONFIG = { "camera_class": Camera, - "CAMERA_CONFIG": {}, - "FILE_WRITER_CONFIG": {}, - "SKIP_ANIMATIONS": False, + "skip_animations": False, "always_update_mobjects": False, "random_seed": 0, - "FROM_ANIMATION_NUMBER": None, - "UPTO_ANIMATION_NUMBER": None, - "LEAVE_PROGRESS_BARS": False, } def __init__(self, **kwargs): Container.__init__(self, **kwargs) - self.camera = self.camera_class(**self.CAMERA_CONFIG) + self.camera = self.camera_class(**camera_config) self.file_writer = SceneFileWriter( - self, **self.FILE_WRITER_CONFIG, + self, **file_writer_config, ) self.mobjects = [] @@ -67,7 +62,7 @@ def __init__(self, **kwargs): self.foreground_mobjects = [] self.num_plays = 0 self.time = 0 - self.original_skipping_status = self.SKIP_ANIMATIONS + self.original_skipping_status = self.skip_animations if self.random_seed is not None: random.seed(self.random_seed) np.random.seed(self.random_seed) @@ -239,7 +234,7 @@ def update_frame( #TODO Description in Docstring **kwargs """ - if self.SKIP_ANIMATIONS and not ignore_skipping: + if self.skip_animations and not ignore_skipping: return if mobjects is None: mobjects = list_update( @@ -680,14 +675,14 @@ def get_time_progression(self, run_time, n_iterations=None, override_skip_animat ProgressDisplay The CommandLine Progress Bar. """ - if self.SKIP_ANIMATIONS and not override_skip_animations: + if self.skip_animations and not override_skip_animations: times = [run_time] else: step = 1 / self.camera.frame_rate times = np.arange(0, run_time, step) time_progression = ProgressDisplay( times, total=n_iterations, - leave=self.LEAVE_PROGRESS_BARS, + leave=file_writer_config['leave_progress_bars'], ascii=False if platform.system() != 'Windows' else True ) return time_progression @@ -825,12 +820,12 @@ def update_skipping_status(self): raises an EndSceneEarlyException if they don't correspond. """ - if self.FROM_ANIMATION_NUMBER: - if self.num_plays == self.FROM_ANIMATION_NUMBER: - self.SKIP_ANIMATIONS = False - if self.UPTO_ANIMATION_NUMBER: - if self.num_plays >= self.UPTO_ANIMATION_NUMBER: - self.SKIP_ANIMATIONS = True + if file_writer_config['from_animation_number']: + if self.num_plays == file_writer_config['from_animation_number']: + self.skip_animations = False + if file_writer_config['upto_animation_number']: + if self.num_plays >= file_writer_config['upto_animation_number']: + self.skip_animations = True raise EndSceneEarlyException() def handle_play_like_call(func): @@ -855,7 +850,7 @@ def handle_play_like_call(func): """ def wrapper(self, *args, **kwargs): self.update_skipping_status() - allow_write = not self.SKIP_ANIMATIONS + allow_write = not self.skip_animations self.file_writer.begin_animation(allow_write) func(self, *args, **kwargs) self.file_writer.end_animation(allow_write) @@ -928,7 +923,7 @@ def finish_animations(self, animations): self.mobjects_from_last_animation = [ anim.mobject for anim in animations ] - if self.SKIP_ANIMATIONS: + if self.skip_animations: # TODO, run this call in for each animation? self.update_mobjects(self.get_run_time(animations)) else: @@ -1071,7 +1066,7 @@ def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): if stop_condition is not None and stop_condition(): time_progression.close() break - elif self.SKIP_ANIMATIONS: + elif self.skip_animations: # Do nothing return self else: @@ -1140,7 +1135,7 @@ def add_frames(self, *frames): """ dt = 1 / self.camera.frame_rate self.increment_time(len(frames) * dt) - if self.SKIP_ANIMATIONS: + if self.skip_animations: return for frame in frames: self.file_writer.write_frame(frame) diff --git a/manim/scene/scene_file_writer.py b/manim/scene/scene_file_writer.py index 1bfa7ed0ee..064e21dfd2 100644 --- a/manim/scene/scene_file_writer.py +++ b/manim/scene/scene_file_writer.py @@ -34,17 +34,6 @@ class SceneFileWriter(object): "movie_file_extension" (str=".mp4") The file-type extension of the outputted video. """ - CONFIG = { - "WRITE_TO_MOVIE": False, - "SAVE_PNGS": False, - "PNG_MODE": "RGBA", - "SAVE_LAST_FRAME": False, - "MOVIE_FILE_EXTENSION": ".mp4", - "GIF_FILE_EXTENSION": ".gif", - "OUTPUT_FILE": None, - "INPUT_FILE": "", - "OUTPUT_DIRECTORY": None, - } def __init__(self, scene, **kwargs): digest_config(self, kwargs) @@ -61,12 +50,12 @@ def init_output_directories(self): files will be written to and read from (within MEDIA_DIR). If they don't already exist, they will be created. """ - module_directory = self.OUTPUT_DIRECTORY or self.get_default_module_directory() - scene_name = self.OUTPUT_FILE or self.get_default_scene_name() - if self.SAVE_LAST_FRAME or self.SAVE_PNGS: - if config['MEDIA_DIR'] != "": + module_directory = self.get_default_module_directory() + scene_name = self.get_default_scene_name() + if file_writer_config['save_last_frame'] or file_writer_config['save_pngs']: + if file_writer_config['media_dir'] != "": image_dir = guarantee_existence(os.path.join( - config['MEDIA_DIR'], + file_writer_config['media_dir'], "images", module_directory, )) @@ -85,13 +74,13 @@ def init_output_directories(self): self.movie_file_path = os.path.join( movie_dir, add_extension_if_not_present( - scene_name, self.MOVIE_FILE_EXTENSION + scene_name, file_writer_config['movie_file_extension'] ) ) self.gif_file_path = os.path.join( movie_dir, add_extension_if_not_present( - scene_name, self.GIF_FILE_EXTENSION + scene_name, file_writer_config['gif_file_extension'] ) ) self.partial_movie_directory = guarantee_existence(os.path.join( @@ -110,7 +99,7 @@ def get_default_module_directory(self): str The name of the directory. """ - filename = os.path.basename(self.INPUT_FILE) + filename = os.path.basename(file_writer_config['input_file']) root, _ = os.path.splitext(filename) return root @@ -126,10 +115,7 @@ def get_default_scene_name(self): str The default scene name. """ - if self.OUTPUT_FILE: - return self.OUTPUT_FILE - else: - return self.scene.__class__.__name__ + return self.scene.__class__.__name__ def get_resolution_directory(self): """ @@ -191,7 +177,7 @@ def get_next_partial_movie_path(self): self.partial_movie_directory, "{:05}{}".format( self.scene.num_plays, - self.MOVIE_FILE_EXTENSION, + file_writer_config['movie_file_extension'], ) ) return result @@ -299,7 +285,7 @@ def begin_animation(self, allow_write=False): allow_write : bool, optional Whether or not to write to a video file. """ - if self.WRITE_TO_MOVIE and allow_write: + if file_writer_config['write_to_movie'] and allow_write: self.open_movie_pipe() def end_animation(self, allow_write=False): @@ -312,7 +298,7 @@ def end_animation(self, allow_write=False): allow_write : bool, optional Whether or not to write to a video file. """ - if self.WRITE_TO_MOVIE and allow_write: + if file_writer_config['write_to_movie'] and allow_write: self.close_movie_pipe() def write_frame(self, frame): @@ -325,9 +311,9 @@ def write_frame(self, frame): frame : np.array Pixel array of the frame. """ - if self.WRITE_TO_MOVIE: + if file_writer_config['write_to_movie']: self.writing_process.stdin.write(frame.tostring()) - if self.SAVE_PNGS: + if file_writer_config['save_pngs']: path, extension = os.path.splitext(self.image_file_path) Image.fromarray(frame).save(f'{path}{self.frame_count}{extension}') self.frame_count += 1 @@ -370,11 +356,11 @@ def finish(self): If save_last_frame is True, saves the last frame in the default image directory. """ - if self.WRITE_TO_MOVIE: + if file_writer_config['write_to_movie']: if hasattr(self, "writing_process"): self.writing_process.terminate() self.combine_movie_files() - if self.SAVE_LAST_FRAME: + if file_writer_config['save_last_frame']: self.scene.update_frame(ignore_skipping=True) self.save_final_image(self.scene.get_image()) @@ -385,7 +371,9 @@ def open_movie_pipe(self): buffer. """ file_path = self.get_next_partial_movie_path() - temp_file_path = os.path.splitext(file_path)[0] + '_temp' + self.MOVIE_FILE_EXTENSION + temp_file_path = (os.path.splitext(file_path)[0] + + '_temp' + + file_writer_config['movie_file_extension']) self.partial_movie_file_path = file_path self.temp_partial_movie_file_path = temp_file_path @@ -406,7 +394,7 @@ def open_movie_pipe(self): ] # TODO, the test for a transparent background should not be based on # the file extension. - if self.MOVIE_FILE_EXTENSION == ".mov": + if file_writer_config['movie_file_extension'] == ".mov": # This is if the background of the exported # video should be transparent. command += [ @@ -448,12 +436,12 @@ def combine_movie_files(self): # single piece. kwargs = { "remove_non_integer_files": True, - "extension": self.MOVIE_FILE_EXTENSION, + "extension": file_writer_config['movie_file_extension'], } - if self.scene.FROM_ANIMATION_NUMBER is not None: - kwargs["min_index"] = self.scene.FROM_ANIMATION_NUMBER - if self.scene.UPTO_ANIMATION_NUMBER is not None: - kwargs["max_index"] = self.scene.UPTO_ANIMATION_NUMBER + if file_writer_config['from_animation_number'] is not None: + kwargs["min_index"] = file_writer_config['from_animation_number'] + if file_writer_config['upto_animation_number'] is not None: + kwargs["max_index"] = file_writer_config['upto_animation_number'] else: kwargs["remove_indices_greater_than"] = self.scene.num_plays - 1 partial_movie_files = get_sorted_integer_files( @@ -495,7 +483,7 @@ def combine_movie_files(self): if self.includes_sound: sound_file_path = movie_file_path.replace( - self.MOVIE_FILE_EXTENSION, ".wav" + file_writer_config['movie_file_extension'], ".wav" ) # Makes sure sound file length will match video file self.add_audio_segment(AudioSegment.silent(0)) From 985e5f88931fba0bb0d01a361e35f7caa179f870 Mon Sep 17 00:00:00 2001 From: leotrs Date: Fri, 5 Jun 2020 07:50:38 -0400 Subject: [PATCH 08/38] fix: data_files seems to be deprecated; add the config files to package_data instead --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index b33b9bb092..af84f44ad7 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ description="Animation engine for explanatory math videos", license="MIT", packages=find_namespace_packages(), - package_data={ "manim": ["*.tex"] }, + package_data={ "manim": ["*.tex", "*.cfg"] }, entry_points={ "console_scripts": [ "manim=manim.__main__:main", @@ -26,5 +26,4 @@ "pyreadline; sys_platform == 'win32'", "rich", ], - data_files=[('manim/', ['manim/default.cfg'])], ) From b00a51fa1218e39e6c325310bbc8b4e88c227ce5 Mon Sep 17 00:00:00 2001 From: leotrs Date: Fri, 5 Jun 2020 07:59:39 -0400 Subject: [PATCH 09/38] clarify comment --- manim/config.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/manim/config.py b/manim/config.py index ef8c007822..fd0cd8c07b 100644 --- a/manim/config.py +++ b/manim/config.py @@ -190,12 +190,11 @@ def _parse_cli(arg_list, input=True): ) # Note the following use (action='store_const', const=True), - # instead of using the built-in (action='store_true'). The reason - # is that these two are not equivalent. The latter is equivalent - # to (action='store_const', const=True, default=False), while the - # former sets no default value. We do not want to set the default - # here, but in the manim.cfg file. Therefore we use the latter, - # (action='store_const', const=True). + # instead of using the built-in (action='store_true'). The latter + # is equivalent to (action='store_const', const=True, + # default=False), while the former sets no default value. Since + # we do not want to set the default here, but in the manim.cfg + # file, we use the latter. parser.add_argument( "-p", "--preview", action="store_const", From fae692702a8618e10f19291a5acdadafd16fd403 Mon Sep 17 00:00:00 2001 From: leotrs Date: Sat, 6 Jun 2020 11:53:15 -0400 Subject: [PATCH 10/38] install ffmpeg on linux --- .travis.yml | 9 ++++++--- .travis/osx.sh | 3 ++- .travis/travis-requirements.txt | 4 +--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 19b2fa691c..c228f4dabf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ jobs: - pip3 install -r ./.travis/travis-requirements.txt - pip3 install pycairo pytest - pip3 install . + - sudo apt install ffmpeg script: - pytest @@ -20,6 +21,7 @@ jobs: - pip3 install -r ./.travis/travis-requirements.txt - pip3 install pycairo pytest - pip3 install . + - sudo apt install ffmpeg script: - pytest @@ -31,6 +33,7 @@ jobs: - pip3 install -r ./.travis/travis-requirements.txt - pip3 install pycairo pytest - pip3 install . + - sudo apt install ffmpeg script: - pytest @@ -87,7 +90,7 @@ jobs: - python ./.travis/cairo-windows.py - cmd.exe //c "RefreshEnv.cmd" - python -m pip install --user . - script: + script: - python -m pytest - os: windows @@ -104,7 +107,7 @@ jobs: - python ./.travis/cairo-windows.py - cmd.exe //c "RefreshEnv.cmd" - python -m pip install --user . - script: + script: - python -m pytest - os: windows @@ -121,7 +124,7 @@ jobs: - python ./.travis/cairo-windows.py - cmd.exe //c "RefreshEnv.cmd" - python -m pip install --user . - script: + script: - python -m pytest before_install: diff --git a/.travis/osx.sh b/.travis/osx.sh index 1d3ec02b0f..01fbfe3482 100644 --- a/.travis/osx.sh +++ b/.travis/osx.sh @@ -3,10 +3,11 @@ brew update brew install openssl readline brew outdated pyenv || brew upgrade pyenv +brew install ffmpeg brew install pyenv-virtualenv pyenv install $PYVER export PYENV_VERSION=$PYVER export PATH="/Users/travis/.pyenv/shims:${PATH}" pyenv virtualenv venv source ~/.pyenv/versions/venv/bin/activate -python --version \ No newline at end of file +python --version diff --git a/.travis/travis-requirements.txt b/.travis/travis-requirements.txt index e51d737699..d373283faf 100644 --- a/.travis/travis-requirements.txt +++ b/.travis/travis-requirements.txt @@ -5,10 +5,8 @@ Pillow progressbar scipy tqdm -# opencv-python -# pycairo pydub pygments pyreadline; sys_platform == 'win32' rich -pytest \ No newline at end of file +pytest From dfccf0ddffb6f8040b37f6de7e44aa2a401da420 Mon Sep 17 00:00:00 2001 From: leotrs Date: Sat, 6 Jun 2020 11:58:36 -0400 Subject: [PATCH 11/38] travis setup --- .travis.yml | 4 +--- .travis/linux.sh | 5 +++++ 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 .travis/linux.sh diff --git a/.travis.yml b/.travis.yml index c228f4dabf..2d795d3ab5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,6 @@ jobs: - pip3 install -r ./.travis/travis-requirements.txt - pip3 install pycairo pytest - pip3 install . - - sudo apt install ffmpeg script: - pytest @@ -21,7 +20,6 @@ jobs: - pip3 install -r ./.travis/travis-requirements.txt - pip3 install pycairo pytest - pip3 install . - - sudo apt install ffmpeg script: - pytest @@ -33,7 +31,6 @@ jobs: - pip3 install -r ./.travis/travis-requirements.txt - pip3 install pycairo pytest - pip3 install . - - sudo apt install ffmpeg script: - pytest @@ -129,6 +126,7 @@ jobs: before_install: - if [ "$TRAVIS_OS_NAME" == "osx" ]; then chmod +x ./.travis/osx.sh; sh ./.travis/osx.sh; fi + - if [ "$TRAVIS_OS_NAME" == "linux" ]; then chmod +x ./.travis/linux.sh; sh ./.travis/linux.sh; fi branches: only: diff --git a/.travis/linux.sh b/.travis/linux.sh new file mode 100644 index 0000000000..573502ce71 --- /dev/null +++ b/.travis/linux.sh @@ -0,0 +1,5 @@ +##### THIS IS FOR TRAVIS BUILDS, DO NOT RUN THIS ON YOUR COMPUTER! ##### + +sudo apt update +sudo apt upgrade +sudo apt install ffmpeg From e6691f25cb3c558dbf97b96673800406f7628470 Mon Sep 17 00:00:00 2001 From: leotrs Date: Sat, 6 Jun 2020 12:13:43 -0400 Subject: [PATCH 12/38] fix dumb --- manim/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manim/__main__.py b/manim/__main__.py index 59d1529d39..9a5e7862f4 100644 --- a/manim/__main__.py +++ b/manim/__main__.py @@ -163,7 +163,7 @@ def main(): for SceneClass in scene_classes_to_render: try: # By invoking, this renders the full scene - scene = SceneClass(**{}) + scene = SceneClass() open_file_if_needed(scene.file_writer) if file_writer_config["sound"]: play_finish_sound() From 392e26ad7f631dd96446ddc2d4bc1f65cc349186 Mon Sep 17 00:00:00 2001 From: leotrs Date: Sat, 6 Jun 2020 12:36:01 -0400 Subject: [PATCH 13/38] fix arg parsing by not passing the first argv to the config parser --- manim/config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/manim/config.py b/manim/config.py index fd0cd8c07b..3647014747 100644 --- a/manim/config.py +++ b/manim/config.py @@ -182,11 +182,13 @@ def _parse_cli(arg_list, input=True): "scene_names", nargs="*", help="Name of the Scene class you want to see", + default=[''], ) parser.add_argument( "-o", "--output_file", help="Specify the name of the output file, if" "it should be different from the scene class name", + default='', ) # Note the following use (action='store_const', const=True), @@ -404,7 +406,7 @@ def _from_command_line(): ] if _from_command_line(): - args = _parse_cli(sys.argv) + args = _parse_cli(sys.argv[1:]) if args.config_file is not None: config_files.append(args.config_file) From 07c6c2956550f889c63c1b79ac680ea745928742 Mon Sep 17 00:00:00 2001 From: leotrs Date: Sat, 6 Jun 2020 18:18:02 -0400 Subject: [PATCH 14/38] now try without upgrading the whole system --- .travis/linux.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis/linux.sh b/.travis/linux.sh index 573502ce71..f428cc1829 100644 --- a/.travis/linux.sh +++ b/.travis/linux.sh @@ -1,5 +1,4 @@ ##### THIS IS FOR TRAVIS BUILDS, DO NOT RUN THIS ON YOUR COMPUTER! ##### sudo apt update -sudo apt upgrade sudo apt install ffmpeg From 27c12ba7530b5a884575095ed03b9dfe3dc1683b Mon Sep 17 00:00:00 2001 From: leotrs Date: Sat, 6 Jun 2020 19:50:12 -0400 Subject: [PATCH 15/38] fix: now --dry_run correctly overrides -w, -a, -s, -g, and -ig --- manim/config.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/manim/config.py b/manim/config.py index 3647014747..b1fc11d6ae 100644 --- a/manim/config.py +++ b/manim/config.py @@ -114,13 +114,13 @@ def _parse_file_writer_config(config_parser, args): # arguments. Note ConfigParser options are all strings and each # needs to be converted to the appropriate type. Thus, we do this # in batches, depending on their type: booleans and strings - for boolean_opt in ['preview', 'show_file_in_finder', 'quiet', - 'sound', 'leave_progress_bars']: + for boolean_opt in ['preview', 'show_file_in_finder', 'quiet', 'sound', + 'leave_progress_bars', 'write_to_movie', 'save_last_frame', + 'save_pngs', 'save_as_gif', 'write_all']: config[boolean_opt] = (default.getboolean(boolean_opt) if getattr(args, boolean_opt) is None else getattr(args, boolean_opt)) - for str_opt in ['media_dir', 'video_dir', 'tex_dir', - 'text_dir']: + for str_opt in ['media_dir', 'video_dir', 'tex_dir', 'text_dir']: config[str_opt] = (default[str_opt] if getattr(args, str_opt) is None else getattr(args, str_opt)) @@ -128,8 +128,7 @@ def _parse_file_writer_config(config_parser, args): # Handle the -t (--transparent) flag. This flag determines which # section to use from the .cfg file. section = config_parser['transparent'] if args.transparent else default - for opt in ['png_mode', 'movie_file_extension', - 'background_opacity']: + for opt in ['png_mode', 'movie_file_extension', 'background_opacity']: config[opt] = section[opt] # Handle the -n flag. Read first from the cfg and then override with CLI. @@ -149,9 +148,11 @@ def _parse_file_writer_config(config_parser, args): # Handle the --dry_run flag. This flag determines which section # to use from the .cfg file. All options involved are boolean. - section = config_parser['dry_run'] if args.dry_run else default - for opt in ['write_to_movie', 'write_all', 'save_last_frame', 'save_pngs', 'save_as_gif']: - config[opt] = section.getboolean(opt) + # Note this overrides the flags -w, -s, -a, -g, and -i. + if args.dry_run: + for opt in ['write_to_movie', 'save_last_frame', 'save_pngs', + 'save_as_gif', 'write_all']: + config[opt] = config_parser['dry_run'].getboolean(opt) # Read in the streaming section -- all values are strings config['streaming'] = {opt: config_parser['streaming'][opt] From dd3b780061376328ca8c98fc7bd98b573f711014 Mon Sep 17 00:00:00 2001 From: leotrs Date: Sat, 6 Jun 2020 19:56:41 -0400 Subject: [PATCH 16/38] fix: now support -o flag --- manim/config.py | 1 + manim/scene/scene_file_writer.py | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/manim/config.py b/manim/config.py index b1fc11d6ae..d05c27f244 100644 --- a/manim/config.py +++ b/manim/config.py @@ -105,6 +105,7 @@ def _parse_file_writer_config(config_parser, args): config['input_file'] = args.file config['scene_names'] = (args.scene_names if args.scene_names is not None else []) + config['output_file'] = args.output_file # Read some options that cannot be overriden by CLI arguments for opt in ['gif_file_extension']: diff --git a/manim/scene/scene_file_writer.py b/manim/scene/scene_file_writer.py index 064e21dfd2..1d67995d35 100644 --- a/manim/scene/scene_file_writer.py +++ b/manim/scene/scene_file_writer.py @@ -106,16 +106,18 @@ def get_default_module_directory(self): def get_default_scene_name(self): """ This method returns the default scene name - which is the value of "file_name", if it exists and + which is the value of "output_file", if it exists and the actual name of the class that inherited from - Scene in your animation script, if "file_name" is None. + Scene in your animation script, if "output_file" is None. Returns ------- str The default scene name. """ - return self.scene.__class__.__name__ + return (file_writer_config['output_file'] + if file_writer_config['output_file'] is not None + else self.scene.__class__.__name__) def get_resolution_directory(self): """ From d5bf02c24809ee605a9191903c5543df971c55a1 Mon Sep 17 00:00:00 2001 From: leotrs Date: Sat, 6 Jun 2020 20:01:08 -0400 Subject: [PATCH 17/38] fix: move config.file_writer_config["gif_file_extension"] to constants.GIF_FILE_EXTENSION --- manim/config.py | 4 ---- manim/constants.py | 3 +++ manim/default.cfg | 3 --- manim/scene/scene_file_writer.py | 4 ++-- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/manim/config.py b/manim/config.py index d05c27f244..7e27534f33 100644 --- a/manim/config.py +++ b/manim/config.py @@ -107,10 +107,6 @@ def _parse_file_writer_config(config_parser, args): if args.scene_names is not None else []) config['output_file'] = args.output_file - # Read some options that cannot be overriden by CLI arguments - for opt in ['gif_file_extension']: - config[opt] = default[opt] - # Handle all options that are directly overridden by CLI # arguments. Note ConfigParser options are all strings and each # needs to be converted to the appropriate type. Thus, we do this diff --git a/manim/constants.py b/manim/constants.py index 45fe51bfed..e8acf30e61 100644 --- a/manim/constants.py +++ b/manim/constants.py @@ -79,6 +79,9 @@ class MyText(Text): # ffmpeg stuff FFMPEG_BIN = "ffmpeg" +# gif stuff +GIF_FILE_EXTENSION = '.gif' + # Colors COLOR_MAP = { "DARK_BLUE": "#236B8E", diff --git a/manim/default.cfg b/manim/default.cfg index 68996ad611..c369910878 100644 --- a/manim/default.cfg +++ b/manim/default.cfg @@ -75,9 +75,6 @@ tex_dir = %(MEDIA_DIR)s/Tex # --text_dir text_dir = %(MEDIA_DIR)s/texts -# This value cannot be changed through CLI -gif_file_extension = .gif - # If the -t (--transparent) flag is used, these will be replaced with the # values specified in the [TRANSPARENT] section later in this file. png_mode = RGB diff --git a/manim/scene/scene_file_writer.py b/manim/scene/scene_file_writer.py index 1d67995d35..61737e09da 100644 --- a/manim/scene/scene_file_writer.py +++ b/manim/scene/scene_file_writer.py @@ -8,7 +8,7 @@ import datetime from PIL import Image -from ..constants import FFMPEG_BIN +from ..constants import FFMPEG_BIN, GIF_FILE_EXTENSION from ..config import file_writer_config from ..logger import logger from ..utils.config_ops import digest_config @@ -80,7 +80,7 @@ def init_output_directories(self): self.gif_file_path = os.path.join( movie_dir, add_extension_if_not_present( - scene_name, file_writer_config['gif_file_extension'] + scene_name, GIF_FILE_EXTENSION ) ) self.partial_movie_directory = guarantee_existence(os.path.join( From a6c31650aa8b35c602bde5ad72ad136dc9769006 Mon Sep 17 00:00:00 2001 From: leotrs Date: Sun, 7 Jun 2020 12:10:17 -0400 Subject: [PATCH 18/38] do not prompt the user --- .travis/linux.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis/linux.sh b/.travis/linux.sh index f428cc1829..6852173fb4 100644 --- a/.travis/linux.sh +++ b/.travis/linux.sh @@ -1,4 +1,4 @@ ##### THIS IS FOR TRAVIS BUILDS, DO NOT RUN THIS ON YOUR COMPUTER! ##### sudo apt update -sudo apt install ffmpeg +sudo apt install -y ffmpeg From aeb20ccd532b86d1a06d9758b83a3d32c3c99e56 Mon Sep 17 00:00:00 2001 From: leotrs Date: Sun, 7 Jun 2020 12:13:13 -0400 Subject: [PATCH 19/38] update comment --- manim/config.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/manim/config.py b/manim/config.py index 7e27534f33..b7dcfcf90a 100644 --- a/manim/config.py +++ b/manim/config.py @@ -189,12 +189,11 @@ def _parse_cli(arg_list, input=True): default='', ) - # Note the following use (action='store_const', const=True), - # instead of using the built-in (action='store_true'). The latter - # is equivalent to (action='store_const', const=True, - # default=False), while the former sets no default value. Since - # we do not want to set the default here, but in the manim.cfg - # file, we use the latter. + # The following use (action='store_const', const=True) instead of + # the built-in (action='store_true'). This is because the latter + # will default to False if not specified, while the former sets no + # default value. Since we want to set the default value in + # manim.cfg rather than here, we use the former. parser.add_argument( "-p", "--preview", action="store_const", From 5c2e085d495091518026c75f24f7124a8cc0331b Mon Sep 17 00:00:00 2001 From: leotrs Date: Sun, 7 Jun 2020 15:52:47 -0400 Subject: [PATCH 20/38] change loglevel to debug in order to see what is going on in Windows travis builds --- manim/scene/scene_file_writer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/manim/scene/scene_file_writer.py b/manim/scene/scene_file_writer.py index 61737e09da..b9ea913b74 100644 --- a/manim/scene/scene_file_writer.py +++ b/manim/scene/scene_file_writer.py @@ -392,7 +392,7 @@ def open_movie_pipe(self): '-r', str(fps), # frames per second '-i', '-', # The imput comes from a pipe '-an', # Tells FFMPEG not to expect any audio - '-loglevel', 'error', + '-loglevel', 'debug', ] # TODO, the test for a transparent background should not be based on # the file extension. @@ -473,7 +473,7 @@ def combine_movie_files(self): '-f', 'concat', '-safe', '0', '-i', file_list, - '-loglevel', 'error', + '-loglevel', 'debug', '-c', 'copy', movie_file_path ] @@ -495,7 +495,7 @@ def combine_movie_files(self): ) temp_file_path = movie_file_path.replace(".", "_temp.") commands = [ - "ffmpeg", + FFMPEG_BIN, "-i", movie_file_path, "-i", sound_file_path, '-y', # overwrite output file if it exists @@ -506,7 +506,7 @@ def combine_movie_files(self): "-map", "0:v:0", # select audio stream from second file "-map", "1:a:0", - '-loglevel', 'error', + '-loglevel', 'debug', # "-shortest", temp_file_path, ] From 834063a1fc7eee5d6aba8ff95bf2d668824dc206 Mon Sep 17 00:00:00 2001 From: Leo Torres Date: Wed, 10 Jun 2020 09:46:28 -0400 Subject: [PATCH 21/38] Prettier if statement Co-authored-by: Pg Biel <9021226+PgBiel@users.noreply.github.com> --- manim/__main__.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/manim/__main__.py b/manim/__main__.py index 9a5e7862f4..ac4443ed89 100644 --- a/manim/__main__.py +++ b/manim/__main__.py @@ -61,13 +61,12 @@ def open_file_if_needed(file_writer): def is_child_scene(obj, module): - if not inspect.isclass(obj): - return False - if not issubclass(obj, Scene): - return False - if obj == Scene: - return False - if not obj.__module__.startswith(module.__name__): + if ( + not inspect.isclass(obj) + or not issubclass(obj, Scene) + or obj == Scene + or not obj.__module__.startswith(module.__name__) + ): return False return True From 754e50b199ac46133ccb12015274aac72edd2a20 Mon Sep 17 00:00:00 2001 From: Leo Torres Date: Wed, 10 Jun 2020 09:47:30 -0400 Subject: [PATCH 22/38] More idiomatic for loop Co-authored-by: Pg Biel <9021226+PgBiel@users.noreply.github.com> --- manim/__main__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manim/__main__.py b/manim/__main__.py index ac4443ed89..6a44359ab5 100644 --- a/manim/__main__.py +++ b/manim/__main__.py @@ -73,7 +73,8 @@ def is_child_scene(obj, module): def prompt_user_for_choice(scene_classes): num_to_class = {} - for count, scene_class in zip(it.count(1), scene_classes): + for count, scene_class in enumerate(scene_classes): + count += 1 # start with 1 instead of 0 name = scene_class.__name__ print("%d: %s" % (count, name)) num_to_class[count] = scene_class From 7b6b3708bfaf413ebcbe4977ef2e7903e17a70e7 Mon Sep 17 00:00:00 2001 From: Leo Torres Date: Wed, 10 Jun 2020 09:49:44 -0400 Subject: [PATCH 23/38] Readability Co-authored-by: Pg Biel <9021226+PgBiel@users.noreply.github.com> --- manim/__main__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/manim/__main__.py b/manim/__main__.py index 6a44359ab5..3be58a2b50 100644 --- a/manim/__main__.py +++ b/manim/__main__.py @@ -159,19 +159,19 @@ def main(): module = get_module(file_writer_config["input_file"]) all_scene_classes = get_scene_classes_from_module(module) scene_classes_to_render = get_scenes_to_render(all_scene_classes) - + sound_on = file_writer_config["sound"] for SceneClass in scene_classes_to_render: try: # By invoking, this renders the full scene scene = SceneClass() open_file_if_needed(scene.file_writer) - if file_writer_config["sound"]: + if sound_on: play_finish_sound() except Exception: print("\n\n") traceback.print_exc() print("\n\n") - if file_writer_config["sound"]: + if sound_on: play_error_sound() From 7ad6aa9d57eb2ab84248542b87ee63ee3db4c4c5 Mon Sep 17 00:00:00 2001 From: leotrs Date: Wed, 10 Jun 2020 09:51:21 -0400 Subject: [PATCH 24/38] fix typo --- manim/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manim/config.py b/manim/config.py index b7dcfcf90a..12f0ff4240 100644 --- a/manim/config.py +++ b/manim/config.py @@ -72,7 +72,7 @@ def _parse_config(config_parser, args): config['left_side'] = config['frame_x_radius'] * constants.LEFT config['right_side'] = config['frame_x_radius'] * constants.RIGHT - # Hangle the --tex_template flag. Note we accept None if the flag is absent + # Handle the --tex_template flag. Note we accept None if the flag is absent filename = (os.path.expanduser(args.tex_template) if args.tex_template is not None else None) From 1fe45058e5c5a1e01e9fa05b9c7e29efcd8c6ad6 Mon Sep 17 00:00:00 2001 From: leotrs Date: Wed, 10 Jun 2020 10:06:53 -0400 Subject: [PATCH 25/38] reverse debugging changes --- manim/scene/scene_file_writer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/manim/scene/scene_file_writer.py b/manim/scene/scene_file_writer.py index b9ea913b74..53a3a9dcdb 100644 --- a/manim/scene/scene_file_writer.py +++ b/manim/scene/scene_file_writer.py @@ -392,7 +392,7 @@ def open_movie_pipe(self): '-r', str(fps), # frames per second '-i', '-', # The imput comes from a pipe '-an', # Tells FFMPEG not to expect any audio - '-loglevel', 'debug', + '-loglevel', 'error', ] # TODO, the test for a transparent background should not be based on # the file extension. @@ -473,7 +473,7 @@ def combine_movie_files(self): '-f', 'concat', '-safe', '0', '-i', file_list, - '-loglevel', 'debug', + '-loglevel', 'error', '-c', 'copy', movie_file_path ] @@ -506,7 +506,7 @@ def combine_movie_files(self): "-map", "0:v:0", # select audio stream from second file "-map", "1:a:0", - '-loglevel', 'debug', + '-loglevel', 'error', # "-shortest", temp_file_path, ] From 85e6b7987674cbb1732d4470413a227ffbf51b82 Mon Sep 17 00:00:00 2001 From: leotrs Date: Thu, 11 Jun 2020 09:46:18 -0400 Subject: [PATCH 26/38] some fixes by PG --- manim/__main__.py | 24 +++++++----------------- manim/config.py | 12 ++++-------- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/manim/__main__.py b/manim/__main__.py index 3be58a2b50..1fba10fa9a 100644 --- a/manim/__main__.py +++ b/manim/__main__.py @@ -61,14 +61,10 @@ def open_file_if_needed(file_writer): def is_child_scene(obj, module): - if ( - not inspect.isclass(obj) - or not issubclass(obj, Scene) - or obj == Scene - or not obj.__module__.startswith(module.__name__) - ): - return False - return True + return (inspect.isclass(obj) + and issubclass(obj, Scene) + and obj != Scene + and obj.__module__.startswith(module.__name__)) def prompt_user_for_choice(scene_classes): @@ -79,19 +75,13 @@ def prompt_user_for_choice(scene_classes): print("%d: %s" % (count, name)) num_to_class[count] = scene_class try: + import re user_input = input(constants.CHOOSE_NUMBER_MESSAGE) - return [ - num_to_class[int(num_str)] - for num_str in user_input.split(",") - ] + return [num_to_class[int(num_str)] + for num_str in re.split(r"\s*,\s*", user_input.strip())] except KeyError: logger.error(constants.INVALID_NUMBER_MESSAGE) sys.exit(2) - user_input = input(constants.CHOOSE_NUMBER_MESSAGE) - return [ - num_to_class[int(num_str)] - for num_str in user_input.split(",") - ] except EOFError: sys.exit(1) diff --git a/manim/config.py b/manim/config.py index 12f0ff4240..67927d28b0 100644 --- a/manim/config.py +++ b/manim/config.py @@ -24,9 +24,6 @@ def _parse_config(config_parser, args): # By default, use the CLI section of the digested .cfg files default = config_parser['CLI'] - # This will be the final config dict exposed to the user - config = {} - # Handle the *_quality flags. These determine the section to read # and are stored in 'camera_config'. Note the highest resolution # passed as argument will be used. @@ -114,13 +111,12 @@ def _parse_file_writer_config(config_parser, args): for boolean_opt in ['preview', 'show_file_in_finder', 'quiet', 'sound', 'leave_progress_bars', 'write_to_movie', 'save_last_frame', 'save_pngs', 'save_as_gif', 'write_all']: + attr = getattr(args, boolean_opt) config[boolean_opt] = (default.getboolean(boolean_opt) - if getattr(args, boolean_opt) is None - else getattr(args, boolean_opt)) + if attr is None else attr) for str_opt in ['media_dir', 'video_dir', 'tex_dir', 'text_dir']: - config[str_opt] = (default[str_opt] - if getattr(args, str_opt) is None - else getattr(args, str_opt)) + attr = getattr(args, str_opt) + config[str_opt] = (default[str_opt] if attr is None else attr) # Handle the -t (--transparent) flag. This flag determines which # section to use from the .cfg file. From 9e5ea500a81b92ae7643e579418fff45933ebb59 Mon Sep 17 00:00:00 2001 From: leotrs Date: Thu, 11 Jun 2020 10:52:09 -0400 Subject: [PATCH 27/38] check for None and for empty string --- manim/scene/scene_file_writer.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/manim/scene/scene_file_writer.py b/manim/scene/scene_file_writer.py index 841cad533d..fb9eac05e9 100644 --- a/manim/scene/scene_file_writer.py +++ b/manim/scene/scene_file_writer.py @@ -115,9 +115,8 @@ def get_default_scene_name(self): str The default scene name. """ - return (file_writer_config['output_file'] - if file_writer_config['output_file'] is not None - else self.scene.__class__.__name__) + fn = file_writer_config['output_file'] + return (fn if fn is not None and fn else self.scene.__class__.__name__) def get_resolution_directory(self): """ From 3b962e4d5f93ccd9b82a6c44ca1af62ea6028eb8 Mon Sep 17 00:00:00 2001 From: leotrs Date: Mon, 15 Jun 2020 09:21:39 -0400 Subject: [PATCH 28/38] handle more general cases of file names --- manim/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manim/__main__.py b/manim/__main__.py index 1fba10fa9a..3d9b6aa805 100644 --- a/manim/__main__.py +++ b/manim/__main__.py @@ -4,6 +4,7 @@ import platform import subprocess as sp import sys +import re import traceback import importlib.util @@ -75,7 +76,6 @@ def prompt_user_for_choice(scene_classes): print("%d: %s" % (count, name)) num_to_class[count] = scene_class try: - import re user_input = input(constants.CHOOSE_NUMBER_MESSAGE) return [num_to_class[int(num_str)] for num_str in re.split(r"\s*,\s*", user_input.strip())] @@ -136,7 +136,7 @@ def get_module(file_name): sys.exit(2) else: if os.path.exists(file_name): - module_name = file_name.replace(os.sep, ".").replace(".py", "") + module_name = re.replace(r"\..+$", "", file_name.replace(os.sep, ".")) spec = importlib.util.spec_from_file_location(module_name, file_name) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) From b3ebc9ec9c3e80d3d5e30c4921cd6a7a3249bb64 Mon Sep 17 00:00:00 2001 From: leotrs Date: Mon, 15 Jun 2020 09:21:56 -0400 Subject: [PATCH 29/38] make the -s flag invalidate -w --- manim/config.py | 8 +++++++- manim/default.cfg | 7 ++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/manim/config.py b/manim/config.py index 67927d28b0..80def38298 100644 --- a/manim/config.py +++ b/manim/config.py @@ -118,6 +118,12 @@ def _parse_file_writer_config(config_parser, args): attr = getattr(args, str_opt) config[str_opt] = (default[str_opt] if attr is None else attr) + # Handle the -s (--save_last_frame) flag: invalidate the -w flag + # At this point the save_last_frame option has already been set by + # both CLI and the cfg file, so read the config dict directly + if config['save_last_frame']: + config['write_to_movie'] = False + # Handle the -t (--transparent) flag. This flag determines which # section to use from the .cfg file. section = config_parser['transparent'] if args.transparent else default @@ -236,7 +242,7 @@ def _parse_cli(arg_list, input=True): "-s", "--save_last_frame", action="store_const", const=True, - help="Save the last frame", + help="Save the last frame (and do not save movie)", ) parser.add_argument( "-g", "--save_pngs", diff --git a/manim/default.cfg b/manim/default.cfg index c369910878..3ef6ae3b98 100644 --- a/manim/default.cfg +++ b/manim/default.cfg @@ -17,12 +17,13 @@ # -w, --write_to_movie write_to_movie = True -# -a, --write_all -write_all = False - # -s, --save_last_frame +# setting save_last_frame to True forces write_to_movie to False save_last_frame = False +# -a, --write_all +write_all = False + # -g, --save_pngs save_pngs = False From bfaffc736903b2a3e7fe08853a279c61413e0026 Mon Sep 17 00:00:00 2001 From: leotrs Date: Mon, 15 Jun 2020 09:27:53 -0400 Subject: [PATCH 30/38] fix: use correct re method --- manim/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manim/__main__.py b/manim/__main__.py index 3d9b6aa805..50dbb1cbb2 100644 --- a/manim/__main__.py +++ b/manim/__main__.py @@ -136,7 +136,7 @@ def get_module(file_name): sys.exit(2) else: if os.path.exists(file_name): - module_name = re.replace(r"\..+$", "", file_name.replace(os.sep, ".")) + module_name = re.sub(r"\..+$", "", file_name.replace(os.sep, ".")) spec = importlib.util.spec_from_file_location(module_name, file_name) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) From a67015f0e87226f4f27d07429577b18a2950e4a7 Mon Sep 17 00:00:00 2001 From: leotrs Date: Mon, 15 Jun 2020 09:28:03 -0400 Subject: [PATCH 31/38] delete unnecessary code --- manim/config.py | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/manim/config.py b/manim/config.py index 80def38298..db153bf4a0 100644 --- a/manim/config.py +++ b/manim/config.py @@ -357,25 +357,9 @@ def _parse_cli(arg_list, input=True): def _init_dirs(config): - if not (config["video_dir"] and config["tex_dir"]): - if config["media_dir"]: - if not os.path.isdir(config["media_dir"]): - os.makedirs(config["media_dir"]) - if not os.path.isdir(config["media_dir"]): - config["media_dir"] = "./media" - else: - logger.warning( - f"Media will be written to {config['media_dir'] + os.sep}. You can change " - "this behavior with the --media_dir flag, or by adjusting manim.cfg." - ) - else: - if config["media_dir"]: - logger.warning( - "Ignoring --media_dir, since both --tex_dir and --video_dir were passed." - ) - # Make sure all folders exist - for folder in [config["video_dir"], config["tex_dir"], config["text_dir"]]: + for folder in [config["media_dir"], config["video_dir"], + config["tex_dir"], config["text_dir"]]: if not os.path.exists(folder): os.makedirs(folder) From 2ef1070f54ec9df582dbfb41bcc75031b98b9373 Mon Sep 17 00:00:00 2001 From: leotrs Date: Wed, 17 Jun 2020 21:27:07 -0400 Subject: [PATCH 32/38] more pythonic --- manim/scene/scene_file_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manim/scene/scene_file_writer.py b/manim/scene/scene_file_writer.py index fb9eac05e9..aaea1d2f1f 100644 --- a/manim/scene/scene_file_writer.py +++ b/manim/scene/scene_file_writer.py @@ -116,7 +116,7 @@ def get_default_scene_name(self): The default scene name. """ fn = file_writer_config['output_file'] - return (fn if fn is not None and fn else self.scene.__class__.__name__) + return (fn if fn else self.scene.__class__.__name__) def get_resolution_directory(self): """ From e4837b40a5ca9f397164f728ef48c51af3ce5c09 Mon Sep 17 00:00:00 2001 From: leotrs Date: Sat, 4 Jul 2020 09:43:37 -0400 Subject: [PATCH 33/38] update tests wo new config system --- manim/__main__.py | 1 - manim/config.py | 37 +++++++++------ manim/default.cfg | 12 ++--- manim/mobject/mobject.py | 10 ++--- manim/scene/graph_scene.py | 3 +- tests/conftest.py | 31 ------------- tests/{test_CLI.py => test_cli/test_cli.py} | 8 ++-- tests/test_geometry.py | 8 ++-- tests/test_graph.py | 8 ++-- tests/test_writing.py | 6 +-- tests/testing_utils.py | 50 +++++++++------------ 11 files changed, 72 insertions(+), 102 deletions(-) rename tests/{test_CLI.py => test_cli/test_cli.py} (91%) diff --git a/manim/__main__.py b/manim/__main__.py index 50dbb1cbb2..d1536e26fc 100644 --- a/manim/__main__.py +++ b/manim/__main__.py @@ -1,5 +1,4 @@ import inspect -import itertools as it import os import platform import subprocess as sp diff --git a/manim/config.py b/manim/config.py index db153bf4a0..6b86fc5f27 100644 --- a/manim/config.py +++ b/manim/config.py @@ -114,9 +114,15 @@ def _parse_file_writer_config(config_parser, args): attr = getattr(args, boolean_opt) config[boolean_opt] = (default.getboolean(boolean_opt) if attr is None else attr) - for str_opt in ['media_dir', 'video_dir', 'tex_dir', 'text_dir']: + # for str_opt in ['media_dir', 'video_dir', 'tex_dir', 'text_dir']: + for str_opt in ['media_dir']: attr = getattr(args, str_opt) config[str_opt] = (default[str_opt] if attr is None else attr) + dir_names = {'video_dir': 'videos', + 'tex_dir': 'Tex', + 'text_dir': 'texts'} + for name in dir_names: + config[name] = os.path.join(config['media_dir'], dir_names[name]) # Handle the -s (--save_last_frame) flag: invalidate the -w flag # At this point the save_last_frame option has already been set by @@ -270,19 +276,19 @@ def _parse_cli(arg_list, input=True): "--media_dir", help="directory to write media", ) - video_group = parser.add_mutually_exclusive_group() - video_group.add_argument( - "--video_dir", - help="directory to write file tree for video", - ) - parser.add_argument( - "--tex_dir", - help="directory to write tex", - ) - parser.add_argument( - "--text_dir", - help="directory to write text", - ) + # video_group = parser.add_mutually_exclusive_group() + # video_group.add_argument( + # "--video_dir", + # help="directory to write file tree for video", + # ) + # parser.add_argument( + # "--tex_dir", + # help="directory to write tex", + # ) + # parser.add_argument( + # "--text_dir", + # help="directory to write text", + # ) parser.add_argument( "--tex_template", help="Specify a custom TeX template file", @@ -390,6 +396,9 @@ def _from_command_line(): if _from_command_line(): args = _parse_cli(sys.argv[1:]) + file_config = os.path.join(os.path.dirname(args.file), 'manim.cfg') + if os.path.exists(file_config): + config_files.append(file_config) if args.config_file is not None: config_files.append(args.config_file) diff --git a/manim/default.cfg b/manim/default.cfg index 3ef6ae3b98..c2cf102950 100644 --- a/manim/default.cfg +++ b/manim/default.cfg @@ -67,14 +67,14 @@ upto_animation_number = -1 # --media_dir media_dir = ./media -# --video_dir -video_dir = %(MEDIA_DIR)s/videos +# # --video_dir +# video_dir = %(MEDIA_DIR)s/videos -# --tex_dir -tex_dir = %(MEDIA_DIR)s/Tex +# # --tex_dir +# tex_dir = %(MEDIA_DIR)s/Tex -# --text_dir -text_dir = %(MEDIA_DIR)s/texts +# # --text_dir +# text_dir = %(MEDIA_DIR)s/texts # If the -t (--transparent) flag is used, these will be replaced with the # values specified in the [TRANSPARENT] section later in this file. diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py index b992de86a9..bb5866af55 100644 --- a/manim/mobject/mobject.py +++ b/manim/mobject/mobject.py @@ -433,7 +433,7 @@ def next_to(self, mobject_or_point, return self def shift_onto_screen(self, **kwargs): - space_lengths = [FRAME_X_RADIUS, FRAME_Y_RADIUS] + space_lengths = [config['frame_x_radius'], config['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) @@ -444,13 +444,13 @@ def shift_onto_screen(self, **kwargs): return self def is_off_screen(self): - if self.get_left()[0] > FRAME_X_RADIUS: + if self.get_left()[0] > config['frame_x_radius']: return True - if self.get_right()[0] < -FRAME_X_RADIUS: + if self.get_right()[0] < -config['frame_x_radius']: return True - if self.get_bottom()[1] > FRAME_Y_RADIUS: + if self.get_bottom()[1] > config['frame_y_radius']: return True - if self.get_top()[1] < -FRAME_Y_RADIUS: + if self.get_top()[1] < -config['frame_y_radius']: return True return False diff --git a/manim/scene/graph_scene.py b/manim/scene/graph_scene.py index a1f456d02c..23a0b42bc7 100644 --- a/manim/scene/graph_scene.py +++ b/manim/scene/graph_scene.py @@ -1,5 +1,6 @@ import itertools as it +from ..config import config from ..animation.creation import Write, DrawBorderThenFill, ShowCreation from ..animation.transform import Transform from ..animation.update import UpdateFromAlphaFunc @@ -421,7 +422,7 @@ def get_graph_label( # Search from right to left for x in np.linspace(self.x_max, self.x_min, 100): point = self.input_to_graph_point(x, graph) - if point[1] < FRAME_Y_RADIUS: + if point[1] < config['frame_y_radius']: break x_val = x label.next_to( diff --git a/tests/conftest.py b/tests/conftest.py index bac45335b7..c07ad705c1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,4 @@ -from manim import dirs from manim import config - import pytest import numpy as np import os @@ -32,32 +30,3 @@ def pytest_collection_modifyitems(config, items): @pytest.fixture(scope="module") def python_version(): return "python3" if sys.platform == "darwin" else "python" - - -@pytest.fixture -def get_config_test(): - """Function used internally by pytest as a fixture. Return the Configuration for the scenes rendered. The config is the one used when - calling the flags -s -l -dry_run - """ - CONFIG = { - 'camera_config': { - 'frame_rate': 15, - 'pixel_height': 480, - 'pixel_width': 854 - }, - 'end_at_animation_number': None, - 'file_writer_config': { - 'file_name': None, - 'input_file_path': 'test.py', - 'movie_file_extension': '.mp4', - 'png_mode': 'RGB', - 'save_as_gif': False, - 'save_last_frame': False, - 'save_pngs': False, - 'write_to_movie': False - }, - 'leave_progress_bars': False, - 'skip_animations': True, - 'start_at_animation_number': None - } - return CONFIG diff --git a/tests/test_CLI.py b/tests/test_cli/test_cli.py similarity index 91% rename from tests/test_CLI.py rename to tests/test_cli/test_cli.py index 9bb48fc8a9..1bd6cb2714 100644 --- a/tests/test_CLI.py +++ b/tests/test_cli/test_cli.py @@ -16,14 +16,14 @@ def capture(command): def test_help(python_version): command = [python_version, "-m", "manim", "--help"] out, err, exitcode = capture(command) - assert exitcode == 0, f"Manim has been installed incorrectly. Please refer to the troubleshooting section on the wiki. Error: {err}" + assert exitcode == 0, f"Manim has been installed incorrectly. Please refer to the troubleshooting section on the wiki. Error:\n{err}" @pytest.mark.skip_end_to_end def test_basicScene(python_version): """ Simulate SquareToCircle. The cache will be saved in tests_caches/media_temp (temporary directory). This is mainly intended to test the partial-movies process. """ - path_basic_scene = os.path.join("tests", "tests_data", "basic_scenes.py") - path_output = os.path.join("tests", "tests_cache", "media_temp") + path_basic_scene = os.path.join("tests_data", "basic_scenes.py") + path_output = os.path.join("tests_cache", "media_temp") command = [python_version, "-m", "manim", path_basic_scene, "SquareToCircle", "-l", "--media_dir", path_output] out, err, exitcode = capture(command) @@ -42,5 +42,5 @@ def test_WriteStuff(python_version): out, err, exitcode = capture(command) assert exitcode == 0, err assert os.path.exists(os.path.join( - path_output, "videos", "basic_scenes", "480p15", "WriteStuff.mp4")), err + path_output, "videos", "basic_scenes", "480p15", "WriteStuff.mp4")), err rmtree(path_output) diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 5fbd9aa89c..5e7a812a5e 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1,11 +1,11 @@ from manim import * +import numpy as np from testing_utils import utils_test_scenes, get_scenes_to_test class CoordinatesTest(Scene): def construct(self): dots = [Dot(np.array([x, y, 0])) for x in range(-7, 8) for y in range(-4, 5)] - self.play(Animation(VGroup(*dots))) @@ -99,10 +99,12 @@ def construct(self): a = Rectangle() self.play(Animation(a)) + class RoundedRectangleTest(Scene): def construct(self): a = RoundedRectangle() self.play(Animation(a)) -def test_scenes(get_config_test): - utils_test_scenes(get_scenes_to_test(__name__), get_config_test, "geometry") + +def test_scenes(): + utils_test_scenes(get_scenes_to_test(__name__), "geometry") diff --git a/tests/test_graph.py b/tests/test_graph.py index e2ae2eccbd..a4783ef783 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -11,15 +11,15 @@ class PlotFunctions(GraphScene): "graph_origin": ORIGIN, "function_color": RED, "axes_color": GREEN, - "x_labeled_nums": range(-10,12,2), + "x_labeled_nums": range(-10,12,2), } - def construct(self): + def construct(self): constants.TEX_TEMPLATE = TexTemplate() self.setup_axes() f = self.get_graph(lambda x: x**2) self.play(Animation(f)) -def test_scenes(get_config_test): - utils_test_scenes(get_scenes_to_test(__name__), get_config_test, "graph", caching_needed=True) +def test_scenes(): + utils_test_scenes(get_scenes_to_test(__name__), "graph", caching_needed=True) diff --git a/tests/test_writing.py b/tests/test_writing.py index 2b53a7059e..6a2a6869d2 100644 --- a/tests/test_writing.py +++ b/tests/test_writing.py @@ -20,7 +20,7 @@ def construct(self): class TexMobjectTest(Scene): def construct(self): - #IMPORTANT NOTE : This won't test the abitilty of manim to write/cache latex. + #IMPORTANT NOTE : This won't test the abitilty of manim to write/cache latex. # i.e It will pass even if latex is not installed. # This is due to the fact that the latex used here has been cached (see test_cache directory) constants.TEX_TEMPLATE = TexTemplate() @@ -32,5 +32,5 @@ def construct(self): -def test_scenes(get_config_test): - utils_test_scenes(get_scenes_to_test(__name__), get_config_test, "writing", caching_needed=True) +def test_scenes(): + utils_test_scenes(get_scenes_to_test(__name__), "writing", caching_needed=True) diff --git a/tests/testing_utils.py b/tests/testing_utils.py index a5b668d59b..6159c43b1c 100644 --- a/tests/testing_utils.py +++ b/tests/testing_utils.py @@ -6,14 +6,13 @@ import pytest from manim import logger -from manim import dirs from manim import config class SceneTester: """Class used to test the animations. - Parameters + Parameters ---------- scene_object : :class:`~.Scene` The scene to be tested @@ -25,40 +24,31 @@ class SceneTester: Attributes ----------- path_tests_medias_cache : : class:`str` - Path to 'media' folder generated by manim. This folder contains cached data used by some tests. + Path to 'media' folder generated by manim. This folder contains cached data used by some tests. path_tests_data : : class:`str` Path to the data used for the tests (i.e the pre-rendered frames). scene : :class:`Scene` The scene tested """ - def __init__(self, scene_object, config_scene, module_tested, caching_needed=False): + def __init__(self, scene_object, module_tested, caching_needed=False): # Disable the the logs, (--quiet is broken) TODO logging.disable(logging.CRITICAL) - self.path_tests_medias_cache = os.path.join( - 'tests', 'tests_cache', module_tested) - self.path_tests_data = os.path.join( - 'tests', 'tests_data', module_tested) + self.path_tests_medias_cache = os.path.join('tests_cache', module_tested) + self.path_tests_data = os.path.join('tests_data', module_tested) - tex_dir, text_dir = None, None if caching_needed: - text_dir = os.path.join( + config['text_dir'] = os.path.join( self.path_tests_medias_cache, scene_object.__name__, 'Text') - tex_dir = os.path.join(self.path_tests_medias_cache, - scene_object.__name__, 'Tex') - conf_dirs = {'media_dir': None, - 'video_dir': None, - 'tex_dir': tex_dir, - 'text_dir': text_dir, - } - # PROVISIONAL. To change when #98 is merged. TODO - config.initialize_directories(conf_dirs) + config['tex_dir'] = os.path.join( + self.path_tests_medias_cache, scene_object.__name__, 'Tex') + # By invoking this, the scene is rendered. - self.scene = scene_object(**config_scene) + self.scene = scene_object() def load_data(self): """Load the np.array of the last frame of a pre-rendered scene. If not found, throw FileNotFoundError. - + Returns ------- :class:`numpy.array` @@ -73,7 +63,7 @@ def load_data(self): return data_loaded def test(self): - """ Core of the test. Will compare the pre-rendered frame (get with load_data()) with the frame rendered during the test (get with scene.get_frame())""" + """Compare pre-rendered frame to the frame rendered during the test.""" test_result = np.array_equal(self.scene.get_frame(), self.load_data()) assert( test_result), f"The frames don't match. {str(self.scene).replace('Test', '')} has been modified. Please ignore if it was intended" @@ -81,34 +71,34 @@ def test(self): def get_scenes_to_test(module_name): """Get all Test classes of the module from which it is called. Used to fetch all the SceneTest of the module. - + Parameters ---------- module_name : :class:`str` - The name of the module tested. + The name of the module tested. Returns ------- :class:`list` - The list of all the classes of the module. + The list of all the classes of the module. """ return inspect.getmembers(sys.modules[module_name], lambda m: inspect.isclass(m) and m.__module__ == module_name) -def utils_test_scenes(scenes_to_test, CONFIG, module_name, caching_needed=False): +def utils_test_scenes(scenes_to_test, module_name, caching_needed=False): for _, scene_tested in scenes_to_test: - SceneTester(scene_tested, CONFIG, module_name, + SceneTester(scene_tested, module_name, caching_needed=caching_needed).test() def set_test_scene(scene_object, module_name): - """Function used to set up the test data for a new feature. This will basically set up a pre-rendered frame for a scene. This is meant to be used only + """Function used to set up the test data for a new feature. This will basically set up a pre-rendered frame for a scene. This is meant to be used only when setting up tests. Please refer to the wiki. - + Parameters ---------- scene_object : :class:`~.Scene` - The scene with wich we want to set up a new test. + The scene with wich we want to set up a new test. module_name : :class:`str` The name of the module in which the functionnality tested is contained. For example, 'Write' is contained in the module 'creation'. This will be used in the folder architecture of '/tests_data'. From b6603037f248c24324c6ab10b4bee95904923bb8 Mon Sep 17 00:00:00 2001 From: leotrs Date: Sat, 4 Jul 2020 09:45:21 -0400 Subject: [PATCH 34/38] update tests to config system --- tests/manim.cfg | 14 ++++++++++++++ tests/test_cli/manim.cfg | 6 ++++++ tests/tests_data/manim.cfg | 6 ++++++ 3 files changed, 26 insertions(+) create mode 100644 tests/manim.cfg create mode 100644 tests/test_cli/manim.cfg create mode 100644 tests/tests_data/manim.cfg diff --git a/tests/manim.cfg b/tests/manim.cfg new file mode 100644 index 0000000000..ee0cd43aa9 --- /dev/null +++ b/tests/manim.cfg @@ -0,0 +1,14 @@ +[CLI] +frame_rate = 15 +pixel_height = 480 +pixel_width = 854 +from_at_animation_number = 0 +upto_at_animation_number = -1 +png_mode = RGB +movie_file_extension = .mp4 +write_to_movie = False +write_all = False +save_last_frame = False +save_pngs = False +save_as_gif = False +leave_progress_bars = False diff --git a/tests/test_cli/manim.cfg b/tests/test_cli/manim.cfg new file mode 100644 index 0000000000..b71cfe2352 --- /dev/null +++ b/tests/test_cli/manim.cfg @@ -0,0 +1,6 @@ +[CLI] +movie_file_extension = .mp4 +write_to_movie = True +# write_all = False +save_last_frame = True +# save_pngs = False diff --git a/tests/tests_data/manim.cfg b/tests/tests_data/manim.cfg new file mode 100644 index 0000000000..573960fff3 --- /dev/null +++ b/tests/tests_data/manim.cfg @@ -0,0 +1,6 @@ +[CLI] +movie_file_extension = .mp4 +write_to_movie = True +# write_all = False +save_last_frame = False +# save_pngs = False From 9077542c0f612a035f75fb092c94e071532c6ee0 Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Sat, 4 Jul 2020 22:39:33 -0700 Subject: [PATCH 35/38] Loose ends --- manim/config.py | 15 +++++++++------ manim/scene/scene.py | 18 +++++++++--------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/manim/config.py b/manim/config.py index 6b86fc5f27..a866eb89dd 100644 --- a/manim/config.py +++ b/manim/config.py @@ -177,7 +177,7 @@ def _parse_file_writer_config(config_parser, args): def _parse_cli(arg_list, input=True): parser = argparse.ArgumentParser( description='Animation engine for explanatory math videos', - epilog='Made with <3 by the manim community devs' + epilog='Made with ❤ by the manim community devs' ) if input: parser.add_argument( @@ -391,16 +391,19 @@ def _from_command_line(): config_files = [ library_wide, os.path.expanduser('~/.manim.cfg'), - os.path.join(os.getcwd(), 'manim.cfg'), ] if _from_command_line(): args = _parse_cli(sys.argv[1:]) - file_config = os.path.join(os.path.dirname(args.file), 'manim.cfg') - if os.path.exists(file_config): - config_files.append(file_config) if args.config_file is not None: - config_files.append(args.config_file) + if os.path.exists(args.config_file): + config_files.append(args.config_file) + else: + raise FileNotFoundError(f"Config file {args.config_file} doesn't exist") + else: + script_directory_file_config = os.path.join(os.path.dirname(args.file), 'manim.cfg') + if os.path.exists(script_directory_file_config): + config_files.append(script_directory_file_config) else: # In this case, we still need an empty args object. diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 766a4e13d6..1c26a7defa 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -62,7 +62,7 @@ def __init__(self, **kwargs): self.foreground_mobjects = [] self.num_plays = 0 self.time = 0 - self.original_skipping_status = self.skip_animations + self.original_skipping_status = file_writer_config['skip_animations'] if self.random_seed is not None: random.seed(self.random_seed) np.random.seed(self.random_seed) @@ -234,7 +234,7 @@ def update_frame( #TODO Description in Docstring **kwargs """ - if self.skip_animations and not ignore_skipping: + if file_writer_config['skip_animations'] and not ignore_skipping: return if mobjects is None: mobjects = list_update( @@ -675,7 +675,7 @@ def get_time_progression(self, run_time, n_iterations=None, override_skip_animat ProgressDisplay The CommandLine Progress Bar. """ - if self.skip_animations and not override_skip_animations: + if file_writer_config['skip_animations'] and not override_skip_animations: times = [run_time] else: step = 1 / self.camera.frame_rate @@ -822,10 +822,10 @@ def update_skipping_status(self): if file_writer_config['from_animation_number']: if self.num_plays == file_writer_config['from_animation_number']: - self.skip_animations = False + file_writer_config['skip_animations'] = False if file_writer_config['upto_animation_number']: if self.num_plays >= file_writer_config['upto_animation_number']: - self.skip_animations = True + file_writer_config['skip_animations'] = True raise EndSceneEarlyException() def handle_play_like_call(func): @@ -850,7 +850,7 @@ def handle_play_like_call(func): """ def wrapper(self, *args, **kwargs): self.update_skipping_status() - allow_write = not self.skip_animations + allow_write = not file_writer_config['skip_animations'] self.file_writer.begin_animation(allow_write) func(self, *args, **kwargs) self.file_writer.end_animation(allow_write) @@ -923,7 +923,7 @@ def finish_animations(self, animations): self.mobjects_from_last_animation = [ anim.mobject for anim in animations ] - if self.skip_animations: + if file_writer_config['skip_animations']: # TODO, run this call in for each animation? self.update_mobjects(self.get_run_time(animations)) else: @@ -1066,7 +1066,7 @@ def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): if stop_condition is not None and stop_condition(): time_progression.close() break - elif self.skip_animations: + elif file_writer_config['skip_animations']: # Do nothing return self else: @@ -1135,7 +1135,7 @@ def add_frames(self, *frames): """ dt = 1 / self.camera.frame_rate self.increment_time(len(frames) * dt) - if self.skip_animations: + if file_writer_config['skip_animations']: return for frame in frames: self.file_writer.write_frame(frame) From f4c8431824b6387531eae7f28d6670290abc6a3e Mon Sep 17 00:00:00 2001 From: leotrs Date: Sun, 5 Jul 2020 09:48:11 -0400 Subject: [PATCH 36/38] fix deprecatet variables --- manim/mobject/svg/drawings.py | 2 +- tests/test_creation.py | 4 ++-- tests/test_indication.py | 6 ++---- tests/test_movements.py | 4 ++-- tests/test_threed.py | 4 ++-- tests/test_transform.py | 4 ++-- tests/test_updaters.py | 4 ++-- tests/tests_data/basic_scenes.py | 6 +++--- 8 files changed, 16 insertions(+), 18 deletions(-) diff --git a/manim/mobject/svg/drawings.py b/manim/mobject/svg/drawings.py index 22e25e5004..96f7c2f505 100644 --- a/manim/mobject/svg/drawings.py +++ b/manim/mobject/svg/drawings.py @@ -302,7 +302,7 @@ def __init__(self, **kwargs): videos = [VideoIcon() for x in range(self.num_videos)] VGroup.__init__(self, *videos, **kwargs) self.arrange() - self.set_width(FRAME_WIDTH - MED_LARGE_BUFF) + self.set_width(config['frame_width'] - config['med_large_buff']) self.set_color_by_gradient(*self.gradient_colors) diff --git a/tests/test_creation.py b/tests/test_creation.py index edf682086c..0745b2139a 100644 --- a/tests/test_creation.py +++ b/tests/test_creation.py @@ -89,5 +89,5 @@ def construct(self): square = Square() self.play(ShrinkToCenter(square)) -def test_scenes(get_config_test): - utils_test_scenes(get_scenes_to_test(__name__), get_config_test, "creation") +def test_scenes(): + utils_test_scenes(get_scenes_to_test(__name__), "creation") diff --git a/tests/test_indication.py b/tests/test_indication.py index 10207771b4..48217c4f46 100644 --- a/tests/test_indication.py +++ b/tests/test_indication.py @@ -73,7 +73,5 @@ def construct(self): self.add(square) self.play(TurnInsideOut(square)) -def test_scenes(get_config_test): - utils_test_scenes(get_scenes_to_test(__name__), get_config_test, "indication") - - +def test_scenes(): + utils_test_scenes(get_scenes_to_test(__name__), "indication") diff --git a/tests/test_movements.py b/tests/test_movements.py index ed2774b95d..7aa7e4b8f2 100644 --- a/tests/test_movements.py +++ b/tests/test_movements.py @@ -46,5 +46,5 @@ def construct(self): self.play(square.shift, UP) -def test_scenes(get_config_test): - utils_test_scenes(get_scenes_to_test(__name__), get_config_test, "movements") +def test_scenes(): + utils_test_scenes(get_scenes_to_test(__name__), "movements") diff --git a/tests/test_threed.py b/tests/test_threed.py index ff75e983c7..37fd406d8e 100644 --- a/tests/test_threed.py +++ b/tests/test_threed.py @@ -31,5 +31,5 @@ def construct(self): self.play(Animation(cube)) -def test_scenes(get_config_test): - utils_test_scenes(get_scenes_to_test(__name__), get_config_test, "threed") +def test_scenes(): + utils_test_scenes(get_scenes_to_test(__name__), "threed") diff --git a/tests/test_transform.py b/tests/test_transform.py index c19df2f146..85ad382329 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -119,5 +119,5 @@ def construct(self): self.play(CyclicReplace(square, circle)) -def test_scenes(get_config_test): - utils_test_scenes(get_scenes_to_test(__name__), get_config_test, "transform") \ No newline at end of file +def test_scenes(): + utils_test_scenes(get_scenes_to_test(__name__), "transform") diff --git a/tests/test_updaters.py b/tests/test_updaters.py index bd8d2badc5..7732989720 100644 --- a/tests/test_updaters.py +++ b/tests/test_updaters.py @@ -21,5 +21,5 @@ def construct(self): line_2.rotate(theta.get_value(), about_point=ORIGIN) -def test_scenes(get_config_test): - utils_test_scenes(get_scenes_to_test(__name__), get_config_test, "updaters") \ No newline at end of file +def test_scenes(): + utils_test_scenes(get_scenes_to_test(__name__), "updaters") diff --git a/tests/tests_data/basic_scenes.py b/tests/tests_data/basic_scenes.py index b0f6b1f93a..cc57e0d67a 100644 --- a/tests/tests_data/basic_scenes.py +++ b/tests/tests_data/basic_scenes.py @@ -14,8 +14,8 @@ def construct(self): self.play(Transform(square, circle)) self.play(FadeOut(square)) -class WriteStuff(Scene): - def construct(self): +class WriteStuff(Scene): + def construct(self): example_text = TextMobject( "This is a some text", tex_to_color_map={"text": YELLOW} @@ -25,6 +25,6 @@ def construct(self): ) group = VGroup(example_text, example_tex) group.arrange(DOWN) - group.set_width(FRAME_WIDTH - 2 * LARGE_BUFF) + group.set_width(config['frame_width'] - 2 * LARGE_BUFF) self.play(Write(example_text)) From 0229f784054604909a69317e2e8089594574f239 Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Sun, 5 Jul 2020 15:07:36 -0700 Subject: [PATCH 37/38] Add information to test failures --- tests/testing_utils.py | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/tests/testing_utils.py b/tests/testing_utils.py index 6159c43b1c..affd66e206 100644 --- a/tests/testing_utils.py +++ b/tests/testing_utils.py @@ -43,6 +43,10 @@ def __init__(self, scene_object, module_tested, caching_needed=False): config['tex_dir'] = os.path.join( self.path_tests_medias_cache, scene_object.__name__, 'Tex') + config['pixel_height'] = 480 + config['pixel_width'] = 854 + config['frame_rate'] = 15 + # By invoking this, the scene is rendered. self.scene = scene_object() @@ -54,19 +58,31 @@ def load_data(self): :class:`numpy.array` The pre-rendered frame. """ - with pytest.raises(FileNotFoundError) as e_info: - data_loaded = np.load(os.path.join( - self.path_tests_data, "{}.npy".format(str(self.scene)))) - raise FileNotFoundError('test_data not found !') - assert (str(e_info.value) == - 'test_data not found !'), f"{str(self.scene).replace('Test', '')} does not seem have a pre-rendered frame for testing, or it has not been found." - return data_loaded + frame_data_path = os.path.join( + self.path_tests_data, "{}.npy".format(str(self.scene))) + return np.load(frame_data_path) + def test(self): """Compare pre-rendered frame to the frame rendered during the test.""" - test_result = np.array_equal(self.scene.get_frame(), self.load_data()) - assert( - test_result), f"The frames don't match. {str(self.scene).replace('Test', '')} has been modified. Please ignore if it was intended" + frame_data = self.scene.get_frame() + expected_frame_data = self.load_data() + + assert frame_data.shape == expected_frame_data.shape, \ + "The frames have different shape:" \ + + f"\nexpected_frame_data.shape = {expected_frame_data.shape}" \ + + f"\nframe_data.shape = {frame_data.shape}" + + test_result = np.array_equal(frame_data, expected_frame_data) + if not test_result: + incorrect_indices = np.argwhere(frame_data != expected_frame_data) + first_incorrect_index = incorrect_indices[0][:2] + first_incorrect_point = frame_data[tuple(first_incorrect_index)] + expected_point = expected_frame_data[tuple(first_incorrect_index)] + assert test_result, \ + f"The frames don't match. {str(self.scene).replace('Test', '')} has been modified." \ + + "\nPlease ignore if it was intended." \ + + f"\nFirst unmatched index is at {first_incorrect_index}: {first_incorrect_point} != {expected_point}" def get_scenes_to_test(module_name): From b69f6e809bd29cca2eac18bee0618d8ecc4fc16b Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Sun, 5 Jul 2020 15:08:15 -0700 Subject: [PATCH 38/38] Update config variables and paths in tests --- manim/mobject/svg/text_mobject.py | 4 ++-- tests/test_cli/test_cli.py | 4 ++-- tests/test_movements.py | 2 +- tests/tests_cache/writing/TextTest/Text/space.svg | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/manim/mobject/svg/text_mobject.py b/manim/mobject/svg/text_mobject.py index 359271790b..74751d92e0 100644 --- a/manim/mobject/svg/text_mobject.py +++ b/manim/mobject/svg/text_mobject.py @@ -96,7 +96,7 @@ def __init__(self, text, **config): def get_space_width(self): size = self.size * 10 - dir_name = config['TEXT_DIR'] + dir_name = config['text_dir'] file_name = os.path.join(dir_name, "space") + '.svg' surface = cairo.SVGSurface(file_name, 600, 400) @@ -292,7 +292,7 @@ def text2svg(self): if NOT_SETTING_FONT_MSG != '': logger.warning(NOT_SETTING_FONT_MSG) - dir_name = config['TEXT_DIR'] + dir_name = config['text_dir'] hash_name = self.text2hash() file_name = os.path.join(dir_name, hash_name)+'.svg' if os.path.exists(file_name): diff --git a/tests/test_cli/test_cli.py b/tests/test_cli/test_cli.py index 1bd6cb2714..da850a1479 100644 --- a/tests/test_cli/test_cli.py +++ b/tests/test_cli/test_cli.py @@ -35,8 +35,8 @@ def test_basicScene(python_version): @pytest.mark.skip_end_to_end def test_WriteStuff(python_version): """This is mainly intended to test the caching process of the tex objects""" - path_basic_scene = os.path.join("tests", "tests_data", "basic_scenes.py") - path_output = os.path.join("tests", "tests_cache", "media_temp") + path_basic_scene = os.path.join("tests_data", "basic_scenes.py") + path_output = os.path.join("tests_cache", "media_temp") command = [python_version, "-m", "manim", path_basic_scene, "WriteStuff", "-l", "--media_dir", path_output] out, err, exitcode = capture(command) diff --git a/tests/test_movements.py b/tests/test_movements.py index 7aa7e4b8f2..1079a240ab 100644 --- a/tests/test_movements.py +++ b/tests/test_movements.py @@ -6,7 +6,7 @@ class HomotopyTest(Scene): def construct(self): def func(x, y, z, t): norm = get_norm([x, y]) - tau = interpolate(5, -5, t) + norm/FRAME_X_RADIUS + tau = interpolate(5, -5, t) + norm/config['frame_x_radius'] alpha = sigmoid(tau) return [x, y + 0.5*np.sin(2*np.pi*alpha)-t*SMALL_BUFF/2, z] square = Square() diff --git a/tests/tests_cache/writing/TextTest/Text/space.svg b/tests/tests_cache/writing/TextTest/Text/space.svg index 4de22d4edd..04356bce9d 100644 --- a/tests/tests_cache/writing/TextTest/Text/space.svg +++ b/tests/tests_cache/writing/TextTest/Text/space.svg @@ -3,14 +3,14 @@ - + - + - +