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
@@ -165,7 +165,8 @@ class ComplexValueTracker(ValueTracker):
165
165
  """Get the current value of this value tracker as a complex number.
166
166
 
167
167
  The value is internally stored as a points array [a, b, 0]. This can be accessed directly
168
- to represent the value geometrically, see the usage example."""
168
+ to represent the value geometrically, see the usage example.
169
+ """
169
170
  return complex(*self.points[0, :2])
170
171
 
171
172
  def set_value(self, z):
@@ -10,8 +10,9 @@ __all__ = [
10
10
 
11
11
  import itertools as it
12
12
  import random
13
+ from collections.abc import Iterable, Sequence
13
14
  from math import ceil, floor
14
- from typing import Callable, Iterable, Sequence
15
+ from typing import Callable
15
16
 
16
17
  import numpy as np
17
18
  from PIL import Image
@@ -352,7 +353,6 @@ class VectorField(VGroup):
352
353
  This vector field.
353
354
 
354
355
  """
355
-
356
356
  self.stop_submobject_movement()
357
357
  self.submob_movement_updater = lambda mob, dt: mob.nudge_submobjects(
358
358
  dt * speed,
@@ -950,7 +950,6 @@ class StreamLines(VectorField):
950
950
  self.wait(stream_lines.virtual_time / stream_lines.flow_speed)
951
951
 
952
952
  """
953
-
954
953
  for line in self.stream_lines:
955
954
  run_time = line.duration / flow_speed
956
955
  line.anim = line_animation_class(
@@ -1010,7 +1009,6 @@ class StreamLines(VectorField):
1010
1009
  self.play(stream_lines.end_animation())
1011
1010
 
1012
1011
  """
1013
-
1014
1012
  if self.flow_animation is None:
1015
1013
  raise ValueError("You have to start the animation before fading it out.")
1016
1014
 
manim/opengl/__init__.py CHANGED
@@ -1,9 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
- try:
3
+ import contextlib
4
+
5
+ with contextlib.suppress(ImportError):
4
6
  from dearpygui import dearpygui as dpg
5
- except ImportError:
6
- pass
7
7
 
8
8
 
9
9
  from manim.mobject.opengl.dot_cloud import *
manim/plugins/__init__.py CHANGED
@@ -1,8 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- from manim import config, logger
4
-
5
- from .plugins_flags import get_plugins, list_plugins
3
+ from manim._config import config, logger
4
+ from manim.plugins.plugins_flags import get_plugins, list_plugins
6
5
 
7
6
  __all__ = [
8
7
  "get_plugins",
@@ -10,7 +10,7 @@ if sys.version_info < (3, 10):
10
10
  else:
11
11
  from importlib.metadata import entry_points
12
12
 
13
- from manim import console
13
+ from manim._config import console
14
14
 
15
15
  __all__ = ["list_plugins"]
16
16
 
@@ -27,5 +27,5 @@ def list_plugins() -> None:
27
27
  console.print("[green bold]Plugins:[/green bold]", justify="left")
28
28
 
29
29
  plugins = get_plugins()
30
- for plugin in plugins:
31
- console.print(f" • {plugin}")
30
+ for plugin_name in plugins:
31
+ console.print(f" • {plugin_name}")
@@ -8,18 +8,19 @@ from manim.utils.hashing import get_hash_from_play_call
8
8
 
9
9
  from .. import config, logger
10
10
  from ..camera.camera import Camera
11
- from ..mobject.mobject import Mobject
11
+ from ..mobject.mobject import Mobject, _AnimationBuilder
12
12
  from ..scene.scene_file_writer import SceneFileWriter
13
13
  from ..utils.exceptions import EndSceneEarlyException
14
14
  from ..utils.iterables import list_update
15
15
 
16
16
  if typing.TYPE_CHECKING:
17
- import types
18
- from typing import Any, Iterable
17
+ from typing import Any
19
18
 
20
19
  from manim.animation.animation import Animation
21
20
  from manim.scene.scene import Scene
22
21
 
22
+ from ..typing import PixelArray
23
+
23
24
  __all__ = ["CairoRenderer"]
24
25
 
25
26
 
@@ -59,7 +60,7 @@ class CairoRenderer:
59
60
  def play(
60
61
  self,
61
62
  scene: Scene,
62
- *args: Animation | Iterable[Animation] | types.GeneratorType[Animation],
63
+ *args: Animation | Mobject | _AnimationBuilder,
63
64
  **kwargs,
64
65
  ):
65
66
  # Reset skip_animations to the original state.
@@ -159,7 +160,7 @@ class CairoRenderer:
159
160
  self.update_frame(scene, moving_mobjects)
160
161
  self.add_frame(self.get_frame())
161
162
 
162
- def get_frame(self):
163
+ def get_frame(self) -> PixelArray:
163
164
  """
164
165
  Gets the current frame as NumPy array.
165
166
 
@@ -186,8 +187,7 @@ class CairoRenderer:
186
187
  if self.skip_animations:
187
188
  return
188
189
  self.time += num_frames * dt
189
- for _ in range(num_frames):
190
- self.file_writer.write_frame(frame)
190
+ self.file_writer.write_frame(frame, num_frames=num_frames)
191
191
 
192
192
  def freeze_current_frame(self, duration: float):
193
193
  """Adds a static frame to the movie for a given duration. The static frame is the current frame.
@@ -252,13 +252,13 @@ class CairoRenderer:
252
252
  if config["save_last_frame"]:
253
253
  self.skip_animations = True
254
254
  if (
255
- config["from_animation_number"]
256
- and self.num_plays < config["from_animation_number"]
255
+ config.from_animation_number > 0
256
+ and self.num_plays < config.from_animation_number
257
257
  ):
258
258
  self.skip_animations = True
259
259
  if (
260
- config["upto_animation_number"]
261
- and self.num_plays > config["upto_animation_number"]
260
+ config.upto_animation_number >= 0
261
+ and self.num_plays > config.upto_animation_number
262
262
  ):
263
263
  self.skip_animations = True
264
264
  raise EndSceneEarlyException()
@@ -1,15 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  import itertools as it
4
- import sys
5
5
  import time
6
+ from functools import cached_property
6
7
  from typing import Any
7
8
 
8
- if sys.version_info < (3, 8):
9
- from backports.cached_property import cached_property
10
- else:
11
- from functools import cached_property
12
-
13
9
  import moderngl
14
10
  import numpy as np
15
11
  from PIL import Image
@@ -67,12 +63,12 @@ class OpenGLCamera(OpenGLMobject):
67
63
  if self.orthographic:
68
64
  self.projection_matrix = opengl.orthographic_projection_matrix()
69
65
  self.unformatted_projection_matrix = opengl.orthographic_projection_matrix(
70
- format=False,
66
+ format_=False,
71
67
  )
72
68
  else:
73
69
  self.projection_matrix = opengl.perspective_projection_matrix()
74
70
  self.unformatted_projection_matrix = opengl.perspective_projection_matrix(
75
- format=False,
71
+ format_=False,
76
72
  )
77
73
 
78
74
  if frame_shape is None:
@@ -220,7 +216,11 @@ class OpenGLCamera(OpenGLMobject):
220
216
 
221
217
 
222
218
  class OpenGLRenderer:
223
- def __init__(self, file_writer_class=SceneFileWriter, skip_animations=False):
219
+ def __init__(
220
+ self,
221
+ file_writer_class: type[SceneFileWriter] = SceneFileWriter,
222
+ skip_animations: bool = False,
223
+ ) -> None:
224
224
  # Measured in pixel widths, used for vector graphics
225
225
  self.anti_alias_width = 1.5
226
226
  self._file_writer_class = file_writer_class
@@ -341,10 +341,8 @@ class OpenGLRenderer:
341
341
  shader_wrapper.uniforms.items(),
342
342
  self.perspective_uniforms.items(),
343
343
  ):
344
- try:
344
+ with contextlib.suppress(KeyError):
345
345
  shader.set_uniform(name, value)
346
- except KeyError:
347
- pass
348
346
  try:
349
347
  shader.set_uniform(
350
348
  "u_view_matrix", self.scene.camera.formatted_view_matrix
@@ -390,7 +388,7 @@ class OpenGLRenderer:
390
388
 
391
389
  return self.path_to_texture_id[repr(path)]
392
390
 
393
- def update_skipping_status(self):
391
+ def update_skipping_status(self) -> None:
394
392
  """
395
393
  This method is used internally to check if the current
396
394
  animation needs to be skipped or not. It also checks if
@@ -402,13 +400,13 @@ class OpenGLRenderer:
402
400
  if self.file_writer.sections[-1].skip_animations:
403
401
  self.skip_animations = True
404
402
  if (
405
- config["from_animation_number"]
406
- and self.num_plays < config["from_animation_number"]
403
+ config.from_animation_number > 0
404
+ and self.num_plays < config.from_animation_number
407
405
  ):
408
406
  self.skip_animations = True
409
407
  if (
410
- config["upto_animation_number"]
411
- and self.num_plays > config["upto_animation_number"]
408
+ config.upto_animation_number >= 0
409
+ and self.num_plays > config.upto_animation_number
412
410
  ):
413
411
  self.skip_animations = True
414
412
  raise EndSceneEarlyException()
@@ -425,8 +423,9 @@ class OpenGLRenderer:
425
423
  self.update_frame(scene)
426
424
 
427
425
  if not self.skip_animations:
428
- for _ in range(int(config.frame_rate * scene.duration)):
429
- self.file_writer.write_frame(self)
426
+ self.file_writer.write_frame(
427
+ self, num_frames=int(config.frame_rate * scene.duration)
428
+ )
430
429
 
431
430
  if self.window is not None:
432
431
  self.window.swap_buffers()
@@ -572,7 +571,7 @@ class OpenGLRenderer:
572
571
  if pixel_shape is None:
573
572
  return np.array([0, 0, 0])
574
573
  pw, ph = pixel_shape
575
- fw, fh = config["frame_width"], config["frame_height"]
574
+ fh = config["frame_height"]
576
575
  fc = self.camera.get_center()
577
576
  if relative:
578
577
  return 2 * np.array([px / pw, py / ph, 0])
manim/renderer/shader.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  import inspect
4
5
  import re
5
6
  import textwrap
@@ -382,10 +383,8 @@ class Shader:
382
383
  shader_program_cache[self.name] = self.shader_program
383
384
 
384
385
  def set_uniform(self, name, value):
385
- try:
386
+ with contextlib.suppress(KeyError):
386
387
  self.shader_program[name] = value
387
- except KeyError:
388
- pass
389
388
 
390
389
 
391
390
  class FullScreenQuad(Mesh):
@@ -1,14 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import copy
4
+ import logging
4
5
  import re
5
6
  from pathlib import Path
6
7
 
7
8
  import moderngl
8
9
  import numpy as np
9
10
 
10
- from .. import logger
11
-
12
11
  # Mobjects that should be rendered with
13
12
  # the same shader will be organized and
14
13
  # clumped together based on keeping track
@@ -17,6 +16,8 @@ from .. import logger
17
16
 
18
17
  __all__ = ["ShaderWrapper"]
19
18
 
19
+ logger = logging.getLogger("manim")
20
+
20
21
 
21
22
  def get_shader_dir():
22
23
  return Path(__file__).parent / "shaders"
@@ -64,6 +64,25 @@ Examples
64
64
  self.play(Restore(self.camera.frame))
65
65
  self.wait()
66
66
 
67
+ .. manim:: SlidingMultipleScenes
68
+
69
+ class SlidingMultipleScenes(MovingCameraScene):
70
+ def construct(self):
71
+ def create_scene(number):
72
+ frame = Rectangle(width=16,height=9)
73
+ circ = Circle().shift(LEFT)
74
+ text = Tex(f"This is Scene {str(number)}").next_to(circ, RIGHT)
75
+ frame.add(circ,text)
76
+ return frame
77
+
78
+ group = VGroup(*(create_scene(i) for i in range(4))).arrange_in_grid(buff=4)
79
+ self.add(group)
80
+ self.camera.auto_zoom(group[0], animate=False)
81
+ for scene in group:
82
+ self.play(self.camera.auto_zoom(scene))
83
+ self.wait()
84
+
85
+ self.play(self.camera.auto_zoom(group, margin=2))
67
86
  """
68
87
 
69
88
  from __future__ import annotations
@@ -83,8 +102,12 @@ class MovingCameraScene(Scene):
83
102
  This is a Scene, with special configurations and properties that
84
103
  make it suitable for cases where the camera must be moved around.
85
104
 
105
+ Note: Examples are included in the moving_camera_scene module
106
+ documentation, see below in the 'see also' section.
107
+
86
108
  .. SEEALSO::
87
109
 
110
+ :mod:`.moving_camera_scene`
88
111
  :class:`.MovingCamera`
89
112
  """
90
113
 
manim/scene/scene.py CHANGED
@@ -52,7 +52,10 @@ from ..utils.file_ops import open_media_file
52
52
  from ..utils.iterables import list_difference_update, list_update
53
53
 
54
54
  if TYPE_CHECKING:
55
- from typing import Callable, Iterable
55
+ from collections.abc import Sequence
56
+ from typing import Callable
57
+
58
+ from manim.mobject.mobject import _AnimationBuilder
56
59
 
57
60
 
58
61
  class RerunSceneHandler(FileSystemEventHandler):
@@ -99,12 +102,12 @@ class Scene:
99
102
 
100
103
  def __init__(
101
104
  self,
102
- renderer=None,
103
- camera_class=Camera,
104
- always_update_mobjects=False,
105
- random_seed=None,
106
- skip_animations=False,
107
- ):
105
+ renderer: CairoRenderer | OpenGLRenderer | None = None,
106
+ camera_class: type[Camera] = Camera,
107
+ always_update_mobjects: bool = False,
108
+ random_seed: int | None = None,
109
+ skip_animations: bool = False,
110
+ ) -> None:
108
111
  self.camera_class = camera_class
109
112
  self.always_update_mobjects = always_update_mobjects
110
113
  self.random_seed = random_seed
@@ -157,6 +160,11 @@ class Scene:
157
160
  def camera(self):
158
161
  return self.renderer.camera
159
162
 
163
+ @property
164
+ def time(self) -> float:
165
+ """The time since the start of the scene."""
166
+ return self.renderer.time
167
+
160
168
  def __deepcopy__(self, clone_from_id):
161
169
  cls = self.__class__
162
170
  result = cls.__new__(cls)
@@ -229,7 +237,7 @@ class Scene:
229
237
  self.construct()
230
238
  except EndSceneEarlyException:
231
239
  pass
232
- except RerunSceneException as e:
240
+ except RerunSceneException:
233
241
  self.remove(*self.mobjects)
234
242
  self.renderer.clear_screen()
235
243
  self.renderer.num_plays = 0
@@ -307,14 +315,14 @@ class Scene:
307
315
  def next_section(
308
316
  self,
309
317
  name: str = "unnamed",
310
- type: str = DefaultSectionType.NORMAL,
318
+ section_type: str = DefaultSectionType.NORMAL,
311
319
  skip_animations: bool = False,
312
320
  ) -> None:
313
321
  """Create separation here; the last section gets finished and a new one gets created.
314
322
  ``skip_animations`` skips the rendering of all animations in this section.
315
323
  Refer to :doc:`the documentation</tutorials/output_and_config>` on how to use sections.
316
324
  """
317
- self.renderer.file_writer.next_section(name, type, skip_animations)
325
+ self.renderer.file_writer.next_section(name, section_type, skip_animations)
318
326
 
319
327
  def __str__(self):
320
328
  return self.__class__.__name__
@@ -480,7 +488,7 @@ class Scene:
480
488
  self.moving_mobjects += mobjects
481
489
  return self
482
490
 
483
- def add_mobjects_from_animations(self, animations):
491
+ def add_mobjects_from_animations(self, animations: list[Animation]) -> None:
484
492
  curr_mobjects = self.get_mobject_family_members()
485
493
  for animation in animations:
486
494
  if animation.is_introducer():
@@ -619,7 +627,7 @@ class Scene:
619
627
 
620
628
  def restructure_mobjects(
621
629
  self,
622
- to_remove: Mobject,
630
+ to_remove: Sequence[Mobject],
623
631
  mobject_list_name: str = "mobjects",
624
632
  extract_families: bool = True,
625
633
  ):
@@ -679,7 +687,6 @@ class Scene:
679
687
  list
680
688
  The list of mobjects with the mobjects to remove removed.
681
689
  """
682
-
683
690
  new_mobjects = []
684
691
 
685
692
  def add_safe_mobjects_from_list(list_to_examine, set_to_remove):
@@ -873,7 +880,7 @@ class Scene:
873
880
 
874
881
  def compile_animations(
875
882
  self,
876
- *args: Animation | Iterable[Animation] | types.GeneratorType[Animation],
883
+ *args: Animation | Mobject | _AnimationBuilder,
877
884
  **kwargs,
878
885
  ):
879
886
  """
@@ -898,16 +905,16 @@ class Scene:
898
905
  for arg in arg_anims:
899
906
  try:
900
907
  animations.append(prepare_animation(arg))
901
- except TypeError:
908
+ except TypeError as e:
902
909
  if inspect.ismethod(arg):
903
910
  raise TypeError(
904
911
  "Passing Mobject methods to Scene.play is no longer"
905
912
  " supported. Use Mobject.animate instead.",
906
- )
913
+ ) from e
907
914
  else:
908
915
  raise TypeError(
909
916
  f"Unexpected argument {arg} passed to Scene.play().",
910
- )
917
+ ) from e
911
918
 
912
919
  for animation in animations:
913
920
  for k, v in kwargs.items():
@@ -1015,6 +1022,35 @@ class Scene:
1015
1022
  )
1016
1023
  return time_progression
1017
1024
 
1025
+ @classmethod
1026
+ def validate_run_time(
1027
+ cls,
1028
+ run_time: float,
1029
+ method: Callable[[Any, ...], Any],
1030
+ parameter_name: str = "run_time",
1031
+ ) -> float:
1032
+ method_name = f"{cls.__name__}.{method.__name__}()"
1033
+ if run_time <= 0:
1034
+ raise ValueError(
1035
+ f"{method_name} has a {parameter_name} of "
1036
+ f"{run_time:g} <= 0 seconds which Manim cannot render. "
1037
+ f"The {parameter_name} must be a positive number."
1038
+ )
1039
+
1040
+ # config.frame_rate holds the number of frames per second
1041
+ fps = config.frame_rate
1042
+ seconds_per_frame = 1 / fps
1043
+ if run_time < seconds_per_frame:
1044
+ logger.warning(
1045
+ f"The original {parameter_name} of {method_name}, "
1046
+ f"{run_time:g} seconds, is too short for the current frame "
1047
+ f"rate of {fps:g} FPS. Rendering with the shortest possible "
1048
+ f"{parameter_name} of {seconds_per_frame:g} seconds instead."
1049
+ )
1050
+ run_time = seconds_per_frame
1051
+
1052
+ return run_time
1053
+
1018
1054
  def get_run_time(self, animations: list[Animation]):
1019
1055
  """
1020
1056
  Gets the total run time for a list of animations.
@@ -1030,16 +1066,13 @@ class Scene:
1030
1066
  float
1031
1067
  The total ``run_time`` of all of the animations in the list.
1032
1068
  """
1033
-
1034
- if len(animations) == 1 and isinstance(animations[0], Wait):
1035
- return animations[0].duration
1036
-
1037
- else:
1038
- return np.max([animation.run_time for animation in animations])
1069
+ run_time = max(animation.run_time for animation in animations)
1070
+ run_time = self.validate_run_time(run_time, self.play, "total run_time")
1071
+ return run_time
1039
1072
 
1040
1073
  def play(
1041
1074
  self,
1042
- *args: Animation | Iterable[Animation] | types.GeneratorType[Animation],
1075
+ *args: Animation | Mobject | _AnimationBuilder,
1043
1076
  subcaption=None,
1044
1077
  subcaption_duration=None,
1045
1078
  subcaption_offset=0,
@@ -1088,15 +1121,15 @@ class Scene:
1088
1121
  )
1089
1122
  return
1090
1123
 
1091
- start_time = self.renderer.time
1124
+ start_time = self.time
1092
1125
  self.renderer.play(self, *args, **kwargs)
1093
- run_time = self.renderer.time - start_time
1126
+ run_time = self.time - start_time
1094
1127
  if subcaption:
1095
1128
  if subcaption_duration is None:
1096
1129
  subcaption_duration = run_time
1097
1130
  # The start of the subcaption needs to be offset by the
1098
1131
  # run_time of the animation because it is added after
1099
- # the animation has already been played (and Scene.renderer.time
1132
+ # the animation has already been played (and Scene.time
1100
1133
  # has already been updated).
1101
1134
  self.add_subcaption(
1102
1135
  content=subcaption,
@@ -1131,6 +1164,7 @@ class Scene:
1131
1164
  --------
1132
1165
  :class:`.Wait`, :meth:`.should_mobjects_update`
1133
1166
  """
1167
+ duration = self.validate_run_time(duration, self.wait, "duration")
1134
1168
  self.play(
1135
1169
  Wait(
1136
1170
  run_time=duration,
@@ -1154,6 +1188,7 @@ class Scene:
1154
1188
  --------
1155
1189
  :meth:`.wait`, :class:`.Wait`
1156
1190
  """
1191
+ duration = self.validate_run_time(duration, self.pause, "duration")
1157
1192
  self.wait(duration=duration, frozen_frame=True)
1158
1193
 
1159
1194
  def wait_until(self, stop_condition: Callable[[], bool], max_time: float = 60):
@@ -1167,11 +1202,12 @@ class Scene:
1167
1202
  max_time
1168
1203
  The maximum wait time in seconds.
1169
1204
  """
1205
+ max_time = self.validate_run_time(max_time, self.wait_until, "max_time")
1170
1206
  self.wait(max_time, stop_condition=stop_condition)
1171
1207
 
1172
1208
  def compile_animation_data(
1173
1209
  self,
1174
- *animations: Animation | Iterable[Animation] | types.GeneratorType[Animation],
1210
+ *animations: Animation | Mobject | _AnimationBuilder,
1175
1211
  **play_kwargs,
1176
1212
  ):
1177
1213
  """Given a list of animations, compile the corresponding
@@ -1205,16 +1241,16 @@ class Scene:
1205
1241
  self.moving_mobjects = []
1206
1242
  self.static_mobjects = []
1207
1243
 
1244
+ self.duration = self.get_run_time(self.animations)
1208
1245
  if len(self.animations) == 1 and isinstance(self.animations[0], Wait):
1209
1246
  if self.should_update_mobjects():
1210
1247
  self.update_mobjects(dt=0) # Any problems with this?
1211
1248
  self.stop_condition = self.animations[0].stop_condition
1212
1249
  else:
1213
- self.duration = self.animations[0].duration
1214
1250
  # Static image logic when the wait is static is done by the renderer, not here.
1215
1251
  self.animations[0].is_static_wait = True
1216
1252
  return None
1217
- self.duration = self.get_run_time(self.animations)
1253
+
1218
1254
  return self
1219
1255
 
1220
1256
  def begin_animations(self) -> None:
@@ -1298,9 +1334,7 @@ class Scene:
1298
1334
  return True
1299
1335
 
1300
1336
  def interactive_embed(self):
1301
- """
1302
- Like embed(), but allows for screen interaction.
1303
- """
1337
+ """Like embed(), but allows for screen interaction."""
1304
1338
  if not self.check_interactive_embed_is_valid():
1305
1339
  return
1306
1340
  self.interactive_mode = True
@@ -1508,7 +1542,7 @@ class Scene:
1508
1542
  r"""Adds an entry in the corresponding subcaption file
1509
1543
  at the current time stamp.
1510
1544
 
1511
- The current time stamp is obtained from ``Scene.renderer.time``.
1545
+ The current time stamp is obtained from ``Scene.time``.
1512
1546
 
1513
1547
  Parameters
1514
1548
  ----------
@@ -1538,18 +1572,15 @@ class Scene:
1538
1572
 
1539
1573
  # second option: within the call to Scene.play
1540
1574
  self.play(
1541
- Transform(square, circle),
1542
- subcaption="The square transforms."
1575
+ Transform(square, circle), subcaption="The square transforms."
1543
1576
  )
1544
1577
 
1545
1578
  """
1546
1579
  subtitle = srt.Subtitle(
1547
1580
  index=len(self.renderer.file_writer.subcaptions),
1548
1581
  content=content,
1549
- start=datetime.timedelta(seconds=float(self.renderer.time + offset)),
1550
- end=datetime.timedelta(
1551
- seconds=float(self.renderer.time + offset + duration)
1552
- ),
1582
+ start=datetime.timedelta(seconds=float(self.time + offset)),
1583
+ end=datetime.timedelta(seconds=float(self.time + offset + duration)),
1553
1584
  )
1554
1585
  self.renderer.file_writer.subcaptions.append(subtitle)
1555
1586
 
@@ -1597,7 +1628,7 @@ class Scene:
1597
1628
  """
1598
1629
  if self.renderer.skip_animations:
1599
1630
  return
1600
- time = self.renderer.time + time_offset
1631
+ time = self.time + time_offset
1601
1632
  self.renderer.file_writer.add_sound(sound_file, time, gain, **kwargs)
1602
1633
 
1603
1634
  def on_mouse_motion(self, point, d_point):