3b1b-manim/manimlib/mobject/numbers.py

221 lines
7.5 KiB
Python
Raw Normal View History

from __future__ import annotations
from functools import lru_cache
2022-04-15 00:55:02 +08:00
import numpy as np
2022-04-12 19:19:59 +08:00
from manimlib.constants import DOWN, LEFT, RIGHT, UP
2022-12-15 20:33:58 -08:00
from manimlib.constants import WHITE
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.svg.text_mobject import Text
from manimlib.mobject.types.vectorized_mobject import VMobject
2024-08-20 10:00:07 -05:00
from manimlib.utils.paths import straight_path
from manimlib.utils.bezier import interpolate
2022-04-12 19:19:59 +08:00
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import TypeVar
from manimlib.typing import ManimColor, Vect3, Self
2022-04-12 19:19:59 +08:00
T = TypeVar("T", bound=VMobject)
@lru_cache()
def char_to_cahced_mob(char: str, **text_config):
return Text(char, **text_config)
class DecimalNumber(VMobject):
2022-12-15 20:33:58 -08:00
def __init__(
self,
number: float | complex = 0,
color: ManimColor = WHITE,
stroke_width: float = 0,
fill_opacity: float = 1.0,
2024-08-16 15:21:29 -05:00
fill_border_width: float = 0.5,
2022-12-15 20:33:58 -08:00
num_decimal_places: int = 2,
include_sign: bool = False,
group_with_commas: bool = True,
digit_buff_per_font_unit: float = 0.001,
show_ellipsis: bool = False,
unit: str | None = None, # Aligned to bottom unless it starts with "^"
include_background_rectangle: bool = False,
edge_to_fix: Vect3 = LEFT,
2023-01-16 13:43:48 -08:00
font_size: float = 48,
2022-12-15 20:33:58 -08:00
text_config: dict = dict(), # Do not pass in font_size here
**kwargs
):
self.num_decimal_places = num_decimal_places
self.include_sign = include_sign
self.group_with_commas = group_with_commas
self.digit_buff_per_font_unit = digit_buff_per_font_unit
self.show_ellipsis = show_ellipsis
self.unit = unit
self.include_background_rectangle = include_background_rectangle
self.edge_to_fix = edge_to_fix
self.font_size = font_size
self.text_config = dict(text_config)
2022-12-15 20:33:58 -08:00
super().__init__(
color=color,
stroke_width=stroke_width,
fill_opacity=fill_opacity,
2024-08-16 15:21:29 -05:00
fill_border_width=fill_border_width,
2022-12-15 20:33:58 -08:00
**kwargs
)
self.set_submobjects_from_number(number)
self.init_colors()
def set_submobjects_from_number(self, number: float | complex) -> None:
# Create the submobject list
self.number = number
self.num_string = self.get_num_string(number)
# Submob_templates will be a list of cached Tex and Text mobjects,
# with the intent of calling .copy or .become on them
submob_templates = list(map(self.char_to_mob, self.num_string))
if self.show_ellipsis:
dots = self.char_to_mob("...")
2021-10-01 12:31:16 -07:00
dots.arrange(RIGHT, buff=2 * dots[0].get_width())
submob_templates.append(dots)
if self.unit is not None:
submob_templates.append(self.char_to_mob(self.unit))
# Set internals
font_size = self.get_font_size()
if len(submob_templates) == len(self.submobjects):
for sm, smt in zip(self.submobjects, submob_templates):
sm.become(smt)
sm.scale(font_size / smt.font_size)
else:
self.set_submobjects([
smt.copy().scale(font_size / smt.font_size)
for smt in submob_templates
])
digit_buff = self.digit_buff_per_font_unit * font_size
self.arrange(RIGHT, buff=digit_buff, aligned_edge=DOWN)
# Handle alignment of special characters
for i, c in enumerate(self.num_string):
if c == "" and len(self.num_string) > i + 1:
2019-03-08 14:39:47 -06:00
self[i].align_to(self[i + 1], UP)
2020-02-18 22:32:02 -08:00
self[i].shift(self[i + 1].get_height() * DOWN / 2)
elif c == ",":
self[i].shift(self[i].get_height() * DOWN / 2)
if self.unit and self.unit.startswith("^"):
self[-1].align_to(self, UP)
2020-06-05 17:57:44 -07:00
if self.include_background_rectangle:
self.add_background_rectangle()
def get_num_string(self, number: float | complex) -> str:
2021-01-13 11:11:25 -10:00
if isinstance(number, complex):
formatter = self.get_complex_formatter()
else:
formatter = self.get_formatter()
if self.num_decimal_places == 0 and isinstance(number, float):
number = int(number)
2021-01-13 11:11:25 -10:00
num_string = formatter.format(number)
rounded_num = np.round(number, self.num_decimal_places)
if num_string.startswith("-") and rounded_num == 0:
if self.include_sign:
num_string = "+" + num_string[1:]
else:
num_string = num_string[1:]
num_string = num_string.replace("-", "")
2021-01-13 11:11:25 -10:00
return num_string
def char_to_mob(self, char: str) -> Text:
return char_to_cahced_mob(char, **self.text_config)
2024-08-20 10:00:07 -05:00
def interpolate(
self,
mobject1: Mobject,
mobject2: Mobject,
alpha: float,
path_func: Callable[[np.ndarray, np.ndarray, float], np.ndarray] = straight_path
) -> Self:
super().interpolate(mobject1, mobject2, alpha, path_func)
if hasattr(mobject1, "font_size") and hasattr(mobject2, "font_size"):
self.font_size = interpolate(mobject1.font_size, mobject2.font_size, alpha)
2021-01-12 13:08:24 -10:00
2023-01-16 13:43:48 -08:00
def get_font_size(self) -> float:
2024-08-20 10:00:07 -05:00
return self.font_size
2020-12-17 15:59:02 -08:00
def get_formatter(self, **kwargs) -> str:
"""
Configuration is based first off instance attributes,
but overwritten by any kew word argument. Relevant
key words:
- include_sign
- group_with_commas
- num_decimal_places
- field_name (e.g. 0 or 0.real)
"""
config = dict([
(attr, getattr(self, attr))
for attr in [
"include_sign",
"group_with_commas",
"num_decimal_places",
]
])
config.update(kwargs)
2022-05-23 11:07:39 -07:00
ndp = config["num_decimal_places"]
2018-08-12 19:22:13 -07:00
return "".join([
"{",
config.get("field_name", ""),
":",
"+" if config["include_sign"] else "",
"," if config["group_with_commas"] else "",
f".{ndp}f" if ndp > 0 else "d",
"}",
])
def get_complex_formatter(self, **kwargs) -> str:
2018-08-12 19:22:13 -07:00
return "".join([
self.get_formatter(field_name="0.real"),
self.get_formatter(field_name="0.imag", include_sign=True),
"i"
])
2022-05-11 12:45:27 -07:00
def get_tex(self):
return self.num_string
2023-01-31 14:16:15 -08:00
def set_value(self, number: float | complex) -> Self:
move_to_point = self.get_edge_center(self.edge_to_fix)
style = self.family_members_with_points()[0].get_style()
self.set_submobjects_from_number(number)
self.move_to(move_to_point, self.edge_to_fix)
self.set_style(**style)
for submob in self.get_family():
submob.uniforms.update(self.uniforms)
2018-08-30 14:24:40 -07:00
return self
2018-08-12 19:05:31 -07:00
2023-01-31 14:16:15 -08:00
def _handle_scale_side_effects(self, scale_factor: float) -> Self:
2024-08-20 10:00:07 -05:00
self.font_size *= scale_factor
2023-01-31 14:16:15 -08:00
return self
def get_value(self) -> float | complex:
2018-08-12 19:05:31 -07:00
return self.number
2023-01-31 14:16:15 -08:00
def increment_value(self, delta_t: float | complex = 1) -> Self:
self.set_value(self.get_value() + delta_t)
2023-01-31 14:16:15 -08:00
return self
class Integer(DecimalNumber):
2022-12-15 20:33:58 -08:00
def __init__(
self,
number: int = 0,
num_decimal_places: int = 0,
**kwargs,
):
super().__init__(number, num_decimal_places=num_decimal_places, **kwargs)
2019-01-04 14:14:15 -08:00
def get_value(self) -> int:
2019-01-17 14:12:14 -08:00
return int(np.round(super().get_value()))