3b1b-manim/manimlib/animation/transform.py
Grant Sanderson 744e695340
Misc. clean up (#2269)
* 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
2024-12-12 08:39:54 -08:00

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