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
@@ -6,7 +6,7 @@
6
6
 
7
7
  .. important::
8
8
 
9
- See the corresponding tutorial :ref:`rendering-with-latex`
9
+ See the corresponding tutorial :ref:`using-text-objects`, especially for information about fonts.
10
10
 
11
11
 
12
12
  The simplest way to add text to your animations is to use the :class:`~.Text` class. It uses the Pango library to render text.
@@ -49,21 +49,22 @@ Examples
49
49
 
50
50
  from __future__ import annotations
51
51
 
52
+ import functools
53
+
52
54
  __all__ = ["Text", "Paragraph", "MarkupText", "register_font"]
53
55
 
54
56
 
55
57
  import copy
56
58
  import hashlib
57
- import os
58
59
  import re
60
+ from collections.abc import Iterable, Iterator, Sequence
59
61
  from contextlib import contextmanager
60
62
  from itertools import chain
61
63
  from pathlib import Path
62
- from typing import Iterable, Sequence
64
+ from typing import TYPE_CHECKING, Any
63
65
 
64
66
  import manimpango
65
67
  import numpy as np
66
- from colour import Color
67
68
  from manimpango import MarkupUtils, PangoUtils, TextSetting
68
69
 
69
70
  from manim import config, logger
@@ -71,15 +72,22 @@ from manim.constants import *
71
72
  from manim.mobject.geometry.arc import Dot
72
73
  from manim.mobject.svg.svg_mobject import SVGMobject
73
74
  from manim.mobject.types.vectorized_mobject import VGroup, VMobject
74
- from manim.utils.color import Colors, color_gradient
75
- from manim.utils.deprecation import deprecated
75
+ from manim.typing import Point3D
76
+ from manim.utils.color import ManimColor, ParsableManimColor, color_gradient
77
+
78
+ if TYPE_CHECKING:
79
+ from typing_extensions import Self
80
+
81
+ from manim.typing import Point3D
76
82
 
77
83
  TEXT_MOB_SCALE_FACTOR = 0.05
78
84
  DEFAULT_LINE_SPACING_SCALE = 0.3
79
85
  TEXT2SVG_ADJUSTMENT_FACTOR = 4.8
80
86
 
87
+ __all__ = ["Text", "Paragraph", "MarkupText", "register_font"]
88
+
81
89
 
82
- def remove_invisible_chars(mobject: SVGMobject) -> SVGMobject:
90
+ def remove_invisible_chars(mobject: VMobject) -> VMobject:
83
91
  """Function to remove unwanted invisible characters from some mobjects.
84
92
 
85
93
  Parameters
@@ -92,24 +100,14 @@ def remove_invisible_chars(mobject: SVGMobject) -> SVGMobject:
92
100
  :class:`~.SVGMobject`
93
101
  The SVGMobject without unwanted invisible characters.
94
102
  """
95
-
96
- iscode = False
97
- if mobject.__class__.__name__ == "Text":
98
- mobject = mobject[:]
99
- elif mobject.__class__.__name__ == "Code":
100
- iscode = True
101
- code = mobject
102
- mobject = mobject.code
103
103
  mobject_without_dots = VGroup()
104
- if mobject[0].__class__ == VGroup:
105
- for i in range(len(mobject)):
106
- mobject_without_dots.add(VGroup())
107
- mobject_without_dots[i].add(*(k for k in mobject[i] if k.__class__ != Dot))
104
+ if isinstance(mobject[0], VGroup):
105
+ for submob in mobject:
106
+ mobject_without_dots.add(
107
+ VGroup(k for k in submob if not isinstance(k, Dot))
108
+ )
108
109
  else:
109
- mobject_without_dots.add(*(k for k in mobject if k.__class__ != Dot))
110
- if iscode:
111
- code.code = mobject_without_dots
112
- return code
110
+ mobject_without_dots.add(*(k for k in mobject if not isinstance(k, Dot)))
113
111
  return mobject_without_dots
114
112
 
115
113
 
@@ -132,10 +130,17 @@ class Paragraph(VGroup):
132
130
  --------
133
131
  Normal usage::
134
132
 
135
- paragraph = Paragraph('this is a awesome', 'paragraph',
136
- 'With \nNewlines', '\tWith Tabs',
137
- ' With Spaces', 'With Alignments',
138
- 'center', 'left', 'right')
133
+ paragraph = Paragraph(
134
+ "this is a awesome",
135
+ "paragraph",
136
+ "With \nNewlines",
137
+ "\tWith Tabs",
138
+ " With Spaces",
139
+ "With Alignments",
140
+ "center",
141
+ "left",
142
+ "right",
143
+ )
139
144
 
140
145
  Remove unwanted invisible characters::
141
146
 
@@ -146,11 +151,11 @@ class Paragraph(VGroup):
146
151
 
147
152
  def __init__(
148
153
  self,
149
- *text: Sequence[str],
154
+ *text: str,
150
155
  line_spacing: float = -1,
151
- alignment: Optional[str] = None,
152
- **kwargs,
153
- ) -> None:
156
+ alignment: str | None = None,
157
+ **kwargs: Any,
158
+ ):
154
159
  self.line_spacing = line_spacing
