Skip to content
Merged
203 changes: 203 additions & 0 deletions manim/animation/creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ def construct(self):
"RemoveTextLetterByLetter",
"ShowSubmobjectsOneByOne",
"AddTextWordByWord",
"AddTextLetterByLetterWithCursor",
"RemoveTextLetterByLetterWithCursor",
]


Expand All @@ -81,8 +83,10 @@ def construct(self):
if TYPE_CHECKING:
from manim.mobject.text.text_mobject import Text

from manim.constants import RIGHT
from manim.mobject.opengl.opengl_surface import OpenGLSurface
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
from manim.scene.scene import Scene
from manim.utils.color import ManimColor

from .. import config
Expand Down Expand Up @@ -668,3 +672,202 @@ def __init__(
)
)
super().__init__(*anims, **kwargs)


class AddTextLetterByLetterWithCursor(AddTextLetterByLetter):
"""Similar to :class:`~.AddTextLetterByLetter` , but with an additional cursor mobject at the end.

Parameters
----------
time_per_char
Frequency of appearance of the letters.
cursor
:class:`~.Mobject` shown after the last added letter.
buff
Controls how far away the cursor is to the right of the last added letter.
keep_cursor_y
If ``True``, the cursor's y-coordinate is set to the center of the ``Text`` and remains the same throughout the animation. Otherwise, it is set to the center of the last added letter.
leave_cursor_on
Whether to show the cursor after the animation.
kwargs
Additional arguments to be passed to the :class:`~.AddTextLetterByLetter` constructor.

.. tip::
This is currently only possible for class:`~.Text` and not for class:`~.MathTex`.


Examples
--------

.. manim:: InsertingTextExample
:ref_classes: Blink

class InsertingTextExample(Scene):
def construct(self):
text = Text("Inserting", color=PURPLE).scale(1.5).to_edge(LEFT)
cursor = Rectangle(
color = GREY_A,
fill_color = GREY_A,
fill_opacity = 1.0,
height = 1.1,
width = 0.5,
).move_to(text[0]) # Position the cursor

self.play(AddTextLetterByLetterWithCursor(text, cursor))
self.play(Blink(cursor, how_many_times=2))

"""

def __init__(
self,
text: Text,
cursor: Mobject,
buff: float = 0.1,
keep_cursor_y: bool = True,
leave_cursor_on: bool = True,
suspend_mobject_updating: bool = False,
int_func: Callable[[np.ndarray], np.ndarray] = np.ceil,
rate_func: Callable[[float], float] = linear,
time_per_char: float = 0.1,
run_time: float | None = None,
reverse_rate_function=False,
introducer=True,
**kwargs,
) -> None:
self.cursor = cursor
self.buff = buff
self.keep_cursor_y = keep_cursor_y
self.leave_cursor_on = leave_cursor_on
super().__init__(
text,
suspend_mobject_updating=suspend_mobject_updating,
int_func=int_func,
rate_func=rate_func,
time_per_char=time_per_char,
run_time=run_time,
reverse_rate_function=reverse_rate_function,
introducer=introducer,
**kwargs,
)

def begin(self) -> None:
self.y_cursor = self.cursor.get_y()
self.cursor.initial_position = self.mobject.get_center()
if self.keep_cursor_y:
self.cursor.set_y(self.y_cursor)

self.cursor.set_opacity(0)
self.mobject.add(self.cursor)
super().begin()

def finish(self) -> None:
if self.leave_cursor_on:
self.cursor.set_opacity(1)
else:
self.cursor.set_opacity(0)
self.mobject.remove(self.cursor)
super().finish()

def clean_up_from_scene(self, scene: Scene) -> None:
if not self.leave_cursor_on:
scene.remove(self.cursor)
super().clean_up_from_scene(scene)

def update_submobject_list(self, index: int) -> None:
for mobj in self.all_submobs[:index]:
mobj.set_opacity(1)

for mobj in self.all_submobs[index:]:
mobj.set_opacity(0)

if index != 0:
self.cursor.next_to(
self.all_submobs[index - 1], RIGHT, buff=self.buff
).set_y(self.cursor.initial_position[1])
else:
self.cursor.move_to(self.all_submobs[0]).set_y(
self.cursor.initial_position[1]
)

if self.keep_cursor_y:
self.cursor.set_y(self.y_cursor)
self.cursor.set_opacity(1)


class RemoveTextLetterByLetterWithCursor(AddTextLetterByLetterWithCursor):
"""Similar to :class:`~.RemoveTextLetterByLetter` , but with an additional cursor mobject at the end.

Parameters
----------
time_per_char
Frequency of appearance of the letters.
cursor
:class:`~.Mobject` shown after the last added letter.
buff
Controls how far away the cursor is to the right of the last added letter.
keep_cursor_y
If ``True``, the cursor's y-coordinate is set to the center of the ``Text`` and remains the same throughout the animation. Otherwise, it is set to the center of the last added letter.
leave_cursor_on
Whether to show the cursor after the animation.
kwargs
Additional arguments to be passed to the :class:`~.AddTextLetterByLetter` constructor.

.. tip::
This is currently only possible for class:`~.Text` and not for class:`~.MathTex`.


Examples
--------

.. manim:: DeletingTextExample
:ref_classes: Blink

class DeletingTextExample(Scene):
def construct(self):
text = Text("Deleting", color=PURPLE).scale(1.5).to_edge(LEFT)
cursor = Rectangle(
color = GREY_A,
fill_color = GREY_A,
fill_opacity = 1.0,
height = 1.1,
width = 0.5,
).move_to(text[0]) # Position the cursor

self.play(RemoveTextLetterByLetterWithCursor(text, cursor))
self.play(Blink(cursor, how_many_times=2))

"""

def __init__(
self,
text: Text,
cursor: VMobject | None = None,
buff: float = 0.1,
keep_cursor_y: bool = True,
leave_cursor_on: bool = True,
suspend_mobject_updating: bool = False,
int_func: Callable[[np.ndarray], np.ndarray] = np.ceil,
rate_func: Callable[[float], float] = linear,
time_per_char: float = 0.1,
run_time: float | None = None,
reverse_rate_function=True,
introducer=False,
remover=True,
**kwargs,
) -> None:
super().__init__(
text,
cursor=cursor,
buff=buff,
keep_cursor_y=keep_cursor_y,
leave_cursor_on=leave_cursor_on,
suspend_mobject_updating=suspend_mobject_updating,
int_func=int_func,
rate_func=rate_func,
time_per_char=time_per_char,
run_time=run_time,
reverse_rate_function=reverse_rate_function,
introducer=introducer,
remover=remover,
**kwargs,
)
67 changes: 67 additions & 0 deletions manim/animation/indication.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def construct(self):
"ApplyWave",
"Circumscribe",
"Wiggle",
"Blink",
]

from typing import Callable, Iterable, Optional, Tuple, Type, Union
Expand All @@ -54,6 +55,7 @@ def construct(self):
from ..animation.fading import FadeIn, FadeOut
from ..animation.movement import Homotopy
from ..animation.transform import Transform
from ..animation.updaters.update import UpdateFromFunc
from ..constants import *
from ..mobject.mobject import Mobject
from ..mobject.types.vectorized_mobject import VGroup, VMobject
Expand Down Expand Up @@ -654,3 +656,68 @@ def __init__(
super().__init__(
ShowPassingFlash(frame, time_width, run_time=run_time), **kwargs
)


class Blink(Succession):
"""Blink the mobject.

Parameters
----------
mobject
The mobject to be blinked.
time_on
The duration that the mobject is shown for one blink.
time_off
The duration that the mobject is hidden for one blink.
how_many_times
The number of blinks
ends_with_off
Whether to show or hide the mobject at the end of the animation.
kwargs
Additional arguments to be passed to the :class:`~.Succession` constructor.

Examples
--------

.. manim:: BlinkingExample

class BlinkingExample(Scene):
def construct(self):
text = Text("Blinking").scale(1.5)
self.add(text)
self.play(Blink(text, how_many_times=3))

"""

def __init__(
self,
mobject: Mobject,
time_on: float = 0.5,
time_off: float = 0.5,
how_many_times: int = 1,
ends_with_off: bool = False,
**kwargs
):
animations = [
UpdateFromFunc(
mobject,
update_function=lambda mob: mob.set_opacity(1.0),
run_time=time_on,
),
UpdateFromFunc(
mobject,
update_function=lambda mob: mob.set_opacity(0.0),
run_time=time_off,
),
] * how_many_times

if not ends_with_off:
animations.append(
UpdateFromFunc(
mobject,
update_function=lambda mob: mob.set_opacity(1.0),
run_time=time_on,
),
)

super().__init__(*animations, **kwargs)