Merge pull request #2266 from 3b1b/video-work

Refactor config
This commit is contained in:
Grant Sanderson 2024-12-11 10:53:26 -06:00 committed by GitHub
commit eeb4fdf270
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 696 additions and 826 deletions

View file

@ -8,38 +8,35 @@ they are only used inside manim.
Frame and pixel shape
---------------------
These values will be determined based on the ``camera`` configuration in default_config.yml or custom_config.yml
.. code-block:: python
ASPECT_RATIO = 16.0 / 9.0
FRAME_HEIGHT = 8.0
FRAME_WIDTH = FRAME_HEIGHT * ASPECT_RATIO
FRAME_Y_RADIUS = FRAME_HEIGHT / 2
FRAME_X_RADIUS = FRAME_WIDTH / 2
ASPECT_RATIO
FRAME_HEIGHT
FRAME_WIDTH
FRAME_Y_RADIUS
FRAME_X_RADIUS
DEFAULT_PIXEL_HEIGHT = 1080
DEFAULT_PIXEL_WIDTH = 1920
DEFAULT_FPS = 30
DEFAULT_PIXEL_HEIGHT
DEFAULT_PIXEL_WIDTH
DEFAULT_FPS
Buffs
-----
.. code-block:: python
These values will be determined based on the ``size`` configuration in default_config.yml or custom_config.yml
SMALL_BUFF = 0.1
MED_SMALL_BUFF = 0.25
MED_LARGE_BUFF = 0.5
LARGE_BUFF = 1
DEFAULT_MOBJECT_TO_EDGE_BUFFER = MED_LARGE_BUFF # Distance between object and edge
DEFAULT_MOBJECT_TO_MOBJECT_BUFFER = MED_SMALL_BUFF # Distance between objects
Run times
---------
.. code-block:: python
DEFAULT_POINTWISE_FUNCTION_RUN_TIME = 3.0
DEFAULT_WAIT_TIME = 1.0
SMALL_BUFF
MED_SMALL_BUFF
MED_LARGE_BUFF
LARGE_BUFF
DEFAULT_MOBJECT_TO_EDGE_BUFF
DEFAULT_MOBJECT_TO_MOBJECT_BUFF
Coordinates
-----------
@ -89,16 +86,11 @@ Text
OBLIQUE = "OBLIQUE"
BOLD = "BOLD"
Stroke width
------------
.. code-block:: python
DEFAULT_STROKE_WIDTH = 4
Colours
-------
Color constants are determined based on the ``color`` configuration in default_config.yml or custom_config.yml
Here are the preview of default colours. (Modified from
`elteoremadebeethoven <https://elteoremadebeethoven.github.io/manim_3feb_docs.github.io/html/_static/colors/colors.html>`_)

View file

