Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
d4a534d
Add font size constants to constants.py
rambip Apr 16, 2026
3e8e21e
Refactor text scaling and font size constants
rambip Apr 16, 2026
9916d45
Refactor font size management in tex_mobject.py
rambip Apr 16, 2026
de8524c
Fix DEFAULT_FONT_SIZE_IN_WOLRD_SPACE definition
rambip Apr 16, 2026
394d52a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 16, 2026
24851f2
Refactor font size handling to use initial_font_size
rambip Apr 16, 2026
bd60688
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 16, 2026
bdc693c
Update imports in tex_mobject.py for constants
rambip Apr 19, 2026
607d8d9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 19, 2026
28b3835
Refactor constants import in tex_mobject.py
rambip Apr 19, 2026
f1dbcc7
Import PT_OVER_PX constant in text_mobject.py
rambip Apr 19, 2026
378978e
Rename TEX_DEFAULT_FONTSIZE_PT to TEX_DEFAULT_FONT_SIZE_PT
rambip Apr 19, 2026
8401941
test: add em-dash width checks for text and math text
Copilot Apr 19, 2026
ef13da6
fix: rename text font-size internals and add world-space aliases
Copilot Apr 19, 2026
0c111b3
test: use world-space constant alias in em-dash tests
Copilot Apr 19, 2026
5a29702
refactor: make world-space font-size constant name canonical
Copilot Apr 19, 2026
511cd8d
fix: stop scaling tex by PT_OVER_PX and relax em-dash precision
Copilot Apr 19, 2026
7474bd7
test: relax text em-dash width precision
Copilot Apr 19, 2026
d4202f6
test: simplify em-dash test names
Copilot Apr 19, 2026
6863275
Merge pull request #1 from rambip/copilot/add-font-size-calculation-t…
rambip Apr 19, 2026
4b2277f
Change default font size in world space to 0.5
rambip Apr 19, 2026
102d45d
Fix scaling factor calculation for font size
rambip Apr 19, 2026
1883cc3
Fix scaling calculation for font size
rambip Apr 19, 2026
5963389
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 19, 2026
e8c7d7e
Change precision of width assertion in test
rambip Apr 20, 2026
a2210cb
Update precision in em dash width test
rambip Apr 20, 2026
04a0508
Update assertion for em dash width test
rambip Apr 20, 2026
d790a68
Modify em dash width test for Liberation Sans font
rambip Apr 20, 2026
aba7b5b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 20, 2026
12b7b06
Move PT_OVER_PX to text_mobject.py (backend-specific)
rambip Apr 20, 2026
0a9bfca
Add TEX_SVG_UNITS_PER_PT constant with 72 DPI documentation
rambip Apr 20, 2026
a876c23
Add PANGO_SVG_UNITS_PER_PT constant (96 DPI), remove import from cons…
rambip Apr 20, 2026
df03431
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 20, 2026
99bc87e
Update text_mobject.py
rambip Apr 20, 2026
ac681f4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 20, 2026
96d53f0
Refactor em dash width test to check available fonts
rambip Apr 20, 2026
c3cc502
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 20, 2026
a43656d
Merge branch 'main' into main
rambip May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion manim/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@
"DEFAULT_POINT_DENSITY_1D",
"DEFAULT_STROKE_WIDTH",
"DEFAULT_FONT_SIZE",
"DEFAULT_FONT_SIZE_IN_WORLD_SPACE",
"DEFAULT_FONTSIZE_IN_WORLD_SPACE",
"DEFAULT_FONT_SIZE_IN_WOLRD_SPACE",
"SCALE_FACTOR_PER_FONT_POINT",
"PI",
"TAU",
Expand Down Expand Up @@ -177,12 +180,25 @@
DEFAULT_POINTWISE_FUNCTION_RUN_TIME = 3.0
DEFAULT_WAIT_TIME = 1.0

