Skip to content

Commit 5fe2568

Browse files
committed
feat: Add animations that together simulate typing
AddTextLetterByLetterWithCursor RemoveTextLetterByLetterWithCursor Blink
1 parent 1520481 commit 5fe2568

File tree

2 files changed

+266
-2
lines changed

2 files changed

+266
-2
lines changed

manim/animation/creation.py

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,10 @@ def construct(self):
8181
if TYPE_CHECKING:
8282
from manim.mobject.text.text_mobject import Text
8383

84+
from manim.constants import RIGHT
8485
from manim.mobject.opengl.opengl_surface import OpenGLSurface
8586
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
87+
from manim.scene.scene import Scene
8688
from manim.utils.color import ManimColor
8789

8890
from .. import config
@@ -668,3 +670,204 @@ def __init__(
668670
)
669671
)
670672
super().__init__(*anims, **kwargs)
673+
674+
675+
class AddTextLetterByLetterWithCursor(AddTextLetterByLetter):
676+
"""Similar to :class:`~.AddTextLetterByLetter` , but with an additional cursor mobject at the end.
677+
678+
Parameters
679+
----------
680+
time_per_char
681+
Frequency of appearance of the letters.
682+
cursor
683+
:class:`~.Mobject` shown after the last added letter.
684+
buff
685+
Controls how far away the cursor is to the right of the last added letter.
686+
keep_cursor_y
687+
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.
688+
leave_cursor_on
689+
Whether to show the cursor after the animation.
690+
kwargs
691+
Additional arguments to be passed to the :class:`~.AddTextLetterByLetter` constructor.
692+
693+
.. tip::
694+
This is currently only possible for class:`~.Text` and not for class:`~.MathTex`.
695+
696+
Examples
697+
--------
698+
699+
.. manim:: TypingAnimation
700+
701+
class TypingAnimation(Scene):
702+
def construct(self):
703+
text = Text("Typing", color=PURPLE).scale(1.5).to_edge(LEFT)
704+
cursor = Rectangle(
705+
color = GREY_A,
706+
fill_color = GREY_A,
707+
fill_opacity = 1.0,
708+
height = 1.1,
709+
width = 0.5,
710+
).move_to(text[0]) # Position the cursor
711+
712+
self.play(Blink(cursor, how_many_times=2))
713+
self.play(AddTextLetterByLetterWithCursor(text, cursor, leave_cursor_on=False)) # Turning off the cursor is important
714+
self.play(Blink(cursor, how_many_times=3))
715+
self.play(RemoveTextLetterByLetterWithCursor(text, cursor))
716+
self.play(Blink(cursor, how_many_times=2, ends_with_off=True))
717+
718+
"""
719+
720+
def __init__(
721+
self,
722+
text: Text,
723+
cursor: Mobject,
724+
buff: float = 0.1,
725+
keep_cursor_y: bool = True,
726+
leave_cursor_on: bool = True,
727+
suspend_mobject_updating: bool = False,
728+
int_func: Callable[[np.ndarray], np.ndarray] = np.ceil,
729+
rate_func: Callable[[float], float] = linear,
730+
time_per_char: float = 0.1,
731+
run_time: float | None = None,
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+
suspend_mobject_updating=suspend_mobject_updating,
743+
int_func=int_func,
744+
rate_func=rate_func,
745+
time_per_char=time_per_char,
746+
run_time=run_time,
747+
reverse_rate_function=reverse_rate_function,
748+
introducer=introducer,
749+
**kwargs,
750+
)
751+
752+
def begin(self) -> None:
753+
self.y_cursor = self.cursor.get_y()
754+
self.cursor.initial_position = self.mobject.get_center()
755+
if self.keep_cursor_y:
756+
self.cursor.set_y(self.y_cursor)
757+
758+
self.cursor.set_opacity(0)
759+
self.mobject.add(self.cursor)
760+
super().begin()
761+
762+
def finish(self) -> None:
763+
if self.leave_cursor_on:
764+
self.cursor.set_opacity(1)
765+
else:
766+
self.cursor.set_opacity(0)
767+
self.mobject.remove(self.cursor)
768+
super().finish()
769+
770+
def clean_up_from_scene(self, scene: Scene) -> None:
771+
if not self.leave_cursor_on:
772+
scene.remove(self.cursor)
773+
super().clean_up_from_scene(scene)
774+
775+
def update_submobject_list(self, index: int) -> None:
776+
for mobj in self.all_submobs[:index]:
777+
mobj.set_opacity(1)
778+
779+
for mobj in self.all_submobs[index:]:
780+
mobj.set_opacity(0)
781+
782+
if index != 0:
783+
self.cursor.next_to(
784+
self.all_submobs[index - 1], RIGHT, buff=self.buff
785+
).set_y(self.cursor.initial_position[1])
786+
else:
787+
self.cursor.move_to(self.all_submobs[0]).set_y(
788+
self.cursor.initial_position[1]
789+
)
790+
791+
if self.keep_cursor_y:
792+
self.cursor.set_y(self.y_cursor)
793+
self.cursor.set_opacity(1)
794+
795+
796+
class RemoveTextLetterByLetterWithCursor(AddTextLetterByLetterWithCursor):
797+
"""Similar to :class:`~.RemoveTextLetterByLetter` , but with an additional cursor mobject at the end.
798+
799+
Parameters
800+
----------
801+
time_per_char
802+
Frequency of appearance of the letters.
803+
cursor
804+
:class:`~.Mobject` shown after the last added letter.
805+
buff
806+
Controls how far away the cursor is to the right of the last added letter.
807+
keep_cursor_y
808+
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.
809+
leave_cursor_on
810+
Whether to show the cursor after the animation.
811+
kwargs
812+
Additional arguments to be passed to the :class:`~.AddTextLetterByLetter` constructor.
813+
814+
.. tip::
815+
This is currently only possible for class:`~.Text` and not for class:`~.MathTex`.
816+
817+
Examples
818+
--------
819+
820+
.. manim:: TypingAnimation
821+
822+
class TypingAnimation(Scene):
823+
def construct(self):
824+
text = Text("Typing", color=PURPLE).scale(1.5).to_edge(LEFT)
825+
cursor = Rectangle(
826+
color = GREY_A,
827+
fill_color = GREY_A,
828+
fill_opacity = 1.0,
829+
height = 1.1,
830+
width = 0.5,
831+
).move_to(text[0]) # Position the cursor
832+
833+
self.play(Blink(cursor, how_many_times=2))
834+
self.play(AddTextLetterByLetterWithCursor(text, cursor, leave_cursor_on=False)) # Turning off the cursor is important
835+
self.play(Blink(cursor, how_many_times=3))
836+
self.play(RemoveTextLetterByLetterWithCursor(text, cursor))
837+
self.play(Blink(cursor, how_many_times=2, ends_with_off=True))
838+
839+
"""
840+
841+
def __init__(
842+
self,
843+
text: Text,
844+
cursor: VMobject | None = None,
845+
buff: float = 0.1,
846+
keep_cursor_y: bool = True,
847+
leave_cursor_on: bool = True,
848+
suspend_mobject_updating: bool = False,
849+
int_func: Callable[[np.ndarray], np.ndarray] = np.ceil,
850+
rate_func: Callable[[float], float] = linear,
851+
time_per_char: float = 0.1,
852+
run_time: float | None = None,
853+
reverse_rate_function=True,
854+
introducer=False,
855+
remover=True,
856+
**kwargs,
857+
) -> None:
858+
super().__init__(
859+
text,
860+
cursor=cursor,
861+
buff=buff,
862+
keep_cursor_y=keep_cursor_y,
863+
leave_cursor_on=leave_cursor_on,
864+
suspend_mobject_updating=suspend_mobject_updating,
865+
int_func=int_func,
866+
rate_func=rate_func,
867+
time_per_char=time_per_char,
868+
run_time=run_time,
869+
reverse_rate_function=reverse_rate_function,
870+
introducer=introducer,
871+
remover=remover,
872+
**kwargs,
873+
)

