manim 0.18.1__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 (129) hide show
  1. manim/__main__.py +45 -12
  2. manim/_config/__init__.py +2 -2
  3. manim/_config/cli_colors.py +8 -4
  4. manim/_config/default.cfg +0 -2
  5. manim/_config/logger_utils.py +5 -0
  6. manim/_config/utils.py +29 -38
  7. manim/animation/animation.py +148 -8
  8. manim/animation/composition.py +16 -13
  9. manim/animation/creation.py +184 -8
  10. manim/animation/fading.py +5 -8
  11. manim/animation/indication.py +93 -26
  12. manim/animation/movement.py +21 -3
  13. manim/animation/rotation.py +2 -1
  14. manim/animation/specialized.py +3 -5
  15. manim/animation/speedmodifier.py +3 -3
  16. manim/animation/transform.py +4 -5
  17. manim/animation/updaters/mobject_update_utils.py +17 -14
  18. manim/camera/camera.py +2 -2
  19. manim/cli/__init__.py +17 -0
  20. manim/cli/cfg/group.py +52 -36
  21. manim/cli/checkhealth/checks.py +92 -76
  22. manim/cli/checkhealth/commands.py +12 -5
  23. manim/cli/default_group.py +148 -24
  24. manim/cli/init/commands.py +28 -23
  25. manim/cli/plugins/commands.py +13 -3
  26. manim/cli/render/commands.py +47 -42
  27. manim/cli/render/global_options.py +43 -9
  28. manim/cli/render/render_options.py +84 -19
  29. manim/constants.py +11 -4
  30. manim/mobject/frame.py +0 -1
  31. manim/mobject/geometry/arc.py +109 -75
  32. manim/mobject/geometry/boolean_ops.py +20 -17
  33. manim/mobject/geometry/labeled.py +300 -77
  34. manim/mobject/geometry/line.py +120 -60
  35. manim/mobject/geometry/polygram.py +109 -25
  36. manim/mobject/geometry/shape_matchers.py +35 -15
  37. manim/mobject/geometry/tips.py +36 -27
  38. manim/mobject/graph.py +48 -40
  39. manim/mobject/graphing/coordinate_systems.py +110 -45
  40. manim/mobject/graphing/functions.py +16 -10
  41. manim/mobject/graphing/number_line.py +23 -9
  42. manim/mobject/graphing/probability.py +2 -10
  43. manim/mobject/graphing/scale.py +6 -5
  44. manim/mobject/matrix.py +17 -19
  45. manim/mobject/mobject.py +149 -103
  46. manim/mobject/opengl/opengl_geometry.py +4 -8
  47. manim/mobject/opengl/opengl_mobject.py +506 -343
  48. manim/mobject/opengl/opengl_point_cloud_mobject.py +3 -7
  49. manim/mobject/opengl/opengl_surface.py +1 -2
  50. manim/mobject/opengl/opengl_vectorized_mobject.py +27 -65
  51. manim/mobject/svg/brace.py +61 -13
  52. manim/mobject/svg/svg_mobject.py +2 -1
  53. manim/mobject/table.py +11 -12
  54. manim/mobject/text/code_mobject.py +186 -550
  55. manim/mobject/text/numbers.py +7 -7
  56. manim/mobject/text/tex_mobject.py +22 -13
  57. manim/mobject/text/text_mobject.py +29 -20
  58. manim/mobject/three_d/polyhedra.py +98 -1
  59. manim/mobject/three_d/three_dimensions.py +59 -31
  60. manim/mobject/types/image_mobject.py +37 -23
  61. manim/mobject/types/point_cloud_mobject.py +103 -67
  62. manim/mobject/types/vectorized_mobject.py +387 -214
  63. manim/mobject/value_tracker.py +2 -1
  64. manim/mobject/vector_field.py +2 -4
  65. manim/opengl/__init__.py +3 -3
  66. manim/plugins/__init__.py +2 -3
  67. manim/plugins/plugins_flags.py +3 -3
  68. manim/renderer/cairo_renderer.py +11 -11
  69. manim/renderer/opengl_renderer.py +19 -20
  70. manim/renderer/shader.py +2 -3
  71. manim/renderer/shader_wrapper.py +3 -2
  72. manim/scene/moving_camera_scene.py +23 -0
  73. manim/scene/scene.py +72 -41
  74. manim/scene/scene_file_writer.py +313 -164
  75. manim/scene/section.py +15 -15
  76. manim/scene/three_d_scene.py +8 -15
  77. manim/scene/vector_space_scene.py +3 -6
  78. manim/typing.py +326 -66
  79. manim/utils/bezier.py +1658 -381
  80. manim/utils/caching.py +11 -5
  81. manim/utils/color/AS2700.py +2 -0
  82. manim/utils/color/BS381.py +2 -0
  83. manim/utils/color/DVIPSNAMES.py +96 -0
  84. manim/utils/color/SVGNAMES.py +179 -0
  85. manim/utils/color/X11.py +3 -0
  86. manim/utils/color/XKCD.py +2 -0
  87. manim/utils/color/__init__.py +8 -5
  88. manim/utils/color/core.py +818 -301
  89. manim/utils/color/manim_colors.py +7 -9
  90. manim/utils/commands.py +40 -19
  91. manim/utils/config_ops.py +18 -13
  92. manim/utils/debug.py +8 -6
  93. manim/utils/deprecation.py +92 -43
  94. manim/utils/docbuild/autoaliasattr_directive.py +45 -8
  95. manim/utils/docbuild/autocolor_directive.py +12 -13
  96. manim/utils/docbuild/manim_directive.py +35 -29
  97. manim/utils/docbuild/module_parsing.py +74 -27
  98. manim/utils/family.py +3 -3
  99. manim/utils/family_ops.py +12 -4
  100. manim/utils/file_ops.py +22 -16
  101. manim/utils/hashing.py +7 -7
  102. manim/utils/images.py +10 -4
  103. manim/utils/ipython_magic.py +12 -8
  104. manim/utils/iterables.py +161 -119
  105. manim/utils/module_ops.py +55 -19
  106. manim/utils/opengl.py +68 -23
  107. manim/utils/parameter_parsing.py +3 -2
  108. manim/utils/paths.py +11 -5
  109. manim/utils/polylabel.py +168 -0
  110. manim/utils/qhull.py +218 -0
  111. manim/utils/rate_functions.py +69 -32
  112. manim/utils/simple_functions.py +24 -15
  113. manim/utils/sounds.py +7 -1
  114. manim/utils/space_ops.py +48 -37
  115. manim/utils/testing/_frames_testers.py +13 -8
  116. manim/utils/testing/_show_diff.py +5 -3
  117. manim/utils/testing/_test_class_makers.py +33 -18
  118. manim/utils/testing/frames_comparison.py +20 -14
  119. manim/utils/tex.py +4 -2
  120. manim/utils/tex_file_writing.py +45 -45
  121. manim/utils/tex_templates.py +1 -1
  122. manim/utils/unit.py +6 -5
  123. {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/METADATA +16 -9
  124. manim-0.19.0.dist-info/RECORD +221 -0
  125. {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/WHEEL +1 -1
  126. manim-0.18.1.dist-info/RECORD +0 -217
  127. {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/LICENSE +0 -0
  128. {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/LICENSE.community +0 -0
  129. {manim-0.18.1.dist-info → manim-0.19.0.dist-info}/entry_points.txt +0 -0
manim/__main__.py CHANGED
@@ -3,22 +3,49 @@ from __future__ import annotations
3
3
  import click
4
4
  import cloup
5
5
 
6
- from . import __version__, cli_ctx_settings, console
7
- from .cli.cfg.group import cfg
8
- from .cli.checkhealth.commands import checkhealth
9
- from .cli.default_group import DefaultGroup
10
- from .cli.init.commands import init
11
- from .cli.plugins.commands import plugins
12
- from .cli.render.commands import render
13
- from .constants import EPILOG
6
+ from manim import __version__
7
+ from manim._config import cli_ctx_settings, console
8
+ from manim.cli.cfg.group import cfg
9
+ from manim.cli.checkhealth.commands import checkhealth
10
+ from manim.cli.default_group import DefaultGroup
11
+ from manim.cli.init.commands import init
12
+ from manim.cli.plugins.commands import plugins
13
+ from manim.cli.render.commands import render
14
+ from manim.constants import EPILOG
14
15
 
15
16
 
16
- def show_splash(ctx, param, value):
17
+ def show_splash(ctx: click.Context, param: click.Option, value: str | None) -> None:
18
+ """When giving a value by console, show an initial message with the Manim
19
+ version before executing any other command: ``Manim Community vA.B.C``.
20
+
21
+ Parameters
22
+ ----------
23
+ ctx
24
+ The Click context.
25
+ param
26
+ A Click option.
27
+ value
28
+ A string value given by console, or None.
29
+ """
17
30
  if value:
18
31
  console.print(f"Manim Community [green]v{__version__}[/green]\n")
19
32
 
20
33
 
21
- def print_version_and_exit(ctx, param, value):
34
+ def print_version_and_exit(
35
+ ctx: click.Context, param: click.Option, value: str | None
36
+ ) -> None:
37
+ """Same as :func:`show_splash`, but also exit when giving a value by
38
+ console.
39
+
40
+ Parameters
41
+ ----------
42
+ ctx
43
+ The Click context.
44
+ param
45
+ A Click option.
46
+ value
47
+ A string value given by console, or None.
48
+ """
22
49
  show_splash(ctx, param, value)
23
50
  if value:
24
51
  ctx.exit()
@@ -53,8 +80,14 @@ def print_version_and_exit(ctx, param, value):
53
80
  expose_value=False,
54
81
  )
55
82
  @cloup.pass_context
56
- def main(ctx):
57
- """The entry point for manim."""
83
+ def main(ctx: click.Context) -> None:
84
+ """The entry point for Manim.
85
+
86
+ Parameters
87
+ ----------
88
+ ctx
89
+ The Click context.
90
+ """
58
91
  pass
59
92
 
60
93
 
manim/_config/__init__.py CHANGED
@@ -3,8 +3,9 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
+ from collections.abc import Generator
6
7
  from contextlib import contextmanager
7
- from typing import Any, Generator
8
+ from typing import Any
8
9
 
9
10
  from .cli_colors import parse_cli_ctx
10
11
  from .logger_utils import make_logger
@@ -67,7 +68,6 @@ def tempconfig(temp: ManimConfig | dict[str, Any]) -> Generator[None, None, None
67
68
  8.0
68
69
  >>> with tempconfig({"frame_height": 100.0}):
69
70
  ... print(config["frame_height"])
70
- ...
71
71
  100.0
72
72
  >>> config["frame_height"]
73
73
  8.0
@@ -1,13 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import configparser
4
+ from typing import Any
4
5
 
5
6
  from cloup import Context, HelpFormatter, HelpTheme, Style
6
7
 
7
8
  __all__ = ["parse_cli_ctx"]
8
9
 
9
10
 
10
- def parse_cli_ctx(parser: configparser.SectionProxy) -> Context:
11
+ def parse_cli_ctx(parser: configparser.SectionProxy) -> dict[str, Any]:
11
12
  formatter_settings: dict[str, str | int] = {
12
13
  "indent_increment": int(parser["indent_increment"]),
13
14
  "width": int(parser["width"]),
@@ -35,15 +36,18 @@ def parse_cli_ctx(parser: configparser.SectionProxy) -> Context:
35
36
  theme = parser["theme"] if parser["theme"] else None
36
37
  if theme is None:
37
38
  formatter = HelpFormatter.settings(
38
- theme=HelpTheme(**theme_settings), **formatter_settings # type: ignore[arg-type]
39
+ theme=HelpTheme(**theme_settings),
40
+ **formatter_settings, # type: ignore[arg-type]
39
41
  )
40
42
  elif theme.lower() == "dark":
41
43
  formatter = HelpFormatter.settings(
42
- theme=HelpTheme.dark().with_(**theme_settings), **formatter_settings # type: ignore[arg-type]
44
+ theme=HelpTheme.dark().with_(**theme_settings),
45
+ **formatter_settings, # type: ignore[arg-type]
43
46
  )
44
47
  elif theme.lower() == "light":
45
48
  formatter = HelpFormatter.settings(
46
- theme=HelpTheme.light().with_(**theme_settings), **formatter_settings # type: ignore[arg-type]
49
+ theme=HelpTheme.light().with_(**theme_settings),
50
+ **formatter_settings, # type: ignore[arg-type]
47
51
  )
48
52
 
49
53
  return Context.settings(
manim/_config/default.cfg CHANGED
@@ -221,8 +221,6 @@ repr_number = green
221
221
  # Uncomment the following line to manually set the loglevel for ffmpeg. See
222
222
  # ffmpeg manpage for accepted values
223
223
  loglevel = ERROR
224
- # defaults to the one present in path
225
- ffmpeg_executable = ffmpeg
226
224
 
227
225
  [jupyter]
228
226
  media_embed = False
@@ -99,6 +99,11 @@ def make_logger(
99
99
  logger = logging.getLogger("manim")
100
100
  logger.addHandler(rich_handler)
101
101
  logger.setLevel(verbosity)
102
+ logger.propagate = False
103
+
104
+ if not (libav_logger := logging.getLogger()).hasHandlers():
105
+ libav_logger.addHandler(rich_handler)
106
+ libav_logger.setLevel(verbosity)
102
107
 
103
108
  return logger, console, error_console
104
109
 
manim/_config/utils.py CHANGED
@@ -20,9 +20,9 @@ import logging
20
20
  import os
21
21
  import re
22
22
  import sys
23
- from collections.abc import Mapping, MutableMapping
23
+ from collections.abc import Iterable, Iterator, Mapping, MutableMapping
24
24
  from pathlib import Path
25
- from typing import TYPE_CHECKING, Any, ClassVar, Iterable, Iterator, NoReturn
25
+ from typing import TYPE_CHECKING, Any, ClassVar, NoReturn
26
26
 
27
27
  import numpy as np
28
28
 
@@ -40,6 +40,8 @@ if TYPE_CHECKING:
40
40
 
41
41
  __all__ = ["config_file_paths", "make_config_parser", "ManimConfig", "ManimFrame"]
42
42
 
43
+ logger = logging.getLogger("manim")
44
+
43
45
 
44
46
  def config_file_paths() -> list[Path]:
45
47
  """The paths where ``.cfg`` files will be searched for.
@@ -263,7 +265,6 @@ class ManimConfig(MutableMapping):
263
265
  "dry_run",
264
266
  "enable_wireframe",
265
267
  "ffmpeg_loglevel",
266
- "ffmpeg_executable",
267
268
  "format",
268
269
  "flush_cache",
269
270
  "frame_height",
@@ -322,7 +323,7 @@ class ManimConfig(MutableMapping):
322
323
  }
323
324
 
324
325
  def __init__(self) -> None:
325
- self._d: dict[str, Any | None] = {k: None for k in self._OPTS}
326
+ self._d: dict[str, Any | None] = dict.fromkeys(self._OPTS)
326
327
 
327
328
  # behave like a dict
328
329
  def __iter__(self) -> Iterator[str]:
@@ -371,7 +372,6 @@ class ManimConfig(MutableMapping):
371
372
  :meth:`~ManimConfig.digest_parser`
372
373
 
373
374
  """
374
-
375
375
  if isinstance(obj, ManimConfig):
376
376
  self._d.update(obj._d)
377
377
  if obj.tex_template:
@@ -642,21 +642,18 @@ class ManimConfig(MutableMapping):
642
642
  gui_location = tuple(
643
643
  map(int, re.split(r"[;,\-]", parser["CLI"]["gui_location"])),
644
644
  )
645
- setattr(self, "gui_location", gui_location)
645
+ self.gui_location = gui_location
646
646
 
647
647
  window_size = parser["CLI"][
648
648
  "window_size"
649
649
  ] # if not "default", get a tuple of the position
650
650
  if window_size != "default":
651
651
  window_size = tuple(map(int, re.split(r"[;,\-]", window_size)))
652
- setattr(self, "window_size", window_size)
652
+ self.window_size = window_size
653
653
 
654
654
  # plugins
655
655
  plugins = parser["CLI"].get("plugins", fallback="", raw=True)
656
- if plugins == "":
657
- plugins = []
658
- else:
659
- plugins = plugins.split(",")
656
+ plugins = [] if plugins == "" else plugins.split(",")
660
657
  self.plugins = plugins
661
658
  # the next two must be set AFTER digesting pixel_width and pixel_height
662
659
  self["frame_height"] = parser["CLI"].getfloat("frame_height", 8.0)
@@ -673,25 +670,21 @@ class ManimConfig(MutableMapping):
673
670
 
674
671
  val = parser["CLI"].get("progress_bar")
675
672
  if val:
676
- setattr(self, "progress_bar", val)
673
+ self.progress_bar = val
677
674
 
678
675
  val = parser["ffmpeg"].get("loglevel")
679
676
  if val:
680
677
  self.ffmpeg_loglevel = val
681
678
 
682
- # TODO: Fix the mess above and below
683
- val = parser["ffmpeg"].get("ffmpeg_executable")
684
- setattr(self, "ffmpeg_executable", val)
685
-
686
679
  try:
687
680
  val = parser["jupyter"].getboolean("media_embed")
688
681
  except ValueError:
689
682
  val = None
690
- setattr(self, "media_embed", val)
683
+ self.media_embed = val
691
684
 
692
685
  val = parser["jupyter"].get("media_width")
693
686
  if val:
694
- setattr(self, "media_width", val)
687
+ self.media_width = val
695
688
 
696
689
  val = parser["CLI"].get("quality", fallback="", raw=True)
697
690
  if val:
@@ -804,7 +797,7 @@ class ManimConfig(MutableMapping):
804
797
  try:
805
798
  self.upto_animation_number = nflag[1]
806
799
  except Exception:
807
- logging.getLogger("manim").info(
800
+ logger.info(
808
801
  f"No end scene number specified in -n option. Rendering from {nflag[0]} onwards...",
809
802
  )
810
803
 
@@ -842,15 +835,12 @@ class ManimConfig(MutableMapping):
842
835
  if args.tex_template:
843
836
  self.tex_template = TexTemplate.from_file(args.tex_template)
844
837
 
845
- if (
846
- self.renderer == RendererType.OPENGL
847
- and getattr(args, "write_to_movie") is None
848
- ):
838
+ if self.renderer == RendererType.OPENGL and args.write_to_movie is None:
849
839
  # --write_to_movie was not passed on the command line, so don't generate video.
850
840
  self["write_to_movie"] = False
851
841
 
852
842
  # Handle --gui_location flag.
853
- if getattr(args, "gui_location") is not None:
843
+ if args.gui_location is not None:
854
844
  self.gui_location = args.gui_location
855
845
 
856
846
  return self
@@ -1043,7 +1033,7 @@ class ManimConfig(MutableMapping):
1043
1033
  val,
1044
1034
  ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
1045
1035
  )
1046
- logging.getLogger("manim").setLevel(val)
1036
+ logger.setLevel(val)
1047
1037
 
1048
1038
  @property
1049
1039
  def format(self) -> str:
@@ -1057,8 +1047,9 @@ class ManimConfig(MutableMapping):
1057
1047
  val,
1058
1048
  [None, "png", "gif", "mp4", "mov", "webm"],
1059
1049
  )
1050
+ self.resolve_movie_file_extension(self.transparent)
1060
1051
  if self.format == "webm":
1061
- logging.getLogger("manim").warning(
1052
+ logger.warning(
1062
1053
  "Output format set as webm, this can be slower than other formats",
1063
1054
  )
1064
1055
 
@@ -1074,15 +1065,7 @@ class ManimConfig(MutableMapping):
1074
1065
  val,
1075
1066
  ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
1076
1067
  )
1077
-
1078
- @property
1079
- def ffmpeg_executable(self) -> str:
1080
- """Custom path to the ffmpeg executable."""
1081
- return self._d["ffmpeg_executable"]
1082
-
1083
- @ffmpeg_executable.setter
1084
- def ffmpeg_executable(self, value: str) -> None:
1085
- self._set_str("ffmpeg_executable", value)
1068
+ logging.getLogger("libav").setLevel(self.ffmpeg_loglevel)
1086
1069
 
1087
1070
  @property
1088
1071
  def media_embed(self) -> bool:
@@ -1284,6 +1267,8 @@ class ManimConfig(MutableMapping):
1284
1267
  @background_opacity.setter
1285
1268
  def background_opacity(self, value: float) -> None:
1286
1269
  self._set_between("background_opacity", value, 0, 1)
1270
+ if self.background_opacity < 1:
1271
+ self.resolve_movie_file_extension(is_transparent=True)
1287
1272
 
1288
1273
  @property
1289
1274
  def frame_size(self) -> tuple[int, int]:
@@ -1318,8 +1303,8 @@ class ManimConfig(MutableMapping):
1318
1303
 
1319
1304
  @property
1320
1305
  def transparent(self) -> bool:
1321
- """Whether the background opacity is 0.0 (-t)."""
1322
- return self._d["background_opacity"] == 0.0
1306
+ """Whether the background opacity is less than 1.0 (-t)."""
1307
+ return self._d["background_opacity"] < 1.0
1323
1308
 
1324
1309
  @transparent.setter
1325
1310
  def transparent(self, value: bool) -> None:
@@ -1437,6 +1422,7 @@ class ManimConfig(MutableMapping):
1437
1422
  self._d.__setitem__("window_size", value)
1438
1423
 
1439
1424
  def resolve_movie_file_extension(self, is_transparent: bool) -> None:
1425
+ prev_file_extension = self.movie_file_extension
1440
1426
  if is_transparent:
1441
1427
  self.movie_file_extension = ".webm" if self.format == "webm" else ".mov"
1442
1428
  elif self.format == "webm":
@@ -1445,6 +1431,11 @@ class ManimConfig(MutableMapping):
1445
1431
  self.movie_file_extension = ".mov"
1446
1432
  else:
1447
1433
  self.movie_file_extension = ".mp4"
1434
+ if self.movie_file_extension != prev_file_extension:
1435
+ logger.warning(
1436
+ f"Output format changed to '{self.movie_file_extension}' "
1437
+ "to support transparency",
1438
+ )
1448
1439
 
1449
1440
  @property
1450
1441
  def enable_gui(self) -> bool:
@@ -1790,7 +1781,7 @@ class ManimConfig(MutableMapping):
1790
1781
  def tex_template_file(self, val: str) -> None:
1791
1782
  if val:
1792
1783
  if not os.access(val, os.R_OK):
1793
- logging.getLogger("manim").warning(
1784
+ logger.warning(
1794
1785
  f"Custom TeX template {val} not found or not readable.",
1795
1786
  )
1796
1787
  else:
@@ -7,15 +7,17 @@ from manim.mobject.opengl.opengl_mobject import OpenGLMobject
7
7
  from .. import config, logger
8
8
  from ..constants import RendererType
9
9
  from ..mobject import mobject
10
- from ..mobject.mobject import Mobject
10
+ from ..mobject.mobject import Group, Mobject
11
11
  from ..mobject.opengl import opengl_mobject
12
12
  from ..utils.rate_functions import linear, smooth
13
13
 
14
- __all__ = ["Animation", "Wait", "override_animation"]
14
+ __all__ = ["Animation", "Wait", "Add", "override_animation"]
15
15
 
16
16
 
17
+ from collections.abc import Iterable, Sequence
17
18
  from copy import deepcopy
18
- from typing import TYPE_CHECKING, Callable, Iterable, Sequence
19
+ from functools import partialmethod
20
+ from typing import TYPE_CHECKING, Any, Callable
19
21
 
20
22
  from typing_extensions import Self
21
23
 
@@ -170,6 +172,19 @@ class Animation:
170
172
  ),
171
173
  )
172
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
+
173
188
  def _typecheck_input(self, mobject: Mobject | None) -> None:
174
189
  if mobject is None:
175
190
  logger.debug("Animation with empty mobject")
@@ -192,11 +207,6 @@ class Animation:
192
207
  method.
193
208
 
194
209
  """
195
- if self.run_time <= 0:
196
- raise ValueError(
197
- f"{self} has a run_time of <= 0 seconds, this cannot be rendered correctly. "
198
- "Please set the run_time to be positive"
199
- )
200
210
  self.starting_mobject = self.create_starting_mobject()
201
211
  if self.suspend_mobject_updating:
202
212
  # All calls to self.mobject's internal updaters
@@ -483,6 +493,52 @@ class Animation:
483
493
  """
484
494
  return self.introducer
485
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
+
486
542
 
487
543
  def prepare_animation(
488
544
  anim: Animation | mobject._AnimationBuilder,
@@ -582,6 +638,90 @@ class Wait(Animation):
582
638
  pass
583
639
 
584
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
+
585
725
  def override_animation(
586
726
  animation_class: type[Animation],
587
727
  ) -> Callable[[Callable], Callable]:
@@ -3,7 +3,8 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import types
6
- from typing import TYPE_CHECKING, Callable, Iterable, Sequence
6
+ from collections.abc import Iterable, Sequence
7
+ from typing import TYPE_CHECKING, Callable
7
8
 
8
9
  import numpy as np
9
10
 
@@ -81,15 +82,10 @@ class AnimationGroup(Animation):
81
82
  return list(self.group)
82
83
 
83
84
  def begin(self) -> None:
84
- if self.run_time <= 0:
85
- tmp = (
86
- "Please set the run_time to be positive"
87
- if len(self.animations) != 0
88
- else "Please add at least one Animation with positive run_time"
89
- )
85
+ if not self.animations:
90
86
  raise ValueError(
91
- f"{self} has a run_time of 0 seconds, this cannot be "
92
- f"rendered correctly. {tmp}."
87
+ f"Trying to play {self} without animations, this is not supported. "
88
+ "Please add at least one subanimation."
93
89
  )
94
90
  self.anim_group_time = 0.0
95
91
  if self.suspend_mobject_updating:
@@ -102,7 +98,8 @@ class AnimationGroup(Animation):
102
98
  anim._setup_scene(scene)
103
99
 
104
100
  def finish(self) -> None:
105
- self.interpolate(1)
101
+ for anim in self.animations:
102
+ anim.finish()
106
103
  self.anims_begun[:] = True
107
104
  self.anims_finished[:] = True
108
105
  if self.suspend_mobject_updating:
@@ -178,11 +175,13 @@ class AnimationGroup(Animation):
178
175
  ]
179
176
 
180
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
181
180
  sub_alphas = (anim_group_time - to_update["start"]) / run_times
182
181
  if time_goes_back:
183
- sub_alphas[sub_alphas < 0] = 0
182
+ sub_alphas[(sub_alphas < 0) | with_zero_run_time] = 0
184
183
  else:
185
- sub_alphas[sub_alphas > 1] = 1
184
+ sub_alphas[(sub_alphas > 1) | with_zero_run_time] = 1
186
185
 
187
186
  for anim_to_update, sub_alpha in zip(to_update["anim"], sub_alphas):
188
187
  anim_to_update.interpolate(sub_alpha)
@@ -233,7 +232,11 @@ class Succession(AnimationGroup):
233
232
  super().__init__(*animations, lag_ratio=lag_ratio, **kwargs)
234
233
 
235
234
  def begin(self) -> None:
236
- 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
+ )
237
240
  self.update_active_animation(0)
238
241
 
239
242
  def finish(self) -> None: