Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement :attr:.Mobject.always, and move builders to their own file #3852

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions manim/mobject/builders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Generic, TypeVar

if TYPE_CHECKING:
from typing_extensions import Self

from manim.animation.animation import Animation

Check failure

Code scanning / CodeQL

Module-level cyclic import Error

'Animation' may not be defined if module
manim.animation.animation
is imported before module
manim.mobject.builders
, as the
definition
of Animation occurs after the cyclic
import
of manim.mobject.builders.
'Animation' may not be defined if module
manim.animation.animation
is imported before module
manim.mobject.builders
, as the
definition
of Animation occurs after the cyclic
import
of manim.mobject.builders.
'Animation' may not be defined if module
manim.animation.animation
is imported before module
manim.mobject.builders
, as the
definition
of Animation occurs after the cyclic
import
of manim.mobject.builders.
'Animation' may not be defined if module
manim.animation.animation
is imported before module
manim.mobject.builders
, as the
definition
of Animation occurs after the cyclic
import
of manim.mobject.builders.
'Animation' may not be defined if module
manim.animation.animation
is imported before module
manim.mobject.builders
, as the
definition
of Animation occurs after the cyclic
import
of manim.mobject.builders.
from manim.mobject.mobject import Mobject

Check notice

Code scanning / CodeQL

Unused import Note

Import of 'Mobject' is not used.

Check failure

Code scanning / CodeQL

Module-level cyclic import Error

'Mobject' may not be defined if module
manim.mobject.mobject
is imported before module
manim.mobject.builders
, as the
definition
of Mobject occurs after the cyclic
import
of manim.mobject.builders.
'Mobject' may not be defined if module
manim.mobject.mobject
is imported before module
manim.mobject.builders
, as the
definition
of Mobject occurs after the cyclic
import
of manim.mobject.builders.
'Mobject' may not be defined if module
manim.mobject.mobject
is imported before module
manim.mobject.builders
, as the
definition
of Mobject occurs after the cyclic
import
of manim.mobject.builders.
from manim.mobject.opengl.opengl_mobject import OpenGLMobject

Check notice

Code scanning / CodeQL

Unused import Note

Import of 'OpenGLMobject' is not used.

Check failure

Code scanning / CodeQL

Module-level cyclic import Error

'OpenGLMobject' may not be defined if module
manim.mobject.opengl.opengl_mobject
is imported before module
manim.mobject.builders
, as the
definition
of OpenGLMobject occurs after the cyclic
import
of manim.mobject.builders.


T = TypeVar("T", bound="Mobject | OpenGLMobject")

__all__ = [
"_AnimationBuilder",
"_UpdaterBuilder",
]


class _AnimationBuilder(Generic[T]):
def __init__(self, mobject: T):
self.mobject = mobject
self.mobject.generate_target()

self.overridden_animation = None
self.is_chaining = False
self.methods = []

# Whether animation args can be passed
self.cannot_pass_args = False
self.anim_args = {}

def __call__(self, **kwargs) -> Self:
if self.cannot_pass_args:
raise ValueError(
"Animation arguments must be passed before accessing methods and can only be passed once",
)

self.anim_args = kwargs
self.cannot_pass_args = True

return self

def __getattr__(self, method_name: str):
method = getattr(self.mobject.target, method_name)
has_overridden_animation = hasattr(method, "_override_animate")

if (self.is_chaining and has_overridden_animation) or self.overridden_animation:
raise NotImplementedError(
"Method chaining is currently not supported for "
"overridden animations",
)

def update_target(*method_args, **method_kwargs):
if has_overridden_animation:
self.overridden_animation = method._override_animate(
self.mobject,
*method_args,
anim_args=self.anim_args,
**method_kwargs,
)
else:
self.methods.append([method, method_args, method_kwargs])
method(*method_args, **method_kwargs)
return self

self.is_chaining = True
self.cannot_pass_args = True

return update_target

def build(self) -> Animation:
from manim.animation.transform import _MethodAnimation
Dismissed Show dismissed Hide dismissed

if self.overridden_animation:
anim = self.overridden_animation
else:
anim = _MethodAnimation(self.mobject, self.methods)