155
160
  self.alignment = alignment
156
161
  self.consider_spaces_as_chars = kwargs.get("disable_ligatures", False)
@@ -292,7 +297,7 @@ class Paragraph(VGroup):
292
297
 
293
298
 
294
299
  class Text(SVGMobject):
295
- r"""Display (non-LaTeX) text rendered using `Pango <https://pango.gnome.org/>`_.
300
+ r"""Display (non-LaTeX) text rendered using `Pango <https://pango.org/>`_.
296
301
 
297
302
  Text objects behave like a :class:`.VGroup`-like iterable of all characters
298
303
  in the given text. In particular, slicing is possible.
@@ -301,6 +306,13 @@ class Text(SVGMobject):
301
306
  ----------
302
307
  text
303
308
  The text that needs to be created as a mobject.
309
+ font
310
+ The font family to be used to render the text. This is either a system font or
311
+ one loaded with `register_font()`. Note that font family names may be different
312
+ across operating systems.
313
+ warn_missing_font
314
+ If True (default), Manim will issue a warning if the font does not exist in the
315
+ (case-sensitive) list of fonts returned from `manimpango.list_fonts()`.
304
316
 
305
317
  Returns
306
318
  -------
@@ -344,7 +356,7 @@ class Text(SVGMobject):
344
356
  )
345
357
  text6.scale(1.3).shift(DOWN)
346
358
  self.add(text1, text2, text3, text4, text5 , text6)
347
- Group(*self.mobjects).arrange(DOWN, buff=.8).set_height(config.frame_height-LARGE_BUFF)
359
+ Group(*self.mobjects).arrange(DOWN, buff=.8).set(height=config.frame_height-LARGE_BUFF)
348
360
 
349
361
  .. manim:: TextMoreCustomization
350
362
  :save_last_frame:
@@ -401,33 +413,55 @@ class Text(SVGMobject):
401
413
 
402
414
  """
403
415
 
416
+ @staticmethod
417
+ @functools.cache
418
+ def font_list() -> list[str]:
419
+ value: list[str] = manimpango.list_fonts()
420
+ return value
421
+
404
422
  def __init__(
405
423
  self,
406
424
  text: str,
407
425
  fill_opacity: float = 1.0,
408
426
  stroke_width: float = 0,
409
- color: Color | str | None = None,
427
+ color: ParsableManimColor | None = None,
410
428
  font_size: float = DEFAULT_FONT_SIZE,
411
429
  line_spacing: float = -1,
412
430
  font: str = "",
413
431
  slant: str = NORMAL,
414
432
  weight: str = NORMAL,
415
- t2c: dict[str, str] = None,
416
- t2f: dict[str, str] = None,
417
- t2g: dict[str, tuple] = None,
418
- t2s: dict[str, str] = None,
419
- t2w: dict[str, str] = None,
420
- gradient: tuple = None,
433
+ t2c: dict[str, str] | None = None,
434
+ t2f: dict[str, str] | None = None,
435
+ t2g: dict[str, Iterable[ParsableManimColor]] | None = None,
436
+ t2s: dict[str, str] | None = None,
437
+ t2w: dict[str, str] | None = None,
438
+ gradient: Iterable[ParsableManimColor] | None = None,
421
439
  tab_width: int = 4,
440
+ warn_missing_font: bool = True,
422
441
  # Mobject
423
- height: float = None,
424
- width: float = None,
442
+ height: float | None = None,
443
+ width: float | None = None,
425
444
  should_center: bool = True,
426
445
  disable_ligatures: bool = False,
427
- **kwargs,
428
- ) -> None:
429
-
446
+ use_svg_cache: bool = False,
447
+ **kwargs: Any,
448
+ ):
430
449
  self.line_spacing = line_spacing
450
+ if font and warn_missing_font:
451
+ fonts_list = Text.font_list()
452
+ # handle special case of sans/sans-serif
453
+ if font.lower() == "sans-serif":
454
+ font = "sans"
455
+ if font not in fonts_list:
456
+ # check if the capitalized version is in the supported fonts
457
+ if font.capitalize() in fonts_list:
458
+ font = font.capitalize()
459
+ elif font.lower() in fonts_list:
460
+ font = font.lower()
461
+ elif font.title() in fonts_list:
462
+ font = font.title()
463
+ else:
464
+ logger.warning(f"Font {font} not in {fonts_list}.")
431
465
  self.font = font
432
466
  self._font_size = float(font_size)
433
467
  # needs to be a float or else size is inflated when font_size = 24
@@ -452,11 +486,16 @@ class Text(SVGMobject):
452
486
  t2g = kwargs.pop("text2gradient", t2g)
453
487
  t2s = kwargs.pop("text2slant", t2s)
454
488
  t2w = kwargs.pop("text2weight", t2w)
455
- self.t2c = t2c
456
- self.t2f = t2f
457
- self.t2g = t2g
458
- self.t2s = t2s
459
- self.t2w = t2w
489
+ assert t2c is not None
490
+ assert t2f is not None
491
+ assert t2g is not None
492
+ assert t2s is not None
493
+ assert t2w is not None
494
+ self.t2c: dict[str, str] = {k: ManimColor(v).to_hex() for k, v in t2c.items()}
495
+ self.t2f: dict[str, str] = t2f
496
+ self.t2g: dict[str, Iterable[ParsableManimColor]] = t2g
497
+ self.t2s: dict[str, str] = t2s
498
+ self.t2w: dict[str, str] = t2w
460
499
 
461
500
  self.original_text = text
462
501
  self.disable_ligatures = disable_ligatures
@@ -471,8 +510,8 @@ class Text(SVGMobject):
471
510
  else:
472
511
  self.line_spacing = self._font_size + self._font_size * self.line_spacing
473
512
 
474
- color = Color(color) if color else VMobject().color
475
- file_name = self._text2svg(color)
513
+ parsed_color: ManimColor = ManimColor(color) if color else VMobject().color
514
+ file_name = self._text2svg(parsed_color.to_hex())
476
515
  PangoUtils.remove_last_M(file_name)
477
516
  super().__init__(
478
517
  file_name,
@@ -481,6 +520,7 @@ class Text(SVGMobject):
481
520
  height=height,
482
521
  width=width,
483
522
  should_center=should_center,
523
+ use_svg_cache=use_svg_cache,
484
524
  **kwargs,
485
525
  )
486
526
  self.text = text
@@ -493,28 +533,68 @@ class Text(SVGMobject):
493
533
  if len(each.points) == 0:
494
534
  continue
495
535
  points = each.points
496
- last = points[0]
497
- each.clear_points()
536
+ curve_start = points[0]
537
+ assert len(curve_start) == self.dim, curve_start
538
+ # Some of the glyphs in this text might not be closed,
539
+ # so we close them by identifying when one curve ends
540
+ # but it is not where the next curve starts.
541
+ # It is more efficient to temporarily create a list
542
+ # of points and add them one at a time, then turn them
543
+ # into a numpy array at the end, rather than creating
544
+ # new numpy arrays every time a point or fixing line
545
+ # is added (which is O(n^2) for numpy arrays).
546
+ closed_curve_points: list[Point3D] = []
547
+ # OpenGL has points be part of quadratic Bezier curves;
548
+ # Cairo uses cubic Bezier curves.
549
+ if nppc == 3: # RendererType.OPENGL
550
+
551
+ def add_line_to(end: Point3D) -> None:
552
+ nonlocal closed_curve_points
553
+ start = closed_curve_points[-1]
554
+ closed_curve_points += [
555
+ start,
556
+ (start + end) / 2,
557
+ end,
558
+ ]
559
+
560
+ else: # RendererType.CAIRO
561
+
562
+ def add_line_to(end: Point3D) -> None:
563
+ nonlocal closed_curve_points
564
+ start = closed_curve_points[-1]
565
+ closed_curve_points += [
566
+ start,
567
+ (start + start + end) / 3,
568
+ (start + end + end) / 3,
569
+ end,
570
+ ]
571
+
498
572
  for index, point in enumerate(points):
499
- each.append_points([point])
573
+ closed_curve_points.append(point)
500
574
  if (
501
575
  index != len(points) - 1
502
576
  and (index + 1) % nppc == 0
503
577
  and any(point != points[index + 1])
504
578
  ):
505
- each.add_line_to(last)
506
- last = points[index + 1]
507
- each.add_line_to(last)
579
+ # Add straight line from last point on this curve to the
580
+ # start point on the next curve. We represent the line
581
+ # as a cubic bezier curve where the two control points
582
+ # are half-way between the start and stop point.
583
+ add_line_to(curve_start)
584
+ curve_start = points[index + 1]
585
+ # Make sure last curve is closed
586
+ add_line_to(curve_start)
587
+ each.points = np.array(closed_curve_points, ndmin=2)
508
588
  # anti-aliasing
509
589
  if height is None and width is None:
510
590
  self.scale(TEXT_MOB_SCALE_FACTOR)
511
591
  self.initial_height = self.height
512
592
 
513
- def __repr__(self):
593
+ def __repr__(self) -> str:
514
594
  return f"Text({repr(self.original_text)})"
515
595
 
516
596
  @property
517
- def font_size(self):
597
+ def font_size(self) -> float:
518
598
  return (
519
599
  self.height
520
600
  / self.initial_height
@@ -525,14 +605,14 @@ class Text(SVGMobject):
525
605
  )
526
606
 
527
607
  @font_size.setter
528
- def font_size(self, font_val):
608
+ def font_size(self, font_val: float) -> None:
529
609
  # TODO: use pango's font size scaling.
530
610
  if font_val <= 0:
531
611
  raise ValueError("font_size must be greater than 0.")
532
612
  else:
533
613
  self.scale(font_val / self.font_size)
534
614
 
535
- def _gen_chars(self):
615
+ def _gen_chars(self) -> VGroup:
536
616
  chars = self.get_group_class()()
537
617
  submobjects_char_index = 0
538
618
  for char_index in range(len(self.text)):
@@ -550,7 +630,7 @@ class Text(SVGMobject):
550
630
  submobjects_char_index += 1
551
631
  return chars
552
632
 
553
- def _find_indexes(self, word: str, text: str):
633
+ def _find_indexes(self, word: str, text: str) -> list[tuple[int, int]]:
554
634
  """Finds the indexes of ``text`` in ``word``."""
555
635
  temp = re.match(r"\[([0-9\-]{0,}):([0-9\-]{0,})\]", word)
556
636
  if temp:
@@ -558,7 +638,9 @@ class Text(SVGMobject):
558
638
  end = int(temp.group(2)) if temp.group(2) != "" else len(text)
559
639
  start = len(text) + start if start < 0 else start
560
640
  end = len(text) + end if end < 0 else end
561
- return [(start, end)]
641
+ return [
642
+ (start, end),
643
+ ]
562
644
  indexes = []
563
645
  index = text.find(word)
564
646
  while index != -1:
@@ -566,39 +648,15 @@ class Text(SVGMobject):
566
648
  index = text.find(word, index + len(word))
567
649
  return indexes
568
650
 
569
- @deprecated(
570
- since="v0.14.0",
571
- until="v0.15.0",
572
- message="This was internal function, you shouldn't be using it anyway.",
573
- )
574
- def _set_color_by_t2c(self, t2c=None):
575
- """Sets color for specified strings."""
576
- t2c = t2c if t2c else self.t2c
577
- for word, color in list(t2c.items()):
578
- for start, end in self._find_indexes(word, self.text):
579
- self.chars[start:end].set_color(color)
580
-
581
- @deprecated(
582
- since="v0.14.0",
583
- until="v0.15.0",
584
- message="This was internal function, you shouldn't be using it anyway.",
585
- )
586
- def _set_color_by_t2g(self, t2g=None):
587
- """Sets gradient colors for specified
588
- strings. Behaves similarly to ``set_color_by_t2c``."""
589
- t2g = t2g if t2g else self.t2g
590
- for word, gradient in list(t2g.items()):
591
- for start, end in self._find_indexes(word, self.text):
592
- self.chars[start:end].set_color_by_gradient(*gradient)
593
-
594
- def _text2hash(self, color: Color):
651
+ def _text2hash(self, color: ParsableManimColor) -> str:
595
652
  """Generates ``sha256`` hash for file name."""
596
653
  settings = (
597
- "PANGO" + self.font + self.slant + self.weight + color.hex_l
654
+ "PANGO" + self.font + self.slant + self.weight + str(color)
598
655
  ) # to differentiate Text and CairoText
599
656
  settings += str(self.t2f) + str(self.t2s) + str(self.t2w) + str(self.t2c)
600
657
  settings += str(self.line_spacing) + str(self._font_size)
601
658
  settings += str(self.disable_ligatures)
659
+ settings += str(self.gradient)
602
660
  id_str = self.text + settings
603
661
  hasher = hashlib.sha256()
604
662
  hasher.update(id_str.encode())
@@ -624,7 +682,7 @@ class Text(SVGMobject):
624
682
  default = default_args[arg]
625
683
  if left != default and getattr(right_setting, arg) != default:
626
684
  raise ValueError(
627
- f"Ambiguous style for text '{self.text[right_setting.start:right_setting.end]}':"
685
+ f"Ambiguous style for text '{self.text[right_setting.start : right_setting.end]}':"
628
686
  + f"'{arg}' cannot be both '{left}' and '{right}'."
629
687
  )
630
688
  setattr(right_setting, arg, left if left != default else right)
@@ -634,12 +692,14 @@ class Text(SVGMobject):
634
692
  self,
635
693
  t2xs: Sequence[tuple[dict[str, str], str]],
636
694
  default_args: dict[str, Iterable[str]],
637
- ) -> Sequence[TextSetting]:
695
+ ) -> list[TextSetting]:
638
696
  settings = []
639
697
  t2xwords = set(chain(*([*t2x.keys()] for t2x, _ in t2xs)))
640
698
  for word in t2xwords:
641
699
  setting_args = {
642
- arg: t2x[word] if word in t2x else default_args[arg]
700
+ arg: str(t2x[word]) if word in t2x else default_args[arg]
701
+ # NOTE: when t2x[word] is a ManimColor, str will yield the
702
+ # hex representation
643
703
  for t2x, arg in t2xs
644
704
  }
645
705
 
@@ -648,42 +708,36 @@ class Text(SVGMobject):
648
708
  return settings
649
709
 
650
710
  def _get_settings_from_gradient(
651
- self, default_args: dict[str, Iterable[str]]
652
- ) -> Sequence[TextSetting]:
711
+ self, default_args: dict[str, Any]
712
+ ) -> list[TextSetting]:
653
713
  settings = []
654
714
  args = copy.copy(default_args)
655
715
  if self.gradient:
656
- colors = color_gradient(self.gradient, len(self.text))
716
+ colors: list[ManimColor] = color_gradient(self.gradient, len(self.text))
657
717
  for i in range(len(self.text)):
658
- args["color"] = colors[i].hex
718
+ args["color"] = colors[i].to_hex()
659
719
  settings.append(TextSetting(i, i + 1, **args))
660
720
 
661
721
  for word, gradient in self.t2g.items():
662
- if isinstance(gradient, str) or len(gradient) == 1:
663
- color = gradient if isinstance(gradient, str) else gradient[0]
664
- gradient = [Color(color)]
665
- colors = (
666
- color_gradient(gradient, len(word))
667
- if len(gradient) != 1
668
- else len(word) * gradient
669
- )
722
+ colors = color_gradient(gradient, len(word))
670
723
  for start, end in self._find_indexes(word, self.text):
671
724
  for i in range(start, end):
672
- args["color"] = colors[i - start].hex
725
+ args["color"] = colors[i - start].to_hex()
673
726
  settings.append(TextSetting(i, i + 1, **args))
674
727
  return settings
675
728
 
676
- def _text2settings(self, color: Color):
729
+ def _text2settings(self, color: ParsableManimColor) -> list[TextSetting]:
677
730
  """Converts the texts and styles to a setting for parsing."""
678
- t2xs = [
731
+ t2xs: list[tuple[dict[str, str], str]] = [
679
732
  (self.t2f, "font"),
680
733
  (self.t2s, "slant"),
681
734
  (self.t2w, "weight"),
682
735
  (self.t2c, "color"),
683
736
  ]
684
737
  # setting_args requires values to be strings
685
- default_args = {
686
- arg: getattr(self, arg) if arg != "color" else str(color) for _, arg in t2xs
738
+
739
+ default_args: dict[str, Any] = {
740
+ arg: getattr(self, arg) if arg != "color" else color for _, arg in t2xs
687
741
  }
688
742
 
689
743
  settings = self._get_settings_from_t2xs(t2xs, default_args)
@@ -720,15 +774,15 @@ class Text(SVGMobject):
720
774
 
721
775
  line_num = 0
722
776
  if re.search(r"\n", self.text):
723
- for start, end in self._find_indexes("\n", self.text):
777
+ for for_start, for_end in self._find_indexes("\n", self.text):
724
778
  for setting in settings:
725
779
  if setting.line_num == -1:
726
780
  setting.line_num = line_num
727
- if start < setting.end:
781
+ if for_start < setting.end:
728
782
  line_num += 1
729
783
  new_setting = copy.copy(setting)
730
- setting.end = end
731
- new_setting.start = end
784
+ setting.end = for_end
785
+ new_setting.start = for_end
732
786
  new_setting.line_num = line_num
733
787
  settings.append(new_setting)
734
788
  settings.sort(key=lambda setting: setting.start)
@@ -739,7 +793,7 @@ class Text(SVGMobject):
739
793
 
740
794
  return settings
741
795
 
742
- def _text2svg(self, color: Color):
796
+ def _text2svg(self, color: ParsableManimColor) -> str:
743
797
  """Convert the text to SVG using Pango."""
744
798
  size = self._font_size
745
799
  line_spacing = self.line_spacing
@@ -774,15 +828,16 @@ class Text(SVGMobject):
774
828
 
775
829
  return svg_file
776
830
 
777
- def init_colors(self, propagate_colors=True):
831
+ def init_colors(self, propagate_colors: bool = True) -> Self:
778
832
  if config.renderer == RendererType.OPENGL:
779
833
  super().init_colors()
780
834
  elif config.renderer == RendererType.CAIRO:
781
835
  super().init_colors(propagate_colors=propagate_colors)
836
+ return self
782
837
 
783
838
 
784
839
  class MarkupText(SVGMobject):
785
- r"""Display (non-LaTeX) text rendered using `Pango <https://pango.gnome.org/>`_.
840
+ r"""Display (non-LaTeX) text rendered using `Pango <https://pango.org/>`_.
786
841
 
787
842
  Text objects behave like a :class:`.VGroup`-like iterable of all characters
788
843
  in the given text. In particular, slicing is possible.
@@ -832,7 +887,7 @@ class MarkupText(SVGMobject):
832
887
  Here is a list of supported tags:
833
888
 
834
889
  - ``<b>bold</b>``, ``<i>italic</i>`` and ``<b><i>bold+italic</i></b>``
835
- - ``<ul>underline</ul>`` and ``<s>strike through</s>``
890
+ - ``<u>underline</u>`` and ``<s>strike through</s>``
836
891
  - ``<tt>typewriter font</tt>``
837
892
  - ``<big>bigger font</big>`` and ``<small>smaller font</small>``
838
893
  - ``<sup>superscript</sup>`` and ``<sub>subscript</sub>``
@@ -911,7 +966,9 @@ class MarkupText(SVGMobject):
911
966
  Global weight setting, e.g. `NORMAL` or `BOLD`. Local overrides are possible.
912
967
  gradient
913
968
  Global gradient setting. Local overrides are possible.
914
-
969
+ warn_missing_font
970
+ If True (default), Manim will issue a warning if the font does not exist in the
971
+ (case-sensitive) list of fonts returned from `manimpango.list_fonts()`.
915
972
 
916
973
  Returns
917
974
  -------
@@ -1077,29 +1134,50 @@ class MarkupText(SVGMobject):
1077
1134
 
1078
1135
  """