manim/animation/indication.py

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,12 @@ def construct(self):
4848
from manim.scene.scene import Scene
4949

5050
from .. import config
51-
from ..animation.animation import Animation
51+
from ..animation.animation import Animation, Wait
5252
from ..animation.composition import AnimationGroup, Succession
5353
from ..animation.creation import Create, ShowPartial, Uncreate
5454
from ..animation.fading import FadeIn, FadeOut
5555
from ..animation.movement import Homotopy
56-
from ..animation.transform import Transform
56+
from ..animation.transform import ApplyMethod, Transform
5757
from ..constants import *
5858
from ..mobject.mobject import Mobject
5959
from ..mobject.types.vectorized_mobject import VGroup, VMobject
@@ -654,3 +654,64 @@ def __init__(
654654
super().__init__(
655655
ShowPassingFlash(frame, time_width, run_time=run_time), **kwargs
656656
)
657+
658+
659+
class Blink(Succession):
660+
"""Blink the mobject.
661+
662+
Parameters
663+
----------
664+
mobject
665+
The mobject to be blinked.
666+
time_on
667+
The duration that the mobject is shown for one blink.
668+
time_off
669+
The duration that the mobject is hidden for one blink.
670+
how_many_times
671+
The number of blinks
672+
ends_with_off
673+
Whether to show or hide the mobject at the end of the animation.
674+
kwargs
675+
Additional arguments to be passed to the :class:`~.Succession` constructor.
676+
677+
Examples
678+
--------
679+
680+
.. manim:: BlinkingCursor
681+
682+
class BlinkingCursor(Scene):
683+
def construct(self):
684+
text = Text("Typing", color=PURPLE).scale(1.5)
685+
cursor = Rectangle(
686+
color = GREY_A,
687+
fill_color = GREY_A,
688+
fill_opacity = 1.0,
689+
height = 1.1,
690+
width = 0.5,
691+
).next_to(text, buff=0.1)
692+
693+
self.add(text)
694+
self.play(Blink(cursor, how_many_times=3))
695+
696+
"""
697+
698+
def __init__(
699+
self,
700+
mobject: Mobject,
701+
time_on: float = 0.5,
702+
time_off: float = 0.5,
703+
how_many_times: int = 1,
704+
ends_with_off: bool = False,
705+
**kwargs
706+
):
707+
animations = [
708+
ApplyMethod(mobject.set_opacity, 1.0, run_time=0.0),
709+
Wait(run_time=time_on),
710+
ApplyMethod(mobject.set_opacity, 0.0, run_time=0.0),
711+
Wait(run_time=time_off),
712+
] * how_many_times
713+
714+
if not ends_with_off:
715+
animations.append(ApplyMethod(mobject.set_opacity, 1.0, run_time=0.0))
716+
717+
super().__init__(*animations, **kwargs)

0 commit comments

Comments
 (0)