for attr, value in self.anim_args.items():
setattr(anim, attr, value)

return anim


class _UpdaterBuilder(Generic[T]):
"""Syntactic sugar for adding updaters to mobjects."""

def __init__(self, mobject: T):
self._mobject = mobject

def __getattr__(self, name: str, /):
# just return a function that will add the updater
def add_updater(*method_args, **method_kwargs):
self._mobject.add_updater(
lambda m: getattr(m, name)(*method_args, **method_kwargs),
call_updater=True,
)
return self

return add_updater
107 changes: 38 additions & 69 deletions manim/mobject/mobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from ..utils.iterables import list_update, remove_list_redundancies
from ..utils.paths import straight_path
from ..utils.space_ops import angle_between_vectors, normalize, rotation_matrix
from .builders import _AnimationBuilder, _UpdaterBuilder
Fixed Show fixed Hide fixed

Check failure

Code scanning / CodeQL

Module-level cyclic import Error

'_AnimationBuilder' may not be defined if module
manim.mobject.builders
is imported before module
manim.mobject.mobject
, as the
definition
of _AnimationBuilder occurs after the cyclic
import
of manim.mobject.mobject.
'_AnimationBuilder' may not be defined if module
manim.mobject.builders
is imported before module
manim.mobject.mobject
, as the
definition
of _AnimationBuilder occurs after the cyclic
import
of manim.mobject.mobject.

Check failure

Code scanning / CodeQL

Module-level cyclic import Error

'_UpdaterBuilder' may not be defined if module
manim.mobject.builders
is imported before module
manim.mobject.mobject
, as the
definition
of _UpdaterBuilder occurs after the cyclic
import
of manim.mobject.mobject.
'_UpdaterBuilder' may not be defined if module
manim.mobject.builders
is imported before module
manim.mobject.mobject
, as the
definition
of _UpdaterBuilder occurs after the cyclic
import
of manim.mobject.mobject.

if TYPE_CHECKING:
from typing_extensions import Self, TypeAlias
Expand Down Expand Up @@ -294,7 +295,7 @@
cls.__init__ = cls._original__init__

@property
def animate(self) -> _AnimationBuilder | Self:
def animate(self) -> _AnimationBuilder[Self] | Self:
"""Used to animate the application of any method of :code:`self`.

Any method called on :code:`animate` is converted to an animation of applying
Expand Down Expand Up @@ -391,6 +392,42 @@
"""
return _AnimationBuilder(self)

@property
def always(self) -> Self:
"""Call a method on a mobject every frame.

This is syntactic sugar for ``mob.add_updater(lambda m: m.method(*args, **kwargs), call_updater=True)``.
Note that this will call the method immediately. If this behavior is not
desired, you should use :meth:`add_updater` directly.

.. warning::

Chaining of methods is allowed, but each method will be added
as its own updater. If you are chaining methods, make sure they
do not interfere with each other or you may get unexpected results.

.. warning::

:attr:`always` is not compatible with :meth:`.ValueTracker.get_value`, because
the value will be computed once and then never updated again. Use :meth:`add_updater`
if you would like to use a :class:`~.ValueTracker` to update the value.

Example
-------

.. manim:: AlwaysExample

class AlwaysExample(Scene):
def construct(self):
sq = Square().to_edge(LEFT)
t = Text("Hello World!")
t.always.next_to(sq, UP)
self.add(sq, t)
self.play(sq.animate.to_edge(RIGHT))
"""
# can't use typing.cast because Self is under TYPE_CHECKING
return _UpdaterBuilder(self) # type: ignore[misc]

def __deepcopy__(self, clone_from_id) -> Self:
cls = self.__class__
result = cls.__new__(cls)
Expand Down Expand Up @@ -3029,74 +3066,6 @@
self.add(*mobjects)


class _AnimationBuilder:
def __init__(self, mobject) -> None:
self.mobject = mobject
self.mobject.generate_target()

self.overridden_animation = None
self.is_chaining = False
self.methods = []

# Whether animation args can be passed
self.cannot_pass_args = False
self.anim_args = {}

def __call__(self, **kwargs) -> Self:
if self.cannot_pass_args:
raise ValueError(
"Animation arguments must be passed before accessing methods and can only be passed once",
)