1079
1136
 
1137
+ @staticmethod
1138
+ @functools.cache
1139
+ def font_list() -> list[str]:
1140
+ value: list[str] = manimpango.list_fonts()
1141
+ return value
1142
+
1080
1143
  def __init__(
1081
1144
  self,
1082
1145
  text: str,
1083
1146
  fill_opacity: float = 1,
1084
1147
  stroke_width: float = 0,
1085
- color: Color | None = None,
1148
+ color: ParsableManimColor | None = None,
1086
1149
  font_size: float = DEFAULT_FONT_SIZE,
1087
- line_spacing: int = -1,
1150
+ line_spacing: float = -1,
1088
1151
  font: str = "",
1089
1152
  slant: str = NORMAL,
1090
1153
  weight: str = NORMAL,
1091
1154
  justify: bool = False,
1092
- gradient: tuple = None,
1155
+ gradient: Iterable[ParsableManimColor] | None = None,
1093
1156
  tab_width: int = 4,
1094
- height: int = None,
1095
- width: int = None,
1157
+ height: int | None = None,
1158
+ width: int | None = None,
1096
1159
  should_center: bool = True,
1097
1160
  disable_ligatures: bool = False,
1098
- **kwargs,
1099
- ) -> None:
1100
-
1161
+ warn_missing_font: bool = True,
1162
+ **kwargs: Any,
1163
+ ):
1101
1164
  self.text = text