@ -9,6 +9,10 @@ custom_config
running file under the ``output`` path, and save the output (``images/``
or ``videos/``) in it.
- ``base``
The root directory that will hold files, such as video files manim renders,
or image resources that it pulls from
- ``output``
Output file path, the videos will be saved in the ``videos/`` folder under it,
and the pictures will be saved in the ``images/`` folder under it.
@ -66,34 +70,62 @@ custom_config
The directory for storing sound files to be used in ``Scene.add_sound()`` (
including ``.wav`` and ``.mp3``).
- ``temporary_storage``
- ``cache``
The directory for storing temporarily generated cache files, including
``Tex`` cache, ``Text`` cache and storage of object points.
``tex``
``window``
----------
- ``position_string``
The relative position of the playback window on the display (two characters,
the first character means upper(U) / middle(O) / lower(D), the second character
means left(L) / middle(O) / right(R)).
- ``monitor_index``
If using multiple monitors, which one should the window show up in?
- ``full_screen``
Should the preview window be full screen. If not, it defaults to half the screen
- ``position``
This is an option to more manually set the default window position, in pixel
coordinates, e.g. (500, 300)
- ``size``
Option to more manually set the default window size, in pixel coordinates,
e.g. (1920, 1080)
``camera``
----------
- ``resolution``
Resolution to render at, e.g. (1920, 1080)
- ``background_color``
Default background color of scenes
- ``fps``
Framerate
- ``background_opacity``
Opacity of the background
``file_writer``
---------------
Configuration specifying how files are written, e.g. what ffmpeg parameters to use
``scene``
-------
Some default configuration for the Scene class
- ``executable``
The executable program used to compile LaTeX (``latex`` or ``xelatex -no-pdf``
is recommended)
- ``template_file``
LaTeX template used, in ``manimlib/tex_templates``
- ``intermediate_filetype``
The type of intermediate vector file generated after compilation (``dvi`` if
``latex`` is used, ``xdv`` if ``xelatex`` is used)
- ``text_to_replace``
The text to be replaced in the template (needn't to change)
``universal_import_line``
-------------------------
Import line that need to execute when entering interactive mode directly.
``style``
---------
``text``
-------
- ``font``
Default font of Text
@ -101,57 +133,44 @@ Import line that need to execute when entering interactive mode directly.
- ``text_alignment``
Default text alignment for LaTeX
- ``background_color``
Default background color
``window_position``
-------------------
The relative position of the playback window on the display (two characters,
the first character means upper(U) / middle(O) / lower(D), the second character
means left(L) / middle(O) / right(R)).
``window_monitor``
------------------
The number of the monitor you want the preview window to pop up on. (default is 0)
``full_screen``
---------------
Whether open the window in full screen. (default is false)
``break_into_partial_movies``
-----------------------------
If this is set to ``True``, then many small files will be written corresponding
to each ``Scene.play`` and ``Scene.wait`` call, and these files will then be combined
to form the full scene.
Sometimes video-editing is made easier when working with the broken up scene, which
effectively has cuts at all the places you might want.
``camera_resolutions``
----------------------
Export resolutions
- ``low``
Low resolutions (default is 480p)
- ``medium``
Medium resolutions (default is 720p)
- ``high``
High resolutions (default is 1080p)
- ``ultra_high``
Ultra high resolutions (default is 4K)
- ``default_resolutions``
Default resolutions (one of the above four, default is high)
``fps``
``tex``
-------
Export frame rate. (default is 30)
- ``template``
Which configuration from the manimlib/tex_template.yml file should be used
to determine the latex compiler to use, and what preamble to include for
rendering tex.
``sizes``
---------
Valuess for various constants used in manimm to specify distances, like the height
of the frame, the value of SMALL_BUFF, LARGE_BUFF, etc.
``colors``
----------
Color pallete to use, determining values of color constants like RED, BLUE_E, TEAL, etc.
``loglevel``
------------
Can be DEBUG / INFO / WARNING / ERROR / CRITICAL
``universal_import_line``
-------------------------
Import line that need to execute when entering interactive mode directly.
``ignore_manimlib_modules_on_reload``
-------------------------------------
When calling ``reload`` during the interactive mode, imported modules are
by default reloaded, in case the user writing a scene which pulls from various
other files they have written. By default, modules withinn the manim library will
be ignored, but one developing manim may want to set this to be False so that
edits to the library are reloaded as well.

View file

@ -103,7 +103,6 @@ Below is the directory structure of manim:
├── family_ops.py # Process family members
├── file_ops.py # Process files and directories
├── images.py # Read image
├── init_config.py # Configuration guide
├── iterables.py # Functions related to list/dictionary processing
├── paths.py # Curve path
├── rate_functions.py # Some defined rate_functions

View file

@ -1,9 +1,48 @@
#!/usr/bin/env python
from addict import Dict
from manimlib import __version__
import manimlib.config
import manimlib.logger
import manimlib.utils.init_config
from manimlib.reload_manager import ReloadManager
from manimlib.config import manim_config
from manimlib.config import parse_cli
import manimlib.extract_scene
from manimlib.window import Window
from IPython.terminal.embed import KillEmbedded
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from argparse import Namespace
def run_scenes():
"""
Runs the scenes in a loop and detects when a scene reload is requested.
"""
# Create a new dict to be able to upate without
# altering global configuration
scene_config = Dict(manim_config.scene)
run_config = manim_config.run
if run_config.show_in_window:
# Create a reusable window
window = Window(**manim_config.window)
scene_config.update(window=window)
while True:
try:
# Blocking call since a scene may init an IPython shell()
scenes = manimlib.extract_scene.main(scene_config, run_config)
for scene in scenes:
scene.run()
return
except KillEmbedded:
# Requested via the `exit_raise` IPython runline magic
# by means of the reload_scene() command
pass
except KeyboardInterrupt:
break
def main():
@ -12,18 +51,11 @@ def main():
"""
print(f"ManimGL \033[32mv{__version__}\033[0m")
args = manimlib.config.parse_cli()
args = parse_cli()
if args.version and args.file is None:
return
if args.log_level:
manimlib.logger.log.setLevel(args.log_level)
if args.config:
manimlib.utils.init_config.init_customization()
return
reload_manager = ReloadManager(args)
reload_manager.run()
run_scenes()
if __name__ == "__main__":

View file

@ -7,8 +7,7 @@ from PIL import Image
from manimlib.camera.camera_frame import CameraFrame
from manimlib.constants import BLACK
from manimlib.constants import DEFAULT_FPS
from manimlib.constants import DEFAULT_PIXEL_HEIGHT, DEFAULT_PIXEL_WIDTH
from manimlib.constants import DEFAULT_RESOLUTION
from manimlib.constants import FRAME_HEIGHT
from manimlib.constants import FRAME_WIDTH
from manimlib.mobject.mobject import Mobject
@ -29,10 +28,9 @@ class Camera(object):
window: Optional[Window] = None,
background_image: Optional[str] = None,
frame_config: dict = dict(),
pixel_width: int = DEFAULT_PIXEL_WIDTH,
pixel_height: int = DEFAULT_PIXEL_HEIGHT,
fps: int = DEFAULT_FPS,
# Note: frame height and width will be resized to match the pixel aspect ratio
# Note: frame height and width will be resized to match this resolution aspect ratio
resolution=DEFAULT_RESOLUTION,
fps: int = 30,
background_color: ManimColor = BLACK,
background_opacity: float = 1.0,
# Points in vectorized mobjects with norm greater
@ -47,9 +45,9 @@ class Camera(object):
# to set samples to be greater than 0.
samples: int = 0,
):
self.background_image = background_image
self.window = window
self.default_pixel_shape = (pixel_width, pixel_height)
self.background_image = background_image
self.default_pixel_shape = resolution # Rename?
self.fps = fps
self.max_allowable_norm = max_allowable_norm
self.image_mode = image_mode

View file

@ -5,15 +5,13 @@ import colour
import importlib
import inspect
import os
import screeninfo
import sys
import yaml
from functools import lru_cache
from ast import literal_eval
from addict import Dict
from manimlib.logger import log
from manimlib.utils.dict_ops import merge_dicts_recursively
from manimlib.utils.init_config import init_customization
from typing import TYPE_CHECKING
if TYPE_CHECKING:
@ -21,6 +19,36 @@ if TYPE_CHECKING:
from typing import Optional
def initialize_manim_config() -> Dict:
"""
Return default configuration for various classes in manim, such as
Scene, Window, Camera, and SceneFileWriter, as well as configuration
determining how the scene is run (e.g. written to file or previewed in window).
The result is initially on the contents of default_config.yml in the manimlib directory,
which can be further updated by a custom configuration file custom_config.yml.
It is further updated based on command line argument.
"""
args = parse_cli()
global_defaults_file = os.path.join(get_manim_dir(), "manimlib", "default_config.yml")
config = merge_dicts_recursively(
load_yaml(global_defaults_file),
load_yaml("custom_config.yml"), # From current working directory
load_yaml(args.config_file) if args.config_file else dict(),
)
log.setLevel(args.log_level or config["log_level"])
update_directory_config(config)
update_window_config(config, args)
update_camera_config(config, args)
update_file_writer_config(config, args)
update_scene_config(config, args)
update_run_config(config, args)
return Dict(config)
def parse_cli():
try:
parser = argparse.ArgumentParser()
@ -48,12 +76,12 @@ def parse_cli():
parser.add_argument(
"-l", "--low_quality",
action="store_true",
help="Render at a low quality (for faster rendering)",
help="Render at 480p",
)
parser.add_argument(
"-m", "--medium_quality",
action="store_true",
help="Render at a medium quality",
help="Render at 720p",
)
parser.add_argument(
"--hd",
@ -119,11 +147,6 @@ def parse_cli():
action="store_true",
help="Show the output file in finder",
)
parser.add_argument(
"--config",
action="store_true",
help="Guide for automatic configuration",
)
parser.add_argument(
"--file_name",
help="Name for the movie or image file",
@ -137,8 +160,9 @@ def parse_cli():
)
parser.add_argument(
"-e", "--embed",
help="Creates a new file where the line `self.embed` is inserted " + \
"at the corresponding line number"
metavar="LINE_NUMBER",
help="Adds a breakpoint at the inputted file dropping into an " + \
"interactive iPython session at that point of the code."
)
parser.add_argument(
"-r", "--resolution",
@ -193,10 +217,101 @@ def parse_cli():
sys.exit(2)
def get_manim_dir():
manimlib_module = importlib.import_module("manimlib")
manimlib_dir = os.path.dirname(inspect.getabsfile(manimlib_module))
return os.path.abspath(os.path.join(manimlib_dir, ".."))
def update_directory_config(config: dict):
dir_config = config["directories"]
base = dir_config['base']
for key, subdir in dir_config['subdirs'].items():
dir_config[key] = os.path.join(base, subdir)
def update_window_config(config: dict, args: Namespace):
window_config = config["window"]
for key in "position", "size":
if window_config.get(key):
window_config[key] = literal_eval(window_config[key])
if args.full_screen:
window_config["full_screen"] = True
def update_camera_config(config: dict, args: Namespace):
camera_config = config["camera"]
arg_resolution = get_resolution_from_args(args, config["resolution_options"])
camera_config["resolution"] = arg_resolution or literal_eval(camera_config["resolution"])
if args.fps:
camera_config["fps"] = args.fps
if args.color:
try:
camera_config["background_color"] = colour.Color(args.color)
except Exception:
log.error("Please use a valid color")
log.error(err)
sys.exit(2)
if args.transparent:
camera_config["background_opacity"] = 0.0
def update_file_writer_config(config: dict, args: Namespace):
file_writer_config = config["file_writer"]
file_writer_config.update(
write_to_movie=(not args.skip_animations and args.write_file),
save_last_frame=(args.skip_animations and args.write_file),
save_pngs=args.save_pngs,
png_mode=("RGBA" if args.transparent else "RGB"),
movie_file_extension=(get_file_ext(args)),
output_directory=get_output_directory(args, config),
file_name=args.file_name,
input_file_path=args.file or "",
open_file_upon_completion=args.open,
show_file_location_upon_completion=args.finder,
quiet=args.quiet,
)
if args.vcodec:
file_writer_config["video_codec"] = args.vcodec
elif args.transparent:
file_writer_config["video_codec"] = 'prores_ks'
file_writer_config["pixel_format"] = ''
elif args.gif:
file_writer_config["video_codec"] = ''
if args.pix_fmt:
file_writer_config["pixel_format"] = args.pix_fmt
def update_scene_config(config: dict, args: Namespace):
scene_config = config["scene"]
start, end = get_animations_numbers(args)
scene_config.update(
# Note, Scene.__init__ makes use of both manimlib.camera and
# manimlib.file_writer below, so the arguments here are just for
# any future specifications beyond what the global configuration holds
camera_config=dict(),
file_writer_config=dict(),
skip_animations=args.skip_animations,
start_at_animation_number=start,
end_at_animation_number=end,
presenter_mode=args.presenter_mode,
)
if args.leave_progress_bars:
scene_config["leave_progress_bars"] = True
if args.show_animation_progress:
scene_config["show_animation_progress"] = True
def update_run_config(config: dict, args: Namespace):
config["run"] = dict(
file_name=args.file,
embed_line=(int(args.embed) if args.embed is not None else None),
is_reload=False,
prerun=args.prerun,
scene_names=args.scene_names,
quiet=args.quiet or args.write_all,
write_all=args.write_all,
show_in_window=not args.write_file
)
# Helpers for the functions above
def load_yaml(file_path: str):
@ -207,22 +322,24 @@ def load_yaml(file_path: str):
return {}
@lru_cache
def get_global_config():
args = parse_cli()
global_defaults_file = os.path.join(get_manim_dir(), "manimlib", "default_config.yml")
config = merge_dicts_recursively(
load_yaml(global_defaults_file),
load_yaml("custom_config.yml"), # From current working directory
load_yaml(args.config_file) if args.config_file else {},
)
def get_manim_dir():
manimlib_module = importlib.import_module("manimlib")
manimlib_dir = os.path.dirname(inspect.getabsfile(manimlib_module))
return os.path.abspath(os.path.join(manimlib_dir, ".."))
# Set the subdirectories
base = config['directories']['base']
for key, subdir in config['directories']['subdirs'].items():
config['directories'][key] = os.path.join(base, subdir)
return config
def get_resolution_from_args(args: Optional[Namespace], resolution_options: dict) -> Optional[tuple[int, int]]:
if args.resolution:
return tuple(map(int, args.resolution.split("x")))
if args.low_quality:
return literal_eval(resolution_options["low"])
if args.medium_quality:
return literal_eval(resolution_options["med"])
if args.hd:
return literal_eval(resolution_options["high"])
if args.uhd:
return literal_eval(resolution_options["4k"])
return None
def get_file_ext(args: Namespace) -> str:
@ -245,8 +362,8 @@ def get_animations_numbers(args: Namespace) -> tuple[int | None, int | None]:
return int(stan), None
def get_output_directory(args: Namespace, global_config: dict) -> str:
dir_config = global_config["directories"]
def get_output_directory(args: Namespace, config: dict) -> str:
dir_config = config["directories"]
output_directory = args.video_dir or dir_config["output"]
if dir_config["mirror_module_path"] and args.file:
to_cut = dir_config["removed_mirror_prefix"]
@ -258,142 +375,5 @@ def get_output_directory(args: Namespace, global_config: dict) -> str:
return output_directory
def get_file_writer_config(args: Namespace, global_config: dict) -> dict:
result = {
"write_to_movie": not args.skip_animations and args.write_file,
"save_last_frame": args.skip_animations and args.write_file,
"save_pngs": args.save_pngs,
# If -t is passed in (for transparent), this will be RGBA
"png_mode": "RGBA" if args.transparent else "RGB",
"movie_file_extension": get_file_ext(args),
"output_directory": get_output_directory(args, global_config),
"file_name": args.file_name,
"input_file_path": args.file or "",
"open_file_upon_completion": args.open,
"show_file_location_upon_completion": args.finder,
"quiet": args.quiet,
**global_config["file_writer_config"],
}
if args.vcodec:
result["video_codec"] = args.vcodec
elif args.transparent:
result["video_codec"] = 'prores_ks'
result["pixel_format"] = ''
elif args.gif:
result["video_codec"] = ''
if args.pix_fmt:
result["pixel_format"] = args.pix_fmt
return result
def get_resolution(args: Optional[Namespace] = None, global_config: Optional[dict] = None):
args = args or parse_cli()
global_config = global_config or get_global_config()
camera_resolutions = global_config["camera_resolutions"]
if args.resolution:
resolution = args.resolution
elif args.low_quality:
resolution = camera_resolutions["low"]
elif args.medium_quality:
resolution = camera_resolutions["med"]
elif args.hd:
resolution = camera_resolutions["high"]
elif args.uhd:
resolution = camera_resolutions["4k"]
else:
resolution = camera_resolutions[camera_resolutions["default_resolution"]]
width_str, height_str = resolution.split("x")
return int(width_str), int(height_str)
def get_window_config(args: Namespace, global_config: dict) -> dict:
# Default to making window half the screen size
# but make it full screen if -f is passed in
try:
monitors = screeninfo.get_monitors()
except screeninfo.ScreenInfoError:
# Default fallback
monitors = [screeninfo.Monitor(width=1920, height=1080)]
mon_index = global_config["window_monitor"]
monitor = monitors[min(mon_index, len(monitors) - 1)]
width, height = get_resolution(args, global_config)
aspect_ratio = width / height
window_width = monitor.width
if not (args.full_screen or global_config["full_screen"]):
window_width //= 2
window_height = int(window_width / aspect_ratio)
return dict(size=(window_width, window_height))
def get_camera_config(args: Optional[Namespace] = None, global_config: Optional[dict] = None) -> dict:
args = args or parse_cli()
global_config = global_config or get_global_config()
width, height = get_resolution(args, global_config)
fps = int(args.fps or global_config["fps"])
camera_config = {
"pixel_width": width,
"pixel_height": height,
"fps": fps,
}
try:
bg_color = args.color or global_config["style"]["background_color"]
camera_config["background_color"] = colour.Color(bg_color)
except ValueError as err:
log.error("Please use a valid color")
log.error(err)
sys.exit(2)
# If rendering a transparent image/movie, make sure the
# scene has a background opacity of 0
if args.transparent:
camera_config["background_opacity"] = 0
return camera_config
def get_scene_config(args: Namespace) -> dict:
"""
Returns a dictionary to be used as key word arguments for Scene
"""
global_config = get_global_config()
camera_config = get_camera_config(args, global_config)
file_writer_config = get_file_writer_config(args, global_config)
start, end = get_animations_numbers(args)
return {
"file_writer_config": file_writer_config,
"camera_config": camera_config,
"skip_animations": args.skip_animations,
"start_at_animation_number": start,
"end_at_animation_number": end,
"presenter_mode": args.presenter_mode,
"leave_progress_bars": args.leave_progress_bars,
"show_animation_progress": args.show_animation_progress,
"embed_exception_mode": global_config["embed_exception_mode"],
"embed_error_sound": global_config["embed_error_sound"],
}
def get_run_config(args: Namespace):
window_config = get_window_config(args, get_global_config())
return {
"file_name": args.file,
"embed_line": int(args.embed) if args.embed is not None else None,
"is_reload": False,
"prerun": args.prerun,
"scene_names": args.scene_names,
"quiet": args.quiet or args.write_all,
"write_all": args.write_all,
"window_config": window_config,
"show_in_window": not args.write_file
}
# Create global configuration
manim_config: Dict = initialize_manim_config()

View file

@ -1,44 +1,39 @@
from __future__ import annotations
import numpy as np
from manimlib.config import get_resolution
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import List
from manimlib.typing import ManimColor, Vect3
# See manimlib/default_config.yml
from manimlib.config import manim_config
# TODO, it feels a bit unprincipled to have some global constants
# depend on the output of this function, and for all that configuration
# code to be run merely upon importing from this file.
DEFAULT_RESOLUTION: tuple[int, int] = get_resolution()
DEFAULT_PIXEL_WIDTH = DEFAULT_RESOLUTION[0]
DEFAULT_PIXEL_HEIGHT = DEFAULT_RESOLUTION[1]
DEFAULT_FPS: int = 30
DEFAULT_RESOLUTION: tuple[int, int] = manim_config.camera.resolution
DEFAULT_PIXEL_WIDTH: int = DEFAULT_RESOLUTION[0]
DEFAULT_PIXEL_HEIGHT: int = DEFAULT_RESOLUTION[1]
# Sizes relevant to default camera frame
ASPECT_RATIO: float = DEFAULT_PIXEL_WIDTH / DEFAULT_PIXEL_HEIGHT
FRAME_HEIGHT: float = 8.0
FRAME_HEIGHT: float = manim_config.sizes.frame_height
FRAME_WIDTH: float = FRAME_HEIGHT * ASPECT_RATIO
FRAME_SHAPE: tuple[float, float] = (FRAME_WIDTH, FRAME_HEIGHT)
FRAME_Y_RADIUS: float = FRAME_HEIGHT / 2
FRAME_X_RADIUS: float = FRAME_WIDTH / 2
SMALL_BUFF: float = 0.1
MED_SMALL_BUFF: float = 0.25
MED_LARGE_BUFF: float = 0.5
LARGE_BUFF: float = 1
# Helpful values for positioning mobjects
SMALL_BUFF: float = manim_config.sizes.small_buff
MED_SMALL_BUFF: float = manim_config.sizes.med_small_buff
MED_LARGE_BUFF: float = manim_config.sizes.med_large_buff
LARGE_BUFF: float = manim_config.sizes.large_buff
DEFAULT_MOBJECT_TO_EDGE_BUFFER: float = MED_LARGE_BUFF
DEFAULT_MOBJECT_TO_MOBJECT_BUFFER: float = MED_SMALL_BUFF
# In seconds
DEFAULT_WAIT_TIME: float = 1.0
DEFAULT_MOBJECT_TO_EDGE_BUFF: float = manim_config.sizes.default_mobject_to_edge_buff
DEFAULT_MOBJECT_TO_MOBJECT_BUFF: float = manim_config.sizes.default_mobject_to_mobject_buff
# Standard vectors
ORIGIN: Vect3 = np.array([0., 0., 0.])
UP: Vect3 = np.array([0., 1., 0.])
DOWN: Vect3 = np.array([0., -1., 0.])
@ -63,6 +58,7 @@ BOTTOM: Vect3 = FRAME_Y_RADIUS * DOWN
LEFT_SIDE: Vect3 = FRAME_X_RADIUS * LEFT
RIGHT_SIDE: Vect3 = FRAME_X_RADIUS * RIGHT
# Angles
PI: float = np.pi
TAU: float = 2 * PI
DEGREES: float = TAU / 360
@ -70,100 +66,71 @@ DEGREES: float = TAU / 360
# when juxtaposed with expressions like 30 * DEGREES
RADIANS: float = 1
FFMPEG_BIN: str = "ffmpeg"
JOINT_TYPE_MAP: dict = {
"no_joint": 0,
"auto": 1,
"bevel": 2,
"miter": 3,
}
# Related to Text
NORMAL: str = "NORMAL"
ITALIC: str = "ITALIC"
OBLIQUE: str = "OBLIQUE"
BOLD: str = "BOLD"
DEFAULT_STROKE_WIDTH: float = 4
# For keyboard interactions
CTRL_SYMBOL: int = 65508
SHIFT_SYMBOL: int = 65505
COMMAND_SYMBOL: int = 65517
DELETE_SYMBOL: int = 65288
ARROW_SYMBOLS: list[int] = list(range(65361, 65365))
DEFAULT_STROKE_WIDTH: float = manim_config.vmobject.default_stroke_width
# Colors
BLUE_E: ManimColor = manim_config.colors.blue_e
BLUE_D: ManimColor = manim_config.colors.blue_d
BLUE_C: ManimColor = manim_config.colors.blue_c
BLUE_B: ManimColor = manim_config.colors.blue_b
BLUE_A: ManimColor = manim_config.colors.blue_a
TEAL_E: ManimColor = manim_config.colors.teal_e
TEAL_D: ManimColor = manim_config.colors.teal_d
TEAL_C: ManimColor = manim_config.colors.teal_c
TEAL_B: ManimColor = manim_config.colors.teal_b
TEAL_A: ManimColor = manim_config.colors.teal_a
GREEN_E: ManimColor = manim_config.colors.green_e
GREEN_D: ManimColor = manim_config.colors.green_d
GREEN_C: ManimColor = manim_config.colors.green_c
GREEN_B: ManimColor = manim_config.colors.green_b
GREEN_A: ManimColor = manim_config.colors.green_a
YELLOW_E: ManimColor = manim_config.colors.yellow_e
YELLOW_D: ManimColor = manim_config.colors.yellow_d
YELLOW_C: ManimColor = manim_config.colors.yellow_c
YELLOW_B: ManimColor = manim_config.colors.yellow_b
YELLOW_A: ManimColor = manim_config.colors.yellow_a
GOLD_E: ManimColor = manim_config.colors.gold_e
GOLD_D: ManimColor = manim_config.colors.gold_d
GOLD_C: ManimColor = manim_config.colors.gold_c
GOLD_B: ManimColor = manim_config.colors.gold_b
GOLD_A: ManimColor = manim_config.colors.gold_a
RED_E: ManimColor = manim_config.colors.red_e
RED_D: ManimColor = manim_config.colors.red_d
RED_C: ManimColor = manim_config.colors.red_c
RED_B: ManimColor = manim_config.colors.red_b
RED_A: ManimColor = manim_config.colors.red_a
MAROON_E: ManimColor = manim_config.colors.maroon_e
MAROON_D: ManimColor = manim_config.colors.maroon_d
MAROON_C: ManimColor = manim_config.colors.maroon_c
MAROON_B: ManimColor = manim_config.colors.maroon_b
MAROON_A: ManimColor = manim_config.colors.maroon_a
PURPLE_E: ManimColor = manim_config.colors.purple_e
PURPLE_D: ManimColor = manim_config.colors.purple_d
PURPLE_C: ManimColor = manim_config.colors.purple_c
PURPLE_B: ManimColor = manim_config.colors.purple_b
PURPLE_A: ManimColor = manim_config.colors.purple_a
GREY_E: ManimColor = manim_config.colors.grey_e
GREY_D: ManimColor = manim_config.colors.grey_d
GREY_C: ManimColor = manim_config.colors.grey_c
GREY_B: ManimColor = manim_config.colors.grey_b
GREY_A: ManimColor = manim_config.colors.grey_a
WHITE: ManimColor = manim_config.colors.white
BLACK: ManimColor = manim_config.colors.black
GREY_BROWN: ManimColor = manim_config.colors.grey_brown
DARK_BROWN: ManimColor = manim_config.colors.dark_brown
LIGHT_BROWN: ManimColor = manim_config.colors.light_brown
PINK: ManimColor = manim_config.colors.pink
LIGHT_PINK: ManimColor = manim_config.colors.light_pink
GREEN_SCREEN: ManimColor = manim_config.colors.green_screen
ORANGE: ManimColor = manim_config.colors.orange
BLUE_E: ManimColor = "#1C758A"
BLUE_D: ManimColor = "#29ABCA"
BLUE_C: ManimColor = "#58C4DD"
BLUE_B: ManimColor = "#9CDCEB"
BLUE_A: ManimColor = "#C7E9F1"
TEAL_E: ManimColor = "#49A88F"
TEAL_D: ManimColor = "#55C1A7"
TEAL_C: ManimColor = "#5CD0B3"
TEAL_B: ManimColor = "#76DDC0"
TEAL_A: ManimColor = "#ACEAD7"
GREEN_E: ManimColor = "#699C52"
GREEN_D: ManimColor = "#77B05D"
GREEN_C: ManimColor = "#83C167"
GREEN_B: ManimColor = "#A6CF8C"
GREEN_A: ManimColor = "#C9E2AE"
YELLOW_E: ManimColor = "#E8C11C"
YELLOW_D: ManimColor = "#F4D345"
YELLOW_C: ManimColor = "#FFFF00"
YELLOW_B: ManimColor = "#FFEA94"
YELLOW_A: ManimColor = "#FFF1B6"
GOLD_E: ManimColor = "#C78D46"
GOLD_D: ManimColor = "#E1A158"
GOLD_C: ManimColor = "#F0AC5F"
GOLD_B: ManimColor = "#F9B775"
GOLD_A: ManimColor = "#F7C797"
RED_E: ManimColor = "#CF5044"
RED_D: ManimColor = "#E65A4C"
RED_C: ManimColor = "#FC6255"
RED_B: ManimColor = "#FF8080"
RED_A: ManimColor = "#F7A1A3"
MAROON_E: ManimColor = "#94424F"
MAROON_D: ManimColor = "#A24D61"
MAROON_C: ManimColor = "#C55F73"
MAROON_B: ManimColor = "#EC92AB"
MAROON_A: ManimColor = "#ECABC1"
PURPLE_E: ManimColor = "#644172"
PURPLE_D: ManimColor = "#715582"
PURPLE_C: ManimColor = "#9A72AC"
PURPLE_B: ManimColor = "#B189C6"
PURPLE_A: ManimColor = "#CAA3E8"
GREY_E: ManimColor = "#222222"
GREY_D: ManimColor = "#444444"
GREY_C: ManimColor = "#888888"
GREY_B: ManimColor = "#BBBBBB"
GREY_A: ManimColor = "#DDDDDD"
WHITE: ManimColor = "#FFFFFF"
BLACK: ManimColor = "#000000"
GREY_BROWN: ManimColor = "#736357"
DARK_BROWN: ManimColor = "#8B4513"
LIGHT_BROWN: ManimColor = "#CD853F"
PINK: ManimColor = "#D147BD"
LIGHT_PINK: ManimColor = "#DC75CD"
GREEN_SCREEN: ManimColor = "#00FF00"
ORANGE: ManimColor = "#FF862F"
MANIM_COLORS: List[ManimColor] = [
BLACK, GREY_E, GREY_D, GREY_C, GREY_B, GREY_A, WHITE,
BLUE_E, BLUE_D, BLUE_C, BLUE_B, BLUE_A,
TEAL_E, TEAL_D, TEAL_C, TEAL_B, TEAL_A,
GREEN_E, GREEN_D, GREEN_C, GREEN_B, GREEN_A,
YELLOW_E, YELLOW_D, YELLOW_C, YELLOW_B, YELLOW_A,
GOLD_E, GOLD_D, GOLD_C, GOLD_B, GOLD_A,
RED_E, RED_D, RED_C, RED_B, RED_A,
MAROON_E, MAROON_D, MAROON_C, MAROON_B, MAROON_A,
PURPLE_E, PURPLE_D, PURPLE_C, PURPLE_B, PURPLE_A,
GREY_BROWN, DARK_BROWN, LIGHT_BROWN,
PINK, LIGHT_PINK,
]
MANIM_COLORS: List[ManimColor] = list(manim_config.colors.values())
# Abbreviated names for the "median" colors
BLUE: ManimColor = BLUE_C

View file

@ -1,3 +1,13 @@
# This file determines the default configuration for how manim is
# run, including names for directories it will write to, default
# parameters for various classes, style choices, etc. To customize
# your own, create a custom_config.yml file in whatever directory
# you are running manim. For 3blue1brown, for instance, mind is
# here: https://github.com/3b1b/videos/blob/master/custom_config.yml
# Alternatively, you can create it whereever you like, and on running
# manim, pass in `--config_file /path/to/custom/config/file.yml`
directories:
# Set this to true if you want the path to video files
# to match the directory structure of the path to the
@ -25,19 +35,23 @@ directories:
# it stores this saved data to whatever directory appdirs.user_cache_dir("manim") returns,
# but here a user can specify a different cache location
cache: ""
universal_import_line: "from manimlib import *"
style:
tex_template: "default"
font: "Consolas"
text_alignment: "LEFT"
window:
# The position of window on screen. UR -> Upper Right, and likewise DL -> Down and Left,
# UO would be upper middle, etc.
position_string: UR
# If using multiple monitors, which one should show the window
monitor_index: 0
# If not full screen, the default to give it half the screen width
full_screen: False
# Other optional specifications that override the above include:
# position: (500, 500) # Specific position, in pixel coordiantes, for upper right corner
# size: (1920, 1080) # Specific size, in pixels
camera:
resolution: (1920, 1080)
background_color: "#333333"
# Set the position of preview window, you can use directions, e.g. UL/DR/OL/OO/...
# also, you can also specify the position(pixel) of the upper left corner of
# the window on the monitor, e.g. "960,540"
window_position: UR
window_monitor: 0
full_screen: False
file_writer_config:
fps: 30
background_opacity: 1.0
file_writer:
# If break_into_partial_movies is set to True, then many small
# files will be written corresponding to each Scene.play and
# Scene.wait call, and these files will then be combined
@ -45,17 +59,106 @@ file_writer_config:
# easier when working with the broken up scene, which
# effectively has cuts at all the places you might want.
break_into_partial_movies: False
# What command to use for ffmpeg
ffmpeg_bin: "ffmpeg"
# Parameters to pass into ffmpeg
video_codec: "libx264"
pixel_format: "yuv420p"
saturation: 1.0
gamma: 1.0
camera_resolutions:
low: "854x480"
med: "1280x720"
high: "1920x1080"
4k: "3840x2160"
default_resolution: "high"
fps: 30
embed_exception_mode: "Verbose"
embed_error_sound: False
# Most of the scene configuration will come from CLI arguments,
# but defaults can be set here
scene:
show_animation_progress: False
leave_progress_bars: False
# How long does a scene pause on Scene.wait calls
default_wait_time: 1.0
vmobject:
default_stroke_width: 4.0
tex:
# See tex_templates.yml
template: "default"
text:
font: "Consolas"
alignment: "LEFT"
embed:
exception_mode: "Verbose"
resolution_options:
# When the user passes in -l, -m, --hd or --uhd, these are the corresponding
# resolutions
low: (854, 480)
med: (1280, 720)
high: (1920, 1080)
4k: (3840, 2160)
sizes:
# This determines the scale of the manim coordinate system with respect to
# the viewing frame
frame_height: 8.0
# These determine the constants SMALL_BUFF, MED_SMALL_BUFF, etc., useful
# for nudging things around and having default spacing values
small_buff: 0.1
med_small_buff: 0.25
med_large_buff: 0.5
large_buff: 1.0
# Default buffers used in Mobject.next_to or Mobject.to_edge
default_mobject_to_edge_buff: 0.5
default_mobject_to_mobject_buff: 0.25
colors:
blue_e: "#1C758A"
blue_d: "#29ABCA"
blue_c: "#58C4DD"
blue_b: "#9CDCEB"
blue_a: "#C7E9F1"
teal_e: "#49A88F"
teal_d: "#55C1A7"
teal_c: "#5CD0B3"
teal_b: "#76DDC0"
teal_a: "#ACEAD7"
green_e: "#699C52"
green_d: "#77B05D"
green_c: "#83C167"
green_b: "#A6CF8C"
green_a: "#C9E2AE"
yellow_e: "#E8C11C"
yellow_d: "#F4D345"
yellow_c: "#FFFF00"
yellow_b: "#FFEA94"
yellow_a: "#FFF1B6"
gold_e: "#C78D46"
gold_d: "#E1A158"
gold_c: "#F0AC5F"
gold_b: "#F9B775"
gold_a: "#F7C797"
red_e: "#CF5044"
red_d: "#E65A4C"
red_c: "#FC6255"
red_b: "#FF8080"
red_a: "#F7A1A3"
maroon_e: "#94424F"
maroon_d: "#A24D61"
maroon_c: "#C55F73"
maroon_b: "#EC92AB"
maroon_a: "#ECABC1"
purple_e: "#644172"
purple_d: "#715582"
purple_c: "#9A72AC"
purple_b: "#B189C6"
purple_a: "#CAA3E8"
grey_e: "#222222"
grey_d: "#444444"
grey_c: "#888888"
grey_b: "#BBBBBB"
grey_a: "#DDDDDD"
white: "#FFFFFF"
black: "#000000"
grey_brown: "#736357"
dark_brown: "#8B4513"
light_brown: "#CD853F"
pink: "#D147BD"
light_pink: "#DC75CD"
green_screen: "#00FF00"
orange: "#FF862F"
# Can be DEBUG / INFO / WARNING / ERROR / CRITICAL
log_level: "INFO"
universal_import_line: "from manimlib import *"
ignore_manimlib_modules_on_reload: True

View file

@ -6,7 +6,7 @@ import sys
from manimlib.module_loader import ModuleLoader
from manimlib.config import get_global_config
from manimlib.config import manim_config
from manimlib.logger import log
from manimlib.scene.interactive_scene import InteractiveScene
from manimlib.scene.scene import Scene
@ -19,7 +19,7 @@ if TYPE_CHECKING:
class BlankScene(InteractiveScene):
def construct(self):
exec(get_global_config()["universal_import_line"])
exec(manim_config.universal_import_line)
self.embed()
@ -77,13 +77,13 @@ def compute_total_frames(scene_class, scene_config):
pre_scene = scene_class(**pre_config)
pre_scene.run()
total_time = pre_scene.time - pre_scene.skip_time
return int(total_time * scene_config["camera_config"]["fps"])
return int(total_time * manim_config.camera.fps)
def scene_from_class(scene_class, scene_config, run_config):
fw_config = scene_config["file_writer_config"]
if fw_config["write_to_movie"] and run_config["prerun"]:
fw_config["total_frames"] = compute_total_frames(scene_class, scene_config)
fw_config = manim_config.file_writer
if fw_config.write_to_movie and run_config.prerun:
scene_config.file_writer_config.total_frames = compute_total_frames(scene_class, scene_config)
return scene_class(**scene_config)
@ -180,4 +180,7 @@ def main(scene_config, run_config):
return [BlankScene(**scene_config)]
all_scene_classes = get_scene_classes_from_module(module)
return get_scenes_to_render(all_scene_classes, scene_config, run_config)
scenes = get_scenes_to_render(all_scene_classes, scene_config, run_config)
if len(scenes) == 0:
print("No scenes found to run")
return scenes

View file

@ -11,4 +11,3 @@ logging.basicConfig(
)
log = logging.getLogger("manimgl")
log.setLevel("WARNING")

View file

@ -12,8 +12,8 @@ import moderngl
import numbers
import numpy as np
from manimlib.constants import DEFAULT_MOBJECT_TO_EDGE_BUFFER
from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFFER
from manimlib.constants import DEFAULT_MOBJECT_TO_EDGE_BUFF
from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFF
from manimlib.constants import DOWN, IN, LEFT, ORIGIN, OUT, RIGHT, UP
from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS
from manimlib.constants import MED_SMALL_BUFF
@ -1055,7 +1055,7 @@ class Mobject(object):
def align_on_border(
self,
direction: Vect3,
buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER
buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFF
) -> Self:
"""
Direction just needs to be a vector pointing towards side or
@ -1071,14 +1071,14 @@ class Mobject(object):
def to_corner(
self,
corner: Vect3 = LEFT + DOWN,
buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER
buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFF
) -> Self:
return self.align_on_border(corner, buff)
def to_edge(
self,
edge: Vect3 = LEFT,
buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER
buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFF
) -> Self:
return self.align_on_border(edge, buff)
@ -1086,7 +1086,7 @@ class Mobject(object):
self,
mobject_or_point: Mobject | Vect3,
direction: Vect3 = RIGHT,
buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFFER,
buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFF,
aligned_edge: Vect3 = ORIGIN,
submobject_to_align: Mobject | None = None,
index_of_submobject_to_align: int | slice | None = None,
@ -1117,7 +1117,7 @@ class Mobject(object):
space_lengths = [FRAME_X_RADIUS, FRAME_Y_RADIUS]
for vect in UP, DOWN, LEFT, RIGHT:
dim = np.argmax(np.abs(vect))
buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_EDGE_BUFFER)
buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_EDGE_BUFF)
max_val = space_lengths[dim] - buff
edge_center = self.get_edge_center(vect)
if np.dot(edge_center, vect) > max_val:

View file

@ -2,7 +2,7 @@ from __future__ import annotations
from colour import Color
from manimlib.config import get_global_config
from manimlib.config import manim_config
from manimlib.constants import BLACK, RED, YELLOW, WHITE
from manimlib.constants import DL, DOWN, DR, LEFT, RIGHT, UL, UR
from manimlib.constants import SMALL_BUFF
@ -57,7 +57,7 @@ class BackgroundRectangle(SurroundingRectangle):
**kwargs
):
if color is None:
color = get_global_config()['style']['background_color']
color = manim_config.camera.background_color
super().__init__(
mobject,
color=color,

View file

@ -5,7 +5,7 @@ import copy
import numpy as np
from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFFER, SMALL_BUFF
from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFF, SMALL_BUFF
from manimlib.constants import DOWN, LEFT, ORIGIN, RIGHT, DL, DR, UL
from manimlib.constants import PI
from manimlib.animation.composition import AnimationGroup
@ -79,7 +79,7 @@ class Brace(Tex):
)
else:
mob.move_to(self.get_tip())
buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_MOBJECT_BUFFER)
buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_MOBJECT_BUFF)
shift_distance = mob.get_width() / 2.0 + buff
mob.shift(self.get_direction() * shift_distance)
return self
@ -116,7 +116,7 @@ class BraceLabel(VMobject):
text: str | Iterable[str],
brace_direction: np.ndarray = DOWN,
label_scale: float = 1.0,
label_buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFFER,
label_buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFF,
**kwargs
) -> None:
super().__init__(**kwargs)

View file

@ -12,7 +12,7 @@ import pygments
import pygments.formatters
import pygments.lexers
from manimlib.config import get_global_config
from manimlib.config import manim_config
from manimlib.constants import DEFAULT_PIXEL_WIDTH, FRAME_WIDTH
from manimlib.constants import NORMAL
from manimlib.logger import log
@ -157,14 +157,14 @@ class MarkupText(StringMobject):
isolate: Selector = re.compile(r"\w+", re.U),
**kwargs
):
default_style = get_global_config()["style"]
text_config = manim_config.text
self.text = text
self.font_size = font_size
self.justify = justify
self.indent = indent
self.alignment = alignment or default_style["text_alignment"]
self.alignment = alignment or text_config.alignment
self.line_width = line_width
self.font = font or default_style["font"]
self.font = font or text_config.font
self.slant = slant
self.weight = weight

View file

@ -8,7 +8,6 @@ from manimlib.constants import GREY_A, GREY_C, GREY_E
from manimlib.constants import BLACK
from manimlib.constants import DEFAULT_STROKE_WIDTH
from manimlib.constants import DEGREES
from manimlib.constants import JOINT_TYPE_MAP
from manimlib.constants import ORIGIN, OUT
from manimlib.constants import PI
from manimlib.constants import TAU
@ -72,6 +71,12 @@ class VMobject(Mobject):
make_smooth_after_applying_functions: bool = False
# TODO, do we care about accounting for varying zoom levels?
tolerance_for_point_equality: float = 1e-8
joint_type_map: dict = {
"no_joint": 0,
"auto": 1,
"bevel": 2,
"miter": 3,
}
def __init__(
self,
@ -123,7 +128,7 @@ class VMobject(Mobject):
super().init_uniforms()
self.uniforms.update(
anti_alias_width=self.anti_alias_width,
joint_type=JOINT_TYPE_MAP[self.joint_type],
joint_type=self.joint_type_map[self.joint_type],
flat_stroke=float(self.flat_stroke),
scale_stroke_with_zoom=float(self.scale_stroke_with_zoom)
)
@ -406,7 +411,7 @@ class VMobject(Mobject):
def set_joint_type(self, joint_type: str, recurse: bool = True) -> Self:
for mob in self.get_family(recurse):
mob.uniforms["joint_type"] = JOINT_TYPE_MAP[joint_type]
mob.uniforms["joint_type"] = self.joint_type_map[joint_type]
return self
def get_joint_type(self) -> float:

View file

@ -6,7 +6,7 @@ import os
import sys
import sysconfig
from manimlib.config import get_global_config
from manimlib.config import manim_config
from manimlib.logger import log
Module = importlib.util.types.ModuleType
@ -142,9 +142,12 @@ class ModuleLoader:
Only user-defined modules are reloaded, see `is_user_defined_module()`.
"""
ignore_manimlib_modules = get_global_config()["ignore_manimlib_modules_on_reload"]
ignore_manimlib_modules = manim_config.ignore_manimlib_modules_on_reload
if ignore_manimlib_modules and module.__name__.startswith("manimlib"):
return
if module.__name__.startswith("manimlib.config"):
# We don't want to reload global config
return
if not hasattr(module, "__dict__"):
return

View file

@ -1,86 +0,0 @@
from __future__ import annotations
from typing import Any
from IPython.terminal.embed import KillEmbedded
import manimlib.config
import manimlib.extract_scene
from manimlib.window import Window
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from argparse import Namespace
class ReloadManager:
"""
Manages the loading and running of scenes and is called directly from the
main entry point of ManimGL.
The name "reload" comes from the fact that this class handles the
reinitialization of scenes when requested by the user via the `reload()`
command in the IPython shell.
"""
window = None
is_reload = False
def __init__(self, cli_args: Namespace):
self.args = cli_args
def set_new_start_at_line(self, start_at_line):
"""
Sets/Updates the line number to load the scene from when reloading.
"""
self.args.embed = str(start_at_line)
def run(self):
"""
Runs the scenes in a loop and detects when a scene reload is requested.
"""
while True:
try:
# blocking call since a scene will init an IPython shell()
self.retrieve_scenes_and_run()
return
except KillEmbedded:
# Requested via the `exit_raise` IPython runline magic
# by means of our scene.reload() command
self.note_reload()
except KeyboardInterrupt:
break
def note_reload(self):
self.is_reload = True
print(" ".join([
"Reloading interactive session for",
f"\033[96m{self.args.scene_names[0]}\033[0m",
f"at line \033[96m{self.args.embed}\033[0m"
]))
def retrieve_scenes_and_run(self):
"""
Creates a new configuration based on the CLI args and runs the scenes.
"""
# Args to Config
scene_config = manimlib.config.get_scene_config(self.args)
scene_config.update(reload_manager=self)
run_config = manimlib.config.get_run_config(self.args)
run_config.update(is_reload=self.is_reload)
# Create or reuse window
if run_config["show_in_window"] and not self.window:
self.window = Window(**run_config["window_config"])
scene_config.update(window=self.window)
# Scenes
scenes = manimlib.extract_scene.main(scene_config, run_config)
if len(scenes) == 0:
print("No scenes found to run")
for scene in scenes:
scene.run()

View file

@ -7,7 +7,6 @@ from IPython.core.getipython import get_ipython
from pyglet.window import key as PygletWindowKeys
from manimlib.animation.fading import FadeIn
from manimlib.constants import ARROW_SYMBOLS, CTRL_SYMBOL, DELETE_SYMBOL, SHIFT_SYMBOL
from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, RIGHT, UL, UP, UR
from manimlib.constants import FRAME_WIDTH, FRAME_HEIGHT, SMALL_BUFF
from manimlib.constants import PI
@ -49,6 +48,15 @@ INFORMATION_KEY = 'i'
CURSOR_KEY = 'k'
COPY_FRAME_POSITION_KEY = 'p'
# For keyboard interactions
ARROW_SYMBOLS: list[int] = [
PygletWindowKeys.LEFT,
PygletWindowKeys.UP,
PygletWindowKeys.RIGHT,
PygletWindowKeys.DOWN,
]
ALL_MODIFIERS = PygletWindowKeys.MOD_CTRL | PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_SHIFT
# Note, a lot of the functionality here is still buggy and very much a work in progress.
@ -472,7 +480,7 @@ class InteractiveScene(Scene):
self.prepare_grab()
elif char == RESIZE_KEY and (modifiers & PygletWindowKeys.MOD_SHIFT):
self.prepare_resizing(about_corner=((modifiers & PygletWindowKeys.MOD_SHIFT) > 0))
elif symbol == SHIFT_SYMBOL:
elif symbol == PygletWindowKeys.LSHIFT:
if self.window.is_key_pressed(ord("t")):
self.prepare_resizing(about_corner=True)
elif char == COLOR_KEY and (modifiers & ALL_MODIFIERS) == 0:
@ -486,7 +494,7 @@ class InteractiveScene(Scene):
elif char == "x" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
self.copy_selection()
self.delete_selection()
elif symbol == DELETE_SYMBOL:
elif symbol == PygletWindowKeys.BACKSPACE:
self.delete_selection()
elif char == "a" and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
self.clear_selection()
@ -527,7 +535,7 @@ class InteractiveScene(Scene):
self.is_grabbing = False
elif chr(symbol) == INFORMATION_KEY:
self.display_information(False)
elif symbol == SHIFT_SYMBOL and self.window.is_key_pressed(ord(RESIZE_KEY)):
elif symbol == PygletWindowKeys.LSHIFT and self.window.is_key_pressed(ord(RESIZE_KEY)):
self.prepare_resizing(about_corner=False)
# Mouse actions
@ -544,7 +552,7 @@ class InteractiveScene(Scene):
if not hasattr(self, "scale_about_point"):
return
vect = point - self.scale_about_point
if self.window.is_key_pressed(CTRL_SYMBOL):
if self.window.is_key_pressed(PygletWindowKeys.LCTRL):
for i in (0, 1):
scalar = vect[i] / self.scale_ref_vect[i]
self.selection.rescale_to_fit(
@ -589,7 +597,7 @@ class InteractiveScene(Scene):
self.handle_grabbing(point)
elif self.window.is_key_pressed(ord(RESIZE_KEY)):
self.handle_resizing(point)
elif self.window.is_key_pressed(ord(SELECT_KEY)) and self.window.is_key_pressed(SHIFT_SYMBOL):
elif self.window.is_key_pressed(ord(SELECT_KEY)) and self.window.is_key_pressed(PygletWindowKeys.LSHIFT):
self.handle_sweeping_selection(point)
def on_mouse_drag(

View file

@ -6,7 +6,6 @@ import random
import time
from functools import wraps
from IPython.core.getipython import get_ipython
from pyglet.window import key as PygletWindowKeys
import numpy as np
@ -15,8 +14,7 @@ from tqdm.auto import tqdm as ProgressDisplay
from manimlib.animation.animation import prepare_animation
from manimlib.camera.camera import Camera
from manimlib.camera.camera_frame import CameraFrame
from manimlib.constants import ARROW_SYMBOLS
from manimlib.constants import DEFAULT_WAIT_TIME
from manimlib.config import manim_config
from manimlib.event_handler import EVENT_DISPATCHER
from manimlib.event_handler.event_type import EventType
from manimlib.logger import log
@ -29,6 +27,7 @@ from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.scene.scene_embed import interactive_scene_embed
from manimlib.scene.scene_embed import CheckpointManager
from manimlib.scene.scene_file_writer import SceneFileWriter
from manimlib.utils.dict_ops import merge_dicts_recursively
from manimlib.utils.family_ops import extract_mobject_family_members
from manimlib.utils.family_ops import recursive_mobject_remove
from manimlib.utils.iterables import batch_by_property
@ -44,7 +43,6 @@ if TYPE_CHECKING:
from PIL.Image import Image
from manimlib.reload_manager import ReloadManager
from manimlib.animation.animation import Animation
@ -68,33 +66,37 @@ class Scene(object):
def __init__(
self,
window: Optional[Window] = None,
camera_config: dict = dict(),
file_writer_config: dict = dict(),
skip_animations: bool = False,
always_update_mobjects: bool = False,
start_at_animation_number: int | None = None,
end_at_animation_number: int | None = None,
leave_progress_bars: bool = False,
window: Optional[Window] = None,
reload_manager: Optional[ReloadManager] = None,
presenter_mode: bool = False,
show_animation_progress: bool = False,
embed_exception_mode: str = "",
embed_error_sound: bool = False,
leave_progress_bars: bool = False,
presenter_mode: bool = False,
default_wait_time: float = 1.0,
):
self.skip_animations = skip_animations
self.always_update_mobjects = always_update_mobjects
self.start_at_animation_number = start_at_animation_number
self.end_at_animation_number = end_at_animation_number
self.show_animation_progress = show_animation_progress
self.leave_progress_bars = leave_progress_bars
self.presenter_mode = presenter_mode
self.show_animation_progress = show_animation_progress
self.embed_exception_mode = embed_exception_mode
self.embed_error_sound = embed_error_sound
self.reload_manager = reload_manager
self.default_wait_time = default_wait_time
self.camera_config = {**self.default_camera_config, **camera_config}
self.file_writer_config = {**self.default_file_writer_config, **file_writer_config}
self.camera_config = merge_dicts_recursively(
manim_config.camera, # Global default
self.default_camera_config, # Updated configuration that subclasses may specify
camera_config, # Updated configuration from instantiation
)
self.file_writer_config = merge_dicts_recursively(
manim_config.file_writer,
self.default_file_writer_config,
file_writer_config,
)
self.window = window
if self.window:
@ -589,11 +591,13 @@ class Scene(object):
def wait(
self,
duration: float = DEFAULT_WAIT_TIME,
duration: Optional[float] = None,
stop_condition: Callable[[], bool] = None,
note: str = None,
ignore_presenter_mode: bool = False
):
if duration is None:
duration = self.default_wait_time
self.pre_play()
self.update_mobjects(dt=0) # Any problems with this?
if self.presenter_mode and not self.skip_animations and not ignore_presenter_mode:
@ -839,7 +843,7 @@ class Scene(object):
elif char == QUIT_KEY and (modifiers & (PygletWindowKeys.MOD_COMMAND | PygletWindowKeys.MOD_CTRL)):
self.quit_interaction = True
# Space or right arrow
elif char == " " or symbol == ARROW_SYMBOLS[2]:
elif char == " " or symbol == PygletWindowKeys.RIGHT:
self.hold_on_wait = False
def on_resize(self, width: int, height: int) -> None:
@ -854,34 +858,6 @@ class Scene(object):
def on_close(self) -> None:
pass
def reload(self, start_at_line: int | None = None) -> None:
"""
Reloads the scene just like the `manimgl` command would do with the
same arguments that were provided for the initial startup. This allows
for quick iteration during scene development since we don't have to exit
the IPython kernel and re-run the `manimgl` command again. The GUI stays
open during the reload.
If `start_at_line` is provided, the scene will be reloaded at that line
number. This corresponds to the `linemarker` param of the
`extract_scene.insert_embed_line_to_module()` method.
Before reload, the scene is cleared and the entire state is reset, such
that we can start from a clean slate. This is taken care of by the
ReloadManager, which will catch the error raised by the `exit_raise`
magic command that we invoke here.
Note that we cannot define a custom exception class for this error,
since the IPython kernel will swallow any exception. While we can catch
such an exception in our custom exception handler registered with the
`set_custom_exc` method, we cannot break out of the IPython shell by
this means.
"""
self.reload_manager.set_new_start_at_line(start_at_line)
shell = get_ipython()
if shell:
shell.run_line_magic("exit_raise", "")
def focus(self) -> None:
"""
Puts focus on the ManimGL window.

View file

@ -1,12 +1,12 @@
import inspect
import pyperclip
import os
from IPython.core.getipython import get_ipython
from IPython.terminal import pt_inputhooks
from IPython.terminal.embed import InteractiveShellEmbed
from manimlib.animation.fading import VFadeInThenOut
from manimlib.config import manim_config
from manimlib.constants import RED
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.frame import FullScreenRectangle
@ -39,11 +39,12 @@ def get_ipython_shell_for_embedded_scene(scene):
module = ModuleLoader.get_module(caller_frame.f_globals["__file__"])
module.__dict__.update(caller_frame.f_locals)
module.__dict__.update(get_shortcuts(scene))
exception_mode = manim_config.embed.exception_mode
return InteractiveShellEmbed(
user_module=module,
display_banner=False,
xmode=scene.embed_exception_mode
xmode=exception_mode
)
@ -59,12 +60,12 @@ def get_shortcuts(scene):
clear=scene.clear,
focus=scene.focus,
save_state=scene.save_state,
reload=scene.reload,
undo=scene.undo,
redo=scene.redo,
i2g=scene.i2g,
i2m=scene.i2m,
checkpoint_paste=scene.checkpoint_paste,
reload=reload_scene # Defined below
)
@ -95,8 +96,6 @@ def ensure_flash_on_error(shell, scene):
def custom_exc(shell, etype, evalue, tb, tb_offset=None):
# Show the error don't just swallow it
shell.showtraceback((etype, evalue, tb), tb_offset=tb_offset)
if scene.embed_error_sound:
os.system("printf '\a'")
rect = FullScreenRectangle().set_stroke(RED, 30).set_fill(opacity=0)
rect.fix_in_frame()
scene.play(VFadeInThenOut(rect, run_time=0.5))
@ -104,6 +103,43 @@ def ensure_flash_on_error(shell, scene):
shell.set_custom_exc((Exception,), custom_exc)
def reload_scene(embed_line: int | None = None) -> None:
"""
Reloads the scene just like the `manimgl` command would do with the
same arguments that were provided for the initial startup. This allows
for quick iteration during scene development since we don't have to exit
the IPython kernel and re-run the `manimgl` command again. The GUI stays
open during the reload.
If `embed_line` is provided, the scene will be reloaded at that line
number. This corresponds to the `linemarker` param of the
`extract_scene.insert_embed_line_to_module()` method.
Before reload, the scene is cleared and the entire state is reset, such
that we can start from a clean slate. This is taken care of by the
run_scenes function in __main__.py, which will catch the error raised by the
`exit_raise` magic command that we invoke here.
Note that we cannot define a custom exception class for this error,
since the IPython kernel will swallow any exception. While we can catch
such an exception in our custom exception handler registered with the
`set_custom_exc` method, we cannot break out of the IPython shell by
this means.
"""
shell = get_ipython()
if not shell:
return
# Update the global run configuration.
run_config = manim_config.run
run_config.is_reload = True
if embed_line:
run_config.embed_line = embed_line
print("Reloading...")
shell.run_line_magic("exit_raise", "")
class CheckpointManager:
checkpoint_states: dict[str, list[tuple[Mobject, Mobject]]] = dict()

View file

@ -11,7 +11,6 @@ from pydub import AudioSegment
from tqdm.auto import tqdm as ProgressDisplay
from pathlib import Path
from manimlib.constants import FFMPEG_BIN
from manimlib.logger import log
from manimlib.mobject.mobject import Mobject
from manimlib.utils.file_ops import add_extension_if_not_present
@ -49,6 +48,8 @@ class SceneFileWriter(object):
quiet: bool = False,
total_frames: int = 0,
progress_description_len: int = 40,
# Name of the binary used for ffmpeg
ffmpeg_bin: str = "ffmpeg",
video_codec: str = "libx264",
pixel_format: str = "yuv420p",
saturation: float = 1.0,
@ -70,6 +71,7 @@ class SceneFileWriter(object):
self.quiet = quiet
self.total_frames = total_frames
self.progress_description_len = progress_description_len
self.ffmpeg_bin = ffmpeg_bin
self.video_codec = video_codec
self.pixel_format = pixel_format
self.saturation = saturation
@ -236,7 +238,7 @@ class SceneFileWriter(object):
vf_arg += f',eq=saturation={self.saturation}:gamma={self.gamma}'
command = [
FFMPEG_BIN,
self.ffmpeg_bin,
'-y', # overwrite output file if it exists
'-f', 'rawvideo',
'-s', f'{width}x{height}', # size of one frame
@ -358,7 +360,7 @@ class SceneFileWriter(object):
movie_file_path = self.get_movie_file_path()
commands = [
FFMPEG_BIN,
self.ffmpeg_bin,
'-y', # overwrite output file if it exists
'-f', 'concat',
'-safe', '0',
@ -385,7 +387,7 @@ class SceneFileWriter(object):
)
temp_file_path = stem + "_temp" + ext
commands = [
FFMPEG_BIN,
self.ffmpeg_bin,
"-i", movie_file_path,
"-i", sound_file_path,
'-y', # overwrite output file if it exists

View file

@ -10,7 +10,7 @@ import numpy as np
from functools import lru_cache
from manimlib.config import parse_cli
from manimlib.config import get_camera_config
from manimlib.config import manim_config
from manimlib.utils.shaders import get_shader_code_from_file
from manimlib.utils.shaders import get_shader_program
from manimlib.utils.shaders import image_path_to_texture
@ -410,8 +410,7 @@ class VShaderWrapper(ShaderWrapper):
which can display that texture as a simple quad onto a screen,
along with the rgb value which is meant to be discarded.
"""
cam_config = get_camera_config()
size = (cam_config['pixel_width'], cam_config['pixel_height'])
size = manim_config.camera.resolution
double_size = (2 * size[0], 2 * size[1])
# Important to make sure dtype is floating point (not fixed point)

View file

@ -5,13 +5,13 @@ import tempfile
import appdirs
from manimlib.config import get_global_config
from manimlib.config import manim_config
from manimlib.config import get_manim_dir
from manimlib.utils.file_ops import guarantee_existence
def get_directories() -> dict[str, str]:
return get_global_config()["directories"]
return manim_config.directories
def get_cache_dir() -> str:

View file

@ -1,164 +0,0 @@
from __future__ import annotations
import importlib
import inspect
import os
import yaml
from rich import box
from rich.console import Console
from rich.prompt import Confirm
from rich.prompt import Prompt
from rich.rule import Rule
from rich.table import Table
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Any
def get_manim_dir() -> str:
manimlib_module = importlib.import_module("manimlib")
manimlib_dir = os.path.dirname(inspect.getabsfile(manimlib_module))
return os.path.abspath(os.path.join(manimlib_dir, ".."))
def remove_empty_value(dictionary: dict[str, Any]) -> None:
for key in list(dictionary.keys()):
if dictionary[key] == "":
dictionary.pop(key)
elif isinstance(dictionary[key], dict):
remove_empty_value(dictionary[key])
def init_customization() -> None:
configuration = {
"directories": {
"mirror_module_path": False,
"base": "",
"subdirs": {
"output": "videos",
"raster_images": "raster_images",
"vector_images": "vector_images",
"sounds": "sounds",
"data": "data",
"downloads": "downloads",
}
},
"universal_import_line": "from manimlib import *",
"style": {
"tex_template": "",
"font": "Consolas",
"background_color": "",
},
"window_position": "UR",
"window_monitor": 0,
"full_screen": False,
"break_into_partial_movies": False,
"camera_resolutions": {
"low": "854x480",
"medium": "1280x720",
"high": "1920x1080",
"4k": "3840x2160",
"default_resolution": "",
},
"fps": 30,
}
console = Console()
console.print(Rule("[bold]Configuration Guide[/bold]"))
# print("Initialize configuration")
try:
scope = Prompt.ask(
" Select the scope of the configuration",
choices=["global", "local"],
default="local"
)
console.print("[bold]Directories:[/bold]")
dir_config = configuration["directories"]
dir_config["base"] = Prompt.ask(
" What base directory should manim use for reading/writing video and images? [prompt.default](optional, default is none)",
default="",
show_default=False
)
dir_config["subdirs"]["output"] = Prompt.ask(
" Within that base directory, which subdirectory should manim [bold]output[/bold] video and image files to?" + \
" [prompt.default](optional, default is \"videos\")",
default="videos",
show_default=False
)
dir_config["subdirs"]["raster_images"] = Prompt.ask(
" Within that base directory, which subdirectory should manim look for raster images (.png, .jpg)" + \
" [prompt.default](optional, default is \"raster_images\")",
default="raster_images",
show_default=False
)
dir_config["subdirs"]["vector_images"] = Prompt.ask(
" Within that base directory, which subdirectory should manim look for raster images (.svg, .xdv)" + \
" [prompt.default](optional, default is \"vector_images\")",
default="vector_images",
show_default=False
)
dir_config["subdirs"]["sounds"] = Prompt.ask(
" Within that base directory, which subdirectory should manim look for sound files (.mp3, .wav)" + \
" [prompt.default](optional, default is \"sounds\")",
default="sounds",
show_default=False
)
dir_config["subdirs"]["downloads"] = Prompt.ask(
" Within that base directory, which subdirectory should manim output downloaded files" + \
" [prompt.default](optional, default is \"downloads\")",
default="downloads",
show_default=False
)
console.print("[bold]Styles:[/bold]")
style_config = configuration["style"]
tex_template = Prompt.ask(
" Select a TeX template to compile a LaTeX source file",
default="default"
)
style_config["tex_template"] = tex_template
style_config["background_color"] = Prompt.ask(
" Which [bold]background color[/bold] do you want [italic](hex code)",
default="#333333"
)
console.print("[bold]Camera qualities:[/bold]")
table = Table(
"low", "medium", "high", "ultra_high",
title="Four defined qualities",
box=box.ROUNDED
)
table.add_row("480p15", "720p30", "1080p60", "2160p60")
console.print(table)
configuration["camera_resolutions"]["default_resolution"] = Prompt.ask(
" Which one to choose as the default rendering quality",
choices=["low", "medium", "high", "ultra_high"],
default="high"
)
write_to_file = Confirm.ask(
"\n[bold]Are you sure to write these configs to file?[/bold]",
default=True
)
if not write_to_file:
raise KeyboardInterrupt
global_file_name = os.path.join(get_manim_dir(), "manimlib", "default_config.yml")
if scope == "global":
file_name = global_file_name
else:
if os.path.exists(global_file_name):
remove_empty_value(configuration)
file_name = os.path.join(os.getcwd(), "custom_config.yml")
with open(file_name, "w", encoding="utf-8") as f:
yaml.dump(configuration, f)
console.print(f"\n:rocket: You have successfully set up a {scope} configuration file!")
console.print(f"You can manually modify it in: [cyan]`{file_name}`[/cyan]")
except KeyboardInterrupt:
console.print("\n[green]Exit configuration guide[/green]")

View file

@ -10,15 +10,12 @@ from pathlib import Path
import tempfile
from manimlib.utils.cache import cache_on_disk
from manimlib.config import get_global_config
from manimlib.config import manim_config
from manimlib.config import get_manim_dir
from manimlib.logger import log
from manimlib.utils.simple_functions import hash_string
SAVED_TEX_CONFIG = {}
def get_tex_template_config(template_name: str) -> dict[str, str]:
name = template_name.replace(" ", "_").lower()
template_path = os.path.join(get_manim_dir(), "manimlib", "tex_templates.yml")
@ -33,7 +30,8 @@ def get_tex_template_config(template_name: str) -> dict[str, str]:
return templates_dict[name]
def get_tex_config() -> dict[str, str]:
@lru_cache
def get_tex_config(template: str = "") -> dict[str, str]:
"""
Returns a dict which should look something like this:
{
@ -42,16 +40,13 @@ def get_tex_config() -> dict[str, str]:
"preamble": "..."
}
"""
# Only load once, then save thereafter
if not SAVED_TEX_CONFIG:
template_name = get_global_config()["style"]["tex_template"]
template_config = get_tex_template_config(template_name)
SAVED_TEX_CONFIG.update({
"template": template_name,
"compiler": template_config["compiler"],
"preamble": template_config["preamble"]
})
return SAVED_TEX_CONFIG
template = template or manim_config.tex.template
template_config = get_tex_template_config(template)
return {
"template": template,
"compiler": template_config["compiler"],
"preamble": template_config["preamble"]
}
def get_full_tex(content: str, preamble: str = ""):
@ -94,17 +89,12 @@ def latex_to_svg(
message = message[:max_message_len - 3] + "..."
print(message, end="\r")
tex_config = get_tex_config()
if template and template != tex_config["template"]:
tex_config = get_tex_template_config(template)
tex_config = get_tex_config(template)
compiler = tex_config["compiler"]
if compiler == "latex":
program = "latex"
dvi_ext = ".dvi"
elif compiler == "xelatex":
program = "xelatex -no-pdf"
dvi_ext = ".xdv"
else:
raise NotImplementedError(f"Compiler '{compiler}' is not implemented")
@ -119,18 +109,18 @@ def latex_to_svg(
dvi_path = base_path + dvi_ext
# Write tex file
with open(tex_path, "w", encoding="utf-8") as tex_file:
tex_file.write(full_tex)
Path(tex_path).write_text(full_tex)
# Run latex compiler
process = subprocess.run(
[
program.split()[0], # Split for xelatex case
compiler,
"-no-pdf",
"-interaction=batchmode",
"-halt-on-error",
"-output-directory=" + temp_dir,
f"-output-directory={temp_dir}",
tex_path
] + (["--no-pdf"] if compiler == "xelatex" else []),
],
capture_output=True,
text=True
)

View file

@ -5,10 +5,10 @@ import numpy as np
import moderngl_window as mglw
from moderngl_window.context.pyglet.window import Window as PygletWindow
from moderngl_window.timers.clock import Timer
from screeninfo import get_monitors
from functools import wraps
import screeninfo
from manimlib.config import get_global_config
from manimlib.constants import ASPECT_RATIO
from manimlib.constants import FRAME_SHAPE
from typing import TYPE_CHECKING
@ -30,16 +30,21 @@ class Window(PygletWindow):
def __init__(
self,
scene: Optional[Scene] = None,
size: tuple[int, int] = (1280, 720),
position_string: str = "UR",
monitor_index: int = 1,
full_screen: bool = False,
size: Optional[tuple[int, int]] = None,
position: Optional[tuple[int, int]] = None,
samples: int = 0
):
super().__init__(size=size, samples=samples)
self.scene = scene
self.default_size = size
self.default_position = self.find_initial_position(size)
self.monitor = self.get_monitor(monitor_index)
self.default_size = size or self.get_default_size(full_screen)
self.default_position = position or self.position_from_string(position_string)
self.pressed_keys = set()
self.size = size
super().__init__(samples=samples)
self.to_default_position()
if self.scene:
self.init_for_scene(scene)
@ -64,7 +69,31 @@ class Window(PygletWindow):
mglw.activate_context(window=self, ctx=self.ctx)
self.timer.start()
self.to_default_position()
self.focus()
def get_monitor(self, index):
try:
monitors = screeninfo.get_monitors()
return monitors[min(index, len(monitors) - 1)]
except screeninfo.ScreenInfoError:
# Default fallback
return screeninfo.Monitor(width=1920, height=1080)
def get_default_size(self, full_screen=False):
width = self.monitor.width // (1 if full_screen else 2)
height = int(width // ASPECT_RATIO)
return (width, height)
def position_from_string(self, position_string):
# Alternatively, it might be specified with a string like
# UR, OO, DL, etc. specifying what corner it should go to
char_to_n = {"L": 0, "U": 0, "O": 1, "R": 2, "D": 2}
size = self.default_size
width_diff = self.monitor.width - size[0]
height_diff = self.monitor.height - size[1]
x_step = char_to_n[position_string[1]] * width_diff // 2
y_step = char_to_n[position_string[0]] * height_diff // 2
return (self.monitor.x + x_step, -self.monitor.y + y_step)
def focus(self):
"""
@ -77,6 +106,8 @@ class Window(PygletWindow):
"""
self._window.set_visible(False)
self._window.set_visible(True)
# This line seems to resync the viewport
self.on_resize(*self.size)
def to_default_position(self):
self.position = self.default_position
@ -86,28 +117,6 @@ class Window(PygletWindow):
self.size = (w - 1, h - 1)
self.size = (w, h)
def find_initial_position(self, size: tuple[int, int]) -> tuple[int, int]:
global_config = get_global_config()
custom_position = global_config["window_position"]
mon_index = global_config["window_monitor"]
monitors = get_monitors()
monitor = monitors[min(mon_index, len(monitors) - 1)]
window_width, window_height = size
# Position might be specified with a string of the form
# x,y for integers x and y
if "," in custom_position:
return tuple(map(int, custom_position.split(",")))
# Alternatively, it might be specified with a string like
# UR, OO, DL, etc. specifying what corner it should go to
char_to_n = {"L": 0, "U": 0, "O": 1, "R": 2, "D": 2}
width_diff = monitor.width - window_width
height_diff = monitor.height - window_height
return (
monitor.x + char_to_n[custom_position[1]] * width_diff // 2,
-monitor.y + char_to_n[custom_position[0]] * height_diff // 2,
)
# Delegate event handling to scene
def pixel_coords_to_space_coords(
self,

View file

@ -1,3 +1,4 @@
addict
appdirs
colour
diskcache
@ -22,7 +23,6 @@ screeninfo
skia-pathops
svgelements>=1.8.1
sympy
tempfile
tqdm
typing-extensions; python_version < "3.11"
validators

View file

@ -29,6 +29,7 @@ classifiers =
packages = find:
include_package_data = True
install_requires =
addict
appdirs
colour
diskcache
@ -53,7 +54,6 @@ install_requires =
skia-pathops
svgelements>=1.8.1
sympy
tempfile
tqdm
typing-extensions; python_version < "3.11"
validators