# Font calculation
DEFAULT_FONT_SIZE_IN_WORLD_SPACE = 0.5
"""Length occupied by an 'EM' character in manim space, when manim default font size is used.
An example of an 'EM' character is the em dash: '—'.
The chosen value is an arbitrary convention."""
DEFAULT_FONT_SIZE_IN_WOLRD_SPACE = DEFAULT_FONT_SIZE_IN_WORLD_SPACE
DEFAULT_FONTSIZE_IN_WORLD_SPACE = DEFAULT_FONT_SIZE_IN_WORLD_SPACE

# Misc
DEFAULT_POINT_DENSITY_2D = 25
DEFAULT_POINT_DENSITY_1D = 10
DEFAULT_STROKE_WIDTH = 4
DEFAULT_FONT_SIZE = 48
SCALE_FACTOR_PER_FONT_POINT = 1 / 960
SCALE_FACTOR_PER_FONT_POINT = ( # legacy, not used in code anymore
DEFAULT_FONT_SIZE_IN_WORLD_SPACE
/ 10 # LaTeX default font size in pixels
/ DEFAULT_FONT_SIZE
)
"""Downscale factor from LaTeX pixel to world space, divided by DEFAULT_FONT_SIZE"""

# Mathematical constants
PI = np.pi
Expand Down
22 changes: 17 additions & 5 deletions manim/mobject/text/tex_mobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@
from ..opengl.opengl_compatibility import ConvertToOpenGL

MATHTEX_SUBSTRING = "substring"
TEX_SVG_UNITS_PER_PT = 1
"""Scale factor from TeX SVG output to point units.
TeX outputs 1 svg unit per point (72 DPI)."""
TEX_DEFAULT_FONT_SIZE_PT = 10
"""The fontsize used by default by tex: 10pt.
This means that one 'EM' character like '—' will be 13.333 svg units, since 1pt=4/3 px"""


class SingleStringMathTex(SVGMobject):
Expand Down Expand Up @@ -70,7 +76,7 @@ def __init__(
if color is None:
color = VMobject().color

self._font_size = font_size
self.initial_font_size = font_size
self.organize_left_to_right = organize_left_to_right
self.tex_environment = tex_environment
if tex_template is None:
Expand All @@ -97,12 +103,18 @@ def __init__(
)
self.init_colors()

if height is None:
self.scale(
font_size
/ DEFAULT_FONT_SIZE
/ TEX_DEFAULT_FONT_SIZE_PT # convert latex svg output to "fontsize" or "em" units
* DEFAULT_FONTSIZE_IN_WORLD_SPACE # then to worldspace
)

# used for scaling via font_size.setter
# we use the initial size in world space
self.initial_height = self.height

if height is None:
self.font_size = self._font_size

if self.organize_left_to_right:
self._organize_submobjects_left_to_right()

Expand All @@ -112,7 +124,7 @@ def __repr__(self) -> str:
@property
def font_size(self) -> float:
"""The font size of the tex mobject."""
return self.height / self.initial_height / SCALE_FACTOR_PER_FONT_POINT
return self.height / self.initial_height * self.initial_font_size

@font_size.setter
def font_size(self, font_val: float) -> None:
Expand Down
75 changes: 39 additions & 36 deletions manim/mobject/text/text_mobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,13 @@ def construct(self):

from manim.typing import Point3D

TEXT_MOB_SCALE_FACTOR = 0.05
DEFAULT_LINE_SPACING_SCALE = 0.3
TEXT2SVG_ADJUSTMENT_FACTOR = 4.8
PANGO_SVG_UNITS_PER_PT = 4 / 3
"""Scale factor from Pango SVG output to point units.
Pango outputs 4/3 svg units per point (72 DPI)."""
TEXT_FONT_SIZE_PT = 10
"""The font size we use to render the unscaled text in the SVG.
Note that the typical EM dash (—) will be 13.333 svg units, since 1pt = 4/3 px"""

__all__ = ["Text", "Paragraph", "MarkupText", "register_font"]

Expand Down Expand Up @@ -466,7 +470,7 @@ def __init__(
else:
logger.warning(f"Font {font} not in {fonts_list}.")
self.font = font
self._font_size = float(font_size)
self.initial_font_size = float(font_size)
# needs to be a float or else size is inflated when font_size = 24
# (unknown cause)
self.slant = slant
Expand Down Expand Up @@ -508,10 +512,13 @@ def __init__(
self.text = text_without_tabs
if self.line_spacing == -1:
self.line_spacing = (
self._font_size + self._font_size * DEFAULT_LINE_SPACING_SCALE
self.initial_font_size
+ self.initial_font_size * DEFAULT_LINE_SPACING_SCALE
)
else:
self.line_spacing = self._font_size + self._font_size * self.line_spacing
self.line_spacing = (
self.initial_font_size + self.initial_font_size * self.line_spacing
)

parsed_color: ManimColor = ManimColor(color) if color else VMobject().color
file_name = self._text2svg(parsed_color.to_hex())
Expand Down Expand Up @@ -590,22 +597,20 @@ def add_line_to(end: Point3D) -> None:
each.points = np.array(closed_curve_points, ndmin=2)
# anti-aliasing
if height is None and width is None:
self.scale(TEXT_MOB_SCALE_FACTOR)
self.scale(
1
/ PANGO_SVG_UNITS_PER_PT # convert svg output to "pt" units
/ TEXT_FONT_SIZE_PT # then to "fontsize" or "EM" units
* DEFAULT_FONT_SIZE_IN_WORLD_SPACE # then to world space
)
self.initial_height = self.height

def __repr__(self) -> str:
return f"Text({repr(self.original_text)})"

@property
def font_size(self) -> float:
return (
self.height
/ self.initial_height
/ TEXT_MOB_SCALE_FACTOR
* 2.4
* self._font_size
/ DEFAULT_FONT_SIZE
)
return self.height / self.initial_height * self.initial_font_size

@font_size.setter
def font_size(self, font_val: float) -> None:
Expand Down Expand Up @@ -657,7 +662,7 @@ def _text2hash(self, color: ParsableManimColor) -> str:
"PANGO" + self.font + self.slant + self.weight + str(color)
) # to differentiate Text and CairoText
settings += str(self.t2f) + str(self.t2s) + str(self.t2w) + str(self.t2c)
settings += str(self.line_spacing) + str(self._font_size)
settings += str(self.line_spacing) + str(self.initial_font_size)
settings += str(self.disable_ligatures)
settings += str(self.gradient)
id_str = self.text + settings
Expand Down Expand Up @@ -798,10 +803,8 @@ def _text2settings(self, color: ParsableManimColor) -> list[TextSetting]:

def _text2svg(self, color: ParsableManimColor) -> str:
"""Convert the text to SVG using Pango."""
size = self._font_size
line_spacing = self.line_spacing
size /= TEXT2SVG_ADJUSTMENT_FACTOR
line_spacing /= TEXT2SVG_ADJUSTMENT_FACTOR
size = TEXT_FONT_SIZE_PT * self.initial_font_size / DEFAULT_FONT_SIZE
line_spacing = TEXT_FONT_SIZE_PT * self.line_spacing / DEFAULT_FONT_SIZE

dir_name = config.get_dir("text_dir")
dir_name.mkdir(parents=True, exist_ok=True)
Expand Down Expand Up @@ -1181,7 +1184,7 @@ def __init__(
else:
logger.warning(f"Font {font} not in {fonts_list}.")
self.font = font
self._font_size = float(font_size)
self.initial_font_size = float(font_size)
self.slant = slant
self.weight = weight
self.gradient = gradient
Expand All @@ -1206,10 +1209,13 @@ def __init__(

if self.line_spacing == -1:
self.line_spacing = (
self._font_size + self._font_size * DEFAULT_LINE_SPACING_SCALE
self.initial_font_size
+ self.initial_font_size * DEFAULT_LINE_SPACING_SCALE
)
else:
self.line_spacing = self._font_size + self._font_size * self.line_spacing
self.line_spacing = (
self.initial_font_size + self.initial_font_size * self.line_spacing
)

parsed_color: ManimColor = ManimColor(color) if color else VMobject().color
file_name = self._text2svg(parsed_color)
Expand Down Expand Up @@ -1302,20 +1308,18 @@ def add_line_to(end: Point3D) -> None:
)
# anti-aliasing
if height is None and width is None:
self.scale(TEXT_MOB_SCALE_FACTOR)
self.scale(
1
/ PANGO_SVG_UNITS_PER_PT # convert svg output to "pt" units
/ TEXT_FONT_SIZE_PT # then to "fontsize" or "EM" units
* DEFAULT_FONT_SIZE_IN_WORLD_SPACE # then to world space
)

self.initial_height = self.height

@property
def font_size(self) -> float:
return (
self.height
/ self.initial_height
/ TEXT_MOB_SCALE_FACTOR
* 2.4
* self._font_size
/ DEFAULT_FONT_SIZE
)
return self.height / self.initial_height * self.initial_font_size

@font_size.setter
def font_size(self, font_val: float) -> None:
Expand All @@ -1334,7 +1338,7 @@ def _text2hash(self, color: ParsableManimColor) -> str:
+ self.weight
+ ManimColor(color).to_hex().lower()
) # to differentiate from classical Pango Text
settings += str(self.line_spacing) + str(self._font_size)
settings += str(self.line_spacing) + str(self.initial_font_size)
settings += str(self.disable_ligatures)
settings += str(self.justify)
id_str = self.text + settings
Expand All @@ -1345,10 +1349,9 @@ def _text2hash(self, color: ParsableManimColor) -> str:
def _text2svg(self, color: ParsableManimColor | None) -> str:
"""Convert the text to SVG using Pango."""
color = ManimColor(color)
size = self._font_size
line_spacing: float = self.line_spacing
size /= TEXT2SVG_ADJUSTMENT_FACTOR
line_spacing /= TEXT2SVG_ADJUSTMENT_FACTOR
# scale down so that manim font size becomes a specific target size in pt for pango
size = TEXT_FONT_SIZE_PT * self.initial_font_size / DEFAULT_FONT_SIZE
line_spacing = TEXT_FONT_SIZE_PT * self.line_spacing / DEFAULT_FONT_SIZE

dir_name = config.get_dir("text_dir")
dir_name.mkdir(parents=True, exist_ok=True)
Expand Down
6 changes: 6 additions & 0 deletions tests/module/mobject/text/test_texmobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pytest

from manim import MathTex, SingleStringMathTex, Tex, TexTemplate, tempconfig
from manim.constants import DEFAULT_FONT_SIZE_IN_WORLD_SPACE


def test_MathTex(config):
Expand Down Expand Up @@ -256,6 +257,11 @@ def test_changing_font_size():
assert num.height == Tex("0", font_size=48).height


def test_mathtex_em_dash_width_default_font_size():
em_dash = MathTex(r"\text{—}")
assert abs(em_dash.width - DEFAULT_FONT_SIZE_IN_WORLD_SPACE) < 0.005


def test_log_error_context(capsys):
"""Test that the environment context of an error is correctly logged if it exists"""
invalid_tex = r"""
Expand Down
15 changes: 15 additions & 0 deletions tests/module/mobject/text/test_text_mobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from contextlib import redirect_stdout
from io import StringIO

from manim.constants import DEFAULT_FONT_SIZE_IN_WORLD_SPACE
from manim.mobject.text.text_mobject import MarkupText, Text


Expand Down Expand Up @@ -32,3 +33,17 @@ def warning_printed(font: str, **kwargs) -> bool:

# check random string (should be warning)
assert warning_printed("Manim!" * 3, warn_missing_font=True)


def test_em_dash_width_default_font_size():
from manimpango import list_fonts

available = list_fonts()
# Fonts that render em dash at full font size width
candidates = ["Arial", "Liberation Sans"]

font = next((f for f in candidates if f in available), None)
assert font is not None, f"No suitable font found. Available: {available}"

text_em_dash = Text("—", font=font)
assert abs(text_em_dash.width - DEFAULT_FONT_SIZE_IN_WORLD_SPACE) < 0.01
Loading