mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +00:00

* Comment tweak * Directly print traceback Since the shell.showtraceback is giving some issues * Make InteracrtiveSceneEmbed into a class This way it can keep track of it's internal shell; use of get_ipython has a finicky relationship with reloading. * Move remaining checkpoint_paste logic into scene_embed.py This involved making a few context managers for Scene: temp_record, temp_skip, temp_progress_bar, which seem useful in and of themselves. * Change null key to be the empty string * Ensure temporary svg paths for Text are deleted * Remove unused dict_ops.py functions * Remove break_into_partial_movies from file_writer configuration * Rewrite guarantee_existence using Path * Clean up SceneFileWriter It had a number of vestigial functions no longer used, and some setup that could be made more organized. * Remove --save_pngs CLI arg (which did nothing) * Add --subdivide CLI arg * Remove add_extension_if_not_present * Remove get_sorted_integer_files * Have find_file return Path * Minor clean up * Clean up num_tex_symbols * Fix find_file * Minor cleanup for extract_scene.py * Add preview_frame_while_skipping option to scene config * Use shell.showtraceback function * Move keybindings to config, instead of in-place constants * Replace DEGREES -> DEG
331 lines
9.7 KiB
Python
331 lines
9.7 KiB
Python
from __future__ import annotations
|
|
|
|
import inspect
|
|
|
|
import numpy as np
|
|
|
|
from manimlib.animation.animation import Animation
|
|
from manimlib.constants import DEG
|
|
from manimlib.constants import OUT
|
|
from manimlib.mobject.mobject import Group
|
|
from manimlib.mobject.mobject import Mobject
|
|
from manimlib.utils.paths import path_along_arc
|
|
from manimlib.utils.paths import straight_path
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from typing import Callable
|
|
import numpy.typing as npt
|
|
from manimlib.scene.scene import Scene
|
|
from manimlib.typing import ManimColor
|
|
|
|
|
|
class Transform(Animation):
|
|
replace_mobject_with_target_in_scene: bool = False
|
|
|
|
def __init__(
|
|
self,
|
|
mobject: Mobject,
|
|
target_mobject: Mobject | None = None,
|
|
path_arc: float = 0.0,
|
|
path_arc_axis: np.ndarray = OUT,
|
|
path_func: Callable | None = None,
|
|
**kwargs
|
|
):
|
|
self.target_mobject = target_mobject
|
|
self.path_arc = path_arc
|
|
self.path_arc_axis = path_arc_axis
|
|
self.path_func = path_func
|
|
super().__init__(mobject, **kwargs)
|
|
self.init_path_func()
|
|
|
|
def init_path_func(self) -> None:
|
|
if self.path_func is not None:
|
|
return
|
|
elif self.path_arc == 0:
|
|
self.path_func = straight_path
|
|
else:
|
|
self.path_func = path_along_arc(
|
|
self.path_arc,
|
|
self.path_arc_axis,
|
|
)
|
|
|
|
def begin(self) -> None:
|
|
self.target_mobject = self.create_target()
|
|
self.check_target_mobject_validity()
|
|
|
|
if self.mobject.is_aligned_with(self.target_mobject):
|
|
self.target_copy = self.target_mobject
|
|
else:
|
|
# Use a copy of target_mobject for the align_data_and_family
|
|
# call so that the actual target_mobject stays
|
|
# preserved, since calling align_data will potentially
|
|
# change the structure of both arguments
|
|
self.target_copy = self.target_mobject.copy()
|
|
self.mobject.align_data_and_family(self.target_copy)
|
|
super().begin()
|
|
if not self.mobject.has_updaters():
|
|
self.mobject.lock_matching_data(
|
|
self.starting_mobject,
|
|
self.target_copy,
|
|
)
|
|
|
|
def finish(self) -> None:
|
|
super().finish()
|
|
self.mobject.unlock_data()
|
|
|
|
def create_target(self) -> Mobject:
|
|
# Has no meaningful effect here, but may be useful
|
|
# in subclasses
|
|
return self.target_mobject
|
|
|
|
def check_target_mobject_validity(self) -> None:
|
|
if self.target_mobject is None:
|
|
raise Exception(
|
|
f"{self.__class__.__name__}.create_target not properly implemented"
|
|
)
|
|
|
|
def clean_up_from_scene(self, scene: Scene) -> None:
|
|
super().clean_up_from_scene(scene)
|
|
if self.replace_mobject_with_target_in_scene:
|
|
scene.remove(self.mobject)
|
|
scene.add(self.target_mobject)
|
|
|
|
def update_config(self, **kwargs) -> None:
|
|
Animation.update_config(self, **kwargs)
|
|
if "path_arc" in kwargs:
|
|
self.path_func = path_along_arc(
|
|
kwargs["path_arc"],
|
|
kwargs.get("path_arc_axis", OUT)
|
|
)
|
|
|
|
def get_all_mobjects(self) -> list[Mobject]:
|
|
return [
|
|
self.mobject,
|
|
self.starting_mobject,
|
|
self.target_mobject,
|
|
self.target_copy,
|
|
]
|
|
|
|
def get_all_families_zipped(self) -> zip[tuple[Mobject]]:
|
|
return zip(*[
|
|
mob.get_family()
|
|
for mob in [
|
|
self.mobject,
|
|
self.starting_mobject,
|
|
self.target_copy,
|
|
]
|
|
])
|
|
|
|
def interpolate_submobject(
|
|
self,
|
|
submob: Mobject,
|
|
start: Mobject,
|
|
target_copy: Mobject,
|
|
alpha: float
|
|
):
|
|
submob.interpolate(start, target_copy, alpha, self.path_func)
|
|
return self
|
|
|
|
|
|
class ReplacementTransform(Transform):
|
|
replace_mobject_with_target_in_scene: bool = True
|
|
|
|
|
|
class TransformFromCopy(Transform):
|
|
replace_mobject_with_target_in_scene: bool = True
|
|
|
|
def __init__(self, mobject: Mobject, target_mobject: Mobject, **kwargs):
|
|
super().__init__(mobject.copy(), target_mobject, **kwargs)
|
|
|
|
|
|
class MoveToTarget(Transform):
|
|
def __init__(self, mobject: Mobject, **kwargs):
|
|
self.check_validity_of_input(mobject)
|
|
super().__init__(mobject, mobject.target, **kwargs)
|
|
|
|
def check_validity_of_input(self, mobject: Mobject) -> None:
|
|
if not hasattr(mobject, "target"):
|
|
raise Exception(
|
|
"MoveToTarget called on mobject without attribute 'target'"
|
|
)
|
|
|
|
|
|
class _MethodAnimation(MoveToTarget):
|
|
def __init__(self, mobject: Mobject, methods: list[Callable], **kwargs):
|
|
self.methods = methods
|
|
super().__init__(mobject, **kwargs)
|
|
|
|
|
|
class ApplyMethod(Transform):
|
|
def __init__(self, method: Callable, *args, **kwargs):
|
|
"""
|
|
method is a method of Mobject, *args are arguments for
|
|
that method. Key word arguments should be passed in
|
|
as the last arg, as a dict, since **kwargs is for
|
|
configuration of the transform itself
|
|
|
|
Relies on the fact that mobject methods return the mobject
|
|
"""
|
|
self.check_validity_of_input(method)
|
|
self.method = method
|
|
self.method_args = args
|
|
super().__init__(method.__self__, **kwargs)
|
|
|
|
def check_validity_of_input(self, method: Callable) -> None:
|
|
if not inspect.ismethod(method):
|
|
raise Exception(
|
|
"Whoops, looks like you accidentally invoked "
|
|
"the method you want to animate"
|
|
)
|
|
assert isinstance(method.__self__, Mobject)
|
|
|
|
def create_target(self) -> Mobject:
|
|
method = self.method
|
|
# Make sure it's a list so that args.pop() works
|
|
args = list(self.method_args)
|
|
|
|
if len(args) > 0 and isinstance(args[-1], dict):
|
|
method_kwargs = args.pop()
|
|
else:
|
|
method_kwargs = {}
|
|
target = method.__self__.copy()
|
|
method.__func__(target, *args, **method_kwargs)
|
|
return target
|
|
|
|
|
|
class ApplyPointwiseFunction(ApplyMethod):
|
|
def __init__(
|
|
self,
|
|
function: Callable[[np.ndarray], np.ndarray],
|
|
mobject: Mobject,
|
|
run_time: float = 3.0,
|
|
**kwargs
|
|
):
|
|
super().__init__(mobject.apply_function, function, run_time=run_time, **kwargs)
|
|
|
|
|
|
class ApplyPointwiseFunctionToCenter(Transform):
|
|
def __init__(
|
|
self,
|
|
function: Callable[[np.ndarray], np.ndarray],
|
|
mobject: Mobject,
|
|
**kwargs
|
|
):
|
|
self.function = function
|
|
super().__init__(mobject, **kwargs)
|
|
|
|
def create_target(self) -> Mobject:
|
|
return self.mobject.copy().move_to(self.function(self.mobject.get_center()))
|
|
|
|
|
|
class FadeToColor(ApplyMethod):
|
|
def __init__(
|
|
self,
|
|
mobject: Mobject,
|
|
color: ManimColor,
|
|
**kwargs
|
|
):
|
|
super().__init__(mobject.set_color, color, **kwargs)
|
|
|
|
|
|
class ScaleInPlace(ApplyMethod):
|
|
def __init__(
|
|
self,
|
|
mobject: Mobject,
|
|
scale_factor: npt.ArrayLike,
|
|
**kwargs
|
|
):
|
|
super().__init__(mobject.scale, scale_factor, **kwargs)
|
|
|
|
|
|
class ShrinkToCenter(ScaleInPlace):
|
|
def __init__(self, mobject: Mobject, **kwargs):
|
|
super().__init__(mobject, 0, **kwargs)
|
|
|
|
|
|
class Restore(Transform):
|
|
def __init__(self, mobject: Mobject, **kwargs):
|
|
if not hasattr(mobject, "saved_state") or mobject.saved_state is None:
|
|
raise Exception("Trying to restore without having saved")
|
|
super().__init__(mobject, mobject.saved_state, **kwargs)
|
|
|
|
|
|
class ApplyFunction(Transform):
|
|
def __init__(
|
|
self,
|
|
function: Callable[[Mobject], Mobject],
|
|
mobject: Mobject,
|
|
**kwargs
|
|
):
|
|
self.function = function
|
|
super().__init__(mobject, **kwargs)
|
|
|
|
def create_target(self) -> Mobject:
|
|
target = self.function(self.mobject.copy())
|
|
if not isinstance(target, Mobject):
|
|
raise Exception("Functions passed to ApplyFunction must return object of type Mobject")
|
|
return target
|
|
|
|
|
|
class ApplyMatrix(ApplyPointwiseFunction):
|
|
def __init__(
|
|
self,
|
|
matrix: npt.ArrayLike,
|
|
mobject: Mobject,
|
|
**kwargs
|
|
):
|
|
matrix = self.initialize_matrix(matrix)
|
|
|
|
def func(p):
|
|
return np.dot(p, matrix.T)
|
|
|
|
super().__init__(func, mobject, **kwargs)
|
|
|
|
def initialize_matrix(self, matrix: npt.ArrayLike) -> np.ndarray:
|
|
matrix = np.array(matrix)
|
|
if matrix.shape == (2, 2):
|
|
new_matrix = np.identity(3)
|
|
new_matrix[:2, :2] = matrix
|
|
matrix = new_matrix
|
|
elif matrix.shape != (3, 3):
|
|
raise Exception("Matrix has bad dimensions")
|
|
return matrix
|
|
|
|
|
|
class ApplyComplexFunction(ApplyMethod):
|
|
def __init__(
|
|
self,
|
|
function: Callable[[complex], complex],
|
|
mobject: Mobject,
|
|
**kwargs
|
|
):
|
|
self.function = function
|
|
method = mobject.apply_complex_function
|
|
super().__init__(method, function, **kwargs)
|
|
|
|
def init_path_func(self) -> None:
|
|
func1 = self.function(complex(1))
|
|
self.path_arc = np.log(func1).imag
|
|
super().init_path_func()
|
|
|
|
###
|
|
|
|
|
|
class CyclicReplace(Transform):
|
|
def __init__(self, *mobjects: Mobject, path_arc=90 * DEG, **kwargs):
|
|
super().__init__(Group(*mobjects), path_arc=path_arc, **kwargs)
|
|
|
|
def create_target(self) -> Mobject:
|
|
group = self.mobject
|
|
target = group.copy()
|
|
cycled_targets = [target[-1], *target[:-1]]
|
|
for m1, m2 in zip(cycled_targets, group):
|
|
m1.move_to(m2)
|
|
return target
|
|
|
|
|
|
class Swap(CyclicReplace):
|
|
"""Alternate name for CyclicReplace"""
|
|
pass
|