mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +00:00
Merge branch '3b1b-master'
This commit is contained in:
commit
e39f81ccff
17 changed files with 134 additions and 60 deletions
|
@ -1,7 +1,6 @@
|
||||||
from manimlib.animation.animation import Animation
|
from manimlib.animation.animation import Animation
|
||||||
from manimlib.animation.composition import Succession
|
from manimlib.animation.composition import Succession
|
||||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||||
from manimlib.mobject.mobject import Group
|
|
||||||
from manimlib.utils.bezier import integer_interpolate
|
from manimlib.utils.bezier import integer_interpolate
|
||||||
from manimlib.utils.config_ops import digest_config
|
from manimlib.utils.config_ops import digest_config
|
||||||
from manimlib.utils.rate_functions import linear
|
from manimlib.utils.rate_functions import linear
|
||||||
|
@ -147,7 +146,7 @@ class Write(DrawBorderThenFill):
|
||||||
else:
|
else:
|
||||||
self.run_time = 2
|
self.run_time = 2
|
||||||
if self.lag_ratio is None:
|
if self.lag_ratio is None:
|
||||||
self.lag_ratio = min(4.0 / length, 0.2)
|
self.lag_ratio = min(4.0 / (length + 1.0), 0.2)
|
||||||
|
|
||||||
|
|
||||||
class ShowIncreasingSubsets(Animation):
|
class ShowIncreasingSubsets(Animation):
|
||||||
|
|
|
@ -17,7 +17,6 @@ class Broadcast(LaggedStart):
|
||||||
"remover": True,
|
"remover": True,
|
||||||
"lag_ratio": 0.2,
|
"lag_ratio": 0.2,
|
||||||
"run_time": 3,
|
"run_time": 3,
|
||||||
"remover": True,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, focal_point, **kwargs):
|
def __init__(self, focal_point, **kwargs):
|
||||||
|
|
|
@ -65,6 +65,12 @@ def parse_cli():
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Show window in full screen",
|
help="Show window in full screen",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-p", "--presenter_mode",
|
||||||
|
action="store_true",
|
||||||
|
help="scene will stay paused during wait calls until "
|
||||||
|
"space bar or right arrow is hit, like a slide show"
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-g", "--save_pngs",
|
"-g", "--save_pngs",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
@ -306,6 +312,7 @@ def get_configuration(args):
|
||||||
"start_at_animation_number": args.start_at_animation_number,
|
"start_at_animation_number": args.start_at_animation_number,
|
||||||
"end_at_animation_number": None,
|
"end_at_animation_number": None,
|
||||||
"preview": not write_file,
|
"preview": not write_file,
|
||||||
|
"presenter_mode": args.presenter_mode,
|
||||||
"leave_progress_bars": args.leave_progress_bars,
|
"leave_progress_bars": args.leave_progress_bars,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,7 @@ def get_scene_config(config):
|
||||||
"end_at_animation_number",
|
"end_at_animation_number",
|
||||||
"leave_progress_bars",
|
"leave_progress_bars",
|
||||||
"preview",
|
"preview",
|
||||||
|
"presenter_mode",
|
||||||
]
|
]
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
|
@ -277,7 +277,7 @@ class CoordinateSystem():
|
||||||
class Axes(VGroup, CoordinateSystem):
|
class Axes(VGroup, CoordinateSystem):
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
"axis_config": {
|
"axis_config": {
|
||||||
"include_tip": True,
|
"include_tip": False,
|
||||||
"numbers_to_exclude": [0],
|
"numbers_to_exclude": [0],
|
||||||
},
|
},
|
||||||
"x_axis_config": {},
|
"x_axis_config": {},
|
||||||
|
|
|
@ -44,6 +44,8 @@ class ParametricCurve(VMobject):
|
||||||
self.add_points_as_corners(points[1:])
|
self.add_points_as_corners(points[1:])
|
||||||
if self.use_smoothing:
|
if self.use_smoothing:
|
||||||
self.make_approximately_smooth()
|
self.make_approximately_smooth()
|
||||||
|
if not self.has_points():
|
||||||
|
self.set_points([self.t_func(t_min)])
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -112,7 +112,7 @@ class Matrix(VMobject):
|
||||||
"\\left[",
|
"\\left[",
|
||||||
"\\begin{array}{c}",
|
"\\begin{array}{c}",
|
||||||
*height * ["\\quad \\\\"],
|
*height * ["\\quad \\\\"],
|
||||||
"\\end{array}"
|
"\\end{array}",
|
||||||
"\\right]",
|
"\\right]",
|
||||||
]))[0]
|
]))[0]
|
||||||
bracket_pair.set_height(
|
bracket_pair.set_height(
|
||||||
|
|
|
@ -899,6 +899,21 @@ class Mobject(object):
|
||||||
self.set_depth(max_depth, **kwargs)
|
self.set_depth(max_depth, **kwargs)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def set_min_width(self, min_width: float, **kwargs):
|
||||||
|
if self.get_width() < min_width:
|
||||||
|
self.set_width(min_width, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_min_height(self, min_height: float, **kwargs):
|
||||||
|
if self.get_height() < min_height:
|
||||||
|
self.set_height(min_height, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_min_depth(self, min_depth: float, **kwargs):
|
||||||
|
if self.get_depth() < min_depth:
|
||||||
|
self.set_depth(min_depth, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
def set_coord(self, value: float, dim: int, direction: np.ndarray = ORIGIN):
|
def set_coord(self, value: float, dim: int, direction: np.ndarray = ORIGIN):
|
||||||
curr = self.get_coord(dim, direction)
|
curr = self.get_coord(dim, direction)
|
||||||
shift_vect = np.zeros(self.dim)
|
shift_vect = np.zeros(self.dim)
|
||||||
|
@ -1295,21 +1310,38 @@ class Mobject(object):
|
||||||
def match_depth(self, mobject: "Mobject", **kwargs):
|
def match_depth(self, mobject: "Mobject", **kwargs):
|
||||||
return self.match_dim_size(mobject, 2, **kwargs)
|
return self.match_dim_size(mobject, 2, **kwargs)
|
||||||
|
|
||||||
def match_coord(self, mobject: "Mobject", dim: int, direction: np.ndarray = ORIGIN):
|
def match_coord(
|
||||||
return self.set_coord(
|
self,
|
||||||
mobject.get_coord(dim, direction),
|
mobject_or_point: "Mobject" | np.ndarray,
|
||||||
dim=dim,
|
dim: int,
|
||||||
direction=direction,
|
direction: np.ndarray = ORIGIN
|
||||||
)
|
):
|
||||||
|
if isinstance(mobject_or_point, Mobject):
|
||||||
|
coord = mobject_or_point.get_coord(dim, direction)
|
||||||
|
else:
|
||||||
|
coord = mobject_or_point[dim]
|
||||||
|
return self.set_coord(coord, dim=dim, direction=direction)
|
||||||
|
|
||||||
def match_x(self, mobject: "Mobject", direction: np.ndarray = ORIGIN):
|
def match_x(
|
||||||
return self.match_coord(mobject, 0, direction)
|
self,
|
||||||
|
mobject_or_point: "Mobject" | np.ndarray,
|
||||||
|
direction: np.ndarray = ORIGIN
|
||||||
|
):
|
||||||
|
return self.match_coord(mobject_or_point, 0, direction)
|
||||||
|
|
||||||
def match_y(self, mobject: "Mobject", direction: np.ndarray = ORIGIN):
|
def match_y(
|
||||||
return self.match_coord(mobject, 1, direction)
|
self,
|
||||||
|
mobject_or_point: "Mobject" | np.ndarray,
|
||||||
|
direction: np.ndarray = ORIGIN
|
||||||
|
):
|
||||||
|
return self.match_coord(mobject_or_point, 1, direction)
|
||||||
|
|
||||||
def match_z(self, mobject: "Mobject", direction: np.ndarray = ORIGIN):
|
def match_z(
|
||||||
return self.match_coord(mobject, 2, direction)
|
self,
|
||||||
|
mobject_or_point: "Mobject" | np.ndarray,
|
||||||
|
direction: np.ndarray = ORIGIN
|
||||||
|
):
|
||||||
|
return self.match_coord(mobject_or_point, 2, direction)
|
||||||
|
|
||||||
def align_to(
|
def align_to(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -318,6 +318,9 @@ class Bubble(SVGMobject):
|
||||||
self.content = Mobject()
|
self.content = Mobject()
|
||||||
self.refresh_triangulation()
|
self.refresh_triangulation()
|
||||||
|
|
||||||
|
def init_colors(self):
|
||||||
|
VMobject.init_colors(self)
|
||||||
|
|
||||||
def get_tip(self):
|
def get_tip(self):
|
||||||
# TODO, find a better way
|
# TODO, find a better way
|
||||||
return self.get_corner(DOWN + self.direction) - 0.6 * self.direction
|
return self.get_corner(DOWN + self.direction) - 0.6 * self.direction
|
||||||
|
|
|
@ -13,7 +13,6 @@ from manimlib.mobject.geometry import Polygon
|
||||||
from manimlib.mobject.geometry import Polyline
|
from manimlib.mobject.geometry import Polyline
|
||||||
from manimlib.mobject.geometry import Rectangle
|
from manimlib.mobject.geometry import Rectangle
|
||||||
from manimlib.mobject.geometry import RoundedRectangle
|
from manimlib.mobject.geometry import RoundedRectangle
|
||||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
|
||||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||||
from manimlib.utils.config_ops import digest_config
|
from manimlib.utils.config_ops import digest_config
|
||||||
from manimlib.utils.directories import get_mobject_data_dir
|
from manimlib.utils.directories import get_mobject_data_dir
|
||||||
|
@ -321,4 +320,4 @@ class VMobjectFromSVGPath(VMobject):
|
||||||
_convert_point_to_3d(*segment.__getattribute__(attr_name))
|
_convert_point_to_3d(*segment.__getattribute__(attr_name))
|
||||||
for attr_name in attr_names
|
for attr_name in attr_names
|
||||||
]
|
]
|
||||||
func(*points)
|
func(*points)
|
|
@ -36,20 +36,19 @@ class SingleStringTex(VMobject):
|
||||||
assert(isinstance(tex_string, str))
|
assert(isinstance(tex_string, str))
|
||||||
self.tex_string = tex_string
|
self.tex_string = tex_string
|
||||||
if tex_string not in tex_string_with_color_to_mob_map:
|
if tex_string not in tex_string_with_color_to_mob_map:
|
||||||
with display_during_execution(f" Writing \"{tex_string}\""):
|
full_tex = self.get_tex_file_body(tex_string)
|
||||||
full_tex = self.get_tex_file_body(tex_string)
|
filename = tex_to_svg_file(full_tex)
|
||||||
filename = tex_to_svg_file(full_tex)
|
svg_mob = SVGMobject(
|
||||||
svg_mob = SVGMobject(
|
filename,
|
||||||
filename,
|
height=None,
|
||||||
height=None,
|
color=self.color,
|
||||||
color=self.color,
|
stroke_width=self.stroke_width,
|
||||||
stroke_width=self.stroke_width,
|
path_string_config={
|
||||||
path_string_config={
|
"should_subdivide_sharp_curves": True,
|
||||||
"should_subdivide_sharp_curves": True,
|
"should_remove_null_curves": True,
|
||||||
"should_remove_null_curves": True,
|
}
|
||||||
}
|
)
|
||||||
)
|
tex_string_with_color_to_mob_map[(self.color, tex_string)] = svg_mob
|
||||||
tex_string_with_color_to_mob_map[(self.color, tex_string)] = svg_mob
|
|
||||||
self.add(*(
|
self.add(*(
|
||||||
sm.copy()
|
sm.copy()
|
||||||
for sm in tex_string_with_color_to_mob_map[(self.color, tex_string)]
|
for sm in tex_string_with_color_to_mob_map[(self.color, tex_string)]
|
||||||
|
@ -140,7 +139,11 @@ class SingleStringTex(VMobject):
|
||||||
Makes Tex resiliant to unmatched braces
|
Makes Tex resiliant to unmatched braces
|
||||||
"""
|
"""
|
||||||
num_unclosed_brackets = 0
|
num_unclosed_brackets = 0
|
||||||
for char in tex:
|
for i in range(len(tex)):
|
||||||
|
if i > 0 and tex[i - 1] == "\\":
|
||||||
|
# So as to not count '\{' type expressions
|
||||||
|
continue
|
||||||
|
char = tex[i]
|
||||||
if char == "{":
|
if char == "{":
|
||||||
num_unclosed_brackets += 1
|
num_unclosed_brackets += 1
|
||||||
elif char == "}":
|
elif char == "}":
|
||||||
|
|
|
@ -35,6 +35,8 @@ class PMobject(Mobject):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_points(self, points: npt.ArrayLike):
|
def set_points(self, points: npt.ArrayLike):
|
||||||
|
if len(points) == 0:
|
||||||
|
points = np.zeros((0, 3))
|
||||||
super().set_points(points)
|
super().set_points(points)
|
||||||
self.resize_points(len(points))
|
self.resize_points(len(points))
|
||||||
return self
|
return self
|
||||||
|
@ -54,14 +56,18 @@ class PMobject(Mobject):
|
||||||
if color is not None:
|
if color is not None:
|
||||||
if opacity is None:
|
if opacity is None:
|
||||||
opacity = self.data["rgbas"][-1, 3]
|
opacity = self.data["rgbas"][-1, 3]
|
||||||
new_rgbas = np.repeat(
|
rgbas = np.repeat(
|
||||||
[color_to_rgba(color, opacity)],
|
[color_to_rgba(color, opacity)],
|
||||||
len(points),
|
len(points),
|
||||||
axis=0
|
axis=0
|
||||||
)
|
)
|
||||||
elif rgbas is not None:
|
if rgbas is not None:
|
||||||
new_rgbas = rgbas
|
self.data["rgbas"][-len(rgbas):] = rgbas
|
||||||
self.data["rgbas"][-len(new_rgbas):] = new_rgbas
|
return self
|
||||||
|
|
||||||
|
def add_point(self, point, rgba=None, color=None, opacity=None):
|
||||||
|
rgbas = None if rgba is None else [rgba]
|
||||||
|
self.add_points([point], rgbas, color, opacity)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_color_by_gradient(self, *colors: ManimColor):
|
def set_color_by_gradient(self, *colors: ManimColor):
|
||||||
|
|
|
@ -277,7 +277,6 @@ class DiscreteGraphScene(Scene):
|
||||||
def trace_cycle(self, cycle=None, color="yellow", run_time=2.0):
|
def trace_cycle(self, cycle=None, color="yellow", run_time=2.0):
|
||||||
if cycle is None:
|
if cycle is None:
|
||||||
cycle = self.graph.region_cycles[0]
|
cycle = self.graph.region_cycles[0]
|
||||||
time_per_edge = run_time / len(cycle)
|
|
||||||
next_in_cycle = it.cycle(cycle)
|
next_in_cycle = it.cycle(cycle)
|
||||||
next(next_in_cycle) # jump one ahead
|
next(next_in_cycle) # jump one ahead
|
||||||
self.traced_cycle = Mobject(*[
|
self.traced_cycle = Mobject(*[
|
||||||
|
|
|
@ -36,6 +36,7 @@ class Scene(object):
|
||||||
"end_at_animation_number": None,
|
"end_at_animation_number": None,
|
||||||
"leave_progress_bars": False,
|
"leave_progress_bars": False,
|
||||||
"preview": True,
|
"preview": True,
|
||||||
|
"presenter_mode": False,
|
||||||
"linger_after_completion": True,
|
"linger_after_completion": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,6 +63,7 @@ class Scene(object):
|
||||||
# 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 = not self.presenter_mode
|
||||||
|
|
||||||
# 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:
|
||||||
|
@ -114,7 +116,7 @@ class Scene(object):
|
||||||
if self.quit_interaction:
|
if self.quit_interaction:
|
||||||
self.unlock_mobject_data()
|
self.unlock_mobject_data()
|
||||||
|
|
||||||
def embed(self):
|
def embed(self, close_scene_on_exit=True):
|
||||||
if not self.preview:
|
if not self.preview:
|
||||||
# If the scene is just being
|
# If the scene is just being
|
||||||
# written, ignore embed calls
|
# written, ignore embed calls
|
||||||
|
@ -139,8 +141,9 @@ class Scene(object):
|
||||||
log.info("Tips: Now the embed iPython terminal is open. But you can't interact with"
|
log.info("Tips: Now the embed iPython terminal is open. But you can't interact with"
|
||||||
" the window directly. To do so, you need to type `touch()` or `self.interact()`")
|
" the window directly. To do so, you need to type `touch()` or `self.interact()`")
|
||||||
shell(local_ns=local_ns, stack_depth=2)
|
shell(local_ns=local_ns, stack_depth=2)
|
||||||
# End scene when exiting an embed.
|
# End scene when exiting an embed
|
||||||
raise EndSceneEarlyException()
|
if close_scene_on_exit:
|
||||||
|
raise EndSceneEarlyException()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.__class__.__name__
|
return self.__class__.__name__
|
||||||
|
@ -432,6 +435,11 @@ class Scene(object):
|
||||||
def unlock_mobject_data(self):
|
def unlock_mobject_data(self):
|
||||||
self.camera.release_static_mobjects()
|
self.camera.release_static_mobjects()
|
||||||
|
|
||||||
|
def refresh_locked_data(self):
|
||||||
|
self.unlock_mobject_data()
|
||||||
|
self.lock_static_mobject_data()
|
||||||
|
return self
|
||||||
|
|
||||||
def begin_animations(self, animations):
|
def begin_animations(self, animations):
|
||||||
for animation in animations:
|
for animation in animations:
|
||||||
animation.begin()
|
animation.begin()
|
||||||
|
@ -477,19 +485,30 @@ class Scene(object):
|
||||||
self.unlock_mobject_data()
|
self.unlock_mobject_data()
|
||||||
|
|
||||||
@handle_play_like_call
|
@handle_play_like_call
|
||||||
def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None):
|
def wait(self,
|
||||||
|
duration=DEFAULT_WAIT_TIME,
|
||||||
|
stop_condition=None,
|
||||||
|
note=None,
|
||||||
|
ignore_presenter_mode=False):
|
||||||
|
if note:
|
||||||
|
log.info(note)
|
||||||
self.update_mobjects(dt=0) # Any problems with this?
|
self.update_mobjects(dt=0) # Any problems with this?
|
||||||
self.lock_static_mobject_data()
|
self.lock_static_mobject_data()
|
||||||
time_progression = self.get_wait_time_progression(duration, stop_condition)
|
if self.presenter_mode and not self.skip_animations and not ignore_presenter_mode:
|
||||||
last_t = 0
|
while self.hold_on_wait:
|
||||||
for t in time_progression:
|
self.update_frame(dt=1 / self.camera.frame_rate)
|
||||||
dt = t - last_t
|
self.hold_on_wait = True
|
||||||
last_t = t
|
else:
|
||||||
self.update_frame(dt)
|
time_progression = self.get_wait_time_progression(duration, stop_condition)
|
||||||
self.emit_frame()
|
last_t = 0
|
||||||
if stop_condition is not None and stop_condition():
|
for t in time_progression:
|
||||||
time_progression.close()
|
dt = t - last_t
|
||||||
break
|
last_t = t
|
||||||
|
self.update_frame(dt)
|
||||||
|
self.emit_frame()
|
||||||
|
if stop_condition is not None and stop_condition():
|
||||||
|
time_progression.close()
|
||||||
|
break
|
||||||
self.unlock_mobject_data()
|
self.unlock_mobject_data()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@ -610,6 +629,10 @@ class Scene(object):
|
||||||
self.camera.frame.to_default_state()
|
self.camera.frame.to_default_state()
|
||||||
elif char == "q":
|
elif char == "q":
|
||||||
self.quit_interaction = True
|
self.quit_interaction = True
|
||||||
|
elif char == " ":
|
||||||
|
self.hold_on_wait = False
|
||||||
|
elif char == "e":
|
||||||
|
self.embed(close_scene_on_exit=False)
|
||||||
|
|
||||||
def on_resize(self, width: int, height: int):
|
def on_resize(self, width: int, height: int):
|
||||||
self.camera.reset_pixel_shape(width, height)
|
self.camera.reset_pixel_shape(width, height)
|
||||||
|
|
|
@ -287,9 +287,6 @@ class LinearTransformationScene(VectorScene):
|
||||||
},
|
},
|
||||||
"background_plane_kwargs": {
|
"background_plane_kwargs": {
|
||||||
"color": GREY,
|
"color": GREY,
|
||||||
"axis_config": {
|
|
||||||
"stroke_color": GREY_B,
|
|
||||||
},
|
|
||||||
"axis_config": {
|
"axis_config": {
|
||||||
"color": GREY,
|
"color": GREY,
|
||||||
},
|
},
|
||||||
|
|
|
@ -21,10 +21,10 @@ def bezier(
|
||||||
n = len(points) - 1
|
n = len(points) - 1
|
||||||
|
|
||||||
def result(t):
|
def result(t):
|
||||||
return sum([
|
return sum(
|
||||||
((1 - t)**(n - k)) * (t**k) * choose(n, k) * point
|
((1 - t)**(n - k)) * (t**k) * choose(n, k) * point
|
||||||
for k, point in enumerate(points)
|
for k, point in enumerate(points)
|
||||||
])
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import inspect
|
import inspect
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from scipy import special
|
import math
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,7 +10,11 @@ def sigmoid(x):
|
||||||
|
|
||||||
@lru_cache(maxsize=10)
|
@lru_cache(maxsize=10)
|
||||||
def choose(n, k):
|
def choose(n, k):
|
||||||
return special.comb(n, k, exact=True)
|
return math.comb(n, k)
|
||||||
|
|
||||||
|
|
||||||
|
def gen_choose(n, r):
|
||||||
|
return np.prod(np.arange(n, n - r, -1)) / math.factorial(r)
|
||||||
|
|
||||||
|
|
||||||
def get_num_args(function):
|
def get_num_args(function):
|
||||||
|
|
Loading…
Add table
Reference in a new issue