manim 0.18.0.post0__py3-none-any.whl → 0.19.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of manim might be problematic. Click here for more details.

Files changed (146) hide show
  1. manim/__init__.py +3 -6
  2. manim/__main__.py +61 -20
  3. manim/_config/__init__.py +6 -3
  4. manim/_config/cli_colors.py +16 -8
  5. manim/_config/default.cfg +1 -3
  6. manim/_config/logger_utils.py +14 -8
  7. manim/_config/utils.py +651 -472
  8. manim/animation/animation.py +152 -5
  9. manim/animation/composition.py +80 -39
  10. manim/animation/creation.py +196 -14
  11. manim/animation/fading.py +5 -9
  12. manim/animation/indication.py +103 -47
  13. manim/animation/movement.py +22 -5
  14. manim/animation/rotation.py +3 -2
  15. manim/animation/specialized.py +4 -6
  16. manim/animation/speedmodifier.py +10 -5
  17. manim/animation/transform.py +4 -5
  18. manim/animation/transform_matching_parts.py +1 -1
  19. manim/animation/updaters/mobject_update_utils.py +17 -14
  20. manim/camera/camera.py +15 -6
  21. manim/cli/__init__.py +17 -0
  22. manim/cli/cfg/group.py +70 -44
  23. manim/cli/checkhealth/checks.py +93 -75
  24. manim/cli/checkhealth/commands.py +14 -5
  25. manim/cli/default_group.py +157 -25
  26. manim/cli/init/commands.py +32 -24
  27. manim/cli/plugins/commands.py +16 -3
  28. manim/cli/render/commands.py +72 -60
  29. manim/cli/render/ease_of_access_options.py +4 -3
  30. manim/cli/render/global_options.py +51 -15
  31. manim/cli/render/output_options.py +6 -5
  32. manim/cli/render/render_options.py +97 -32
  33. manim/constants.py +65 -19
  34. manim/gui/gui.py +2 -0
  35. manim/mobject/frame.py +0 -1
  36. manim/mobject/geometry/arc.py +112 -78
  37. manim/mobject/geometry/boolean_ops.py +32 -25
  38. manim/mobject/geometry/labeled.py +300 -77
  39. manim/mobject/geometry/line.py +132 -64
  40. manim/mobject/geometry/polygram.py +126 -30
  41. manim/mobject/geometry/shape_matchers.py +35 -15
  42. manim/mobject/geometry/tips.py +38 -29
  43. manim/mobject/graph.py +414 -133
  44. manim/mobject/graphing/coordinate_systems.py +126 -64
  45. manim/mobject/graphing/functions.py +25 -15
  46. manim/mobject/graphing/number_line.py +24 -10
  47. manim/mobject/graphing/probability.py +2 -10
  48. manim/mobject/graphing/scale.py +6 -5
  49. manim/mobject/matrix.py +17 -19
  50. manim/mobject/mobject.py +314 -165
  51. manim/mobject/opengl/opengl_compatibility.py +2 -0
  52. manim/mobject/opengl/opengl_geometry.py +30 -9
  53. manim/mobject/opengl/opengl_image_mobject.py +2 -0
  54. manim/mobject/opengl/opengl_mobject.py +509 -343
  55. manim/mobject/opengl/opengl_point_cloud_mobject.py +5 -7
  56. manim/mobject/opengl/opengl_surface.py +3 -2
  57. manim/mobject/opengl/opengl_three_dimensions.py +2 -0
  58. manim/mobject/opengl/opengl_vectorized_mobject.py +46 -79
  59. manim/mobject/svg/brace.py +63 -13
  60. manim/mobject/svg/svg_mobject.py +4 -3
  61. manim/mobject/table.py +11 -13
  62. manim/mobject/text/code_mobject.py +186 -548
  63. manim/mobject/text/numbers.py +9 -7
  64. manim/mobject/text/tex_mobject.py +23 -14
  65. manim/mobject/text/text_mobject.py +70 -24
  66. manim/mobject/three_d/polyhedra.py +98 -1
  67. manim/mobject/three_d/three_d_utils.py +4 -4
  68. manim/mobject/three_d/three_dimensions.py +62 -34
  69. manim/mobject/types/image_mobject.py +42 -24
  70. manim/mobject/types/point_cloud_mobject.py +105 -67
  71. manim/mobject/types/vectorized_mobject.py +496 -228
  72. manim/mobject/value_tracker.py +5 -4
  73. manim/mobject/vector_field.py +5 -5
  74. manim/opengl/__init__.py +3 -3
  75. manim/plugins/__init__.py +14 -1
  76. manim/plugins/plugins_flags.py +14 -8
  77. manim/renderer/cairo_renderer.py +20 -10
  78. manim/renderer/opengl_renderer.py +21 -23
  79. manim/renderer/opengl_renderer_window.py +2 -0
  80. manim/renderer/shader.py +2 -3
  81. manim/renderer/shader_wrapper.py +5 -2
  82. manim/renderer/vectorized_mobject_rendering.py +5 -0
  83. manim/scene/moving_camera_scene.py +23 -0
  84. manim/scene/scene.py +90 -43
  85. manim/scene/scene_file_writer.py +316 -165
  86. manim/scene/section.py +17 -15
  87. manim/scene/three_d_scene.py +13 -21
  88. manim/scene/vector_space_scene.py +22 -9
  89. manim/typing.py +830 -70
  90. manim/utils/bezier.py +1667 -399
  91. manim/utils/caching.py +13 -5
  92. manim/utils/color/AS2700.py +2 -0
  93. manim/utils/color/BS381.py +3 -0
  94. manim/utils/color/DVIPSNAMES.py +96 -0
  95. manim/utils/color/SVGNAMES.py +179 -0
  96. manim/utils/color/X11.py +3 -0
  97. manim/utils/color/XKCD.py +3 -0
  98. manim/utils/color/__init__.py +8 -5
  99. manim/utils/color/core.py +844 -309
  100. manim/utils/color/manim_colors.py +7 -9
  101. manim/utils/commands.py +48 -20
  102. manim/utils/config_ops.py +18 -13
  103. manim/utils/debug.py +8 -7
  104. manim/utils/deprecation.py +90 -40
  105. manim/utils/docbuild/__init__.py +17 -0
  106. manim/utils/docbuild/autoaliasattr_directive.py +234 -0
  107. manim/utils/docbuild/autocolor_directive.py +21 -17
  108. manim/utils/docbuild/manim_directive.py +50 -35
  109. manim/utils/docbuild/module_parsing.py +245 -0
  110. manim/utils/exceptions.py +6 -0
  111. manim/utils/family.py +5 -3
  112. manim/utils/family_ops.py +17 -4
  113. manim/utils/file_ops.py +26 -16
  114. manim/utils/hashing.py +9 -7
  115. manim/utils/images.py +10 -4
  116. manim/utils/ipython_magic.py +14 -8
  117. manim/utils/iterables.py +161 -119
  118. manim/utils/module_ops.py +57 -19
  119. manim/utils/opengl.py +83 -24
  120. manim/utils/parameter_parsing.py +32 -0
  121. manim/utils/paths.py +21 -23
  122. manim/utils/polylabel.py +168 -0
  123. manim/utils/qhull.py +218 -0
  124. manim/utils/rate_functions.py +74 -39
  125. manim/utils/simple_functions.py +24 -15
  126. manim/utils/sounds.py +7 -1
  127. manim/utils/space_ops.py +125 -69
  128. manim/utils/testing/__init__.py +17 -0
  129. manim/utils/testing/_frames_testers.py +13 -8
  130. manim/utils/testing/_show_diff.py +5 -3
  131. manim/utils/testing/_test_class_makers.py +33 -18
  132. manim/utils/testing/frames_comparison.py +27 -19
  133. manim/utils/tex.py +127 -197
  134. manim/utils/tex_file_writing.py +47 -45
  135. manim/utils/tex_templates.py +2 -1
  136. manim/utils/unit.py +6 -5
  137. {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/LICENSE.community +1 -1
  138. {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/METADATA +40 -39
  139. manim-0.19.0.dist-info/RECORD +221 -0
  140. {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/WHEEL +1 -1
  141. manim/cli/new/__init__.py +0 -0
  142. manim/cli/new/group.py +0 -189
  143. manim/plugins/import_plugins.py +0 -43
  144. manim-0.18.0.post0.dist-info/RECORD +0 -217
  145. {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/LICENSE +0 -0
  146. {manim-0.18.0.post0.dist-info → manim-0.19.0.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,5 @@
1
1
  """Animate mobjects."""
2
2
 
3
-
4
3
  from __future__ import annotations
5
4
 
6
5
  from manim.mobject.opengl.opengl_mobject import OpenGLMobject
@@ -8,15 +7,19 @@ from manim.mobject.opengl.opengl_mobject import OpenGLMobject
8
7
  from .. import config, logger
9
8
  from ..constants import RendererType
10
9
  from ..mobject import mobject
11
- from ..mobject.mobject import Mobject
10
+ from ..mobject.mobject import Group, Mobject
12
11
  from ..mobject.opengl import opengl_mobject
13
12
  from ..utils.rate_functions import linear, smooth
14
13
 
15
- __all__ = ["Animation", "Wait", "override_animation"]
14
+ __all__ = ["Animation", "Wait", "Add", "override_animation"]
16
15
 
17
16
 
17
+ from collections.abc import Iterable, Sequence
18
18
  from copy import deepcopy
19
- from typing import TYPE_CHECKING, Callable, Iterable, Sequence
19
+ from functools import partialmethod
20
+ from typing import TYPE_CHECKING, Any, Callable
21
+
22
+ from typing_extensions import Self
20
23
 
21
24
  if TYPE_CHECKING:
22
25
  from manim.scene.scene import Scene
@@ -112,7 +115,7 @@ class Animation:
112
115
  *args,
113
116
  use_override=True,
114
117
  **kwargs,
115
- ):
118
+ ) -> Self:
116
119
  if isinstance(mobject, Mobject) and use_override:
117
120
  func = mobject.animation_override_for(cls)
118
121
  if func is not None:
@@ -169,6 +172,19 @@ class Animation:
169
172
  ),
170
173
  )
171
174
 
175
+ @property
176
+ def run_time(self) -> float:
177
+ return self._run_time
178
+
179
+ @run_time.setter
180
+ def run_time(self, value: float) -> None:
181
+ if value < 0:
182
+ raise ValueError(
183
+ f"The run_time of {self.__class__.__name__} cannot be "
184
+ f"negative. The given value was {value}."
185
+ )
186
+ self._run_time = value
187
+
172
188
  def _typecheck_input(self, mobject: Mobject | None) -> None:
173
189
  if mobject is None:
174
190
  logger.debug("Animation with empty mobject")
@@ -398,6 +414,7 @@ class Animation:
398
414
  self.run_time = run_time
399
415
  return self
400
416
 
417
+ # TODO: is this getter even necessary?
401
418
  def get_run_time(self) -> float:
402
419
  """Get the run time of the animation.
403
420
 
@@ -476,6 +493,52 @@ class Animation:
476
493
  """
477
494
  return self.introducer
478
495
 
496
+ @classmethod
497
+ def __init_subclass__(cls, **kwargs) -> None:
498
+ super().__init_subclass__(**kwargs)
499
+
500
+ cls._original__init__ = cls.__init__
501
+
502
+ @classmethod
503
+ def set_default(cls, **kwargs) -> None:
504
+ """Sets the default values of keyword arguments.
505
+
506
+ If this method is called without any additional keyword
507
+ arguments, the original default values of the initialization
508
+ method of this class are restored.
509
+
510
+ Parameters
511
+ ----------
512
+
513
+ kwargs
514
+ Passing any keyword argument will update the default
515
+ values of the keyword arguments of the initialization
516
+ function of this class.
517
+
518
+ Examples
519
+ --------
520
+
521
+ .. manim:: ChangeDefaultAnimation
522
+
523
+ class ChangeDefaultAnimation(Scene):
524
+ def construct(self):
525
+ Rotate.set_default(run_time=2, rate_func=rate_functions.linear)
526
+ Indicate.set_default(color=None)
527
+
528
+ S = Square(color=BLUE, fill_color=BLUE, fill_opacity=0.25)
529
+ self.add(S)
530
+ self.play(Rotate(S, PI))
531
+ self.play(Indicate(S))
532
+
533
+ Rotate.set_default()
534
+ Indicate.set_default()
535
+
536
+ """
537
+ if kwargs:
538
+ cls.__init__ = partialmethod(cls.__init__, **kwargs)
539
+ else:
540
+ cls.__init__ = cls._original__init__
541
+
479
542
 
480
543
  def prepare_animation(
481
544
  anim: Animation | mobject._AnimationBuilder,
@@ -575,6 +638,90 @@ class Wait(Animation):
575
638
  pass
576
639
 
577
640
 
641
+ class Add(Animation):
642
+ """Add Mobjects to a scene, without animating them in any other way. This
643
+ is similar to the :meth:`.Scene.add` method, but :class:`Add` is an
644
+ animation which can be grouped into other animations.
645
+
646
+ Parameters
647
+ ----------
648
+ mobjects
649
+ One :class:`~.Mobject` or more to add to a scene.
650
+ run_time
651
+ The duration of the animation after adding the ``mobjects``. Defaults
652
+ to 0, which means this is an instant animation without extra wait time
653
+ after adding them.
654
+ **kwargs
655
+ Additional arguments to pass to the parent :class:`Animation` class.
656
+
657
+ Examples
658
+ --------
659
+
660
+ .. manim:: DefaultAddScene
661
+
662
+ class DefaultAddScene(Scene):
663
+ def construct(self):
664
+ text_1 = Text("I was added with Add!")
665
+ text_2 = Text("Me too!")
666
+ text_3 = Text("And me!")
667
+ texts = VGroup(text_1, text_2, text_3).arrange(DOWN)
668
+ rect = SurroundingRectangle(texts, buff=0.5)
669
+
670
+ self.play(
671
+ Create(rect, run_time=3.0),
672
+ Succession(
673
+ Wait(1.0),
674
+ # You can Add a Mobject in the middle of an animation...
675
+ Add(text_1),
676
+ Wait(1.0),
677
+ # ...or multiple Mobjects at once!
678
+ Add(text_2, text_3),
679
+ ),
680
+ )
681
+ self.wait()
682
+
683
+ .. manim:: AddWithRunTimeScene
684
+
685
+ class AddWithRunTimeScene(Scene):
686
+ def construct(self):
687
+ # A 5x5 grid of circles
688
+ circles = VGroup(
689
+ *[Circle(radius=0.5) for _ in range(25)]
690
+ ).arrange_in_grid(5, 5)
691
+
692
+ self.play(
693
+ Succession(
694
+ # Add a run_time of 0.2 to wait for 0.2 seconds after
695
+ # adding the circle, instead of using Wait(0.2) after Add!
696
+ *[Add(circle, run_time=0.2) for circle in circles],
697
+ rate_func=smooth,
698
+ )
699
+ )
700
+ self.wait()
701
+ """
702
+
703
+ def __init__(
704
+ self, *mobjects: Mobject, run_time: float = 0.0, **kwargs: Any
705
+ ) -> None:
706
+ mobject = mobjects[0] if len(mobjects) == 1 else Group(*mobjects)
707
+ super().__init__(mobject, run_time=run_time, introducer=True, **kwargs)
708
+
709
+ def begin(self) -> None:
710
+ pass
711
+
712
+ def finish(self) -> None:
713
+ pass
714
+
715
+ def clean_up_from_scene(self, scene: Scene) -> None:
716
+ pass
717
+
718
+ def update_mobjects(self, dt: float) -> None:
719
+ pass
720
+
721
+ def interpolate(self, alpha: float) -> None:
722
+ pass
723
+
724
+
578
725
  def override_animation(
579
726
  animation_class: type[Animation],
580
727
  ) -> Callable[[Callable], Callable]:
@@ -1,26 +1,26 @@
1
1
  """Tools for displaying multiple animations at once."""
2
2
 
3
-
4
3
  from __future__ import annotations
5
4
 
6
- from typing import TYPE_CHECKING, Callable, Sequence
5
+ import types
6
+ from collections.abc import Iterable, Sequence
7
+ from typing import TYPE_CHECKING, Callable
7
8
 
8
9
  import numpy as np
9
10
 
11
+ from manim._config import config
12
+ from manim.animation.animation import Animation, prepare_animation
13
+ from manim.constants import RendererType
14
+ from manim.mobject.mobject import Group, Mobject
10
15
  from manim.mobject.opengl.opengl_mobject import OpenGLGroup
11
-
12
- from .._config import config
13
- from ..animation.animation import Animation, prepare_animation
14
- from ..constants import RendererType
15
- from ..mobject.mobject import Group, Mobject
16
- from ..scene.scene import Scene
17
- from ..utils.iterables import remove_list_redundancies
18
- from ..utils.rate_functions import linear
16
+ from manim.scene.scene import Scene
17
+ from manim.utils.iterables import remove_list_redundancies
18
+ from manim.utils.parameter_parsing import flatten_iterable_parameters
19
+ from manim.utils.rate_functions import linear
19
20
 
20
21
  if TYPE_CHECKING:
21
22
  from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup
22
-
23
- from ..mobject.types.vectorized_mobject import VGroup
23
+ from manim.mobject.types.vectorized_mobject import VGroup
24
24
 
25
25
  __all__ = ["AnimationGroup", "Succession", "LaggedStart", "LaggedStartMap"]
26
26
 
@@ -54,14 +54,15 @@ class AnimationGroup(Animation):
54
54
 
55
55
  def __init__(
56
56
  self,
57
- *animations: Animation,
57
+ *animations: Animation | Iterable[Animation] | types.GeneratorType[Animation],
58
58
  group: Group | VGroup | OpenGLGroup | OpenGLVGroup = None,
59
59
  run_time: float | None = None,
60
60
  rate_func: Callable[[float], float] = linear,
61
61
  lag_ratio: float = 0,
62
62
  **kwargs,
63
63
  ) -> None:
64
- self.animations = [prepare_animation(anim) for anim in animations]
64
+ arg_anim = flatten_iterable_parameters(animations)
65
+ self.animations = [prepare_animation(anim) for anim in arg_anim]
65
66
  self.rate_func = rate_func
66
67
  self.group = group
67
68
  if self.group is None:
@@ -81,6 +82,12 @@ class AnimationGroup(Animation):
81
82
  return list(self.group)
82
83
 
83
84
  def begin(self) -> None:
85
+ if not self.animations:
86
+ raise ValueError(
87
+ f"Trying to play {self} without animations, this is not supported. "
88
+ "Please add at least one subanimation."
89
+ )
90
+ self.anim_group_time = 0.0
84
91
  if self.suspend_mobject_updating:
85
92
  self.group.suspend_updating()
86
93
  for anim in self.animations:
@@ -93,6 +100,8 @@ class AnimationGroup(Animation):
93
100
  def finish(self) -> None:
94
101
  for anim in self.animations:
95
102
  anim.finish()
103
+ self.anims_begun[:] = True
104
+ self.anims_finished[:] = True
96
105
  if self.suspend_mobject_updating:
97
106
  self.group.resume_updating()
98
107
 
@@ -104,7 +113,9 @@ class AnimationGroup(Animation):
104
113
  anim.clean_up_from_scene(scene)
105
114
 
106
115
  def update_mobjects(self, dt: float) -> None:
107
- for anim in self.animations:
116
+ for anim in self.anims_with_timings["anim"][
117
+ self.anims_begun & ~self.anims_finished
118
+ ]:
108
119
  anim.update_mobjects(dt)
109
120
 
110
121
  def init_run_time(self, run_time) -> float:
@@ -121,22 +132,30 @@ class AnimationGroup(Animation):
121
132
  The duration of the animation in seconds.
122
133
  """
123
134
  self.build_animations_with_timings()
124
- if self.anims_with_timings:
125
- self.max_end_time = np.max([awt[2] for awt in self.anims_with_timings])
126
- else:
127
- self.max_end_time = 0
135
+ # Note: if lag_ratio < 1, then not necessarily the final animation's
136
+ # end time will be the max end time! Therefore we must calculate the
137
+ # maximum over all the end times, and not just take the last one.
138
+ # Example: if you want to play 2 animations of 10s and 1s with a
139
+ # lag_ratio of 0.1, the 1st one will end at t=10 and the 2nd one will
140
+ # end at t=2, so the AnimationGroup will end at t=10.
141
+ self.max_end_time = max(self.anims_with_timings["end"], default=0)
128
142
  return self.max_end_time if run_time is None else run_time
129
143
 
130
144
  def build_animations_with_timings(self) -> None:
131
145
  """Creates a list of triplets of the form (anim, start_time, end_time)."""
132
- self.anims_with_timings = []
133
- curr_time: float = 0
134
- for anim in self.animations:
135
- start_time: float = curr_time
136
- end_time: float = start_time + anim.get_run_time()
137
- self.anims_with_timings.append((anim, start_time, end_time))
138
- # Start time of next animation is based on the lag_ratio
139
- curr_time = (1 - self.lag_ratio) * start_time + self.lag_ratio * end_time
146
+ run_times = np.array([anim.run_time for anim in self.animations])
147
+ num_animations = run_times.shape[0]
148
+ dtype = [("anim", "O"), ("start", "f8"), ("end", "f8")]
149
+ self.anims_with_timings = np.zeros(num_animations, dtype=dtype)
150
+ self.anims_begun = np.zeros(num_animations, dtype=bool)
151
+ self.anims_finished = np.zeros(num_animations, dtype=bool)
152
+ if num_animations == 0:
153
+ return
154
+
155
+ lags = run_times[:-1] * self.lag_ratio
156
+ self.anims_with_timings["anim"] = self.animations
157
+ self.anims_with_timings["start"][1:] = np.add.accumulate(lags)
158
+ self.anims_with_timings["end"] = self.anims_with_timings["start"] + run_times
140
159
 
141
160
  def interpolate(self, alpha: float) -> None:
142
161
  # Note, if the run_time of AnimationGroup has been
@@ -144,14 +163,32 @@ class AnimationGroup(Animation):
144
163
  # times might not correspond to actual times,
145
164
  # e.g. of the surrounding scene. Instead they'd
146
165
  # be a rescaled version. But that's okay!
147
- time = self.rate_func(alpha) * self.max_end_time
148
- for anim, start_time, end_time in self.anims_with_timings:
149
- anim_time = end_time - start_time
150
- if anim_time == 0:
151
- sub_alpha = 0
152
- else:
153
- sub_alpha = np.clip((time - start_time) / anim_time, 0, 1)
154
- anim.interpolate(sub_alpha)
166
+ anim_group_time = self.rate_func(alpha) * self.max_end_time
167
+ time_goes_back = anim_group_time < self.anim_group_time
168
+
169
+ # Only update ongoing animations
170
+ awt = self.anims_with_timings
171
+ new_begun = anim_group_time >= awt["start"]
172
+ new_finished = anim_group_time > awt["end"]
173
+ to_update = awt[
174
+ (self.anims_begun | new_begun) & (~self.anims_finished | ~new_finished)
175
+ ]
176
+
177
+ run_times = to_update["end"] - to_update["start"]
178
+ with_zero_run_time = run_times == 0
179
+ run_times[with_zero_run_time] = 1
180
+ sub_alphas = (anim_group_time - to_update["start"]) / run_times
181
+ if time_goes_back:
182
+ sub_alphas[(sub_alphas < 0) | with_zero_run_time] = 0
183
+ else:
184
+ sub_alphas[(sub_alphas > 1) | with_zero_run_time] = 1
185
+
186
+ for anim_to_update, sub_alpha in zip(to_update["anim"], sub_alphas):
187
+ anim_to_update.interpolate(sub_alpha)
188
+
189
+ self.anim_group_time = anim_group_time
190
+ self.anims_begun = new_begun
191
+ self.anims_finished = new_finished
155
192
 
156
193
 
157
194
  class Succession(AnimationGroup):
@@ -195,7 +232,11 @@ class Succession(AnimationGroup):
195
232
  super().__init__(*animations, lag_ratio=lag_ratio, **kwargs)
196
233
 
197
234
  def begin(self) -> None:
198
- assert len(self.animations) > 0
235
+ if not self.animations:
236
+ raise ValueError(
237
+ f"Trying to play {self} without animations, this is not supported. "
238
+ "Please add at least one subanimation."
239
+ )
199
240
  self.update_active_animation(0)
200
241
 
201
242
  def finish(self) -> None:
@@ -226,8 +267,8 @@ class Succession(AnimationGroup):
226
267
  self.active_animation = self.animations[index]
227
268
  self.active_animation._setup_scene(self.scene)
228
269
  self.active_animation.begin()
229
- self.active_start_time = self.anims_with_timings[index][1]
230
- self.active_end_time = self.anims_with_timings[index][2]
270
+ self.active_start_time = self.anims_with_timings[index]["start"]
271
+ self.active_end_time = self.anims_with_timings[index]["end"]
231
272
 
232
273
  def next_animation(self) -> None:
233
274
  """Proceeds to the next animation.
@@ -244,7 +285,7 @@ class Succession(AnimationGroup):
244
285
  self.next_animation()
245
286
  if self.active_animation is not None and self.active_start_time is not None:
246
287
  elapsed = current_time - self.active_start_time
247
- active_run_time = self.active_animation.get_run_time()
288
+ active_run_time = self.active_animation.run_time
248
289
  subalpha = elapsed / active_run_time if active_run_time != 0.0 else 1.0
249
290
  self.active_animation.interpolate(subalpha)
250
291
 
@@ -70,17 +70,22 @@ __all__ = [
70
70
  "RemoveTextLetterByLetter",
71
71
  "ShowSubmobjectsOneByOne",
72
72
  "AddTextWordByWord",
73
+ "TypeWithCursor",
74
+ "UntypeWithCursor",
73
75
  ]
74
76
 
75
77
 
76
78
  import itertools as it
77
- from typing import TYPE_CHECKING, Callable, Iterable, Sequence
79
+ from collections.abc import Iterable, Sequence
80
+ from typing import TYPE_CHECKING, Callable
78
81
 
79
82
  import numpy as np
80
83
 
81
84
  if TYPE_CHECKING:
82
85
  from manim.mobject.text.text_mobject import Text
86
+ from manim.scene.scene import Scene
83
87
 
88
+ from manim.constants import RIGHT, TAU
84
89
  from manim.mobject.opengl.opengl_surface import OpenGLSurface
85
90
  from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
86
91
  from manim.utils.color import ManimColor
@@ -88,7 +93,6 @@ from manim.utils.color import ManimColor
88
93
  from .. import config
89
94
  from ..animation.animation import Animation
90
95
  from ..animation.composition import Succession
91
- from ..constants import TAU
92
96
  from ..mobject.mobject import Group, Mobject
93
97
  from ..mobject.types.vectorized_mobject import VMobject
94
98
  from ..utils.bezier import integer_interpolate
@@ -246,7 +250,9 @@ class DrawBorderThenFill(Animation):
246
250
 
247
251
  def _typecheck_input(self, vmobject: VMobject | OpenGLVMobject) -> None:
248
252
  if not isinstance(vmobject, (VMobject, OpenGLVMobject)):
249
- raise TypeError("DrawBorderThenFill only works for vectorized Mobjects")
253
+ raise TypeError(
254
+ f"{self.__class__.__name__} only works for vectorized Mobjects"
255
+ )
250
256
 
251
257
  def begin(self) -> None:
252
258
  self.outline = self.get_outline()
@@ -277,7 +283,7 @@ class DrawBorderThenFill(Animation):
277
283
  alpha: float,
278
284
  ) -> None: # Fixme: not matching the parent class? What is outline doing here?
279
285
  index: int
280
- subalpha: int
286
+ subalpha: float
281
287
  index, subalpha = integer_interpolate(0, 2, alpha)
282
288
  if index == 0:
283
289
  submobject.pointwise_become_partial(outline, 0, subalpha)
@@ -347,10 +353,7 @@ class Write(DrawBorderThenFill):
347
353
  ) -> tuple[float, float]:
