Skip to content

feat: Add three animations that together simulate a typing animation #3612

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

Merged
merged 11 commits into from
Apr 28, 2024
Merged
178 changes: 177 additions & 1 deletion 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",
"TypeWithCursor",
"UntypeWithCursor",
]


Expand All @@ -80,15 +82,16 @@ def construct(self):

if TYPE_CHECKING:
from manim.mobject.text.text_mobject import Text
from manim.scene.scene import Scene

from manim.constants import RIGHT, TAU
from manim.mobject.opengl.opengl_surface import OpenGLSurface
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
from manim.utils.color import ManimColor

from .. import config
from ..animation.animation import Animation
from ..animation.composition import Succession
from ..constants import TAU
from ..mobject.mobject import Group, Mobject
from ..mobject.types.vectorized_mobject import VMobject
from ..utils.bezier import integer_interpolate
Expand Down Expand Up @@ -674,3 +677,176 @@ def __init__(
)
)
super().__init__(*anims, **kwargs)


class TypeWithCursor(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.

.. 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(TypeWithCursor(text, cursor))
self.play(Blink(cursor, blinks=2))

"""

def __init__(
self,
text: Text,
cursor: Mobject,
buff: float = 0.1,
keep_cursor_y: bool = True,
leave_cursor_on: bool = True,
time_per_char: float = 0.1,
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,
time_per_char=time_per_char,
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 UntypeWithCursor(TypeWithCursor):
"""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.

.. 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(UntypeWithCursor(text, cursor))
self.play(Blink(cursor, blinks=2))

"""

def __init__(
self,
text: Text,
cursor: VMobject | None = None,
time_per_char: float = 0.1,
reverse_rate_function=True,
introducer=False,
remover=True,
**kwargs,
) -> None:
super().__init__(
text,
cursor=cursor,
time_per_char=time_per_char,
reverse_rate_function=reverse_rate_function,
introducer=introducer,
remover=remover,
**kwargs,
)
69 changes: 67 additions & 2 deletions manim/animation/indication.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def construct(self):
"ApplyWave",
"Circumscribe",
"Wiggle",
"Blink",
]

from typing import Callable, Iterable, Optional, Tuple, Type, Union
Expand All @@ -53,6 +54,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 All @@ -76,8 +78,6 @@ class FocusOn(Transform):
The color of the spotlight.
run_time
The duration of the animation.
kwargs
Additional arguments to be passed to the :class:`~.Succession` constructor

Examples
--------
Expand Down Expand Up @@ -643,3 +643,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.
blinks
The number of blinks
hide_at_end
Whether to 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, blinks=3))

"""

def __init__(
self,
mobject: Mobject,
time_on: float = 0.5,
time_off: float = 0.5,
blinks: int = 1,
hide_at_end: 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,
),
] * blinks

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

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