self.anim_args = kwargs
self.cannot_pass_args = True

return self

def __getattr__(self, method_name) -> types.MethodType:
method = getattr(self.mobject.target, method_name)
has_overridden_animation = hasattr(method, "_override_animate")

if (self.is_chaining and has_overridden_animation) or self.overridden_animation:
raise NotImplementedError(
"Method chaining is currently not supported for "
"overridden animations",
)

def update_target(*method_args, **method_kwargs):
if has_overridden_animation:
self.overridden_animation = method._override_animate(
self.mobject,
*method_args,
anim_args=self.anim_args,
**method_kwargs,
)
else:
self.methods.append([method, method_args, method_kwargs])
method(*method_args, **method_kwargs)
return self

self.is_chaining = True
self.cannot_pass_args = True

return update_target

def build(self) -> Animation:
from ..animation.transform import ( # is this to prevent circular import?
_MethodAnimation,
)

if self.overridden_animation:
anim = self.overridden_animation
else:
anim = _MethodAnimation(self.mobject, self.methods)

for attr, value in self.anim_args.items():
setattr(anim, attr, value)

return anim


def override_animate(method) -> types.FunctionType:
r"""Decorator for overriding method animations.

Expand Down
37 changes: 37 additions & 0 deletions manim/mobject/opengl/opengl_mobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from manim import config, logger
from manim.constants import *
from manim.mobject.builders import _UpdaterBuilder

Check failure

Code scanning / CodeQL

Module-level cyclic import Error

'_UpdaterBuilder' may not be defined if module
manim.mobject.builders
is imported before module
manim.mobject.opengl.opengl_mobject
, as the
definition
of _UpdaterBuilder occurs after the cyclic
import
of manim.mobject.opengl.opengl_mobject.
'_UpdaterBuilder' may not be defined if module
manim.mobject.builders
is imported before module
manim.mobject.opengl.opengl_mobject
, as the
definition
of _UpdaterBuilder occurs after the cyclic
import
of manim.mobject.opengl.opengl_mobject.
'_UpdaterBuilder' may not be defined if module
manim.mobject.builders
is imported before module
manim.mobject.opengl.opengl_mobject
, as the
definition
of _UpdaterBuilder occurs after the cyclic
import
of manim.mobject.opengl.opengl_mobject.
from manim.renderer.shader_wrapper import get_colormap_code
from manim.utils.bezier import integer_interpolate, interpolate
from manim.utils.color import (
Expand Down Expand Up @@ -474,6 +475,42 @@
"""
return _AnimationBuilder(self)

@property
def always(self) -> Self:
"""Call a method on a mobject every frame.

This is syntactic sugar for ``mob.add_updater(lambda m: m.method(*args, **kwargs), call_updater=True)``.
Note that this will call the method immediately. If this behavior is not
desired, you should use :meth:`add_updater` directly.

.. warning::

Chaining of methods is allowed, but each method will be added
as its own updater. If you are chaining methods, make sure they
do not interfere with each other or you may get unexpected results.

.. warning::

:attr:`always` is not compatible with :meth:`.ValueTracker.get_value`, because
the value will be computed once and then never updated again. Use :meth:`add_updater`
if you would like to use a :class:`~.ValueTracker` to update the value.

Example
-------

.. manim:: AlwaysExample

class AlwaysExample(Scene):
def construct(self):
sq = Square().to_edge(LEFT)
t = Text("Hello World!")
t.always.next_to(sq, UP)
self.add(sq, t)
self.play(sq.animate.to_edge(RIGHT))
"""
# can't use typing.cast because Self is under TYPE_CHECKING
return _UpdaterBuilder(self) # type: ignore[misc]

@property
def width(self) -> float:
"""The width of the mobject.
Expand Down
7 changes: 7 additions & 0 deletions tests/test_graphical_units/test_updaters.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,10 @@ def test_LastFrameWhenCleared(scene):
scene.play(dot.animate.shift(UP * 2), rate_func=linear)
square.clear_updaters()
scene.wait()


def test_always():
d = Dot()
sq = Square()
d.always.next_to(sq, UP)
assert len(d.updaters) == 1
Loading