348
354
  length = len(vmobject.family_members_with_points())
349
355
  if run_time is None:
350
- if length < 15:
351
- run_time = 1
352
- else:
353
- run_time = 2
356
+ run_time = 1 if length < 15 else 2
354
357
  if lag_ratio is None:
355
358
  lag_ratio = min(4.0 / max(1.0, length), 0.2)
356
359
  return run_time, lag_ratio
@@ -456,7 +459,7 @@ class SpiralIn(Animation):
456
459
  fade_in_fraction=0.3,
457
460
  **kwargs,
458
461
  ) -> None:
459
- self.shapes = shapes
462
+ self.shapes = shapes.copy()
460
463
  self.scale_factor = scale_factor
461
464
  self.shape_center = shapes.get_center()
462
465
  self.fade_in_fraction = fade_in_fraction
@@ -473,15 +476,21 @@ class SpiralIn(Animation):
473
476
 
474
477
  def interpolate_mobject(self, alpha: float) -> None:
475
478
  alpha = self.rate_func(alpha)
476
- for shape in self.shapes:
479
+ for original_shape, shape in zip(self.shapes, self.mobject):
477
480
  shape.restore()
478
- shape.save_state()
479
- opacity = shape.get_fill_opacity()
480
- new_opacity = min(opacity, alpha * opacity / self.fade_in_fraction)
481
+ fill_opacity = original_shape.get_fill_opacity()
482
+ stroke_opacity = original_shape.get_stroke_opacity()
483
+ new_fill_opacity = min(
484
+ fill_opacity, alpha * fill_opacity / self.fade_in_fraction
485
+ )
486
+ new_stroke_opacity = min(
487
+ stroke_opacity, alpha * stroke_opacity / self.fade_in_fraction
488
+ )
481
489
  shape.shift((shape.final_position - shape.initial_position) * alpha)
482
490
  shape.rotate(TAU * alpha, about_point=self.shape_center)
483
491
  shape.rotate(-TAU * alpha, about_point=shape.get_center_of_mass())
484
- shape.set_opacity(new_opacity)
492
+ shape.set_fill(opacity=new_fill_opacity)
493
+ shape.set_stroke(opacity=new_stroke_opacity)
485
494
 
486
495
 
487
496
  class ShowIncreasingSubsets(Animation):
@@ -668,3 +677,176 @@ class AddTextWordByWord(Succession):
668
677
  )
669
678
  )
670
679
  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
+ )