1102
- self.line_spacing = line_spacing
1165
+ self.line_spacing: float = line_spacing
1166
+ if font and warn_missing_font:
1167
+ fonts_list = Text.font_list()
1168
+ # handle special case of sans/sans-serif
1169
+ if font.lower() == "sans-serif":
1170
+ font = "sans"
1171
+ if font not in fonts_list:
1172
+ # check if the capitalized version is in the supported fonts
1173
+ if font.capitalize() in fonts_list:
1174
+ font = font.capitalize()
1175
+ elif font.lower() in fonts_list:
1176
+ font = font.lower()
1177
+ elif font.title() in fonts_list:
1178
+ font = font.title()
1179
+ else:
1180
+ logger.warning(f"Font {font} not in {fonts_list}.")
1103
1181
  self.font = font
1104
1182
  self._font_size = float(font_size)
1105
1183
  self.slant = slant
@@ -1131,8 +1209,8 @@ class MarkupText(SVGMobject):
1131
1209
  else:
1132
1210
  self.line_spacing = self._font_size + self._font_size * self.line_spacing
1133
1211
 
1134
- color = Color(color) if color else VMobject().color
1135
- file_name = self._text2svg(color)
1212
+ parsed_color: ManimColor = ManimColor(color) if color else VMobject().color
1213
+ file_name = self._text2svg(parsed_color)
1136
1214
 
