manim 0.17.0__py3-none-any.whl → 0.19.1__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.
Files changed (163) hide show
  1. manim/__init__.py +11 -6
  2. manim/__main__.py +62 -19
  3. manim/_config/__init__.py +10 -9
  4. manim/_config/cli_colors.py +26 -9
  5. manim/_config/default.cfg +1 -3
  6. manim/_config/logger_utils.py +23 -13
  7. manim/_config/utils.py +662 -468
  8. manim/animation/animation.py +164 -18
  9. manim/animation/changing.py +34 -23
  10. manim/animation/composition.py +265 -67
  11. manim/animation/creation.py +208 -26
  12. manim/animation/fading.py +16 -18
  13. manim/animation/growing.py +35 -15
  14. manim/animation/indication.py +150 -76
  15. manim/animation/movement.py +56 -22
  16. manim/animation/numbers.py +64 -6
  17. manim/animation/rotation.py +78 -7
  18. manim/animation/specialized.py +6 -7
  19. manim/animation/speedmodifier.py +13 -10
  20. manim/animation/transform.py +14 -11
  21. manim/animation/transform_matching_parts.py +3 -4
  22. manim/animation/updaters/mobject_update_utils.py +152 -30
  23. manim/animation/updaters/update.py +10 -7
  24. manim/camera/camera.py +182 -118
  25. manim/camera/mapping_camera.py +34 -3
  26. manim/camera/moving_camera.py +95 -74
  27. manim/camera/multi_camera.py +23 -15
  28. manim/camera/three_d_camera.py +70 -52
  29. manim/cli/__init__.py +17 -0
  30. manim/cli/cfg/group.py +76 -44
  31. manim/cli/checkhealth/checks.py +192 -0
  32. manim/cli/checkhealth/commands.py +90 -0
  33. manim/cli/default_group.py +158 -25
  34. manim/cli/init/commands.py +33 -25
  35. manim/cli/plugins/commands.py +16 -3
  36. manim/cli/render/commands.py +72 -60
  37. manim/cli/render/ease_of_access_options.py +4 -3
  38. manim/cli/render/global_options.py +59 -17
  39. manim/cli/render/output_options.py +6 -5
  40. manim/cli/render/render_options.py +98 -33
  41. manim/constants.py +109 -59
  42. manim/data_structures.py +31 -0
  43. manim/mobject/frame.py +8 -5
  44. manim/mobject/geometry/__init__.py +1 -0
  45. manim/mobject/geometry/arc.py +277 -135
  46. manim/mobject/geometry/boolean_ops.py +32 -31
  47. manim/mobject/geometry/labeled.py +376 -0
  48. manim/mobject/geometry/line.py +192 -87
  49. manim/mobject/geometry/polygram.py +224 -58
  50. manim/mobject/geometry/shape_matchers.py +61 -25
  51. manim/mobject/geometry/tips.py +122 -48
  52. manim/mobject/graph.py +1027 -419
  53. manim/mobject/graphing/coordinate_systems.py +533 -278
  54. manim/mobject/graphing/functions.py +53 -32
  55. manim/mobject/graphing/number_line.py +123 -65
  56. manim/mobject/graphing/probability.py +88 -62
  57. manim/mobject/graphing/scale.py +33 -19
  58. manim/mobject/logo.py +118 -28
  59. manim/mobject/matrix.py +87 -83
  60. manim/mobject/mobject.py +912 -442
  61. manim/mobject/opengl/dot_cloud.py +16 -5
  62. manim/mobject/opengl/opengl_compatibility.py +4 -2
  63. manim/mobject/opengl/opengl_geometry.py +254 -153
  64. manim/mobject/opengl/opengl_image_mobject.py +3 -1
  65. manim/mobject/opengl/opengl_mobject.py +779 -482
  66. manim/mobject/opengl/opengl_point_cloud_mobject.py +41 -14
  67. manim/mobject/opengl/opengl_surface.py +14 -92
  68. manim/mobject/opengl/opengl_three_dimensions.py +12 -8
  69. manim/mobject/opengl/opengl_vectorized_mobject.py +98 -100
  70. manim/mobject/svg/brace.py +173 -41
  71. manim/mobject/svg/svg_mobject.py +139 -53
  72. manim/mobject/table.py +61 -68
  73. manim/mobject/text/code_mobject.py +193 -539
  74. manim/mobject/text/numbers.py +81 -34
  75. manim/mobject/text/tex_mobject.py +130 -78
  76. manim/mobject/text/text_mobject.py +288 -164
  77. manim/mobject/three_d/polyhedra.py +111 -13
  78. manim/mobject/three_d/three_d_utils.py +17 -8
  79. manim/mobject/three_d/three_dimensions.py +239 -106
  80. manim/mobject/types/image_mobject.py +50 -30
  81. manim/mobject/types/point_cloud_mobject.py +120 -75
  82. manim/mobject/types/vectorized_mobject.py +841 -408
  83. manim/mobject/value_tracker.py +105 -38
  84. manim/mobject/vector_field.py +50 -31
  85. manim/opengl/__init__.py +3 -3
  86. manim/plugins/__init__.py +14 -1
  87. manim/plugins/plugins_flags.py +10 -14
  88. manim/renderer/cairo_renderer.py +65 -50
  89. manim/renderer/opengl_renderer.py +89 -69
  90. manim/renderer/opengl_renderer_window.py +39 -18
  91. manim/renderer/shader.py +123 -87
  92. manim/renderer/shader_wrapper.py +44 -28
  93. manim/renderer/vectorized_mobject_rendering.py +38 -10
  94. manim/scene/moving_camera_scene.py +32 -3
  95. manim/scene/scene.py +507 -242
  96. manim/scene/scene_file_writer.py +371 -220
  97. manim/scene/section.py +20 -16
  98. manim/scene/three_d_scene.py +14 -22
  99. manim/scene/vector_space_scene.py +223 -129
  100. manim/scene/zoomed_scene.py +46 -41
  101. manim/typing.py +990 -0
  102. manim/utils/bezier.py +1823 -371
  103. manim/utils/caching.py +12 -5
  104. manim/utils/color/AS2700.py +236 -0
  105. manim/utils/color/BS381.py +318 -0
  106. manim/utils/color/DVIPSNAMES.py +96 -0
  107. manim/utils/color/SVGNAMES.py +179 -0
  108. manim/utils/color/X11.py +533 -0
  109. manim/utils/color/XKCD.py +952 -0
  110. manim/utils/color/__init__.py +61 -0
  111. manim/utils/color/core.py +1667 -0
  112. manim/utils/color/manim_colors.py +218 -0
  113. manim/utils/commands.py +48 -20
  114. manim/utils/config_ops.py +39 -19
  115. manim/utils/debug.py +8 -7
  116. manim/utils/deprecation.py +86 -39
  117. manim/utils/docbuild/__init__.py +17 -0
  118. manim/utils/docbuild/autoaliasattr_directive.py +236 -0
  119. manim/utils/docbuild/autocolor_directive.py +99 -0
  120. manim/utils/docbuild/manim_directive.py +94 -41
  121. manim/utils/docbuild/module_parsing.py +245 -0
  122. manim/utils/exceptions.py +6 -0
  123. manim/utils/family.py +5 -3
  124. manim/utils/family_ops.py +17 -4
  125. manim/utils/file_ops.py +27 -17
  126. manim/utils/hashing.py +55 -45
  127. manim/utils/images.py +13 -7
  128. manim/utils/ipython_magic.py +13 -7
  129. manim/utils/iterables.py +163 -120
  130. manim/utils/module_ops.py +66 -24
  131. manim/utils/opengl.py +77 -24
  132. manim/utils/parameter_parsing.py +32 -0
  133. manim/utils/paths.py +30 -33
  134. manim/utils/polylabel.py +235 -0
  135. manim/utils/qhull.py +218 -0
  136. manim/utils/rate_functions.py +98 -32
  137. manim/utils/simple_functions.py +25 -33
  138. manim/utils/sounds.py +7 -1
  139. manim/utils/space_ops.py +188 -115
  140. manim/utils/testing/__init__.py +17 -0
  141. manim/utils/testing/_frames_testers.py +13 -8
  142. manim/utils/testing/_show_diff.py +5 -3
  143. manim/utils/testing/_test_class_makers.py +34 -18
  144. manim/utils/testing/frames_comparison.py +37 -19
  145. manim/utils/tex.py +130 -198
  146. manim/utils/tex_file_writing.py +77 -47
  147. manim/utils/tex_templates.py +2 -1
  148. manim/utils/unit.py +6 -5
  149. {manim-0.17.0.dist-info → manim-0.19.1.dist-info}/METADATA +64 -65
  150. manim-0.19.1.dist-info/RECORD +220 -0
  151. {manim-0.17.0.dist-info → manim-0.19.1.dist-info}/WHEEL +1 -1
  152. manim-0.19.1.dist-info/entry_points.txt +3 -0
  153. {manim-0.17.0.dist-info → manim-0.19.1.dist-info/licenses}/LICENSE.community +1 -1
  154. manim/cli/new/group.py +0 -189
  155. manim/communitycolors.py +0 -9
  156. manim/gui/__init__.py +0 -0
  157. manim/gui/gui.py +0 -82
  158. manim/plugins/import_plugins.py +0 -43
  159. manim/utils/color.py +0 -552
  160. manim-0.17.0.dist-info/RECORD +0 -206
  161. manim-0.17.0.dist-info/entry_points.txt +0 -4
  162. /manim/cli/{new → checkhealth}/__init__.py +0 -0
  163. {manim-0.17.0.dist-info → manim-0.19.1.dist-info/licenses}/LICENSE +0 -0
@@ -11,37 +11,61 @@ __all__ = [
11
11
  "DashedVMobject",
12
12
  ]
13
13
 
14
-
15
14
  import itertools as it
16
15
  import sys
17
- import typing
18
- from typing import Callable, Optional, Sequence, Union
16
+ from collections.abc import Callable, Hashable, Iterable, Mapping, Sequence
17
+ from typing import TYPE_CHECKING, Any, Literal
19
18
 
20
- import colour
21
19
  import numpy as np
22
20
  from PIL.Image import Image
23
21
 
22
+ from manim import config
23
+ from manim.constants import *
24
+ from manim.mobject.mobject import Mobject
24
25
  from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
26
+ from manim.mobject.opengl.opengl_mobject import OpenGLMobject
25
27
  from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
26
28
  from manim.mobject.three_d.three_d_utils import (
27
29
  get_3d_vmob_gradient_start_and_end_points,
28
30
  )
