from manim import *
from manim.typing import Point3D, Vector3DLike
import numpy as np
import platform
from typing import Any, Union
DEFAULT_CJK_FONT = (
"SimSun" if platform.system() == "Windows" else "Noto Serif CJK SC"
)
[docs]
class ChineseMathTex(MathTex):
r"""A :class:`~manim.mobject.text.tex_mobject.MathTex` subclass that supports Chinese characters.
Automatically wraps Chinese characters in ``\text{}`` and configures
``xelatex`` with the ``xeCJK`` package so that CJK fonts are rendered
correctly.
.. inheritance-diagram:: manim_extensions.mobjects.ChineseMathTex
:parts: 1
Parameters
----------
*texts : str
LaTeX text strings to render.
font : str, optional
Name of the Chinese font to use. Defaults to ``"SimSun"``.
tex_to_color_map : dict, optional
Mapping from text substrings to colours. Defaults to ``{}``.
**kwargs
Additional keyword arguments forwarded to :class:`~manim.mobject.text.tex_mobject.MathTex`.
Examples
--------
.. manim:: ChineseMathTexDocExample
:save_last_frame:
from manim import *
from manim_extensions import ChineseMathTex
class ChineseMathTexDocExample(Scene):
def construct(self):
formula = ChineseMathTex(
r"\frac{1}{2} + \text{hello} = x",
tex_to_color_map={r"\text{hello}": RED},
)
self.add(formula)
"""
[docs]
def __init__(
self,
*texts: str,
font: str = DEFAULT_CJK_FONT,
tex_to_color_map: dict = {},
**kwargs,
) -> None:
tex_template = TexTemplate(tex_compiler="xelatex", output_format=".xdv")
tex_template.add_to_preamble(r"\usepackage{amsmath}")
tex_template.add_to_preamble(r"\usepackage{xeCJK}")
tex_template.add_to_preamble(rf"\setCJKmainfont{{{font}}}")
combined_chinesetext = []
for text in texts:
chinesetext = ""
for i in range(len(text)):
if (
("\u4e00" <= text[i] <= "\u9fff")
or ("\u3000" <= text[i] <= "\u303f")
or ("\uff00" <= text[i] <= "\uffef")
):
chinesetext += rf"\text{{{text[i]}}}"
else:
chinesetext += text[i]
combined_chinesetext.append(chinesetext)
new_dict = {}
for key in tex_to_color_map.keys():
new_key = ""
for char in key:
if (
("\u4e00" <= char <= "\u9fff")
or ("\u3000" <= char <= "\u303f")
or ("\uff00" <= char <= "\uffef")
):
new_key += rf"\text{{{char}}}"
else:
new_key += char
new_dict[new_key] = tex_to_color_map[key]
super().__init__(
*combined_chinesetext,
tex_template=tex_template,
tex_to_color_map=new_dict,
**kwargs,
)
[docs]
class LabelDot(VGroup):
"""A dot with a :class:`~manim.mobject.text.tex_mobject.MathTex` label.
Creates a :class:`~manim.mobject.geometry.Dot` at the given position and places a
:class:`~manim.mobject.text.tex_mobject.MathTex` label next to it.
.. inheritance-diagram:: manim_extensions.mobjects.LabelDot
:parts: 1
Parameters
----------
dot_label : str
Text content of the label.
dot_pos : numpy.ndarray
Position of the dot.
label_pos : numpy.ndarray, optional
Direction of the label relative to the dot. Defaults to ``DOWN``.
buff : float, optional
Buffer between the label and the dot. Defaults to ``0.1``.
**kwargs
Additional keyword arguments forwarded to :class:`~manim.mobject.types.vectorized_mobject.VGroup`.
Attributes
----------
dot : :class:`~manim.mobject.geometry.Dot`
The underlying dot mobject.
dot_pos : numpy.ndarray
The position of the dot.
Examples
--------
.. manim:: LabelDotDocExample
:save_last_frame:
from manim_extensions import LabelDot
class LabelDotDocExample(Scene):
def construct(self):
dot = LabelDot("A", [0, 0, 0], label_pos=UP, buff=0.2)
self.add(dot)
"""
[docs]
def __init__(
self,
dot_label: str,
dot_pos: np.ndarray,
label_pos: np.ndarray = DOWN,
buff: float = 0.1,
**kwargs,
) -> None:
super().__init__(**kwargs)
dot = Dot().move_to(dot_pos)
label = MathTex(dot_label).next_to(dot, label_pos, buff=buff)
self.add(dot, label)
self.dot = dot
self.dot_pos = dot_pos
[docs]
def get_center(self) -> Point3D:
"""Return the center of the underlying dot.
Examples
--------
"""
return self.dot.get_center()
[docs]
def get_boundary_point(self, direction: Vector3DLike) -> Point3D:
"""Return the center of the underlying dot (boundary approximation).
Examples
--------
"""
return self.dot.get_center()
[docs]
class MathTexLine(VGroup):
"""A line segment paired with a :class:`~manim.mobject.text.tex_mobject.MathTex` formula.
Creates a :class:`~manim.mobject.geometry.Line` and places a :class:`~manim.mobject.text.tex_mobject.MathTex`
formula next to it in the specified direction.
.. inheritance-diagram:: manim_extensions.mobjects.MathTexLine
:parts: 1
Parameters
----------
formula : :class:`~manim.mobject.text.tex_mobject.MathTex`
The formula to place beside the line.
direction : numpy.ndarray, optional
Direction of the formula relative to the line. Defaults to ``UP``.
buff : float, optional
Buffer between the formula and the line. Defaults to ``0.5``.
**kwargs
Additional keyword arguments forwarded to :class:`~manim.mobject.geometry.Line`.
Examples
--------
.. manim:: MathTexLineDocExample
:save_last_frame:
from manim_extensions import MathTexLine
class MathTexLineDocExample(Scene):
def construct(self):
line = MathTexLine(MathTex("y = x"), direction=UP, color=BLUE)
self.add(line)
"""
[docs]
def __init__(
self,
formula: MathTex,
direction: np.ndarray = UP,
buff: float = 0.5,
**kwargs,
) -> None:
super().__init__()
line = Line(**kwargs)
tex = formula.next_to(line, direction, buff=buff)
self.add(line, tex)
[docs]
class MathTexBrace(VGroup):
r"""A brace with a :class:`~manim.mobject.text.tex_mobject.MathTex` formula.
Creates a :class:`~manim.mobject.geometry.Brace` around a target mobject and places a
:class:`~manim.mobject.text.tex_mobject.MathTex` formula next to the brace.
.. inheritance-diagram:: manim_extensions.mobjects.MathTexBrace
:parts: 1
Parameters
----------
target : :class:`~manim.mobject.mobject.Mobject`
The mobject to be braced (e.g. a line, rectangle, etc.).
formula : :class:`~manim.mobject.text.tex_mobject.MathTex`
The formula to place beside the brace.
direction : numpy.ndarray, optional
Direction of the brace and formula relative to the target.
Defaults to ``UP``.
buff : float, optional
Buffer between the formula and the brace. Defaults to ``0.5``.
**kwargs
Additional keyword arguments forwarded to :class:`~manim.mobject.geometry.Brace`.
Examples
--------
.. manim:: MathTexBraceDocExample
:save_last_frame:
from manim_extensions import MathTexBrace
class MathTexBraceDocExample(Scene):
def construct(self):
line = Line(LEFT * 2, RIGHT * 2)
brace = MathTexBrace(line, MathTex(r"\Delta x"), direction=UP)
self.add(line, brace)
"""
[docs]
def __init__(
self,
target,
formula: MathTex,
direction: np.ndarray = UP,
buff: float = 0.5,
**kwargs,
) -> None:
super().__init__()
brace = Brace(target, direction=direction, **kwargs)
tex = formula.next_to(brace, direction, buff=buff)
self.add(brace, tex)
[docs]
class MathTexDoublearrow(VGroup):
r"""A double arrow with a :class:`~manim.mobject.text.tex_mobject.MathTex` formula.
Creates a :class:`~manim.mobject.geometry.DoubleArrow` and places a :class:`~manim.mobject.text.tex_mobject.MathTex`
formula next to it in the specified direction.
.. inheritance-diagram:: manim_extensions.mobjects.MathTexDoublearrow
:parts: 1
Parameters
----------
formula : :class:`~manim.mobject.text.tex_mobject.MathTex`
The formula to place beside the double arrow.
direction : numpy.ndarray, optional
Direction of the formula relative to the double arrow.
Defaults to ``UP``.
buff : float, optional
Buffer between the formula and the double arrow. Defaults to ``0.5``.
**kwargs
Additional keyword arguments forwarded to :class:`~manim.mobject.geometry.DoubleArrow`.
Examples
--------
.. manim:: MathTexDoublearrowDocExample
:save_last_frame:
from manim_extensions import MathTexDoublearrow
class MathTexDoublearrowDocExample(Scene):
def construct(self):
arrow = MathTexDoublearrow(MathTex(r"\Leftrightarrow"), direction=UP)
self.add(arrow)
"""
[docs]
def __init__(
self,
formula: MathTex,
direction: np.ndarray = UP,
buff: float = 0.5,
**kwargs,
) -> None:
super().__init__()
doublearrow = DoubleArrow(**kwargs)
tex = formula.next_to(doublearrow, direction, buff=buff)
self.add(doublearrow, tex)
[docs]
class PerpendicularLine(Line):
"""A perpendicular line segment from a point to a given line.
Computes the foot of the perpendicular from *point* onto *line* and
creates a :class:`~manim.mobject.geometry.Line` from *point* to that foot.
.. inheritance-diagram:: manim_extensions.mobjects.PerpendicularLine
:parts: 1
Parameters
----------
point : Union[numpy.ndarray, tuple, list, :class:`~manim.mobject.mobject.Mobject`]
The point from which the perpendicular is dropped. If an
:class:`~manim.mobject.mobject.Mobject` is given, its centre is used.
line : :class:`~manim.mobject.geometry.Line`
The target line.
**kwargs
Additional keyword arguments forwarded to :class:`~manim.mobject.geometry.Line`.
Attributes
----------
point : numpy.ndarray
The 3‑D point from which the perpendicular is drawn.
target_line : :class:`~manim.mobject.geometry.Line`
The line onto which the perpendicular is dropped.
foot : numpy.ndarray
The foot of the perpendicular on *target_line*.
Examples
--------
.. manim:: PerpendicularLineDocExample
:save_last_frame:
from manim_extensions import PerpendicularLine
class PerpendicularLineDocExample(Scene):
def construct(self):
base = Line(LEFT * 3, RIGHT * 3)
perp = PerpendicularLine(UP * 1.5, base, color=YELLOW)
self.add(base, perp)
"""
[docs]
def __init__(
self,
point: Union[np.ndarray, tuple, list, Mobject],
line: Line,
**kwargs: Any,
) -> None:
if isinstance(point, Mobject):
self.point = point.get_center()
else:
self.point = np.array(point)
self.target_line = line
self.foot = self._compute_foot()
super().__init__(self.point, self.foot, **kwargs)
def _compute_foot(self) -> np.ndarray:
a = self.target_line.get_start()
b = self.target_line.get_end()
ab = b - a
ap = self.point - a
ab_dot_ab = np.dot(ab, ab)
if ab_dot_ab < 1e-12:
return a
t = np.dot(ap, ab) / ab_dot_ab
return a + t * ab
[docs]
class ExtendedLine(Line):
"""A line segment extended at both ends.
Takes an existing :class:`~manim.mobject.geometry.Line` and extends it by
*extend_distance* along its original direction on both sides.
The style of the original line is preserved.
.. inheritance-diagram:: manim_extensions.mobjects.ExtendedLine
:parts: 1
Parameters
----------
line : :class:`~manim.mobject.geometry.Line`
The original line segment to extend.
extend_distance : float
Distance to extend at each end.
**kwargs
Additional keyword arguments forwarded to :class:`~manim.mobject.geometry.Line`.
Examples
--------
.. manim:: ExtendedLineDocExample
:save_last_frame:
from manim_extensions import ExtendedLine
class ExtendedLineDocExample(Scene):
def construct(self):
base = Line(LEFT, RIGHT, color=BLUE)
extended = ExtendedLine(base, extend_distance=1.0, color=RED)
self.add(base, extended)
"""
[docs]
def __init__(self, line: Line, extend_distance: float, **kwargs) -> None:
start_point = line.get_start()
end_point = line.get_end()
direction_vector = end_point - start_point
vector_length = np.linalg.norm(direction_vector)
if vector_length < 1e-8:
super().__init__(start_point, end_point, **kwargs)
else:
unit_direction_vector = direction_vector / vector_length
new_start_point = start_point - extend_distance * unit_direction_vector
new_end_point = end_point + extend_distance * unit_direction_vector
super().__init__(new_start_point, new_end_point, **kwargs)
self.match_style(line)
[docs]
class PerpendicularSign(VGroup):
"""A right‑angle (perpendicular) sign.
Draws a small L‑shaped corner at the intersection of two lines to
indicate that they are perpendicular. The sign consists of two short
line segments.
.. inheritance-diagram:: manim_extensions.mobjects.PerpendicularSign
:parts: 1
Parameters
----------
line1 : :class:`~manim.mobject.geometry.Line`
The first line.
line2 : :class:`~manim.mobject.geometry.Line`
The second line.
length : float, optional
Length of each leg of the corner. Defaults to ``0.25``.
corner_direction : Union[numpy.ndarray, tuple, list, None], optional
A direction vector that selects on which side of the intersection
the corner is drawn. If ``None`` (the default), the side that
points toward the nearer endpoints of the two lines is chosen
automatically.
**kwargs
Additional keyword arguments forwarded to :class:`~manim.mobject.types.vectorized_mobject.VGroup`.
Attributes
----------
intersection : numpy.ndarray
The 3‑D intersection point of the two lines. If the lines are
parallel this attribute is not set.
Examples
--------
.. manim:: PerpendicularSignDocExample
:save_last_frame:
from manim_extensions import PerpendicularLine, PerpendicularSign
class PerpendicularSignDocExample(Scene):
def construct(self):
base = Line(LEFT * 3, RIGHT * 3)
perp = PerpendicularLine(UP * 1.5, base, color=YELLOW)
sign = PerpendicularSign(base, perp, length=0.25, color=WHITE)
self.add(base, perp, sign)
"""
[docs]
def __init__(
self,
line1: Line,
line2: Line,
length: float = 0.25,
corner_direction: Union[np.ndarray, tuple, list, None] = None,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
# Compute the intersection of the two lines
intersection = self._compute_intersection(line1, line2)
if intersection is None:
return
# Get unit direction vectors on both sides of each line
dirs1 = self._get_both_directions(line1, intersection)
dirs2 = self._get_both_directions(line2, intersection)
# Select the best pair of directions
d1, d2 = self._select_directions(
dirs1, dirs2, corner_direction
)
# Three vertices of the corner
corner1 = intersection + length * d1
corner2 = intersection + length * d2
# Inner vertex: move along the sum of the two directions
inner = intersection + length * d1 + length * d2
# Two line segments that form the corner
leg1 = Line(corner1, inner, **kwargs)
leg2 = Line(corner2, inner, **kwargs)
self.add(leg1, leg2)
self.intersection = intersection
def _compute_intersection(
self, line1: Line, line2: Line
) -> Union[np.ndarray, None]:
a1 = line1.get_start()
b1 = line1.get_end()
a2 = line2.get_start()
b2 = line2.get_end()
d1 = b1 - a1
d2 = b2 - a2
# Use full 3D vectors for cross product to avoid NumPy 2.0 deprecation
cross = np.cross(d1, d2)
if abs(cross[2]) < 1e-12:
return None
t = np.cross(a2 - a1, d2)[2] / cross[2]
return a1 + t * d1
def _get_both_directions(
self, line: Line, point: np.ndarray
) -> tuple[np.ndarray, np.ndarray]:
"""Return the two unit direction vectors from *point* toward the line's endpoints."""
start = line.get_start()
end = line.get_end()
d1 = start - point
d2 = end - point
d1_len = np.linalg.norm(d1)
d2_len = np.linalg.norm(d2)
if d1_len > 1e-12:
d1 = d1 / d1_len
else:
d1 = np.array([0.0, 0.0, 0.0])
if d2_len > 1e-12:
d2 = d2 / d2_len
else:
d2 = np.array([0.0, 0.0, 0.0])
return d1, d2
def _select_directions(
self,
dirs1: tuple[np.ndarray, np.ndarray],
dirs2: tuple[np.ndarray, np.ndarray],
corner_direction: Union[np.ndarray, tuple, list, None],
) -> tuple[np.ndarray, np.ndarray]:
"""Select the best pair of directions based on *corner_direction*."""
candidates = []
for d1 in dirs1:
for d2 in dirs2:
inner_dir = d1 + d2
norm = np.linalg.norm(inner_dir)
if norm < 1e-12:
continue
candidates.append((d1, d2, inner_dir / norm))
if not candidates:
return dirs1[0], dirs2[0]
if corner_direction is None:
# Default: choose the pair pointing toward the nearer endpoints
# (the pair with the largest norm of inner_dir)
best = max(candidates, key=lambda c: np.linalg.norm(c[0] + c[1]))
return best[0], best[1]
corner_direction = np.array(corner_direction)
corner_direction = corner_direction / np.linalg.norm(corner_direction)
# Select the pair with the largest dot product with corner_direction
best = max(
candidates,
key=lambda c: np.dot(c[2], corner_direction),
)
return best[0], best[1]