mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +00:00
Resolve conflicts
This commit is contained in:
commit
59eba943e5
24 changed files with 529 additions and 362 deletions
|
@ -18,7 +18,7 @@ Frame and pixel shape
|
|||
|
||||
DEFAULT_PIXEL_HEIGHT = 1080
|
||||
DEFAULT_PIXEL_WIDTH = 1920
|
||||
DEFAULT_FRAME_RATE = 30
|
||||
DEFAULT_FPS = 30
|
||||
|
||||
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.
|
||||
``--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"
|
||||
``--frame_rate FRAME_RATE`` Frame rate, as an integer
|
||||
``--fps FPS`` Frame rate, as an integer
|
||||
``--color COLOR`` ``-c`` Background color
|
||||
``--leave_progress_bars`` Leave progress bars displayed in terminal
|
||||
``--video_dir VIDEO_DIR`` Directory to write video
|
||||
|
|
|
@ -77,10 +77,6 @@ class AnimatingMethods(Scene):
|
|||
# ".animate" syntax:
|
||||
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
|
||||
# state and whatever happens when you apply that method.
|
||||
# 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.utils.config_ops import digest_config
|
||||
from manimlib.utils.rate_functions import smooth
|
||||
from manimlib.utils.rate_functions import squish_rate_func
|
||||
from manimlib.utils.simple_functions import clip
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
@ -23,6 +24,7 @@ DEFAULT_ANIMATION_LAG_RATIO = 0
|
|||
class Animation(object):
|
||||
CONFIG = {
|
||||
"run_time": DEFAULT_ANIMATION_RUN_TIME,
|
||||
"time_span": None, # Tuple of times, between which the animation will run
|
||||
"rate_func": smooth,
|
||||
"name": None,
|
||||
# Does this animation add or remove a mobject form the screen
|
||||
|
@ -53,6 +55,12 @@ class Animation(object):
|
|||
# played. As much initialization as possible,
|
||||
# especially any mobject copying, should live in
|
||||
# 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.starting_mobject = self.create_starting_mobject()
|
||||
if self.suspend_mobject_updating:
|
||||
|
@ -166,6 +174,8 @@ class Animation(object):
|
|||
return self
|
||||
|
||||
def get_run_time(self) -> float:
|
||||
if self.time_span:
|
||||
return max(self.run_time, self.time_span[1])
|
||||
return self.run_time
|
||||
|
||||
def set_rate_func(self, rate_func: Callable[[float], float]):
|
||||
|
|
|
@ -68,10 +68,11 @@ class Transform(Animation):
|
|||
self.target_copy = self.target_mobject.copy()
|
||||
self.mobject.align_data_and_family(self.target_copy)
|
||||
super().begin()
|
||||
self.mobject.lock_matching_data(
|
||||
self.starting_mobject,
|
||||
self.target_copy,
|
||||
)
|
||||
if not self.mobject.has_updaters:
|
||||
self.mobject.lock_matching_data(
|
||||
self.starting_mobject,
|
||||
self.target_copy,
|
||||
)
|
||||
|
||||
def finish(self) -> None:
|
||||
super().finish()
|
||||
|
|
|
@ -11,7 +11,7 @@ from scipy.spatial.transform import Rotation
|
|||
|
||||
from manimlib.constants import BLACK
|
||||
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 FRAME_HEIGHT, FRAME_WIDTH
|
||||
from manimlib.constants import DOWN, LEFT, ORIGIN, OUT, RIGHT, UP
|
||||
|
@ -170,7 +170,7 @@ class Camera(object):
|
|||
"frame_config": {},
|
||||
"pixel_width": DEFAULT_PIXEL_WIDTH,
|
||||
"pixel_height": DEFAULT_PIXEL_HEIGHT,
|
||||
"frame_rate": DEFAULT_FRAME_RATE,
|
||||
"fps": DEFAULT_FPS,
|
||||
# Note: frame height and width will be resized to match
|
||||
# the pixel aspect ratio
|
||||
"background_color": BLACK,
|
||||
|
|
|
@ -136,7 +136,7 @@ def parse_cli():
|
|||
help="Resolution, passed as \"WxH\", e.g. \"1920x1080\"",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--frame_rate",
|
||||
"--fps",
|
||||
help="Frame rate, as an integer",
|
||||
)
|
||||
parser.add_argument(
|
||||
|
@ -148,6 +148,11 @@ def parse_cli():
|
|||
action="store_true",
|
||||
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(
|
||||
"--video_dir",
|
||||
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:
|
||||
prev_line_num = index - 2
|
||||
break
|
||||
if prev_line_num is None:
|
||||
prev_line_num = len(lines) - 2
|
||||
elif line_marker.isdigit():
|
||||
# Treat the argument as a line number
|
||||
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:
|
||||
prev_line_num = next(
|
||||
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]
|
||||
)
|
||||
except StopIteration:
|
||||
|
@ -330,6 +337,16 @@ def get_configuration(args):
|
|||
else:
|
||||
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 = {
|
||||
"write_to_movie": not args.skip_animations and write_file,
|
||||
"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
|
||||
"png_mode": "RGBA" if args.transparent else "RGB",
|
||||
"movie_file_extension": file_ext,
|
||||
"mirror_module_path": custom_config["directories"]["mirror_module_path"],
|
||||
"output_directory": args.video_dir or custom_config["directories"]["output"],
|
||||
"output_directory": output_directory,
|
||||
"file_name": args.file_name,
|
||||
"input_file_path": args.file or "",
|
||||
"open_file_upon_completion": args.open,
|
||||
|
@ -365,6 +381,7 @@ def get_configuration(args):
|
|||
"preview": not write_file,
|
||||
"presenter_mode": args.presenter_mode,
|
||||
"leave_progress_bars": args.leave_progress_bars,
|
||||
"show_animation_progress": args.show_animation_progress,
|
||||
}
|
||||
|
||||
# Camera configuration
|
||||
|
@ -398,31 +415,31 @@ def get_configuration(args):
|
|||
|
||||
def get_camera_configuration(args, custom_config):
|
||||
camera_config = {}
|
||||
camera_qualities = get_custom_config()["camera_qualities"]
|
||||
camera_resolutions = get_custom_config()["camera_resolutions"]
|
||||
if args.low_quality:
|
||||
quality = camera_qualities["low"]
|
||||
resolution = camera_resolutions["low"]
|
||||
elif args.medium_quality:
|
||||
quality = camera_qualities["medium"]
|
||||
resolution = camera_resolutions["med"]
|
||||
elif args.hd:
|
||||
quality = camera_qualities["high"]
|
||||
resolution = camera_resolutions["high"]
|
||||
elif args.uhd:
|
||||
quality = camera_qualities["ultra_high"]
|
||||
resolution = camera_resolutions["4k"]
|
||||
else:
|
||||
quality = camera_qualities[camera_qualities["default_quality"]]
|
||||
resolution = camera_resolutions[camera_resolutions["default_resolution"]]
|
||||
|
||||
if args.resolution:
|
||||
quality["resolution"] = args.resolution
|
||||
if args.frame_rate:
|
||||
quality["frame_rate"] = int(args.frame_rate)
|
||||
if args.fps:
|
||||
fps = int(args.fps)
|
||||
else:
|
||||
fps = get_custom_config()["fps"]
|
||||
|
||||
width_str, height_str = quality["resolution"].split("x")
|
||||
width_str, height_str = resolution.split("x")
|
||||
width = int(width_str)
|
||||
height = int(height_str)
|
||||
|
||||
camera_config.update({
|
||||
"pixel_width": width,
|
||||
"pixel_height": height,
|
||||
"frame_rate": quality["frame_rate"],
|
||||
"fps": fps,
|
||||
})
|
||||
|
||||
try:
|
||||
|
|
|
@ -10,7 +10,7 @@ FRAME_X_RADIUS = FRAME_WIDTH / 2
|
|||
|
||||
DEFAULT_PIXEL_HEIGHT = 1080
|
||||
DEFAULT_PIXEL_WIDTH = 1920
|
||||
DEFAULT_FRAME_RATE = 30
|
||||
DEFAULT_FPS = 30
|
||||
|
||||
SMALL_BUFF = 0.1
|
||||
MED_SMALL_BUFF = 0.25
|
||||
|
|
|
@ -20,6 +20,7 @@ universal_import_line: "from manimlib import *"
|
|||
style:
|
||||
tex_font: "default"
|
||||
font: "Consolas"
|
||||
text_alignment: "LEFT"
|
||||
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
|
||||
|
@ -34,17 +35,10 @@ full_screen: False
|
|||
# easier when working with the broken up scene, which
|
||||
# effectively has cuts at all the places you might want.
|
||||
break_into_partial_movies: False
|
||||
camera_qualities:
|
||||
low:
|
||||
resolution: "854x480"
|
||||
frame_rate: 15
|
||||
medium:
|
||||
resolution: "1280x720"
|
||||
frame_rate: 30
|
||||
high:
|
||||
resolution: "1920x1080"
|
||||
frame_rate: 30
|
||||
ultra_high:
|
||||
resolution: "3840x2160"
|
||||
frame_rate: 60
|
||||
default_quality: "high"
|
||||
camera_resolutions:
|
||||
low: "854x480"
|
||||
med: "1280x720"
|
||||
high: "1920x1080"
|
||||
4k: "3840x2160"
|
||||
default_resolution: "high"
|
||||
fps: 30
|
|
@ -64,6 +64,7 @@ def get_scene_config(config):
|
|||
"start_at_animation_number",
|
||||
"end_at_animation_number",
|
||||
"leave_progress_bars",
|
||||
"show_animation_progress",
|
||||
"preview",
|
||||
"presenter_mode",
|
||||
]
|
||||
|
@ -87,7 +88,7 @@ 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"]["frame_rate"])
|
||||
return int(total_time * scene_config["camera_config"]["fps"])
|
||||
|
||||
|
||||
def get_scenes_to_render(scene_classes, scene_config, config):
|
||||
|
|
|
@ -75,6 +75,7 @@ class ParametricCurve(VMobject):
|
|||
if hasattr(self, "x_range"):
|
||||
return self.x_range
|
||||
|
||||
|
||||
class FunctionGraph(ParametricCurve):
|
||||
CONFIG = {
|
||||
"color": YELLOW,
|
||||
|
|
|
@ -821,7 +821,7 @@ class FillArrow(Line):
|
|||
|
||||
def reset_points_around_ends(self):
|
||||
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
|
||||
|
||||
|
|
|
@ -286,7 +286,7 @@ class Mobject(object):
|
|||
def are_points_touching(
|
||||
self,
|
||||
points: np.ndarray,
|
||||
buff: float = MED_SMALL_BUFF
|
||||
buff: float = 0
|
||||
) -> bool:
|
||||
bb = self.get_bounding_box()
|
||||
mins = (bb[0] - buff)
|
||||
|
@ -296,7 +296,7 @@ class Mobject(object):
|
|||
def is_point_touching(
|
||||
self,
|
||||
point: np.ndarray,
|
||||
buff: float = MED_SMALL_BUFF
|
||||
buff: float = 0
|
||||
) -> bool:
|
||||
return self.are_points_touching(np.array(point, ndmin=2), buff)[0]
|
||||
|
||||
|
@ -359,8 +359,9 @@ class Mobject(object):
|
|||
if p not in excluded:
|
||||
ancestors.append(p)
|
||||
to_process.append(p)
|
||||
# Remove redundancies while preserving order
|
||||
# Ensure mobjects highest in the hierarchy show up first
|
||||
ancestors.reverse()
|
||||
# Remove list redundancies while preserving order
|
||||
return list(dict.fromkeys(ancestors))
|
||||
|
||||
def add(self, *mobjects: Mobject):
|
||||
|
@ -475,6 +476,31 @@ class Mobject(object):
|
|||
self.center()
|
||||
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(
|
||||
self,
|
||||
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.render_primitive = sm2.render_primitive
|
||||
self.refresh_bounding_box(recurse_down=True)
|
||||
self.match_updaters(mobject)
|
||||
return self
|
||||
|
||||
def looks_identical(self, mobject: Mobject):
|
||||
fam1 = self.get_family()
|
||||
fam2 = mobject.get_family()
|
||||
fam1 = self.family_members_with_points()
|
||||
fam2 = mobject.family_members_with_points()
|
||||
if len(fam1) != len(fam2):
|
||||
return False
|
||||
for m1, m2 in zip(fam1, fam2):
|
||||
|
@ -628,11 +655,13 @@ class Mobject(object):
|
|||
if set(d1).difference(d2):
|
||||
return False
|
||||
for key in d1:
|
||||
if isinstance(d1[key], np.ndarray):
|
||||
if not np.all(d1[key] == d2[key]):
|
||||
eq = (d1[key] == d2[key])
|
||||
if isinstance(eq, bool):
|
||||
if not eq:
|
||||
return False
|
||||
else:
|
||||
if not eq.all():
|
||||
return False
|
||||
elif d1[key] != d2[key]:
|
||||
return False
|
||||
return True
|
||||
|
||||
# Creating new Mobjects from this one
|
||||
|
|
|
@ -28,27 +28,31 @@ class DecimalNumber(VMobject):
|
|||
"include_background_rectangle": False,
|
||||
"edge_to_fix": LEFT,
|
||||
"font_size": 48,
|
||||
"text_config": {} # Do not pass in font_size here
|
||||
"text_config": {} # Do not pass in font_size here
|
||||
}
|
||||
|
||||
def __init__(self, number: float | complex = 0, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.set_submobjects_from_number(number)
|
||||
self.init_colors()
|
||||
|
||||
def set_submobjects_from_number(self, number: float | complex) -> None:
|
||||
self.number = number
|
||||
self.set_submobjects([])
|
||||
string_to_mob_ = lambda s: self.string_to_mob(s, **self.text_config)
|
||||
num_string = self.get_num_string(number)
|
||||
self.add(*map(string_to_mob_, num_string))
|
||||
self.text_config["font_size"] = self.get_font_size()
|
||||
num_string = self.num_string = self.get_num_string(number)
|
||||
self.add(*(
|
||||
Text(ns, **self.text_config)
|
||||
for ns in num_string
|
||||
))
|
||||
|
||||
# Add non-numerical bits
|
||||
if self.show_ellipsis:
|
||||
dots = string_to_mob_("...")
|
||||
dots = Text("...", **self.text_config)
|
||||
dots.arrange(RIGHT, buff=2 * dots[0].get_width())
|
||||
self.add(dots)
|
||||
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.arrange(
|
||||
|
@ -91,12 +95,7 @@ class DecimalNumber(VMobject):
|
|||
self.data["font_size"] = np.array([self.font_size], dtype=float)
|
||||
|
||||
def get_font_size(self) -> float:
|
||||
return 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
|
||||
return int(self.data["font_size"][0])
|
||||
|
||||
def get_formatter(self, **kwargs) -> str:
|
||||
"""
|
||||
|
@ -117,13 +116,14 @@ class DecimalNumber(VMobject):
|
|||
]
|
||||
])
|
||||
config.update(kwargs)
|
||||
ndp = config["num_decimal_places"]
|
||||
return "".join([
|
||||
"{",
|
||||
config.get("field_name", ""),
|
||||
":",
|
||||
"+" if config["include_sign"] else "",
|
||||
"," if config["group_with_commas"] else "",
|
||||
".", str(config["num_decimal_places"]), "f",
|
||||
f".{ndp}f",
|
||||
"}",
|
||||
])
|
||||
|
||||
|
@ -134,13 +134,15 @@ class DecimalNumber(VMobject):
|
|||
"i"
|
||||
])
|
||||
|
||||
def get_tex(self):
|
||||
return self.num_string
|
||||
|
||||
def set_value(self, number: float | complex):
|
||||
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.move_to(move_to_point, self.edge_to_fix)
|
||||
for sm1, sm2 in zip(self.submobjects, old_submobjects):
|
||||
sm1.match_style(sm2)
|
||||
self.set_style(**style)
|
||||
return self
|
||||
|
||||
def _handle_scale_side_effects(self, scale_factor: float) -> None:
|
||||
|
|
|
@ -87,7 +87,7 @@ class Cross(VGroup):
|
|||
Line(UL, DR),
|
||||
Line(UR, DL),
|
||||
)
|
||||
self.insert_n_curves(2)
|
||||
self.insert_n_curves(20)
|
||||
self.replace(mobject, stretch=True)
|
||||
self.set_stroke(self.stroke_color, width=self.stroke_width)
|
||||
|
||||
|
|
|
@ -308,7 +308,7 @@ class Bubble(SVGMobject):
|
|||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
digest_config(self, kwargs, locals())
|
||||
digest_config(self, kwargs)
|
||||
if self.file_name is None:
|
||||
raise Exception("Must invoke Bubble subclass")
|
||||
SVGMobject.__init__(self, self.file_name, **kwargs)
|
||||
|
@ -317,7 +317,11 @@ class Bubble(SVGMobject):
|
|||
self.stretch_to_fit_width(self.width)
|
||||
if self.direction[0] > 0:
|
||||
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.refresh_triangulation()
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ class SingleStringTex(SVGMobject):
|
|||
"fill_opacity": 1.0,
|
||||
"stroke_width": 0,
|
||||
"svg_default": {
|
||||
"color": WHITE,
|
||||
"fill_color": WHITE,
|
||||
},
|
||||
"path_string_config": {
|
||||
"should_subdivide_sharp_curves": True,
|
||||
|
|
|
@ -68,7 +68,7 @@ class MarkupText(StringMobject):
|
|||
"lsh": None,
|
||||
"justify": False,
|
||||
"indent": 0,
|
||||
"alignment": "LEFT",
|
||||
"alignment": "",
|
||||
"line_width": None,
|
||||
"font": "",
|
||||
"slant": NORMAL,
|
||||
|
@ -114,6 +114,8 @@ class MarkupText(StringMobject):
|
|||
|
||||
if not self.font:
|
||||
self.font = get_customization()["style"]["font"]
|
||||
if not self.alignment:
|
||||
self.alignment = get_customization()["style"]["text_alignment"]
|
||||
if self.is_markup:
|
||||
self.validate_markup_string(text)
|
||||
|
||||
|
|
|
@ -203,36 +203,38 @@ class VMobject(Mobject):
|
|||
shadow: float | None = None,
|
||||
recurse: bool = True
|
||||
):
|
||||
if fill_rgba is not None:
|
||||
self.data['fill_rgba'] = resize_with_interpolation(fill_rgba, len(fill_rgba))
|
||||
else:
|
||||
self.set_fill(
|
||||
color=fill_color,
|
||||
opacity=fill_opacity,
|
||||
recurse=recurse
|
||||
)
|
||||
for mob in self.get_family(recurse):
|
||||
if fill_rgba is not None:
|
||||
mob.data['fill_rgba'] = resize_with_interpolation(fill_rgba, len(fill_rgba))
|
||||
else:
|
||||
mob.set_fill(
|
||||
color=fill_color,
|
||||
opacity=fill_opacity,
|
||||
recurse=False
|
||||
)
|
||||
|
||||
if stroke_rgba is not None:
|
||||
self.data['stroke_rgba'] = resize_with_interpolation(stroke_rgba, len(fill_rgba))
|
||||
self.set_stroke(
|
||||
width=stroke_width,
|
||||
background=stroke_background,
|
||||
)
|
||||
else:
|
||||
self.set_stroke(
|
||||
color=stroke_color,
|
||||
width=stroke_width,
|
||||
opacity=stroke_opacity,
|
||||
recurse=recurse,
|
||||
background=stroke_background,
|
||||
)
|
||||
if stroke_rgba is not None:
|
||||
mob.data['stroke_rgba'] = resize_with_interpolation(stroke_rgba, len(fill_rgba))
|
||||
mob.set_stroke(
|
||||
width=stroke_width,
|
||||
background=stroke_background,
|
||||
recurse=False,
|
||||
)
|
||||
else:
|
||||
mob.set_stroke(
|
||||
color=stroke_color,
|
||||
width=stroke_width,
|
||||
opacity=stroke_opacity,
|
||||
recurse=False,
|
||||
background=stroke_background,
|
||||
)
|
||||
|
||||
if reflectiveness is not None:
|
||||
self.set_reflectiveness(reflectiveness, recurse=recurse)
|
||||
if gloss is not None:
|
||||
self.set_gloss(gloss, recurse=recurse)
|
||||
if shadow is not None:
|
||||
self.set_shadow(shadow, recurse=recurse)
|
||||
if reflectiveness is not None:
|
||||
mob.set_reflectiveness(reflectiveness, recurse=False)
|
||||
if gloss is not None:
|
||||
mob.set_gloss(gloss, recurse=False)
|
||||
if shadow is not None:
|
||||
mob.set_shadow(shadow, recurse=False)
|
||||
return self
|
||||
|
||||
def get_style(self):
|
||||
|
@ -1202,17 +1204,17 @@ class VHighlight(VGroup):
|
|||
def __init__(
|
||||
self,
|
||||
vmobject: VMobject,
|
||||
n_layers: int = 3,
|
||||
n_layers: int = 5,
|
||||
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.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)
|
||||
for part, added_width, color in zip(reversed(outline), added_widths, colors):
|
||||
for sm in part.family_members_with_points():
|
||||
part.set_stroke(
|
||||
sm.set_stroke(
|
||||
width=sm.get_stroke_width() + added_width,
|
||||
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 DL, DOWN, DR, LEFT, ORIGIN, RIGHT, UL, UP, UR
|
||||
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 Square
|
||||
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 VMobject
|
||||
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.space_ops import get_norm
|
||||
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]
|
||||
RESIZE_KEY = 't'
|
||||
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.
|
||||
|
@ -68,25 +72,34 @@ class InteractiveScene(Scene):
|
|||
)
|
||||
selection_rectangle_stroke_color = WHITE
|
||||
selection_rectangle_stroke_width = 1.0
|
||||
colors = MANIM_COLORS
|
||||
palette_colors = MANIM_COLORS
|
||||
selection_nudge_size = 0.05
|
||||
cursor_location_config = dict(
|
||||
font_size=14,
|
||||
font_size=24,
|
||||
fill_color=GREY_C,
|
||||
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):
|
||||
self.selection = Group()
|
||||
self.selection_highlight = Group()
|
||||
self.selection_highlight = self.get_selection_highlight()
|
||||
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.cursor_location_label = self.get_cursor_location_label()
|
||||
self.unselectables = [
|
||||
self.selection,
|
||||
self.selection_highlight,
|
||||
self.selection_rectangle,
|
||||
self.cursor_location_label,
|
||||
self.crosshair,
|
||||
self.information_label,
|
||||
self.camera.frame
|
||||
]
|
||||
self.select_top_level_mobs = True
|
||||
|
@ -94,6 +107,7 @@ class InteractiveScene(Scene):
|
|||
|
||||
self.is_selecting = False
|
||||
self.is_grabbing = False
|
||||
|
||||
self.add(self.selection_highlight)
|
||||
|
||||
def get_selection_rectangle(self):
|
||||
|
@ -106,7 +120,7 @@ class InteractiveScene(Scene):
|
|||
rect.add_updater(self.update_selection_rectangle)
|
||||
return rect
|
||||
|
||||
def update_selection_rectangle(self, rect):
|
||||
def update_selection_rectangle(self, rect: Rectangle):
|
||||
p1 = rect.fixed_corner
|
||||
p2 = self.mouse_point.get_center()
|
||||
rect.set_points_as_corners([
|
||||
|
@ -116,10 +130,50 @@ class InteractiveScene(Scene):
|
|||
])
|
||||
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):
|
||||
palette = VGroup(*(
|
||||
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.arrange(RIGHT, buff=0.5)
|
||||
|
@ -128,22 +182,51 @@ class InteractiveScene(Scene):
|
|||
palette.fix_in_frame()
|
||||
return palette
|
||||
|
||||
def get_cursor_location_label(self):
|
||||
decimals = VGroup(*(
|
||||
def get_information_label(self):
|
||||
loc_label = VGroup(*(
|
||||
DecimalNumber(**self.cursor_location_config)
|
||||
for n in range(3)
|
||||
))
|
||||
|
||||
def update_coords(decimals):
|
||||
for mob, coord in zip(decimals, self.mouse_point.get_location()):
|
||||
def update_coords(loc_label):
|
||||
for mob, coord in zip(loc_label, self.mouse_point.get_location()):
|
||||
mob.set_value(coord)
|
||||
decimals.arrange(RIGHT, buff=decimals.get_height())
|
||||
decimals.to_corner(DR, buff=SMALL_BUFF)
|
||||
decimals.fix_in_frame()
|
||||
return decimals
|
||||
loc_label.arrange(RIGHT, buff=loc_label.get_height())
|
||||
loc_label.to_corner(DR, buff=SMALL_BUFF)
|
||||
loc_label.fix_in_frame()
|
||||
return loc_label
|
||||
|
||||
decimals.add_updater(update_coords)
|
||||
return decimals
|
||||
loc_label.add_updater(update_coords)
|
||||
|
||||
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
|
||||
|
||||
|
@ -184,7 +267,6 @@ class InteractiveScene(Scene):
|
|||
curr, exclude_pointless=True,
|
||||
)
|
||||
)
|
||||
self.refresh_selection_highlight()
|
||||
|
||||
def get_corner_dots(self, mobject: Mobject) -> Mobject:
|
||||
dots = DotCloud(**self.corner_dot_config)
|
||||
|
@ -192,7 +274,7 @@ class InteractiveScene(Scene):
|
|||
if mobject.get_depth() < 1e-2:
|
||||
vects = [DL, UL, UR, DR]
|
||||
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([
|
||||
mobject.get_corner(v) + v * radius
|
||||
for v in vects
|
||||
|
@ -201,27 +283,19 @@ class InteractiveScene(Scene):
|
|||
|
||||
def get_highlight(self, mobject: Mobject) -> Mobject:
|
||||
if isinstance(mobject, VMobject) and mobject.has_points() and not self.select_top_level_mobs:
|
||||
result = VHighlight(mobject)
|
||||
result.add_updater(lambda m: m.replace(mobject))
|
||||
length = max([mobject.get_height(), mobject.get_width()])
|
||||
result = VHighlight(
|
||||
mobject,
|
||||
max_stroke_addition=min([50 * length, 10]),
|
||||
)
|
||||
result.add_updater(lambda m: m.replace(mobject, stretch=True))
|
||||
return result
|
||||
elif isinstance(mobject, DotCloud):
|
||||
return Mobject()
|
||||
else:
|
||||
return self.get_corner_dots(mobject)
|
||||
|
||||
def refresh_selection_highlight(self):
|
||||
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):
|
||||
def add_to_selection(self, *mobjects: Mobject):
|
||||
mobs = list(filter(
|
||||
lambda m: m not in self.unselectables and m not in self.selection,
|
||||
mobjects
|
||||
|
@ -229,46 +303,34 @@ class InteractiveScene(Scene):
|
|||
if len(mobs) == 0:
|
||||
return
|
||||
self.selection.add(*mobs)
|
||||
self.refresh_selection_highlight()
|
||||
for sm in mobs:
|
||||
for mob in self.mobjects:
|
||||
if sm in mob.get_family():
|
||||
mob.set_animating_status(True)
|
||||
self.refresh_static_mobjects()
|
||||
self.selection.set_animating_status(True)
|
||||
|
||||
def toggle_from_selection(self, *mobjects):
|
||||
def toggle_from_selection(self, *mobjects: Mobject):
|
||||
for mob in mobjects:
|
||||
if mob in self.selection:
|
||||
self.selection.remove(mob)
|
||||
mob.set_animating_status(False)
|
||||
else:
|
||||
self.add_to_selection(mob)
|
||||
self.refresh_selection_highlight()
|
||||
self.refresh_static_mobjects()
|
||||
|
||||
def clear_selection(self):
|
||||
for mob in self.selection:
|
||||
mob.set_animating_status(False)
|
||||
self.selection.set_submobjects([])
|
||||
self.selection_highlight.set_submobjects([])
|
||||
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):
|
||||
for mob in mobjects:
|
||||
self.unselectables.append(mob)
|
||||
for sm in mob.get_family():
|
||||
self.unselectables.append(sm)
|
||||
self.regenerate_selection_search_set()
|
||||
|
||||
def enable_interaction(self, *mobjects: Mobject):
|
||||
for mob in mobjects:
|
||||
if mob in self.unselectables:
|
||||
self.unselectables.remove(mob)
|
||||
for sm in mob.get_family():
|
||||
if sm in self.unselectables:
|
||||
self.unselectables.remove(sm)
|
||||
|
||||
# Functions for keyboard actions
|
||||
|
||||
|
@ -308,10 +370,6 @@ class InteractiveScene(Scene):
|
|||
self.remove(*self.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):
|
||||
self.is_selecting = True
|
||||
self.add(self.selection_rectangle)
|
||||
|
@ -352,6 +410,12 @@ class InteractiveScene(Scene):
|
|||
else:
|
||||
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):
|
||||
group = self.get_group(*self.selection)
|
||||
self.add(group)
|
||||
|
@ -393,8 +457,8 @@ class InteractiveScene(Scene):
|
|||
self.prepare_resizing(about_corner=True)
|
||||
elif char == COLOR_KEY and modifiers == 0:
|
||||
self.toggle_color_palette()
|
||||
elif char == CURSOR_LOCATION_KEY and modifiers == 0:
|
||||
self.add(self.cursor_location_label)
|
||||
elif char == INFORMATION_KEY and modifiers == 0:
|
||||
self.display_information()
|
||||
elif char == "c" and modifiers == COMMAND_MODIFIER:
|
||||
self.copy_selection()
|
||||
elif char == "v" and modifiers == COMMAND_MODIFIER:
|
||||
|
@ -420,6 +484,14 @@ class InteractiveScene(Scene):
|
|||
vect=[LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)],
|
||||
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
|
||||
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)
|
||||
if chr(symbol) == SELECT_KEY:
|
||||
self.gather_new_selection()
|
||||
# self.remove(self.crosshair)
|
||||
if chr(symbol) in GRAB_KEYS:
|
||||
self.is_grabbing = False
|
||||
elif chr(symbol) == CURSOR_LOCATION_KEY:
|
||||
self.remove(self.cursor_location_label)
|
||||
elif chr(symbol) == INFORMATION_KEY:
|
||||
self.display_information(False)
|
||||
elif symbol == SHIFT_SYMBOL and self.window.is_key_pressed(ord(RESIZE_KEY)):
|
||||
self.prepare_resizing(about_corner=False)
|
||||
|
||||
|
@ -447,6 +520,8 @@ class InteractiveScene(Scene):
|
|||
self.selection.set_y(diff[1])
|
||||
|
||||
def handle_resizing(self, point: np.ndarray):
|
||||
if not hasattr(self, "scale_about_point"):
|
||||
return
|
||||
vect = point - self.scale_about_point
|
||||
if self.window.is_key_pressed(CTRL_SYMBOL):
|
||||
for i in (0, 1):
|
||||
|
@ -485,17 +560,9 @@ class InteractiveScene(Scene):
|
|||
self.selection.set_color(mob.get_color())
|
||||
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:
|
||||
super().on_mouse_motion(point, d_point)
|
||||
self.crosshair.move_to(point)
|
||||
if self.is_grabbing:
|
||||
self.handle_grabbing(point)
|
||||
elif self.window.is_key_pressed(ord(RESIZE_KEY)):
|
||||
|
@ -507,7 +574,13 @@ class InteractiveScene(Scene):
|
|||
super().on_mouse_release(point, button, mods)
|
||||
if self.color_palette in self.mobjects:
|
||||
self.choose_color(point)
|
||||
elif self.window.is_key_pressed(SHIFT_SYMBOL):
|
||||
self.toggle_clicked_mobject_from_selection(point)
|
||||
return
|
||||
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:
|
||||
self.clear_selection()
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
from functools import wraps
|
||||
import inspect
|
||||
import os
|
||||
import platform
|
||||
import pyperclip
|
||||
import random
|
||||
import time
|
||||
|
||||
|
@ -11,7 +13,6 @@ import numpy as np
|
|||
from tqdm import tqdm as ProgressDisplay
|
||||
|
||||
from manimlib.animation.animation import prepare_animation
|
||||
from manimlib.animation.transform import MoveToTarget
|
||||
from manimlib.camera.camera import Camera
|
||||
from manimlib.constants import ARROW_SYMBOLS
|
||||
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.event_type import EventType
|
||||
from manimlib.logger import log
|
||||
from manimlib.mobject.mobject import _AnimationBuilder
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.mobject.mobject import Point
|
||||
|
@ -61,7 +63,7 @@ class Scene(object):
|
|||
"leave_progress_bars": False,
|
||||
"preview": True,
|
||||
"presenter_mode": False,
|
||||
"linger_after_completion": True,
|
||||
"show_animation_progress": False,
|
||||
"pan_sensitivity": 3,
|
||||
"max_num_saved_states": 50,
|
||||
}
|
||||
|
@ -72,7 +74,7 @@ class Scene(object):
|
|||
from manimlib.window import Window
|
||||
self.window = Window(scene=self, **self.window_config)
|
||||
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.redo_stack = []
|
||||
else:
|
||||
|
@ -86,14 +88,19 @@ class Scene(object):
|
|||
self.time: float = 0
|
||||
self.skip_time: float = 0
|
||||
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:
|
||||
self.skip_animations = True
|
||||
if self.file_writer.has_progress_display:
|
||||
self.show_animation_progress = False
|
||||
|
||||
# Items associated with interaction
|
||||
self.mouse_point = Point()
|
||||
self.mouse_drag_point = Point()
|
||||
self.hold_on_wait = self.presenter_mode
|
||||
self.inside_embed = False
|
||||
self.quit_interaction = False
|
||||
|
||||
# Much nicer to work with deterministic scenes
|
||||
if self.random_seed is not None:
|
||||
|
@ -111,8 +118,13 @@ class Scene(object):
|
|||
self.setup()
|
||||
try:
|
||||
self.construct()
|
||||
except EndSceneEarlyException:
|
||||
self.interact()
|
||||
except EndScene:
|
||||
pass
|
||||
except KeyboardInterrupt:
|
||||
# Get rid keyboard interupt symbols
|
||||
print("", end="\r")
|
||||
self.file_writer.ended_with_interrupt = True
|
||||
self.tear_down()
|
||||
|
||||
def setup(self) -> None:
|
||||
|
@ -131,32 +143,31 @@ class Scene(object):
|
|||
def tear_down(self) -> None:
|
||||
self.stop_skipping()
|
||||
self.file_writer.finish()
|
||||
if self.window and self.linger_after_completion:
|
||||
self.interact()
|
||||
if self.window:
|
||||
self.window.destroy()
|
||||
self.window = None
|
||||
|
||||
def interact(self) -> None:
|
||||
# If there is a window, enter a loop
|
||||
# which updates the frame while under
|
||||
# the hood calling the pyglet event loop
|
||||
if self.window is None:
|
||||
return
|
||||
log.info(
|
||||
"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`"
|
||||
" if you want to quit."
|
||||
)
|
||||
self.quit_interaction = False
|
||||
self.skip_animations = False
|
||||
self.refresh_static_mobjects()
|
||||
while not (self.window.is_closing or self.quit_interaction):
|
||||
self.update_frame(1 / self.camera.frame_rate)
|
||||
if self.window.is_closing:
|
||||
self.window.destroy()
|
||||
while not self.is_window_closing():
|
||||
self.update_frame(1 / self.camera.fps)
|
||||
|
||||
def embed(self, close_scene_on_exit: bool = True) -> None:
|
||||
if not self.preview:
|
||||
# Ignore embed calls when there is no preview
|
||||
return
|
||||
return # Embed is only relevant with a preview
|
||||
self.inside_embed = True
|
||||
self.stop_skipping()
|
||||
self.linger_after_completion = False
|
||||
self.update_frame()
|
||||
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
|
||||
def inputhook(context):
|
||||
while not context.input_is_ready():
|
||||
if not self.window.is_closing:
|
||||
if not self.is_window_closing():
|
||||
self.update_frame(dt=0)
|
||||
if self.is_window_closing():
|
||||
shell.ask_exit()
|
||||
|
||||
pt_inputhooks.register("manim", inputhook)
|
||||
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
|
||||
def post_cell_func(*args, **kwargs):
|
||||
def post_cell_func():
|
||||
self.refresh_static_mobjects()
|
||||
if not self.is_window_closing():
|
||||
self.update_frame(dt=0, ignore_skipping=True)
|
||||
self.save_state()
|
||||
|
||||
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)
|
||||
|
||||
self.inside_embed = False
|
||||
# End scene when exiting an embed
|
||||
if close_scene_on_exit:
|
||||
raise EndSceneEarlyException()
|
||||
raise EndScene()
|
||||
|
||||
# Only these methods should touch the camera
|
||||
|
||||
|
@ -214,6 +266,9 @@ class Scene(object):
|
|||
if self.skip_animations and not ignore_skipping:
|
||||
return
|
||||
|
||||
if self.is_window_closing():
|
||||
raise EndScene()
|
||||
|
||||
if self.window:
|
||||
self.window.clear()
|
||||
self.camera.clear()
|
||||
|
@ -396,7 +451,7 @@ class Scene(object):
|
|||
self.stop_skipping()
|
||||
if self.end_at_animation_number is not None:
|
||||
if self.num_plays >= self.end_at_animation_number:
|
||||
raise EndSceneEarlyException()
|
||||
raise EndScene()
|
||||
|
||||
def stop_skipping(self) -> None:
|
||||
self.virtual_animation_start_time = self.time
|
||||
|
@ -413,24 +468,25 @@ class Scene(object):
|
|||
) -> list[float] | np.ndarray | ProgressDisplay:
|
||||
if self.skip_animations and not override_skip_animations:
|
||||
return [run_time]
|
||||
else:
|
||||
step = 1 / self.camera.frame_rate
|
||||
times = np.arange(0, run_time, step)
|
||||
|
||||
times = np.arange(0, run_time, 1 / self.camera.fps)
|
||||
|
||||
if self.file_writer.has_progress_display:
|
||||
self.file_writer.set_progress_display_subdescription(desc)
|
||||
|
||||
if self.show_animation_progress:
|
||||
return ProgressDisplay(
|
||||
times,
|
||||
total=n_iterations,
|
||||
leave=self.leave_progress_bars,
|
||||
ascii=True if platform.system() == 'Windows' else None,
|
||||
desc=desc,
|
||||
)
|
||||
else:
|
||||
return times
|
||||
|
||||
return ProgressDisplay(
|
||||
times,
|
||||
total=n_iterations,
|
||||
leave=self.leave_progress_bars,
|
||||
ascii=True if platform.system() == 'Windows' else None,
|
||||
desc=desc,
|
||||
)
|
||||
|
||||
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(
|
||||
self,
|
||||
|
@ -454,74 +510,16 @@ class Scene(object):
|
|||
kw["override_skip_animations"] = True
|
||||
return self.get_time_progression(duration, **kw)
|
||||
|
||||
def anims_from_play_args(self, *args, **kwargs) -> list[Animation]:
|
||||
"""
|
||||
Each arg can either be an animation, or a mobject method
|
||||
followed by that methods arguments (and potentially follow
|
||||
by a dict of kwargs for that method).
|
||||
This animation list is built by going through the args list,
|
||||
and each animation is simply added, but when a mobject method
|
||||
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:
|
||||
def prepare_animations(
|
||||
self,
|
||||
proto_animations: list[Animation | _AnimationBuilder],
|
||||
animation_config: dict,
|
||||
):
|
||||
animations = list(map(prepare_animation, proto_animations))
|
||||
for anim in animations:
|
||||
# This is where kwargs to play like run_time and rate_func
|
||||
# get applied to all animations
|
||||
animation.update_config(**kwargs)
|
||||
|
||||
anim.update_config(**animation_config)
|
||||
return animations
|
||||
|
||||
def handle_play_like_call(func):
|
||||
|
@ -529,6 +527,8 @@ class Scene(object):
|
|||
def wrapper(self, *args, **kwargs):
|
||||
if self.inside_embed:
|
||||
self.save_state()
|
||||
if self.presenter_mode and self.num_plays == 0:
|
||||
self.hold_loop()
|
||||
|
||||
self.update_skipping_status()
|
||||
should_write = not self.skip_animations
|
||||
|
@ -548,6 +548,10 @@ class Scene(object):
|
|||
if self.inside_embed:
|
||||
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
|
||||
return wrapper
|
||||
|
||||
|
@ -587,11 +591,11 @@ class Scene(object):
|
|||
self.update_mobjects(0)
|
||||
|
||||
@handle_play_like_call
|
||||
def play(self, *args, **kwargs) -> None:
|
||||
if len(args) == 0:
|
||||
def play(self, *proto_animations, **animation_config) -> None:
|
||||
if len(proto_animations) == 0:
|
||||
log.warning("Called Scene.play with no animations")
|
||||
return
|
||||
animations = self.anims_from_play_args(*args, **kwargs)
|
||||
animations = self.prepare_animations(proto_animations, animation_config)
|
||||
self.begin_animations(animations)
|
||||
self.progress_through_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 note:
|
||||
log.info(note)
|
||||
while self.hold_on_wait:
|
||||
self.update_frame(dt=1 / self.camera.frame_rate)
|
||||
self.hold_on_wait = True
|
||||
self.hold_loop()
|
||||
else:
|
||||
time_progression = self.get_wait_time_progression(duration, stop_condition)
|
||||
last_t = 0
|
||||
|
@ -624,6 +626,11 @@ class Scene(object):
|
|||
self.refresh_static_mobjects()
|
||||
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(
|
||||
self,
|
||||
stop_condition: Callable[[], bool],
|
||||
|
@ -655,50 +662,52 @@ class Scene(object):
|
|||
|
||||
# Helpers for interactive development
|
||||
|
||||
def get_state(self) -> tuple[list[tuple[Mobject, Mobject]], int]:
|
||||
if self.undo_stack:
|
||||
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 get_state(self) -> SceneState:
|
||||
return SceneState(self)
|
||||
|
||||
def restore_state(self, mobject_states: list[tuple[Mobject, Mobject]]):
|
||||
self.mobjects = [mob.become(mob_copy) for mob, mob_copy in mobject_states]
|
||||
def restore_state(self, scene_state: SceneState):
|
||||
scene_state.restore_scene(self)
|
||||
|
||||
def save_state(self) -> None:
|
||||
if not self.preview:
|
||||
return
|
||||
state = self.get_state()
|
||||
if self.undo_stack and state.mobjects_match(self.undo_stack[-1]):
|
||||
return
|
||||
self.redo_stack = []
|
||||
state, n_changes = self.get_state()
|
||||
if n_changes > 0:
|
||||
self.undo_stack.append(state)
|
||||
if len(self.undo_stack) > self.max_num_saved_states:
|
||||
self.undo_stack.pop(0)
|
||||
self.undo_stack.append(state)
|
||||
if len(self.undo_stack) > self.max_num_saved_states:
|
||||
self.undo_stack.pop(0)
|
||||
|
||||
def undo(self):
|
||||
if self.undo_stack:
|
||||
state, n_changes = self.get_state()
|
||||
self.redo_stack.append(state)
|
||||
self.redo_stack.append(self.get_state())
|
||||
self.restore_state(self.undo_stack.pop())
|
||||
self.refresh_static_mobjects()
|
||||
|
||||
def redo(self):
|
||||
if self.redo_stack:
|
||||
state, n_changes = self.get_state()
|
||||
self.undo_stack.append(state)
|
||||
self.undo_stack.append(self.get_state())
|
||||
self.restore_state(self.redo_stack.pop())
|
||||
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:
|
||||
if file_path is None:
|
||||
file_path = self.file_writer.get_saved_mobject_path(mobject)
|
||||
|
@ -714,6 +723,9 @@ class Scene(object):
|
|||
path = os.path.join(directory, file_name)
|
||||
return Mobject.load(path)
|
||||
|
||||
def is_window_closing(self):
|
||||
return self.window and (self.window.is_closing or self.quit_interaction)
|
||||
|
||||
# Event handling
|
||||
|
||||
def on_mouse_motion(
|
||||
|
@ -850,5 +862,49 @@ class Scene(object):
|
|||
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
|
||||
|
|
|
@ -37,9 +37,6 @@ class SceneFileWriter(object):
|
|||
"png_mode": "RGBA",
|
||||
"save_last_frame": False,
|
||||
"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
|
||||
"input_file_path": "",
|
||||
# Where should this be written
|
||||
|
@ -57,16 +54,13 @@ class SceneFileWriter(object):
|
|||
self.scene: Scene = scene
|
||||
self.writing_process: sp.Popen | None = None
|
||||
self.has_progress_display: bool = False
|
||||
self.ended_with_interrupt: bool = False
|
||||
self.init_output_directories()
|
||||
self.init_audio()
|
||||
|
||||
# Output directories and files
|
||||
def init_output_directories(self) -> None:
|
||||
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()
|
||||
if self.save_last_frame:
|
||||
image_dir = guarantee_existence(os.path.join(out_dir, "images"))
|
||||
|
@ -81,7 +75,9 @@ class SceneFileWriter(object):
|
|||
movie_dir, "partial_movie_files", scene_name,
|
||||
))
|
||||
# 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:
|
||||
path, _ = os.path.splitext(self.input_file_path)
|
||||
|
@ -101,9 +97,9 @@ class SceneFileWriter(object):
|
|||
|
||||
def get_resolution_directory(self) -> str:
|
||||
pixel_height = self.scene.camera.pixel_height
|
||||
frame_rate = self.scene.camera.frame_rate
|
||||
fps = self.scene.camera.fps
|
||||
return "{}p{}".format(
|
||||
pixel_height, frame_rate
|
||||
pixel_height, fps
|
||||
)
|
||||
|
||||
# Directory getters
|
||||
|
@ -124,10 +120,7 @@ class SceneFileWriter(object):
|
|||
return self.movie_file_path
|
||||
|
||||
def get_saved_mobject_directory(self) -> str:
|
||||
return guarantee_existence(os.path.join(
|
||||
self.saved_mobject_directory,
|
||||
str(self.scene),
|
||||
))
|
||||
return guarantee_existence(self.saved_mobject_directory)
|
||||
|
||||
def get_saved_mobject_path(self, mobject: Mobject) -> str | None:
|
||||
directory = self.get_saved_mobject_directory()
|
||||
|
@ -241,7 +234,7 @@ class SceneFileWriter(object):
|
|||
self.final_file_path = file_path
|
||||
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()
|
||||
|
||||
command = [
|
||||
|
@ -305,7 +298,11 @@ class SceneFileWriter(object):
|
|||
self.writing_process.terminate()
|
||||
if self.has_progress_display:
|
||||
self.progress_display.close()
|
||||
shutil.move(self.temp_file_path, self.final_file_path)
|
||||
|
||||
if not self.ended_with_interrupt:
|
||||
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:
|
||||
kwargs = {
|
||||
|
|
|
@ -52,25 +52,14 @@ def init_customization() -> None:
|
|||
"window_monitor": 0,
|
||||
"full_screen": False,
|
||||
"break_into_partial_movies": False,
|
||||
"camera_qualities": {
|
||||
"low": {
|
||||
"resolution": "854x480",
|
||||
"frame_rate": 15,
|
||||
},
|
||||
"medium": {
|
||||
"resolution": "1280x720",
|
||||
"frame_rate": 30,
|
||||
},
|
||||
"high": {
|
||||
"resolution": "1920x1080",
|
||||
"frame_rate": 60,
|
||||
},
|
||||
"ultra_high": {
|
||||
"resolution": "3840x2160",
|
||||
"frame_rate": 60,
|
||||
},
|
||||
"default_quality": "",
|
||||
}
|
||||
"camera_resolutions": {
|
||||
"low": "854x480",
|
||||
"medium": "1280x720",
|
||||
"high": "1920x1080",
|
||||
"4k": "3840x2160",
|
||||
"default_resolution": "high",
|
||||
},
|
||||
"fps": 30,
|
||||
}
|
||||
|
||||
console = Console()
|
||||
|
|
|
@ -13,19 +13,12 @@ if TYPE_CHECKING:
|
|||
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
|
||||
Keeps the last occurrence of each element
|
||||
"""
|
||||
reversed_result = []
|
||||
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
|
||||
return list(reversed(dict.fromkeys(reversed(lst))))
|
||||
|
||||
|
||||
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,
|
||||
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]:
|
||||
|
@ -119,7 +112,7 @@ def resize_with_interpolation(nparray: np.ndarray, length: int) -> np.ndarray:
|
|||
|
||||
|
||||
def make_even(
|
||||
iterable_1: Sequence[T],
|
||||
iterable_1: Sequence[T],
|
||||
iterable_2: Sequence[S]
|
||||
) -> tuple[list[T], list[S]]:
|
||||
len1 = len(iterable_1)
|
||||
|
|
Loading…
Add table
Reference in a new issue