29
-
30
- from ... import config
31
- from ...constants import *
32
- from ...mobject.mobject import Mobject
33
- from ...utils.bezier import (
31
+ from manim.utils.bezier import (
34
32
  bezier,
35
- get_smooth_handle_points,
33
+ bezier_remap,
34
+ get_smooth_cubic_bezier_handle_points,
36
35
  integer_interpolate,
37
36
  interpolate,
38
37
  partial_bezier_points,
39
38
  proportions_along_bezier_curve_for_point,
40
39
  )
41
- from ...utils.color import BLACK, WHITE, color_to_rgba
42
- from ...utils.deprecation import deprecated
43
- from ...utils.iterables import make_even, resize_array, stretch_array_to_length, tuplify
44
- from ...utils.space_ops import rotate_vector, shoelace_direction
40
+ from manim.utils.color import BLACK, WHITE, ManimColor, ParsableManimColor
41
+ from manim.utils.iterables import (
42
+ make_even,
43
+ resize_array,
44
+ stretch_array_to_length,
45
+ tuplify,
46
+ )
47
+ from manim.utils.space_ops import rotate_vector, shoelace_direction
48
+
49
+ if TYPE_CHECKING:
50
+ import numpy.typing as npt
51
+ from typing_extensions import Self
52
+
53
+ from manim.typing import (
54
+ CubicBezierPath,
55
+ CubicBezierPointsLike,
56
+ CubicSpline,
57
+ FloatRGBA,
58
+ FloatRGBA_Array,
59
+ ManimFloat,
60
+ MappingFunction,
61
+ Point2DLike,
62
+ Point3D,
63
+ Point3D_Array,
64
+ Point3DLike,
65
+ Point3DLike_Array,
66
+ Vector3D,
67
+ Vector3DLike,
68
+ )
45
69
 
46
70
  # TODO
47
71
  # - Change cubic curve groups to have 4 points instead of 3
@@ -81,68 +105,86 @@ class VMobject(Mobject):
81
105
 
82
106
  def __init__(
83
107
  self,
84
- fill_color=None,
85
- fill_opacity=0.0,
86
- stroke_color=None,
87
- stroke_opacity=1.0,
88
- stroke_width=DEFAULT_STROKE_WIDTH,
89
- background_stroke_color=BLACK,
90
- background_stroke_opacity=1.0,
91
- background_stroke_width=0,
92
- sheen_factor=0.0,
108
+ fill_color: ParsableManimColor | None = None,
109
+ fill_opacity: float = 0.0,
110
+ stroke_color: ParsableManimColor | None = None,
111
+ stroke_opacity: float = 1.0,
112
+ stroke_width: float = DEFAULT_STROKE_WIDTH,
113
+ background_stroke_color: ParsableManimColor | None = BLACK,
114
+ background_stroke_opacity: float = 1.0,
115
+ background_stroke_width: float = 0,
116
+ sheen_factor: float = 0.0,
93
117
  joint_type: LineJointType | None = None,
94
- sheen_direction=UL,
95
- close_new_points=False,
96
- pre_function_handle_to_anchor_scale_factor=0.01,
97
- make_smooth_after_applying_functions=False,
98
- background_image=None,
99
- shade_in_3d=False,
118
+ sheen_direction: Vector3DLike = UL,
119
+ close_new_points: bool = False,
120
+ pre_function_handle_to_anchor_scale_factor: float = 0.01,
121
+ make_smooth_after_applying_functions: bool = False,
122
+ background_image: Image | str | None = None,
123
+ shade_in_3d: bool = False,
100
124
  # TODO, do we care about accounting for varying zoom levels?
101
- tolerance_for_point_equality=1e-6,
102
- n_points_per_cubic_curve=4,
103
- **kwargs,
125
+ tolerance_for_point_equality: float = 1e-6,
126
+ n_points_per_cubic_curve: int = 4,
127
+ cap_style: CapStyleType = CapStyleType.AUTO,
128
+ **kwargs: Any,
104
129
  ):
105
130
  self.fill_opacity = fill_opacity
106
131
  self.stroke_opacity = stroke_opacity
107
132
  self.stroke_width = stroke_width
108
- self.background_stroke_color = background_stroke_color
109
- self.background_stroke_opacity = background_stroke_opacity
110
- self.background_stroke_width = background_stroke_width
111
- self.sheen_factor = sheen_factor
112
- if joint_type is None:
113
- joint_type = LineJointType.AUTO
114
- self.joint_type = joint_type
133
+ if background_stroke_color is not None:
134
+ self.background_stroke_color: ManimColor = ManimColor(
135
+ background_stroke_color
136
+ )
137
+ self.background_stroke_opacity: float = background_stroke_opacity
138
+ self.background_stroke_width: float = background_stroke_width
139
+ self.sheen_factor: float = sheen_factor
140
+ self.joint_type: LineJointType = (
141
+ LineJointType.AUTO if joint_type is None else joint_type
142
+ )
115
143
  self.sheen_direction = sheen_direction
116
- self.close_new_points = close_new_points
117
- self.pre_function_handle_to_anchor_scale_factor = (
144
+ self.close_new_points: bool = close_new_points
145
+ self.pre_function_handle_to_anchor_scale_factor: float = (
118
146
  pre_function_handle_to_anchor_scale_factor
119
147
  )
120
- self.make_smooth_after_applying_functions = make_smooth_after_applying_functions
121
- self.background_image = background_image
122
- self.shade_in_3d = shade_in_3d
123
- self.tolerance_for_point_equality = tolerance_for_point_equality
124
- self.n_points_per_cubic_curve = n_points_per_cubic_curve
148
+ self.make_smooth_after_applying_functions: bool = (
149
+ make_smooth_after_applying_functions
150
+ )
151
+ self.background_image: Image | str | None = background_image
152
+ self.shade_in_3d: bool = shade_in_3d
153
+ self.tolerance_for_point_equality: float = tolerance_for_point_equality
154
+ self.n_points_per_cubic_curve: int = n_points_per_cubic_curve
155
+ self._bezier_t_values: npt.NDArray[float] = np.linspace(
156
+ 0, 1, n_points_per_cubic_curve
157
+ )
158
+ self.cap_style: CapStyleType = cap_style
125
159
  super().__init__(**kwargs)
160
+ self.submobjects: list[VMobject]
126
161
 
127
- if fill_color:
128
- self.fill_color = fill_color
129
- if stroke_color:
130
- self.stroke_color = stroke_color
162
+ # TODO: Find where color overwrites are happening and remove the color doubling
163
+ # if "color" in kwargs:
164
+ # fill_color = kwargs["color"]
165
+ # stroke_color = kwargs["color"]
166
+ if fill_color is not None:
167
+ self.fill_color = ManimColor.parse(fill_color)
168
+ if stroke_color is not None:
169
+ self.stroke_color = ManimColor.parse(stroke_color)
170
+
171
+ def _assert_valid_submobjects(self, submobjects: Iterable[VMobject]) -> Self:
172
+ return self._assert_valid_submobjects_internal(submobjects, VMobject)
131
173
 
132
174
  # OpenGL compatibility
133
175
  @property
134
- def n_points_per_curve(self):
176
+ def n_points_per_curve(self) -> int:
135
177
  return self.n_points_per_cubic_curve
136
178
 
137
- def get_group_class(self):
179
+ def get_group_class(self) -> type[VGroup]:
138
180
  return VGroup
139
181
 
140
182
  @staticmethod
141
- def get_mobject_type_class():
183
+ def get_mobject_type_class() -> type[VMobject]:
142
184
  return VMobject
143
185
 
144
186
  # Colors
145
- def init_colors(self, propagate_colors=True):
187
+ def init_colors(self, propagate_colors: bool = True) -> Self:
146
188
  self.set_fill(
147
189
  color=self.fill_color,
148
190
  opacity=self.fill_opacity,
@@ -172,7 +214,11 @@ class VMobject(Mobject):
172
214
 
173
215
  return self
174
216
 
175
- def generate_rgbas_array(self, color, opacity):
217
+ def generate_rgbas_array(
218
+ self,
219
+ color: ParsableManimColor | Iterable[ManimColor] | None,
220
+ opacity: float | Iterable[float],
221
+ ) -> FloatRGBA:
176
222
  """
177
223
  First arg can be either a color, or a tuple/list of colors.
178
224
  Likewise, opacity can either be a float, or a tuple of floats.
@@ -180,10 +226,14 @@ class VMobject(Mobject):
180
226
  one color was passed in, a second slightly light color
181
227
  will automatically be added for the gradient
182
228
  """
183
- colors = [c if (c is not None) else BLACK for c in tuplify(color)]
184
- opacities = [o if (o is not None) else 0 for o in tuplify(opacity)]
185
- rgbas = np.array(
186
- [color_to_rgba(c, o) for c, o in zip(*make_even(colors, opacities))],
229
+ colors: list[ManimColor] = [
230
+ ManimColor(c) if (c is not None) else BLACK for c in tuplify(color)
231
+ ]
232
+ opacities: list[float] = [
233
+ o if (o is not None) else 0.0 for o in tuplify(opacity)
234
+ ]
235
+ rgbas: FloatRGBA_Array = np.array(
236
+ [c.to_rgba_with_alpha(o) for c, o in zip(*make_even(colors, opacities))],
187
237
  )
188
238
 
189
239
  sheen_factor = self.get_sheen_factor()
@@ -194,7 +244,12 @@ class VMobject(Mobject):
194
244
  rgbas = np.append(rgbas, light_rgbas, axis=0)
195
245
  return rgbas
196
246
 
197
- def update_rgbas_array(self, array_name, color=None, opacity=None):
247
+ def update_rgbas_array(
248
+ self,
249
+ array_name: str,
250
+ color: ParsableManimColor | Iterable[ManimColor] | None = None,
251
+ opacity: float | None = None,
252
+ ) -> Self:
198
253
  rgbas = self.generate_rgbas_array(color, opacity)
199
254
  if not hasattr(self, array_name):
200
255
  setattr(self, array_name, rgbas)
@@ -217,10 +272,10 @@ class VMobject(Mobject):
217
272
 
218
273
  def set_fill(
219
274
  self,
220
- color: str | None = None,
275
+ color: ParsableManimColor | None = None,
221
276
  opacity: float | None = None,
222
277
  family: bool = True,
223
- ):
278
+ ) -> Self:
224
279
  """Set the fill color and fill opacity of a :class:`VMobject`.
225
280
 
226
281
  Parameters
@@ -260,18 +315,19 @@ class VMobject(Mobject):
260
315
  for submobject in self.submobjects:
261
316
  submobject.set_fill(color, opacity, family)
262
317
  self.update_rgbas_array("fill_rgbas", color, opacity)
318
+ self.fill_rgbas: FloatRGBA_Array
263
319
  if opacity is not None:
264
320
  self.fill_opacity = opacity
265
321
  return self
266
322
 
267
323
  def set_stroke(
268
324
  self,
269
- color=None,
270
- width=None,
271
- opacity=None,
325
+ color: ParsableManimColor = None,
326
+ width: float | None = None,
327
+ opacity: float | None = None,
272
328
  background=False,
273
- family=True,
274
- ):
329
+ family: bool = True,
330
+ ) -> Self:
275
331
  if family:
276
332
  for submobject in self.submobjects:
277
333
  submobject.set_stroke(color, width, opacity, background, family)
@@ -289,29 +345,60 @@ class VMobject(Mobject):
289
345
  if opacity is not None:
290
346
  setattr(self, opacity_name, opacity)
291
347
  if color is not None and background:
292
- self.background_stroke_color = color
348
+ if isinstance(color, (list, tuple)):
349
+ self.background_stroke_color = ManimColor.parse(color)
350
+ else:
351
+ self.background_stroke_color = ManimColor(color)
293
352
  return self
294
353
 
295
- def set_background_stroke(self, **kwargs):
354
+ def set_cap_style(self, cap_style: CapStyleType) -> Self:
355
+ """
356
+ Sets the cap style of the :class:`VMobject`.
357
+
358
+ Parameters
359
+ ----------
360
+ cap_style
361
+ The cap style to be set. See :class:`.CapStyleType` for options.
362
+
363
+ Returns
364
+ -------
365
+ :class:`VMobject`
366
+ ``self``
367
+
368
+ Examples
369
+ --------
370
+ .. manim:: CapStyleExample
371
+ :save_last_frame:
372
+
373
+ class CapStyleExample(Scene):
374
+ def construct(self):
375
+ line = Line(LEFT, RIGHT, color=YELLOW, stroke_width=20)
376
+ line.set_cap_style(CapStyleType.ROUND)
377
+ self.add(line)
378
+ """
379
+ self.cap_style = cap_style
380
+ return self
381
+
382
+ def set_background_stroke(self, **kwargs) -> Self:
296
383
  kwargs["background"] = True
297
384
  self.set_stroke(**kwargs)
298
385
  return self
299
386
 
300
387
  def set_style(
301
388
  self,
302
- fill_color=None,
303
- fill_opacity=None,
304
- stroke_color=None,
305
- stroke_width=None,
306
- stroke_opacity=None,
307
- background_stroke_color=None,
308
- background_stroke_width=None,
309
- background_stroke_opacity=None,
310
- sheen_factor=None,
311
- sheen_direction=None,
312
- background_image=None,
313
- family=True,
314
- ):
389
+ fill_color: ParsableManimColor | None = None,
390
+ fill_opacity: float | None = None,
391
+ stroke_color: ParsableManimColor | None = None,
392
+ stroke_width: float | None = None,
393
+ stroke_opacity: float | None = None,
394
+ background_stroke_color: ParsableManimColor | None = None,
395
+ background_stroke_width: float | None = None,
396
+ background_stroke_opacity: float | None = None,
397
+ sheen_factor: float | None = None,
398
+ sheen_direction: Vector3DLike | None = None,
399
+ background_image: Image | str | None = None,
400
+ family: bool = True,
401
+ ) -> Self:
315
402
  self.set_fill(color=fill_color, opacity=fill_opacity, family=family)
316
403
  self.set_stroke(
317
404
  color=stroke_color,
@@ -335,16 +422,17 @@ class VMobject(Mobject):
335
422
  self.color_using_background_image(background_image)
336
423
  return self
337
424
 
338
- def get_style(self, simple=False):
425
+ def get_style(self, simple: bool = False) -> dict:
339
426
  ret = {
340
427
  "stroke_opacity": self.get_stroke_opacity(),
341
428
  "stroke_width": self.get_stroke_width(),
342
429
  }
343
430
 
431
+ # TODO: FIX COLORS HERE
344
432
  if simple:
345
- ret["fill_color"] = colour.rgb2hex(self.get_fill_color().get_rgb())
433
+ ret["fill_color"] = self.get_fill_color()
346
434
  ret["fill_opacity"] = self.get_fill_opacity()
347
- ret["stroke_color"] = colour.rgb2hex(self.get_stroke_color().get_rgb())
435
+ ret["stroke_color"] = self.get_stroke_color()
348
436
  else:
349
437
  ret["fill_color"] = self.get_fill_colors()
350
438
  ret["fill_opacity"] = self.get_fill_opacities()
@@ -358,7 +446,7 @@ class VMobject(Mobject):
358
446
 
359
447
  return ret
360
448
 
361
- def match_style(self, vmobject, family=True):
449
+ def match_style(self, vmobject: VMobject, family: bool = True) -> Self:
362
450
  self.set_style(**vmobject.get_style(), family=False)
363
451
 
364
452
  if family:
@@ -373,18 +461,83 @@ class VMobject(Mobject):
373
461
  sm1.match_style(sm2)
374
462
  return self
375
463
 
376
- def set_color(self, color, family=True):
464
+ def set_color(self, color: ParsableManimColor, family: bool = True) -> Self:
377
465
  self.set_fill(color, family=family)
378
466
  self.set_stroke(color, family=family)
379
467
  return self
380
468
 
381
- def set_opacity(self, opacity, family=True):
469
+ def set_opacity(self, opacity: float, family: bool = True) -> Self:
382
470
  self.set_fill(opacity=opacity, family=family)
383
471
  self.set_stroke(opacity=opacity, family=family)
384
472
  self.set_stroke(opacity=opacity, family=family, background=True)
385
473
  return self
386
474
 
387
- def fade(self, darkness=0.5, family=True):
475
+ def scale(
476
+ self,
477
+ scale_factor: float,
478
+ scale_stroke: bool = False,
479
+ *,
480
+ about_point: Point3DLike | None = None,
481
+ about_edge: Vector3DLike | None = None,
482
+ ) -> Self:
483
+ r"""Scale the size by a factor.
484
+
485
+ Default behavior is to scale about the center of the vmobject.
486
+
487
+ Parameters
488
+ ----------
489
+ scale_factor
490
+ The scaling factor :math:`\alpha`. If :math:`0 < |\alpha| < 1`, the mobject
491
+ will shrink, and for :math:`|\alpha| > 1` it will grow. Furthermore,
492
+ if :math:`\alpha < 0`, the mobject is also flipped.
493
+ scale_stroke
494
+ Boolean determining if the object's outline is scaled when the object is scaled.
495
+ If enabled, and object with 2px outline is scaled by a factor of .5, it will have an outline of 1px.
496
+ kwargs
497
+ Additional keyword arguments passed to
498
+ :meth:`~.Mobject.scale`.
499
+
500
+ Returns
501
+ -------
502
+ :class:`VMobject`
503
+ ``self``
504
+
505
+ Examples
506
+ --------
507
+
508
+ .. manim:: MobjectScaleExample
509
+ :save_last_frame:
510
+
511
+ class MobjectScaleExample(Scene):
512
+ def construct(self):
513
+ c1 = Circle(1, RED).set_x(-1)
514
+ c2 = Circle(1, GREEN).set_x(1)
515
+
516
+ vg = VGroup(c1, c2)
517
+ vg.set_stroke(width=50)
518
+ self.add(vg)
519
+
520
+ self.play(
521
+ c1.animate.scale(.25),
522
+ c2.animate.scale(.25,
523
+ scale_stroke=True)
524
+ )
525
+
526
+ See also
527
+ --------
528
+ :meth:`move_to`
529
+
530
+ """
531
+ if scale_stroke:
532
+ self.set_stroke(width=abs(scale_factor) * self.get_stroke_width())
533
+ self.set_stroke(
534
+ width=abs(scale_factor) * self.get_stroke_width(background=True),
535
+ background=True,
536
+ )
537
+ super().scale(scale_factor, about_point=about_point, about_edge=about_edge)
538
+ return self
539
+
540
+ def fade(self, darkness: float = 0.5, family: bool = True) -> Self:
388
541
  factor = 1.0 - darkness
389
542
  self.set_fill(opacity=factor * self.get_fill_opacity(), family=False)
390
543
  self.set_stroke(opacity=factor * self.get_stroke_opacity(), family=False)
@@ -395,13 +548,13 @@ class VMobject(Mobject):
395
548
  super().fade(darkness, family)
396
549
  return self
397
550
 
398
- def get_fill_rgbas(self):
551
+ def get_fill_rgbas(self) -> FloatRGBA_Array:
399
552
  try:
400
553
  return self.fill_rgbas
401
554
  except AttributeError:
402
555
  return np.zeros((1, 4))
403
556
 
404
- def get_fill_color(self):
557
+ def get_fill_color(self) -> ManimColor:
405
558
  """
406
559
  If there are multiple colors (for gradient)
407
560
  this returns the first one
@@ -410,66 +563,70 @@ class VMobject(Mobject):
410
563
 
411
564
  fill_color = property(get_fill_color, set_fill)
412
565
 
413
- def get_fill_opacity(self):
566
+ def get_fill_opacity(self) -> ManimFloat:
414
567
  """
415
568
  If there are multiple opacities, this returns the
416
569
  first
417
570
  """
418
571
  return self.get_fill_opacities()[0]
419
572
 
420
- def get_fill_colors(self):
573
+ # TODO: Does this just do a copy?
574
+ # TODO: I have the feeling that this function should not return None, does that have any usage ?
575
+ def get_fill_colors(self) -> list[ManimColor | None]:
421
576
  return [
422
- colour.Color(rgb=rgba[:3]) if rgba.any() else None
577
+ ManimColor(rgba[:3]) if rgba.any() else None
423
578
  for rgba in self.get_fill_rgbas()
424
579
  ]
425
580
 
426
- def get_fill_opacities(self):
581
+ def get_fill_opacities(self) -> npt.NDArray[ManimFloat]:
427
582
  return self.get_fill_rgbas()[:, 3]
428
583
 
429
- def get_stroke_rgbas(self, background=False):
584
+ def get_stroke_rgbas(self, background: bool = False) -> FloatRGBA_Array:
430
585
  try:
431
586
  if background:
587
+ self.background_stroke_rgbas: FloatRGBA_Array
432
588
  rgbas = self.background_stroke_rgbas
433
589
  else:
590
+ self.stroke_rgbas: FloatRGBA_Array
434
591
  rgbas = self.stroke_rgbas
435
592
  return rgbas
436
593
  except AttributeError:
437
594
  return np.zeros((1, 4))
438
595
 
439
- def get_stroke_color(self, background=False):
596
+ def get_stroke_color(self, background: bool = False) -> ManimColor | None:
440
597
  return self.get_stroke_colors(background)[0]
441
598
 
442
599
  stroke_color = property(get_stroke_color, set_stroke)
443
600
 
444
- def get_stroke_width(self, background=False):
601
+ def get_stroke_width(self, background: bool = False) -> float:
445
602
  if background:
446
603
  width = self.background_stroke_width
447
604
  else:
448
605
  width = self.stroke_width
449
606
  if isinstance(width, str):
450
607
  width = int(width)
451
- return max(0, width)
608
+ return max(0.0, width)
452
609
 
453
- def get_stroke_opacity(self, background=False):
610
+ def get_stroke_opacity(self, background: bool = False) -> ManimFloat:
454
611
  return self.get_stroke_opacities(background)[0]
455
612
 
456
- def get_stroke_colors(self, background=False):
613
+ def get_stroke_colors(self, background: bool = False) -> list[ManimColor | None]:
457
614
  return [
458
- colour.Color(rgb=rgba[:3]) if rgba.any() else None
615
+ ManimColor(rgba[:3]) if rgba.any() else None
459
616
  for rgba in self.get_stroke_rgbas(background)
460
617
  ]
461
618
 
462
- def get_stroke_opacities(self, background=False):
619
+ def get_stroke_opacities(self, background: bool = False) -> npt.NDArray[ManimFloat]:
463
620
  return self.get_stroke_rgbas(background)[:, 3]
464
621
 
465
- def get_color(self):
622
+ def get_color(self) -> ManimColor:
466
623
  if np.all(self.get_fill_opacities() == 0):
467
624
  return self.get_stroke_color()
468
625
  return self.get_fill_color()
469
626
 
470
- color = property(get_color, set_color)
627
+ color: ManimColor = property(get_color, set_color)
471
628
 
472
- def set_sheen_direction(self, direction: np.ndarray, family=True):
629
+ def set_sheen_direction(self, direction: Vector3DLike, family: bool = True) -> Self:
473
630
  """Sets the direction of the applied sheen.
474
631
 
475
632
  Parameters
@@ -488,16 +645,17 @@ class VMobject(Mobject):
488
645
  :meth:`~.VMobject.set_sheen`
489
646
  :meth:`~.VMobject.rotate_sheen_direction`
490
647
  """
491
-
492
- direction = np.array(direction)
648
+ direction_copy = np.array(direction)
493
649
  if family:
494
650
  for submob in self.get_family():
495
- submob.sheen_direction = direction
651
+ submob.sheen_direction = direction_copy.copy()
496
652
  else:
497
- self.sheen_direction = direction
653
+ self.sheen_direction = direction_copy
498
654
  return self
499
655
 
500
- def rotate_sheen_direction(self, angle: float, axis: np.ndarray = OUT, family=True):
656
+ def rotate_sheen_direction(
657
+ self, angle: float, axis: Vector3DLike = OUT, family: bool = True
658
+ ) -> Self:
501
659
  """Rotates the direction of the applied sheen.
502
660
 
503
661
  Parameters
@@ -528,7 +686,9 @@ class VMobject(Mobject):
528
686
  self.sheen_direction = rotate_vector(self.sheen_direction, angle, axis)
529
687
  return self
530
688
 
531
- def set_sheen(self, factor: float, direction: np.ndarray = None, family=True):
689
+ def set_sheen(
690
+ self, factor: float, direction: Vector3DLike | None = None, family: bool = True
691
+ ) -> Self:
532
692
  """Applies a color gradient from a direction.
533
693
 
534
694
  Parameters
@@ -550,11 +710,10 @@ class VMobject(Mobject):
550
710
  circle = Circle(fill_opacity=1).set_sheen(-0.3, DR)
551
711
  self.add(circle)
552
712
  """
553
-
554
713
  if family:
555
714
  for submob in self.submobjects:
556
715
  submob.set_sheen(factor, direction, family)
557
- self.sheen_factor = factor
716
+ self.sheen_factor: float = factor
558
717
  if direction is not None:
559
718
  # family set to false because recursion will
560
719
  # already be handled above
@@ -565,13 +724,13 @@ class VMobject(Mobject):
565
724
  self.set_fill(self.get_fill_color(), family=family)
566
725
  return self
567
726
 
568
- def get_sheen_direction(self):
727
+ def get_sheen_direction(self) -> Vector3D:
569
728
  return np.array(self.sheen_direction)
570
729
 
571
- def get_sheen_factor(self):
730
+ def get_sheen_factor(self) -> float:
572
731
  return self.sheen_factor
573
732
 
574
- def get_gradient_start_and_end_points(self):
733
+ def get_gradient_start_and_end_points(self) -> tuple[Point3D, Point3D]:
575
734
  if self.shade_in_3d:
576
735
  return get_3d_vmob_gradient_start_and_end_points(self)
577
736
  else:
@@ -583,8 +742,8 @@ class VMobject(Mobject):
583
742
  offset = np.dot(bases, direction)
584
743
  return (c - offset, c + offset)
585
744
 
586
- def color_using_background_image(self, background_image: Image | str):
587
- self.background_image = background_image
745
+ def color_using_background_image(self, background_image: Image | str) -> Self:
746
+ self.background_image: Image | str = background_image
588
747
  self.set_color(WHITE)
589
748
  for submob in self.submobjects:
590
749
  submob.color_using_background_image(background_image)
@@ -593,26 +752,28 @@ class VMobject(Mobject):
593
752
  def get_background_image(self) -> Image | str:
594
753
  return self.background_image
595
754
 
596
- def match_background_image(self, vmobject):
755
+ def match_background_image(self, vmobject: VMobject) -> Self:
597
756
  self.color_using_background_image(vmobject.get_background_image())
598
757
  return self
599
758
 
600
- def set_shade_in_3d(self, value=True, z_index_as_group=False):
759
+ def set_shade_in_3d(
760
+ self, value: bool = True, z_index_as_group: bool = False
761
+ ) -> Self:
601
762
  for submob in self.get_family():
602
763
  submob.shade_in_3d = value
603
764
  if z_index_as_group:
604
765
  submob.z_index_group = self
605
766
  return self
606
767
 
607
- def set_points(self, points):
608
- self.points = np.array(points)
768
+ def set_points(self, points: Point3DLike_Array) -> Self:
769
+ self.points: Point3D_Array = np.array(points)
609
770
  return self
610
771
 
611
772
  def resize_points(
612
773
  self,
613
774
  new_length: int,
614
- resize_func: Callable[[np.ndarray, int], np.ndarray] = resize_array,
615
- ):
775
+ resize_func: Callable[[Point3D_Array, int], Point3D_Array] = resize_array,
776
+ ) -> Self:
616
777
  """Resize the array of anchor points and handles to have
617
778
  the specified size.
618
779
 
@@ -631,11 +792,11 @@ class VMobject(Mobject):
631
792
 
632
793
  def set_anchors_and_handles(
633
794
  self,
634
- anchors1: Sequence[float],
635
- handles1: Sequence[float],
636
- handles2: Sequence[float],
637
- anchors2: Sequence[float],
638
- ):
795
+ anchors1: Point3DLike_Array,
796
+ handles1: Point3DLike_Array,
797
+ handles2: Point3DLike_Array,
798
+ anchors2: Point3DLike_Array,
799
+ ) -> Self:
639
800
  """Given two sets of anchors and handles, process them to set them as anchors
640
801
  and handles of the VMobject.
641
802
 
@@ -652,7 +813,7 @@ class VMobject(Mobject):
652
813
  assert len(anchors1) == len(handles1) == len(handles2) == len(anchors2)
653
814
  nppcc = self.n_points_per_cubic_curve # 4
654
815
  total_len = nppcc * len(anchors1)
655
- self.points = np.zeros((total_len, self.dim))
816
+ self.points = np.empty((total_len, self.dim))
656
817
  # the following will, from the four sets, dispatch them in points such that
657
818
  # self.points = [
658
819
  # anchors1[0], handles1[0], handles2[0], anchors1[0], anchors1[1],
@@ -663,45 +824,84 @@ class VMobject(Mobject):
663
824
  self.points[index::nppcc] = array
664
825
  return self
665
826
 
666
- def clear_points(self):
827
+ def clear_points(self) -> None:
828
+ # TODO: shouldn't this return self instead of None?
667
829
  self.points = np.zeros((0, self.dim))
668
830
 
669
- def append_points(self, new_points):
831
+ def append_points(self, new_points: Point3DLike_Array) -> Self:
832
+ """Append the given ``new_points`` to the end of
833
+ :attr:`VMobject.points`.
834
+
835
+ Parameters
836
+ ----------
837
+ new_points
838
+ An array of 3D points to append.
839
+
840
+ Returns
841
+ -------
842
+ :class:`VMobject`
843
+ The VMobject itself, after appending ``new_points``.
844
+ """
670
845
  # TODO, check that number new points is a multiple of 4?
671
846
  # or else that if len(self.points) % 4 == 1, then
672
847
  # len(new_points) % 4 == 3?
673
- self.points = np.append(self.points, new_points, axis=0)
848
+ n = len(self.points)
849
+ points = np.empty((n + len(new_points), self.dim))
850
+ points[:n] = self.points
851
+ points[n:] = new_points
852
+ self.points = points
674
853
  return self
675
854
 
676
- def start_new_path(self, point):
677
- if len(self.points) % 4 != 0:
855
+ def start_new_path(self, point: Point3DLike) -> Self:
856
+ """Append a ``point`` to the :attr:`VMobject.points`, which will be the
857
+ beginning of a new Bézier curve in the path given by the points. If
858
+ there's an unfinished curve at the end of :attr:`VMobject.points`,
859
+ complete it by appending the last Bézier curve's start anchor as many
860
+ times as needed.
861
+
862
+ Parameters
863
+ ----------
864
+ point
865
+ A 3D point to append to :attr:`VMobject.points`.
866
+
867
+ Returns
868
+ -------
869
+ :class:`VMobject`
870
+ The VMobject itself, after appending ``point`` and starting a new
871
+ curve.
872
+ """
873
+ n_points = len(self.points)
874
+ nppc = self.n_points_per_curve
875
+ if n_points % nppc != 0:
678
876
  # close the open path by appending the last
679
877
  # start anchor sufficiently often
680
878
  last_anchor = self.get_start_anchors()[-1]
681
- for _ in range(4 - (len(self.points) % 4)):
682
- self.append_points([last_anchor])
683
- self.append_points([point])
879
+ closure = [last_anchor] * (nppc - (n_points % nppc))
880
+ self.append_points(closure + [point])
881
+ else:
882
+ self.append_points([point])
684
883
  return self
685
884
 
686
885
  def add_cubic_bezier_curve(
687
886
  self,
688
- anchor1: np.ndarray,
689
- handle1: np.ndarray,
690
- handle2: np.ndarray,
691
- anchor2,
887
+ anchor1: Point3DLike,
888
+ handle1: Point3DLike,
889
+ handle2: Point3DLike,
890
+ anchor2: Point3DLike,
692
891
  ) -> None:
693
892
  # TODO, check the len(self.points) % 4 == 0?
694
893
  self.append_points([anchor1, handle1, handle2, anchor2])
695
894
 
696
- def add_cubic_bezier_curves(self, curves):
895
+ # what type is curves?
896
+ def add_cubic_bezier_curves(self, curves) -> None:
697
897
  self.append_points(curves.flatten())
698
898
 
699
899
  def add_cubic_bezier_curve_to(
700
900
  self,
701
- handle1: np.ndarray,
702
- handle2: np.ndarray,
703
- anchor: np.ndarray,
704
- ):
901
+ handle1: Point3DLike,
902
+ handle2: Point3DLike,
903
+ anchor: Point3DLike,
904
+ ) -> Self:
705
905
  """Add cubic bezier curve to the path.
706
906
 
707
907
  NOTE : the first anchor is not a parameter as by default the end of the last sub-path!
@@ -730,9 +930,9 @@ class VMobject(Mobject):
730
930
 
731
931
  def add_quadratic_bezier_curve_to(
732
932
  self,
733
- handle: np.ndarray,
734
- anchor: np.ndarray,
735
- ):
933
+ handle: Point3DLike,
934
+ anchor: Point3DLike,
935
+ ) -> Self:
736
936
  """Add Quadratic bezier curve to the path.
737
937
 
738
938
  Returns
@@ -754,30 +954,29 @@ class VMobject(Mobject):
754
954
  )
755
955
  return self
756
956
 
757
- def add_line_to(self, point: np.ndarray):
957
+ def add_line_to(self, point: Point3DLike) -> Self:
758
958
  """Add a straight line from the last point of VMobject to the given point.
759
959
 
760
960
  Parameters
761
961
  ----------
762
962
 
763
963
  point
764
- end of the straight line.
964
+ The end of the straight line.
765
965
 
766
966
  Returns
767
967
  -------
768
968
  :class:`VMobject`
769
969
  ``self``
770
970
  """
771
- nppcc = self.n_points_per_cubic_curve
772
971
  self.add_cubic_bezier_curve_to(
773
972
  *(
774
- interpolate(self.get_last_point(), point, a)
775
- for a in np.linspace(0, 1, nppcc)[1:]
973
+ interpolate(self.get_last_point(), point, t)
974
+ for t in self._bezier_t_values[1:]
776
975
  )
777
976
  )
778
977
  return self
779
978
 
780
- def add_smooth_curve_to(self, *points: np.array):
979
+ def add_smooth_curve_to(self, *points: Point3DLike) -> Self:
781
980
  """Creates a smooth curve from given points and add it to the VMobject. If two points are passed in, the first is interpreted
782
981
  as a handle, the second as an anchor.
783
982
 
@@ -820,32 +1019,73 @@ class VMobject(Mobject):
820
1019
  self.append_points([last_a2, handle1, handle2, new_anchor])
821
1020
  return self
822
1021
 
823
- def has_new_path_started(self):
1022
+ def has_new_path_started(self) -> bool:
824
1023
  nppcc = self.n_points_per_cubic_curve # 4
825
1024
  # A new path starting is defined by a control point which is not part of a bezier subcurve.
826
1025
  return len(self.points) % nppcc == 1
827
1026
 
828
- def get_last_point(self):
1027
+ def get_last_point(self) -> Point3D:
829
1028
  return self.points[-1]
830
1029
 
831
- def is_closed(self):
1030
+ def is_closed(self) -> bool:
832
1031
  # TODO use consider_points_equals_2d ?
833
1032
  return self.consider_points_equals(self.points[0], self.points[-1])
834
1033
 
835
- def close_path(self):
1034
+ def close_path(self) -> None:
836
1035
  if not self.is_closed():
837
1036
  self.add_line_to(self.get_subpaths()[-1][0])
838
1037
 
839
- def add_points_as_corners(self, points: np.ndarray) -> VMobject:
840
- for point in points:
841
- self.add_line_to(point)
842
- return points
1038
+ def add_points_as_corners(self, points: Point3DLike_Array) -> Self:
1039
+ """Append multiple straight lines at the end of
1040
+ :attr:`VMobject.points`, which connect the given ``points`` in order
1041
+ starting from the end of the current path. These ``points`` would be
1042
+ therefore the corners of the new polyline appended to the path.
1043
+
1044
+ Parameters
1045
+ ----------
1046
+ points
1047
+ An array of 3D points representing the corners of the polyline to
1048
+ append to :attr:`VMobject.points`.
843
1049
 
844
- def set_points_as_corners(self, points: Sequence[float]):
845
- """Given an array of points, set them as corner of the vmobject.
1050
+ Returns
1051
+ -------
1052
+ :class:`VMobject`
1053
+ The VMobject itself, after appending the straight lines to its
1054
+ path.
1055
+ """
1056
+ self.throw_error_if_no_points()
1057
+
1058
+ points = np.asarray(points).reshape(-1, self.dim)
1059
+ num_points = points.shape[0]
1060
+ if num_points == 0:
1061
+ return self
846
1062
 
847
- To achieve that, this algorithm sets handles aligned with the anchors such that the resultant bezier curve will be the segment
848
- between the two anchors.
1063
+ start_corners = np.empty((num_points, self.dim))
1064
+ start_corners[0] = self.points[-1]
1065
+ start_corners[1:] = points[:-1]
1066
+ end_corners = points
1067
+
1068
+ if self.has_new_path_started():
1069
+ # Remove the last point from the new path
1070
+ self.points = self.points[:-1]
1071
+
1072
+ nppcc = self.n_points_per_cubic_curve
1073
+ new_points = np.empty((nppcc * start_corners.shape[0], self.dim))
1074
+ new_points[::nppcc] = start_corners
1075
+ new_points[nppcc - 1 :: nppcc] = end_corners
1076
+ for i, t in enumerate(self._bezier_t_values):
1077
+ new_points[i::nppcc] = interpolate(start_corners, end_corners, t)
1078
+
1079
+ self.append_points(new_points)
1080
+ return self
1081
+
1082
+ def set_points_as_corners(self, points: Point3DLike_Array) -> Self:
1083
+ """Given an array of points, set them as corners of the
1084
+ :class:`VMobject`.
1085
+
1086
+ To achieve that, this algorithm sets handles aligned with the anchors
1087
+ such that the resultant Bézier curve will be the segment between the
1088
+ two anchors.
849
1089
 
850
1090
  Parameters
851
1091
  ----------
@@ -855,23 +1095,43 @@ class VMobject(Mobject):
855
1095
  Returns
856
1096
  -------
857
1097
  :class:`VMobject`
858
- ``self``
1098
+ The VMobject itself, after setting the new points as corners.
1099
+
1100
+
1101
+ Examples
1102
+ --------
1103
+ .. manim:: PointsAsCornersExample
1104
+ :save_last_frame:
1105
+
1106
+ class PointsAsCornersExample(Scene):
1107
+ def construct(self):
1108
+ corners = (
1109
+ # create square
1110
+ UR, UL,
1111
+ DL, DR,
1112
+ UR,
1113
+ # create crosses
1114
+ DL, UL,
1115
+ DR
1116
+ )
1117
+ vmob = VMobject(stroke_color=RED)
1118
+ vmob.set_points_as_corners(corners).scale(2)
1119
+ self.add(vmob)
859
1120
  """
860
- nppcc = self.n_points_per_cubic_curve
861
1121
  points = np.array(points)
862
1122
  # This will set the handles aligned with the anchors.
863
1123
  # Id est, a bezier curve will be the segment from the two anchors such that the handles belongs to this segment.
864
1124
  self.set_anchors_and_handles(
865
- *(interpolate(points[:-1], points[1:], a) for a in np.linspace(0, 1, nppcc))
1125
+ *(interpolate(points[:-1], points[1:], t) for t in self._bezier_t_values)
866
1126
  )
867
1127
  return self
868
1128
 
869
- def set_points_smoothly(self, points):
1129
+ def set_points_smoothly(self, points: Point3DLike_Array) -> Self:
870
1130
  self.set_points_as_corners(points)
871
1131
  self.make_smooth()
872
1132
  return self
873
1133
 
874
- def change_anchor_mode(self, mode: str):
1134
+ def change_anchor_mode(self, mode: Literal["jagged", "smooth"]) -> Self:
875
1135
  """Changes the anchor mode of the bezier curves. This will modify the handles.
876
1136
 
877
1137
  There can be only two modes, "jagged", and "smooth".
@@ -881,7 +1141,7 @@ class VMobject(Mobject):
881
1141
  :class:`VMobject`
882
1142
  ``self``
883
1143
  """
884
- assert mode in ["jagged", "smooth"]
1144
+ assert mode in ["jagged", "smooth"], 'mode must be either "jagged" or "smooth"'
885
1145
  nppcc = self.n_points_per_cubic_curve
886
1146
  for submob in self.family_members_with_points():
887
1147
  subpaths = submob.get_subpaths()
@@ -892,8 +1152,8 @@ class VMobject(Mobject):
892
1152
  # The append is needed as the last element is not reached when slicing with numpy.
893
1153
  anchors = np.append(subpath[::nppcc], subpath[-1:], 0)
894
1154
  if mode == "smooth":
895
- h1, h2 = get_smooth_handle_points(anchors)
896
- elif mode == "jagged":
1155
+ h1, h2 = get_smooth_cubic_bezier_handle_points(anchors)
1156
+ else: # mode == "jagged"
897
1157
  # The following will make the handles aligned with the anchors, thus making the bezier curve a segment
898
1158
  a1 = anchors[:-1]
899
1159
  a2 = anchors[1:]
@@ -905,27 +1165,31 @@ class VMobject(Mobject):
905
1165
  submob.append_points(new_subpath)
906
1166
  return self
907
1167
 
908
- def make_smooth(self):
1168
+ def make_smooth(self) -> Self:
909
1169
  return self.change_anchor_mode("smooth")
910
1170
 
911
- def make_jagged(self):
1171
+ def make_jagged(self) -> Self:
912
1172
  return self.change_anchor_mode("jagged")
913
1173
 
914
- def add_subpath(self, points: np.ndarray):
1174
+ def add_subpath(self, points: CubicBezierPathLike) -> Self:
915
1175
  assert len(points) % 4 == 0
916
- self.points = np.append(self.points, points, axis=0)
1176
+ self.append_points(points)
917
1177
  return self
918
1178
 
919
- def append_vectorized_mobject(self, vectorized_mobject):
920
- new_points = list(vectorized_mobject.points)
921
-
1179
+ def append_vectorized_mobject(self, vectorized_mobject: VMobject) -> None:
922
1180
  if self.has_new_path_started():
923
1181
  # Remove last point, which is starting
924
1182
  # a new path
925
1183
  self.points = self.points[:-1]
926
- self.append_points(new_points)
1184
+ self.append_points(vectorized_mobject.points)
927
1185
 
928
- def apply_function(self, function):
1186
+ def apply_function(
1187
+ self,
1188
+ function: MappingFunction,
1189
+ *,
1190
+ about_point: Point3DLike | None = None,
1191
+ about_edge: Vector3DLike | None = None,
1192
+ ) -> Self:
929
1193
  factor = self.pre_function_handle_to_anchor_scale_factor
930
1194
  self.scale_handle_to_anchor_distances(factor)
931
1195
  super().apply_function(function)
@@ -937,15 +1201,16 @@ class VMobject(Mobject):
937
1201
  def rotate(
938
1202
  self,
939
1203
  angle: float,
940
- axis: np.ndarray = OUT,
941
- about_point: Sequence[float] | None = None,
942
- **kwargs,
943
- ):
1204
+ axis: Vector3DLike = OUT,
1205
+ *,
1206
+ about_point: Point3DLike | None = None,
1207
+ about_edge: Vector3DLike | None = None,
1208
+ ) -> Self:
944
1209
  self.rotate_sheen_direction(angle, axis)
945
- super().rotate(angle, axis, about_point, **kwargs)
1210
+ super().rotate(angle, axis, about_point=about_point, about_edge=about_edge)
946
1211
  return self
947
1212
 
948
- def scale_handle_to_anchor_distances(self, factor: float):
1213
+ def scale_handle_to_anchor_distances(self, factor: float) -> Self:
949
1214
  """If the distance between a given handle point H and its associated
950
1215
  anchor point A is d, then it changes H to be a distances factor*d
951
1216
  away from A, but so that the line from A to H doesn't change.
@@ -977,10 +1242,10 @@ class VMobject(Mobject):
977
1242
  return self
978
1243
 
979
1244
  #
980
- def consider_points_equals(self, p0, p1):
1245
+ def consider_points_equals(self, p0: Point3DLike, p1: Point3DLike) -> bool:
981
1246
  return np.allclose(p0, p1, atol=self.tolerance_for_point_equality)
982
1247
 
983
- def consider_points_equals_2d(self, p0: np.ndarray, p1: np.ndarray) -> bool:
1248
+ def consider_points_equals_2d(self, p0: Point2DLike, p1: Point2DLike) -> bool:
984
1249
  """Determine if two points are close enough to be considered equal.
985
1250
 
986
1251
  This uses the algorithm from np.isclose(), but expanded here for the
@@ -1001,15 +1266,17 @@ class VMobject(Mobject):
1001
1266
  atol = self.tolerance_for_point_equality
1002
1267
  if abs(p0[0] - p1[0]) > atol + rtol * abs(p1[0]):
1003
1268
  return False
1004
- if abs(p0[1] - p1[1]) > atol + rtol * abs(p1[1]):
1005
- return False
1006
- return True
1269
+ return abs(p0[1] - p1[1]) <= atol + rtol * abs(p1[1])
1007
1270
 
1008
1271
  # Information about line
1009
- def get_cubic_bezier_tuples_from_points(self, points):
1010
- return np.array(list(self.gen_cubic_bezier_tuples_from_points(points)))
1011
-
1012
- def gen_cubic_bezier_tuples_from_points(self, points: np.ndarray) -> tuple:
1272
+ def get_cubic_bezier_tuples_from_points(
1273
+ self, points: CubicBezierPathLike
1274
+ ) -> CubicBezierPoints_Array:
1275
+ return np.array(self.gen_cubic_bezier_tuples_from_points(points))
1276
+
1277
+ def gen_cubic_bezier_tuples_from_points(
1278
+ self, points: CubicBezierPathLike
1279
+ ) -> tuple[CubicBezierPointsLike, ...]:
1013
1280
  """Returns the bezier tuples from an array of points.
1014
1281
 
1015
1282
  self.points is a list of the anchors and handles of the bezier curves of the mobject (ie [anchor1, handle1, handle2, anchor2, anchor3 ..])
@@ -1024,23 +1291,23 @@ class VMobject(Mobject):
1024
1291
 
1025
1292
  Returns
1026
1293
  -------
1027
- typing.Tuple
1294
+ tuple
1028
1295
  Bezier control points.
1029
1296
  """
1030
1297
  nppcc = self.n_points_per_cubic_curve
1031
1298
  remainder = len(points) % nppcc
1032
1299
  points = points[: len(points) - remainder]
1033
1300
  # Basically take every nppcc element.
1034
- return (points[i : i + nppcc] for i in range(0, len(points), nppcc))
1301
+ return tuple(points[i : i + nppcc] for i in range(0, len(points), nppcc))
1035
1302
 
1036
- def get_cubic_bezier_tuples(self):
1303
+ def get_cubic_bezier_tuples(self) -> CubicBezierPoints_Array:
1037
1304
  return self.get_cubic_bezier_tuples_from_points(self.points)
1038
1305
 
1039
1306
  def _gen_subpaths_from_points(
1040
1307
  self,
1041
- points: np.ndarray,
1042
- filter_func: typing.Callable[[int], bool],
1043
- ) -> tuple:
1308
+ points: CubicBezierPath,
1309
+ filter_func: Callable[[int], bool],
1310
+ ) -> Iterable[CubicSpline]:
1044
1311
  """Given an array of points defining the bezier curves of the vmobject, return subpaths formed by these points.
1045
1312
  Here, Two bezier curves form a path if at least two of their anchors are evaluated True by the relation defined by filter_func.
1046
1313
 
@@ -1058,7 +1325,7 @@ class VMobject(Mobject):
1058
1325
 
1059
1326
  Returns
1060
1327
  -------
1061
- typing.Tuple
1328
+ Iterable[CubicSpline]
1062
1329
  subpaths formed by the points.
1063
1330
  """
1064
1331
  nppcc = self.n_points_per_cubic_curve
@@ -1070,7 +1337,7 @@ class VMobject(Mobject):
1070
1337
  if (i2 - i1) >= nppcc
1071
1338
  )
1072
1339
 
1073
- def get_subpaths_from_points(self, points):
1340
+ def get_subpaths_from_points(self, points: CubicBezierPath) -> list[CubicSpline]:
1074
1341
  return list(
1075
1342
  self._gen_subpaths_from_points(
1076
1343
  points,
@@ -1078,25 +1345,27 @@ class VMobject(Mobject):
1078
1345
  ),
1079
1346
  )
1080
1347
 
1081
- def gen_subpaths_from_points_2d(self, points):
1348
+ def gen_subpaths_from_points_2d(
1349
+ self, points: CubicBezierPath
1350
+ ) -> Iterable[CubicSpline]:
1082
1351
  return self._gen_subpaths_from_points(
1083
1352
  points,
1084
1353
  lambda n: not self.consider_points_equals_2d(points[n - 1], points[n]),
1085
1354
  )
1086
1355
 
1087
- def get_subpaths(self) -> tuple:
1356
+ def get_subpaths(self) -> list[CubicSpline]:
1088
1357
  """Returns subpaths formed by the curves of the VMobject.
1089
1358
 
1090
1359
  Subpaths are ranges of curves with each pair of consecutive curves having their end/start points coincident.
1091
1360
 
1092
1361
  Returns
1093
1362
  -------
1094
- typing.Tuple
1363
+ list[CubicSpline]
1095
1364
  subpaths.
1096
1365
  """
1097
1366
  return self.get_subpaths_from_points(self.points)
1098
1367
 
1099
- def get_nth_curve_points(self, n: int) -> np.ndarray:
1368
+ def get_nth_curve_points(self, n: int) -> CubicBezierPoints:
1100
1369
  """Returns the points defining the nth curve of the vmobject.
1101
1370
 
1102
1371
  Parameters
@@ -1106,14 +1375,14 @@ class VMobject(Mobject):
1106
1375
 
1107
1376
  Returns
1108
1377
  -------
1109
- np.ndarray
1110
- points defininf the nth bezier curve (anchors, handles)
1378
+ CubicBezierPoints
1379
+ points defining the nth bezier curve (anchors, handles)
1111
1380
  """
1112
1381
  assert n < self.get_num_curves()
1113
1382
  nppcc = self.n_points_per_cubic_curve
1114
1383
  return self.points[nppcc * n : nppcc * (n + 1)]
1115
1384
 
1116
- def get_nth_curve_function(self, n: int) -> typing.Callable[[float], np.ndarray]:
1385
+ def get_nth_curve_function(self, n: int) -> Callable[[float], Point3D]:
1117
1386
  """Returns the expression of the nth curve.
1118
1387
 
1119
1388
  Parameters
@@ -1123,7 +1392,7 @@ class VMobject(Mobject):
1123
1392
 
1124
1393
  Returns
1125
1394
  -------
1126
- typing.Callable[float]
1395
+ Callable[float, Point3D]
1127
1396
  expression of the nth bezier curve.
1128
1397
  """
1129
1398
  return bezier(self.get_nth_curve_points(n))
@@ -1132,7 +1401,7 @@ class VMobject(Mobject):
1132
1401
  self,
1133
1402
  n: int,
1134
1403
  sample_points: int | None = None,
1135
- ) -> np.ndarray:
1404
+ ) -> npt.NDArray[ManimFloat]:
1136
1405
  """Returns the array of short line lengths used for length approximation.
1137
1406
 
1138
1407
  Parameters
@@ -1144,7 +1413,6 @@ class VMobject(Mobject):
1144
1413
 
1145
1414
  Returns
1146
1415
  -------
1147
- np.ndarray
1148
1416
  The short length-pieces of the nth curve.
1149
1417
  """
1150
1418
  if sample_points is None:
@@ -1176,7 +1444,6 @@ class VMobject(Mobject):
1176
1444
  length : :class:`float`
1177
1445
  The length of the nth curve.
1178
1446
  """
1179
-
1180
1447
  _, length = self.get_nth_curve_function_with_length(n, sample_points)
1181
1448
 
1182
1449
  return length
@@ -1185,7 +1452,7 @@ class VMobject(Mobject):
1185
1452
  self,
1186
1453
  n: int,
1187
1454
  sample_points: int | None = None,
1188
- ) -> tuple[typing.Callable[[float], np.ndarray], float]:
1455
+ ) -> tuple[Callable[[float], Point3D], float]:
1189
1456
  """Returns the expression of the nth curve along with its (approximate) length.
1190
1457
 
1191
1458
  Parameters
@@ -1197,12 +1464,11 @@ class VMobject(Mobject):
1197
1464
 
1198
1465
  Returns
1199
1466
  -------
1200
- curve : typing.Callable[[float], np.ndarray]
1467
+ curve : Callable[[float], Point3D]
1201
1468
  The function for the nth curve.
1202
1469
  length : :class:`float`
1203
1470
  The length of the nth curve.
1204
1471
  """
1205
-
1206
1472
  curve = self.get_nth_curve_function(n)
1207
1473
  norms = self.get_nth_curve_length_pieces(n, sample_points=sample_points)
1208
1474
  length = np.sum(norms)
@@ -1215,22 +1481,21 @@ class VMobject(Mobject):
1215
1481
  Returns
1216
1482
  -------
1217
1483
  int
1218
- number of curves. of the vmobject.
1484
+ number of curves of the vmobject.
1219
1485
  """
1220
1486
  nppcc = self.n_points_per_cubic_curve
1221
1487
  return len(self.points) // nppcc
1222
1488
 
1223
1489
  def get_curve_functions(
1224
1490
  self,
1225
- ) -> typing.Iterable[typing.Callable[[float], np.ndarray]]:
1491
+ ) -> Iterable[Callable[[float], Point3D]]:
1226
1492
  """Gets the functions for the curves of the mobject.
1227
1493
 
1228
1494
  Returns
1229
1495
  -------
1230
- typing.Iterable[typing.Callable[[float], np.ndarray]]
1496
+ Iterable[Callable[[float], Point3D]]
1231
1497
  The functions for the curves.
1232
1498
  """
1233
-
1234
1499
  num_curves = self.get_num_curves()
1235
1500
 
1236
1501
  for n in range(num_curves):
@@ -1238,7 +1503,7 @@ class VMobject(Mobject):
1238
1503
 
1239
1504
  def get_curve_functions_with_lengths(
1240
1505
  self, **kwargs
1241
- ) -> typing.Iterable[tuple[typing.Callable[[float], np.ndarray], float]]:
1506
+ ) -> Iterable[tuple[Callable[[float], Point3D], float]]:
1242
1507
  """Gets the functions and lengths of the curves for the mobject.
1243
1508
 
1244
1509
  Parameters
@@ -1248,16 +1513,15 @@ class VMobject(Mobject):
1248
1513
 
1249
1514
  Returns
1250
1515
  -------
1251
- typing.Iterable[typing.Tuple[typing.Callable[[float], np.ndarray], float]]
1516
+ Iterable[tuple[Callable[[float], Point3D], float]]
1252
1517
  The functions and lengths of the curves.
1253
1518
  """
1254
-
1255
1519
  num_curves = self.get_num_curves()
1256
1520
 
1257
1521
  for n in range(num_curves):
1258
1522
  yield self.get_nth_curve_function_with_length(n, **kwargs)
1259
1523
 
1260
- def point_from_proportion(self, alpha: float) -> np.ndarray:
1524
+ def point_from_proportion(self, alpha: float) -> Point3D:
1261
1525
  """Gets the point at a proportion along the path of the :class:`VMobject`.
1262
1526
 
1263
1527
  Parameters
@@ -1276,8 +1540,23 @@ class VMobject(Mobject):
1276
1540
  If ``alpha`` is not between 0 and 1.
1277
1541
  :exc:`Exception`
1278
1542
  If the :class:`VMobject` has no points.
1279
- """
1280
1543
 
1544
+ Example
1545
+ -------
1546
+ .. manim:: PointFromProportion
1547
+ :save_last_frame:
1548
+
1549
+ class PointFromProportion(Scene):
1550
+ def construct(self):
1551
+ line = Line(2*DL, 2*UR)
1552
+ self.add(line)
1553
+ colors = (RED, BLUE, YELLOW)
1554
+ proportions = (1/4, 1/2, 3/4)
1555
+ for color, proportion in zip(colors, proportions):
1556
+ self.add(Dot(color=color).move_to(
1557
+ line.point_from_proportion(proportion)
1558
+ ))
1559
+ """
1281
1560
  if alpha < 0 or alpha > 1:
1282
1561
  raise ValueError(f"Alpha {alpha} not between 0 and 1.")
1283
1562
 
@@ -1300,10 +1579,13 @@ class VMobject(Mobject):
1300
1579
  return curve(residue)
1301
1580
 
1302
1581
  current_length += length
1582
+ raise Exception(
1583
+ "Not sure how you reached here, please file a bug report at https://github.com/ManimCommunity/manim/issues/new/choose"
1584
+ )
1303
1585
 
1304
1586
  def proportion_from_point(
1305
1587
  self,
1306
- point: typing.Iterable[float | int],
1588
+ point: Point3DLike,
1307
1589
  ) -> float:
1308
1590
  """Returns the proportion along the path of the :class:`VMobject`
1309
1591
  a particular given point is at.
@@ -1357,7 +1639,7 @@ class VMobject(Mobject):
1357
1639
 
1358
1640
  return alpha
1359
1641
 
1360
- def get_anchors_and_handles(self) -> typing.Iterable[np.ndarray]:
1642
+ def get_anchors_and_handles(self) -> list[Point3D_Array]:
1361
1643
  """Returns anchors1, handles1, handles2, anchors2,
1362
1644
  where (anchors1[i], handles1[i], handles2[i], anchors2[i])
1363
1645
  will be four points defining a cubic bezier curve
@@ -1365,50 +1647,53 @@ class VMobject(Mobject):
1365
1647
 
1366
1648
  Returns
1367
1649
  -------
1368
- typing.Iterable[np.ndarray]
1650
+ `list[Point3D_Array]`
1369
1651
  Iterable of the anchors and handles.
1370
1652
  """
1371
1653
  nppcc = self.n_points_per_cubic_curve
1372
1654
  return [self.points[i::nppcc] for i in range(nppcc)]
1373
1655
 
1374
- def get_start_anchors(self) -> np.ndarray:
1656
+ def get_start_anchors(self) -> Point3D_Array:
1375
1657
  """Returns the start anchors of the bezier curves.
1376
1658
 
1377
1659
  Returns
1378
1660
  -------
1379
- np.ndarray
1661
+ Point3D_Array
1380
1662
  Starting anchors
1381
1663
  """
1382
- return self.points[0 :: self.n_points_per_cubic_curve]
1664
+ return self.points[:: self.n_points_per_cubic_curve]
1383
1665
 
1384
- def get_end_anchors(self) -> np.ndarray:
1666
+ def get_end_anchors(self) -> Point3D_Array:
1385
1667
  """Return the end anchors of the bezier curves.
1386
1668
 
1387
1669
  Returns
1388
1670
  -------
1389
- np.ndarray
1671
+ Point3D_Array
1390
1672
  Starting anchors
1391
1673
  """
1392
1674
  nppcc = self.n_points_per_cubic_curve
1393
1675
  return self.points[nppcc - 1 :: nppcc]
1394
1676
 
1395
- def get_anchors(self) -> np.ndarray:
1677
+ def get_anchors(self) -> list[Point3D]:
1396
1678
  """Returns the anchors of the curves forming the VMobject.
1397
1679
 
1398
1680
  Returns
1399
1681
  -------
1400
- np.ndarray
1682
+ Point3D_Array
1401
1683
  The anchors.
1402
1684
  """
1403
1685
  if self.points.shape[0] == 1:
1404
1686
  return self.points
1405
- return np.array(
1406
- list(it.chain(*zip(self.get_start_anchors(), self.get_end_anchors()))),
1407
- )
1408
1687
 
1409
- def get_points_defining_boundary(self):
1688
+ s = self.get_start_anchors()
1689
+ e = self.get_end_anchors()
1690
+ return list(it.chain.from_iterable(zip(s, e)))
1691
+
1692
+ def get_points_defining_boundary(self) -> Point3D_Array:
1410
1693
  # Probably returns all anchors, but this is weird regarding the name of the method.
1411
- return np.array(list(it.chain(*(sm.get_anchors() for sm in self.get_family()))))
1694
+ return np.array(
1695
+ tuple(it.chain(*(sm.get_anchors() for sm in self.get_family())))
1696
+ )
1412
1697
 
1413
1698
  def get_arc_length(self, sample_points_per_curve: int | None = None) -> float:
1414
1699
  """Return the approximated length of the whole curve.
@@ -1423,7 +1708,6 @@ class VMobject(Mobject):
1423
1708
  float
1424
1709
  The length of the :class:`VMobject`.
1425
1710
  """
1426
-
1427
1711
  return sum(
1428
1712
  length
1429
1713
  for _, length in self.get_curve_functions_with_lengths(
@@ -1432,7 +1716,7 @@ class VMobject(Mobject):
1432
1716
  )
1433
1717
 
1434
1718
  # Alignment
1435
- def align_points(self, vmobject: VMobject):
1719
+ def align_points(self, vmobject: VMobject) -> Self:
1436
1720
  """Adds points to self and vmobject so that they both have the same number of subpaths, with
1437
1721
  corresponding subpaths each containing the same number of points.
1438
1722
 
@@ -1448,6 +1732,10 @@ class VMobject(Mobject):
1448
1732
  -------
1449
1733
  :class:`VMobject`
1450
1734
  ``self``
1735
+
1736
+ See also
1737
+ --------
1738
+ :meth:`~.Mobject.interpolate`, :meth:`~.Mobject.align_data`
1451
1739
  """
1452
1740
  self.align_rgbas(vmobject)
1453
1741
  # TODO: This shortcut can be a bit over eager. What if they have the same length, but different subpath lengths?
@@ -1503,7 +1791,7 @@ class VMobject(Mobject):
1503
1791
  vmobject.set_points(new_path2)
1504
1792
  return self
1505
1793
 
1506
- def insert_n_curves(self, n: int):
1794
+ def insert_n_curves(self, n: int) -> Self:
1507
1795
  """Inserts n curves to the bezier curves of the vmobject.
1508
1796
 
1509
1797
  Parameters
@@ -1527,7 +1815,9 @@ class VMobject(Mobject):
1527
1815
  self.append_points([new_path_point])
1528
1816
  return self
1529
1817
 
1530
- def insert_n_curves_to_point_list(self, n: int, points: np.ndarray) -> np.ndarray:
1818
+ def insert_n_curves_to_point_list(
1819
+ self, n: int, points: BezierPathLike
1820
+ ) -> BezierPath:
1531
1821
  """Given an array of k points defining a bezier curves (anchors and handles), returns points defining exactly k + n bezier curves.
1532
1822
 
1533
1823
  Parameters
@@ -1539,50 +1829,19 @@ class VMobject(Mobject):
1539
1829
 
1540
1830
  Returns
1541
1831
  -------
1542
- np.ndarray
1543
1832
  Points generated.
1544
1833
  """
1545
-
1546
1834
  if len(points) == 1:
1547
1835
  nppcc = self.n_points_per_cubic_curve
1548
1836
  return np.repeat(points, nppcc * n, 0)
1549
- bezier_quads = self.get_cubic_bezier_tuples_from_points(points)
1550
- curr_num = len(bezier_quads)
1551
- target_num = curr_num + n
1552
- # This is an array with values ranging from 0
1553
- # up to curr_num, with repeats such that
1554
- # it's total length is target_num. For example,
1555
- # with curr_num = 10, target_num = 15, this would
1556
- # be [0, 0, 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 8, 9]
1557
- repeat_indices = (np.arange(target_num, dtype="i") * curr_num) // target_num
1558
-
1559
- # If the nth term of this list is k, it means
1560
- # that the nth curve of our path should be split
1561
- # into k pieces.
1562
- # In the above example our array had the following elements
1563
- # [0, 0, 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 8, 9]
1564
- # We have two 0s, one 1, two 2s and so on.
1565
- # The split factors array would hence be:
1566
- # [2, 1, 2, 1, 2, 1, 2, 1, 2, 1]
1567
- split_factors = np.zeros(curr_num, dtype="i")
1568
- for val in repeat_indices:
1569
- split_factors[val] += 1
1570
-
1571
- new_points = np.zeros((0, self.dim))
1572
- for quad, sf in zip(bezier_quads, split_factors):
1573
- # What was once a single cubic curve defined
1574
- # by "quad" will now be broken into sf
1575
- # smaller cubic curves
1576
- alphas = np.linspace(0, 1, sf + 1)
1577
- for a1, a2 in zip(alphas, alphas[1:]):
1578
- new_points = np.append(
1579
- new_points,
1580
- partial_bezier_points(quad, a1, a2),
1581
- axis=0,
1582
- )
1837
+ bezier_tuples = self.get_cubic_bezier_tuples_from_points(points)
1838
+ current_number_of_curves = len(bezier_tuples)
1839
+ new_number_of_curves = current_number_of_curves + n
1840
+ new_bezier_tuples = bezier_remap(bezier_tuples, new_number_of_curves)
1841
+ new_points = new_bezier_tuples.reshape(-1, 3)
1583
1842
  return new_points
1584
1843
 
1585
- def align_rgbas(self, vmobject):
1844
+ def align_rgbas(self, vmobject: VMobject) -> Self:
1586
1845
  attrs = ["fill_rgbas", "stroke_rgbas", "background_stroke_rgbas"]
1587
1846
  for attr in attrs:
1588
1847
  a1 = getattr(self, attr)
@@ -1595,14 +1854,16 @@ class VMobject(Mobject):
1595
1854
  setattr(self, attr, new_a1)
1596
1855
  return self
1597
1856
 
1598
- def get_point_mobject(self, center=None):
1857
+ def get_point_mobject(self, center: Point3DLike | None = None) -> VectorizedPoint:
1599
1858
  if center is None:
1600
1859
  center = self.get_center()
1601
1860
  point = VectorizedPoint(center)
1602
1861
  point.match_style(self)
1603
1862
  return point
1604
1863
 
1605
- def interpolate_color(self, mobject1, mobject2, alpha):
1864
+ def interpolate_color(
1865
+ self, mobject1: VMobject, mobject2: VMobject, alpha: float
1866
+ ) -> None:
1606
1867
  attrs = [
1607
1868
  "fill_rgbas",
1608
1869
  "stroke_rgbas",
@@ -1619,71 +1880,110 @@ class VMobject(Mobject):
1619
1880
  interpolate(getattr(mobject1, attr), getattr(mobject2, attr), alpha),
1620
1881
  )
1621
1882
  if alpha == 1.0:
1622
- setattr(self, attr, getattr(mobject2, attr))
1883
+ val = getattr(mobject2, attr)
1884
+ if isinstance(val, np.ndarray):
1885
+ val = val.copy()
1886
+ setattr(self, attr, val)
1623
1887
 
1624
1888
  def pointwise_become_partial(
1625
1889
  self,
1626
1890
  vmobject: VMobject,
1627
1891
  a: float,
1628
1892
  b: float,
1629
- ):
1630
- """Given two bounds a and b, transforms the points of the self vmobject into the points of the vmobject
1631
- passed as parameter with respect to the bounds. Points here stand for control points of the bezier curves (anchors and handles)
1893
+ ) -> Self:
1894
+ """Given a 2nd :class:`.VMobject` ``vmobject``, a lower bound ``a`` and
1895
+ an upper bound ``b``, modify this :class:`.VMobject`'s points to
1896
+ match the portion of the Bézier spline described by ``vmobject.points``
1897
+ with the parameter ``t`` between ``a`` and ``b``.
1632
1898
 
1633
1899
  Parameters
1634
1900
  ----------
1635
1901
  vmobject
1636
- The vmobject that will serve as a model.
1902
+ The :class:`.VMobject` that will serve as a model.
1637
1903
  a
1638
- upper-bound.
1904
+ The lower bound for ``t``.
1639
1905
  b
1640
- lower-bound
1906
+ The upper bound for ``t``
1641
1907
 
1642
1908
  Returns
1643
1909
  -------
1644
- :class:`VMobject`
1645
- ``self``
1910
+ :class:`.VMobject`
1911
+ The :class:`.VMobject` itself, after the transformation.
1912
+
1913
+ Raises
1914
+ ------
1915
+ TypeError
1916
+ If ``vmobject`` is not an instance of :class:`VMobject`.
1646
1917
  """
1647
- assert isinstance(vmobject, VMobject)
1918
+ if not isinstance(vmobject, VMobject):
1919
+ raise TypeError(
1920
+ f"Expected a VMobject, got value {vmobject} of type "
1921
+ f"{type(vmobject).__name__}."
1922
+ )
1648
1923
  # Partial curve includes three portions:
1649
- # - A middle section, which matches the curve exactly
1650
- # - A start, which is some ending portion of an inner cubic
1651
- # - An end, which is the starting portion of a later inner cubic
1924
+ # - A middle section, which matches the curve exactly.
1925
+ # - A start, which is some ending portion of an inner cubic.
1926
+ # - An end, which is the starting portion of a later inner cubic.
1652
1927
  if a <= 0 and b >= 1:
1653
1928
  self.set_points(vmobject.points)
1654
1929
  return self
1655
- bezier_quads = vmobject.get_cubic_bezier_tuples()
1656
- num_cubics = len(bezier_quads)
1930
+ num_curves = vmobject.get_num_curves()
1931
+ if num_curves == 0:
1932
+ return self
1657
1933
 
1658
- # The following two lines will compute which bezier curves of the given mobject need to be processed.
1659
- # The residue basically indicates de proportion of the selected bezier curve that have to be selected.
1660
- # Ex : if lower_index is 3, and lower_residue is 0.4, then the algorithm will append to the points 0.4 of the third bezier curve
1661
- lower_index, lower_residue = integer_interpolate(0, num_cubics, a)
1662
- upper_index, upper_residue = integer_interpolate(0, num_cubics, b)
1934
+ # The following two lines will compute which Bézier curves of the given Mobject must be processed.
1935
+ # The residue indicates the proportion of the selected Bézier curve which must be selected.
1936
+ #
1937
+ # Example: if num_curves is 10, a is 0.34 and b is 0.78, then:
1938
+ # - lower_index is 3 and lower_residue is 0.4, which means the algorithm will look at the 3rd Bézier
1939
+ # and select its part which ranges from t=0.4 to t=1.
1940
+ # - upper_index is 7 and upper_residue is 0.8, which means the algorithm will look at the 7th Bézier
1941
+ # and select its part which ranges from t=0 to t=0.8.
1942
+ lower_index, lower_residue = integer_interpolate(0, num_curves, a)
1943
+ upper_index, upper_residue = integer_interpolate(0, num_curves, b)
1944
+
1945
+ nppc = self.n_points_per_curve
1946
+
1947
+ # Copy vmobject.points if vmobject is self to prevent unintended in-place modification
1948
+ vmobject_points = (
1949
+ vmobject.points.copy() if self is vmobject else vmobject.points
1950
+ )
1663
1951
 
1664
- self.clear_points()
1665
- if num_cubics == 0:
1666
- return self
1952
+ # If both indices coincide, get a part of a single Bézier curve.
1667
1953
  if lower_index == upper_index:
1668
- self.append_points(
1669
- partial_bezier_points(
1670
- bezier_quads[lower_index],
1671
- lower_residue,
1672
- upper_residue,
1673
- ),
1954
+ # Look at the "lower_index"-th Bézier curve and select its part from
1955
+ # t=lower_residue to t=upper_residue.
1956
+ self.points = partial_bezier_points(
1957
+ vmobject_points[nppc * lower_index : nppc * (lower_index + 1)],
1958
+ lower_residue,
1959
+ upper_residue,
1674
1960
  )
1675
1961
  else:
1676
- self.append_points(
1677
- partial_bezier_points(bezier_quads[lower_index], lower_residue, 1),
1962
+ # Allocate space for (upper_index-lower_index+1) Bézier curves.
1963
+ self.points = np.empty((nppc * (upper_index - lower_index + 1), self.dim))
1964
+ # Look at the "lower_index"-th Bezier curve and select its part from
1965
+ # t=lower_residue to t=1. This is the first curve in self.points.
1966
+ self.points[:nppc] = partial_bezier_points(
1967
+ vmobject_points[nppc * lower_index : nppc * (lower_index + 1)],
1968
+ lower_residue,
1969
+ 1,
1678
1970
  )
1679
- for quad in bezier_quads[lower_index + 1 : upper_index]:
1680
- self.append_points(quad)
1681
- self.append_points(
1682
- partial_bezier_points(bezier_quads[upper_index], 0, upper_residue),
1971
+ # If there are more curves between the "lower_index"-th and the
1972
+ # "upper_index"-th Béziers, add them all to self.points.
1973
+ self.points[nppc:-nppc] = vmobject_points[
1974
+ nppc * (lower_index + 1) : nppc * upper_index
1975
+ ]
1976
+ # Look at the "upper_index"-th Bézier curve and select its part from
1977
+ # t=0 to t=upper_residue. This is the last curve in self.points.
1978
+ self.points[-nppc:] = partial_bezier_points(
1979
+ vmobject_points[nppc * upper_index : nppc * (upper_index + 1)],
1980
+ 0,
1981
+ upper_residue,
1683
1982
  )
1983
+
1684
1984
  return self
1685
1985
 
1686
- def get_subcurve(self, a: float, b: float) -> VMobject:
1986
+ def get_subcurve(self, a: float, b: float) -> Self:
1687
1987
  """Returns the subcurve of the VMobject between the interval [a, b].
1688
1988
  The curve is a VMobject itself.
1689
1989
 
@@ -1711,7 +2011,7 @@ class VMobject(Mobject):
1711
2011
  vmob.pointwise_become_partial(self, a, b)
1712
2012
  return vmob
1713
2013
 
1714
- def get_direction(self):
2014
+ def get_direction(self) -> Literal["CW", "CCW"]:
1715
2015
  """Uses :func:`~.space_ops.shoelace_direction` to calculate the direction.
1716
2016
  The direction of points determines in which direction the
1717
2017
  object is drawn, clockwise or counterclockwise.
@@ -1731,7 +2031,7 @@ class VMobject(Mobject):
1731
2031
  """
1732
2032
  return shoelace_direction(self.get_start_anchors())
1733
2033
 
1734
- def reverse_direction(self):
2034
+ def reverse_direction(self) -> Self:
1735
2035
  """Reverts the point direction by inverting the point order.
1736
2036
 
1737
2037
  Returns
@@ -1756,7 +2056,7 @@ class VMobject(Mobject):
1756
2056
  self.points = self.points[::-1]
1757
2057
  return self
1758
2058
 
1759
- def force_direction(self, target_direction: str):
2059
+ def force_direction(self, target_direction: Literal["CW", "CCW"]) -> Self:
1760
2060
  """Makes sure that points are either directed clockwise or
1761
2061
  counterclockwise.
1762
2062
 
@@ -1799,19 +2099,21 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
1799
2099
  >>> triangle, square = Triangle(), Square()
1800
2100
  >>> vg.add(triangle)
1801
2101
  VGroup(Triangle)
1802
- >>> vg + square # a new VGroup is constructed
2102
+ >>> vg + square # a new VGroup is constructed
1803
2103
  VGroup(Triangle, Square)
1804
- >>> vg # not modified
2104
+ >>> vg # not modified
1805
2105
  VGroup(Triangle)
1806
- >>> vg += square; vg # modifies vg
2106
+ >>> vg += square
2107
+ >>> vg # modifies vg
1807
2108
  VGroup(Triangle, Square)
1808
2109
  >>> vg.remove(triangle)
1809
2110
  VGroup(Square)
1810
- >>> vg - square; # a new VGroup is constructed
2111
+ >>> vg - square # a new VGroup is constructed
1811
2112
  VGroup()
1812
- >>> vg # not modified
2113
+ >>> vg # not modified
1813
2114
  VGroup(Square)
1814
- >>> vg -= square; vg # modifies vg
2115
+ >>> vg -= square
2116
+ >>> vg # modifies vg
1815
2117
  VGroup()
1816
2118
 
1817
2119
  .. manim:: ArcShapeIris
@@ -1831,31 +2133,31 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
1831
2133
 
1832
2134
  """
1833
2135
 
1834
- def __init__(self, *vmobjects, **kwargs):
2136
+ def __init__(
2137
+ self, *vmobjects: VMobject | Iterable[VMobject], **kwargs: Any
2138
+ ) -> None:
1835
2139
  super().__init__(**kwargs)
1836
2140
  self.add(*vmobjects)
1837
2141
 
1838
- def __repr__(self):
1839
- return (
1840
- self.__class__.__name__
1841
- + "("
1842
- + ", ".join(str(mob) for mob in self.submobjects)
1843
- + ")"
1844
- )
2142
+ def __repr__(self) -> str:
2143
+ return f"{self.__class__.__name__}({', '.join(str(mob) for mob in self.submobjects)})"
1845
2144
 
1846
- def __str__(self):
2145
+ def __str__(self) -> str:
1847
2146
  return (
1848
2147
  f"{self.__class__.__name__} of {len(self.submobjects)} "
1849
2148
  f"submobject{'s' if len(self.submobjects) > 0 else ''}"
1850
2149
  )
1851
2150
 
1852
- def add(self, *vmobjects: VMobject):
1853
- """Checks if all passed elements are an instance of VMobject and then add them to submobjects
2151
+ def add(
2152
+ self,
2153
+ *vmobjects: VMobject | Iterable[VMobject],
2154
+ ) -> Self:
2155
+ """Checks if all passed elements are an instance, or iterables of VMobject and then adds them to submobjects
1854
2156
 
1855
2157
  Parameters
1856
2158
  ----------
1857
2159
  vmobjects
1858
- List of VMobject to add
2160
+ List or iterable of VMobjects to add
1859
2161
 
1860
2162
  Returns
1861
2163
  -------
@@ -1864,10 +2166,13 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
1864
2166
  Raises
1865
2167
  ------
1866
2168
  TypeError
1867
- If one element of the list is not an instance of VMobject
2169
+ If one element of the list, or iterable is not an instance of VMobject
1868
2170
 
1869
2171
  Examples
1870
2172
  --------
2173
+ The following example shows how to add individual or multiple `VMobject` instances through the `VGroup`
2174
+ constructor and its `.add()` method.
2175
+
1871
2176
  .. manim:: AddToVGroup
1872
2177
 
1873
2178
  class AddToVGroup(Scene):
@@ -1896,26 +2201,81 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
1896
2201
  self.play( # Animate group without component
1897
2202
  (gr-circle_red).animate.shift(RIGHT)
1898
2203
  )
2204
+
2205
+ A `VGroup` can be created using iterables as well. Keep in mind that all generated values from an
2206
+ iterable must be an instance of `VMobject`. This is demonstrated below:
2207
+
2208
+ .. manim:: AddIterableToVGroupExample
2209
+ :save_last_frame:
2210
+
2211
+ class AddIterableToVGroupExample(Scene):
2212
+ def construct(self):
2213
+ v = VGroup(
2214
+ Square(), # Singular VMobject instance
2215
+ [Circle(), Triangle()], # List of VMobject instances
2216
+ Dot(),
2217
+ (Dot() for _ in range(2)), # Iterable that generates VMobjects
2218
+ )
2219
+ v.arrange()
2220
+ self.add(v)
2221
+
2222
+ To facilitate this, the iterable is unpacked before its individual instances are added to the `VGroup`.
2223
+ As a result, when you index a `VGroup`, you will never get back an iterable.
2224
+ Instead, you will always receive `VMobject` instances, including those
2225
+ that were part of the iterable/s that you originally added to the `VGroup`.
1899
2226
  """
1900
- if not all(isinstance(m, (VMobject, OpenGLVMobject)) for m in vmobjects):
1901
- raise TypeError("All submobjects must be of type VMobject")
1902
- return super().add(*vmobjects)
1903
2227
 
1904
- def __add__(self, vmobject):
2228
+ def get_type_error_message(invalid_obj, invalid_indices):
2229
+ return (
2230
+ f"Only values of type {vmobject_render_type.__name__} can be added "
2231
+ "as submobjects of VGroup, but the value "
2232
+ f"{repr(invalid_obj)} (at index {invalid_indices[1]} of "
2233
+ f"parameter {invalid_indices[0]}) is of type "
2234
+ f"{type(invalid_obj).__name__}."
2235
+ )
2236
+
2237
+ vmobject_render_type = (
2238
+ OpenGLVMobject if config.renderer == RendererType.OPENGL else VMobject
2239
+ )
2240
+ valid_vmobjects = []
2241
+
2242
+ for i, vmobject in enumerate(vmobjects):
2243
+ if isinstance(vmobject, vmobject_render_type):
2244
+ valid_vmobjects.append(vmobject)
2245
+ elif isinstance(vmobject, Iterable) and not isinstance(
2246
+ vmobject, (Mobject, OpenGLMobject)
2247
+ ):
2248
+ for j, subvmobject in enumerate(vmobject):
2249
+ if not isinstance(subvmobject, vmobject_render_type):
2250
+ raise TypeError(get_type_error_message(subvmobject, (i, j)))
2251
+ valid_vmobjects.append(subvmobject)
2252
+ elif isinstance(vmobject, Iterable) and isinstance(
2253
+ vmobject, (Mobject, OpenGLMobject)
2254
+ ):
2255
+ raise TypeError(
2256
+ f"{get_type_error_message(vmobject, (i, 0))} "
2257
+ "You can try adding this value into a Group instead."
2258
+ )
2259
+ else:
2260
+ raise TypeError(get_type_error_message(vmobject, (i, 0)))
2261
+
2262
+ return super().add(*valid_vmobjects)
2263
+
2264
+ def __add__(self, vmobject: VMobject) -> Self:
1905
2265
  return VGroup(*self.submobjects, vmobject)
1906
2266
 
1907
- def __iadd__(self, vmobject):
2267
+ def __iadd__(self, vmobject: VMobject) -> Self:
1908
2268
  return self.add(vmobject)
1909
2269
 
1910
- def __sub__(self, vmobject):
2270
+ def __sub__(self, vmobject: VMobject) -> Self:
1911
2271
  copy = VGroup(*self.submobjects)
1912
2272
  copy.remove(vmobject)
1913
2273
  return copy
1914
2274
 
1915
- def __isub__(self, vmobject):
2275
+ def __isub__(self, vmobject: VMobject) -> Self:
1916
2276
  return self.remove(vmobject)
1917
2277
 
1918
- def __setitem__(self, key: int, value: VMobject | typing.Sequence[VMobject]):
2278
+ def __setitem__(self, key: int, value: VMobject | Sequence[VMobject]) -> None:
1919
2279
  """Override the [] operator for item assignment.
1920
2280
 
1921
2281
  Parameters
@@ -1936,8 +2296,7 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
1936
2296
  >>> new_obj = VMobject()
1937
2297
  >>> vgroup[0] = new_obj
1938
2298
  """
1939
- if not all(isinstance(m, (VMobject, OpenGLVMobject)) for m in value):
1940
- raise TypeError("All submobjects must be of type VMobject")
2299
+ self._assert_valid_submobjects(tuplify(value))
1941
2300
  self.submobjects[key] = value
1942
2301
 
1943
2302
 
@@ -2042,27 +2401,25 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
2042
2401
  def __init__(
2043
2402
  self,
2044
2403
  mapping_or_iterable: (
2045
- typing.Mapping[typing.Hashable, VMobject]
2046
- | typing.Iterable[tuple[typing.Hashable, VMobject]]
2404
+ Mapping[Hashable, VMobject] | Iterable[tuple[Hashable, VMobject]]
2047
2405
  ) = {},
2048
2406
  show_keys: bool = False,
2049
2407
  **kwargs,
2050
- ):
2408
+ ) -> None:
2051
2409
  super().__init__(**kwargs)
2052
2410
  self.show_keys = show_keys
2053
2411
  self.submob_dict = {}
2054
2412
  self.add(mapping_or_iterable)
2055
2413
 
2056
- def __repr__(self):
2057
- return __class__.__name__ + "(" + repr(self.submob_dict) + ")"
2414
+ def __repr__(self) -> str:
2415
+ return f"{self.__class__.__name__}({repr(self.submob_dict)})"
2058
2416
 
2059
2417
  def add(
2060
2418
  self,
2061
2419
  mapping_or_iterable: (
2062
- typing.Mapping[typing.Hashable, VMobject]
2063
- | typing.Iterable[tuple[typing.Hashable, VMobject]]
2420
+ Mapping[Hashable, VMobject] | Iterable[tuple[Hashable, VMobject]]
2064
2421
  ),
2065
- ):
2422
+ ) -> Self:
2066
2423
  """Adds the key-value pairs to the :class:`VDict` object.
2067
2424
 
2068
2425
  Also, it internally adds the value to the `submobjects` :class:`list`
@@ -2083,14 +2440,14 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
2083
2440
  Normal usage::
2084
2441
 
2085
2442
  square_obj = Square()
2086
- my_dict.add([('s', square_obj)])
2443
+ my_dict.add([("s", square_obj)])
2087
2444
  """
2088
2445
  for key, value in dict(mapping_or_iterable).items():
2089
2446
  self.add_key_value_pair(key, value)
2090
2447
 
2091
2448
  return self
2092
2449
 
2093
- def remove(self, key: typing.Hashable):
2450
+ def remove(self, key: Hashable) -> Self:
2094
2451
  """Removes the mobject from the :class:`VDict` object having the key `key`
2095
2452
 
2096
2453
  Also, it internally removes the mobject from the `submobjects` :class:`list`
@@ -2110,15 +2467,15 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
2110
2467
  --------
2111
2468
  Normal usage::
2112
2469
 
2113
- my_dict.remove('square')
2470
+ my_dict.remove("square")
2114
2471
  """
2115
2472
  if key not in self.submob_dict:
2116
- raise KeyError("The given key '%s' is not present in the VDict" % str(key))
2473
+ raise KeyError(f"The given key '{key!s}' is not present in the VDict")
2117
2474
  super().remove(self.submob_dict[key])
2118
2475
  del self.submob_dict[key]
2119
2476
  return self
2120
2477
 
2121
- def __getitem__(self, key: typing.Hashable):
2478
+ def __getitem__(self, key: Hashable):
2122
2479
  """Override the [] operator for item retrieval.
2123
2480
 
2124
2481
  Parameters
@@ -2135,12 +2492,12 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
2135
2492
  --------
2136
2493
  Normal usage::
2137
2494
 
2138
- self.play(Create(my_dict['s']))
2495
+ self.play(Create(my_dict["s"]))
2139
2496
  """
2140
2497
  submob = self.submob_dict[key]
2141
2498
  return submob
2142
2499
 
2143
- def __setitem__(self, key: typing.Hashable, value: VMobject):
2500
+ def __setitem__(self, key: Hashable, value: VMobject) -> None:
2144
2501
  """Override the [] operator for item assignment.
2145
2502
 
2146
2503
  Parameters
@@ -2159,13 +2516,13 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
2159
2516
  Normal usage::
2160
2517
 
2161
2518
  square_obj = Square()
2162
- my_dict['sq'] = square_obj
2519
+ my_dict["sq"] = square_obj
2163
2520
  """
2164
2521
  if key in self.submob_dict:
2165
2522
  self.remove(key)
2166
2523
  self.add([(key, value)])
2167
2524
 
2168
- def __delitem__(self, key: typing.Hashable):
2525
+ def __delitem__(self, key: Hashable):
2169
2526
  """Override the del operator for deleting an item.
2170
2527
 
2171
2528
  Parameters
@@ -2197,7 +2554,7 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
2197
2554
  """
2198
2555
  del self.submob_dict[key]
2199
2556
 
2200
- def __contains__(self, key: typing.Hashable):
2557
+ def __contains__(self, key: Hashable):
2201
2558
  """Override the in operator.
2202
2559
 
2203
2560
  Parameters
@@ -2221,7 +2578,7 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
2221
2578
  """
2222
2579
  return key in self.submob_dict
2223
2580
 
2224
- def get_all_submobjects(self):
2581
+ def get_all_submobjects(self) -> list[list]:
2225
2582
  """To get all the submobjects associated with a particular :class:`VDict` object
2226
2583
 
2227
2584
  Returns
@@ -2239,7 +2596,7 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
2239
2596
  submobjects = self.submob_dict.values()
2240
2597
  return submobjects
2241
2598
 
2242
- def add_key_value_pair(self, key: typing.Hashable, value: VMobject):
2599
+ def add_key_value_pair(self, key: Hashable, value: VMobject) -> None:
2243
2600
  """A utility function used by :meth:`add` to add the key-value pair
2244
2601
  to :attr:`submob_dict`. Not really meant to be used externally.
2245
2602
 
@@ -2264,11 +2621,10 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
2264
2621
  Normal usage::
2265
2622
 
2266
2623
  square_obj = Square()
2267
- self.add_key_value_pair('s', square_obj)
2624
+ self.add_key_value_pair("s", square_obj)
2268
2625
 
2269
2626
  """
2270
- if not isinstance(value, (VMobject, OpenGLVMobject)):
2271
- raise TypeError("All submobjects must be of type VMobject")
2627
+ self._assert_valid_submobjects([value])
2272
2628
  mob = value
2273
2629
  if self.show_keys:
2274
2630
  # This import is here and not at the top to avoid circular import
@@ -2284,14 +2640,14 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
2284
2640
  class VectorizedPoint(VMobject, metaclass=ConvertToOpenGL):
2285
2641
  def __init__(
2286
2642
  self,
2287
- location=ORIGIN,
2288
- color=BLACK,
2289
- fill_opacity=0,
2290
- stroke_width=0,
2291
- artificial_width=0.01,
2292
- artificial_height=0.01,
2643
+ location: Point3DLike = ORIGIN,
2644
+ color: ManimColor = BLACK,
2645
+ fill_opacity: float = 0,
2646
+ stroke_width: float = 0,
2647
+ artificial_width: float = 0.01,
2648
+ artificial_height: float = 0.01,
2293
2649
  **kwargs,
2294
- ):
2650
+ ) -> None:
2295
2651
  self.artificial_width = artificial_width
2296
2652
  self.artificial_height = artificial_height
2297
2653
  super().__init__(
@@ -2305,17 +2661,17 @@ class VectorizedPoint(VMobject, metaclass=ConvertToOpenGL):
2305
2661
  basecls = OpenGLVMobject if config.renderer == RendererType.OPENGL else VMobject
2306
2662
 
2307
2663
  @basecls.width.getter
2308
- def width(self):
2664
+ def width(self) -> float:
2309
2665
  return self.artificial_width
2310
2666
 
2311
2667
  @basecls.height.getter
2312
- def height(self):
2668
+ def height(self) -> float:
2313
2669
  return self.artificial_height
2314
2670
 
2315
- def get_location(self):
2671
+ def get_location(self) -> Point3D:
2316
2672
  return np.array(self.points[0])
2317
2673
 
2318
- def set_location(self, new_loc):
2674
+ def set_location(self, new_loc: Point3D):
2319
2675
  self.set_points(np.array([new_loc]))
2320
2676
 
2321
2677
 
@@ -2336,7 +2692,7 @@ class CurvesAsSubmobjects(VGroup):
2336
2692
 
2337
2693
  """
2338
2694
 
2339
- def __init__(self, vmobject, **kwargs):
2695
+ def __init__(self, vmobject: VMobject, **kwargs) -> None:
2340
2696
  super().__init__(**kwargs)
2341
2697
  tuples = vmobject.get_cubic_bezier_tuples()
2342
2698
  for tup in tuples:
@@ -2345,6 +2701,69 @@ class CurvesAsSubmobjects(VGroup):
2345
2701
  part.match_style(vmobject)
2346
2702
  self.add(part)
2347
2703
 
2704
+ def point_from_proportion(self, alpha: float) -> Point3D:
2705
+ """Gets the point at a proportion along the path of the :class:`CurvesAsSubmobjects`.
2706
+
2707
+ Parameters
2708
+ ----------
2709
+ alpha
2710
+ The proportion along the the path of the :class:`CurvesAsSubmobjects`.
2711
+
2712
+ Returns
2713
+ -------
2714
+ :class:`numpy.ndarray`
2715
+ The point on the :class:`CurvesAsSubmobjects`.
2716
+
2717
+ Raises
2718
+ ------
2719
+ :exc:`ValueError`
2720
+ If ``alpha`` is not between 0 and 1.
2721
+ :exc:`Exception`
2722
+ If the :class:`CurvesAsSubmobjects` has no submobjects, or no submobject has points.
2723
+ """
2724
+ if alpha < 0 or alpha > 1:
2725
+ raise ValueError(f"Alpha {alpha} not between 0 and 1.")
2726
+
2727
+ self._throw_error_if_no_submobjects()
2728
+ submobjs_with_pts = self._get_submobjects_with_points()
2729
+
2730
+ if alpha == 1:
2731
+ return submobjs_with_pts[-1].points[-1]
2732
+
2733
+ submobjs_arc_lengths = tuple(
2734
+ part.get_arc_length() for part in submobjs_with_pts
2735
+ )
2736
+
2737
+ total_length = sum(submobjs_arc_lengths)
2738
+ target_length = alpha * total_length
2739
+ current_length = 0
2740
+
2741
+ for i, part in enumerate(submobjs_with_pts):
2742
+ part_length = submobjs_arc_lengths[i]
2743
+ if current_length + part_length >= target_length:
2744
+ residue = (target_length - current_length) / part_length
2745
+ return part.point_from_proportion(residue)
2746
+
2747
+ current_length += part_length
2748
+
2749
+ def _throw_error_if_no_submobjects(self):
2750
+ if len(self.submobjects) == 0:
2751
+ caller_name = sys._getframe(1).f_code.co_name
2752
+ raise Exception(
2753
+ f"Cannot call CurvesAsSubmobjects. {caller_name} for a CurvesAsSubmobject with no submobjects"
2754
+ )
2755
+
2756
+ def _get_submobjects_with_points(self):
2757
+ submobjs_with_pts = tuple(
2758
+ part for part in self.submobjects if len(part.points) > 0
2759
+ )
2760
+ if len(submobjs_with_pts) == 0:
2761
+ caller_name = sys._getframe(1).f_code.co_name
2762
+ raise Exception(
2763
+ f"Cannot call CurvesAsSubmobjects. {caller_name} for a CurvesAsSubmobject whose submobjects have no points"
2764
+ )
2765
+ return submobjs_with_pts
2766
+
2348
2767
 
2349
2768
  class DashedVMobject(VMobject, metaclass=ConvertToOpenGL):
2350
2769
  """A :class:`VMobject` composed of dashes instead of lines.
@@ -2402,18 +2821,32 @@ class DashedVMobject(VMobject, metaclass=ConvertToOpenGL):
2402
2821
 
2403
2822
  def __init__(
2404
2823
  self,
2405
- vmobject,
2406
- num_dashes=15,
2407
- dashed_ratio=0.5,
2408
- dash_offset=0,
2409
- color=WHITE,
2410
- equal_lengths=True,
2824
+ vmobject: VMobject,
2825
+ num_dashes: int = 15,
2826
+ dashed_ratio: float = 0.5,
2827
+ dash_offset: float = 0,
2828
+ color: ManimColor = WHITE,
2829
+ equal_lengths: bool = True,
2411
2830
  **kwargs,
2412
- ):
2413
-
2831
+ ) -> None:
2414
2832
  self.dashed_ratio = dashed_ratio
2415
2833
  self.num_dashes = num_dashes
2416
2834
  super().__init__(color=color, **kwargs)
2835
+
2836
+ # Work on a copy to avoid mutating the caller's mobject (e.g. removing tips).
2837
+ base_vmobject = vmobject
2838
+ vmobject = base_vmobject.copy()
2839
+
2840
+ # TipableVMobject instances (Arrow, Vector, etc.) carry tips as submobjects.
2841
+ # When dashing such objects, each subcurve would otherwise include its own
2842
+ # tip, leading to many overlapping arrowheads. Pop tips from the working
2843
+ # copy and re-attach them only once after the dashes are created.
2844
+ tips = None
2845
+ if hasattr(vmobject, "pop_tips"):
2846
+ popped_tips = vmobject.pop_tips()
2847
+ if len(popped_tips.submobjects) > 0:
2848
+ tips = popped_tips
2849
+
2417
2850
  r = self.dashed_ratio
2418
2851
  n = self.num_dashes
2419
2852
  if n > 0:
@@ -2422,15 +2855,12 @@ class DashedVMobject(VMobject, metaclass=ConvertToOpenGL):
2422
2855
  if vmobject.is_closed():
2423
2856
  void_len = (1 - r) / n
2424
2857
  else:
2425
- if n == 1:
2426
- void_len = 1 - r
2427
- else:
2428
- void_len = (1 - r) / (n - 1)
2858
+ void_len = 1 - r if n == 1 else (1 - r) / (n - 1)
2429
2859
 
2430
2860
  period = dash_len + void_len
2431
2861
  phase_shift = (dash_offset % 1) * period
2432
2862
 
2433
- if vmobject.is_closed():
2863
+ if vmobject.is_closed(): # noqa: SIM108
2434
2864
  # closed curves have equal amount of dashes and voids
2435
2865
  pattern_len = 1
2436
2866
  else:
@@ -2502,6 +2932,9 @@ class DashedVMobject(VMobject, metaclass=ConvertToOpenGL):
2502
2932
  # Family is already taken care of by get_subcurve
2503
2933
  # implementation
2504
2934
  if config.renderer == RendererType.OPENGL:
2505
- self.match_style(vmobject, recurse=False)
2935
+ self.match_style(base_vmobject, recurse=False)
2506
2936
  else:
2507
- self.match_style(vmobject, family=False)
2937
+ self.match_style(base_vmobject, family=False)
2938
+
2939
+ if tips is not None:
2940
+ self.add(*tips.submobjects)