mirror of
https://github.com/3b1b/manim.git
synced 2025-08-05 16:49:03 +00:00
commit
07a8274cb1
25 changed files with 535 additions and 366 deletions
|
@ -18,7 +18,7 @@ Frame and pixel shape
|
||||||
|
|
||||||
DEFAULT_PIXEL_HEIGHT = 1080
|
DEFAULT_PIXEL_HEIGHT = 1080
|
||||||
DEFAULT_PIXEL_WIDTH = 1920
|
DEFAULT_PIXEL_WIDTH = 1920
|
||||||
DEFAULT_FRAME_RATE = 30
|
DEFAULT_FPS = 30
|
||||||
|
|
||||||
Buffs
|
Buffs
|
||||||
-----
|
-----
|
||||||
|
|
|
@ -56,7 +56,7 @@ flag abbr function
|
||||||
``--start_at_animation_number START_AT_ANIMATION_NUMBER`` ``-n`` Start rendering not from the first animation, but from another, specified by its index. If you passing two comma separated values, e.g. "3,6", it will end the rendering at the second value.
|
``--start_at_animation_number START_AT_ANIMATION_NUMBER`` ``-n`` Start rendering not from the first animation, but from another, specified by its index. If you passing two comma separated values, e.g. "3,6", it will end the rendering at the second value.
|
||||||
``--embed LINENO`` ``-e`` Takes a line number as an argument, and results in the scene being called as if the line ``self.embed()`` was inserted into the scene code at that line number
|
``--embed LINENO`` ``-e`` Takes a line number as an argument, and results in the scene being called as if the line ``self.embed()`` was inserted into the scene code at that line number
|
||||||
``--resolution RESOLUTION`` ``-r`` Resolution, passed as "WxH", e.g. "1920x1080"
|
``--resolution RESOLUTION`` ``-r`` Resolution, passed as "WxH", e.g. "1920x1080"
|
||||||
``--frame_rate FRAME_RATE`` Frame rate, as an integer
|
``--fps FPS`` Frame rate, as an integer
|
||||||
``--color COLOR`` ``-c`` Background color
|
``--color COLOR`` ``-c`` Background color
|
||||||
``--leave_progress_bars`` Leave progress bars displayed in terminal
|
``--leave_progress_bars`` Leave progress bars displayed in terminal
|
||||||
``--video_dir VIDEO_DIR`` Directory to write video
|
``--video_dir VIDEO_DIR`` Directory to write video
|
||||||
|
|
|
@ -77,10 +77,6 @@ class AnimatingMethods(Scene):
|
||||||
# ".animate" syntax:
|
# ".animate" syntax:
|
||||||
self.play(grid.animate.shift(LEFT))
|
self.play(grid.animate.shift(LEFT))
|
||||||
|
|
||||||
# Alternatively, you can use the older syntax by passing the
|
|
||||||
# method and then the arguments to the scene's "play" function:
|
|
||||||
self.play(grid.shift, LEFT)
|
|
||||||
|
|
||||||
# Both of those will interpolate between the mobject's initial
|
# Both of those will interpolate between the mobject's initial
|
||||||
# state and whatever happens when you apply that method.
|
# state and whatever happens when you apply that method.
|
||||||
# For this example, calling grid.shift(LEFT) would shift the
|
# For this example, calling grid.shift(LEFT) would shift the
|
||||||
|
|
|
@ -6,6 +6,7 @@ from manimlib.mobject.mobject import _AnimationBuilder
|
||||||
from manimlib.mobject.mobject import Mobject
|
from manimlib.mobject.mobject import Mobject
|
||||||
from manimlib.utils.config_ops import digest_config
|
from manimlib.utils.config_ops import digest_config
|
||||||
from manimlib.utils.rate_functions import smooth
|
from manimlib.utils.rate_functions import smooth
|
||||||
|
from manimlib.utils.rate_functions import squish_rate_func
|
||||||
from manimlib.utils.simple_functions import clip
|
from manimlib.utils.simple_functions import clip
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
@ -23,6 +24,7 @@ DEFAULT_ANIMATION_LAG_RATIO = 0
|
||||||
class Animation(object):
|
class Animation(object):
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
"run_time": DEFAULT_ANIMATION_RUN_TIME,
|
"run_time": DEFAULT_ANIMATION_RUN_TIME,
|
||||||
|
"time_span": None, # Tuple of times, between which the animation will run
|
||||||
"rate_func": smooth,
|
"rate_func": smooth,
|
||||||
"name": None,
|
"name": None,
|
||||||
# Does this animation add or remove a mobject form the screen
|
# Does this animation add or remove a mobject form the screen
|
||||||
|
@ -53,6 +55,12 @@ class Animation(object):
|
||||||
# played. As much initialization as possible,
|
# played. As much initialization as possible,
|
||||||
# especially any mobject copying, should live in
|
# especially any mobject copying, should live in
|
||||||
# this method
|
# this method
|
||||||
|
if self.time_span is not None:
|
||||||
|
start, end = self.time_span
|
||||||
|
self.run_time = max(end, self.run_time)
|
||||||
|
self.rate_func = squish_rate_func(
|
||||||
|
self.rate_func, start / self.run_time, end / self.run_time,
|
||||||
|
)
|
||||||
self.mobject.set_animating_status(True)
|
self.mobject.set_animating_status(True)
|
||||||
self.starting_mobject = self.create_starting_mobject()
|
self.starting_mobject = self.create_starting_mobject()
|
||||||
if self.suspend_mobject_updating:
|
if self.suspend_mobject_updating:
|
||||||
|
@ -166,6 +174,8 @@ class Animation(object):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def get_run_time(self) -> float:
|
def get_run_time(self) -> float:
|
||||||
|
if self.time_span:
|
||||||
|
return max(self.run_time, self.time_span[1])
|
||||||
return self.run_time
|
return self.run_time
|
||||||
|
|
||||||
def set_rate_func(self, rate_func: Callable[[float], float]):
|
def set_rate_func(self, rate_func: Callable[[float], float]):
|
||||||
|
|
|
@ -68,6 +68,7 @@ class Transform(Animation):
|
||||||
self.target_copy = self.target_mobject.copy()
|
self.target_copy = self.target_mobject.copy()
|
||||||
self.mobject.align_data_and_family(self.target_copy)
|
self.mobject.align_data_and_family(self.target_copy)
|
||||||
super().begin()
|
super().begin()
|
||||||
|
if not self.mobject.has_updaters:
|
||||||
self.mobject.lock_matching_data(
|
self.mobject.lock_matching_data(
|
||||||
self.starting_mobject,
|
self.starting_mobject,
|
||||||
self.target_copy,
|
self.target_copy,
|
||||||
|
|
|
@ -11,7 +11,7 @@ from scipy.spatial.transform import Rotation
|
||||||
|
|
||||||
from manimlib.constants import BLACK
|
from manimlib.constants import BLACK
|
||||||
from manimlib.constants import DEGREES, RADIANS
|
from manimlib.constants import DEGREES, RADIANS
|
||||||
from manimlib.constants import DEFAULT_FRAME_RATE
|
from manimlib.constants import DEFAULT_FPS
|
||||||
from manimlib.constants import DEFAULT_PIXEL_HEIGHT, DEFAULT_PIXEL_WIDTH
|
from manimlib.constants import DEFAULT_PIXEL_HEIGHT, DEFAULT_PIXEL_WIDTH
|
||||||
from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH
|
from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH
|
||||||
from manimlib.constants import DOWN, LEFT, ORIGIN, OUT, RIGHT, UP
|
from manimlib.constants import DOWN, LEFT, ORIGIN, OUT, RIGHT, UP
|
||||||
|
@ -170,7 +170,7 @@ class Camera(object):
|
||||||
"frame_config": {},
|
"frame_config": {},
|
||||||
"pixel_width": DEFAULT_PIXEL_WIDTH,
|
"pixel_width": DEFAULT_PIXEL_WIDTH,
|
||||||
"pixel_height": DEFAULT_PIXEL_HEIGHT,
|
"pixel_height": DEFAULT_PIXEL_HEIGHT,
|
||||||
"frame_rate": DEFAULT_FRAME_RATE,
|
"fps": DEFAULT_FPS,
|
||||||
# Note: frame height and width will be resized to match
|
# Note: frame height and width will be resized to match
|
||||||
# the pixel aspect ratio
|
# the pixel aspect ratio
|
||||||
"background_color": BLACK,
|
"background_color": BLACK,
|
||||||
|
|
|
@ -136,7 +136,7 @@ def parse_cli():
|
||||||
help="Resolution, passed as \"WxH\", e.g. \"1920x1080\"",
|
help="Resolution, passed as \"WxH\", e.g. \"1920x1080\"",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--frame_rate",
|
"--fps",
|
||||||
help="Frame rate, as an integer",
|
help="Frame rate, as an integer",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
@ -148,6 +148,11 @@ def parse_cli():
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Leave progress bars displayed in terminal",
|
help="Leave progress bars displayed in terminal",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--show_animation_progress",
|
||||||
|
action="store_true",
|
||||||
|
help="Show progress bar for each animation",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--video_dir",
|
"--video_dir",
|
||||||
help="Directory to write video",
|
help="Directory to write video",
|
||||||
|
@ -225,6 +230,8 @@ def insert_embed_line(file_name: str, scene_name: str, line_marker: str):
|
||||||
if len(line.strip()) > 0 and get_indent(line) < n_spaces:
|
if len(line.strip()) > 0 and get_indent(line) < n_spaces:
|
||||||
prev_line_num = index - 2
|
prev_line_num = index - 2
|
||||||
break
|
break
|
||||||
|
if prev_line_num is None:
|
||||||
|
prev_line_num = len(lines) - 2
|
||||||
elif line_marker.isdigit():
|
elif line_marker.isdigit():
|
||||||
# Treat the argument as a line number
|
# Treat the argument as a line number
|
||||||
prev_line_num = int(line_marker) - 1
|
prev_line_num = int(line_marker) - 1
|
||||||
|
@ -233,7 +240,7 @@ def insert_embed_line(file_name: str, scene_name: str, line_marker: str):
|
||||||
try:
|
try:
|
||||||
prev_line_num = next(
|
prev_line_num = next(
|
||||||
i
|
i
|
||||||
for i in range(len(lines) - 1, scene_line_number, -1)
|
for i in range(scene_line_number, len(lines) - 1)
|
||||||
if line_marker in lines[i]
|
if line_marker in lines[i]
|
||||||
)
|
)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
|
@ -330,6 +337,16 @@ def get_configuration(args):
|
||||||
else:
|
else:
|
||||||
file_ext = ".mp4"
|
file_ext = ".mp4"
|
||||||
|
|
||||||
|
dir_config = custom_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"]
|
||||||
|
ext = os.path.abspath(args.file)
|
||||||
|
ext = ext.replace(to_cut, "").replace(".py", "")
|
||||||
|
if ext.startswith("_"):
|
||||||
|
ext = ext[1:]
|
||||||
|
output_directory = os.path.join(output_directory, ext)
|
||||||
|
|
||||||
file_writer_config = {
|
file_writer_config = {
|
||||||
"write_to_movie": not args.skip_animations and write_file,
|
"write_to_movie": not args.skip_animations and write_file,
|
||||||
"break_into_partial_movies": custom_config["break_into_partial_movies"],
|
"break_into_partial_movies": custom_config["break_into_partial_movies"],
|
||||||
|
@ -338,8 +355,7 @@ def get_configuration(args):
|
||||||
# If -t is passed in (for transparent), this will be RGBA
|
# If -t is passed in (for transparent), this will be RGBA
|
||||||
"png_mode": "RGBA" if args.transparent else "RGB",
|
"png_mode": "RGBA" if args.transparent else "RGB",
|
||||||
"movie_file_extension": file_ext,
|
"movie_file_extension": file_ext,
|
||||||
"mirror_module_path": custom_config["directories"]["mirror_module_path"],
|
"output_directory": output_directory,
|
||||||
"output_directory": args.video_dir or custom_config["directories"]["output"],
|
|
||||||
"file_name": args.file_name,
|
"file_name": args.file_name,
|
||||||
"input_file_path": args.file or "",
|
"input_file_path": args.file or "",
|
||||||
"open_file_upon_completion": args.open,
|
"open_file_upon_completion": args.open,
|
||||||
|
@ -365,6 +381,7 @@ def get_configuration(args):
|
||||||
"preview": not write_file,
|
"preview": not write_file,
|
||||||
"presenter_mode": args.presenter_mode,
|
"presenter_mode": args.presenter_mode,
|
||||||
"leave_progress_bars": args.leave_progress_bars,
|
"leave_progress_bars": args.leave_progress_bars,
|
||||||
|
"show_animation_progress": args.show_animation_progress,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Camera configuration
|
# Camera configuration
|
||||||
|
@ -398,31 +415,31 @@ def get_configuration(args):
|
||||||
|
|
||||||
def get_camera_configuration(args, custom_config):
|
def get_camera_configuration(args, custom_config):
|
||||||
camera_config = {}
|
camera_config = {}
|
||||||
camera_qualities = get_custom_config()["camera_qualities"]
|
camera_resolutions = get_custom_config()["camera_resolutions"]
|
||||||
if args.low_quality:
|
if args.low_quality:
|
||||||
quality = camera_qualities["low"]
|
resolution = camera_resolutions["low"]
|
||||||
elif args.medium_quality:
|
elif args.medium_quality:
|
||||||
quality = camera_qualities["medium"]
|
resolution = camera_resolutions["med"]
|
||||||
elif args.hd:
|
elif args.hd:
|
||||||
quality = camera_qualities["high"]
|
resolution = camera_resolutions["high"]
|
||||||
elif args.uhd:
|
elif args.uhd:
|
||||||
quality = camera_qualities["ultra_high"]
|
resolution = camera_resolutions["4k"]
|
||||||
else:
|
else:
|
||||||
quality = camera_qualities[camera_qualities["default_quality"]]
|
resolution = camera_resolutions[camera_resolutions["default_resolution"]]
|
||||||
|
|
||||||
if args.resolution:
|
if args.fps:
|
||||||
quality["resolution"] = args.resolution
|
fps = int(args.fps)
|
||||||
if args.frame_rate:
|
else:
|
||||||
quality["frame_rate"] = int(args.frame_rate)
|
fps = get_custom_config()["fps"]
|
||||||
|
|
||||||
width_str, height_str = quality["resolution"].split("x")
|
width_str, height_str = resolution.split("x")
|
||||||
width = int(width_str)
|
width = int(width_str)
|
||||||
height = int(height_str)
|
height = int(height_str)
|
||||||
|
|
||||||
camera_config.update({
|
camera_config.update({
|
||||||
"pixel_width": width,
|
"pixel_width": width,
|
||||||
"pixel_height": height,
|
"pixel_height": height,
|
||||||
"frame_rate": quality["frame_rate"],
|
"fps": fps,
|
||||||
})
|
})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -10,7 +10,7 @@ FRAME_X_RADIUS = FRAME_WIDTH / 2
|
||||||
|
|
||||||
DEFAULT_PIXEL_HEIGHT = 1080
|
DEFAULT_PIXEL_HEIGHT = 1080
|
||||||
DEFAULT_PIXEL_WIDTH = 1920
|
DEFAULT_PIXEL_WIDTH = 1920
|
||||||
DEFAULT_FRAME_RATE = 30
|
DEFAULT_FPS = 30
|
||||||
|
|
||||||
SMALL_BUFF = 0.1
|
SMALL_BUFF = 0.1
|
||||||
MED_SMALL_BUFF = 0.25
|
MED_SMALL_BUFF = 0.25
|
||||||
|
|
|
@ -28,6 +28,7 @@ tex:
|
||||||
universal_import_line: "from manimlib import *"
|
universal_import_line: "from manimlib import *"
|
||||||
style:
|
style:
|
||||||
font: "Consolas"
|
font: "Consolas"
|
||||||
|
text_alignment: "LEFT"
|
||||||
background_color: "#333333"
|
background_color: "#333333"
|
||||||
# Set the position of preview window, you can use directions, e.g. UL/DR/OL/OO/...
|
# 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
|
# also, you can also specify the position(pixel) of the upper left corner of
|
||||||
|
@ -42,17 +43,10 @@ full_screen: False
|
||||||
# easier when working with the broken up scene, which
|
# easier when working with the broken up scene, which
|
||||||
# effectively has cuts at all the places you might want.
|
# effectively has cuts at all the places you might want.
|
||||||
break_into_partial_movies: False
|
break_into_partial_movies: False
|
||||||
camera_qualities:
|
camera_resolutions:
|
||||||
low:
|
low: "854x480"
|
||||||
resolution: "854x480"
|
med: "1280x720"
|
||||||
frame_rate: 15
|
high: "1920x1080"
|
||||||
medium:
|
4k: "3840x2160"
|
||||||
resolution: "1280x720"
|
default_resolution: "high"
|
||||||
frame_rate: 30
|
fps: 30
|
||||||
high:
|
|
||||||
resolution: "1920x1080"
|
|
||||||
frame_rate: 30
|
|
||||||
ultra_high:
|
|
||||||
resolution: "3840x2160"
|
|
||||||
frame_rate: 60
|
|
||||||
default_quality: "high"
|
|
|
@ -64,6 +64,7 @@ def get_scene_config(config):
|
||||||
"start_at_animation_number",
|
"start_at_animation_number",
|
||||||
"end_at_animation_number",
|
"end_at_animation_number",
|
||||||
"leave_progress_bars",
|
"leave_progress_bars",
|
||||||
|
"show_animation_progress",
|
||||||
"preview",
|
"preview",
|
||||||
"presenter_mode",
|
"presenter_mode",
|
||||||
]
|
]
|
||||||
|
@ -87,7 +88,7 @@ def compute_total_frames(scene_class, scene_config):
|
||||||
pre_scene = scene_class(**pre_config)
|
pre_scene = scene_class(**pre_config)
|
||||||
pre_scene.run()
|
pre_scene.run()
|
||||||
total_time = pre_scene.time - pre_scene.skip_time
|
total_time = pre_scene.time - pre_scene.skip_time
|
||||||
return int(total_time * scene_config["camera_config"]["frame_rate"])
|
return int(total_time * scene_config["camera_config"]["fps"])
|
||||||
|
|
||||||
|
|
||||||
def get_scenes_to_render(scene_classes, scene_config, config):
|
def get_scenes_to_render(scene_classes, scene_config, config):
|
||||||
|
|
|
@ -75,6 +75,7 @@ class ParametricCurve(VMobject):
|
||||||
if hasattr(self, "x_range"):
|
if hasattr(self, "x_range"):
|
||||||
return self.x_range
|
return self.x_range
|
||||||
|
|
||||||
|
|
||||||
class FunctionGraph(ParametricCurve):
|
class FunctionGraph(ParametricCurve):
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
"color": YELLOW,
|
"color": YELLOW,
|
||||||
|
|
|
@ -821,7 +821,7 @@ class FillArrow(Line):
|
||||||
|
|
||||||
def reset_points_around_ends(self):
|
def reset_points_around_ends(self):
|
||||||
self.set_points_by_ends(
|
self.set_points_by_ends(
|
||||||
self.get_start(), self.get_end(), path_arc=self.path_arc
|
self.get_start().copy(), self.get_end().copy(), path_arc=self.path_arc
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
|
@ -286,7 +286,7 @@ class Mobject(object):
|
||||||
def are_points_touching(
|
def are_points_touching(
|
||||||
self,
|
self,
|
||||||
points: np.ndarray,
|
points: np.ndarray,
|
||||||
buff: float = MED_SMALL_BUFF
|
buff: float = 0
|
||||||
) -> bool:
|
) -> bool:
|
||||||
bb = self.get_bounding_box()
|
bb = self.get_bounding_box()
|
||||||
mins = (bb[0] - buff)
|
mins = (bb[0] - buff)
|
||||||
|
@ -296,7 +296,7 @@ class Mobject(object):
|
||||||
def is_point_touching(
|
def is_point_touching(
|
||||||
self,
|
self,
|
||||||
point: np.ndarray,
|
point: np.ndarray,
|
||||||
buff: float = MED_SMALL_BUFF
|
buff: float = 0
|
||||||
) -> bool:
|
) -> bool:
|
||||||
return self.are_points_touching(np.array(point, ndmin=2), buff)[0]
|
return self.are_points_touching(np.array(point, ndmin=2), buff)[0]
|
||||||
|
|
||||||
|
@ -359,8 +359,9 @@ class Mobject(object):
|
||||||
if p not in excluded:
|
if p not in excluded:
|
||||||
ancestors.append(p)
|
ancestors.append(p)
|
||||||
to_process.append(p)
|
to_process.append(p)
|
||||||
# Remove redundancies while preserving order
|
# Ensure mobjects highest in the hierarchy show up first
|
||||||
ancestors.reverse()
|
ancestors.reverse()
|
||||||
|
# Remove list redundancies while preserving order
|
||||||
return list(dict.fromkeys(ancestors))
|
return list(dict.fromkeys(ancestors))
|
||||||
|
|
||||||
def add(self, *mobjects: Mobject):
|
def add(self, *mobjects: Mobject):
|
||||||
|
@ -475,6 +476,31 @@ class Mobject(object):
|
||||||
self.center()
|
self.center()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def arrange_to_fit_dim(self, length: float, dim: int, about_edge=ORIGIN):
|
||||||
|
ref_point = self.get_bounding_box_point(about_edge)
|
||||||
|
n_submobs = len(self.submobjects)
|
||||||
|
if n_submobs <= 1:
|
||||||
|
return
|
||||||
|
total_length = sum(sm.length_over_dim(dim) for sm in self.submobjects)
|
||||||
|
buff = (length - total_length) / (n_submobs - 1)
|
||||||
|
vect = np.zeros(self.dim)
|
||||||
|
vect[dim] = 1
|
||||||
|
x = 0
|
||||||
|
for submob in self.submobjects:
|
||||||
|
submob.set_coord(x, dim, -vect)
|
||||||
|
x += submob.length_over_dim(dim) + buff
|
||||||
|
self.move_to(ref_point, about_edge)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def arrange_to_fit_width(self, width: float, about_edge=ORIGIN):
|
||||||
|
return self.arrange_to_fit_dim(width, 0, about_edge)
|
||||||
|
|
||||||
|
def arrange_to_fit_height(self, height: float, about_edge=ORIGIN):
|
||||||
|
return self.arrange_to_fit_dim(height, 1, about_edge)
|
||||||
|
|
||||||
|
def arrange_to_fit_depth(self, depth: float, about_edge=ORIGIN):
|
||||||
|
return self.arrange_to_fit_dim(depth, 2, about_edge)
|
||||||
|
|
||||||
def sort(
|
def sort(
|
||||||
self,
|
self,
|
||||||
point_to_num_func: Callable[[np.ndarray], float] = lambda p: p[0],
|
point_to_num_func: Callable[[np.ndarray], float] = lambda p: p[0],
|
||||||
|
@ -616,11 +642,12 @@ class Mobject(object):
|
||||||
sm1.depth_test = sm2.depth_test
|
sm1.depth_test = sm2.depth_test
|
||||||
sm1.render_primitive = sm2.render_primitive
|
sm1.render_primitive = sm2.render_primitive
|
||||||
self.refresh_bounding_box(recurse_down=True)
|
self.refresh_bounding_box(recurse_down=True)
|
||||||
|
self.match_updaters(mobject)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def looks_identical(self, mobject: Mobject):
|
def looks_identical(self, mobject: Mobject):
|
||||||
fam1 = self.get_family()
|
fam1 = self.family_members_with_points()
|
||||||
fam2 = mobject.get_family()
|
fam2 = mobject.family_members_with_points()
|
||||||
if len(fam1) != len(fam2):
|
if len(fam1) != len(fam2):
|
||||||
return False
|
return False
|
||||||
for m1, m2 in zip(fam1, fam2):
|
for m1, m2 in zip(fam1, fam2):
|
||||||
|
@ -628,10 +655,12 @@ class Mobject(object):
|
||||||
if set(d1).difference(d2):
|
if set(d1).difference(d2):
|
||||||
return False
|
return False
|
||||||
for key in d1:
|
for key in d1:
|
||||||
if isinstance(d1[key], np.ndarray):
|
eq = (d1[key] == d2[key])
|
||||||
if not np.all(d1[key] == d2[key]):
|
if isinstance(eq, bool):
|
||||||
|
if not eq:
|
||||||
return False
|
return False
|
||||||
elif d1[key] != d2[key]:
|
else:
|
||||||
|
if not eq.all():
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
|
@ -34,21 +34,25 @@ class DecimalNumber(VMobject):
|
||||||
def __init__(self, number: float | complex = 0, **kwargs):
|
def __init__(self, number: float | complex = 0, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.set_submobjects_from_number(number)
|
self.set_submobjects_from_number(number)
|
||||||
|
self.init_colors()
|
||||||
|
|
||||||
def set_submobjects_from_number(self, number: float | complex) -> None:
|
def set_submobjects_from_number(self, number: float | complex) -> None:
|
||||||
self.number = number
|
self.number = number
|
||||||
self.set_submobjects([])
|
self.set_submobjects([])
|
||||||
string_to_mob_ = lambda s: self.string_to_mob(s, **self.text_config)
|
self.text_config["font_size"] = self.get_font_size()
|
||||||
num_string = self.get_num_string(number)
|
num_string = self.num_string = self.get_num_string(number)
|
||||||
self.add(*map(string_to_mob_, num_string))
|
self.add(*(
|
||||||
|
Text(ns, **self.text_config)
|
||||||
|
for ns in num_string
|
||||||
|
))
|
||||||
|
|
||||||
# Add non-numerical bits
|
# Add non-numerical bits
|
||||||
if self.show_ellipsis:
|
if self.show_ellipsis:
|
||||||
dots = string_to_mob_("...")
|
dots = Text("...", **self.text_config)
|
||||||
dots.arrange(RIGHT, buff=2 * dots[0].get_width())
|
dots.arrange(RIGHT, buff=2 * dots[0].get_width())
|
||||||
self.add(dots)
|
self.add(dots)
|
||||||
if self.unit is not None:
|
if self.unit is not None:
|
||||||
self.unit_sign = self.string_to_mob(self.unit, SingleStringTex)
|
self.unit_sign = SingleStringTex(self.unit, font_size=self.get_font_size())
|
||||||
self.add(self.unit_sign)
|
self.add(self.unit_sign)
|
||||||
|
|
||||||
self.arrange(
|
self.arrange(
|
||||||
|
@ -91,12 +95,7 @@ class DecimalNumber(VMobject):
|
||||||
self.data["font_size"] = np.array([self.font_size], dtype=float)
|
self.data["font_size"] = np.array([self.font_size], dtype=float)
|
||||||
|
|
||||||
def get_font_size(self) -> float:
|
def get_font_size(self) -> float:
|
||||||
return self.data["font_size"][0]
|
return int(self.data["font_size"][0])
|
||||||
|
|
||||||
def string_to_mob(self, string: str, mob_class: Type[T] = Text, **kwargs) -> T:
|
|
||||||
mob = mob_class(string, font_size=1, **kwargs)
|
|
||||||
mob.scale(self.get_font_size())
|
|
||||||
return mob
|
|
||||||
|
|
||||||
def get_formatter(self, **kwargs) -> str:
|
def get_formatter(self, **kwargs) -> str:
|
||||||
"""
|
"""
|
||||||
|
@ -117,13 +116,14 @@ class DecimalNumber(VMobject):
|
||||||
]
|
]
|
||||||
])
|
])
|
||||||
config.update(kwargs)
|
config.update(kwargs)
|
||||||
|
ndp = config["num_decimal_places"]
|
||||||
return "".join([
|
return "".join([
|
||||||
"{",
|
"{",
|
||||||
config.get("field_name", ""),
|
config.get("field_name", ""),
|
||||||
":",
|
":",
|
||||||
"+" if config["include_sign"] else "",
|
"+" if config["include_sign"] else "",
|
||||||
"," if config["group_with_commas"] else "",
|
"," if config["group_with_commas"] else "",
|
||||||
".", str(config["num_decimal_places"]), "f",
|
f".{ndp}f",
|
||||||
"}",
|
"}",
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@ -134,13 +134,15 @@ class DecimalNumber(VMobject):
|
||||||
"i"
|
"i"
|
||||||
])
|
])
|
||||||
|
|
||||||
|
def get_tex(self):
|
||||||
|
return self.num_string
|
||||||
|
|
||||||
def set_value(self, number: float | complex):
|
def set_value(self, number: float | complex):
|
||||||
move_to_point = self.get_edge_center(self.edge_to_fix)
|
move_to_point = self.get_edge_center(self.edge_to_fix)
|
||||||
old_submobjects = list(self.submobjects)
|
style = self.family_members_with_points()[0].get_style()
|
||||||
self.set_submobjects_from_number(number)
|
self.set_submobjects_from_number(number)
|
||||||
self.move_to(move_to_point, self.edge_to_fix)
|
self.move_to(move_to_point, self.edge_to_fix)
|
||||||
for sm1, sm2 in zip(self.submobjects, old_submobjects):
|
self.set_style(**style)
|
||||||
sm1.match_style(sm2)
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def _handle_scale_side_effects(self, scale_factor: float) -> None:
|
def _handle_scale_side_effects(self, scale_factor: float) -> None:
|
||||||
|
|
|
@ -87,7 +87,7 @@ class Cross(VGroup):
|
||||||
Line(UL, DR),
|
Line(UL, DR),
|
||||||
Line(UR, DL),
|
Line(UR, DL),
|
||||||
)
|
)
|
||||||
self.insert_n_curves(2)
|
self.insert_n_curves(20)
|
||||||
self.replace(mobject, stretch=True)
|
self.replace(mobject, stretch=True)
|
||||||
self.set_stroke(self.stroke_color, width=self.stroke_width)
|
self.set_stroke(self.stroke_color, width=self.stroke_width)
|
||||||
|
|
||||||
|
|
|
@ -308,7 +308,7 @@ class Bubble(SVGMobject):
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
digest_config(self, kwargs, locals())
|
digest_config(self, kwargs)
|
||||||
if self.file_name is None:
|
if self.file_name is None:
|
||||||
raise Exception("Must invoke Bubble subclass")
|
raise Exception("Must invoke Bubble subclass")
|
||||||
SVGMobject.__init__(self, self.file_name, **kwargs)
|
SVGMobject.__init__(self, self.file_name, **kwargs)
|
||||||
|
@ -317,7 +317,11 @@ class Bubble(SVGMobject):
|
||||||
self.stretch_to_fit_width(self.width)
|
self.stretch_to_fit_width(self.width)
|
||||||
if self.direction[0] > 0:
|
if self.direction[0] > 0:
|
||||||
self.flip()
|
self.flip()
|
||||||
self.direction_was_specified = ("direction" in kwargs)
|
if "direction" in kwargs:
|
||||||
|
self.direction = kwargs["direction"]
|
||||||
|
self.direction_was_specified = True
|
||||||
|
else:
|
||||||
|
self.direction_was_specified = False
|
||||||
self.content = Mobject()
|
self.content = Mobject()
|
||||||
self.refresh_triangulation()
|
self.refresh_triangulation()
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ class SingleStringTex(SVGMobject):
|
||||||
"fill_opacity": 1.0,
|
"fill_opacity": 1.0,
|
||||||
"stroke_width": 0,
|
"stroke_width": 0,
|
||||||
"svg_default": {
|
"svg_default": {
|
||||||
"color": WHITE,
|
"fill_color": WHITE,
|
||||||
},
|
},
|
||||||
"path_string_config": {
|
"path_string_config": {
|
||||||
"should_subdivide_sharp_curves": True,
|
"should_subdivide_sharp_curves": True,
|
||||||
|
|
|
@ -68,7 +68,7 @@ class MarkupText(StringMobject):
|
||||||
"lsh": None,
|
"lsh": None,
|
||||||
"justify": False,
|
"justify": False,
|
||||||
"indent": 0,
|
"indent": 0,
|
||||||
"alignment": "LEFT",
|
"alignment": "",
|
||||||
"line_width": None,
|
"line_width": None,
|
||||||
"font": "",
|
"font": "",
|
||||||
"slant": NORMAL,
|
"slant": NORMAL,
|
||||||
|
@ -114,6 +114,8 @@ class MarkupText(StringMobject):
|
||||||
|
|
||||||
if not self.font:
|
if not self.font:
|
||||||
self.font = get_customization()["style"]["font"]
|
self.font = get_customization()["style"]["font"]
|
||||||
|
if not self.alignment:
|
||||||
|
self.alignment = get_customization()["style"]["text_alignment"]
|
||||||
if self.is_markup:
|
if self.is_markup:
|
||||||
self.validate_markup_string(text)
|
self.validate_markup_string(text)
|
||||||
|
|
||||||
|
|
|
@ -203,36 +203,38 @@ class VMobject(Mobject):
|
||||||
shadow: float | None = None,
|
shadow: float | None = None,
|
||||||
recurse: bool = True
|
recurse: bool = True
|
||||||
):
|
):
|
||||||
|
for mob in self.get_family(recurse):
|
||||||
if fill_rgba is not None:
|
if fill_rgba is not None:
|
||||||
self.data['fill_rgba'] = resize_with_interpolation(fill_rgba, len(fill_rgba))
|
mob.data['fill_rgba'] = resize_with_interpolation(fill_rgba, len(fill_rgba))
|
||||||
else:
|
else:
|
||||||
self.set_fill(
|
mob.set_fill(
|
||||||
color=fill_color,
|
color=fill_color,
|
||||||
opacity=fill_opacity,
|
opacity=fill_opacity,
|
||||||
recurse=recurse
|
recurse=False
|
||||||
)
|
)
|
||||||
|
|
||||||
if stroke_rgba is not None:
|
if stroke_rgba is not None:
|
||||||
self.data['stroke_rgba'] = resize_with_interpolation(stroke_rgba, len(fill_rgba))
|
mob.data['stroke_rgba'] = resize_with_interpolation(stroke_rgba, len(fill_rgba))
|
||||||
self.set_stroke(
|
mob.set_stroke(
|
||||||
width=stroke_width,
|
width=stroke_width,
|
||||||
background=stroke_background,
|
background=stroke_background,
|
||||||
|
recurse=False,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.set_stroke(
|
mob.set_stroke(
|
||||||
color=stroke_color,
|
color=stroke_color,
|
||||||
width=stroke_width,
|
width=stroke_width,
|
||||||
opacity=stroke_opacity,
|
opacity=stroke_opacity,
|
||||||
recurse=recurse,
|
recurse=False,
|
||||||
background=stroke_background,
|
background=stroke_background,
|
||||||
)
|
)
|
||||||
|
|
||||||
if reflectiveness is not None:
|
if reflectiveness is not None:
|
||||||
self.set_reflectiveness(reflectiveness, recurse=recurse)
|
mob.set_reflectiveness(reflectiveness, recurse=False)
|
||||||
if gloss is not None:
|
if gloss is not None:
|
||||||
self.set_gloss(gloss, recurse=recurse)
|
mob.set_gloss(gloss, recurse=False)
|
||||||
if shadow is not None:
|
if shadow is not None:
|
||||||
self.set_shadow(shadow, recurse=recurse)
|
mob.set_shadow(shadow, recurse=False)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def get_style(self):
|
def get_style(self):
|
||||||
|
@ -1202,17 +1204,17 @@ class VHighlight(VGroup):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
vmobject: VMobject,
|
vmobject: VMobject,
|
||||||
n_layers: int = 3,
|
n_layers: int = 5,
|
||||||
color_bounds: tuple[ManimColor] = (GREY_C, GREY_E),
|
color_bounds: tuple[ManimColor] = (GREY_C, GREY_E),
|
||||||
max_stroke_width: float = 10.0,
|
max_stroke_addition: float = 5.0,
|
||||||
):
|
):
|
||||||
outline = vmobject.replicate(n_layers)
|
outline = vmobject.replicate(n_layers)
|
||||||
outline.set_fill(opacity=0)
|
outline.set_fill(opacity=0)
|
||||||
added_widths = np.linspace(0, max_stroke_width, n_layers + 1)[1:]
|
added_widths = np.linspace(0, max_stroke_addition, n_layers + 1)[1:]
|
||||||
colors = color_gradient(color_bounds, n_layers)
|
colors = color_gradient(color_bounds, n_layers)
|
||||||
for part, added_width, color in zip(reversed(outline), added_widths, colors):
|
for part, added_width, color in zip(reversed(outline), added_widths, colors):
|
||||||
for sm in part.family_members_with_points():
|
for sm in part.family_members_with_points():
|
||||||
part.set_stroke(
|
sm.set_stroke(
|
||||||
width=sm.get_stroke_width() + added_width,
|
width=sm.get_stroke_width() + added_width,
|
||||||
color=color,
|
color=color,
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,7 +9,9 @@ from manimlib.constants import ARROW_SYMBOLS, CTRL_SYMBOL, DELETE_SYMBOL, SHIFT_
|
||||||
from manimlib.constants import COMMAND_MODIFIER, SHIFT_MODIFIER
|
from manimlib.constants import COMMAND_MODIFIER, SHIFT_MODIFIER
|
||||||
from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, RIGHT, UL, UP, UR
|
from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, RIGHT, UL, UP, UR
|
||||||
from manimlib.constants import FRAME_WIDTH, SMALL_BUFF
|
from manimlib.constants import FRAME_WIDTH, SMALL_BUFF
|
||||||
from manimlib.constants import MANIM_COLORS, WHITE, GREY_C
|
from manimlib.constants import PI
|
||||||
|
from manimlib.constants import MANIM_COLORS, WHITE, GREY_A, GREY_C
|
||||||
|
from manimlib.mobject.geometry import Line
|
||||||
from manimlib.mobject.geometry import Rectangle
|
from manimlib.mobject.geometry import Rectangle
|
||||||
from manimlib.mobject.geometry import Square
|
from manimlib.mobject.geometry import Square
|
||||||
from manimlib.mobject.mobject import Group
|
from manimlib.mobject.mobject import Group
|
||||||
|
@ -22,6 +24,7 @@ from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||||
from manimlib.mobject.types.vectorized_mobject import VHighlight
|
from manimlib.mobject.types.vectorized_mobject import VHighlight
|
||||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||||
from manimlib.scene.scene import Scene
|
from manimlib.scene.scene import Scene
|
||||||
|
from manimlib.scene.scene import SceneState
|
||||||
from manimlib.utils.family_ops import extract_mobject_family_members
|
from manimlib.utils.family_ops import extract_mobject_family_members
|
||||||
from manimlib.utils.space_ops import get_norm
|
from manimlib.utils.space_ops import get_norm
|
||||||
from manimlib.utils.tex_file_writing import LatexError
|
from manimlib.utils.tex_file_writing import LatexError
|
||||||
|
@ -34,7 +37,8 @@ Y_GRAB_KEY = 'v'
|
||||||
GRAB_KEYS = [GRAB_KEY, X_GRAB_KEY, Y_GRAB_KEY]
|
GRAB_KEYS = [GRAB_KEY, X_GRAB_KEY, Y_GRAB_KEY]
|
||||||
RESIZE_KEY = 't'
|
RESIZE_KEY = 't'
|
||||||
COLOR_KEY = 'c'
|
COLOR_KEY = 'c'
|
||||||
CURSOR_LOCATION_KEY = 'l'
|
INFORMATION_KEY = 'i'
|
||||||
|
CURSOR_KEY = 'k'
|
||||||
|
|
||||||
|
|
||||||
# Note, a lot of the functionality here is still buggy and very much a work in progress.
|
# Note, a lot of the functionality here is still buggy and very much a work in progress.
|
||||||
|
@ -68,25 +72,34 @@ class InteractiveScene(Scene):
|
||||||
)
|
)
|
||||||
selection_rectangle_stroke_color = WHITE
|
selection_rectangle_stroke_color = WHITE
|
||||||
selection_rectangle_stroke_width = 1.0
|
selection_rectangle_stroke_width = 1.0
|
||||||
colors = MANIM_COLORS
|
palette_colors = MANIM_COLORS
|
||||||
selection_nudge_size = 0.05
|
selection_nudge_size = 0.05
|
||||||
cursor_location_config = dict(
|
cursor_location_config = dict(
|
||||||
font_size=14,
|
font_size=24,
|
||||||
fill_color=GREY_C,
|
fill_color=GREY_C,
|
||||||
num_decimal_places=3,
|
num_decimal_places=3,
|
||||||
)
|
)
|
||||||
|
time_label_config = dict(
|
||||||
|
font_size=24,
|
||||||
|
fill_color=GREY_C,
|
||||||
|
num_decimal_places=1,
|
||||||
|
)
|
||||||
|
crosshair_width = 0.2
|
||||||
|
crosshair_color = GREY_A
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
self.selection = Group()
|
self.selection = Group()
|
||||||
self.selection_highlight = Group()
|
self.selection_highlight = self.get_selection_highlight()
|
||||||
self.selection_rectangle = self.get_selection_rectangle()
|
self.selection_rectangle = self.get_selection_rectangle()
|
||||||
|
self.crosshair = self.get_crosshair()
|
||||||
|
self.information_label = self.get_information_label()
|
||||||
self.color_palette = self.get_color_palette()
|
self.color_palette = self.get_color_palette()
|
||||||
self.cursor_location_label = self.get_cursor_location_label()
|
|
||||||
self.unselectables = [
|
self.unselectables = [
|
||||||
self.selection,
|
self.selection,
|
||||||
self.selection_highlight,
|
self.selection_highlight,
|
||||||
self.selection_rectangle,
|
self.selection_rectangle,
|
||||||
self.cursor_location_label,
|
self.crosshair,
|
||||||
|
self.information_label,
|
||||||
self.camera.frame
|
self.camera.frame
|
||||||
]
|
]
|
||||||
self.select_top_level_mobs = True
|
self.select_top_level_mobs = True
|
||||||
|
@ -94,6 +107,7 @@ class InteractiveScene(Scene):
|
||||||
|
|
||||||
self.is_selecting = False
|
self.is_selecting = False
|
||||||
self.is_grabbing = False
|
self.is_grabbing = False
|
||||||
|
|
||||||
self.add(self.selection_highlight)
|
self.add(self.selection_highlight)
|
||||||
|
|
||||||
def get_selection_rectangle(self):
|
def get_selection_rectangle(self):
|
||||||
|
@ -106,7 +120,7 @@ class InteractiveScene(Scene):
|
||||||
rect.add_updater(self.update_selection_rectangle)
|
rect.add_updater(self.update_selection_rectangle)
|
||||||
return rect
|
return rect
|
||||||
|
|
||||||
def update_selection_rectangle(self, rect):
|
def update_selection_rectangle(self, rect: Rectangle):
|
||||||
p1 = rect.fixed_corner
|
p1 = rect.fixed_corner
|
||||||
p2 = self.mouse_point.get_center()
|
p2 = self.mouse_point.get_center()
|
||||||
rect.set_points_as_corners([
|
rect.set_points_as_corners([
|
||||||
|
@ -116,10 +130,50 @@ class InteractiveScene(Scene):
|
||||||
])
|
])
|
||||||
return rect
|
return rect
|
||||||
|
|
||||||
|
def get_selection_highlight(self):
|
||||||
|
result = Group()
|
||||||
|
result.tracked_mobjects = []
|
||||||
|
result.add_updater(self.update_selection_highlight)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def update_selection_highlight(self, highlight: Mobject):
|
||||||
|
if set(highlight.tracked_mobjects) == set(self.selection):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Otherwise, refresh contents of highlight
|
||||||
|
highlight.tracked_mobjects = list(self.selection)
|
||||||
|
highlight.set_submobjects([
|
||||||
|
self.get_highlight(mob)
|
||||||
|
for mob in self.selection
|
||||||
|
])
|
||||||
|
try:
|
||||||
|
index = min((
|
||||||
|
i for i, mob in enumerate(self.mobjects)
|
||||||
|
for sm in self.selection
|
||||||
|
if sm in mob.get_family()
|
||||||
|
))
|
||||||
|
self.mobjects.remove(highlight)
|
||||||
|
self.mobjects.insert(index - 1, highlight)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_crosshair(self):
|
||||||
|
line = Line(LEFT, RIGHT)
|
||||||
|
line.insert_n_curves(1)
|
||||||
|
lines = line.replicate(2)
|
||||||
|
lines[1].rotate(PI / 2)
|
||||||
|
crosshair = VMobject()
|
||||||
|
crosshair.set_points([*lines[0].get_points(), *lines[1].get_points()])
|
||||||
|
crosshair.set_width(self.crosshair_width)
|
||||||
|
crosshair.set_stroke(self.crosshair_color, width=[2, 0, 2, 2, 0, 2])
|
||||||
|
crosshair.set_animating_status(True)
|
||||||
|
crosshair.fix_in_frame()
|
||||||
|
return crosshair
|
||||||
|
|
||||||
def get_color_palette(self):
|
def get_color_palette(self):
|
||||||
palette = VGroup(*(
|
palette = VGroup(*(
|
||||||
Square(fill_color=color, fill_opacity=1, side_length=1)
|
Square(fill_color=color, fill_opacity=1, side_length=1)
|
||||||
for color in self.colors
|
for color in self.palette_colors
|
||||||
))
|
))
|
||||||
palette.set_stroke(width=0)
|
palette.set_stroke(width=0)
|
||||||
palette.arrange(RIGHT, buff=0.5)
|
palette.arrange(RIGHT, buff=0.5)
|
||||||
|
@ -128,22 +182,51 @@ class InteractiveScene(Scene):
|
||||||
palette.fix_in_frame()
|
palette.fix_in_frame()
|
||||||
return palette
|
return palette
|
||||||
|
|
||||||
def get_cursor_location_label(self):
|
def get_information_label(self):
|
||||||
decimals = VGroup(*(
|
loc_label = VGroup(*(
|
||||||
DecimalNumber(**self.cursor_location_config)
|
DecimalNumber(**self.cursor_location_config)
|
||||||
for n in range(3)
|
for n in range(3)
|
||||||
))
|
))
|
||||||
|
|
||||||
def update_coords(decimals):
|
def update_coords(loc_label):
|
||||||
for mob, coord in zip(decimals, self.mouse_point.get_location()):
|
for mob, coord in zip(loc_label, self.mouse_point.get_location()):
|
||||||
mob.set_value(coord)
|
mob.set_value(coord)
|
||||||
decimals.arrange(RIGHT, buff=decimals.get_height())
|
loc_label.arrange(RIGHT, buff=loc_label.get_height())
|
||||||
decimals.to_corner(DR, buff=SMALL_BUFF)
|
loc_label.to_corner(DR, buff=SMALL_BUFF)
|
||||||
decimals.fix_in_frame()
|
loc_label.fix_in_frame()
|
||||||
return decimals
|
return loc_label
|
||||||
|
|
||||||
decimals.add_updater(update_coords)
|
loc_label.add_updater(update_coords)
|
||||||
return decimals
|
|
||||||
|
time_label = DecimalNumber(0, **self.time_label_config)
|
||||||
|
time_label.to_corner(DL, buff=SMALL_BUFF)
|
||||||
|
time_label.fix_in_frame()
|
||||||
|
time_label.add_updater(lambda m, dt: m.increment_value(dt))
|
||||||
|
|
||||||
|
return VGroup(loc_label, time_label)
|
||||||
|
|
||||||
|
# Overrides
|
||||||
|
def get_state(self):
|
||||||
|
return SceneState(self, ignore=[
|
||||||
|
self.selection_highlight,
|
||||||
|
self.selection_rectangle,
|
||||||
|
self.crosshair,
|
||||||
|
])
|
||||||
|
|
||||||
|
def restore_state(self, scene_state: SceneState):
|
||||||
|
super().restore_state(scene_state)
|
||||||
|
self.mobjects.insert(0, self.selection_highlight)
|
||||||
|
|
||||||
|
def add(self, *mobjects: Mobject):
|
||||||
|
super().add(*mobjects)
|
||||||
|
self.regenerate_selection_search_set()
|
||||||
|
|
||||||
|
def remove(self, *mobjects: Mobject):
|
||||||
|
super().remove(*mobjects)
|
||||||
|
self.regenerate_selection_search_set()
|
||||||
|
|
||||||
|
# def increment_time(self, dt: float) -> None:
|
||||||
|
# super().increment_time(dt)
|
||||||
|
|
||||||
# Related to selection
|
# Related to selection
|
||||||
|
|
||||||
|
@ -184,7 +267,6 @@ class InteractiveScene(Scene):
|
||||||
curr, exclude_pointless=True,
|
curr, exclude_pointless=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.refresh_selection_highlight()
|
|
||||||
|
|
||||||
def get_corner_dots(self, mobject: Mobject) -> Mobject:
|
def get_corner_dots(self, mobject: Mobject) -> Mobject:
|
||||||
dots = DotCloud(**self.corner_dot_config)
|
dots = DotCloud(**self.corner_dot_config)
|
||||||
|
@ -192,7 +274,7 @@ class InteractiveScene(Scene):
|
||||||
if mobject.get_depth() < 1e-2:
|
if mobject.get_depth() < 1e-2:
|
||||||
vects = [DL, UL, UR, DR]
|
vects = [DL, UL, UR, DR]
|
||||||
else:
|
else:
|
||||||
vects = list(it.product(*3 * [[-1, 1]]))
|
vects = np.array(list(it.product(*3 * [[-1, 1]])))
|
||||||
dots.add_updater(lambda d: d.set_points([
|
dots.add_updater(lambda d: d.set_points([
|
||||||
mobject.get_corner(v) + v * radius
|
mobject.get_corner(v) + v * radius
|
||||||
for v in vects
|
for v in vects
|
||||||
|
@ -201,27 +283,19 @@ class InteractiveScene(Scene):
|
||||||
|
|
||||||
def get_highlight(self, mobject: Mobject) -> Mobject:
|
def get_highlight(self, mobject: Mobject) -> Mobject:
|
||||||
if isinstance(mobject, VMobject) and mobject.has_points() and not self.select_top_level_mobs:
|
if isinstance(mobject, VMobject) and mobject.has_points() and not self.select_top_level_mobs:
|
||||||
result = VHighlight(mobject)
|
length = max([mobject.get_height(), mobject.get_width()])
|
||||||
result.add_updater(lambda m: m.replace(mobject))
|
result = VHighlight(
|
||||||
|
mobject,
|
||||||
|
max_stroke_addition=min([50 * length, 10]),
|
||||||
|
)
|
||||||
|
result.add_updater(lambda m: m.replace(mobject, stretch=True))
|
||||||
return result
|
return result
|
||||||
|
elif isinstance(mobject, DotCloud):
|
||||||
|
return Mobject()
|
||||||
else:
|
else:
|
||||||
return self.get_corner_dots(mobject)
|
return self.get_corner_dots(mobject)
|
||||||
|
|
||||||
def refresh_selection_highlight(self):
|
def add_to_selection(self, *mobjects: Mobject):
|
||||||
if len(self.selection) > 0:
|
|
||||||
self.remove(self.selection_highlight)
|
|
||||||
self.selection_highlight.set_submobjects([
|
|
||||||
self.get_highlight(mob)
|
|
||||||
for mob in self.selection
|
|
||||||
])
|
|
||||||
index = min((
|
|
||||||
i for i, mob in enumerate(self.mobjects)
|
|
||||||
for sm in self.selection
|
|
||||||
if sm in mob.get_family()
|
|
||||||
))
|
|
||||||
self.mobjects.insert(index, self.selection_highlight)
|
|
||||||
|
|
||||||
def add_to_selection(self, *mobjects):
|
|
||||||
mobs = list(filter(
|
mobs = list(filter(
|
||||||
lambda m: m not in self.unselectables and m not in self.selection,
|
lambda m: m not in self.unselectables and m not in self.selection,
|
||||||
mobjects
|
mobjects
|
||||||
|
@ -229,46 +303,34 @@ class InteractiveScene(Scene):
|
||||||
if len(mobs) == 0:
|
if len(mobs) == 0:
|
||||||
return
|
return
|
||||||
self.selection.add(*mobs)
|
self.selection.add(*mobs)
|
||||||
self.refresh_selection_highlight()
|
self.selection.set_animating_status(True)
|
||||||
for sm in mobs:
|
|
||||||
for mob in self.mobjects:
|
|
||||||
if sm in mob.get_family():
|
|
||||||
mob.set_animating_status(True)
|
|
||||||
self.refresh_static_mobjects()
|
|
||||||
|
|
||||||
def toggle_from_selection(self, *mobjects):
|
def toggle_from_selection(self, *mobjects: Mobject):
|
||||||
for mob in mobjects:
|
for mob in mobjects:
|
||||||
if mob in self.selection:
|
if mob in self.selection:
|
||||||
self.selection.remove(mob)
|
self.selection.remove(mob)
|
||||||
mob.set_animating_status(False)
|
mob.set_animating_status(False)
|
||||||
else:
|
else:
|
||||||
self.add_to_selection(mob)
|
self.add_to_selection(mob)
|
||||||
self.refresh_selection_highlight()
|
self.refresh_static_mobjects()
|
||||||
|
|
||||||
def clear_selection(self):
|
def clear_selection(self):
|
||||||
for mob in self.selection:
|
for mob in self.selection:
|
||||||
mob.set_animating_status(False)
|
mob.set_animating_status(False)
|
||||||
self.selection.set_submobjects([])
|
self.selection.set_submobjects([])
|
||||||
self.selection_highlight.set_submobjects([])
|
|
||||||
self.refresh_static_mobjects()
|
self.refresh_static_mobjects()
|
||||||
|
|
||||||
def add(self, *new_mobjects: Mobject):
|
|
||||||
super().add(*new_mobjects)
|
|
||||||
self.regenerate_selection_search_set()
|
|
||||||
|
|
||||||
def remove(self, *mobjects: Mobject):
|
|
||||||
super().remove(*mobjects)
|
|
||||||
self.regenerate_selection_search_set()
|
|
||||||
|
|
||||||
def disable_interaction(self, *mobjects: Mobject):
|
def disable_interaction(self, *mobjects: Mobject):
|
||||||
for mob in mobjects:
|
for mob in mobjects:
|
||||||
self.unselectables.append(mob)
|
for sm in mob.get_family():
|
||||||
|
self.unselectables.append(sm)
|
||||||
self.regenerate_selection_search_set()
|
self.regenerate_selection_search_set()
|
||||||
|
|
||||||
def enable_interaction(self, *mobjects: Mobject):
|
def enable_interaction(self, *mobjects: Mobject):
|
||||||
for mob in mobjects:
|
for mob in mobjects:
|
||||||
if mob in self.unselectables:
|
for sm in mob.get_family():
|
||||||
self.unselectables.remove(mob)
|
if sm in self.unselectables:
|
||||||
|
self.unselectables.remove(sm)
|
||||||
|
|
||||||
# Functions for keyboard actions
|
# Functions for keyboard actions
|
||||||
|
|
||||||
|
@ -308,10 +370,6 @@ class InteractiveScene(Scene):
|
||||||
self.remove(*self.selection)
|
self.remove(*self.selection)
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
|
|
||||||
def restore_state(self, mobject_states: list[tuple[Mobject, Mobject]]):
|
|
||||||
super().restore_state(mobject_states)
|
|
||||||
self.refresh_selection_highlight()
|
|
||||||
|
|
||||||
def enable_selection(self):
|
def enable_selection(self):
|
||||||
self.is_selecting = True
|
self.is_selecting = True
|
||||||
self.add(self.selection_rectangle)
|
self.add(self.selection_rectangle)
|
||||||
|
@ -352,6 +410,12 @@ class InteractiveScene(Scene):
|
||||||
else:
|
else:
|
||||||
self.remove(self.color_palette)
|
self.remove(self.color_palette)
|
||||||
|
|
||||||
|
def display_information(self, show=True):
|
||||||
|
if show:
|
||||||
|
self.add(self.information_label)
|
||||||
|
else:
|
||||||
|
self.remove(self.information_label)
|
||||||
|
|
||||||
def group_selection(self):
|
def group_selection(self):
|
||||||
group = self.get_group(*self.selection)
|
group = self.get_group(*self.selection)
|
||||||
self.add(group)
|
self.add(group)
|
||||||
|
@ -393,8 +457,8 @@ class InteractiveScene(Scene):
|
||||||
self.prepare_resizing(about_corner=True)
|
self.prepare_resizing(about_corner=True)
|
||||||
elif char == COLOR_KEY and modifiers == 0:
|
elif char == COLOR_KEY and modifiers == 0:
|
||||||
self.toggle_color_palette()
|
self.toggle_color_palette()
|
||||||
elif char == CURSOR_LOCATION_KEY and modifiers == 0:
|
elif char == INFORMATION_KEY and modifiers == 0:
|
||||||
self.add(self.cursor_location_label)
|
self.display_information()
|
||||||
elif char == "c" and modifiers == COMMAND_MODIFIER:
|
elif char == "c" and modifiers == COMMAND_MODIFIER:
|
||||||
self.copy_selection()
|
self.copy_selection()
|
||||||
elif char == "v" and modifiers == COMMAND_MODIFIER:
|
elif char == "v" and modifiers == COMMAND_MODIFIER:
|
||||||
|
@ -420,6 +484,14 @@ class InteractiveScene(Scene):
|
||||||
vect=[LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)],
|
vect=[LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)],
|
||||||
large=(modifiers & SHIFT_MODIFIER),
|
large=(modifiers & SHIFT_MODIFIER),
|
||||||
)
|
)
|
||||||
|
# Adding crosshair
|
||||||
|
if char == CURSOR_KEY:
|
||||||
|
if self.crosshair in self.mobjects:
|
||||||
|
self.remove(self.crosshair)
|
||||||
|
else:
|
||||||
|
self.add(self.crosshair)
|
||||||
|
if char == SELECT_KEY:
|
||||||
|
self.add(self.crosshair)
|
||||||
|
|
||||||
# Conditions for saving state
|
# Conditions for saving state
|
||||||
if char in [GRAB_KEY, X_GRAB_KEY, Y_GRAB_KEY, RESIZE_KEY]:
|
if char in [GRAB_KEY, X_GRAB_KEY, Y_GRAB_KEY, RESIZE_KEY]:
|
||||||
|
@ -429,10 +501,11 @@ class InteractiveScene(Scene):
|
||||||
super().on_key_release(symbol, modifiers)
|
super().on_key_release(symbol, modifiers)
|
||||||
if chr(symbol) == SELECT_KEY:
|
if chr(symbol) == SELECT_KEY:
|
||||||
self.gather_new_selection()
|
self.gather_new_selection()
|
||||||
|
# self.remove(self.crosshair)
|
||||||
if chr(symbol) in GRAB_KEYS:
|
if chr(symbol) in GRAB_KEYS:
|
||||||
self.is_grabbing = False
|
self.is_grabbing = False
|
||||||
elif chr(symbol) == CURSOR_LOCATION_KEY:
|
elif chr(symbol) == INFORMATION_KEY:
|
||||||
self.remove(self.cursor_location_label)
|
self.display_information(False)
|
||||||
elif symbol == SHIFT_SYMBOL and self.window.is_key_pressed(ord(RESIZE_KEY)):
|
elif symbol == SHIFT_SYMBOL and self.window.is_key_pressed(ord(RESIZE_KEY)):
|
||||||
self.prepare_resizing(about_corner=False)
|
self.prepare_resizing(about_corner=False)
|
||||||
|
|
||||||
|
@ -447,6 +520,8 @@ class InteractiveScene(Scene):
|
||||||
self.selection.set_y(diff[1])
|
self.selection.set_y(diff[1])
|
||||||
|
|
||||||
def handle_resizing(self, point: np.ndarray):
|
def handle_resizing(self, point: np.ndarray):
|
||||||
|
if not hasattr(self, "scale_about_point"):
|
||||||
|
return
|
||||||
vect = point - self.scale_about_point
|
vect = point - self.scale_about_point
|
||||||
if self.window.is_key_pressed(CTRL_SYMBOL):
|
if self.window.is_key_pressed(CTRL_SYMBOL):
|
||||||
for i in (0, 1):
|
for i in (0, 1):
|
||||||
|
@ -485,17 +560,9 @@ class InteractiveScene(Scene):
|
||||||
self.selection.set_color(mob.get_color())
|
self.selection.set_color(mob.get_color())
|
||||||
self.remove(self.color_palette)
|
self.remove(self.color_palette)
|
||||||
|
|
||||||
def toggle_clicked_mobject_from_selection(self, point: np.ndarray):
|
|
||||||
mob = self.point_to_mobject(
|
|
||||||
point,
|
|
||||||
search_set=self.get_selection_search_set(),
|
|
||||||
buff=SMALL_BUFF
|
|
||||||
)
|
|
||||||
if mob is not None:
|
|
||||||
self.toggle_from_selection(mob)
|
|
||||||
|
|
||||||
def on_mouse_motion(self, point: np.ndarray, d_point: np.ndarray) -> None:
|
def on_mouse_motion(self, point: np.ndarray, d_point: np.ndarray) -> None:
|
||||||
super().on_mouse_motion(point, d_point)
|
super().on_mouse_motion(point, d_point)
|
||||||
|
self.crosshair.move_to(point)
|
||||||
if self.is_grabbing:
|
if self.is_grabbing:
|
||||||
self.handle_grabbing(point)
|
self.handle_grabbing(point)
|
||||||
elif self.window.is_key_pressed(ord(RESIZE_KEY)):
|
elif self.window.is_key_pressed(ord(RESIZE_KEY)):
|
||||||
|
@ -507,7 +574,13 @@ class InteractiveScene(Scene):
|
||||||
super().on_mouse_release(point, button, mods)
|
super().on_mouse_release(point, button, mods)
|
||||||
if self.color_palette in self.mobjects:
|
if self.color_palette in self.mobjects:
|
||||||
self.choose_color(point)
|
self.choose_color(point)
|
||||||
elif self.window.is_key_pressed(SHIFT_SYMBOL):
|
return
|
||||||
self.toggle_clicked_mobject_from_selection(point)
|
mobject = self.point_to_mobject(
|
||||||
|
point,
|
||||||
|
search_set=self.get_selection_search_set(),
|
||||||
|
buff=1e-4,
|
||||||
|
)
|
||||||
|
if mobject is not None:
|
||||||
|
self.toggle_from_selection(mobject)
|
||||||
else:
|
else:
|
||||||
self.clear_selection()
|
self.clear_selection()
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
import pyperclip
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
@ -11,7 +13,6 @@ import numpy as np
|
||||||
from tqdm import tqdm as ProgressDisplay
|
from tqdm import tqdm as ProgressDisplay
|
||||||
|
|
||||||
from manimlib.animation.animation import prepare_animation
|
from manimlib.animation.animation import prepare_animation
|
||||||
from manimlib.animation.transform import MoveToTarget
|
|
||||||
from manimlib.camera.camera import Camera
|
from manimlib.camera.camera import Camera
|
||||||
from manimlib.constants import ARROW_SYMBOLS
|
from manimlib.constants import ARROW_SYMBOLS
|
||||||
from manimlib.constants import DEFAULT_WAIT_TIME
|
from manimlib.constants import DEFAULT_WAIT_TIME
|
||||||
|
@ -20,6 +21,7 @@ from manimlib.constants import SHIFT_MODIFIER
|
||||||
from manimlib.event_handler import EVENT_DISPATCHER
|
from manimlib.event_handler import EVENT_DISPATCHER
|
||||||
from manimlib.event_handler.event_type import EventType
|
from manimlib.event_handler.event_type import EventType
|
||||||
from manimlib.logger import log
|
from manimlib.logger import log
|
||||||
|
from manimlib.mobject.mobject import _AnimationBuilder
|
||||||
from manimlib.mobject.mobject import Group
|
from manimlib.mobject.mobject import Group
|
||||||
from manimlib.mobject.mobject import Mobject
|
from manimlib.mobject.mobject import Mobject
|
||||||
from manimlib.mobject.mobject import Point
|
from manimlib.mobject.mobject import Point
|
||||||
|
@ -61,7 +63,7 @@ class Scene(object):
|
||||||
"leave_progress_bars": False,
|
"leave_progress_bars": False,
|
||||||
"preview": True,
|
"preview": True,
|
||||||
"presenter_mode": False,
|
"presenter_mode": False,
|
||||||
"linger_after_completion": True,
|
"show_animation_progress": False,
|
||||||
"pan_sensitivity": 3,
|
"pan_sensitivity": 3,
|
||||||
"max_num_saved_states": 50,
|
"max_num_saved_states": 50,
|
||||||
}
|
}
|
||||||
|
@ -72,7 +74,7 @@ class Scene(object):
|
||||||
from manimlib.window import Window
|
from manimlib.window import Window
|
||||||
self.window = Window(scene=self, **self.window_config)
|
self.window = Window(scene=self, **self.window_config)
|
||||||
self.camera_config["ctx"] = self.window.ctx
|
self.camera_config["ctx"] = self.window.ctx
|
||||||
self.camera_config["frame_rate"] = 30 # Where's that 30 from?
|
self.camera_config["fps"] = 30 # Where's that 30 from?
|
||||||
self.undo_stack = []
|
self.undo_stack = []
|
||||||
self.redo_stack = []
|
self.redo_stack = []
|
||||||
else:
|
else:
|
||||||
|
@ -86,14 +88,19 @@ class Scene(object):
|
||||||
self.time: float = 0
|
self.time: float = 0
|
||||||
self.skip_time: float = 0
|
self.skip_time: float = 0
|
||||||
self.original_skipping_status: bool = self.skip_animations
|
self.original_skipping_status: bool = self.skip_animations
|
||||||
|
self.checkpoint_states: dict[str, list[tuple[Mobject, Mobject]]] = dict()
|
||||||
|
|
||||||
if self.start_at_animation_number is not None:
|
if self.start_at_animation_number is not None:
|
||||||
self.skip_animations = True
|
self.skip_animations = True
|
||||||
|
if self.file_writer.has_progress_display:
|
||||||
|
self.show_animation_progress = False
|
||||||
|
|
||||||
# Items associated with interaction
|
# Items associated with interaction
|
||||||
self.mouse_point = Point()
|
self.mouse_point = Point()
|
||||||
self.mouse_drag_point = Point()
|
self.mouse_drag_point = Point()
|
||||||
self.hold_on_wait = self.presenter_mode
|
self.hold_on_wait = self.presenter_mode
|
||||||
self.inside_embed = False
|
self.inside_embed = False
|
||||||
|
self.quit_interaction = False
|
||||||
|
|
||||||
# Much nicer to work with deterministic scenes
|
# Much nicer to work with deterministic scenes
|
||||||
if self.random_seed is not None:
|
if self.random_seed is not None:
|
||||||
|
@ -111,8 +118,13 @@ class Scene(object):
|
||||||
self.setup()
|
self.setup()
|
||||||
try:
|
try:
|
||||||
self.construct()
|
self.construct()
|
||||||
except EndSceneEarlyException:
|
self.interact()
|
||||||
|
except EndScene:
|
||||||
pass
|
pass
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
# Get rid keyboard interupt symbols
|
||||||
|
print("", end="\r")
|
||||||
|
self.file_writer.ended_with_interrupt = True
|
||||||
self.tear_down()
|
self.tear_down()
|
||||||
|
|
||||||
def setup(self) -> None:
|
def setup(self) -> None:
|
||||||
|
@ -131,32 +143,31 @@ class Scene(object):
|
||||||
def tear_down(self) -> None:
|
def tear_down(self) -> None:
|
||||||
self.stop_skipping()
|
self.stop_skipping()
|
||||||
self.file_writer.finish()
|
self.file_writer.finish()
|
||||||
if self.window and self.linger_after_completion:
|
if self.window:
|
||||||
self.interact()
|
self.window.destroy()
|
||||||
|
self.window = None
|
||||||
|
|
||||||
def interact(self) -> None:
|
def interact(self) -> None:
|
||||||
# If there is a window, enter a loop
|
# If there is a window, enter a loop
|
||||||
# which updates the frame while under
|
# which updates the frame while under
|
||||||
# the hood calling the pyglet event loop
|
# the hood calling the pyglet event loop
|
||||||
|
if self.window is None:
|
||||||
|
return
|
||||||
log.info(
|
log.info(
|
||||||
"Tips: You are now in the interactive mode. Now you can use the keyboard"
|
"Tips: You are now in the interactive mode. Now you can use the keyboard"
|
||||||
" and the mouse to interact with the scene. Just press `command + q` or `esc`"
|
" and the mouse to interact with the scene. Just press `command + q` or `esc`"
|
||||||
" if you want to quit."
|
" if you want to quit."
|
||||||
)
|
)
|
||||||
self.quit_interaction = False
|
self.skip_animations = False
|
||||||
self.refresh_static_mobjects()
|
self.refresh_static_mobjects()
|
||||||
while not (self.window.is_closing or self.quit_interaction):
|
while not self.is_window_closing():
|
||||||
self.update_frame(1 / self.camera.frame_rate)
|
self.update_frame(1 / self.camera.fps)
|
||||||
if self.window.is_closing:
|
|
||||||
self.window.destroy()
|
|
||||||
|
|
||||||
def embed(self, close_scene_on_exit: bool = True) -> None:
|
def embed(self, close_scene_on_exit: bool = True) -> None:
|
||||||
if not self.preview:
|
if not self.preview:
|
||||||
# Ignore embed calls when there is no preview
|
return # Embed is only relevant with a preview
|
||||||
return
|
|
||||||
self.inside_embed = True
|
self.inside_embed = True
|
||||||
self.stop_skipping()
|
self.stop_skipping()
|
||||||
self.linger_after_completion = False
|
|
||||||
self.update_frame()
|
self.update_frame()
|
||||||
self.save_state()
|
self.save_state()
|
||||||
|
|
||||||
|
@ -175,29 +186,70 @@ class Scene(object):
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# This is useful if one wants to re-run a block of scene
|
||||||
|
# code, while developing, tweaking it each time.
|
||||||
|
# As long as the copied selection starts with a comment,
|
||||||
|
# this will revert to the state of the scene at the first
|
||||||
|
# point of running.
|
||||||
|
def checkpoint_paste(skip=False, show_progress=True):
|
||||||
|
pasted = pyperclip.paste()
|
||||||
|
line0 = pasted.lstrip().split("\n")[0]
|
||||||
|
if line0.startswith("#"):
|
||||||
|
if line0 not in self.checkpoint_states:
|
||||||
|
self.checkpoint(line0)
|
||||||
|
else:
|
||||||
|
self.revert_to_checkpoint(line0)
|
||||||
|
self.update_frame(dt=0)
|
||||||
|
if skip:
|
||||||
|
originally_skip = self.skip_animations
|
||||||
|
self.skip_animations = True
|
||||||
|
if show_progress:
|
||||||
|
originally_show_animation_progress = self.show_animation_progress
|
||||||
|
self.show_animation_progress = True
|
||||||
|
shell.run_line_magic("paste", "")
|
||||||
|
if skip:
|
||||||
|
self.skip_animations = originally_skip
|
||||||
|
if show_progress:
|
||||||
|
self.show_animation_progress = originally_show_animation_progress
|
||||||
|
|
||||||
|
local_ns['checkpoint_paste'] = checkpoint_paste
|
||||||
|
|
||||||
# Enables gui interactions during the embed
|
# Enables gui interactions during the embed
|
||||||
def inputhook(context):
|
def inputhook(context):
|
||||||
while not context.input_is_ready():
|
while not context.input_is_ready():
|
||||||
if not self.window.is_closing:
|
if not self.is_window_closing():
|
||||||
self.update_frame(dt=0)
|
self.update_frame(dt=0)
|
||||||
|
if self.is_window_closing():
|
||||||
|
shell.ask_exit()
|
||||||
|
|
||||||
pt_inputhooks.register("manim", inputhook)
|
pt_inputhooks.register("manim", inputhook)
|
||||||
shell.enable_gui("manim")
|
shell.enable_gui("manim")
|
||||||
|
|
||||||
|
# This is hacky, but there's an issue with ipython which is that
|
||||||
|
# when you define lambda's or list comprehensions during a shell session,
|
||||||
|
# they are not aware of local variables in the surrounding scope. Because
|
||||||
|
# That comes up a fair bit during scene construction, to get around this,
|
||||||
|
# we (admittedly sketchily) update the global namespace to match the local
|
||||||
|
# namespace, since this is just a shell session anyway.
|
||||||
|
shell.events.register(
|
||||||
|
"pre_run_cell",
|
||||||
|
lambda: shell.user_global_ns.update(shell.user_ns)
|
||||||
|
)
|
||||||
|
|
||||||
# Operation to run after each ipython command
|
# Operation to run after each ipython command
|
||||||
def post_cell_func(*args, **kwargs):
|
def post_cell_func():
|
||||||
self.refresh_static_mobjects()
|
self.refresh_static_mobjects()
|
||||||
|
if not self.is_window_closing():
|
||||||
|
self.update_frame(dt=0, ignore_skipping=True)
|
||||||
self.save_state()
|
self.save_state()
|
||||||
|
|
||||||
shell.events.register("post_run_cell", post_cell_func)
|
shell.events.register("post_run_cell", post_cell_func)
|
||||||
|
|
||||||
# Launch shell, with stack_depth=2 indicating we should use caller globals/locals
|
|
||||||
shell(local_ns=local_ns, stack_depth=2)
|
shell(local_ns=local_ns, stack_depth=2)
|
||||||
|
|
||||||
self.inside_embed = False
|
|
||||||
# End scene when exiting an embed
|
# End scene when exiting an embed
|
||||||
if close_scene_on_exit:
|
if close_scene_on_exit:
|
||||||
raise EndSceneEarlyException()
|
raise EndScene()
|
||||||
|
|
||||||
# Only these methods should touch the camera
|
# Only these methods should touch the camera
|
||||||
|
|
||||||
|
@ -214,6 +266,9 @@ class Scene(object):
|
||||||
if self.skip_animations and not ignore_skipping:
|
if self.skip_animations and not ignore_skipping:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self.is_window_closing():
|
||||||
|
raise EndScene()
|
||||||
|
|
||||||
if self.window:
|
if self.window:
|
||||||
self.window.clear()
|
self.window.clear()
|
||||||
self.camera.clear()
|
self.camera.clear()
|
||||||
|
@ -396,7 +451,7 @@ class Scene(object):
|
||||||
self.stop_skipping()
|
self.stop_skipping()
|
||||||
if self.end_at_animation_number is not None:
|
if self.end_at_animation_number is not None:
|
||||||
if self.num_plays >= self.end_at_animation_number:
|
if self.num_plays >= self.end_at_animation_number:
|
||||||
raise EndSceneEarlyException()
|
raise EndScene()
|
||||||
|
|
||||||
def stop_skipping(self) -> None:
|
def stop_skipping(self) -> None:
|
||||||
self.virtual_animation_start_time = self.time
|
self.virtual_animation_start_time = self.time
|
||||||
|
@ -413,14 +468,13 @@ class Scene(object):
|
||||||
) -> list[float] | np.ndarray | ProgressDisplay:
|
) -> list[float] | np.ndarray | ProgressDisplay:
|
||||||
if self.skip_animations and not override_skip_animations:
|
if self.skip_animations and not override_skip_animations:
|
||||||
return [run_time]
|
return [run_time]
|
||||||
else:
|
|
||||||
step = 1 / self.camera.frame_rate
|
times = np.arange(0, run_time, 1 / self.camera.fps)
|
||||||
times = np.arange(0, run_time, step)
|
|
||||||
|
|
||||||
if self.file_writer.has_progress_display:
|
if self.file_writer.has_progress_display:
|
||||||
self.file_writer.set_progress_display_subdescription(desc)
|
self.file_writer.set_progress_display_subdescription(desc)
|
||||||
return times
|
|
||||||
|
|
||||||
|
if self.show_animation_progress:
|
||||||
return ProgressDisplay(
|
return ProgressDisplay(
|
||||||
times,
|
times,
|
||||||
total=n_iterations,
|
total=n_iterations,
|
||||||
|
@ -428,9 +482,11 @@ class Scene(object):
|
||||||
ascii=True if platform.system() == 'Windows' else None,
|
ascii=True if platform.system() == 'Windows' else None,
|
||||||
desc=desc,
|
desc=desc,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
return times
|
||||||
|
|
||||||
def get_run_time(self, animations: Iterable[Animation]) -> float:
|
def get_run_time(self, animations: Iterable[Animation]) -> float:
|
||||||
return np.max([animation.run_time for animation in animations])
|
return np.max([animation.get_run_time() for animation in animations])
|
||||||
|
|
||||||
def get_animation_time_progression(
|
def get_animation_time_progression(
|
||||||
self,
|
self,
|
||||||
|
@ -454,74 +510,16 @@ class Scene(object):
|
||||||
kw["override_skip_animations"] = True
|
kw["override_skip_animations"] = True
|
||||||
return self.get_time_progression(duration, **kw)
|
return self.get_time_progression(duration, **kw)
|
||||||
|
|
||||||
def anims_from_play_args(self, *args, **kwargs) -> list[Animation]:
|
def prepare_animations(
|
||||||
"""
|
self,
|
||||||
Each arg can either be an animation, or a mobject method
|
proto_animations: list[Animation | _AnimationBuilder],
|
||||||
followed by that methods arguments (and potentially follow
|
animation_config: dict,
|
||||||
by a dict of kwargs for that method).
|
):
|
||||||
This animation list is built by going through the args list,
|
animations = list(map(prepare_animation, proto_animations))
|
||||||
and each animation is simply added, but when a mobject method
|
for anim in animations:
|
||||||
s hit, a MoveToTarget animation is built using the args that
|
|
||||||
follow up until either another animation is hit, another method
|
|
||||||
is hit, or the args list runs out.
|
|
||||||
"""
|
|
||||||
animations = []
|
|
||||||
state = {
|
|
||||||
"curr_method": None,
|
|
||||||
"last_method": None,
|
|
||||||
"method_args": [],
|
|
||||||
}
|
|
||||||
|
|
||||||
def compile_method(state):
|
|
||||||
if state["curr_method"] is None:
|
|
||||||
return
|
|
||||||
mobject = state["curr_method"].__self__
|
|
||||||
if state["last_method"] and state["last_method"].__self__ is mobject:
|
|
||||||
animations.pop()
|
|
||||||
# method should already have target then.
|
|
||||||
else:
|
|
||||||
mobject.generate_target()
|
|
||||||
#
|
|
||||||
if len(state["method_args"]) > 0 and isinstance(state["method_args"][-1], dict):
|
|
||||||
method_kwargs = state["method_args"].pop()
|
|
||||||
else:
|
|
||||||
method_kwargs = {}
|
|
||||||
state["curr_method"].__func__(
|
|
||||||
mobject.target,
|
|
||||||
*state["method_args"],
|
|
||||||
**method_kwargs
|
|
||||||
)
|
|
||||||
animations.append(MoveToTarget(mobject))
|
|
||||||
state["last_method"] = state["curr_method"]
|
|
||||||
state["curr_method"] = None
|
|
||||||
state["method_args"] = []
|
|
||||||
|
|
||||||
for arg in args:
|
|
||||||
if inspect.ismethod(arg):
|
|
||||||
compile_method(state)
|
|
||||||
state["curr_method"] = arg
|
|
||||||
elif state["curr_method"] is not None:
|
|
||||||
state["method_args"].append(arg)
|
|
||||||
elif isinstance(arg, Mobject):
|
|
||||||
raise Exception("""
|
|
||||||
I think you may have invoked a method
|
|
||||||
you meant to pass in as a Scene.play argument
|
|
||||||
""")
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
anim = prepare_animation(arg)
|
|
||||||
except TypeError:
|
|
||||||
raise TypeError(f"Unexpected argument {arg} passed to Scene.play()")
|
|
||||||
|
|
||||||
compile_method(state)
|
|
||||||
animations.append(anim)
|
|
||||||
compile_method(state)
|
|
||||||
|
|
||||||
for animation in animations:
|
|
||||||
# This is where kwargs to play like run_time and rate_func
|
# This is where kwargs to play like run_time and rate_func
|
||||||
# get applied to all animations
|
# get applied to all animations
|
||||||
animation.update_config(**kwargs)
|
anim.update_config(**animation_config)
|
||||||
|
|
||||||
return animations
|
return animations
|
||||||
|
|
||||||
def handle_play_like_call(func):
|
def handle_play_like_call(func):
|
||||||
|
@ -529,6 +527,8 @@ class Scene(object):
|
||||||
def wrapper(self, *args, **kwargs):
|
def wrapper(self, *args, **kwargs):
|
||||||
if self.inside_embed:
|
if self.inside_embed:
|
||||||
self.save_state()
|
self.save_state()
|
||||||
|
if self.presenter_mode and self.num_plays == 0:
|
||||||
|
self.hold_loop()
|
||||||
|
|
||||||
self.update_skipping_status()
|
self.update_skipping_status()
|
||||||
should_write = not self.skip_animations
|
should_write = not self.skip_animations
|
||||||
|
@ -548,6 +548,10 @@ class Scene(object):
|
||||||
if self.inside_embed:
|
if self.inside_embed:
|
||||||
self.save_state()
|
self.save_state()
|
||||||
|
|
||||||
|
if self.skip_animations and self.window is not None:
|
||||||
|
# Show some quick frames along the way
|
||||||
|
self.update_frame(dt=0, ignore_skipping=True)
|
||||||
|
|
||||||
self.num_plays += 1
|
self.num_plays += 1
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
@ -587,11 +591,11 @@ class Scene(object):
|
||||||
self.update_mobjects(0)
|
self.update_mobjects(0)
|
||||||
|
|
||||||
@handle_play_like_call
|
@handle_play_like_call
|
||||||
def play(self, *args, **kwargs) -> None:
|
def play(self, *proto_animations, **animation_config) -> None:
|
||||||
if len(args) == 0:
|
if len(proto_animations) == 0:
|
||||||
log.warning("Called Scene.play with no animations")
|
log.warning("Called Scene.play with no animations")
|
||||||
return
|
return
|
||||||
animations = self.anims_from_play_args(*args, **kwargs)
|
animations = self.prepare_animations(proto_animations, animation_config)
|
||||||
self.begin_animations(animations)
|
self.begin_animations(animations)
|
||||||
self.progress_through_animations(animations)
|
self.progress_through_animations(animations)
|
||||||
self.finish_animations(animations)
|
self.finish_animations(animations)
|
||||||
|
@ -608,9 +612,7 @@ class Scene(object):
|
||||||
if self.presenter_mode and not self.skip_animations and not ignore_presenter_mode:
|
if self.presenter_mode and not self.skip_animations and not ignore_presenter_mode:
|
||||||
if note:
|
if note:
|
||||||
log.info(note)
|
log.info(note)
|
||||||
while self.hold_on_wait:
|
self.hold_loop()
|
||||||
self.update_frame(dt=1 / self.camera.frame_rate)
|
|
||||||
self.hold_on_wait = True
|
|
||||||
else:
|
else:
|
||||||
time_progression = self.get_wait_time_progression(duration, stop_condition)
|
time_progression = self.get_wait_time_progression(duration, stop_condition)
|
||||||
last_t = 0
|
last_t = 0
|
||||||
|
@ -624,6 +626,11 @@ class Scene(object):
|
||||||
self.refresh_static_mobjects()
|
self.refresh_static_mobjects()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def hold_loop(self):
|
||||||
|
while self.hold_on_wait:
|
||||||
|
self.update_frame(dt=1 / self.camera.fps)
|
||||||
|
self.hold_on_wait = True
|
||||||
|
|
||||||
def wait_until(
|
def wait_until(
|
||||||
self,
|
self,
|
||||||
stop_condition: Callable[[], bool],
|
stop_condition: Callable[[], bool],
|
||||||
|
@ -655,50 +662,52 @@ class Scene(object):
|
||||||
|
|
||||||
# Helpers for interactive development
|
# Helpers for interactive development
|
||||||
|
|
||||||
def get_state(self) -> tuple[list[tuple[Mobject, Mobject]], int]:
|
def get_state(self) -> SceneState:
|
||||||
if self.undo_stack:
|
return SceneState(self)
|
||||||
last_state = dict(self.undo_stack[-1])
|
|
||||||
else:
|
|
||||||
last_state = {}
|
|
||||||
result = []
|
|
||||||
n_changes = 0
|
|
||||||
for mob in self.mobjects:
|
|
||||||
# If it hasn't changed since the last state, just point to the
|
|
||||||
# same copy as before
|
|
||||||
if mob in last_state and last_state[mob].looks_identical(mob):
|
|
||||||
result.append((mob, last_state[mob]))
|
|
||||||
else:
|
|
||||||
result.append((mob, mob.copy()))
|
|
||||||
n_changes += 1
|
|
||||||
return result, n_changes
|
|
||||||
|
|
||||||
def restore_state(self, mobject_states: list[tuple[Mobject, Mobject]]):
|
def restore_state(self, scene_state: SceneState):
|
||||||
self.mobjects = [mob.become(mob_copy) for mob, mob_copy in mobject_states]
|
scene_state.restore_scene(self)
|
||||||
|
|
||||||
def save_state(self) -> None:
|
def save_state(self) -> None:
|
||||||
if not self.preview:
|
if not self.preview:
|
||||||
return
|
return
|
||||||
|
state = self.get_state()
|
||||||
|
if self.undo_stack and state.mobjects_match(self.undo_stack[-1]):
|
||||||
|
return
|
||||||
self.redo_stack = []
|
self.redo_stack = []
|
||||||
state, n_changes = self.get_state()
|
|
||||||
if n_changes > 0:
|
|
||||||
self.undo_stack.append(state)
|
self.undo_stack.append(state)
|
||||||
if len(self.undo_stack) > self.max_num_saved_states:
|
if len(self.undo_stack) > self.max_num_saved_states:
|
||||||
self.undo_stack.pop(0)
|
self.undo_stack.pop(0)
|
||||||
|
|
||||||
def undo(self):
|
def undo(self):
|
||||||
if self.undo_stack:
|
if self.undo_stack:
|
||||||
state, n_changes = self.get_state()
|
self.redo_stack.append(self.get_state())
|
||||||
self.redo_stack.append(state)
|
|
||||||
self.restore_state(self.undo_stack.pop())
|
self.restore_state(self.undo_stack.pop())
|
||||||
self.refresh_static_mobjects()
|
self.refresh_static_mobjects()
|
||||||
|
|
||||||
def redo(self):
|
def redo(self):
|
||||||
if self.redo_stack:
|
if self.redo_stack:
|
||||||
state, n_changes = self.get_state()
|
self.undo_stack.append(self.get_state())
|
||||||
self.undo_stack.append(state)
|
|
||||||
self.restore_state(self.redo_stack.pop())
|
self.restore_state(self.redo_stack.pop())
|
||||||
self.refresh_static_mobjects()
|
self.refresh_static_mobjects()
|
||||||
|
|
||||||
|
def checkpoint(self, key: str):
|
||||||
|
self.checkpoint_states[key] = self.get_state()
|
||||||
|
|
||||||
|
def revert_to_checkpoint(self, key: str):
|
||||||
|
if key not in self.checkpoint_states:
|
||||||
|
log.error(f"No checkpoint at {key}")
|
||||||
|
return
|
||||||
|
all_keys = list(self.checkpoint_states.keys())
|
||||||
|
index = all_keys.index(key)
|
||||||
|
for later_key in all_keys[index + 1:]:
|
||||||
|
self.checkpoint_states.pop(later_key)
|
||||||
|
|
||||||
|
self.restore_state(self.checkpoint_states[key])
|
||||||
|
|
||||||
|
def clear_checkpoints(self):
|
||||||
|
self.checkpoint_states = dict()
|
||||||
|
|
||||||
def save_mobject_to_file(self, mobject: Mobject, file_path: str | None = None) -> None:
|
def save_mobject_to_file(self, mobject: Mobject, file_path: str | None = None) -> None:
|
||||||
if file_path is None:
|
if file_path is None:
|
||||||
file_path = self.file_writer.get_saved_mobject_path(mobject)
|
file_path = self.file_writer.get_saved_mobject_path(mobject)
|
||||||
|
@ -714,6 +723,9 @@ class Scene(object):
|
||||||
path = os.path.join(directory, file_name)
|
path = os.path.join(directory, file_name)
|
||||||
return Mobject.load(path)
|
return Mobject.load(path)
|
||||||
|
|
||||||
|
def is_window_closing(self):
|
||||||
|
return self.window and (self.window.is_closing or self.quit_interaction)
|
||||||
|
|
||||||
# Event handling
|
# Event handling
|
||||||
|
|
||||||
def on_mouse_motion(
|
def on_mouse_motion(
|
||||||
|
@ -850,5 +862,49 @@ class Scene(object):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class EndSceneEarlyException(Exception):
|
class SceneState():
|
||||||
|
def __init__(self, scene: Scene, ignore: list[Mobject] | None = None):
|
||||||
|
self.time = scene.time
|
||||||
|
self.num_plays = scene.num_plays
|
||||||
|
self.mobjects_to_copies = OrderedDict.fromkeys(scene.mobjects)
|
||||||
|
if ignore:
|
||||||
|
for mob in ignore:
|
||||||
|
self.mobjects_to_copies.pop(mob, None)
|
||||||
|
|
||||||
|
last_m2c = scene.undo_stack[-1].mobjects_to_copies if scene.undo_stack else dict()
|
||||||
|
for mob in self.mobjects_to_copies:
|
||||||
|
# If it hasn't changed since the last state, just point to the
|
||||||
|
# same copy as before
|
||||||
|
if mob in last_m2c and last_m2c[mob].looks_identical(mob):
|
||||||
|
self.mobjects_to_copies[mob] = last_m2c[mob]
|
||||||
|
else:
|
||||||
|
self.mobjects_to_copies[mob] = mob.copy()
|
||||||
|
|
||||||
|
def __eq__(self, state: SceneState):
|
||||||
|
return all((
|
||||||
|
self.time == state.time,
|
||||||
|
self.num_plays == state.num_plays,
|
||||||
|
self.mobjects_to_copies == state.mobjects_to_copies
|
||||||
|
))
|
||||||
|
|
||||||
|
def mobjects_match(self, state: SceneState):
|
||||||
|
return self.mobjects_to_copies == state.mobjects_to_copies
|
||||||
|
|
||||||
|
def n_changes(self, state: SceneState):
|
||||||
|
m2c = state.mobjects_to_copies
|
||||||
|
return sum(
|
||||||
|
1 - int(mob in m2c and mob.looks_identical(m2c[mob]))
|
||||||
|
for mob in self.mobjects_to_copies
|
||||||
|
)
|
||||||
|
|
||||||
|
def restore_scene(self, scene: Scene):
|
||||||
|
scene.time = self.time
|
||||||
|
scene.num_plays = self.num_plays
|
||||||
|
scene.mobjects = [
|
||||||
|
mob.become(mob_copy)
|
||||||
|
for mob, mob_copy in self.mobjects_to_copies.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class EndScene(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -37,9 +37,6 @@ class SceneFileWriter(object):
|
||||||
"png_mode": "RGBA",
|
"png_mode": "RGBA",
|
||||||
"save_last_frame": False,
|
"save_last_frame": False,
|
||||||
"movie_file_extension": ".mp4",
|
"movie_file_extension": ".mp4",
|
||||||
# Should the path of output files mirror the directory
|
|
||||||
# structure of the module holding the scene?
|
|
||||||
"mirror_module_path": False,
|
|
||||||
# What python file is generating this scene
|
# What python file is generating this scene
|
||||||
"input_file_path": "",
|
"input_file_path": "",
|
||||||
# Where should this be written
|
# Where should this be written
|
||||||
|
@ -57,16 +54,13 @@ class SceneFileWriter(object):
|
||||||
self.scene: Scene = scene
|
self.scene: Scene = scene
|
||||||
self.writing_process: sp.Popen | None = None
|
self.writing_process: sp.Popen | None = None
|
||||||
self.has_progress_display: bool = False
|
self.has_progress_display: bool = False
|
||||||
|
self.ended_with_interrupt: bool = False
|
||||||
self.init_output_directories()
|
self.init_output_directories()
|
||||||
self.init_audio()
|
self.init_audio()
|
||||||
|
|
||||||
# Output directories and files
|
# Output directories and files
|
||||||
def init_output_directories(self) -> None:
|
def init_output_directories(self) -> None:
|
||||||
out_dir = self.output_directory or ""
|
out_dir = self.output_directory or ""
|
||||||
if self.mirror_module_path:
|
|
||||||
module_dir = self.get_default_module_directory()
|
|
||||||
out_dir = os.path.join(out_dir, module_dir)
|
|
||||||
|
|
||||||
scene_name = self.file_name or self.get_default_scene_name()
|
scene_name = self.file_name or self.get_default_scene_name()
|
||||||
if self.save_last_frame:
|
if self.save_last_frame:
|
||||||
image_dir = guarantee_existence(os.path.join(out_dir, "images"))
|
image_dir = guarantee_existence(os.path.join(out_dir, "images"))
|
||||||
|
@ -81,7 +75,9 @@ class SceneFileWriter(object):
|
||||||
movie_dir, "partial_movie_files", scene_name,
|
movie_dir, "partial_movie_files", scene_name,
|
||||||
))
|
))
|
||||||
# A place to save mobjects
|
# A place to save mobjects
|
||||||
self.saved_mobject_directory = os.path.join(out_dir, "mobjects")
|
self.saved_mobject_directory = os.path.join(
|
||||||
|
out_dir, "mobjects", str(self.scene)
|
||||||
|
)
|
||||||
|
|
||||||
def get_default_module_directory(self) -> str:
|
def get_default_module_directory(self) -> str:
|
||||||
path, _ = os.path.splitext(self.input_file_path)
|
path, _ = os.path.splitext(self.input_file_path)
|
||||||
|
@ -101,9 +97,9 @@ class SceneFileWriter(object):
|
||||||
|
|
||||||
def get_resolution_directory(self) -> str:
|
def get_resolution_directory(self) -> str:
|
||||||
pixel_height = self.scene.camera.pixel_height
|
pixel_height = self.scene.camera.pixel_height
|
||||||
frame_rate = self.scene.camera.frame_rate
|
fps = self.scene.camera.fps
|
||||||
return "{}p{}".format(
|
return "{}p{}".format(
|
||||||
pixel_height, frame_rate
|
pixel_height, fps
|
||||||
)
|
)
|
||||||
|
|
||||||
# Directory getters
|
# Directory getters
|
||||||
|
@ -124,10 +120,7 @@ class SceneFileWriter(object):
|
||||||
return self.movie_file_path
|
return self.movie_file_path
|
||||||
|
|
||||||
def get_saved_mobject_directory(self) -> str:
|
def get_saved_mobject_directory(self) -> str:
|
||||||
return guarantee_existence(os.path.join(
|
return guarantee_existence(self.saved_mobject_directory)
|
||||||
self.saved_mobject_directory,
|
|
||||||
str(self.scene),
|
|
||||||
))
|
|
||||||
|
|
||||||
def get_saved_mobject_path(self, mobject: Mobject) -> str | None:
|
def get_saved_mobject_path(self, mobject: Mobject) -> str | None:
|
||||||
directory = self.get_saved_mobject_directory()
|
directory = self.get_saved_mobject_directory()
|
||||||
|
@ -241,7 +234,7 @@ class SceneFileWriter(object):
|
||||||
self.final_file_path = file_path
|
self.final_file_path = file_path
|
||||||
self.temp_file_path = stem + "_temp" + ext
|
self.temp_file_path = stem + "_temp" + ext
|
||||||
|
|
||||||
fps = self.scene.camera.frame_rate
|
fps = self.scene.camera.fps
|
||||||
width, height = self.scene.camera.get_pixel_shape()
|
width, height = self.scene.camera.get_pixel_shape()
|
||||||
|
|
||||||
command = [
|
command = [
|
||||||
|
@ -305,7 +298,11 @@ class SceneFileWriter(object):
|
||||||
self.writing_process.terminate()
|
self.writing_process.terminate()
|
||||||
if self.has_progress_display:
|
if self.has_progress_display:
|
||||||
self.progress_display.close()
|
self.progress_display.close()
|
||||||
|
|
||||||
|
if not self.ended_with_interrupt:
|
||||||
shutil.move(self.temp_file_path, self.final_file_path)
|
shutil.move(self.temp_file_path, self.final_file_path)
|
||||||
|
else:
|
||||||
|
self.movie_file_path = self.temp_file_path
|
||||||
|
|
||||||
def combine_movie_files(self) -> None:
|
def combine_movie_files(self) -> None:
|
||||||
kwargs = {
|
kwargs = {
|
||||||
|
|
|
@ -57,25 +57,14 @@ def init_customization() -> None:
|
||||||
"window_monitor": 0,
|
"window_monitor": 0,
|
||||||
"full_screen": False,
|
"full_screen": False,
|
||||||
"break_into_partial_movies": False,
|
"break_into_partial_movies": False,
|
||||||
"camera_qualities": {
|
"camera_resolutions": {
|
||||||
"low": {
|
"low": "854x480",
|
||||||
"resolution": "854x480",
|
"medium": "1280x720",
|
||||||
"frame_rate": 15,
|
"high": "1920x1080",
|
||||||
|
"4k": "3840x2160",
|
||||||
|
"default_resolution": "high",
|
||||||
},
|
},
|
||||||
"medium": {
|
"fps": 30,
|
||||||
"resolution": "1280x720",
|
|
||||||
"frame_rate": 30,
|
|
||||||
},
|
|
||||||
"high": {
|
|
||||||
"resolution": "1920x1080",
|
|
||||||
"frame_rate": 60,
|
|
||||||
},
|
|
||||||
"ultra_high": {
|
|
||||||
"resolution": "3840x2160",
|
|
||||||
"frame_rate": 60,
|
|
||||||
},
|
|
||||||
"default_quality": "",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
|
@ -13,19 +13,12 @@ if TYPE_CHECKING:
|
||||||
S = TypeVar("S")
|
S = TypeVar("S")
|
||||||
|
|
||||||
|
|
||||||
def remove_list_redundancies(l: Sequence[T]) -> list[T]:
|
def remove_list_redundancies(lst: Sequence[T]) -> list[T]:
|
||||||
"""
|
"""
|
||||||
Used instead of list(set(l)) to maintain order
|
Used instead of list(set(l)) to maintain order
|
||||||
Keeps the last occurrence of each element
|
Keeps the last occurrence of each element
|
||||||
"""
|
"""
|
||||||
reversed_result = []
|
return list(reversed(dict.fromkeys(reversed(lst))))
|
||||||
used = set()
|
|
||||||
for x in reversed(l):
|
|
||||||
if x not in used:
|
|
||||||
reversed_result.append(x)
|
|
||||||
used.add(x)
|
|
||||||
reversed_result.reverse()
|
|
||||||
return reversed_result
|
|
||||||
|
|
||||||
|
|
||||||
def list_update(l1: Iterable[T], l2: Iterable[T]) -> list[T]:
|
def list_update(l1: Iterable[T], l2: Iterable[T]) -> list[T]:
|
||||||
|
@ -33,7 +26,7 @@ def list_update(l1: Iterable[T], l2: Iterable[T]) -> list[T]:
|
||||||
Used instead of list(set(l1).update(l2)) to maintain order,
|
Used instead of list(set(l1).update(l2)) to maintain order,
|
||||||
making sure duplicates are removed from l1, not l2.
|
making sure duplicates are removed from l1, not l2.
|
||||||
"""
|
"""
|
||||||
return [e for e in l1 if e not in l2] + list(l2)
|
return remove_list_redundancies([*l1, *l2])
|
||||||
|
|
||||||
|
|
||||||
def list_difference_update(l1: Iterable[T], l2: Iterable[T]) -> list[T]:
|
def list_difference_update(l1: Iterable[T], l2: Iterable[T]) -> list[T]:
|
||||||
|
|
|
@ -90,11 +90,13 @@ def tex_to_dvi(tex_file: str) -> str:
|
||||||
if exit_code != 0:
|
if exit_code != 0:
|
||||||
log_file = tex_file.replace(".tex", ".log")
|
log_file = tex_file.replace(".tex", ".log")
|
||||||
log.error("LaTeX Error! Not a worry, it happens to the best of us.")
|
log.error("LaTeX Error! Not a worry, it happens to the best of us.")
|
||||||
|
error_str = ""
|
||||||
with open(log_file, "r", encoding="utf-8") as file:
|
with open(log_file, "r", encoding="utf-8") as file:
|
||||||
for line in file.readlines():
|
for line in file.readlines():
|
||||||
if line.startswith("!"):
|
if line.startswith("!"):
|
||||||
log.debug(f"The error could be: `{line[2:-1]}`")
|
error_str = line[2:-1]
|
||||||
raise LatexError()
|
log.debug(f"The error could be: `{error_str}`")
|
||||||
|
raise LatexError(error_str)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue