Skip to content

Commit 4a0c705

Browse files
authored
feat: Add three animations that together simulate a typing animation (#3612)
* feat: Add animations that together simulate typing AddTextLetterByLetterWithCursor RemoveTextLetterByLetterWithCursor Blink * Revert "feat: Add animations that together simulate typing" This reverts commit 5fe2568. * Revert "Revert "feat: Add animations that together simulate typing"" This reverts commit 6a8244a. * Add new animations to __all__ * Temporarily remove docs example * Modify "Blink" and add docstring examples back in To avoid 0-second animations, which fail docstring test * Address requested changes Fix imports Remove redundant constructor arguments Improve names * Shorten names
1 parent 33e5604 commit 4a0c705

File tree

2 files changed

+244
-3
lines changed

2 files changed

+244
-3
lines changed

manim/animation/creation.py

Lines changed: 177 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ def construct(self):
7070
"RemoveTextLetterByLetter",
7171
"ShowSubmobjectsOneByOne",
7272
"AddTextWordByWord",
73+
"TypeWithCursor",
74+
"UntypeWithCursor",
7375
]
7476

7577

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

8183
if TYPE_CHECKING:
8284
from manim.mobject.text.text_mobject import Text
85+
from manim.scene.scene import Scene
8386

87+
from manim.constants import RIGHT, TAU
8488
from manim.mobject.opengl.opengl_surface import OpenGLSurface
8589
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
8690
from manim.utils.color import ManimColor
8791

8892
from .. import config
8993
from ..animation.animation import Animation
9094
from ..animation.composition import Succession
91-
from ..constants import TAU
9295
from ..mobject.mobject import Group, Mobject
9396
from ..mobject.types.vectorized_mobject import VMobject
9497
from ..utils.bezier import integer_interpolate
@@ -674,3 +677,176 @@ def __init__(
674677
)
675678
)
676679
super().__init__(*anims, **kwargs)
680+
681+
682+
class TypeWithCursor(AddTextLetterByLetter):
683+
"""Similar to :class:`~.AddTextLetterByLetter` , but with an additional cursor mobject at the end.
684+
685+
Parameters
686+
----------
687+
time_per_char
688+
Frequency of appearance of the letters.
689+
cursor
690+
:class:`~.Mobject` shown after the last added letter.
691+
buff
692+
Controls how far away the cursor is to the right of the last added letter.
693+
keep_cursor_y
694+
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.
695+
leave_cursor_on
696+
Whether to show the cursor after the animation.
697+
698+
.. tip::
699+
This is currently only possible for class:`~.Text` and not for class:`~.MathTex`.
700+
701+
702+
Examples
703+
--------
704+
705+
.. manim:: InsertingTextExample
706+
:ref_classes: Blink
707+
708+
class InsertingTextExample(Scene):
709+
def construct(self):
710+
text = Text("Inserting", color=PURPLE).scale(1.5).to_edge(LEFT)
711+
cursor = Rectangle(
712+
color = GREY_A,
713+
fill_color = GREY_A,
714+
fill_opacity = 1.0,
715+
height = 1.1,
716+
width = 0.5,
717+
).move_to(text[0]) # Position the cursor
718+
719+
self.play(TypeWithCursor(text, cursor))
720+
self.play(Blink(cursor, blinks=2))
721+
722+
"""
723+
724+
def __init__(
725+
self,
726+
text: Text,
727+
cursor: Mobject,
728+
buff: float = 0.1,
729+
keep_cursor_y: bool = True,
730+
leave_cursor_on: bool = True,
731+
time_per_char: float = 0.1,
732+
reverse_rate_function=False,
733+
introducer=True,
734+
**kwargs,
735+
) -> None:
736+
self.cursor = cursor
737+
self.buff = buff
738+
self.keep_cursor_y = keep_cursor_y
739+
self.leave_cursor_on = leave_cursor_on
740+
super().__init__(
741+
text,
742+
time_per_char=time_per_char,
743+
reverse_rate_function=reverse_rate_function,
744+
introducer=introducer,
745+
**kwargs,
746+
)
747+
748+
def begin(self) -> None:
749+
self.y_cursor = self.cursor.get_y()
750+
self.cursor.initial_position = self.mobject.get_center()
751+
if self.keep_cursor_y:
752+
self.cursor.set_y(self.y_cursor)
753+
754+
self.cursor.set_opacity(0)
755+
self.mobject.add(self.cursor)
756+
super().begin()
757+
758+
def finish(self) -> None:
759+
if self.leave_cursor_on:
760+
self.cursor.set_opacity(1)
761+
else:
762+
self.cursor.set_opacity(0)
763+
self.mobject.remove(self.cursor)
764+
super().finish()
765+
766+
def clean_up_from_scene(self, scene: Scene) -> None:
767+
if not self.leave_cursor_on:
768+
scene.remove(self.cursor)
769+
super().clean_up_from_scene(scene)
770+
771+
def update_submobject_list(self, index: int) -> None:
772+
for mobj in self.all_submobs[:index]:
773+
mobj.set_opacity(1)
774+
775+
for mobj in self.all_submobs[index:]:
776+
mobj.set_opacity(0)
777+
778+
if index != 0:
779+
self.cursor.next_to(
780+
self.all_submobs[index - 1], RIGHT, buff=self.buff
781+
).set_y(self.cursor.initial_position[1])
782+
else:
783+
self.cursor.move_to(self.all_submobs[0]).set_y(
784+
self.cursor.initial_position[1]
785+
)
786+
787+
if self.keep_cursor_y:
788+
self.cursor.set_y(self.y_cursor)
789+
self.cursor.set_opacity(1)
790+
791+
792+
class UntypeWithCursor(TypeWithCursor):
793+
"""Similar to :class:`~.RemoveTextLetterByLetter` , but with an additional cursor mobject at the end.
794+
795+
Parameters
796+
----------
797+
time_per_char
798+
Frequency of appearance of the letters.
799+
cursor
800+
:class:`~.Mobject` shown after the last added letter.
801+
buff
802+
Controls how far away the cursor is to the right of the last added letter.
803+
keep_cursor_y
804+
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.
805+
leave_cursor_on
806+
Whether to show the cursor after the animation.
807+
808+
.. tip::
809+
This is currently only possible for class:`~.Text` and not for class:`~.MathTex`.
810+
811+
812+
Examples
813+
--------
814+
815+
.. manim:: DeletingTextExample
816+
:ref_classes: Blink
817+
818+
class DeletingTextExample(Scene):
819+
def construct(self):
820+
text = Text("Deleting", color=PURPLE).scale(1.5).to_edge(LEFT)
821+
cursor = Rectangle(
822+
color = GREY_A,
823+
fill_color = GREY_A,
824+
fill_opacity = 1.0,
825+
height = 1.1,
826+
width = 0.5,
827+
).move_to(text[0]) # Position the cursor
828+
829+
self.play(UntypeWithCursor(text, cursor))
830+
self.play(Blink(cursor, blinks=2))
831+
832+
"""
833+
834+
def __init__(
835+
self,
836+
text: Text,
837+
cursor: VMobject | None = None,
838+
time_per_char: float = 0.1,
839+
reverse_rate_function=True,
840+
introducer=False,
841+
remover=True,
842+
**kwargs,
843+
) -> None:
844+
super().__init__(
845+
text,
846+
cursor=cursor,
847+
time_per_char=time_per_char,
848+
reverse_rate_function=reverse_rate_function,
849+
introducer=introducer,
850+
remover=remover,
851+
**kwargs,
852+
)

manim/animation/indication.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def construct(self):
3434
"ApplyWave",
3535
"Circumscribe",
3636
"Wiggle",
37+
"Blink",
3738
]
3839

3940
from typing import Callable, Iterable, Optional, Tuple, Type, Union
@@ -53,6 +54,7 @@ def construct(self):
5354
from ..animation.fading import FadeIn, FadeOut
5455
from ..animation.movement import Homotopy
5556
from ..animation.transform import Transform
57+
from ..animation.updaters.update import UpdateFromFunc
5658
from ..constants import *
5759
from ..mobject.mobject import Mobject
5860
from ..mobject.types.vectorized_mobject import VGroup, VMobject
@@ -76,8 +78,6 @@ class FocusOn(Transform):
7678
The color of the spotlight.
7779
run_time
7880
The duration of the animation.
79-
kwargs
80-
Additional arguments to be passed to the :class:`~.Succession` constructor
8181
8282
Examples
8383
--------
@@ -643,3 +643,68 @@ def __init__(
643643
super().__init__(
644644
ShowPassingFlash(frame, time_width, run_time=run_time), **kwargs
645645
)
646+
647+
648+
class Blink(Succession):
649+
"""Blink the mobject.
650+
651+
Parameters
652+
----------
653+
mobject
654+
The mobject to be blinked.
655+
time_on
656+
The duration that the mobject is shown for one blink.
657+
time_off
658+
The duration that the mobject is hidden for one blink.
659+
blinks
660+
The number of blinks
661+
hide_at_end
662+
Whether to hide the mobject at the end of the animation.
663+
kwargs
664+
Additional arguments to be passed to the :class:`~.Succession` constructor.
665+
666+
Examples
667+
--------
668+
669+
.. manim:: BlinkingExample
670+
671+
class BlinkingExample(Scene):
672+
def construct(self):
673+
text = Text("Blinking").scale(1.5)
674+
self.add(text)
675+
self.play(Blink(text, blinks=3))
676+
677+
"""
678+
679+
def __init__(
680+
self,
681+
mobject: Mobject,
682+
time_on: float = 0.5,
683+
time_off: float = 0.5,
684+
blinks: int = 1,
685+
hide_at_end: bool = False,
686+
**kwargs
687+
):
688+
animations = [
689+
UpdateFromFunc(
690+
mobject,
691+
update_function=lambda mob: mob.set_opacity(1.0),
692+
run_time=time_on,
693+
),
694+
UpdateFromFunc(
695+
mobject,
696+
update_function=lambda mob: mob.set_opacity(0.0),
697+
run_time=time_off,
698+
),
699+
] * blinks
700+
701+
if not hide_at_end:
702+
animations.append(
703+
UpdateFromFunc(
704+
mobject,
705+
update_function=lambda mob: mob.set_opacity(1.0),
706+
run_time=time_on,
707+
),
708+
)
709+
710+
super().__init__(*animations, **kwargs)

0 commit comments

Comments
 (0)