1137
1215
  PangoUtils.remove_last_M(file_name)
1138
1216
  super().__init__(
@@ -1153,32 +1231,68 @@ class MarkupText(SVGMobject):
1153
1231
  if len(each.points) == 0:
1154
1232
  continue
1155
1233
  points = each.points
1156
- last = points[0]
1157
- each.clear_points()
1234
+ curve_start = points[0]
1235
+ assert len(curve_start) == self.dim, curve_start
1236
+ # Some of the glyphs in this text might not be closed,
1237
+ # so we close them by identifying when one curve ends
1238
+ # but it is not where the next curve starts.
1239
+ # It is more efficient to temporarily create a list
1240
+ # of points and add them one at a time, then turn them
1241
+ # into a numpy array at the end, rather than creating
1242
+ # new numpy arrays every time a point or fixing line
1243
+ # is added (which is O(n^2) for numpy arrays).
1244
+ closed_curve_points: list[Point3D] = []
1245
+ # OpenGL has points be part of quadratic Bezier curves;
1246
+ # Cairo uses cubic Bezier curves.
1247
+ if nppc == 3: # RendererType.OPENGL
1248
+
1249
+ def add_line_to(end: Point3D) -> None:
1250
+ nonlocal closed_curve_points
1251
+ start = closed_curve_points[-1]
1252
+ closed_curve_points += [
1253
+ start,
1254
+ (start + end) / 2,
1255
+ end,
1256
+ ]
1257
+
1258
+ else: # RendererType.CAIRO
1259
+
1260
+ def add_line_to(end: Point3D) -> None:
1261
+ nonlocal closed_curve_points
1262
+ start = closed_curve_points[-1]
1263
+ closed_curve_points += [
1264
+ start,
1265
+ (start + start + end) / 3,
1266
+ (start + end + end) / 3,
1267
+ end,
1268
+ ]
1269
+
1158
1270
  for index, point in enumerate(points):
1159
- each.append_points([point])
1271
+ closed_curve_points.append(point)
1160
1272
  if (
1161
1273
  index != len(points) - 1
1162
1274
  and (index + 1) % nppc == 0
1163
1275
  and any(point != points[index + 1])
1164
1276
  ):
1165
- each.add_line_to(last)
1166
- last = points[index + 1]
1167
- each.add_line_to(last)
1277
+ # Add straight line from last point on this curve to the
1278
+ # start point on the next curve.
1279
+ add_line_to(curve_start)
1280
+ curve_start = points[index + 1]
1281
+ # Make sure last curve is closed
1282
+ add_line_to(curve_start)
1283
+ each.points = np.array(closed_curve_points, ndmin=2)
1168
1284
 
1169
1285
  if self.gradient:
1170
1286
  self.set_color_by_gradient(*self.gradient)
1171
1287
  for col in colormap:
1172
1288
  self.chars[
1173
- col["start"]
1174
- - col["start_offset"] : col["end"]
1289
+ col["start"] - col["start_offset"] : col["end"]
1175
1290
  - col["start_offset"]
1176
1291
  - col["end_offset"]
1177
1292
  ].set_color(self._parse_color(col["color"]))
1178
1293
  for grad in gradientmap:
1179
1294
  self.chars[
1180
- grad["start"]
1181
- - grad["start_offset"] : grad["end"]
1295
+ grad["start"] - grad["start_offset"] : grad["end"]
1182
1296
  - grad["start_offset"]
1183
1297
  - grad["end_offset"]
1184
1298
  ].set_color_by_gradient(
@@ -1191,7 +1305,7 @@ class MarkupText(SVGMobject):
1191
1305
  self.initial_height = self.height
1192
1306
 
1193
1307
  @property
1194
- def font_size(self):
1308
+ def font_size(self) -> float:
1195
1309
  return (
1196
1310
  self.height
1197
1311
  / self.initial_height
@@ -1202,17 +1316,21 @@ class MarkupText(SVGMobject):
1202
1316
  )
1203
1317
 
1204
1318
  @font_size.setter
1205
- def font_size(self, font_val):
1319
+ def font_size(self, font_val: float) -> None:
1206
1320
  # TODO: use pango's font size scaling.
1207
1321
  if font_val <= 0:
1208
1322
  raise ValueError("font_size must be greater than 0.")
1209
1323
  else:
1210
1324
  self.scale(font_val / self.font_size)
1211
1325
 
1212
- def _text2hash(self, color: Color):
1326
+ def _text2hash(self, color: ParsableManimColor) -> str:
1213
1327
  """Generates ``sha256`` hash for file name."""
1214
1328
  settings = (
1215
- "MARKUPPANGO" + self.font + self.slant + self.weight + color.hex_l
1329
+ "MARKUPPANGO"
1330
+ + self.font
1331
+ + self.slant
1332
+ + self.weight
1333
+ + ManimColor(color).to_hex().lower()
1216
1334
  ) # to differentiate from classical Pango Text
1217
1335
  settings += str(self.line_spacing) + str(self._font_size)
1218
1336
  settings += str(self.disable_ligatures)
@@ -1222,23 +1340,25 @@ class MarkupText(SVGMobject):
1222
1340
  hasher.update(id_str.encode())
1223
1341
  return hasher.hexdigest()[:16]
1224
1342
 
1225
- def _text2svg(self, color: Color | None):
1343
+ def _text2svg(self, color: ParsableManimColor | None) -> str:
1226
1344
  """Convert the text to SVG using Pango."""
1345
+ color = ManimColor(color)
1227
1346
  size = self._font_size
1228
- line_spacing = self.line_spacing
1347
+ line_spacing: float = self.line_spacing
1229
1348
  size /= TEXT2SVG_ADJUSTMENT_FACTOR
1230
1349
  line_spacing /= TEXT2SVG_ADJUSTMENT_FACTOR
1231
1350
 
1232
1351
  dir_name = config.get_dir("text_dir")
1233
- if not dir_name.exists():
1352
+ if not dir_name.is_dir():
1234
1353
  dir_name.mkdir(parents=True)
1235
1354
  hash_name = self._text2hash(color)
1236
1355
  file_name = dir_name / (hash_name + ".svg")
1356
+
1237
1357
  if file_name.exists():
1238
- svg_file = str(file_name.resolve())
1358
+ svg_file: str = str(file_name.resolve())
1239
1359
  else:
1240
1360
  final_text = (
1241
- f'<span foreground="{color}">{self.text}</span>'
1361
+ f'<span foreground="{color.to_hex()}">{self.text}</span>'
1242
1362
  if color is not None
1243
1363
  else self.text
1244
1364
  )
@@ -1261,11 +1381,12 @@ class MarkupText(SVGMobject):
1261
1381
  )
1262
1382
  return svg_file
1263
1383
 
1264
- def _count_real_chars(self, s):
1384
+ def _count_real_chars(self, s: str) -> int:
1265
1385
  """Counts characters that will be displayed.
1266
1386
 
1267
1387
  This is needed for partial coloring or gradients, because space
1268
- counts to the text's `len`, but has no corresponding character."""
1388
+ counts to the text's `len`, but has no corresponding character.
1389
+ """
1269
1390
  count = 0
1270
1391
  level = 0
1271
1392
  # temporarily replace HTML entities by single char
@@ -1279,7 +1400,7 @@ class MarkupText(SVGMobject):
1279
1400
  count += 1
1280
1401
  return count
1281
1402
 
1282
- def _extract_gradient_tags(self):
1403
+ def _extract_gradient_tags(self) -> list[dict[str, Any]]:
1283
1404
  """Used to determine which parts (if any) of the string should be formatted
1284
1405
  with a gradient.
1285
1406
 
@@ -1290,7 +1411,7 @@ class MarkupText(SVGMobject):
1290
1411
  self.original_text,
1291
1412
  re.S,
1292
1413
  )
1293
- gradientmap = []
1414
+ gradientmap: list[dict[str, Any]] = []
1294
1415
  for tag in tags:
1295
1416
  start = self._count_real_chars(self.original_text[: tag.start(0)])
1296
1417
  end = start + self._count_real_chars(tag.group(5))
@@ -1308,17 +1429,19 @@ class MarkupText(SVGMobject):
1308
1429
  "end_offset": end_offset,
1309
1430
  },
1310
1431
  )
1311
- self.text = re.sub("<gradient[^>]+>(.+?)</gradient>", r"\1", self.text, 0, re.S)
1432
+ self.text = re.sub(
1433
+ "<gradient[^>]+>(.+?)</gradient>", r"\1", self.text, count=0, flags=re.S
1434
+ )
1312
1435
  return gradientmap
1313
1436
 
1314
- def _parse_color(self, col):
1437
+ def _parse_color(self, col: str) -> str:
1315
1438
  """Parse color given in ``<color>`` or ``<gradient>`` tags."""
1316
1439
  if re.match("#[0-9a-f]{6}", col):
1317
1440
  return col
1318
1441
  else:
1319
- return Colors[col.lower()].value
1442
+ return ManimColor(col).to_hex()
1320
1443
 
1321
- def _extract_color_tags(self):
1444
+ def _extract_color_tags(self) -> list[dict[str, Any]]:
1322
1445
  """Used to determine which parts (if any) of the string should be formatted
1323
1446
  with a custom color.
1324
1447
 
@@ -1333,7 +1456,7 @@ class MarkupText(SVGMobject):
1333
1456
  re.S,
1334
1457
  )
1335
1458
 
1336
- colormap = []
1459
+ colormap: list[dict[str, Any]] = []
1337
1460
  for tag in tags:
1338
1461
  start = self._count_real_chars(self.original_text[: tag.start(0)])
1339
1462
  end = start + self._count_real_chars(tag.group(4))
@@ -1350,15 +1473,17 @@ class MarkupText(SVGMobject):
1350
1473
  "end_offset": end_offset,
1351
1474
  },
1352
1475
  )
1353
- self.text = re.sub("<color[^>]+>(.+?)</color>", r"\1", self.text, 0, re.S)
1476
+ self.text = re.sub(
1477
+ "<color[^>]+>(.+?)</color>", r"\1", self.text, count=0, flags=re.S
1478
+ )
1354
1479
  return colormap
1355
1480
 
1356
- def __repr__(self):
1481
+ def __repr__(self) -> str:
1357
1482
  return f"MarkupText({repr(self.original_text)})"
1358
1483
 
1359
1484
 
1360
1485
  @contextmanager
1361
- def register_font(font_file: str | Path):
1486
+ def register_font(font_file: str | Path) -> Iterator[None]:
1362
1487
  """Temporarily add a font file to Pango's search path.
1363
1488
 
1364
1489
  This searches for the font_file at various places. The order it searches it described below.
@@ -1396,7 +1521,6 @@ def register_font(font_file: str | Path):
1396
1521
  This method is available for macOS for ``ManimPango>=v0.2.3``. Using this
1397
1522
  method with previous releases will raise an :class:`AttributeError` on macOS.
1398
1523
  """
1399
-
1400
1524
  input_folder = Path(config.input_file).parent.resolve()
1401
1525
  possible_paths = [
1402
1526
  Path(font_file),
@@ -1411,7 +1535,7 @@ def register_font(font_file: str | Path):
1411
1535
  logger.debug("Found file at %s", file_path.absolute())
1412
1536
  break
1413
1537
  else:
1414
- error = f"Can't find {font_file}." f"Tried these : {possible_paths}"
1538
+ error = f"Can't find {font_file}. Checked paths: {possible_paths}"
1415
1539
  raise FileNotFoundError(error)
1416
1540
 
1417
1541
  try: