mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-22 10:01:47 +00:00
Merge branch 'main' of https://github.com/ManimCommunity/manim into experimental
This commit is contained in:
commit
497debad8e
57 changed files with 2603 additions and 601 deletions
|
|
@ -13,7 +13,7 @@ repos:
|
|||
- id: check-toml
|
||||
name: Validate Poetry
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.7.0
|
||||
rev: v0.8.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
name: ruff lint
|
||||
|
|
@ -22,7 +22,7 @@ repos:
|
|||
- id: ruff-format
|
||||
types: [python]
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.12.1
|
||||
rev: v1.13.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies:
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ Basic Concepts
|
|||
[[i * 256 / n for i in range(0, n)] for _ in range(0, n)]
|
||||
)
|
||||
image = ImageMobject(imageArray).scale(2)
|
||||
image.background_rectangle = SurroundingRectangle(image, GREEN)
|
||||
image.background_rectangle = SurroundingRectangle(image, color=GREEN)
|
||||
self.add(image, image.background_rectangle)
|
||||
|
||||
.. manim:: BooleanOperations
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ Module Index
|
|||
:toctree: ../reference
|
||||
|
||||
~utils.bezier
|
||||
cli
|
||||
~utils.color
|
||||
~utils.commands
|
||||
~utils.config_ops
|
||||
|
|
|
|||
|
|
@ -3,22 +3,49 @@ from __future__ import annotations
|
|||
import click
|
||||
import cloup
|
||||
|
||||
from . import __version__, cli_ctx_settings, console
|
||||
from .cli.cfg.group import cfg
|
||||
from .cli.checkhealth.commands import checkhealth
|
||||
from .cli.default_group import DefaultGroup
|
||||
from .cli.init.commands import init
|
||||
from .cli.plugins.commands import plugins
|
||||
from .cli.render.commands import render
|
||||
from .constants import EPILOG
|
||||
from manim import __version__
|
||||
from manim._config import cli_ctx_settings, console
|
||||
from manim.cli.cfg.group import cfg
|
||||
from manim.cli.checkhealth.commands import checkhealth
|
||||
from manim.cli.default_group import DefaultGroup
|
||||
from manim.cli.init.commands import init
|
||||
from manim.cli.plugins.commands import plugins
|
||||
from manim.cli.render.commands import render
|
||||
from manim.constants import EPILOG
|
||||
|
||||
|
||||
def show_splash(ctx, param, value):
|
||||
def show_splash(ctx: click.Context, param: click.Option, value: str | None) -> None:
|
||||
"""When giving a value by console, show an initial message with the Manim
|
||||
version before executing any other command: ``Manim Community vA.B.C``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx
|
||||
The Click context.
|
||||
param
|
||||
A Click option.
|
||||
value
|
||||
A string value given by console, or None.
|
||||
"""
|
||||
if value:
|
||||
console.print(f"Manim Community [green]v{__version__}[/green]\n")
|
||||
|
||||
|
||||
def print_version_and_exit(ctx, param, value):
|
||||
def print_version_and_exit(
|
||||
ctx: click.Context, param: click.Option, value: str | None
|
||||
) -> None:
|
||||
"""Same as :func:`show_splash`, but also exit when giving a value by
|
||||
console.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx
|
||||
The Click context.
|
||||
param
|
||||
A Click option.
|
||||
value
|
||||
A string value given by console, or None.
|
||||
"""
|
||||
show_splash(ctx, param, value)
|
||||
if value:
|
||||
ctx.exit()
|
||||
|
|
@ -53,8 +80,14 @@ def print_version_and_exit(ctx, param, value):
|
|||
expose_value=False,
|
||||
)
|
||||
@cloup.pass_context
|
||||
def main(ctx):
|
||||
"""The entry point for manim."""
|
||||
def main(ctx: click.Context) -> None:
|
||||
"""The entry point for Manim.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx
|
||||
The Click context.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
from typing import Any
|
||||
|
||||
from cloup import Context, HelpFormatter, HelpTheme, Style
|
||||
|
||||
__all__ = ["parse_cli_ctx"]
|
||||
|
||||
|
||||
def parse_cli_ctx(parser: configparser.SectionProxy) -> Context:
|
||||
def parse_cli_ctx(parser: configparser.SectionProxy) -> dict[str, Any]:
|
||||
formatter_settings: dict[str, str | int] = {
|
||||
"indent_increment": int(parser["indent_increment"]),
|
||||
"width": int(parser["width"]),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
|||
from collections.abc import Iterable, Sequence
|
||||
from copy import deepcopy
|
||||
from functools import partialmethod
|
||||
from typing import TYPE_CHECKING, Callable, cast, overload
|
||||
from typing import TYPE_CHECKING, Any, Callable, cast, overload
|
||||
|
||||
import numpy as np
|
||||
from typing_extensions import Self, TypeVar, assert_never
|
||||
|
|
@ -14,8 +14,9 @@ from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
|||
|
||||
from .. import config, logger
|
||||
from ..mobject import mobject
|
||||
from ..mobject.mobject import Mobject
|
||||
from ..mobject.mobject import Group, Mobject
|
||||
from ..mobject.opengl import opengl_mobject
|
||||
from ..scene.scene import Scene
|
||||
from ..utils.rate_functions import linear, smooth
|
||||
from .protocol import AnimationProtocol, MobjectAnimation
|
||||
from .scene_buffer import SceneBuffer, SceneOperation
|
||||
|
|
@ -176,7 +177,20 @@ class Animation(AnimationProtocol):
|
|||
),
|
||||
)
|
||||
|
||||
def _typecheck_input(self, mobject: Mobject | OpenGLMobject | None) -> None:
|
||||
@property
|
||||
def run_time(self) -> float:
|
||||
return self._run_time
|
||||
|
||||
@run_time.setter
|
||||
def run_time(self, value: float) -> None:
|
||||
if value < 0:
|
||||
raise ValueError(
|
||||
f"The run_time of {self.__class__.__name__} cannot be "
|
||||
f"negative. The given value was {value}."
|
||||
)
|
||||
self._run_time = value
|
||||
|
||||
def _typecheck_input(self, mobject: Mobject | None) -> None:
|
||||
if mobject is None:
|
||||
logger.debug("Animation with empty mobject")
|
||||
elif not isinstance(mobject, (Mobject, OpenGLMobject)):
|
||||
|
|
@ -630,6 +644,90 @@ class Wait(Animation):
|
|||
pass
|
||||
|
||||
|
||||
class Add(Animation):
|
||||
"""Add Mobjects to a scene, without animating them in any other way. This
|
||||
is similar to the :meth:`.Scene.add` method, but :class:`Add` is an
|
||||
animation which can be grouped into other animations.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mobjects
|
||||
One :class:`~.Mobject` or more to add to a scene.
|
||||
run_time
|
||||
The duration of the animation after adding the ``mobjects``. Defaults
|
||||
to 0, which means this is an instant animation without extra wait time
|
||||
after adding them.
|
||||
**kwargs
|
||||
Additional arguments to pass to the parent :class:`Animation` class.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim:: DefaultAddScene
|
||||
|
||||
class DefaultAddScene(Scene):
|
||||
def construct(self):
|
||||
text_1 = Text("I was added with Add!")
|
||||
text_2 = Text("Me too!")
|
||||
text_3 = Text("And me!")
|
||||
texts = VGroup(text_1, text_2, text_3).arrange(DOWN)
|
||||
rect = SurroundingRectangle(texts, buff=0.5)
|
||||
|
||||
self.play(
|
||||
Create(rect, run_time=3.0),
|
||||
Succession(
|
||||
Wait(1.0),
|
||||
# You can Add a Mobject in the middle of an animation...
|
||||
Add(text_1),
|
||||
Wait(1.0),
|
||||
# ...or multiple Mobjects at once!
|
||||
Add(text_2, text_3),
|
||||
),
|
||||
)
|
||||
self.wait()
|
||||
|
||||
.. manim:: AddWithRunTimeScene
|
||||
|
||||
class AddWithRunTimeScene(Scene):
|
||||
def construct(self):
|
||||
# A 5x5 grid of circles
|
||||
circles = VGroup(
|
||||
*[Circle(radius=0.5) for _ in range(25)]
|
||||
).arrange_in_grid(5, 5)
|
||||
|
||||
self.play(
|
||||
Succession(
|
||||
# Add a run_time of 0.2 to wait for 0.2 seconds after
|
||||
# adding the circle, instead of using Wait(0.2) after Add!
|
||||
*[Add(circle, run_time=0.2) for circle in circles],
|
||||
rate_func=smooth,
|
||||
)
|
||||
)
|
||||
self.wait()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, *mobjects: Mobject, run_time: float = 0.0, **kwargs: Any
|
||||
) -> None:
|
||||
mobject = mobjects[0] if len(mobjects) == 1 else Group(*mobjects)
|
||||
super().__init__(mobject, run_time=run_time, introducer=True, **kwargs)
|
||||
|
||||
def begin(self) -> None:
|
||||
pass
|
||||
|
||||
def finish(self) -> None:
|
||||
pass
|
||||
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
pass
|
||||
|
||||
def update_mobjects(self, dt: float) -> None:
|
||||
pass
|
||||
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def override_animation(
|
||||
animation_class: type[Animation],
|
||||
) -> Callable[[Callable], Callable]:
|
||||
|
|
|
|||
|
|
@ -175,11 +175,13 @@ class AnimationGroup(Animation):
|
|||
]
|
||||
|
||||
run_times = to_update["end"] - to_update["start"]
|
||||
with_zero_run_time = run_times == 0
|
||||
run_times[with_zero_run_time] = 1
|
||||
sub_alphas = (anim_group_time - to_update["start"]) / run_times
|
||||
if time_goes_back:
|
||||
sub_alphas[sub_alphas < 0] = 0
|
||||
sub_alphas[(sub_alphas < 0) | with_zero_run_time] = 0
|
||||
else:
|
||||
sub_alphas[sub_alphas > 1] = 1
|
||||
sub_alphas[(sub_alphas > 1) | with_zero_run_time] = 1
|
||||
|
||||
for anim_to_update, sub_alpha in zip(to_update["anim"], sub_alphas):
|
||||
anim_to_update.interpolate(sub_alpha)
|
||||
|
|
|
|||
|
|
@ -611,8 +611,8 @@ class Circumscribe(Succession):
|
|||
if shape is Rectangle:
|
||||
frame = SurroundingRectangle(
|
||||
mobject,
|
||||
color,
|
||||
buff,
|
||||
color=color,
|
||||
buff=buff,
|
||||
stroke_width=stroke_width,
|
||||
)
|
||||
elif shape is Circle:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
"""The Manim CLI, and the available commands for ``manim``.
|
||||
|
||||
This page is a work in progress. Please run ``manim`` or ``manim --help`` in
|
||||
your terminal to find more information on the following commands.
|
||||
|
||||
Available commands
|
||||
------------------
|
||||
|
||||
.. autosummary::
|
||||
:toctree: ../reference
|
||||
|
||||
cfg
|
||||
checkhealth
|
||||
init
|
||||
plugins
|
||||
render
|
||||
"""
|
||||
|
|
@ -11,22 +11,23 @@ from __future__ import annotations
|
|||
import contextlib
|
||||
from ast import literal_eval
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
|
||||
import cloup
|
||||
from rich.errors import StyleSyntaxError
|
||||
from rich.style import Style
|
||||
|
||||
from ... import cli_ctx_settings, console
|
||||
from ..._config.utils import config_file_paths, make_config_parser
|
||||
from ...constants import EPILOG
|
||||
from ...utils.file_ops import guarantee_existence, open_file
|
||||
from manim._config import cli_ctx_settings, console
|
||||
from manim._config.utils import config_file_paths, make_config_parser
|
||||
from manim.constants import EPILOG
|
||||
from manim.utils.file_ops import guarantee_existence, open_file
|
||||
|
||||
RICH_COLOUR_INSTRUCTIONS: str = """
|
||||
[red]The default colour is used by the input statement.
|
||||
If left empty, the default colour will be used.[/red]
|
||||
[magenta] For a full list of styles, visit[/magenta] [green]https://rich.readthedocs.io/en/latest/style.html[/green]
|
||||
"""
|
||||
RICH_NON_STYLE_ENTRIES: str = ["log.width", "log.height", "log.timestamps"]
|
||||
RICH_NON_STYLE_ENTRIES: list[str] = ["log.width", "log.height", "log.timestamps"]
|
||||
|
||||
__all__ = [
|
||||
"value_from_string",
|
||||
|
|
@ -41,7 +42,8 @@ __all__ = [
|
|||
|
||||
|
||||
def value_from_string(value: str) -> str | int | bool:
|
||||
"""Extracts the literal of proper datatype from a string.
|
||||
"""Extract the literal of proper datatype from a ``value`` string.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value
|
||||
|
|
@ -49,49 +51,60 @@ def value_from_string(value: str) -> str | int | bool:
|
|||
|
||||
Returns
|
||||
-------
|
||||
Union[:class:`str`, :class:`int`, :class:`bool`]
|
||||
Returns the literal of appropriate datatype.
|
||||
:class:`str` | :class:`int` | :class:`bool`
|
||||
The literal of appropriate datatype.
|
||||
"""
|
||||
with contextlib.suppress(SyntaxError, ValueError):
|
||||
value = literal_eval(value)
|
||||
return value
|
||||
|
||||
|
||||
def _is_expected_datatype(value: str, expected: str, style: bool = False) -> bool:
|
||||
"""Checks whether `value` is the same datatype as `expected`,
|
||||
and checks if it is a valid `style` if `style` is true.
|
||||
def _is_expected_datatype(
|
||||
value: str, expected: str, validate_style: bool = False
|
||||
) -> bool:
|
||||
"""Check whether the literal from ``value`` is the same datatype as the
|
||||
literal from ``expected``. If ``validate_style`` is ``True``, also check if
|
||||
the style given by ``value`` is valid, according to ``rich``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value
|
||||
The string of the value to check (obtained from reading the user input).
|
||||
The string of the value to check, obtained from reading the user input.
|
||||
expected
|
||||
The string of the literal datatype must be matched by `value`. Obtained from
|
||||
reading the cfg file.
|
||||
style
|
||||
Whether or not to confirm if `value` is a style, by default False
|
||||
The string of the literal datatype which must be matched by ``value``.
|
||||
This is obtained from reading the ``cfg`` file.
|
||||
validate_style
|
||||
Whether or not to confirm if ``value`` is a valid style, according to
|
||||
``rich``. Default is ``False``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`bool`
|
||||
Whether or not `value` matches the datatype of `expected`.
|
||||
Whether or not the literal from ``value`` matches the datatype of the
|
||||
literal from ``expected``.
|
||||
"""
|
||||
value = value_from_string(value)
|
||||
expected = type(value_from_string(expected))
|
||||
value_literal = value_from_string(value)
|
||||
ExpectedLiteralType = type(value_from_string(expected))
|
||||
|
||||
return isinstance(value, expected) and (is_valid_style(value) if style else True)
|
||||
return isinstance(value_literal, ExpectedLiteralType) and (
|
||||
(isinstance(value_literal, str) and is_valid_style(value_literal))
|
||||
if validate_style
|
||||
else True
|
||||
)
|
||||
|
||||
|
||||
def is_valid_style(style: str) -> bool:
|
||||
"""Checks whether the entered color is a valid color according to rich
|
||||
"""Checks whether the entered color style is valid, according to ``rich``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
style
|
||||
The style to check whether it is valid.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Boolean
|
||||
Returns whether it is valid style or not according to rich.
|
||||
:class:`bool`
|
||||
Whether the color style is valid or not, according to ``rich``.
|
||||
"""
|
||||
try:
|
||||
Style.parse(style)
|
||||
|
|
@ -100,16 +113,20 @@ def is_valid_style(style: str) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def replace_keys(default: dict) -> dict:
|
||||
"""Replaces _ to . and vice versa in a dictionary for rich
|
||||
def replace_keys(default: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Replace ``_`` with ``.`` and vice versa in a dictionary's keys for
|
||||
``rich``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
default
|
||||
The dictionary to check and replace
|
||||
The dictionary whose keys will be checked and replaced.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`dict`
|
||||
The dictionary which is modified by replacing _ with . and vice versa
|
||||
The dictionary whose keys are modified by replacing ``_`` with ``.``
|
||||
and vice versa.
|
||||
"""
|
||||
for key in default:
|
||||
if "_" in key:
|
||||
|
|
@ -133,7 +150,7 @@ def replace_keys(default: dict) -> dict:
|
|||
help="Manages Manim configuration files.",
|
||||
)
|
||||
@cloup.pass_context
|
||||
def cfg(ctx):
|
||||
def cfg(ctx: cloup.Context) -> None:
|
||||
"""Responsible for the cfg subcommand."""
|
||||
pass
|
||||
|
||||
|
|
@ -147,7 +164,7 @@ def cfg(ctx):
|
|||
help="Specify if this config is for user or the working directory.",
|
||||
)
|
||||
@cloup.option("-o", "--open", "openfile", is_flag=True)
|
||||
def write(level: str = None, openfile: bool = False) -> None:
|
||||
def write(level: str | None = None, openfile: bool = False) -> None:
|
||||
config_paths = config_file_paths()
|
||||
console.print(
|
||||
"[yellow bold]Manim Configuration File Writer[/yellow bold]",
|
||||
|
|
@ -166,7 +183,7 @@ To save your config please save that file and place it in your current working d
|
|||
action = "save this as"
|
||||
for category in parser:
|
||||
console.print(f"{category}", style="bold green underline")
|
||||
default = parser[category]
|
||||
default = cast(dict[str, Any], parser[category])
|
||||
if category == "logger":
|
||||
console.print(RICH_COLOUR_INSTRUCTIONS)
|
||||
default = replace_keys(default)
|
||||
|
|
@ -249,7 +266,7 @@ modify write_cfg_subcmd_input to account for it.""",
|
|||
|
||||
|
||||
@cfg.command(context_settings=cli_ctx_settings)
|
||||
def show():
|
||||
def show() -> None:
|
||||
parser = make_config_parser()
|
||||
rich_non_style_entries = [a.replace(".", "_") for a in RICH_NON_STYLE_ENTRIES]
|
||||
for category in parser:
|
||||
|
|
@ -269,7 +286,7 @@ def show():
|
|||
@cfg.command(context_settings=cli_ctx_settings)
|
||||
@cloup.option("-d", "--directory", default=Path.cwd())
|
||||
@cloup.pass_context
|
||||
def export(ctx, directory):
|
||||
def export(ctx: cloup.Context, directory: str) -> None:
|
||||
directory_path = Path(directory)
|
||||
if directory_path.absolute == Path.cwd().absolute:
|
||||
console.print(
|
||||
|
|
|
|||
|
|
@ -6,58 +6,74 @@ from __future__ import annotations
|
|||
|
||||
import os
|
||||
import shutil
|
||||
from typing import Callable
|
||||
from typing import Callable, Protocol, cast
|
||||
|
||||
__all__ = ["HEALTH_CHECKS"]
|
||||
|
||||
HEALTH_CHECKS = []
|
||||
|
||||
class HealthCheckFunction(Protocol):
|
||||
description: str
|
||||
recommendation: str
|
||||
skip_on_failed: list[str]
|
||||
post_fail_fix_hook: Callable[..., object] | None
|
||||
__name__: str
|
||||
|
||||
def __call__(self) -> bool: ...
|
||||
|
||||
|
||||
HEALTH_CHECKS: list[HealthCheckFunction] = []
|
||||
|
||||
|
||||
def healthcheck(
|
||||
description: str,
|
||||
recommendation: str,
|
||||
skip_on_failed: list[Callable | str] | None = None,
|
||||
post_fail_fix_hook: Callable | None = None,
|
||||
):
|
||||
skip_on_failed: list[HealthCheckFunction | str] | None = None,
|
||||
post_fail_fix_hook: Callable[..., object] | None = None,
|
||||
) -> Callable[[Callable[[], bool]], HealthCheckFunction]:
|
||||
"""Decorator used for declaring health checks.
|
||||
|
||||
This decorator attaches some data to a function,
|
||||
which is then added to a list containing all checks.
|
||||
This decorator attaches some data to a function, which is then added to a
|
||||
a list containing all checks.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
description
|
||||
A brief description of this check, displayed when
|
||||
the checkhealth subcommand is run.
|
||||
A brief description of this check, displayed when the ``checkhealth``
|
||||
subcommand is run.
|
||||
recommendation
|
||||
Help text which is displayed in case the check fails.
|
||||
skip_on_failed
|
||||
A list of check functions which, if they fail, cause
|
||||
the current check to be skipped.
|
||||
A list of check functions which, if they fail, cause the current check
|
||||
to be skipped.
|
||||
post_fail_fix_hook
|
||||
A function that is supposed to (interactively) help
|
||||
to fix the detected problem, if possible. This is
|
||||
only called upon explicit confirmation of the user.
|
||||
A function that is meant to (interactively) help to fix the detected
|
||||
problem, if possible. This is only called upon explicit confirmation of
|
||||
the user.
|
||||
|
||||
Returns
|
||||
-------
|
||||
A check function, as required by the checkhealth subcommand.
|
||||
Callable[Callable[[], bool], :class:`HealthCheckFunction`]
|
||||
A decorator which converts a function into a health check function, as
|
||||
required by the ``checkhealth`` subcommand.
|
||||
"""
|
||||
new_skip_on_failed: list[str]
|
||||
if skip_on_failed is None:
|
||||
skip_on_failed = []
|
||||
skip_on_failed = [
|
||||
skip.__name__ if callable(skip) else skip for skip in skip_on_failed
|
||||
]
|
||||
new_skip_on_failed = []
|
||||
else:
|
||||
new_skip_on_failed = [
|
||||
skip.__name__ if callable(skip) else skip for skip in skip_on_failed
|
||||
]
|
||||
|
||||
def decorator(func):
|
||||
func.description = description
|
||||
func.recommendation = recommendation
|
||||
func.skip_on_failed = skip_on_failed
|
||||
func.post_fail_fix_hook = post_fail_fix_hook
|
||||
HEALTH_CHECKS.append(func)
|
||||
return func
|
||||
def wrapper(func: Callable[[], bool]) -> HealthCheckFunction:
|
||||
health_func = cast(HealthCheckFunction, func)
|
||||
health_func.description = description
|
||||
health_func.recommendation = recommendation
|
||||
health_func.skip_on_failed = new_skip_on_failed
|
||||
health_func.post_fail_fix_hook = post_fail_fix_hook
|
||||
HEALTH_CHECKS.append(health_func)
|
||||
return health_func
|
||||
|
||||
return decorator
|
||||
return wrapper
|
||||
|
||||
|
||||
@healthcheck(
|
||||
|
|
@ -75,7 +91,14 @@ def healthcheck(
|
|||
"PATH variable."
|
||||
),
|
||||
)
|
||||
def is_manim_on_path():
|
||||
def is_manim_on_path() -> bool:
|
||||
"""Check whether ``manim`` is in ``PATH``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`bool`
|
||||
Whether ``manim`` is in ``PATH`` or not.
|
||||
"""
|
||||
path_to_manim = shutil.which("manim")
|
||||
return path_to_manim is not None
|
||||
|
||||
|
|
@ -91,10 +114,30 @@ def is_manim_on_path():
|
|||
),
|
||||
skip_on_failed=[is_manim_on_path],
|
||||
)
|
||||
def is_manim_executable_associated_to_this_library():
|
||||
def is_manim_executable_associated_to_this_library() -> bool:
|
||||
"""Check whether the ``manim`` executable in ``PATH`` is associated to this
|
||||
library. To verify this, the executable should look like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
#!<MANIM_PATH>/.../python
|
||||
import sys
|
||||
from manim.__main__ import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`bool`
|
||||
Whether the ``manim`` executable in ``PATH`` is associated to this
|
||||
library or not.
|
||||
"""
|
||||
path_to_manim = shutil.which("manim")
|
||||
with open(path_to_manim, "rb") as f:
|
||||
manim_exec = f.read()
|
||||
assert path_to_manim is not None
|
||||
with open(path_to_manim, "rb") as manim_binary:
|
||||
manim_exec = manim_binary.read()
|
||||
|
||||
# first condition below corresponds to the executable being
|
||||
# some sort of python script. second condition happens when
|
||||
|
|
@ -114,7 +157,14 @@ def is_manim_executable_associated_to_this_library():
|
|||
"LaTeX distribution on your operating system."
|
||||
),
|
||||
)
|
||||
def is_latex_available():
|
||||
def is_latex_available() -> bool:
|
||||
"""Check whether ``latex`` is in ``PATH`` and can be executed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`bool`
|
||||
Whether ``latex`` is in ``PATH`` and can be executed or not.
|
||||
"""
|
||||
path_to_latex = shutil.which("latex")
|
||||
return path_to_latex is not None and os.access(path_to_latex, os.X_OK)
|
||||
|
||||
|
|
@ -129,6 +179,13 @@ def is_latex_available():
|
|||
),
|
||||
skip_on_failed=[is_latex_available],
|
||||
)
|
||||
def is_dvisvgm_available():
|
||||
def is_dvisvgm_available() -> bool:
|
||||
"""Check whether ``dvisvgm`` is in ``PATH`` and can be executed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`bool`
|
||||
Whether ``dvisvgm`` is in ``PATH`` and can be executed or not.
|
||||
"""
|
||||
path_to_dvisvgm = shutil.which("dvisvgm")
|
||||
return path_to_dvisvgm is not None and os.access(path_to_dvisvgm, os.X_OK)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import timeit
|
|||
import click
|
||||
import cloup
|
||||
|
||||
from manim.cli.checkhealth.checks import HEALTH_CHECKS
|
||||
from manim.cli.checkhealth.checks import HEALTH_CHECKS, HealthCheckFunction
|
||||
|
||||
__all__ = ["checkhealth"]
|
||||
|
||||
|
|
@ -19,13 +19,13 @@ __all__ = ["checkhealth"]
|
|||
@cloup.command(
|
||||
context_settings=None,
|
||||
)
|
||||
def checkhealth():
|
||||
def checkhealth() -> None:
|
||||
"""This subcommand checks whether Manim is installed correctly
|
||||
and has access to its required (and optional) system dependencies.
|
||||
"""
|
||||
click.echo(f"Python executable: {sys.executable}\n")
|
||||
click.echo("Checking whether your installation of Manim Community is healthy...")
|
||||
failed_checks = []
|
||||
failed_checks: list[HealthCheckFunction] = []
|
||||
|
||||
for check in HEALTH_CHECKS:
|
||||
click.echo(f"- {check.description} ... ", nl=False)
|
||||
|
|
@ -63,7 +63,7 @@ def checkhealth():
|
|||
import manim as mn
|
||||
|
||||
class CheckHealthDemo(mn.Scene):
|
||||
def _inner_construct(self):
|
||||
def _inner_construct(self) -> None:
|
||||
banner = mn.ManimBanner().shift(mn.UP * 0.5)
|
||||
self.play(banner.create())
|
||||
self.wait(0.5)
|
||||
|
|
@ -80,7 +80,7 @@ def checkhealth():
|
|||
mn.FadeOut(text_tex_group, shift=mn.DOWN),
|
||||
)
|
||||
|
||||
def construct(self):
|
||||
def construct(self) -> None:
|
||||
self.execution_time = timeit.timeit(self._inner_construct, number=1)
|
||||
|
||||
with mn.tempconfig({"preview": True, "disable_caching": True}):
|
||||
|
|
|
|||
|
|
@ -6,62 +6,183 @@ In particular, this class is what allows ``manim`` to act as ``manim render``.
|
|||
This is a vendored version of https://github.com/click-contrib/click-default-group/
|
||||
under the BSD 3-Clause "New" or "Revised" License.
|
||||
|
||||
This library isn't used as a dependency as we need to inherit from ``cloup.Group`` instead
|
||||
of ``click.Group``.
|
||||
This library isn't used as a dependency, as we need to inherit from
|
||||
:class:`cloup.Group` instead of :class:`click.Group`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
|
||||
import cloup
|
||||
|
||||
from manim.utils.deprecation import deprecated
|
||||
|
||||
__all__ = ["DefaultGroup"]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from click import Command, Context
|
||||
|
||||
|
||||
class DefaultGroup(cloup.Group):
|
||||
"""Invokes a subcommand marked with ``default=True`` if any subcommand not
|
||||
"""Invokes a subcommand marked with ``default=True`` if any subcommand is not
|
||||
chosen.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*args
|
||||
Positional arguments to forward to :class:`cloup.Group`.
|
||||
**kwargs
|
||||
Keyword arguments to forward to :class:`cloup.Group`. The keyword
|
||||
``ignore_unknown_options`` must be set to ``False``.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
default_cmd_name : str | None
|
||||
The name of the default command, if specified through the ``default``
|
||||
keyword argument. Otherwise, this is set to ``None``.
|
||||
default_if_no_args : bool
|
||||
Whether to include or not the default command, if no command arguments
|
||||
are supplied. This can be specified through the ``default_if_no_args``
|
||||
keyword argument. Default is ``False``.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args: Any, **kwargs: Any):
|
||||
# To resolve as the default command.
|
||||
if not kwargs.get("ignore_unknown_options", True):
|
||||
raise ValueError("Default group accepts unknown options")
|
||||
self.ignore_unknown_options = True
|
||||
self.default_cmd_name = kwargs.pop("default", None)
|
||||
self.default_if_no_args = kwargs.pop("default_if_no_args", False)
|
||||
self.default_cmd_name: str | None = kwargs.pop("default", None)
|
||||
self.default_if_no_args: bool = kwargs.pop("default_if_no_args", False)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def set_default_command(self, command):
|
||||
"""Sets a command function as the default command."""
|
||||
def set_default_command(self, command: Command) -> None:
|
||||
"""Sets a command function as the default command.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
command
|
||||
The command to set as default.
|
||||
"""
|
||||
cmd_name = command.name
|
||||
self.add_command(command)
|
||||
self.default_cmd_name = cmd_name
|
||||
|
||||
def parse_args(self, ctx, args):
|
||||
if not args and self.default_if_no_args:
|
||||
args.insert(0, self.default_cmd_name)
|
||||
return super().parse_args(ctx, args)
|
||||
def parse_args(self, ctx: Context, args: list[str]) -> list[str]:
|
||||
"""Parses the list of ``args`` by forwarding it to
|
||||
:meth:`cloup.Group.parse_args`. Before doing so, if
|
||||
:attr:`default_if_no_args` is set to ``True`` and ``args`` is empty,
|
||||
this function appends to it the name of the default command specified
|
||||
by :attr:`default_cmd_name`.
|
||||
|
||||
def get_command(self, ctx, cmd_name):
|
||||
if cmd_name not in self.commands:
|
||||
Parameters
|
||||
----------
|
||||
ctx
|
||||
The Click context.
|
||||
args
|
||||
A list of arguments. If it's empty and :attr:`default_if_no_args`
|
||||
is ``True``, append the name of the default command to it.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[str]
|
||||
The parsed arguments.
|
||||
"""
|
||||
if not args and self.default_if_no_args and self.default_cmd_name:
|
||||
args.insert(0, self.default_cmd_name)
|
||||
parsed_args: list[str] = super().parse_args(ctx, args)
|
||||
return parsed_args
|
||||
|
||||
def get_command(self, ctx: Context, cmd_name: str) -> Command | None:
|
||||
"""Get a command function by its name, by forwarding the arguments to
|
||||
:meth:`cloup.Group.get_command`. If ``cmd_name`` does not match any of
|
||||
the command names in :attr:`commands`, attempt to get the default command
|
||||
instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx
|
||||
The Click context.
|
||||
cmd_name
|
||||
The name of the command to get.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`click.Command` | None
|
||||
The command, if found. Otherwise, ``None``.
|
||||
"""
|
||||
if cmd_name not in self.commands and self.default_cmd_name:
|
||||
# No command name matched.
|
||||
ctx.arg0 = cmd_name
|
||||
ctx.meta["arg0"] = cmd_name
|
||||
cmd_name = self.default_cmd_name
|
||||
return super().get_command(ctx, cmd_name)
|
||||
|
||||
def resolve_command(self, ctx, args):
|
||||
base = super()
|
||||
cmd_name, cmd, args = base.resolve_command(ctx, args)
|
||||
if hasattr(ctx, "arg0"):
|
||||
args.insert(0, ctx.arg0)
|
||||
cmd_name = cmd.name
|
||||
def resolve_command(
|
||||
self, ctx: Context, args: list[str]
|
||||
) -> tuple[str | None, Command | None, list[str]]:
|
||||
"""Given a list of ``args`` given by a CLI, find a command which
|
||||
matches the first element, and return its name (``cmd_name``), the
|
||||
command function itself (``cmd``) and the rest of the arguments which
|
||||
shall be passed to the function (``cmd_args``). If not found, return
|
||||
``None``, ``None`` and the rest of the arguments.
|
||||
|
||||
After resolving the command, if the Click context given by ``ctx``
|
||||
contains an ``arg0`` attribute in its :attr:`click.Context.meta`
|
||||
dictionary, insert it as the first element of the returned
|
||||
``cmd_args``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx
|
||||
The Click context.
|
||||
cmd_name
|
||||
The name of the command to get.
|
||||
|
||||
Returns
|
||||
-------
|
||||
cmd_name : str | None
|
||||
The command name, if found. Otherwise, ``None``.
|
||||
cmd : :class:`click.Command` | None
|
||||
The command, if found. Otherwise, ``None``.
|
||||
cmd_args : list[str]
|
||||
The rest of the arguments to be passed to ``cmd``.
|
||||
"""
|
||||
cmd_name, cmd, args = super().resolve_command(ctx, args)
|
||||
if "arg0" in ctx.meta:
|
||||
args.insert(0, ctx.meta["arg0"])
|
||||
if cmd is not None:
|
||||
cmd_name = cmd.name
|
||||
return cmd_name, cmd, args
|
||||
|
||||
def command(self, *args, **kwargs):
|
||||
@deprecated
|
||||
def command(
|
||||
self, *args: Any, **kwargs: Any
|
||||
) -> Callable[[Callable[..., object]], Command]:
|
||||
"""Return a decorator which converts any function into the default
|
||||
subcommand for this :class:`DefaultGroup`.
|
||||
|
||||
.. warning::
|
||||
This method is deprecated. Use the ``default`` parameter of
|
||||
:class:`DefaultGroup` or :meth:`set_default_command` instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*args
|
||||
Positional arguments to pass to :meth:`cloup.Group.command`.
|
||||
**kwargs
|
||||
Keyword arguments to pass to :meth:`cloup.Group.command`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Callable[[Callable[..., object]], click.Command]
|
||||
A decorator which transforms its input into this
|
||||
:class:`DefaultGroup`'s default subcommand.
|
||||
"""
|
||||
default = kwargs.pop("default", False)
|
||||
decorator = super().command(*args, **kwargs)
|
||||
decorator: Callable[[Callable[..., object]], Command] = super().command(
|
||||
*args, **kwargs
|
||||
)
|
||||
if not default:
|
||||
return decorator
|
||||
warnings.warn(
|
||||
|
|
@ -70,7 +191,7 @@ class DefaultGroup(cloup.Group):
|
|||
stacklevel=1,
|
||||
)
|
||||
|
||||
def _decorator(f):
|
||||
def _decorator(f: Callable) -> Command:
|
||||
cmd = decorator(f)
|
||||
self.set_default_command(cmd)
|
||||
return cmd
|
||||
|
|
|
|||
|
|
@ -10,13 +10,14 @@ from __future__ import annotations
|
|||
|
||||
import configparser
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
import cloup
|
||||
|
||||
from ... import console
|
||||
from ...constants import CONTEXT_SETTINGS, EPILOG, QUALITIES
|
||||
from ...utils.file_ops import (
|
||||
from manim._config import console
|
||||
from manim.constants import CONTEXT_SETTINGS, EPILOG, QUALITIES
|
||||
from manim.utils.file_ops import (
|
||||
add_import_statement,
|
||||
copy_template_files,
|
||||
get_template_names,
|
||||
|
|
@ -34,15 +35,15 @@ CFG_DEFAULTS = {
|
|||
__all__ = ["select_resolution", "update_cfg", "project", "scene"]
|
||||
|
||||
|
||||
def select_resolution():
|
||||
def select_resolution() -> tuple[int, int]:
|
||||
"""Prompts input of type click.Choice from user. Presents options from QUALITIES constant.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`tuple`
|
||||
Tuple containing height and width.
|
||||
tuple[int, int]
|
||||
Tuple containing height and width.
|
||||
"""
|
||||
resolution_options = []
|
||||
resolution_options: list[tuple[int, int]] = []
|
||||
for quality in QUALITIES.items():
|
||||
resolution_options.append(
|
||||
(quality[1]["pixel_height"], quality[1]["pixel_width"]),
|
||||
|
|
@ -54,18 +55,21 @@ def select_resolution():
|
|||
show_default=False,
|
||||
default="480p",
|
||||
)
|
||||
return [res for res in resolution_options if f"{res[0]}p" == choice][0]
|
||||
matches = [res for res in resolution_options if f"{res[0]}p" == choice]
|
||||
return matches[0]
|
||||
|
||||
|
||||
def update_cfg(cfg_dict: dict, project_cfg_path: Path):
|
||||
"""Updates the manim.cfg file after reading it from the project_cfg_path.
|
||||
def update_cfg(cfg_dict: dict[str, Any], project_cfg_path: Path) -> None:
|
||||
"""Update the ``manim.cfg`` file after reading it from the specified
|
||||
``project_cfg_path``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cfg_dict
|
||||
values used to update manim.cfg found project_cfg_path.
|
||||
Values used to update ``manim.cfg`` which is found in
|
||||
``project_cfg_path``.
|
||||
project_cfg_path
|
||||
Path of manim.cfg file.
|
||||
Path of the ``manim.cfg`` file.
|
||||
"""
|
||||
config = configparser.ConfigParser()
|
||||
config.read(project_cfg_path)
|
||||
|
|
@ -85,7 +89,7 @@ def update_cfg(cfg_dict: dict, project_cfg_path: Path):
|
|||
context_settings=CONTEXT_SETTINGS,
|
||||
epilog=EPILOG,
|
||||
)
|
||||
@cloup.argument("project_name", type=Path, required=False)
|
||||
@cloup.argument("project_name", type=cloup.Path(path_type=Path), required=False)
|
||||
@cloup.option(
|
||||
"-d",
|
||||
"--default",
|
||||
|
|
@ -94,13 +98,14 @@ def update_cfg(cfg_dict: dict, project_cfg_path: Path):
|
|||
help="Default settings for project creation.",
|
||||
nargs=1,
|
||||
)
|
||||
def project(default_settings, **args):
|
||||
def project(default_settings: bool, **kwargs: Any) -> None:
|
||||
"""Creates a new project.
|
||||
|
||||
PROJECT_NAME is the name of the folder in which the new project will be initialized.
|
||||
"""
|
||||
if args["project_name"]:
|
||||
project_name = args["project_name"]
|
||||
project_name: Path
|
||||
if kwargs["project_name"]:
|
||||
project_name = kwargs["project_name"]
|
||||
else:
|
||||
project_name = click.prompt("Project Name", type=Path)
|
||||
|
||||
|
|
@ -117,7 +122,7 @@ def project(default_settings, **args):
|
|||
)
|
||||
else:
|
||||
project_name.mkdir()
|
||||
new_cfg = {}
|
||||
new_cfg: dict[str, Any] = {}
|
||||
new_cfg_path = Path.resolve(project_name / "manim.cfg")
|
||||
|
||||
if not default_settings:
|
||||
|
|
@ -145,23 +150,23 @@ def project(default_settings, **args):
|
|||
)
|
||||
@cloup.argument("scene_name", type=str, required=True)
|
||||
@cloup.argument("file_name", type=str, required=False)
|
||||
def scene(**args):
|
||||
def scene(**kwargs: Any) -> None:
|
||||
"""Inserts a SCENE to an existing FILE or creates a new FILE.
|
||||
|
||||
SCENE is the name of the scene that will be inserted.
|
||||
|
||||
FILE is the name of file in which the SCENE will be inserted.
|
||||
"""
|
||||
template_name = click.prompt(
|
||||
template_name: str = click.prompt(
|
||||
"template",
|
||||
type=click.Choice(get_template_names(), False),
|
||||
default="Default",
|
||||
)
|
||||
scene = (get_template_path() / f"{template_name}.mtp").resolve().read_text()
|
||||
scene = scene.replace(template_name + "Template", args["scene_name"], 1)
|
||||
scene = scene.replace(template_name + "Template", kwargs["scene_name"], 1)
|
||||
|
||||
if args["file_name"]:
|
||||
file_name = Path(args["file_name"])
|
||||
if kwargs["file_name"]:
|
||||
file_name = Path(kwargs["file_name"])
|
||||
|
||||
if file_name.suffix != ".py":
|
||||
file_name = file_name.with_suffix(file_name.suffix + ".py")
|
||||
|
|
@ -190,7 +195,7 @@ def scene(**args):
|
|||
help="Create a new project or insert a new scene.",
|
||||
)
|
||||
@cloup.pass_context
|
||||
def init(ctx):
|
||||
def init(ctx: cloup.Context) -> None:
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ from __future__ import annotations
|
|||
|
||||
import cloup
|
||||
|
||||
from ...constants import CONTEXT_SETTINGS, EPILOG
|
||||
from ...plugins.plugins_flags import list_plugins
|
||||
from manim.constants import CONTEXT_SETTINGS, EPILOG
|
||||
from manim.plugins.plugins_flags import list_plugins
|
||||
|
||||
__all__ = ["plugins"]
|
||||
|
||||
|
|
@ -29,6 +29,16 @@ __all__ = ["plugins"]
|
|||
is_flag=True,
|
||||
help="List available plugins.",
|
||||
)
|
||||
def plugins(list_available):
|
||||
def plugins(list_available: bool) -> None:
|
||||
"""Print a list of all available plugins when calling ``manim plugins -l``
|
||||
or ``manim plugins --list``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
list_available
|
||||
If the ``-l`` or ``-list`` option is passed to ``manim plugins``, this
|
||||
parameter will be set to ``True``, which will print a list of all
|
||||
available plugins.
|
||||
"""
|
||||
if list_available:
|
||||
list_plugins()
|
||||
|
|
|
|||
|
|
@ -13,13 +13,20 @@ import json
|
|||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from argparse import Namespace
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
from typing import Any, cast
|
||||
|
||||
import cloup
|
||||
|
||||
from manim import __version__, config, console, error_console, logger
|
||||
from manim._config import tempconfig
|
||||
from manim import __version__
|
||||
from manim._config import (
|
||||
config,
|
||||
console,
|
||||
error_console,
|
||||
logger,
|
||||
tempconfig,
|
||||
)
|
||||
from manim.cli.render.ease_of_access_options import ease_of_access_options
|
||||
from manim.cli.render.global_options import global_options
|
||||
from manim.cli.render.output_options import output_options
|
||||
|
|
@ -30,7 +37,25 @@ from manim.utils.module_ops import scene_classes_from_file
|
|||
|
||||
__all__ = ["render"]
|
||||
|
||||
__all__ = ["render"]
|
||||
|
||||
class ClickArgs(Namespace):
|
||||
def __init__(self, args: dict[str, Any]) -> None:
|
||||
for name in args:
|
||||
setattr(self, name, args[name])
|
||||
|
||||
def _get_kwargs(self) -> list[tuple[str, Any]]:
|
||||
return list(self.__dict__.items())
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, ClickArgs):
|
||||
return NotImplemented
|
||||
return vars(self) == vars(other)
|
||||
|
||||
def __contains__(self, key: str) -> bool:
|
||||
return key in self.__dict__
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return str(self.__dict__)
|
||||
|
||||
|
||||
@cloup.command(
|
||||
|
|
@ -38,47 +63,26 @@ __all__ = ["render"]
|
|||
no_args_is_help=True,
|
||||
epilog=EPILOG,
|
||||
)
|
||||
@cloup.argument("file", type=Path, required=True)
|
||||
@cloup.argument("file", type=cloup.Path(path_type=Path), required=True)
|
||||
@cloup.argument("scene_names", required=False, nargs=-1)
|
||||
@global_options
|
||||
@output_options
|
||||
@render_options
|
||||
@ease_of_access_options
|
||||
def render(
|
||||
**args,
|
||||
):
|
||||
def render(**kwargs: Any) -> ClickArgs | dict[str, Any]:
|
||||
"""Render SCENE(S) from the input FILE.
|
||||
|
||||
FILE is the file path of the script or a config file.
|
||||
|
||||
SCENES is an optional list of scenes in the file.
|
||||
"""
|
||||
if args["show_in_file_browser"]:
|
||||
if kwargs["show_in_file_browser"]:
|
||||
logger.warning(
|
||||
"The short form of show_in_file_browser is deprecated and will be moved to support --format.",
|
||||
)
|
||||
|
||||
class ClickArgs:
|
||||
def __init__(self, args):
|
||||
for name in args:
|
||||
setattr(self, name, args[name])
|
||||
|
||||
def _get_kwargs(self):
|
||||
return list(self.__dict__.items())
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, ClickArgs):
|
||||
return NotImplemented
|
||||
return vars(self) == vars(other)
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self.__dict__
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.__dict__)
|
||||
|
||||
click_args = ClickArgs(args)
|
||||
if args["jupyter"]:
|
||||
click_args = ClickArgs(kwargs)
|
||||
if kwargs["jupyter"]:
|
||||
return click_args
|
||||
|
||||
config.digest_args(click_args)
|
||||
|
|
@ -123,4 +127,4 @@ def render(
|
|||
"You should consider upgrading via [yellow]pip install -U manim[/yellow]",
|
||||
)
|
||||
|
||||
return args
|
||||
return kwargs
|
||||
|
|
|
|||
|
|
@ -2,23 +2,56 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from cloup import Choice, option, option_group
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from click import Context, Option
|
||||
|
||||
__all__ = ["global_options"]
|
||||
|
||||
|
||||
logger = logging.getLogger("manim")
|
||||
|
||||
|
||||
def validate_gui_location(ctx, param, value):
|
||||
if value:
|
||||
try:
|
||||
x_offset, y_offset = map(int, re.split(r"[;,\-]", value))
|
||||
return (x_offset, y_offset)
|
||||
except Exception:
|
||||
logger.error("GUI location option is invalid.")
|
||||
exit()
|
||||
def validate_gui_location(
|
||||
ctx: Context, param: Option, value: str | None
|
||||
) -> tuple[int, int] | None:
|
||||
"""If the ``value`` string is given, extract from it the GUI location,
|
||||
which should be in any of these formats: 'x;y', 'x,y' or 'x-y'.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx
|
||||
The Click context.
|
||||
param
|
||||
A Click option.
|
||||
value
|
||||
The optional string which will be parsed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[int, int] | None
|
||||
If ``value`` is ``None``, the return value is ``None``. Otherwise, it's
|
||||
the ``(x, y)`` location for the GUI.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If ``value`` has an invalid format.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
x_offset, y_offset = map(int, re.split(r"[;,\-]", value))
|
||||
except Exception:
|
||||
logger.error("GUI location option is invalid.")
|
||||
sys.exit()
|
||||
|
||||
return (x_offset, y_offset)
|
||||
|
||||
|
||||
global_options = option_group(
|
||||
|
|
|
|||
|
|
@ -2,11 +2,16 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from cloup import Choice, option, option_group
|
||||
|
||||
from manim.constants import QUALITIES
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from click import Context, Option
|
||||
|
||||
__all__ = ["render_options"]
|
||||
|
||||
logger = logging.getLogger("manim")
|
||||
|
|
@ -14,30 +19,89 @@ logger = logging.getLogger("manim")
|
|||
__all__ = ["render_options"]
|
||||
|
||||
|
||||
def validate_scene_range(ctx, param, value):
|
||||
def validate_scene_range(
|
||||
ctx: Context, param: Option, value: str | None
|
||||
) -> tuple[int] | tuple[int, int] | None:
|
||||
"""If the ``value`` string is given, extract from it the scene range, which
|
||||
should be in any of these formats: 'start', 'start;end', 'start,end' or
|
||||
'start-end'. Otherwise, return ``None``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx
|
||||
The Click context.
|
||||
param
|
||||
A Click option.
|
||||
value
|
||||
The optional string which will be parsed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[int] | tuple[int, int] | None
|
||||
If ``value`` is ``None``, the return value is ``None``. Otherwise, it's
|
||||
the scene range, given by a tuple which may contain a single value
|
||||
``start`` or two values ``start`` and ``end``.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If ``value`` has an invalid format.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
start = int(value)
|
||||
return (start,)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if value:
|
||||
try:
|
||||
start, end = map(int, re.split(r"[;,\-]", value))
|
||||
return start, end
|
||||
except Exception:
|
||||
logger.error("Couldn't determine a range for -n option.")
|
||||
exit()
|
||||
try:
|
||||
start, end = map(int, re.split(r"[;,\-]", value))
|
||||
except Exception:
|
||||
logger.error("Couldn't determine a range for -n option.")
|
||||
sys.exit()
|
||||
|
||||
return start, end
|
||||
|
||||
|
||||
def validate_resolution(ctx, param, value):
|
||||
if value:
|
||||
try:
|
||||
start, end = map(int, re.split(r"[;,\-]", value))
|
||||
return (start, end)
|
||||
except Exception:
|
||||
logger.error("Resolution option is invalid.")
|
||||
exit()
|
||||
def validate_resolution(
|
||||
ctx: Context, param: Option, value: str | None
|
||||
) -> tuple[int, int] | None:
|
||||
"""If the ``value`` string is given, extract from it the resolution, which
|
||||
should be in any of these formats: 'W;H', 'W,H' or 'W-H'. Otherwise, return
|
||||
``None``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx
|
||||
The Click context.
|
||||
param
|
||||
A Click option.
|
||||
value
|
||||
The optional string which will be parsed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[int, int] | None
|
||||
If ``value`` is ``None``, the return value is ``None``. Otherwise, it's
|
||||
the resolution as a ``(W, H)`` tuple.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If ``value`` has an invalid format.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
width, height = map(int, re.split(r"[;,\-]", value))
|
||||
except Exception:
|
||||
logger.error("Resolution option is invalid.")
|
||||
sys.exit()
|
||||
|
||||
return width, height
|
||||
|
||||
|
||||
render_options = option_group(
|
||||
|
|
@ -81,7 +145,7 @@ render_options = option_group(
|
|||
"--quality",
|
||||
default=None,
|
||||
type=Choice(
|
||||
reversed([q["flag"] for q in QUALITIES.values() if q["flag"]]), # type: ignore[arg-type]
|
||||
list(reversed([q["flag"] for q in QUALITIES.values() if q["flag"]])),
|
||||
case_sensitive=False,
|
||||
),
|
||||
help="Render quality at the follow resolution framerates, respectively: "
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import TypedDict
|
||||
|
||||
import numpy as np
|
||||
from cloup import Context
|
||||
|
|
@ -199,8 +200,16 @@ DEGREES = TAU / 360
|
|||
RADIANS: float = 1.0
|
||||
"""Just a default to select for camera."""
|
||||
|
||||
|
||||
class QualityDict(TypedDict):
|
||||
flag: str | None
|
||||
pixel_height: int
|
||||
pixel_width: int
|
||||
frame_rate: int
|
||||
|
||||
|
||||
# Video qualities
|
||||
QUALITIES: dict[str, dict[str, str | int | None]] = {
|
||||
QUALITIES: dict[str, QualityDict] = {
|
||||
"fourk_quality": {
|
||||
"flag": "k",
|
||||
"pixel_height": 2160,
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ __all__ = [
|
|||
|
||||
import itertools
|
||||
import warnings
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
import numpy as np
|
||||
from typing_extensions import Self
|
||||
|
|
@ -64,11 +64,20 @@ from manim.utils.space_ops import (
|
|||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
import manim.mobject.geometry.tips as tips
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.text.tex_mobject import SingleStringMathTex, Tex
|
||||
from manim.mobject.text.text_mobject import Text
|
||||
from manim.typing import CubicBezierPoints, Point3D, QuadraticBezierPoints, Vector3D
|
||||
from manim.typing import (
|
||||
CubicBezierPoints,
|
||||
InternalPoint3D,
|
||||
Point3D,
|
||||
QuadraticBezierPoints,
|
||||
Vector3D,
|
||||
)
|
||||
|
||||
|
||||
class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
|
||||
|
|
@ -94,7 +103,7 @@ class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
tip_length: float = DEFAULT_ARROW_TIP_LENGTH,
|
||||
normal_vector: Vector3D = OUT,
|
||||
tip_style: dict = {},
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.tip_length: float = tip_length
|
||||
self.normal_vector: Vector3D = normal_vector
|
||||
|
|
@ -127,10 +136,10 @@ class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
def create_tip(
|
||||
self,
|
||||
tip_shape: type[tips.ArrowTip] | None = None,
|
||||
tip_length: float = None,
|
||||
tip_width: float = None,
|
||||
tip_length: float | None = None,
|
||||
tip_width: float | None = None,
|
||||
at_start: bool = False,
|
||||
):
|
||||
) -> tips.ArrowTip:
|
||||
"""Stylises the tip, positions it spatially, and returns
|
||||
the newly instantiated tip to the caller.
|
||||
"""
|
||||
|
|
@ -143,13 +152,13 @@ class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
tip_shape: type[tips.ArrowTip] | None = None,
|
||||
tip_length: float | None = None,
|
||||
tip_width: float | None = None,
|
||||
):
|
||||
) -> tips.ArrowTip | tips.ArrowTriangleFilledTip:
|
||||
"""Returns a tip that has been stylistically configured,
|
||||
but has not yet been given a position in space.
|
||||
"""
|
||||
from manim.mobject.geometry.tips import ArrowTriangleFilledTip
|
||||
|
||||
style = {}
|
||||
style: dict[str, Any] = {}
|
||||
|
||||
if tip_shape is None:
|
||||
tip_shape = ArrowTriangleFilledTip
|
||||
|
|
@ -167,7 +176,7 @@ class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
tip = tip_shape(length=tip_length, **style)
|
||||
return tip
|
||||
|
||||
def position_tip(self, tip: tips.ArrowTip, at_start: bool = False):
|
||||
def position_tip(self, tip: tips.ArrowTip, at_start: bool = False) -> tips.ArrowTip:
|
||||
# Last two control points, defining both
|
||||
# the end, and the tangency direction
|
||||
if at_start:
|
||||
|
|
@ -176,16 +185,18 @@ class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
else:
|
||||
handle = self.get_last_handle()
|
||||
anchor = self.get_end()
|
||||
angles = cartesian_to_spherical(handle - anchor)
|
||||
angles = cartesian_to_spherical((handle - anchor).tolist())
|
||||
tip.rotate(
|
||||
angles[1] - PI - tip.tip_angle,
|
||||
) # Rotates the tip along the azimuthal
|
||||
if not hasattr(self, "_init_positioning_axis"):
|
||||
axis = [
|
||||
np.sin(angles[1]),
|
||||
-np.cos(angles[1]),
|
||||
0,
|
||||
] # Obtains the perpendicular of the tip
|
||||
axis = np.array(
|
||||
[
|
||||
np.sin(angles[1]),
|
||||
-np.cos(angles[1]),
|
||||
0,
|
||||
]
|
||||
) # Obtains the perpendicular of the tip
|
||||
tip.rotate(
|
||||
-angles[2] + PI / 2,
|
||||
axis=axis,
|
||||
|
|
@ -245,7 +256,7 @@ class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
result.add(self.start_tip)
|
||||
return result
|
||||
|
||||
def get_tip(self):
|
||||
def get_tip(self) -> VMobject:
|
||||
"""Returns the TipableVMobject instance's (first) tip,
|
||||
otherwise throws an exception.
|
||||
"""
|
||||
|
|
@ -253,24 +264,28 @@ class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
if len(tips) == 0:
|
||||
raise Exception("tip not found")
|
||||
else:
|
||||
return tips[0]
|
||||
tip: VMobject = tips[0]
|
||||
return tip
|
||||
|
||||
def get_default_tip_length(self) -> float:
|
||||
return self.tip_length
|
||||
|
||||
def get_first_handle(self) -> Point3D:
|
||||
def get_first_handle(self) -> InternalPoint3D:
|
||||
# Type inference of extracting an element from a list, is not
|
||||
# supported by numpy, see this numpy issue
|
||||
# https://github.com/numpy/numpy/issues/16544
|
||||
return self.points[1]
|
||||
|
||||
def get_last_handle(self) -> Point3D:
|
||||
def get_last_handle(self) -> InternalPoint3D:
|
||||
return self.points[-2]
|
||||
|
||||
def get_end(self) -> Point3D:
|
||||
def get_end(self) -> InternalPoint3D:
|
||||
if self.has_tip():
|
||||
return self.tip.get_start()
|
||||
else:
|
||||
return super().get_end()
|
||||
|
||||
def get_start(self) -> Point3D:
|
||||
def get_start(self) -> InternalPoint3D:
|
||||
if self.has_start_tip():
|
||||
return self.start_tip.get_start()
|
||||
else:
|
||||
|
|
@ -278,7 +293,7 @@ class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
def get_length(self) -> float:
|
||||
start, end = self.get_start_and_end()
|
||||
return np.linalg.norm(start - end)
|
||||
return float(np.linalg.norm(start - end))
|
||||
|
||||
|
||||
class Arc(TipableVMobject):
|
||||
|
|
@ -298,20 +313,20 @@ class Arc(TipableVMobject):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
radius: float = 1.0,
|
||||
radius: float | None = 1.0,
|
||||
start_angle: float = 0,
|
||||
angle: float = TAU / 4,
|
||||
num_components: int = 9,
|
||||
arc_center: Point3D = ORIGIN,
|
||||
**kwargs,
|
||||
arc_center: InternalPoint3D = ORIGIN,
|
||||
**kwargs: Any,
|
||||
):
|
||||
if radius is None: # apparently None is passed by ArcBetweenPoints
|
||||
radius = 1.0
|
||||
self.radius = radius
|
||||
self.num_components: int = num_components
|
||||
self.arc_center: Point3D = arc_center
|
||||
self.start_angle: float = start_angle
|
||||
self.angle: float = angle
|
||||
self.num_components = num_components
|
||||
self.arc_center = arc_center
|
||||
self.start_angle = start_angle
|
||||
self.angle = angle
|
||||
self._failed_to_get_center: bool = False
|
||||
super().__init__(**kwargs)
|
||||
|
||||
|
|
@ -380,7 +395,7 @@ class Arc(TipableVMobject):
|
|||
handles2 = anchors[1:] - (d_theta / 3) * tangent_vectors[1:]
|
||||
self.set_anchors_and_handles(anchors[:-1], handles1, handles2, anchors[1:])
|
||||
|
||||
def get_arc_center(self, warning: bool = True) -> Point3D:
|
||||
def get_arc_center(self, warning: bool = True) -> InternalPoint3D:
|
||||
"""Looks at the normals to the first two
|
||||
anchors, and finds their intersection points
|
||||
"""
|
||||
|
|
@ -408,12 +423,15 @@ class Arc(TipableVMobject):
|
|||
self._failed_to_get_center = True
|
||||
return np.array(ORIGIN)
|
||||
|
||||
def move_arc_center_to(self, point: Point3D) -> Self:
|
||||
def move_arc_center_to(self, point: InternalPoint3D) -> Self:
|
||||
self.shift(point - self.get_arc_center())
|
||||
return self
|
||||
|
||||
def stop_angle(self) -> float:
|
||||
return angle_of_vector(self.points[-1] - self.get_arc_center()) % TAU
|
||||
return cast(
|
||||
float,
|
||||
angle_of_vector(self.points[-1] - self.get_arc_center()) % TAU,
|
||||
)
|
||||
|
||||
|
||||
class ArcBetweenPoints(Arc):
|
||||
|
|
@ -440,8 +458,8 @@ class ArcBetweenPoints(Arc):
|
|||
start: Point3D,
|
||||
end: Point3D,
|
||||
angle: float = TAU / 4,
|
||||
radius: float = None,
|
||||
**kwargs,
|
||||
radius: float | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
if radius is not None:
|
||||
self.radius = radius
|
||||
|
|
@ -461,19 +479,20 @@ class ArcBetweenPoints(Arc):
|
|||
|
||||
super().__init__(radius=radius, angle=angle, **kwargs)
|
||||
if angle == 0:
|
||||
self.set_points_as_corners([LEFT, RIGHT])
|
||||
self.set_points_as_corners(np.array([LEFT, RIGHT]))
|
||||
self.put_start_and_end_on(start, end)
|
||||
|
||||
if radius is None:
|
||||
center = self.get_arc_center(warning=False)
|
||||
if not self._failed_to_get_center:
|
||||
self.radius = np.linalg.norm(np.array(start) - np.array(center))
|
||||
temp_radius: float = np.linalg.norm(np.array(start) - np.array(center))
|
||||
self.radius = temp_radius
|
||||
else:
|
||||
self.radius = np.inf
|
||||
|
||||
|
||||
class CurvedArrow(ArcBetweenPoints):
|
||||
def __init__(self, start_point: Point3D, end_point: Point3D, **kwargs) -> None:
|
||||
def __init__(self, start_point: Point3D, end_point: Point3D, **kwargs: Any) -> None:
|
||||
from manim.mobject.geometry.tips import ArrowTriangleFilledTip
|
||||
|
||||
tip_shape = kwargs.pop("tip_shape", ArrowTriangleFilledTip)
|
||||
|
|
@ -482,7 +501,7 @@ class CurvedArrow(ArcBetweenPoints):
|
|||
|
||||
|
||||
class CurvedDoubleArrow(CurvedArrow):
|
||||
def __init__(self, start_point: Point3D, end_point: Point3D, **kwargs) -> None:
|
||||
def __init__(self, start_point: Point3D, end_point: Point3D, **kwargs: Any) -> None:
|
||||
if "tip_shape_end" in kwargs:
|
||||
kwargs["tip_shape"] = kwargs.pop("tip_shape_end")
|
||||
from manim.mobject.geometry.tips import ArrowTriangleFilledTip
|
||||
|
|
@ -521,7 +540,7 @@ class Circle(Arc):
|
|||
self,
|
||||
radius: float | None = None,
|
||||
color: ParsableManimColor = RED,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
radius=radius,
|
||||
|
|
@ -618,7 +637,9 @@ class Circle(Arc):
|
|||
return self.point_from_proportion(proportion)
|
||||
|
||||
@staticmethod
|
||||
def from_three_points(p1: Point3D, p2: Point3D, p3: Point3D, **kwargs) -> Self:
|
||||
def from_three_points(
|
||||
p1: Point3D, p2: Point3D, p3: Point3D, **kwargs: Any
|
||||
) -> Circle:
|
||||
"""Returns a circle passing through the specified
|
||||
three points.
|
||||
|
||||
|
|
@ -638,10 +659,10 @@ class Circle(Arc):
|
|||
self.add(NumberPlane(), circle, dots)
|
||||
"""
|
||||
center = line_intersection(
|
||||
perpendicular_bisector([p1, p2]),
|
||||
perpendicular_bisector([p2, p3]),
|
||||
perpendicular_bisector([np.asarray(p1), np.asarray(p2)]),
|
||||
perpendicular_bisector([np.asarray(p2), np.asarray(p3)]),
|
||||
)
|
||||
radius = np.linalg.norm(p1 - center)
|
||||
radius: float = np.linalg.norm(p1 - center)
|
||||
return Circle(radius=radius, **kwargs).shift(center)
|
||||
|
||||
|
||||
|
|
@ -683,7 +704,7 @@ class Dot(Circle):
|
|||
stroke_width: float = 0,
|
||||
fill_opacity: float = 1.0,
|
||||
color: ParsableManimColor = WHITE,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
arc_center=point,
|
||||
|
|
@ -704,7 +725,7 @@ class AnnotationDot(Dot):
|
|||
stroke_width: float = 5,
|
||||
stroke_color: ParsableManimColor = WHITE,
|
||||
fill_color: ParsableManimColor = BLUE,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
radius=radius,
|
||||
|
|
@ -753,12 +774,12 @@ class LabeledDot(Dot):
|
|||
self,
|
||||
label: str | SingleStringMathTex | Text | Tex,
|
||||
radius: float | None = None,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
if isinstance(label, str):
|
||||
from manim import MathTex
|
||||
|
||||
rendered_label = MathTex(label, color=BLACK)
|
||||
rendered_label: VMobject = MathTex(label, color=BLACK)
|
||||
else:
|
||||
rendered_label = label
|
||||
|
||||
|
|
@ -794,7 +815,7 @@ class Ellipse(Circle):
|
|||
self.add(ellipse_group)
|
||||
"""
|
||||
|
||||
def __init__(self, width: float = 2, height: float = 1, **kwargs) -> None:
|
||||
def __init__(self, width: float = 2, height: float = 1, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.stretch_to_fit_width(width)
|
||||
self.stretch_to_fit_height(height)
|
||||
|
|
@ -855,7 +876,7 @@ class AnnularSector(Arc):
|
|||
fill_opacity: float = 1,
|
||||
stroke_width: float = 0,
|
||||
color: ParsableManimColor = WHITE,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.inner_radius = inner_radius
|
||||
self.outer_radius = outer_radius
|
||||
|
|
@ -904,7 +925,7 @@ class Sector(AnnularSector):
|
|||
self.add(sector, sector2)
|
||||
"""
|
||||
|
||||
def __init__(self, radius: float = 1, **kwargs) -> None:
|
||||
def __init__(self, radius: float = 1, **kwargs: Any) -> None:
|
||||
super().__init__(inner_radius=0, outer_radius=radius, **kwargs)
|
||||
|
||||
|
||||
|
|
@ -934,13 +955,13 @@ class Annulus(Circle):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
inner_radius: float | None = 1,
|
||||
outer_radius: float | None = 2,
|
||||
inner_radius: float = 1,
|
||||
outer_radius: float = 2,
|
||||
fill_opacity: float = 1,
|
||||
stroke_width: float = 0,
|
||||
color: ParsableManimColor = WHITE,
|
||||
mark_paths_closed: bool = False,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.mark_paths_closed = mark_paths_closed # is this even used?
|
||||
self.inner_radius = inner_radius
|
||||
|
|
@ -990,7 +1011,7 @@ class CubicBezier(VMobject, metaclass=ConvertToOpenGL):
|
|||
start_handle: CubicBezierPoints,
|
||||
end_handle: CubicBezierPoints,
|
||||
end_anchor: CubicBezierPoints,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.add_cubic_bezier_curve(start_anchor, start_handle, end_handle, end_anchor)
|
||||
|
|
@ -1081,14 +1102,16 @@ class ArcPolygon(VMobject, metaclass=ConvertToOpenGL):
|
|||
angle: float = PI / 4,
|
||||
radius: float | None = None,
|
||||
arc_config: list[dict] | None = None,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
n = len(vertices)
|
||||
point_pairs = [(vertices[k], vertices[(k + 1) % n]) for k in range(n)]
|
||||
|
||||
if not arc_config:
|
||||
if radius:
|
||||
all_arc_configs = itertools.repeat({"radius": radius}, len(point_pairs))
|
||||
all_arc_configs: Iterable[dict] = itertools.repeat(
|
||||
{"radius": radius}, len(point_pairs)
|
||||
)
|
||||
else:
|
||||
all_arc_configs = itertools.repeat({"angle": angle}, len(point_pairs))
|
||||
elif isinstance(arc_config, dict):
|
||||
|
|
@ -1220,7 +1243,7 @@ class ArcPolygonFromArcs(VMobject, metaclass=ConvertToOpenGL):
|
|||
self.wait(2)
|
||||
"""
|
||||
|
||||
def __init__(self, *arcs: Arc | ArcBetweenPoints, **kwargs) -> None:
|
||||
def __init__(self, *arcs: Arc | ArcBetweenPoints, **kwargs: Any) -> None:
|
||||
if not all(isinstance(m, (Arc, ArcBetweenPoints)) for m in arcs):
|
||||
raise ValueError(
|
||||
"All ArcPolygon submobjects must be of type Arc/ArcBetweenPoints",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
|||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.typing import Point2D_Array, Point3D_Array
|
||||
from typing import Any
|
||||
|
||||
from manim.typing import InternalPoint3D_Array, Point2D_Array
|
||||
|
||||
|
||||
__all__ = ["Union", "Intersection", "Difference", "Exclusion"]
|
||||
|
|
@ -28,7 +30,7 @@ class _BooleanOps(VMobject, metaclass=ConvertToOpenGL):
|
|||
self,
|
||||
points: Point2D_Array,
|
||||
z_dim: float = 0.0,
|
||||
) -> Point3D_Array:
|
||||
) -> InternalPoint3D_Array:
|
||||
"""Converts an iterable with coordinates in 2D to 3D by adding
|
||||
:attr:`z_dim` as the Z coordinate.
|
||||
|
||||
|
|
@ -49,13 +51,14 @@ class _BooleanOps(VMobject, metaclass=ConvertToOpenGL):
|
|||
>>> a = _BooleanOps()
|
||||
>>> p = [(1, 2), (3, 4)]
|
||||
>>> a._convert_2d_to_3d_array(p)
|
||||
[array([1., 2., 0.]), array([3., 4., 0.])]
|
||||
array([[1., 2., 0.],
|
||||
[3., 4., 0.]])
|
||||
"""
|
||||
points = list(points)
|
||||
for i, point in enumerate(points):
|
||||
list_of_points = list(points)
|
||||
for i, point in enumerate(list_of_points):
|
||||
if len(point) == 2:
|
||||
points[i] = np.array(list(point) + [z_dim])
|
||||
return points
|
||||
list_of_points[i] = np.array(list(point) + [z_dim])
|
||||
return np.asarray(list_of_points)
|
||||
|
||||
def _convert_vmobject_to_skia_path(self, vmobject: VMobject) -> SkiaPath:
|
||||
"""Converts a :class:`~.VMobject` to SkiaPath. This method only works for
|
||||
|
|
@ -81,7 +84,6 @@ class _BooleanOps(VMobject, metaclass=ConvertToOpenGL):
|
|||
if len(points) == 0: # what? No points so return empty path
|
||||
return path
|
||||
|
||||
# In OpenGL it's quadratic beizer curves while on Cairo it's cubic...
|
||||
subpaths = vmobject.get_subpaths_from_points(points)
|
||||
for subpath in subpaths:
|
||||
quads = vmobject.get_bezier_tuples_from_points(subpath)
|
||||
|
|
@ -162,7 +164,7 @@ class Union(_BooleanOps):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, *vmobjects: VMobject, **kwargs) -> None:
|
||||
def __init__(self, *vmobjects: VMobject, **kwargs: Any) -> None:
|
||||
if len(vmobjects) < 2:
|
||||
raise ValueError("At least 2 mobjects needed for Union.")
|
||||
super().__init__(**kwargs)
|
||||
|
|
@ -201,7 +203,7 @@ class Difference(_BooleanOps):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, subject: VMobject, clip: VMobject, **kwargs) -> None:
|
||||
def __init__(self, subject: VMobject, clip: VMobject, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
outpen = SkiaPath()
|
||||
difference(
|
||||
|
|
@ -243,7 +245,7 @@ class Intersection(_BooleanOps):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, *vmobjects: VMobject, **kwargs) -> None:
|
||||
def __init__(self, *vmobjects: VMobject, **kwargs: Any) -> None:
|
||||
if len(vmobjects) < 2:
|
||||
raise ValueError("At least 2 mobjects needed for Intersection.")
|
||||
|
||||
|
|
@ -296,7 +298,7 @@ class Exclusion(_BooleanOps):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, subject: VMobject, clip: VMobject, **kwargs) -> None:
|
||||
def __init__(self, subject: VMobject, clip: VMobject, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
outpen = SkiaPath()
|
||||
xor(
|
||||
|
|
|
|||
|
|
@ -2,17 +2,116 @@ r"""Mobjects that inherit from lines and contain a label along the length."""
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["LabeledLine", "LabeledArrow"]
|
||||
__all__ = ["Label", "LabeledLine", "LabeledArrow", "LabeledPolygram"]
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.constants import *
|
||||
from manim.mobject.geometry.line import Arrow, Line
|
||||
from manim.mobject.geometry.polygram import Polygram
|
||||
from manim.mobject.geometry.shape_matchers import (
|
||||
BackgroundRectangle,
|
||||
SurroundingRectangle,
|
||||
)
|
||||
from manim.mobject.text.tex_mobject import MathTex, Tex
|
||||
from manim.mobject.text.text_mobject import Text
|
||||
from manim.utils.color import WHITE, ManimColor, ParsableManimColor
|
||||
from manim.mobject.types.vectorized_mobject import VGroup
|
||||
from manim.utils.color import WHITE
|
||||
from manim.utils.polylabel import polylabel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from manim.typing import Point3D_Array
|
||||
|
||||
|
||||
class Label(VGroup):
|
||||
"""A Label consisting of text surrounded by a frame.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label
|
||||
Label that will be displayed.
|
||||
label_config
|
||||
A dictionary containing the configuration for the label.
|
||||
This is only applied if ``label`` is of type ``str``.
|
||||
box_config
|
||||
A dictionary containing the configuration for the background box.
|
||||
frame_config
|
||||
A dictionary containing the configuration for the frame.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. manim:: LabelExample
|
||||
:save_last_frame:
|
||||
:quality: high
|
||||
|
||||
class LabelExample(Scene):
|
||||
def construct(self):
|
||||
label = Label(
|
||||
label=Text('Label Text', font='sans-serif'),
|
||||
box_config = {
|
||||
"color" : BLUE,
|
||||
"fill_opacity" : 0.75
|
||||
}
|
||||
)
|
||||
label.scale(3)
|
||||
self.add(label)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
label: str | Tex | MathTex | Text,
|
||||
label_config: dict[str, Any] | None = None,
|
||||
box_config: dict[str, Any] | None = None,
|
||||
frame_config: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Setup Defaults
|
||||
default_label_config: dict[str, Any] = {
|
||||
"color": WHITE,
|
||||
"font_size": DEFAULT_FONT_SIZE,
|
||||
}
|
||||
|
||||
default_box_config: dict[str, Any] = {
|
||||
"color": None,
|
||||
"buff": 0.05,
|
||||
"fill_opacity": 1,
|
||||
"stroke_width": 0.5,
|
||||
}
|
||||
|
||||
default_frame_config: dict[str, Any] = {
|
||||
"color": WHITE,
|
||||
"buff": 0.05,
|
||||
"stroke_width": 0.5,
|
||||
}
|
||||
|
||||
# Merge Defaults
|
||||
label_config = default_label_config | (label_config or {})
|
||||
box_config = default_box_config | (box_config or {})
|
||||
frame_config = default_frame_config | (frame_config or {})
|
||||
|
||||
# Determine the type of label and instantiate the appropriate object
|
||||
self.rendered_label: MathTex | Tex | Text
|
||||
if isinstance(label, str):
|
||||
self.rendered_label = MathTex(label, **label_config)
|
||||
elif isinstance(label, (MathTex, Tex, Text)):
|
||||
self.rendered_label = label
|
||||
else:
|
||||
raise TypeError("Unsupported label type. Must be MathTex, Tex, or Text.")
|
||||
|
||||
# Add a background box
|
||||
self.background_rect = BackgroundRectangle(self.rendered_label, **box_config)
|
||||
|
||||
# Add a frame around the label
|
||||
self.frame = SurroundingRectangle(self.rendered_label, **frame_config)
|
||||
|
||||
# Add components to the VGroup
|
||||
self.add(self.background_rect, self.rendered_label, self.frame)
|
||||
|
||||
|
||||
class LabeledLine(Line):
|
||||
|
|
@ -20,42 +119,38 @@ class LabeledLine(Line):
|
|||
|
||||
Parameters
|
||||
----------
|
||||
label : str | Tex | MathTex | Text
|
||||
label
|
||||
Label that will be displayed on the line.
|
||||
label_position : float | optional
|
||||
label_position
|
||||
A ratio in the range [0-1] to indicate the position of the label with respect to the length of the line. Default value is 0.5.
|
||||
font_size : float | optional
|
||||
Control font size for the label. This parameter is only used when `label` is of type `str`.
|
||||
label_color: ParsableManimColor | optional
|
||||
The color of the label's text. This parameter is only used when `label` is of type `str`.
|
||||
label_frame : Bool | optional
|
||||
Add a `SurroundingRectangle` frame to the label box.
|
||||
frame_fill_color : ParsableManimColor | optional
|
||||
Background color to fill the label box. If no value is provided, the background color of the canvas will be used.
|
||||
frame_fill_opacity : float | optional
|
||||
Determine the opacity of the label box by passing a value in the range [0-1], where 0 indicates complete transparency and 1 means full opacity.
|
||||
label_config
|
||||
A dictionary containing the configuration for the label.
|
||||
This is only applied if ``label`` is of type ``str``.
|
||||
box_config
|
||||
A dictionary containing the configuration for the background box.
|
||||
frame_config
|
||||
A dictionary containing the configuration for the frame.
|
||||
|
||||
.. seealso::
|
||||
:class:`LabeledArrow`
|
||||
.. seealso::
|
||||
:class:`LabeledArrow`
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. manim:: LabeledLineExample
|
||||
:save_last_frame:
|
||||
:quality: high
|
||||
|
||||
class LabeledLineExample(Scene):
|
||||
def construct(self):
|
||||
line = LabeledLine(
|
||||
label = '0.5',
|
||||
label_position = 0.8,
|
||||
font_size = 20,
|
||||
label_color = WHITE,
|
||||
label_frame = True,
|
||||
|
||||
label_config = {
|
||||
"font_size" : 20
|
||||
},
|
||||
start=LEFT+DOWN,
|
||||
end=RIGHT+UP)
|
||||
|
||||
|
||||
line.set_length(line.get_length() * 2)
|
||||
self.add(line)
|
||||
"""
|
||||
|
|
@ -64,50 +159,29 @@ class LabeledLine(Line):
|
|||
self,
|
||||
label: str | Tex | MathTex | Text,
|
||||
label_position: float = 0.5,
|
||||
font_size: float = DEFAULT_FONT_SIZE,
|
||||
label_color: ParsableManimColor = WHITE,
|
||||
label_frame: bool = True,
|
||||
frame_fill_color: ParsableManimColor = None,
|
||||
frame_fill_opacity: float = 1,
|
||||
*args,
|
||||
**kwargs,
|
||||
label_config: dict[str, Any] | None = None,
|
||||
box_config: dict[str, Any] | None = None,
|
||||
frame_config: dict[str, Any] | None = None,
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
label_color = ManimColor(label_color)
|
||||
frame_fill_color = ManimColor(frame_fill_color)
|
||||
if isinstance(label, str):
|
||||
from manim import MathTex
|
||||
|
||||
rendered_label = MathTex(label, color=label_color, font_size=font_size)
|
||||
else:
|
||||
rendered_label = label
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# calculating the vector for the label position
|
||||
# Create Label
|
||||
self.label = Label(
|
||||
label=label,
|
||||
label_config=label_config,
|
||||
box_config=box_config,
|
||||
frame_config=frame_config,
|
||||
)
|
||||
|
||||
# Compute Label Position
|
||||
line_start, line_end = self.get_start_and_end()
|
||||
new_vec = (line_end - line_start) * label_position
|
||||
label_coords = line_start + new_vec
|
||||
|
||||
# rendered_label.move_to(self.get_vector() * label_position)
|
||||
rendered_label.move_to(label_coords)
|
||||
|
||||
box = BackgroundRectangle(
|
||||
rendered_label,
|
||||
buff=0.05,
|
||||
color=frame_fill_color,
|
||||
fill_opacity=frame_fill_opacity,
|
||||
stroke_width=0.5,
|
||||
)
|
||||
self.add(box)
|
||||
|
||||
if label_frame:
|
||||
box_frame = SurroundingRectangle(
|
||||
rendered_label, buff=0.05, color=label_color, stroke_width=0.5
|
||||
)
|
||||
|
||||
self.add(box_frame)
|
||||
|
||||
self.add(rendered_label)
|
||||
self.label.move_to(label_coords)
|
||||
self.add(self.label)
|
||||
|
||||
|
||||
class LabeledArrow(LabeledLine, Arrow):
|
||||
|
|
@ -116,29 +190,26 @@ class LabeledArrow(LabeledLine, Arrow):
|
|||
|
||||
Parameters
|
||||
----------
|
||||
label : str | Tex | MathTex | Text
|
||||
Label that will be displayed on the line.
|
||||
label_position : float | optional
|
||||
label
|
||||
Label that will be displayed on the Arrow.
|
||||
label_position
|
||||
A ratio in the range [0-1] to indicate the position of the label with respect to the length of the line. Default value is 0.5.
|
||||
font_size : float | optional
|
||||
Control font size for the label. This parameter is only used when `label` is of type `str`.
|
||||
label_color: ParsableManimColor | optional
|
||||
The color of the label's text. This parameter is only used when `label` is of type `str`.
|
||||
label_frame : Bool | optional
|
||||
Add a `SurroundingRectangle` frame to the label box.
|
||||
frame_fill_color : ParsableManimColor | optional
|
||||
Background color to fill the label box. If no value is provided, the background color of the canvas will be used.
|
||||
frame_fill_opacity : float | optional
|
||||
Determine the opacity of the label box by passing a value in the range [0-1], where 0 indicates complete transparency and 1 means full opacity.
|
||||
label_config
|
||||
A dictionary containing the configuration for the label.
|
||||
This is only applied if ``label`` is of type ``str``.
|
||||
box_config
|
||||
A dictionary containing the configuration for the background box.
|
||||
frame_config
|
||||
A dictionary containing the configuration for the frame.
|
||||
|
||||
|
||||
.. seealso::
|
||||
:class:`LabeledLine`
|
||||
.. seealso::
|
||||
:class:`LabeledLine`
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. manim:: LabeledArrowExample
|
||||
:save_last_frame:
|
||||
:quality: high
|
||||
|
||||
class LabeledArrowExample(Scene):
|
||||
def construct(self):
|
||||
|
|
@ -149,7 +220,159 @@ class LabeledArrow(LabeledLine, Arrow):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
**kwargs,
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class LabeledPolygram(Polygram):
|
||||
"""Constructs a polygram containing a label box at its pole of inaccessibility.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
vertex_groups
|
||||
Vertices passed to the :class:`~.Polygram` constructor.
|
||||
label
|
||||
Label that will be displayed on the Polygram.
|
||||
precision
|
||||
The precision used by the PolyLabel algorithm.
|
||||
label_config
|
||||
A dictionary containing the configuration for the label.
|
||||
This is only applied if ``label`` is of type ``str``.
|
||||
box_config
|
||||
A dictionary containing the configuration for the background box.
|
||||
frame_config
|
||||
A dictionary containing the configuration for the frame.
|
||||
|
||||
.. note::
|
||||
The PolyLabel Algorithm expects each vertex group to form a closed ring.
|
||||
If the input is open, :class:`LabeledPolygram` will attempt to close it.
|
||||
This may cause the polygon to intersect itself leading to unexpected results.
|
||||
|
||||
.. tip::
|
||||
Make sure the precision corresponds to the scale of your inputs!
|
||||
For instance, if the bounding box of your polygon stretches from 0 to 10,000, a precision of 1.0 or 10.0 should be sufficient.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. manim:: LabeledPolygramExample
|
||||
:save_last_frame:
|
||||
:quality: high
|
||||
|
||||
class LabeledPolygramExample(Scene):
|
||||
def construct(self):
|
||||
# Define Rings
|
||||
ring1 = [
|
||||
[-3.8, -2.4, 0], [-2.4, -2.5, 0], [-1.3, -1.6, 0], [-0.2, -1.7, 0],
|
||||
[1.7, -2.5, 0], [2.9, -2.6, 0], [3.5, -1.5, 0], [4.9, -1.4, 0],
|
||||
[4.5, 0.2, 0], [4.7, 1.6, 0], [3.5, 2.4, 0], [1.1, 2.5, 0],
|
||||
[-0.1, 0.9, 0], [-1.2, 0.5, 0], [-1.6, 0.7, 0], [-1.4, 1.9, 0],
|
||||
[-2.6, 2.6, 0], [-4.4, 1.2, 0], [-4.9, -0.8, 0], [-3.8, -2.4, 0]
|
||||
]
|
||||
ring2 = [
|
||||
[0.2, -1.2, 0], [0.9, -1.2, 0], [1.4, -2.0, 0], [2.1, -1.6, 0],
|
||||
[2.2, -0.5, 0], [1.4, 0.0, 0], [0.4, -0.2, 0], [0.2, -1.2, 0]
|
||||
]
|
||||
ring3 = [[-2.7, 1.4, 0], [-2.3, 1.7, 0], [-2.8, 1.9, 0], [-2.7, 1.4, 0]]
|
||||
|
||||
# Create Polygons (for reference)
|
||||
p1 = Polygon(*ring1, fill_opacity=0.75)
|
||||
p2 = Polygon(*ring2, fill_color=BLACK, fill_opacity=1)
|
||||
p3 = Polygon(*ring3, fill_color=BLACK, fill_opacity=1)
|
||||
|
||||
# Create Labeled Polygram
|
||||
polygram = LabeledPolygram(
|
||||
*[ring1, ring2, ring3],
|
||||
label=Text('Pole', font='sans-serif'),
|
||||
precision=0.01,
|
||||
)
|
||||
|
||||
# Display Circle (for reference)
|
||||
circle = Circle(radius=polygram.radius, color=WHITE).move_to(polygram.pole)
|
||||
|
||||
self.add(p1, p2, p3)
|
||||
self.add(polygram)
|
||||
self.add(circle)
|
||||
|
||||
.. manim:: LabeledCountryExample
|
||||
:save_last_frame:
|
||||
:quality: high
|
||||
|
||||
import requests
|
||||
import json
|
||||
|
||||
class LabeledCountryExample(Scene):
|
||||
def construct(self):
|
||||
# Fetch JSON data and process arcs
|
||||
data = requests.get('https://cdn.jsdelivr.net/npm/us-atlas@3/nation-10m.json').json()
|
||||
arcs, transform = data['arcs'], data['transform']
|
||||
sarcs = [np.cumsum(arc, axis=0) * transform['scale'] + transform['translate'] for arc in arcs]
|
||||
ssarcs = sorted(sarcs, key=len, reverse=True)[:1]
|
||||
|
||||
# Compute Bounding Box
|
||||
points = np.concatenate(ssarcs)
|
||||
mins, maxs = np.min(points, axis=0), np.max(points, axis=0)
|
||||
|
||||
# Build Axes
|
||||
ax = Axes(
|
||||
x_range=[mins[0], maxs[0], maxs[0] - mins[0]], x_length=10,
|
||||
y_range=[mins[1], maxs[1], maxs[1] - mins[1]], y_length=7,
|
||||
tips=False
|
||||
)
|
||||
|
||||
# Adjust Coordinates
|
||||
array = [[ax.c2p(*point) for point in sarc] for sarc in ssarcs]
|
||||
|
||||
# Add Polygram
|
||||
polygram = LabeledPolygram(
|
||||
*array,
|
||||
label=Text('USA', font='sans-serif'),
|
||||
precision=0.01,
|
||||
fill_color=BLUE,
|
||||
stroke_width=0,
|
||||
fill_opacity=0.75
|
||||
)
|
||||
|
||||
# Display Circle (for reference)
|
||||
circle = Circle(radius=polygram.radius, color=WHITE).move_to(polygram.pole)
|
||||
|
||||
self.add(ax)
|
||||
self.add(polygram)
|
||||
self.add(circle)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*vertex_groups: Point3D_Array,
|
||||
label: str | Tex | MathTex | Text,
|
||||
precision: float = 0.01,
|
||||
label_config: dict[str, Any] | None = None,
|
||||
box_config: dict[str, Any] | None = None,
|
||||
frame_config: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
# Initialize the Polygram with the vertex groups
|
||||
super().__init__(*vertex_groups, **kwargs)
|
||||
|
||||
# Create Label
|
||||
self.label = Label(
|
||||
label=label,
|
||||
label_config=label_config,
|
||||
box_config=box_config,
|
||||
frame_config=frame_config,
|
||||
)
|
||||
|
||||
# Close Vertex Groups
|
||||
rings = [
|
||||
group if np.array_equal(group[0], group[-1]) else list(group) + [group[0]]
|
||||
for group in vertex_groups
|
||||
]
|
||||
|
||||
# Compute the Pole of Inaccessibility
|
||||
cell = polylabel(rings, precision=precision)
|
||||
self.pole, self.radius = np.pad(cell.c, (0, 1), "constant"), cell.d
|
||||
|
||||
# Position the label at the pole
|
||||
self.label.move_to(self.pole)
|
||||
self.add(self.label)
|
||||
|
|
|
|||
|
|
@ -31,9 +31,11 @@ from manim.utils.color import WHITE
|
|||
from manim.utils.space_ops import angle_of_vector, line_intersection, normalize
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim.typing import Point2D, Point3D, Vector3D
|
||||
from manim.typing import InternalPoint3D, Point2D, Point3D, Vector3D
|
||||
from manim.utils.color import ParsableManimColor
|
||||
|
||||
from ..matrix import Matrix # Avoid circular import
|
||||
|
|
@ -46,20 +48,21 @@ class Line(TipableVMobject):
|
|||
end: Point3D | Mobject = RIGHT,
|
||||
buff: float = 0,
|
||||
path_arc: float | None = None,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.dim = 3
|
||||
self.buff = buff
|
||||
self.path_arc = path_arc
|
||||
self._set_start_and_end_attrs(start, end)
|
||||
super().__init__(**kwargs)
|
||||
# TODO: Deal with the situation where path_arc is None
|
||||
|
||||
def generate_points(self) -> None:
|
||||
self.set_points_by_ends(
|
||||
start=self.start,
|
||||
end=self.end,
|
||||
buff=self.buff,
|
||||
path_arc=self.path_arc,
|
||||
path_arc=self.path_arc, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
def set_points_by_ends(
|
||||
|
|
@ -86,16 +89,19 @@ class Line(TipableVMobject):
|
|||
"""
|
||||
self._set_start_and_end_attrs(start, end)
|
||||
if path_arc:
|
||||
# self.path_arc could potentially be None, which is not accepted
|
||||
# as parameter.
|
||||
assert self.path_arc is not None
|
||||
arc = ArcBetweenPoints(self.start, self.end, angle=self.path_arc)
|
||||
self.set_points(arc.points)
|
||||
else:
|
||||
self.set_points_as_corners([self.start, self.end])
|
||||
self.set_points_as_corners(np.asarray([self.start, self.end]))
|
||||
|
||||
self._account_for_buff(buff)
|
||||
|
||||
init_points = generate_points
|
||||
|
||||
def _account_for_buff(self, buff: float) -> Self | None:
|
||||
def _account_for_buff(self, buff: float) -> None:
|
||||
if buff == 0:
|
||||
return
|
||||
#
|
||||
|
|
@ -105,7 +111,7 @@ class Line(TipableVMobject):
|
|||
return
|
||||
buff_proportion = buff / length
|
||||
self.pointwise_become_partial(self, buff_proportion, 1 - buff_proportion)
|
||||
return self
|
||||
return
|
||||
|
||||
def _set_start_and_end_attrs(
|
||||
self, start: Point3D | Mobject, end: Point3D | Mobject
|
||||
|
|
@ -125,7 +131,7 @@ class Line(TipableVMobject):
|
|||
self,
|
||||
mob_or_point: Mobject | Point3D,
|
||||
direction: Vector3D | None = None,
|
||||
) -> Point3D:
|
||||
) -> InternalPoint3D:
|
||||
"""Transforms a mobject into its corresponding point. Does nothing if a point is passed.
|
||||
|
||||
``direction`` determines the location of the point along its bounding box in that direction.
|
||||
|
|
@ -149,7 +155,11 @@ class Line(TipableVMobject):
|
|||
self.path_arc = new_value
|
||||
self.init_points()
|
||||
|
||||
def put_start_and_end_on(self, start: Point3D, end: Point3D) -> Self:
|
||||
def put_start_and_end_on(
|
||||
self,
|
||||
start: InternalPoint3D,
|
||||
end: InternalPoint3D,
|
||||
) -> Self:
|
||||
"""Sets starts and end coordinates of a line.
|
||||
|
||||
Examples
|
||||
|
|
@ -189,7 +199,7 @@ class Line(TipableVMobject):
|
|||
def get_angle(self) -> float:
|
||||
return angle_of_vector(self.get_vector())
|
||||
|
||||
def get_projection(self, point: Point3D) -> Vector3D:
|
||||
def get_projection(self, point: InternalPoint3D) -> Vector3D:
|
||||
"""Returns the projection of a point onto a line.
|
||||
|
||||
Parameters
|
||||
|
|
@ -200,10 +210,10 @@ class Line(TipableVMobject):
|
|||
start = self.get_start()
|
||||
end = self.get_end()
|
||||
unit_vect = normalize(end - start)
|
||||
return start + np.dot(point - start, unit_vect) * unit_vect
|
||||
return start + float(np.dot(point - start, unit_vect)) * unit_vect
|
||||
|
||||
def get_slope(self) -> float:
|
||||
return np.tan(self.get_angle())
|
||||
return float(np.tan(self.get_angle()))
|
||||
|
||||
def set_angle(self, angle: float, about_point: Point3D | None = None) -> Self:
|
||||
if about_point is None:
|
||||
|
|
@ -217,7 +227,8 @@ class Line(TipableVMobject):
|
|||
return self
|
||||
|
||||
def set_length(self, length: float) -> Self:
|
||||
return self.scale(length / self.get_length())
|
||||
scale_factor: float = length / self.get_length()
|
||||
return self.scale(scale_factor)
|
||||
|
||||
|
||||
class DashedLine(Line):
|
||||
|
|
@ -256,10 +267,10 @@ class DashedLine(Line):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
*args: Any,
|
||||
dash_length: float = DEFAULT_DASH_LENGTH,
|
||||
dashed_ratio: float = 0.5,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.dash_length = dash_length
|
||||
self.dashed_ratio = dashed_ratio
|
||||
|
|
@ -288,7 +299,7 @@ class DashedLine(Line):
|
|||
int(np.ceil((self.get_length() / self.dash_length) * self.dashed_ratio)),
|
||||
)
|
||||
|
||||
def get_start(self) -> Point3D:
|
||||
def get_start(self) -> InternalPoint3D:
|
||||
"""Returns the start point of the line.
|
||||
|
||||
Examples
|
||||
|
|
@ -303,7 +314,7 @@ class DashedLine(Line):
|
|||
else:
|
||||
return super().get_start()
|
||||
|
||||
def get_end(self) -> Point3D:
|
||||
def get_end(self) -> InternalPoint3D:
|
||||
"""Returns the end point of the line.
|
||||
|
||||
Examples
|
||||
|
|
@ -318,7 +329,7 @@ class DashedLine(Line):
|
|||
else:
|
||||
return super().get_end()
|
||||
|
||||
def get_first_handle(self) -> Point3D:
|
||||
def get_first_handle(self) -> InternalPoint3D:
|
||||
"""Returns the point of the first handle.
|
||||
|
||||
Examples
|
||||
|
|
@ -328,9 +339,12 @@ class DashedLine(Line):
|
|||
>>> DashedLine().get_first_handle()
|
||||
array([-0.98333333, 0. , 0. ])
|
||||
"""
|
||||
# Type inference of extracting an element from a list, is not
|
||||
# supported by numpy, see this numpy issue
|
||||
# https://github.com/numpy/numpy/issues/16544
|
||||
return self.submobjects[0].points[1]
|
||||
|
||||
def get_last_handle(self) -> Point3D:
|
||||
def get_last_handle(self) -> InternalPoint3D:
|
||||
"""Returns the point of the last handle.
|
||||
|
||||
Examples
|
||||
|
|
@ -340,6 +354,9 @@ class DashedLine(Line):
|
|||
>>> DashedLine().get_last_handle()
|
||||
array([0.98333333, 0. , 0. ])
|
||||
"""
|
||||
# Type inference of extracting an element from a list, is not
|
||||
# supported by numpy, see this numpy issue
|
||||
# https://github.com/numpy/numpy/issues/16544
|
||||
return self.submobjects[-1].points[-2]
|
||||
|
||||
|
||||
|
|
@ -382,7 +399,7 @@ class TangentLine(Line):
|
|||
alpha: float,
|
||||
length: float = 1,
|
||||
d_alpha: float = 1e-6,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.length = length
|
||||
self.d_alpha = d_alpha
|
||||
|
|
@ -425,10 +442,10 @@ class Elbow(VMobject, metaclass=ConvertToOpenGL):
|
|||
self.add(elbow_group)
|
||||
"""
|
||||
|
||||
def __init__(self, width: float = 0.2, angle: float = 0, **kwargs) -> None:
|
||||
def __init__(self, width: float = 0.2, angle: float = 0, **kwargs: Any) -> None:
|
||||
self.angle = angle
|
||||
super().__init__(**kwargs)
|
||||
self.set_points_as_corners([UP, UP + RIGHT, RIGHT])
|
||||
self.set_points_as_corners(np.array([UP, UP + RIGHT, RIGHT]))
|
||||
self.scale_to_fit_width(width, about_point=ORIGIN)
|
||||
self.rotate(self.angle, about_point=ORIGIN)
|
||||
|
||||
|
|
@ -523,24 +540,24 @@ class Arrow(Line):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
*args: Any,
|
||||
stroke_width: float = 6,
|
||||
buff: float = MED_SMALL_BUFF,
|
||||
max_tip_length_to_length_ratio: float = 0.25,
|
||||
max_stroke_width_to_length_ratio: float = 5,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.max_tip_length_to_length_ratio = max_tip_length_to_length_ratio
|
||||
self.max_stroke_width_to_length_ratio = max_stroke_width_to_length_ratio
|
||||
tip_shape = kwargs.pop("tip_shape", ArrowTriangleFilledTip)
|
||||
super().__init__(*args, buff=buff, stroke_width=stroke_width, **kwargs)
|
||||
super().__init__(*args, buff=buff, stroke_width=stroke_width, **kwargs) # type: ignore[misc]
|
||||
# TODO, should this be affected when
|
||||
# Arrow.set_stroke is called?
|
||||
self.initial_stroke_width = self.stroke_width
|
||||
self.add_tip(tip_shape=tip_shape)
|
||||
self._set_stroke_width_from_length()
|
||||
|
||||
def scale(self, factor: float, scale_tips: bool = False, **kwargs) -> Self:
|
||||
def scale(self, factor: float, scale_tips: bool = False, **kwargs: Any) -> Self: # type: ignore[override]
|
||||
r"""Scale an arrow, but keep stroke width and arrow tip size fixed.
|
||||
|
||||
|
||||
|
|
@ -663,7 +680,10 @@ class Vector(Arrow):
|
|||
"""
|
||||
|
||||
def __init__(
|
||||
self, direction: Point2D | Point3D = RIGHT, buff: float = 0, **kwargs
|
||||
self,
|
||||
direction: Point2D | Point3D = RIGHT,
|
||||
buff: float = 0,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.buff = buff
|
||||
if len(direction) == 2:
|
||||
|
|
@ -676,7 +696,7 @@ class Vector(Arrow):
|
|||
integer_labels: bool = True,
|
||||
n_dim: int = 2,
|
||||
color: ParsableManimColor | None = None,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> Matrix:
|
||||
"""Creates a label based on the coordinates of the vector.
|
||||
|
||||
|
|
@ -779,7 +799,7 @@ class DoubleArrow(Arrow):
|
|||
self.add(box, d1, d2, d3)
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
if "tip_shape_end" in kwargs:
|
||||
kwargs["tip_shape"] = kwargs.pop("tip_shape_end")
|
||||
tip_shape_start = kwargs.pop("tip_shape_start", ArrowTriangleFilledTip)
|
||||
|
|
@ -908,7 +928,7 @@ class Angle(VMobject, metaclass=ConvertToOpenGL):
|
|||
dot_distance: float = 0.55,
|
||||
dot_color: ParsableManimColor = WHITE,
|
||||
elbow: bool = False,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.lines = (line1, line2)
|
||||
|
|
@ -945,9 +965,9 @@ class Angle(VMobject, metaclass=ConvertToOpenGL):
|
|||
+ quadrant[0] * radius * line1.get_unit_vector()
|
||||
+ quadrant[1] * radius * line2.get_unit_vector()
|
||||
)
|
||||
angle_mobject = Elbow(**kwargs)
|
||||
angle_mobject: VMobject = Elbow(**kwargs)
|
||||
angle_mobject.set_points_as_corners(
|
||||
[anchor_angle_1, anchor_middle, anchor_angle_2],
|
||||
np.array([anchor_angle_1, anchor_middle, anchor_angle_2]),
|
||||
)
|
||||
else:
|
||||
angle_1 = angle_of_vector(anchor_angle_1 - inter)
|
||||
|
|
@ -1047,7 +1067,7 @@ class Angle(VMobject, metaclass=ConvertToOpenGL):
|
|||
return self.angle_value / DEGREES if degrees else self.angle_value
|
||||
|
||||
@staticmethod
|
||||
def from_three_points(A: Point3D, B: Point3D, C: Point3D, **kwargs) -> Angle:
|
||||
def from_three_points(A: Point3D, B: Point3D, C: Point3D, **kwargs: Any) -> Angle:
|
||||
r"""The angle between the lines AB and BC.
|
||||
|
||||
This constructs the angle :math:`\\angle ABC`.
|
||||
|
|
@ -1123,6 +1143,10 @@ class RightAngle(Angle):
|
|||
"""
|
||||
|
||||
def __init__(
|
||||
self, line1: Line, line2: Line, length: float | None = None, **kwargs
|
||||
self,
|
||||
line1: Line,
|
||||
line2: Line,
|
||||
length: float | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(line1, line2, radius=length, elbow=True, **kwargs)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ __all__ = [
|
|||
"Square",
|
||||
"RoundedRectangle",
|
||||
"Cutout",
|
||||
"ConvexHull",
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -27,12 +28,21 @@ from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
|
|||
from manim.mobject.types.vectorized_mobject import VGroup
|
||||
from manim.utils.color import BLUE, WHITE, ParsableManimColor
|
||||
from manim.utils.iterables import adjacent_n_tuples, adjacent_pairs
|
||||
from manim.utils.qhull import QuickHull
|
||||
from manim.utils.space_ops import angle_between_vectors, normalize, regular_vertices
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Literal
|
||||
|
||||
import numpy.typing as npt
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim.typing import Point3D, Point3D_Array
|
||||
from manim.typing import (
|
||||
InternalPoint3D,
|
||||
InternalPoint3D_Array,
|
||||
Point3D,
|
||||
Point3D_Array,
|
||||
)
|
||||
from manim.utils.color import ParsableManimColor
|
||||
|
||||
|
||||
|
|
@ -72,11 +82,16 @@ class Polygram(OpenGLVMobject):
|
|||
"""
|
||||
|
||||
def __init__(
|
||||
self, *vertex_groups: Point3D, color: ParsableManimColor = BLUE, **kwargs
|
||||
self,
|
||||
*vertex_groups: Point3D_Array,
|
||||
color: ParsableManimColor = BLUE,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(color=color, **kwargs)
|
||||
|
||||
for vertices in vertex_groups:
|
||||
# The inferred type for *vertices is Any, but it should be
|
||||
# InternalPoint3D_Array
|
||||
first_vertex, *vertices = vertices
|
||||
first_vertex = np.array(first_vertex)
|
||||
|
||||
|
|
@ -85,7 +100,7 @@ class Polygram(OpenGLVMobject):
|
|||
[*(np.array(vertex) for vertex in vertices), first_vertex],
|
||||
)
|
||||
|
||||
def get_vertices(self) -> Point3D_Array:
|
||||
def get_vertices(self) -> InternalPoint3D_Array:
|
||||
"""Gets the vertices of the :class:`Polygram`.
|
||||
|
||||
Returns
|
||||
|
|
@ -106,7 +121,7 @@ class Polygram(OpenGLVMobject):
|
|||
"""
|
||||
return self.get_start_anchors()
|
||||
|
||||
def get_vertex_groups(self) -> np.ndarray[Point3D_Array]:
|
||||
def get_vertex_groups(self) -> InternalPoint3D_Array:
|
||||
"""Gets the vertex groups of the :class:`Polygram`.
|
||||
|
||||
Returns
|
||||
|
|
@ -205,7 +220,7 @@ class Polygram(OpenGLVMobject):
|
|||
if radius == 0:
|
||||
return self
|
||||
|
||||
new_points = []
|
||||
new_points: list[InternalPoint3D] = []
|
||||
|
||||
for vertices in self.get_vertex_groups():
|
||||
arcs = []
|
||||
|
|
@ -274,7 +289,7 @@ class Polygram(OpenGLVMobject):
|
|||
|
||||
new_points.extend(line.points)
|
||||
|
||||
self.set_points(new_points)
|
||||
self.set_points(np.array(new_points))
|
||||
|
||||
return self
|
||||
|
||||
|
|
@ -309,7 +324,7 @@ class Polygon(Polygram):
|
|||
self.add(isosceles, square_and_triangles)
|
||||
"""
|
||||
|
||||
def __init__(self, *vertices: Point3D, **kwargs) -> None:
|
||||
def __init__(self, *vertices: InternalPoint3D, **kwargs: Any) -> None:
|
||||
super().__init__(vertices, **kwargs)
|
||||
|
||||
|
||||
|
|
@ -352,7 +367,7 @@ class RegularPolygram(Polygram):
|
|||
density: int = 2,
|
||||
radius: float = 1,
|
||||
start_angle: float | None = None,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
# Regular polygrams can be expressed by the number of their vertices
|
||||
# and their density. This relation can be expressed as its Schläfli
|
||||
|
|
@ -373,7 +388,7 @@ class RegularPolygram(Polygram):
|
|||
|
||||
# Utility function for generating the individual
|
||||
# polygon vertices.
|
||||
def gen_polygon_vertices(start_angle):
|
||||
def gen_polygon_vertices(start_angle: float | None) -> tuple[list[Any], float]:
|
||||
reg_vertices, start_angle = regular_vertices(
|
||||
num_vertices,
|
||||
radius=radius,
|
||||
|
|
@ -429,7 +444,7 @@ class RegularPolygon(RegularPolygram):
|
|||
self.add(poly_group)
|
||||
"""
|
||||
|
||||
def __init__(self, n: int = 6, **kwargs) -> None:
|
||||
def __init__(self, n: int = 6, **kwargs: Any) -> None:
|
||||
super().__init__(n, density=1, **kwargs)
|
||||
|
||||
|
||||
|
|
@ -499,7 +514,7 @@ class Star(Polygon):
|
|||
inner_radius: float | None = None,
|
||||
density: int = 2,
|
||||
start_angle: float | None = TAU / 4,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
inner_angle = TAU / (2 * n)
|
||||
|
||||
|
|
@ -531,7 +546,7 @@ class Star(Polygon):
|
|||
start_angle=self.start_angle + inner_angle,
|
||||
)
|
||||
|
||||
vertices = []
|
||||
vertices: list[npt.NDArray] = []
|
||||
for pair in zip(outer_vertices, inner_vertices):
|
||||
vertices.extend(pair)
|
||||
|
||||
|
|
@ -559,7 +574,7 @@ class Triangle(RegularPolygon):
|
|||
self.add(tri_group)
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(n=3, **kwargs)
|
||||
|
||||
|
||||
|
|
@ -610,7 +625,7 @@ class Rectangle(Polygon):
|
|||
grid_ystep: float | None = None,
|
||||
mark_paths_closed: bool = True,
|
||||
close_new_points: bool = True,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(UR, UL, DL, DR, color=color, **kwargs)
|
||||
self.stretch_to_fit_width(width)
|
||||
|
|
@ -681,15 +696,15 @@ class Square(Rectangle):
|
|||
self.add(square_1, square_2, square_3)
|
||||
"""
|
||||
|
||||
def __init__(self, side_length: float = 2.0, **kwargs) -> None:
|
||||
def __init__(self, side_length: float = 2.0, **kwargs: Any) -> None:
|
||||
super().__init__(height=side_length, width=side_length, **kwargs)
|
||||
|
||||
@property
|
||||
def side_length(self):
|
||||
return np.linalg.norm(self.get_vertices()[0] - self.get_vertices()[1])
|
||||
def side_length(self) -> float:
|
||||
return float(np.linalg.norm(self.get_vertices()[0] - self.get_vertices()[1]))
|
||||
|
||||
@side_length.setter
|
||||
def side_length(self, value):
|
||||
def side_length(self, value: float) -> None:
|
||||
self.scale(value / self.side_length)
|
||||
|
||||
|
||||
|
|
@ -717,7 +732,7 @@ class RoundedRectangle(Rectangle):
|
|||
self.add(rect_group)
|
||||
"""
|
||||
|
||||
def __init__(self, corner_radius: float | list[float] = 0.5, **kwargs):
|
||||
def __init__(self, corner_radius: float | list[float] = 0.5, **kwargs: Any):
|
||||
super().__init__(**kwargs)
|
||||
self.corner_radius = corner_radius
|
||||
self.round_corners(self.corner_radius)
|
||||
|
|
@ -758,9 +773,77 @@ class Cutout(OpenGLVMobject):
|
|||
self.wait()
|
||||
"""
|
||||
|
||||
def __init__(self, main_shape: VMobject, *mobjects: VMobject, **kwargs) -> None:
|
||||
def __init__(
|
||||
self, main_shape: VMobject, *mobjects: VMobject, **kwargs: Any
|
||||
) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.append_points(main_shape.points)
|
||||
sub_direction = "CCW" if main_shape.get_direction() == "CW" else "CW"
|
||||
sub_direction: Literal["CCW", "CW"] = (
|
||||
"CCW" if main_shape.get_direction() == "CW" else "CW"
|
||||
)
|
||||
for mobject in mobjects:
|
||||
self.append_points(mobject.force_direction(sub_direction).points)
|
||||
|
||||
|
||||
class ConvexHull(Polygram):
|
||||
"""Constructs a convex hull for a set of points in no particular order.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
points
|
||||
The points to consider.
|
||||
tolerance
|
||||
The tolerance used by quickhull.
|
||||
kwargs
|
||||
Forwarded to the parent constructor.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. manim:: ConvexHullExample
|
||||
:save_last_frame:
|
||||
:quality: high
|
||||
|
||||
class ConvexHullExample(Scene):
|
||||
def construct(self):
|
||||
points = [
|
||||
[-2.35, -2.25, 0],
|
||||
[1.65, -2.25, 0],
|
||||
[2.65, -0.25, 0],
|
||||
[1.65, 1.75, 0],
|
||||
[-0.35, 2.75, 0],
|
||||
[-2.35, 0.75, 0],
|
||||
[-0.35, -1.25, 0],
|
||||
[0.65, -0.25, 0],
|
||||
[-1.35, 0.25, 0],
|
||||
[0.15, 0.75, 0]
|
||||
]
|
||||
hull = ConvexHull(*points, color=BLUE)
|
||||
dots = VGroup(*[Dot(point) for point in points])
|
||||
self.add(hull)
|
||||
self.add(dots)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, *points: Point3D, tolerance: float = 1e-5, **kwargs: Any
|
||||
) -> None:
|
||||
# Build Convex Hull
|
||||
array = np.array(points)[:, :2]
|
||||
hull = QuickHull(tolerance)
|
||||
hull.build(array)
|
||||
|
||||
# Extract Vertices
|
||||
facets = set(hull.facets) - hull.removed
|
||||
facet = facets.pop()
|
||||
subfacets = list(facet.subfacets)
|
||||
while len(subfacets) <= len(facets):
|
||||
sf = subfacets[-1]
|
||||
(facet,) = hull.neighbors[sf] - {facet}
|
||||
(sf,) = facet.subfacets - {sf}
|
||||
subfacets.append(sf)
|
||||
|
||||
# Setup Vertices as Point3D
|
||||
coordinates = np.vstack([sf.coordinates for sf in subfacets])
|
||||
vertices = np.hstack((coordinates, np.zeros((len(coordinates), 1))))
|
||||
|
||||
# Call Polygram
|
||||
super().__init__(vertices, **kwargs)
|
||||
|
|
|
|||
|
|
@ -8,8 +8,15 @@ from typing import Any
|
|||
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim import config, logger
|
||||
from manim.constants import *
|
||||
from manim import logger
|
||||
from manim._config import config
|
||||
from manim.constants import (
|
||||
DOWN,
|
||||
LEFT,
|
||||
RIGHT,
|
||||
SMALL_BUFF,
|
||||
UP,
|
||||
)
|
||||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.geometry.polygram import RoundedRectangle
|
||||
from manim.mobject.mobject import Mobject
|
||||
|
|
@ -43,21 +50,29 @@ class SurroundingRectangle(RoundedRectangle):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
*mobjects: Mobject,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
buff: float = SMALL_BUFF,
|
||||
corner_radius: float = 0.0,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
from manim.mobject.mobject import Group
|
||||
|
||||
if not all(isinstance(mob, Mobject) for mob in mobjects):
|
||||
raise TypeError(
|
||||
"Expected all inputs for parameter mobjects to be a Mobjects"
|
||||
)
|
||||
|
||||
group = Group(*mobjects)
|
||||
super().__init__(
|
||||
color=color,
|
||||
width=mobject.width + 2 * buff,
|
||||
height=mobject.height + 2 * buff,
|
||||
width=group.width + 2 * buff,
|
||||
height=group.height + 2 * buff,
|
||||
corner_radius=corner_radius,
|
||||
**kwargs,
|
||||
)
|
||||
self.buff = buff
|
||||
self.move_to(mobject)
|
||||
self.move_to(group)
|
||||
|
||||
|
||||
class BackgroundRectangle(SurroundingRectangle):
|
||||
|
|
@ -87,19 +102,19 @@ class BackgroundRectangle(SurroundingRectangle):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
*mobjects: Mobject,
|
||||
color: ParsableManimColor | None = None,
|
||||
stroke_width: float = 0,
|
||||
stroke_opacity: float = 0,
|
||||
fill_opacity: float = 0.75,
|
||||
buff: float = 0,
|
||||
**kwargs,
|
||||
):
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
if color is None:
|
||||
color = config.background_color
|
||||
|
||||
super().__init__(
|
||||
mobject,
|
||||
*mobjects,
|
||||
color=color,
|
||||
stroke_width=stroke_width,
|
||||
stroke_opacity=stroke_opacity,
|
||||
|
|
@ -113,7 +128,7 @@ class BackgroundRectangle(SurroundingRectangle):
|
|||
self.set_fill(opacity=b * self.original_fill_opacity)
|
||||
return self
|
||||
|
||||
def set_style(self, fill_opacity: float, **kwargs) -> Self:
|
||||
def set_style(self, fill_opacity: float, **kwargs: Any) -> Self: # type: ignore[override]
|
||||
# Unchangeable style, except for fill_opacity
|
||||
# All other style arguments are ignored
|
||||
super().set_style(
|
||||
|
|
@ -130,7 +145,10 @@ class BackgroundRectangle(SurroundingRectangle):
|
|||
return self
|
||||
|
||||
def get_fill_color(self) -> ManimColor:
|
||||
return self.color
|
||||
# The type of the color property is set to Any using the property decorator
|
||||
# vectorized_mobject.py#L571
|
||||
temp_color: ManimColor = self.color
|
||||
return temp_color
|
||||
|
||||
|
||||
class Cross(VGroup):
|
||||
|
|
@ -164,7 +182,7 @@ class Cross(VGroup):
|
|||
stroke_color: ParsableManimColor = RED,
|
||||
stroke_width: float = 6.0,
|
||||
scale_factor: float = 1.0,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
Line(UP + LEFT, DOWN + RIGHT), Line(UP + RIGHT, DOWN + LEFT), **kwargs
|
||||
|
|
@ -190,7 +208,9 @@ class Underline(Line):
|
|||
self.add(man, ul)
|
||||
"""
|
||||
|
||||
def __init__(self, mobject: Mobject, buff: float = SMALL_BUFF, **kwargs) -> None:
|
||||
def __init__(
|
||||
self, mobject: Mobject, buff: float = SMALL_BUFF, **kwargs: Any
|
||||
) -> None:
|
||||
super().__init__(LEFT, RIGHT, buff=buff, **kwargs)
|
||||
self.match_width(mobject)
|
||||
self.next_to(mobject, DOWN, buff=self.buff)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,9 @@ from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMo
|
|||
from manim.utils.space_ops import angle_of_vector
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.typing import Point3D, Vector3D
|
||||
from typing import Any
|
||||
|
||||
from manim.typing import InternalPoint3D, Point3D, Vector3D
|
||||
|
||||
|
||||
class ArrowTip(VMobject, metaclass=ConvertToOpenGL):
|
||||
|
|
@ -112,7 +114,7 @@ class ArrowTip(VMobject, metaclass=ConvertToOpenGL):
|
|||
self.add(*big_arrows, *small_arrows, *labels)
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
raise NotImplementedError("Has to be implemented in inheriting subclasses.")
|
||||
|
||||
@property
|
||||
|
|
@ -134,7 +136,7 @@ class ArrowTip(VMobject, metaclass=ConvertToOpenGL):
|
|||
return self.point_from_proportion(0.5)
|
||||
|
||||
@property
|
||||
def tip_point(self) -> Point3D:
|
||||
def tip_point(self) -> InternalPoint3D:
|
||||
r"""The tip point of the arrow tip.
|
||||
|
||||
Examples
|
||||
|
|
@ -147,6 +149,9 @@ class ArrowTip(VMobject, metaclass=ConvertToOpenGL):
|
|||
array([2., 0., 0.])
|
||||
|
||||
"""
|
||||
# Type inference of extracting an element from a list, is not
|
||||
# supported by numpy, see this numpy issue
|
||||
# https://github.com/numpy/numpy/issues/16544
|
||||
return self.points[0]
|
||||
|
||||
@property
|
||||
|
|
@ -182,7 +187,7 @@ class ArrowTip(VMobject, metaclass=ConvertToOpenGL):
|
|||
return angle_of_vector(self.vector)
|
||||
|
||||
@property
|
||||
def length(self) -> np.floating:
|
||||
def length(self) -> float:
|
||||
r"""The length of the arrow tip.
|
||||
|
||||
Examples
|
||||
|
|
@ -195,7 +200,7 @@ class ArrowTip(VMobject, metaclass=ConvertToOpenGL):
|
|||
0.35
|
||||
|
||||
"""
|
||||
return np.linalg.norm(self.vector)
|
||||
return float(np.linalg.norm(self.vector))
|
||||
|
||||
|
||||
class StealthTip(ArrowTip):
|
||||
|
|
@ -207,36 +212,38 @@ class StealthTip(ArrowTip):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
fill_opacity=1,
|
||||
stroke_width=3,
|
||||
length=DEFAULT_ARROW_TIP_LENGTH / 2,
|
||||
start_angle=PI,
|
||||
**kwargs,
|
||||
fill_opacity: float = 1,
|
||||
stroke_width: float = 3,
|
||||
length: float = DEFAULT_ARROW_TIP_LENGTH / 2,
|
||||
start_angle: float = PI,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.start_angle = start_angle
|
||||
VMobject.__init__(
|
||||
self, fill_opacity=fill_opacity, stroke_width=stroke_width, **kwargs
|
||||
)
|
||||
self.set_points_as_corners(
|
||||
[
|
||||
[2, 0, 0], # tip
|
||||
[-1.2, 1.6, 0],
|
||||
[0, 0, 0], # base
|
||||
[-1.2, -1.6, 0],
|
||||
[2, 0, 0], # close path, back to tip
|
||||
]
|
||||
np.array(
|
||||
[
|
||||
[2, 0, 0], # tip
|
||||
[-1.2, 1.6, 0],
|
||||
[0, 0, 0], # base
|
||||
[-1.2, -1.6, 0],
|
||||
[2, 0, 0], # close path, back to tip
|
||||
]
|
||||
)
|
||||
)
|
||||
self.scale(length / self.length)
|
||||
|
||||
@property
|
||||
def length(self):
|
||||
def length(self) -> float:
|
||||
"""The length of the arrow tip.
|
||||
|
||||
In this case, the length is computed as the height of
|
||||
the triangle encompassing the stealth tip (otherwise,
|
||||
the tip is scaled too large).
|
||||
"""
|
||||
return np.linalg.norm(self.vector) * 1.6
|
||||
return float(np.linalg.norm(self.vector) * 1.6)
|
||||
|
||||
|
||||
class ArrowTriangleTip(ArrowTip, Triangle):
|
||||
|
|
@ -249,7 +256,7 @@ class ArrowTriangleTip(ArrowTip, Triangle):
|
|||
length: float = DEFAULT_ARROW_TIP_LENGTH,
|
||||
width: float = DEFAULT_ARROW_TIP_LENGTH,
|
||||
start_angle: float = PI,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
Triangle.__init__(
|
||||
self,
|
||||
|
|
@ -271,7 +278,7 @@ class ArrowTriangleFilledTip(ArrowTriangleTip):
|
|||
"""
|
||||
|
||||
def __init__(
|
||||
self, fill_opacity: float = 1, stroke_width: float = 0, **kwargs
|
||||
self, fill_opacity: float = 1, stroke_width: float = 0, **kwargs: Any
|
||||
) -> None:
|
||||
super().__init__(fill_opacity=fill_opacity, stroke_width=stroke_width, **kwargs)
|
||||
|
||||
|
|
@ -285,7 +292,7 @@ class ArrowCircleTip(ArrowTip, Circle):
|
|||
stroke_width: float = 3,
|
||||
length: float = DEFAULT_ARROW_TIP_LENGTH,
|
||||
start_angle: float = PI,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.start_angle = start_angle
|
||||
Circle.__init__(
|
||||
|
|
@ -299,7 +306,7 @@ class ArrowCircleFilledTip(ArrowCircleTip):
|
|||
r"""Circular arrow tip with filled tip."""
|
||||
|
||||
def __init__(
|
||||
self, fill_opacity: float = 1, stroke_width: float = 0, **kwargs
|
||||
self, fill_opacity: float = 1, stroke_width: float = 0, **kwargs: Any
|
||||
) -> None:
|
||||
super().__init__(fill_opacity=fill_opacity, stroke_width=stroke_width, **kwargs)
|
||||
|
||||
|
|
@ -313,7 +320,7 @@ class ArrowSquareTip(ArrowTip, Square):
|
|||
stroke_width: float = 3,
|
||||
length: float = DEFAULT_ARROW_TIP_LENGTH,
|
||||
start_angle: float = PI,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.start_angle = start_angle
|
||||
Square.__init__(
|
||||
|
|
@ -331,6 +338,6 @@ class ArrowSquareFilledTip(ArrowSquareTip):
|
|||
r"""Square arrow tip with filled tip."""
|
||||
|
||||
def __init__(
|
||||
self, fill_opacity: float = 1, stroke_width: float = 0, **kwargs
|
||||
self, fill_opacity: float = 1, stroke_width: float = 0, **kwargs: Any
|
||||
) -> None:
|
||||
super().__init__(fill_opacity=fill_opacity, stroke_width=stroke_width, **kwargs)
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ if TYPE_CHECKING:
|
|||
from manim.animation.animation import Animation
|
||||
from manim.typing import (
|
||||
FunctionOverride,
|
||||
InternalPoint3D,
|
||||
ManimFloat,
|
||||
ManimInt,
|
||||
MappingFunction,
|
||||
|
|
@ -2156,17 +2157,17 @@ class Mobject:
|
|||
"""Returns z Point3D of the center of the :class:`~.Mobject` as ``float``"""
|
||||
return self.get_coord(2, direction)
|
||||
|
||||
def get_start(self) -> Point3D:
|
||||
def get_start(self) -> InternalPoint3D:
|
||||
"""Returns the point, where the stroke that surrounds the :class:`~.Mobject` starts."""
|
||||
self.throw_error_if_no_points()
|
||||
return np.array(self.points[0])
|
||||
|
||||
def get_end(self) -> Point3D:
|
||||
def get_end(self) -> InternalPoint3D:
|
||||
"""Returns the point, where the stroke that surrounds the :class:`~.Mobject` ends."""
|
||||
self.throw_error_if_no_points()
|
||||
return np.array(self.points[-1])
|
||||
|
||||
def get_start_and_end(self) -> tuple[Point3D, Point3D]:
|
||||
def get_start_and_end(self) -> tuple[InternalPoint3D, InternalPoint3D]:
|
||||
"""Returns starting and ending point of a stroke as a ``tuple``."""
|
||||
return self.get_start(), self.get_end()
|
||||
|
||||
|
|
|
|||
|
|
@ -10,11 +10,20 @@ from manim.mobject.geometry.polygram import Polygon
|
|||
from manim.mobject.graph import Graph
|
||||
from manim.mobject.three_d.three_dimensions import Dot3D
|
||||
from manim.mobject.types.vectorized_mobject import VGroup
|
||||
from manim.utils.qhull import QuickHull
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.typing import Point3D
|
||||
|
||||
__all__ = ["Polyhedron", "Tetrahedron", "Octahedron", "Icosahedron", "Dodecahedron"]
|
||||
__all__ = [
|
||||
"Polyhedron",
|
||||
"Tetrahedron",
|
||||
"Octahedron",
|
||||
"Icosahedron",
|
||||
"Dodecahedron",
|
||||
"ConvexHull3D",
|
||||
]
|
||||
|
||||
|
||||
class Polyhedron(VGroup):
|
||||
|
|
@ -361,3 +370,91 @@ class Dodecahedron(Polyhedron):
|
|||
],
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class ConvexHull3D(Polyhedron):
|
||||
"""A convex hull for a set of points
|
||||
|
||||
Parameters
|
||||
----------
|
||||
points
|
||||
The points to consider.
|
||||
tolerance
|
||||
The tolerance used for quickhull.
|
||||
kwargs
|
||||
Forwarded to the parent constructor.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. manim:: ConvexHull3DExample
|
||||
:save_last_frame:
|
||||
:quality: high
|
||||
|
||||
class ConvexHull3DExample(ThreeDScene):
|
||||
def construct(self):
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
points = [
|
||||
[ 1.93192757, 0.44134585, -1.52407061],
|
||||
[-0.93302521, 1.23206983, 0.64117067],
|
||||
[-0.44350918, -0.61043677, 0.21723705],
|
||||
[-0.42640268, -1.05260843, 1.61266094],
|
||||
[-1.84449637, 0.91238739, -1.85172623],
|
||||
[ 1.72068132, -0.11880457, 0.51881751],
|
||||
[ 0.41904805, 0.44938012, -1.86440686],
|
||||
[ 0.83864666, 1.66653337, 1.88960123],
|
||||
[ 0.22240514, -0.80986286, 1.34249326],
|
||||
[-1.29585759, 1.01516189, 0.46187522],
|
||||
[ 1.7776499, -1.59550796, -1.70240747],
|
||||
[ 0.80065226, -0.12530398, 1.70063977],
|
||||
[ 1.28960948, -1.44158255, 1.39938582],
|
||||
[-0.93538943, 1.33617705, -0.24852643],
|
||||
[-1.54868271, 1.7444399, -0.46170734]
|
||||
]
|
||||
hull = ConvexHull3D(
|
||||
*points,
|
||||
faces_config = {"stroke_opacity": 0},
|
||||
graph_config = {
|
||||
"vertex_type": Dot3D,
|
||||
"edge_config": {
|
||||
"stroke_color": BLUE,
|
||||
"stroke_width": 2,
|
||||
"stroke_opacity": 0.05,
|
||||
}
|
||||
}
|
||||
)
|
||||
dots = VGroup(*[Dot3D(point) for point in points])
|
||||
self.add(hull)
|
||||
self.add(dots)
|
||||
"""
|
||||
|
||||
def __init__(self, *points: Point3D, tolerance: float = 1e-5, **kwargs):
|
||||
# Build Convex Hull
|
||||
array = np.array(points)
|
||||
hull = QuickHull(tolerance)
|
||||
hull.build(array)
|
||||
|
||||
# Setup Lists
|
||||
vertices = []
|
||||
faces = []
|
||||
|
||||
# Extract Faces
|
||||
c = 0
|
||||
d = {}
|
||||
facets = set(hull.facets) - hull.removed
|
||||
for facet in facets:
|
||||
tmp = set()
|
||||
for subfacet in facet.subfacets:
|
||||
for point in subfacet.points:
|
||||
if point not in d:
|
||||
vertices.append(point.coordinates)
|
||||
d[point] = c
|
||||
c += 1
|
||||
tmp.add(point)
|
||||
faces.append([d[point] for point in tmp])
|
||||
|
||||
# Call Polyhedron
|
||||
super().__init__(
|
||||
vertex_coords=vertices,
|
||||
faces_list=faces,
|
||||
**kwargs,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
|||
__all__ = ["AbstractImageMobject", "ImageMobject", "ImageMobjectFromCamera"]
|
||||
|
||||
import pathlib
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
|
@ -21,6 +22,14 @@ from ...utils.images import change_to_rgba_array, get_full_raster_image_path
|
|||
|
||||
__all__ = ["ImageMobject", "ImageMobjectFromCamera"]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
import numpy.typing as npt
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim.typing import StrPath
|
||||
|
||||
|
||||
class AbstractImageMobject(Mobject):
|
||||
"""
|
||||
|
|
@ -39,23 +48,23 @@ class AbstractImageMobject(Mobject):
|
|||
def __init__(
|
||||
self,
|
||||
scale_to_resolution: int,
|
||||
pixel_array_dtype="uint8",
|
||||
resampling_algorithm=Resampling.BICUBIC,
|
||||
**kwargs,
|
||||
):
|
||||
pixel_array_dtype: str = "uint8",
|
||||
resampling_algorithm: Resampling = Resampling.BICUBIC,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.pixel_array_dtype = pixel_array_dtype
|
||||
self.scale_to_resolution = scale_to_resolution
|
||||
self.set_resampling_algorithm(resampling_algorithm)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def get_pixel_array(self):
|
||||
def get_pixel_array(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_color(self, color, alpha=None, family=True):
|
||||
# Likely to be implemented in subclasses, but no obligation
|
||||
pass
|
||||
|
||||
def set_resampling_algorithm(self, resampling_algorithm: int):
|
||||
def set_resampling_algorithm(self, resampling_algorithm: int) -> Self:
|
||||
"""
|
||||
Sets the interpolation method for upscaling the image. By default the image is
|
||||
interpolated using bicubic algorithm. This method lets you change it.
|
||||
|
|
@ -87,7 +96,7 @@ class AbstractImageMobject(Mobject):
|
|||
)
|
||||
return self
|
||||
|
||||
def reset_points(self):
|
||||
def reset_points(self) -> None:
|
||||
"""Sets :attr:`points` to be the four image corners."""
|
||||
self.points = np.array(
|
||||
[
|
||||
|
|
@ -171,15 +180,15 @@ class ImageMobject(AbstractImageMobject):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
filename_or_array,
|
||||
filename_or_array: StrPath | npt.NDArray,
|
||||
scale_to_resolution: int = QUALITIES[DEFAULT_QUALITY]["pixel_height"],
|
||||
invert=False,
|
||||
image_mode="RGBA",
|
||||
**kwargs,
|
||||
):
|
||||
self.fill_opacity = 1
|
||||
self.stroke_opacity = 1
|
||||
self.invert = invert
|
||||
invert: bool = False,
|
||||
image_mode: str = "RGBA",
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.fill_opacity: float = 1
|
||||
self.stroke_opacity: float = 1
|
||||
self.invert_image = invert
|
||||
self.image_mode = image_mode
|
||||
if isinstance(filename_or_array, (str, pathlib.PurePath)):
|
||||
path = get_full_raster_image_path(filename_or_array)
|
||||
|
|
@ -192,7 +201,7 @@ class ImageMobject(AbstractImageMobject):
|
|||
self.pixel_array = change_to_rgba_array(
|
||||
self.pixel_array, self.pixel_array_dtype
|
||||
)
|
||||
if self.invert:
|
||||
if self.invert_image:
|
||||
self.pixel_array[:, :, :3] = (
|
||||
np.iinfo(self.pixel_array_dtype).max - self.pixel_array[:, :, :3]
|
||||
)
|
||||
|
|
@ -212,7 +221,7 @@ class ImageMobject(AbstractImageMobject):
|
|||
self.color = color
|
||||
return self
|
||||
|
||||
def set_opacity(self, alpha: float):
|
||||
def set_opacity(self, alpha: float) -> Self:
|
||||
"""Sets the image's opacity.
|
||||
|
||||
Parameters
|
||||
|
|
@ -226,7 +235,7 @@ class ImageMobject(AbstractImageMobject):
|
|||
self.stroke_opacity = alpha
|
||||
return self
|
||||
|
||||
def fade(self, darkness: float = 0.5, family: bool = True):
|
||||
def fade(self, darkness: float = 0.5, family: bool = True) -> Self:
|
||||
"""Sets the image's opacity using a 1 - alpha relationship.
|
||||
|
||||
Parameters
|
||||
|
|
@ -243,7 +252,7 @@ class ImageMobject(AbstractImageMobject):
|
|||
|
||||
def interpolate_color(
|
||||
self, mobject1: ImageMobject, mobject2: ImageMobject, alpha: float
|
||||
):
|
||||
) -> None:
|
||||
"""Interpolates the array of pixel color values from one ImageMobject
|
||||
into an array of equal size in the target ImageMobject.
|
||||
|
||||
|
|
@ -279,7 +288,7 @@ class ImageMobject(AbstractImageMobject):
|
|||
alpha,
|
||||
).astype(self.pixel_array_dtype)
|
||||
|
||||
def get_style(self):
|
||||
def get_style(self) -> dict[str, Any]:
|
||||
return {
|
||||
"fill_color": ManimColor(self.color.get_rgb()).to_hex(),
|
||||
"fill_opacity": self.fill_opacity,
|
||||
|
|
@ -292,7 +301,12 @@ class ImageMobject(AbstractImageMobject):
|
|||
|
||||
|
||||
class ImageMobjectFromCamera(AbstractImageMobject):
|
||||
def __init__(self, camera, default_display_frame_config=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
camera,
|
||||
default_display_frame_config: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.camera = camera
|
||||
if default_display_frame_config is None:
|
||||
default_display_frame_config = {
|
||||
|
|
@ -309,14 +323,14 @@ class ImageMobjectFromCamera(AbstractImageMobject):
|
|||
self.pixel_array = self.camera.pixel_array
|
||||
return self.pixel_array
|
||||
|
||||
def add_display_frame(self, **kwargs):
|
||||
def add_display_frame(self, **kwargs: Any) -> Self:
|
||||
config = dict(self.default_display_frame_config)
|
||||
config.update(kwargs)
|
||||
self.display_frame = SurroundingRectangle(self, **config)
|
||||
self.add(self.display_frame)
|
||||
return self
|
||||
|
||||
def interpolate_color(self, mobject1, mobject2, alpha):
|
||||
def interpolate_color(self, mobject1, mobject2, alpha) -> None:
|
||||
assert mobject1.pixel_array.shape == mobject2.pixel_array.shape, (
|
||||
f"Mobject pixel array shapes incompatible for interpolation.\n"
|
||||
f"Mobject 1 ({mobject1}) : {mobject1.pixel_array.shape}\n"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["PMobject", "Mobject1D", "Mobject2D", "PGroup", "PointCloudDot", "Point"]
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
|
|
@ -17,6 +19,7 @@ from ...utils.color import (
|
|||
WHITE,
|
||||
YELLOW,
|
||||
ManimColor,
|
||||
ParsableManimColor,
|
||||
color_gradient,
|
||||
color_to_rgba,
|
||||
rgba_to_color,
|
||||
|
|
@ -25,6 +28,15 @@ from ...utils.iterables import stretch_array_to_length
|
|||
|
||||
__all__ = ["PMobject", "Mobject1D", "Mobject2D", "PGroup", "PointCloudDot"]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
import numpy.typing as npt
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim.typing import ManimFloat, Point3D, Vector3D
|
||||
|
||||
|
||||
class PMobject(Mobject, metaclass=ConvertToOpenGL):
|
||||
"""A disc made of a cloud of Dots
|
||||
|
|
@ -55,19 +67,25 @@ class PMobject(Mobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, stroke_width=DEFAULT_STROKE_WIDTH, **kwargs):
|
||||
def __init__(self, stroke_width: int = DEFAULT_STROKE_WIDTH, **kwargs: Any) -> None:
|
||||
self.stroke_width = stroke_width
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def reset_points(self):
|
||||
def reset_points(self) -> Self:
|
||||
self.rgbas = np.zeros((0, 4))
|
||||
self.points = np.zeros((0, 3))
|
||||
return self
|
||||
|
||||
def get_array_attrs(self):
|
||||
def get_array_attrs(self) -> list[str]:
|
||||
return super().get_array_attrs() + ["rgbas"]
|
||||
|
||||
def add_points(self, points, rgbas=None, color=None, alpha=1):
|
||||
def add_points(
|
||||
self,
|
||||
points: npt.NDArray,
|
||||
rgbas: npt.NDArray | None = None,
|
||||
color: ParsableManimColor | None = None,
|
||||
alpha: float = 1,
|
||||
) -> Self:
|
||||
"""Add points.
|
||||
|
||||
Points must be a Nx3 numpy array.
|
||||
|
|
@ -85,24 +103,26 @@ class PMobject(Mobject, metaclass=ConvertToOpenGL):
|
|||
self.rgbas = np.append(self.rgbas, rgbas, axis=0)
|
||||
return self
|
||||
|
||||
def set_color(self, color=YELLOW, family=True):
|
||||
def set_color(
|
||||
self, color: ParsableManimColor = YELLOW, family: bool = True
|
||||
) -> Self:
|
||||
rgba = color_to_rgba(color)
|
||||
mobs = self.family_members_with_points() if family else [self]
|
||||
for mob in mobs:
|
||||
mob.rgbas[:, :] = rgba
|
||||
self.color = color
|
||||
self.color = ManimColor.parse(color)
|
||||
return self
|
||||
|
||||
def get_stroke_width(self):
|
||||
def get_stroke_width(self) -> int:
|
||||
return self.stroke_width
|
||||
|
||||
def set_stroke_width(self, width, family=True):
|
||||
def set_stroke_width(self, width: int, family: bool = True) -> Self:
|
||||
mobs = self.family_members_with_points() if family else [self]
|
||||
for mob in mobs:
|
||||
mob.stroke_width = width
|
||||
return self
|
||||
|
||||
def set_color_by_gradient(self, *colors):
|
||||
def set_color_by_gradient(self, *colors: ParsableManimColor) -> Self:
|
||||
self.rgbas = np.array(
|
||||
list(map(color_to_rgba, color_gradient(*colors, len(self.points)))),
|
||||
)
|
||||
|
|
@ -110,11 +130,11 @@ class PMobject(Mobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
def set_colors_by_radial_gradient(
|
||||
self,
|
||||
center=None,
|
||||
radius=1,
|
||||
inner_color=WHITE,
|
||||
outer_color=BLACK,
|
||||
):
|
||||
center: Point3D | None = None,
|
||||
radius: float = 1,
|
||||
inner_color: ParsableManimColor = WHITE,
|
||||
outer_color: ParsableManimColor = BLACK,
|
||||
) -> Self:
|
||||
start_rgba, end_rgba = list(map(color_to_rgba, [inner_color, outer_color]))
|
||||
if center is None:
|
||||
center = self.get_center()
|
||||
|
|
@ -129,19 +149,19 @@ class PMobject(Mobject, metaclass=ConvertToOpenGL):
|
|||
)
|
||||
return self
|
||||
|
||||
def match_colors(self, mobject):
|
||||
def match_colors(self, mobject: Mobject) -> Self:
|
||||
Mobject.align_data(self, mobject)
|
||||
self.rgbas = np.array(mobject.rgbas)
|
||||
return self
|
||||
|
||||
def filter_out(self, condition):
|
||||
def filter_out(self, condition: npt.NDArray) -> Self:
|
||||
for mob in self.family_members_with_points():
|
||||
to_eliminate = ~np.apply_along_axis(condition, 1, mob.points)
|
||||
mob.points = mob.points[to_eliminate]
|
||||
mob.rgbas = mob.rgbas[to_eliminate]
|
||||
return self
|
||||
|
||||
def thin_out(self, factor=5):
|
||||
def thin_out(self, factor: int = 5) -> Self:
|
||||
"""Removes all but every nth point for n = factor"""
|
||||
for mob in self.family_members_with_points():
|
||||
num_points = self.get_num_points()
|
||||
|
|
@ -150,23 +170,27 @@ class PMobject(Mobject, metaclass=ConvertToOpenGL):
|
|||
)
|
||||
return self
|
||||
|
||||
def sort_points(self, function=lambda p: p[0]):
|
||||
def sort_points(
|
||||
self, function: Callable[[npt.NDArray[ManimFloat]], float] = lambda p: p[0]
|
||||
) -> Self:
|
||||
"""Function is any map from R^3 to R"""
|
||||
for mob in self.family_members_with_points():
|
||||
indices = np.argsort(np.apply_along_axis(function, 1, mob.points))
|
||||
mob.apply_over_attr_arrays(lambda arr, idx=indices: arr[idx])
|
||||
return self
|
||||
|
||||
def fade_to(self, color, alpha, family=True):
|
||||
def fade_to(
|
||||
self, color: ParsableManimColor, alpha: float, family: bool = True
|
||||
) -> Self:
|
||||
self.rgbas = interpolate(self.rgbas, color_to_rgba(color), alpha)
|
||||
for mob in self.submobjects:
|
||||
mob.fade_to(color, alpha, family)
|
||||
return self
|
||||
|
||||
def get_all_rgbas(self):
|
||||
def get_all_rgbas(self) -> npt.NDArray:
|
||||
return self.get_merged_array("rgbas")
|
||||
|
||||
def ingest_submobjects(self):
|
||||
def ingest_submobjects(self) -> Self:
|
||||
attrs = self.get_array_attrs()
|
||||
arrays = list(map(self.get_merged_array, attrs))
|
||||
for attr, array in zip(attrs, arrays):
|
||||
|
|
@ -175,30 +199,32 @@ class PMobject(Mobject, metaclass=ConvertToOpenGL):
|
|||
self.note_changed_family()
|
||||
return self
|
||||
|
||||
def get_color(self):
|
||||
def get_color(self) -> ManimColor:
|
||||
return rgba_to_color(self.rgbas[0, :])
|
||||
|
||||
def point_from_proportion(self, alpha):
|
||||
def point_from_proportion(self, alpha: float) -> Any:
|
||||
index = alpha * (self.get_num_points() - 1)
|
||||
return self.points[index]
|
||||
return self.points[np.floor(index)]
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_type_class():
|
||||
def get_mobject_type_class() -> type[PMobject]:
|
||||
return PMobject
|
||||
|
||||
# Alignment
|
||||
def align_points_with_larger(self, larger_mobject):
|
||||
def align_points_with_larger(self, larger_mobject: Mobject) -> None:
|
||||
assert isinstance(larger_mobject, PMobject)
|
||||
self.apply_over_attr_arrays(
|
||||
lambda a: stretch_array_to_length(a, larger_mobject.get_num_points()),
|
||||
)
|
||||
|
||||
def get_point_mobject(self, center=None):
|
||||
def get_point_mobject(self, center: Point3D | None = None) -> Point:
|
||||
if center is None:
|
||||
center = self.get_center()
|
||||
return PMobject().set_points([center])
|
||||
|
||||
def interpolate_color(self, mobject1, mobject2, alpha):
|
||||
def interpolate_color(
|
||||
self, mobject1: Mobject, mobject2: Mobject, alpha: float
|
||||
) -> Self:
|
||||
self.rgbas = interpolate(mobject1.rgbas, mobject2.rgbas, alpha)
|
||||
self.set_stroke_width(
|
||||
interpolate(
|
||||
|
|
@ -209,7 +235,7 @@ class PMobject(Mobject, metaclass=ConvertToOpenGL):
|
|||
)
|
||||
return self
|
||||
|
||||
def pointwise_become_partial(self, mobject, a, b):
|
||||
def pointwise_become_partial(self, mobject: Mobject, a: float, b: float) -> None:
|
||||
lower_index, upper_index = (int(x * mobject.get_num_points()) for x in (a, b))
|
||||
for attr in self.get_array_attrs():
|
||||
full_array = getattr(mobject, attr)
|
||||
|
|
@ -219,24 +245,31 @@ class PMobject(Mobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
# TODO, Make the two implementations below non-redundant
|
||||
class Mobject1D(PMobject, metaclass=ConvertToOpenGL):
|
||||
def __init__(self, density=DEFAULT_POINT_DENSITY_1D, **kwargs):
|
||||
def __init__(self, density: int = DEFAULT_POINT_DENSITY_1D, **kwargs: Any) -> None:
|
||||
self.density = density
|
||||
self.epsilon = 1.0 / self.density
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def add_line(self, start, end, color=None):
|
||||
def add_line(
|
||||
self,
|
||||
start: npt.NDArray,
|
||||
end: npt.NDArray,
|
||||
color: ParsableManimColor | None = None,
|
||||
) -> None:
|
||||
start, end = list(map(np.array, [start, end]))
|
||||
length = np.linalg.norm(end - start)
|
||||
if length == 0:
|
||||
points = [start]
|
||||
points = np.array([start])
|
||||
else:
|
||||
epsilon = self.epsilon / length
|
||||
points = [interpolate(start, end, t) for t in np.arange(0, 1, epsilon)]
|
||||
points = np.array(
|
||||
[interpolate(start, end, t) for t in np.arange(0, 1, epsilon)]
|
||||
)
|
||||
self.add_points(points, color=color)
|
||||
|
||||
|
||||
class Mobject2D(PMobject, metaclass=ConvertToOpenGL):
|
||||
def __init__(self, density=DEFAULT_POINT_DENSITY_2D, **kwargs):
|
||||
def __init__(self, density: int = DEFAULT_POINT_DENSITY_2D, **kwargs: Any) -> None:
|
||||
self.density = density
|
||||
self.epsilon = 1.0 / self.density
|
||||
super().__init__(**kwargs)
|
||||
|
|
@ -265,7 +298,7 @@ class PGroup(PMobject):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, *pmobs, **kwargs):
|
||||
def __init__(self, *pmobs: Any, **kwargs: Any) -> None:
|
||||
if not all(isinstance(m, (PMobject, OpenGLPMobject)) for m in pmobs):
|
||||
raise ValueError(
|
||||
"All submobjects must be of type PMobject or OpenGLPMObject"
|
||||
|
|
@ -274,10 +307,13 @@ class PGroup(PMobject):
|
|||
super().__init__(**kwargs)
|
||||
self.add(*pmobs)
|
||||
|
||||
def fade_to(self, color, alpha, family=True):
|
||||
def fade_to(
|
||||
self, color: ParsableManimColor, alpha: float, family: bool = True
|
||||
) -> Self:
|
||||
if family:
|
||||
for mob in self.submobjects:
|
||||
mob.fade_to(color, alpha, family)
|
||||
return self
|
||||
|
||||
|
||||
class PointCloudDot(Mobject1D):
|
||||
|
|
@ -314,13 +350,13 @@ class PointCloudDot(Mobject1D):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
center=ORIGIN,
|
||||
radius=2.0,
|
||||
stroke_width=2,
|
||||
density=DEFAULT_POINT_DENSITY_1D,
|
||||
color=YELLOW,
|
||||
**kwargs,
|
||||
):
|
||||
center: Vector3D = ORIGIN,
|
||||
radius: float = 2.0,
|
||||
stroke_width: int = 2,
|
||||
density: int = DEFAULT_POINT_DENSITY_1D,
|
||||
color: ManimColor = YELLOW,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.radius = radius
|
||||
self.epsilon = 1.0 / density
|
||||
super().__init__(
|
||||
|
|
@ -328,20 +364,22 @@ class PointCloudDot(Mobject1D):
|
|||
)
|
||||
self.shift(center)
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
self.reset_points()
|
||||
self.generate_points()
|
||||
|
||||
def generate_points(self):
|
||||
def generate_points(self) -> None:
|
||||
self.add_points(
|
||||
[
|
||||
r * (np.cos(theta) * RIGHT + np.sin(theta) * UP)
|
||||
for r in np.arange(self.epsilon, self.radius, self.epsilon)
|
||||
# Num is equal to int(stop - start)/ (step + 1) reformulated.
|
||||
for theta in np.linspace(
|
||||
0,
|
||||
2 * np.pi,
|
||||
num=int(2 * np.pi * (r + self.epsilon) / self.epsilon),
|
||||
)
|
||||
],
|
||||
np.array(
|
||||
[
|
||||
r * (np.cos(theta) * RIGHT + np.sin(theta) * UP)
|
||||
for r in np.arange(self.epsilon, self.radius, self.epsilon)
|
||||
# Num is equal to int(stop - start)/ (step + 1) reformulated.
|
||||
for theta in np.linspace(
|
||||
0,
|
||||
2 * np.pi,
|
||||
num=int(2 * np.pi * (r + self.epsilon) / self.epsilon),
|
||||
)
|
||||
]
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -49,12 +49,15 @@ from manim.utils.iterables import (
|
|||
from manim.utils.space_ops import rotate_vector, shoelace_direction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
import numpy.typing as npt
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim.typing import (
|
||||
BezierPoints,
|
||||
CubicBezierPoints,
|
||||
InternalPoint3D_Array,
|
||||
ManimFloat,
|
||||
MappingFunction,
|
||||
Point2D,
|
||||
|
|
@ -134,7 +137,7 @@ class VMobject(Mobject):
|
|||
tolerance_for_point_equality: float = 1e-6,
|
||||
n_points_per_cubic_curve: int = 4,
|
||||
cap_style: CapStyleType = CapStyleType.AUTO,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.stroke_width = stroke_width
|
||||
if background_stroke_color is not None:
|
||||
|
|
@ -481,6 +484,64 @@ class VMobject(Mobject):
|
|||
self.set_stroke(opacity=opacity, family=family, background=True)
|
||||
return self
|
||||
|
||||
def scale(self, scale_factor: float, scale_stroke: bool = False, **kwargs) -> Self:
|
||||
r"""Scale the size by a factor.
|
||||
|
||||
Default behavior is to scale about the center of the vmobject.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
scale_factor
|
||||
The scaling factor :math:`\alpha`. If :math:`0 < |\alpha| < 1`, the mobject
|
||||
will shrink, and for :math:`|\alpha| > 1` it will grow. Furthermore,
|
||||
if :math:`\alpha < 0`, the mobject is also flipped.
|
||||
scale_stroke
|
||||
Boolean determining if the object's outline is scaled when the object is scaled.
|
||||
If enabled, and object with 2px outline is scaled by a factor of .5, it will have an outline of 1px.
|
||||
kwargs
|
||||
Additional keyword arguments passed to
|
||||
:meth:`~.Mobject.scale`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`VMobject`
|
||||
``self``
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim:: MobjectScaleExample
|
||||
:save_last_frame:
|
||||
|
||||
class MobjectScaleExample(Scene):
|
||||
def construct(self):
|
||||
c1 = Circle(1, RED).set_x(-1)
|
||||
c2 = Circle(1, GREEN).set_x(1)
|
||||
|
||||
vg = VGroup(c1, c2)
|
||||
vg.set_stroke(width=50)
|
||||
self.add(vg)
|
||||
|
||||
self.play(
|
||||
c1.animate.scale(.25),
|
||||
c2.animate.scale(.25,
|
||||
scale_stroke=True)
|
||||
)
|
||||
|
||||
See also
|
||||
--------
|
||||
:meth:`move_to`
|
||||
|
||||
"""
|
||||
if scale_stroke:
|
||||
self.set_stroke(width=abs(scale_factor) * self.get_stroke_width())
|
||||
self.set_stroke(
|
||||
width=abs(scale_factor) * self.get_stroke_width(background=True),
|
||||
background=True,
|
||||
)
|
||||
super().scale(scale_factor, **kwargs)
|
||||
return self
|
||||
|
||||
def fade(self, darkness: float = 0.5, family: bool = True) -> Self:
|
||||
factor = 1.0 - darkness
|
||||
self.set_fill(opacity=factor * self.get_fill_opacity(), family=False)
|
||||
|
|
@ -711,7 +772,7 @@ class VMobject(Mobject):
|
|||
return self
|
||||
|
||||
def set_points(self, points: Point3D_Array) -> Self:
|
||||
self.points: Point3D_Array = np.array(points)
|
||||
self.points: InternalPoint3D_Array = np.array(points)
|
||||
return self
|
||||
|
||||
def set_z(self, z: float) -> Self:
|
||||
|
|
@ -1601,7 +1662,7 @@ class VMobject(Mobject):
|
|||
nppcc = self.n_points_per_cubic_curve
|
||||
return [self.points[i::nppcc] for i in range(nppcc)]
|
||||
|
||||
def get_start_anchors(self) -> Point3D_Array:
|
||||
def get_start_anchors(self) -> InternalPoint3D_Array:
|
||||
"""Returns the start anchors of the bezier curves.
|
||||
|
||||
Returns
|
||||
|
|
@ -2073,10 +2134,8 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
|
|||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*vmobjects: VMobject | Iterable[VMobject],
|
||||
**kwargs,
|
||||
):
|
||||
self, *vmobjects: VMobject | Iterable[VMobject], **kwargs: Any
|
||||
) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.add(*vmobjects)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from manim import config, logger
|
||||
|
||||
from .plugins_flags import get_plugins, list_plugins
|
||||
from manim._config import config, logger
|
||||
from manim.plugins.plugins_flags import get_plugins, list_plugins
|
||||
|
||||
__all__ = [
|
||||
"list_plugins",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
|||
from importlib.metadata import entry_points
|
||||
from typing import Any
|
||||
|
||||
from manim import console
|
||||
from manim._config import console
|
||||
|
||||
__all__ = ["list_plugins"]
|
||||
|
||||
|
|
@ -22,5 +22,5 @@ def list_plugins() -> None:
|
|||
console.print("[green bold]Plugins:[/green bold]", justify="left")
|
||||
|
||||
plugins = get_plugins()
|
||||
for plugin in plugins:
|
||||
console.print(f" • {plugin}")
|
||||
for plugin_name in plugins:
|
||||
console.print(f" • {plugin_name}")
|
||||
|
|
|
|||
|
|
@ -250,13 +250,13 @@ class CairoRenderer:
|
|||
if config["save_last_frame"]:
|
||||
self.skip_animations = True
|
||||
if (
|
||||
config["from_animation_number"]
|
||||
and self.num_plays < config["from_animation_number"]
|
||||
config.from_animation_number > 0
|
||||
and self.num_plays < config.from_animation_number
|
||||
):
|
||||
self.skip_animations = True
|
||||
if (
|
||||
config["upto_animation_number"]
|
||||
and self.num_plays > config["upto_animation_number"]
|
||||
config.upto_animation_number >= 0
|
||||
and self.num_plays > config.upto_animation_number
|
||||
):
|
||||
self.skip_animations = True
|
||||
raise EndSceneEarlyException()
|
||||
|
|
|
|||
|
|
@ -55,6 +55,10 @@ __all__ = [
|
|||
"Point3D",
|
||||
"InternalPoint3D_Array",
|
||||
"Point3D_Array",
|
||||
"InternalPointND",
|
||||
"PointND",
|
||||
"InternalPointND_Array",
|
||||
"PointND_Array",
|
||||
"Vector2D",
|
||||
"Vector2D_Array",
|
||||
"Vector3D",
|
||||
|
|
@ -301,7 +305,7 @@ parameter can handle being passed a `Point3D` instead.
|
|||
"""
|
||||
|
||||
InternalPoint2D_Array: TypeAlias = npt.NDArray[PointDType]
|
||||
"""``shape: (N, 2)``
|
||||
"""``shape: (M, 2)``
|
||||
|
||||
An array of `InternalPoint2D` objects: ``[[float, float], ...]``.
|
||||
|
||||
|
|
@ -311,7 +315,7 @@ An array of `InternalPoint2D` objects: ``[[float, float], ...]``.
|
|||
"""
|
||||
|
||||
Point2D_Array: TypeAlias = Union[InternalPoint2D_Array, tuple[Point2D, ...]]
|
||||
"""``shape: (N, 2)``
|
||||
"""``shape: (M, 2)``
|
||||
|
||||
An array of `Point2D` objects: ``[[float, float], ...]``.
|
||||
|
||||
|
|
@ -339,7 +343,7 @@ A 3-dimensional point: ``[float, float, float]``.
|
|||
"""
|
||||
|
||||
InternalPoint3D_Array: TypeAlias = npt.NDArray[PointDType]
|
||||
"""``shape: (N, 3)``
|
||||
"""``shape: (M, 3)``
|
||||
|
||||
An array of `Point3D` objects: ``[[float, float, float], ...]``.
|
||||
|
||||
|
|
@ -349,7 +353,7 @@ An array of `Point3D` objects: ``[[float, float, float], ...]``.
|
|||
"""
|
||||
|
||||
Point3D_Array: TypeAlias = Union[InternalPoint3D_Array, tuple[Point3D, ...]]
|
||||
"""``shape: (N, 3)``
|
||||
"""``shape: (M, 3)``
|
||||
|
||||
An array of `Point3D` objects: ``[[float, float, float], ...]``.
|
||||
|
||||
|
|
@ -357,6 +361,41 @@ Please refer to the documentation of the function you are using for
|
|||
further type information.
|
||||
"""
|
||||
|
||||
InternalPointND: TypeAlias = npt.NDArray[PointDType]
|
||||
"""``shape: (N,)``
|
||||
|
||||
An N-dimensional point: ``[float, ...]``.
|
||||
|
||||
.. note::
|
||||
This type alias is mostly made available for internal use, and
|
||||
only includes the NumPy type.
|
||||
"""
|
||||
|
||||
PointND: TypeAlias = Union[InternalPointND, tuple[float, ...]]
|
||||
"""``shape: (N,)``
|
||||
|
||||
An N-dimensional point: ``[float, ...]``.
|
||||
"""
|
||||
|
||||
InternalPointND_Array: TypeAlias = npt.NDArray[PointDType]
|
||||
"""``shape: (M, N)``
|
||||
|
||||
An array of `PointND` objects: ``[[float, ...], ...]``.
|
||||
|
||||
.. note::
|
||||
This type alias is mostly made available for internal use, and
|
||||
only includes the NumPy type.
|
||||
"""
|
||||
|
||||
PointND_Array: TypeAlias = Union[InternalPointND_Array, tuple[PointND, ...]]
|
||||
"""``shape: (M, N)``
|
||||
|
||||
An array of `PointND` objects: ``[[float, ...], ...]``.
|
||||
|
||||
Please refer to the documentation of the function you are using for
|
||||
further type information.
|
||||
"""
|
||||
|
||||
|
||||
"""
|
||||
[CATEGORY]
|
||||
|
|
|
|||
96
manim/utils/color/DVIPSNAMES.py
Normal file
96
manim/utils/color/DVIPSNAMES.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
r"""dvips Colors
|
||||
|
||||
This module contains the colors defined in the dvips driver, which are commonly accessed
|
||||
as named colors in LaTeX via the ``\usepackage[dvipsnames]{xcolor}`` package.
|
||||
|
||||
To use the colors from this list, access them directly from the module (which
|
||||
is exposed to Manim's global name space):
|
||||
|
||||
.. code:: pycon
|
||||
|
||||
>>> from manim import DVIPSNAMES
|
||||
>>> DVIPSNAMES.DARKORCHID
|
||||
ManimColor('#A4538A')
|
||||
|
||||
List of Color Constants
|
||||
-----------------------
|
||||
|
||||
These hex values are derived from those specified in the ``xcolor`` package
|
||||
documentation (see https://ctan.org/pkg/xcolor):
|
||||
|
||||
.. automanimcolormodule:: manim.utils.color.DVIPSNAMES
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .core import ManimColor
|
||||
|
||||
AQUAMARINE = ManimColor("#00B5BE")
|
||||
BITTERSWEET = ManimColor("#C04F17")
|
||||
APRICOT = ManimColor("#FBB982")
|
||||
BLACK = ManimColor("#221E1F")
|
||||
BLUE = ManimColor("#2D2F92")
|
||||
BLUEGREEN = ManimColor("#00B3B8")
|
||||
BLUEVIOLET = ManimColor("#473992")
|
||||
BRICKRED = ManimColor("#B6321C")
|
||||
BROWN = ManimColor("#792500")
|
||||
BURNTORANGE = ManimColor("#F7921D")
|
||||
CADETBLUE = ManimColor("#74729A")
|
||||
CARNATIONPINK = ManimColor("#F282B4")
|
||||
CERULEAN = ManimColor("#00A2E3")
|
||||
CORNFLOWERBLUE = ManimColor("#41B0E4")
|
||||
CYAN = ManimColor("#00AEEF")
|
||||
DANDELION = ManimColor("#FDBC42")
|
||||
DARKORCHID = ManimColor("#A4538A")
|
||||
EMERALD = ManimColor("#00A99D")
|
||||
FORESTGREEN = ManimColor("#009B55")
|
||||
FUCHSIA = ManimColor("#8C368C")
|
||||
GOLDENROD = ManimColor("#FFDF42")
|
||||
GRAY = ManimColor("#949698")
|
||||
GREEN = ManimColor("#00A64F")
|
||||
GREENYELLOW = ManimColor("#DFE674")
|
||||
JUNGLEGREEN = ManimColor("#00A99A")
|
||||
LAVENDER = ManimColor("#F49EC4")
|
||||
LIMEGREEN = ManimColor("#8DC73E")
|
||||
MAGENTA = ManimColor("#EC008C")
|
||||
MAHOGANY = ManimColor("#A9341F")
|
||||
MAROON = ManimColor("#AF3235")
|
||||
MELON = ManimColor("#F89E7B")
|
||||
MIDNIGHTBLUE = ManimColor("#006795")
|
||||
MULBERRY = ManimColor("#A93C93")
|
||||
NAVYBLUE = ManimColor("#006EB8")
|
||||
OLIVEGREEN = ManimColor("#3C8031")
|
||||
ORANGE = ManimColor("#F58137")
|
||||
ORANGERED = ManimColor("#ED135A")
|
||||
ORCHID = ManimColor("#AF72B0")
|
||||
PEACH = ManimColor("#F7965A")
|
||||
PERIWINKLE = ManimColor("#7977B8")
|
||||
PINEGREEN = ManimColor("#008B72")
|
||||
PLUM = ManimColor("#92268F")
|
||||
PROCESSBLUE = ManimColor("#00B0F0")
|
||||
PURPLE = ManimColor("#99479B")
|
||||
RAWSIENNA = ManimColor("#974006")
|
||||
RED = ManimColor("#ED1B23")
|
||||
REDORANGE = ManimColor("#F26035")
|
||||
REDVIOLET = ManimColor("#A1246B")
|
||||
RHODAMINE = ManimColor("#EF559F")
|
||||
ROYALBLUE = ManimColor("#0071BC")
|
||||
ROYALPURPLE = ManimColor("#613F99")
|
||||
RUBINERED = ManimColor("#ED017D")
|
||||
SALMON = ManimColor("#F69289")
|
||||
SEAGREEN = ManimColor("#3FBC9D")
|
||||
SEPIA = ManimColor("#671800")
|
||||
SKYBLUE = ManimColor("#46C5DD")
|
||||
SPRINGGREEN = ManimColor("#C6DC67")
|
||||
TAN = ManimColor("#DA9D76")
|
||||
TEALBLUE = ManimColor("#00AEB3")
|
||||
THISTLE = ManimColor("#D883B7")
|
||||
TURQUOISE = ManimColor("#00B4CE")
|
||||
VIOLET = ManimColor("#58429B")
|
||||
VIOLETRED = ManimColor("#EF58A0")
|
||||
WHITE = ManimColor("#FFFFFF")
|
||||
WILDSTRAWBERRY = ManimColor("#EE2967")
|
||||
YELLOW = ManimColor("#FFF200")
|
||||
YELLOWGREEN = ManimColor("#98CC70")
|
||||
YELLOWORANGE = ManimColor("#FAA21A")
|
||||
179
manim/utils/color/SVGNAMES.py
Normal file
179
manim/utils/color/SVGNAMES.py
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
r"""SVG 1.1 Colors
|
||||
|
||||
This module contains the colors defined in the SVG 1.1 specification, which are commonly
|
||||
accessed as named colors in LaTeX via the ``\usepackage[svgnames]{xcolor}`` package.
|
||||
|
||||
To use the colors from this list, access them directly from the module (which
|
||||
is exposed to Manim's global name space):
|
||||
|
||||
.. code:: pycon
|
||||
|
||||
>>> from manim import SVGNAMES
|
||||
>>> SVGNAMES.LIGHTCORAL
|
||||
ManimColor('#EF7F7F')
|
||||
|
||||
List of Color Constants
|
||||
-----------------------
|
||||
|
||||
These hex values are derived from those specified in the ``xcolor`` package
|
||||
documentation (see https://ctan.org/pkg/xcolor):
|
||||
|
||||
.. automanimcolormodule:: manim.utils.color.SVGNAMES
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .core import ManimColor
|
||||
|
||||
ALICEBLUE = ManimColor("#EFF7FF")
|
||||
ANTIQUEWHITE = ManimColor("#F9EAD7")
|
||||
AQUA = ManimColor("#00FFFF")
|
||||
AQUAMARINE = ManimColor("#7EFFD3")
|
||||
AZURE = ManimColor("#EFFFFF")
|
||||
BEIGE = ManimColor("#F4F4DC")
|
||||
BISQUE = ManimColor("#FFE3C4")
|
||||
BLACK = ManimColor("#000000")
|
||||
BLANCHEDALMOND = ManimColor("#FFEACD")
|
||||
BLUE = ManimColor("#0000FF")
|
||||
BLUEVIOLET = ManimColor("#892BE2")
|
||||
BROWN = ManimColor("#A52A2A")
|
||||
BURLYWOOD = ManimColor("#DDB787")
|
||||
CADETBLUE = ManimColor("#5E9EA0")
|
||||
CHARTREUSE = ManimColor("#7EFF00")
|
||||
CHOCOLATE = ManimColor("#D2681D")
|
||||
CORAL = ManimColor("#FF7E4F")
|
||||
CORNFLOWERBLUE = ManimColor("#6395ED")
|
||||
CORNSILK = ManimColor("#FFF7DC")
|
||||
CRIMSON = ManimColor("#DC143B")
|
||||
CYAN = ManimColor("#00FFFF")
|
||||
DARKBLUE = ManimColor("#00008A")
|
||||
DARKCYAN = ManimColor("#008A8A")
|
||||
DARKGOLDENROD = ManimColor("#B7850B")
|
||||
DARKGRAY = ManimColor("#A9A9A9")
|
||||
DARKGREEN = ManimColor("#006300")
|
||||
DARKGREY = ManimColor("#A9A9A9")
|
||||
DARKKHAKI = ManimColor("#BCB66B")
|
||||
DARKMAGENTA = ManimColor("#8A008A")
|
||||
DARKOLIVEGREEN = ManimColor("#546B2F")
|
||||
DARKORANGE = ManimColor("#FF8C00")
|
||||
DARKORCHID = ManimColor("#9931CC")
|
||||
DARKRED = ManimColor("#8A0000")
|
||||
DARKSALMON = ManimColor("#E8967A")
|
||||
DARKSEAGREEN = ManimColor("#8EBB8E")
|
||||
DARKSLATEBLUE = ManimColor("#483D8A")
|
||||
DARKSLATEGRAY = ManimColor("#2F4F4F")
|
||||
DARKSLATEGREY = ManimColor("#2F4F4F")
|
||||
DARKTURQUOISE = ManimColor("#00CED1")
|
||||
DARKVIOLET = ManimColor("#9300D3")
|
||||
DEEPPINK = ManimColor("#FF1492")
|
||||
DEEPSKYBLUE = ManimColor("#00BFFF")
|
||||
DIMGRAY = ManimColor("#686868")
|
||||
DIMGREY = ManimColor("#686868")
|
||||
DODGERBLUE = ManimColor("#1D90FF")
|
||||
FIREBRICK = ManimColor("#B12121")
|
||||
FLORALWHITE = ManimColor("#FFF9EF")
|
||||
FORESTGREEN = ManimColor("#218A21")
|
||||
FUCHSIA = ManimColor("#FF00FF")
|
||||
GAINSBORO = ManimColor("#DCDCDC")
|
||||
GHOSTWHITE = ManimColor("#F7F7FF")
|
||||
GOLD = ManimColor("#FFD700")
|
||||
GOLDENROD = ManimColor("#DAA51F")
|
||||
GRAY = ManimColor("#7F7F7F")
|
||||
GREEN = ManimColor("#007F00")
|
||||
GREENYELLOW = ManimColor("#ADFF2F")
|
||||
GREY = ManimColor("#7F7F7F")
|
||||
HONEYDEW = ManimColor("#EFFFEF")
|
||||
HOTPINK = ManimColor("#FF68B3")
|
||||
INDIANRED = ManimColor("#CD5B5B")
|
||||
INDIGO = ManimColor("#4A0082")
|
||||
IVORY = ManimColor("#FFFFEF")
|
||||
KHAKI = ManimColor("#EFE58C")
|
||||
LAVENDER = ManimColor("#E5E5F9")
|
||||
LAVENDERBLUSH = ManimColor("#FFEFF4")
|
||||
LAWNGREEN = ManimColor("#7CFC00")
|
||||
LEMONCHIFFON = ManimColor("#FFF9CD")
|
||||
LIGHTBLUE = ManimColor("#ADD8E5")
|
||||
LIGHTCORAL = ManimColor("#EF7F7F")
|
||||
LIGHTCYAN = ManimColor("#E0FFFF")
|
||||
LIGHTGOLDENROD = ManimColor("#EDDD82")
|
||||
LIGHTGOLDENRODYELLOW = ManimColor("#F9F9D2")
|
||||
LIGHTGRAY = ManimColor("#D3D3D3")
|
||||
LIGHTGREEN = ManimColor("#90ED90")
|
||||
LIGHTGREY = ManimColor("#D3D3D3")
|
||||
LIGHTPINK = ManimColor("#FFB5C0")
|
||||
LIGHTSALMON = ManimColor("#FFA07A")
|
||||
LIGHTSEAGREEN = ManimColor("#1FB1AA")
|
||||
LIGHTSKYBLUE = ManimColor("#87CEF9")
|
||||
LIGHTSLATEBLUE = ManimColor("#8470FF")
|
||||
LIGHTSLATEGRAY = ManimColor("#778799")
|
||||
LIGHTSLATEGREY = ManimColor("#778799")
|
||||
LIGHTSTEELBLUE = ManimColor("#AFC4DD")
|
||||
LIGHTYELLOW = ManimColor("#FFFFE0")
|
||||
LIME = ManimColor("#00FF00")
|
||||
LIMEGREEN = ManimColor("#31CD31")
|
||||
LINEN = ManimColor("#F9EFE5")
|
||||
MAGENTA = ManimColor("#FF00FF")
|
||||
MAROON = ManimColor("#7F0000")
|
||||
MEDIUMAQUAMARINE = ManimColor("#66CDAA")
|
||||
MEDIUMBLUE = ManimColor("#0000CD")
|
||||
MEDIUMORCHID = ManimColor("#BA54D3")
|
||||
MEDIUMPURPLE = ManimColor("#9270DB")
|
||||
MEDIUMSEAGREEN = ManimColor("#3BB271")
|
||||
MEDIUMSLATEBLUE = ManimColor("#7B68ED")
|
||||
MEDIUMSPRINGGREEN = ManimColor("#00F99A")
|
||||
MEDIUMTURQUOISE = ManimColor("#48D1CC")
|
||||
MEDIUMVIOLETRED = ManimColor("#C61584")
|
||||
MIDNIGHTBLUE = ManimColor("#181870")
|
||||
MINTCREAM = ManimColor("#F4FFF9")
|
||||
MISTYROSE = ManimColor("#FFE3E1")
|
||||
MOCCASIN = ManimColor("#FFE3B5")
|
||||
NAVAJOWHITE = ManimColor("#FFDDAD")
|
||||
NAVY = ManimColor("#00007F")
|
||||
NAVYBLUE = ManimColor("#00007F")
|
||||
OLDLACE = ManimColor("#FCF4E5")
|
||||
OLIVE = ManimColor("#7F7F00")
|
||||
OLIVEDRAB = ManimColor("#6B8D22")
|
||||
ORANGE = ManimColor("#FFA500")
|
||||
ORANGERED = ManimColor("#FF4400")
|
||||
ORCHID = ManimColor("#DA70D6")
|
||||
PALEGOLDENROD = ManimColor("#EDE8AA")
|
||||
PALEGREEN = ManimColor("#97FB97")
|
||||
PALETURQUOISE = ManimColor("#AFEDED")
|
||||
PALEVIOLETRED = ManimColor("#DB7092")
|
||||
PAPAYAWHIP = ManimColor("#FFEED4")
|
||||
PEACHPUFF = ManimColor("#FFDAB8")
|
||||
PERU = ManimColor("#CD843F")
|
||||
PINK = ManimColor("#FFBFCA")
|
||||
PLUM = ManimColor("#DDA0DD")
|
||||
POWDERBLUE = ManimColor("#AFE0E5")
|
||||
PURPLE = ManimColor("#7F007F")
|
||||
RED = ManimColor("#FF0000")
|
||||
ROSYBROWN = ManimColor("#BB8E8E")
|
||||
ROYALBLUE = ManimColor("#4168E1")
|
||||
SADDLEBROWN = ManimColor("#8A4413")
|
||||
SALMON = ManimColor("#F97F72")
|
||||
SANDYBROWN = ManimColor("#F3A45F")
|
||||
SEAGREEN = ManimColor("#2D8A56")
|
||||
SEASHELL = ManimColor("#FFF4ED")
|
||||
SIENNA = ManimColor("#A0512C")
|
||||
SILVER = ManimColor("#BFBFBF")
|
||||
SKYBLUE = ManimColor("#87CEEA")
|
||||
SLATEBLUE = ManimColor("#6959CD")
|
||||
SLATEGRAY = ManimColor("#707F90")
|
||||
SLATEGREY = ManimColor("#707F90")
|
||||
SNOW = ManimColor("#FFF9F9")
|
||||
SPRINGGREEN = ManimColor("#00FF7E")
|
||||
STEELBLUE = ManimColor("#4682B3")
|
||||
TAN = ManimColor("#D2B38C")
|
||||
TEAL = ManimColor("#007F7F")
|
||||
THISTLE = ManimColor("#D8BFD8")
|
||||
TOMATO = ManimColor("#FF6347")
|
||||
TURQUOISE = ManimColor("#3FE0CF")
|
||||
VIOLET = ManimColor("#ED82ED")
|
||||
VIOLETRED = ManimColor("#D01F90")
|
||||
WHEAT = ManimColor("#F4DDB2")
|
||||
WHITE = ManimColor("#FFFFFF")
|
||||
WHITESMOKE = ManimColor("#F4F4F4")
|
||||
YELLOW = ManimColor("#FFFF00")
|
||||
YELLOWGREEN = ManimColor("#9ACD30")
|
||||
|
|
@ -16,8 +16,9 @@ There are several predefined colors available in Manim:
|
|||
|
||||
- The colors listed in :mod:`.color.manim_colors` are loaded into
|
||||
Manim's global name space.
|
||||
- The colors in :mod:`.color.AS2700`, :mod:`.color.BS381`, :mod:`.color.X11`,
|
||||
and :mod:`.color.XKCD` need to be accessed via their module (which are available
|
||||
- The colors in :mod:`.color.AS2700`, :mod:`.color.BS381`,
|
||||
:mod:`.color.DVIPSNAMES`, :mod:`.color.SVGNAMES`, :mod:`.color.X11` and
|
||||
:mod:`.color.XKCD` need to be accessed via their module (which are available
|
||||
in Manim's global name space), or imported separately. For example:
|
||||
|
||||
.. code:: pycon
|
||||
|
|
@ -42,6 +43,8 @@ The following modules contain the predefined color constants:
|
|||
manim_colors
|
||||
AS2700
|
||||
BS381
|
||||
DVIPSNAMES
|
||||
SVGNAMES
|
||||
XKCD
|
||||
X11
|
||||
|
||||
|
|
@ -49,7 +52,7 @@ The following modules contain the predefined color constants:
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import AS2700, BS381, X11, XKCD
|
||||
from . import AS2700, BS381, DVIPSNAMES, SVGNAMES, X11, XKCD
|
||||
from .core import *
|
||||
from .manim_colors import *
|
||||
|
||||
|
|
|
|||
|
|
@ -585,7 +585,7 @@ class ManimColor:
|
|||
new[-1] = alpha
|
||||
return self._construct_from_space(new)
|
||||
|
||||
def interpolate(self, other: ManimColor, alpha: float) -> Self:
|
||||
def interpolate(self, other: Self, alpha: float) -> Self:
|
||||
"""Interpolates between the current and the given ManimColor an returns the interpolated color
|
||||
|
||||
Parameters
|
||||
|
|
@ -606,6 +606,99 @@ class ManimColor:
|
|||
self._internal_space * (1 - alpha) + other._internal_space * alpha
|
||||
)
|
||||
|
||||
def darker(self, blend: float = 0.2) -> Self:
|
||||
"""Returns a new color that is darker than the current color, i.e.
|
||||
interpolated with black. The opacity is unchanged.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
blend : float, optional
|
||||
The blend ratio for the interpolation, from 0 (the current color
|
||||
unchanged) to 1 (pure black). By default 0.2 which results in a
|
||||
slightly darker color
|
||||
|
||||
Returns
|
||||
-------
|
||||
ManimColor
|
||||
The darker ManimColor
|
||||
|
||||
See Also
|
||||
--------
|
||||
:meth:`lighter`
|
||||
"""
|
||||
from manim.utils.color.manim_colors import BLACK
|
||||
|
||||
alpha = self._internal_space[3]
|
||||
black = self._from_internal(BLACK._internal_value)
|
||||
return self.interpolate(black, blend).opacity(alpha)
|
||||
|
||||
def lighter(self, blend: float = 0.2) -> Self:
|
||||
"""Returns a new color that is lighter than the current color, i.e.
|
||||
interpolated with white. The opacity is unchanged.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
blend : float, optional
|
||||
The blend ratio for the interpolation, from 0 (the current color
|
||||
unchanged) to 1 (pure white). By default 0.2 which results in a
|
||||
slightly lighter color
|
||||
|
||||
Returns
|
||||
-------
|
||||
ManimColor
|
||||
The lighter ManimColor
|
||||
|
||||
See Also
|
||||
--------
|
||||
:meth:`darker`
|
||||
"""
|
||||
from manim.utils.color.manim_colors import WHITE
|
||||
|
||||
alpha = self._internal_space[3]
|
||||
white = self._from_internal(WHITE._internal_value)
|
||||
return self.interpolate(white, blend).opacity(alpha)
|
||||
|
||||
def contrasting(
|
||||
self,
|
||||
threshold: float = 0.5,
|
||||
light: Self | None = None,
|
||||
dark: Self | None = None,
|
||||
) -> Self:
|
||||
"""Returns one of two colors, light or dark (by default white or black),
|
||||
that contrasts with the current color (depending on its luminance).
|
||||
This is typically used to set text in a contrasting color that ensures
|
||||
it is readable against a background of the current color.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
threshold : float, optional
|
||||
The luminance threshold that dictates whether the current color is
|
||||
considered light or dark (and thus whether to return the dark or
|
||||
light color, respectively), by default 0.5
|
||||
light : ManimColor, optional
|
||||
The light color to return if the current color is considered dark,
|
||||
by default pure white
|
||||
dark : ManimColor, optional
|
||||
The dark color to return if the current color is considered light,
|
||||
by default pure black
|
||||
|
||||
Returns
|
||||
-------
|
||||
ManimColor
|
||||
The contrasting ManimColor
|
||||
"""
|
||||
from manim.utils.color.manim_colors import BLACK, WHITE
|
||||
|
||||
luminance, _, _ = colorsys.rgb_to_yiq(*self.to_rgb())
|
||||
if luminance < threshold:
|
||||
if light is not None:
|
||||
return light
|
||||
return self._from_internal(WHITE._internal_value)
|
||||
else:
|
||||
if dark is not None:
|
||||
return dark
|
||||
return self._from_internal(BLACK._internal_value)
|
||||
|
||||
@overload
|
||||
def opacity(self, opacity: float) -> Self: ...
|
||||
|
||||
|
|
@ -1293,7 +1386,7 @@ def color_gradient(
|
|||
|
||||
|
||||
def interpolate_color(
|
||||
color1: ManimColorT, color2: ManimColor, alpha: float
|
||||
color1: ManimColorT, color2: ManimColorT, alpha: float
|
||||
) -> ManimColorT:
|
||||
"""Standalone function to interpolate two ManimColors and get the result refer to :meth:`interpolate` in :class:`ManimColor`
|
||||
|
||||
|
|
|
|||
|
|
@ -46,21 +46,22 @@ These colors form Manim's default color space.
|
|||
for line, char in zip(color_groups[0], "abcde"):
|
||||
color_groups.add(Text(char).scale(0.6).next_to(line, LEFT, buff=0.2))
|
||||
|
||||
def named_lines_group(length, colors, names, text_colors, align_to_block):
|
||||
def named_lines_group(length, color_names, labels, align_to_block):
|
||||
colors = [getattr(Colors, color.upper()) for color in color_names]
|
||||
lines = VGroup(
|
||||
*[
|
||||
Line(
|
||||
ORIGIN,
|
||||
RIGHT * length,
|
||||
stroke_width=55,
|
||||
color=getattr(Colors, color.upper()),
|
||||
color=color,
|
||||
)
|
||||
for color in colors
|
||||
]
|
||||
).arrange_submobjects(buff=0.6, direction=DOWN)
|
||||
|
||||
for line, name, color in zip(lines, names, text_colors):
|
||||
line.add(Text(name, color=color).scale(0.6).move_to(line))
|
||||
for line, name, color in zip(lines, labels, colors):
|
||||
line.add(Text(name, color=color.contrasting()).scale(0.6).move_to(line))
|
||||
lines.next_to(color_groups, DOWN, buff=0.5).align_to(
|
||||
color_groups[align_to_block], LEFT
|
||||
)
|
||||
|
|
@ -79,7 +80,6 @@ These colors form Manim's default color space.
|
|||
3.2,
|
||||
other_colors,
|
||||
other_colors,
|
||||
[BLACK] * 4 + [WHITE] * 2,
|
||||
0,
|
||||
)
|
||||
|
||||
|
|
@ -95,7 +95,6 @@ These colors form Manim's default color space.
|
|||
"darker_gray / gray_e",
|
||||
"black",
|
||||
],
|
||||
[BLACK] * 3 + [WHITE] * 4,
|
||||
2,
|
||||
)
|
||||
|
||||
|
|
@ -109,7 +108,6 @@ These colors form Manim's default color space.
|
|||
3.2,
|
||||
pure_colors,
|
||||
pure_colors,
|
||||
[BLACK, BLACK, WHITE],
|
||||
6,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,9 @@ from typing import TYPE_CHECKING
|
|||
import numpy as np
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.scene.scene_file_writer import SceneFileWriter
|
||||
from manim.typing import StrPath
|
||||
|
||||
from ..file_writer import FileWriter
|
||||
|
||||
from manim import __version__, config, console, logger
|
||||
|
||||
|
|
@ -159,7 +161,7 @@ def guarantee_empty_existence(path: Path) -> Path:
|
|||
|
||||
|
||||
def seek_full_path_from_defaults(
|
||||
file_name: str, default_dir: Path, extensions: list[str]
|
||||
file_name: StrPath, default_dir: Path, extensions: list[str]
|
||||
) -> Path:
|
||||
possible_paths = [Path(file_name).expanduser()]
|
||||
possible_paths += [
|
||||
|
|
@ -186,7 +188,7 @@ def modify_atime(file_path: str) -> None:
|
|||
os.utime(file_path, times=(time.time(), Path(file_path).stat().st_mtime))
|
||||
|
||||
|
||||
def open_file(file_path, in_browser=False):
|
||||
def open_file(file_path: Path, in_browser: bool = False) -> None:
|
||||
current_os = platform.system()
|
||||
if current_os == "Windows":
|
||||
os.startfile(file_path if not in_browser else file_path.parent)
|
||||
|
|
@ -209,7 +211,7 @@ def open_file(file_path, in_browser=False):
|
|||
sp.run(commands)
|
||||
|
||||
|
||||
def open_media_file(file_writer: SceneFileWriter) -> None:
|
||||
def open_media_file(file_writer: FileWriter) -> None:
|
||||
file_paths = []
|
||||
|
||||
if config["save_last_frame"]:
|
||||
|
|
@ -249,7 +251,7 @@ def get_template_path() -> Path:
|
|||
return Path.resolve(Path(__file__).parent.parent / "templates")
|
||||
|
||||
|
||||
def add_import_statement(file: Path):
|
||||
def add_import_statement(file: Path) -> None:
|
||||
"""Prepends an import statement in a file
|
||||
|
||||
Parameters
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ __all__ = [
|
|||
"change_to_rgba_array",
|
||||
]
|
||||
|
||||
from pathlib import Path
|
||||
from pathlib import Path, PurePath
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
|
@ -17,8 +18,11 @@ from PIL import Image
|
|||
from .. import config
|
||||
from ..utils.file_ops import seek_full_path_from_defaults
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy.typing as npt
|
||||
|
||||
def get_full_raster_image_path(image_file_name: str) -> Path:
|
||||
|
||||
def get_full_raster_image_path(image_file_name: str | PurePath) -> Path:
|
||||
return seek_full_path_from_defaults(
|
||||
image_file_name,
|
||||
default_dir=config.get_dir("assets_dir"),
|
||||
|
|
@ -26,7 +30,7 @@ def get_full_raster_image_path(image_file_name: str) -> Path:
|
|||
)
|
||||
|
||||
|
||||
def get_full_vector_image_path(image_file_name: str) -> Path:
|
||||
def get_full_vector_image_path(image_file_name: str | PurePath) -> Path:
|
||||
return seek_full_path_from_defaults(
|
||||
image_file_name,
|
||||
default_dir=config.get_dir("assets_dir"),
|
||||
|
|
@ -49,7 +53,7 @@ def invert_image(image: np.array) -> Image:
|
|||
return Image.fromarray(arr)
|
||||
|
||||
|
||||
def change_to_rgba_array(image, dtype="uint8"):
|
||||
def change_to_rgba_array(image: npt.NDArray, dtype="uint8") -> npt.NDArray:
|
||||
"""Converts an RGB array into RGBA with the alpha value opacity maxed."""
|
||||
pa = image
|
||||
if len(pa.shape) == 2:
|
||||
|
|
|
|||
156
manim/utils/polylabel.py
Normal file
156
manim/utils/polylabel.py
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
#!/usr/bin/env python
|
||||
from __future__ import annotations
|
||||
|
||||
from queue import PriorityQueue
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
from manim.typing import Point3D, Point3D_Array
|
||||
|
||||
|
||||
class Polygon:
|
||||
"""
|
||||
Initializes the Polygon with the given rings.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
rings
|
||||
A collection of closed polygonal ring.
|
||||
"""
|
||||
|
||||
def __init__(self, rings: Sequence[Point3D_Array]) -> None:
|
||||
# Flatten Array
|
||||
csum = np.cumsum([ring.shape[0] for ring in rings])
|
||||
self.array = np.concatenate(rings, axis=0)
|
||||
|
||||
# Compute Boundary
|
||||
self.start = np.delete(self.array, csum - 1, axis=0)
|
||||
self.stop = np.delete(self.array, csum % csum[-1], axis=0)
|
||||
self.diff = np.delete(np.diff(self.array, axis=0), csum[:-1] - 1, axis=0)
|
||||
self.norm = self.diff / np.einsum("ij,ij->i", self.diff, self.diff).reshape(
|
||||
-1, 1
|
||||
)
|
||||
|
||||
# Compute Centroid
|
||||
x, y = self.start[:, 0], self.start[:, 1]
|
||||
xr, yr = self.stop[:, 0], self.stop[:, 1]
|
||||
self.area = 0.5 * (np.dot(x, yr) - np.dot(xr, y))
|
||||
if self.area:
|
||||
factor = x * yr - xr * y
|
||||
cx = np.sum((x + xr) * factor) / (6.0 * self.area)
|
||||
cy = np.sum((y + yr) * factor) / (6.0 * self.area)
|
||||
self.centroid = np.array([cx, cy])
|
||||
|
||||
def compute_distance(self, point: Point3D) -> float:
|
||||
"""Compute the minimum distance from a point to the polygon."""
|
||||
scalars = np.einsum("ij,ij->i", self.norm, point - self.start)
|
||||
clips = np.clip(scalars, 0, 1).reshape(-1, 1)
|
||||
d = np.min(np.linalg.norm(self.start + self.diff * clips - point, axis=1))
|
||||
return d if self.inside(point) else -d
|
||||
|
||||
def inside(self, point: Point3D) -> bool:
|
||||
"""Check if a point is inside the polygon."""
|
||||
# Views
|
||||
px, py = point
|
||||
x, y = self.start[:, 0], self.start[:, 1]
|
||||
xr, yr = self.stop[:, 0], self.stop[:, 1]
|
||||
|
||||
# Count Crossings (enforce short-circuit)
|
||||
c = (y > py) != (yr > py)
|
||||
c = px < x[c] + (py - y[c]) * (xr[c] - x[c]) / (yr[c] - y[c])
|
||||
return np.sum(c) % 2 == 1
|
||||
|
||||
|
||||
class Cell:
|
||||
"""
|
||||
A square in a mesh covering the :class:`~.Polygon` passed as an argument.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
c
|
||||
Center coordinates of the Cell.
|
||||
h
|
||||
Half-Size of the Cell.
|
||||
polygon
|
||||
:class:`~.Polygon` object for which the distance is computed.
|
||||
"""
|
||||
|
||||
def __init__(self, c: Point3D, h: float, polygon: Polygon) -> None:
|
||||
self.c = c
|
||||
self.h = h
|
||||
self.d = polygon.compute_distance(self.c)
|
||||
self.p = self.d + self.h * np.sqrt(2)
|
||||
|
||||
def __lt__(self, other: Cell) -> bool:
|
||||
return self.d < other.d
|
||||
|
||||
def __gt__(self, other: Cell) -> bool:
|
||||
return self.d > other.d
|
||||
|
||||
def __le__(self, other: Cell) -> bool:
|
||||
return self.d <= other.d
|
||||
|
||||
def __ge__(self, other: Cell) -> bool:
|
||||
return self.d >= other.d
|
||||
|
||||
|
||||
def polylabel(rings: Sequence[Point3D_Array], precision: float = 0.01) -> Cell:
|
||||
"""
|
||||
Finds the pole of inaccessibility (the point that is farthest from the edges of the polygon)
|
||||
using an iterative grid-based approach.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
rings
|
||||
A list of lists, where each list is a sequence of points representing the rings of the polygon.
|
||||
Typically, multiple rings indicate holes in the polygon.
|
||||
precision
|
||||
The precision of the result (default is 0.01).
|
||||
|
||||
Returns
|
||||
-------
|
||||
Cell
|
||||
A Cell containing the pole of inaccessibility to a given precision.
|
||||
"""
|
||||
# Precompute Polygon Data
|
||||
array = [np.array(ring)[:, :2] for ring in rings]
|
||||
polygon = Polygon(array)
|
||||
|
||||
# Bounding Box
|
||||
mins = np.min(polygon.array, axis=0)
|
||||
maxs = np.max(polygon.array, axis=0)
|
||||
dims = maxs - mins
|
||||
s = np.min(dims)
|
||||
h = s / 2.0
|
||||
|
||||
# Initial Grid
|
||||
queue = PriorityQueue()
|
||||
xv, yv = np.meshgrid(np.arange(mins[0], maxs[0], s), np.arange(mins[1], maxs[1], s))
|
||||
for corner in np.vstack([xv.ravel(), yv.ravel()]).T:
|
||||
queue.put(Cell(corner + h, h, polygon))
|
||||
|
||||
# Initial Guess
|
||||
best = Cell(polygon.centroid, 0, polygon)
|
||||
bbox = Cell(mins + (dims / 2), 0, polygon)
|
||||
if bbox.d > best.d:
|
||||
best = bbox
|
||||
|
||||
# While there are cells to consider...
|
||||
directions = np.array([[-1, -1], [1, -1], [-1, 1], [1, 1]])
|
||||
while not queue.empty():
|
||||
cell = queue.get()
|
||||
if cell > best:
|
||||
best = cell
|
||||
# If a cell is promising, subdivide!
|
||||
if cell.p - best.d > precision:
|
||||
h = cell.h / 2.0
|
||||
offsets = cell.c + directions * h
|
||||
queue.put(Cell(offsets[0], h, polygon))
|
||||
queue.put(Cell(offsets[1], h, polygon))
|
||||
queue.put(Cell(offsets[2], h, polygon))
|
||||
queue.put(Cell(offsets[3], h, polygon))
|
||||
return best
|
||||
196
manim/utils/qhull.py
Normal file
196
manim/utils/qhull.py
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
#!/usr/bin/env python
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.typing import PointND, PointND_Array
|
||||
|
||||
|
||||
class QuickHullPoint:
|
||||
def __init__(self, coordinates: PointND_Array) -> None:
|
||||
self.coordinates = coordinates
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.coordinates.tobytes())
|
||||
|
||||
def __eq__(self, other: QuickHullPoint) -> bool:
|
||||
return np.array_equal(self.coordinates, other.coordinates)
|
||||
|
||||
|
||||
class SubFacet:
|
||||
def __init__(self, coordinates: PointND_Array) -> None:
|
||||
self.coordinates = coordinates
|
||||
self.points = frozenset(QuickHullPoint(c) for c in coordinates)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.points)
|
||||
|
||||
def __eq__(self, other: SubFacet) -> bool:
|
||||
return self.points == other.points
|
||||
|
||||
|
||||
class Facet:
|
||||
def __init__(self, coordinates: PointND_Array, internal: PointND) -> None:
|
||||
self.coordinates = coordinates
|
||||
self.center = np.mean(coordinates, axis=0)
|
||||
self.normal = self.compute_normal(internal)
|
||||
self.subfacets = frozenset(
|
||||
SubFacet(np.delete(self.coordinates, i, axis=0))
|
||||
for i in range(self.coordinates.shape[0])
|
||||
)
|
||||
|
||||
def compute_normal(self, internal: PointND) -> PointND:
|
||||
centered = self.coordinates - self.center
|
||||
_, _, vh = np.linalg.svd(centered)
|
||||
normal = vh[-1, :]
|
||||
normal /= np.linalg.norm(normal)
|
||||
|
||||
# If the normal points towards the internal point, flip it!
|
||||
if np.dot(normal, self.center - internal) < 0:
|
||||
normal *= -1
|
||||
|
||||
return normal
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.subfacets)
|
||||
|
||||
def __eq__(self, other: Facet) -> bool:
|
||||
return self.subfacets == other.subfacets
|
||||
|
||||
|
||||
class Horizon:
|
||||
def __init__(self) -> None:
|
||||
self.facets: set[Facet] = set()
|
||||
self.boundary: list[SubFacet] = []
|
||||
|
||||
|
||||
class QuickHull:
|
||||
"""
|
||||
QuickHull algorithm for constructing a convex hull from a set of points.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tolerance
|
||||
A tolerance threshold for determining when points lie on the convex hull (default is 1e-5).
|
||||
|
||||
Attributes
|
||||
----------
|
||||
facets
|
||||
List of facets considered.
|
||||
removed
|
||||
Set of internal facets that have been removed from the hull during the construction process.
|
||||
outside
|
||||
Dictionary mapping each facet to its outside points and eye point.
|
||||
neighbors
|
||||
Mapping of subfacets to their neighboring facets. Each subfacet links precisely two neighbors.
|
||||
unclaimed
|
||||
Points that have not yet been classified as inside or outside the current hull.
|
||||
internal
|
||||
An internal point (i.e., the center of the initial simplex) used as a reference during hull construction.
|
||||
tolerance
|
||||
The tolerance used to determine if points are considered outside the current hull.
|
||||
"""
|
||||
|
||||
def __init__(self, tolerance: float = 1e-5) -> None:
|
||||
self.facets: list[Facet] = []
|
||||
self.removed: set[Facet] = set()
|
||||
self.outside: dict[Facet, tuple[PointND_Array, PointND | None]] = {}
|
||||
self.neighbors: dict[SubFacet, set[Facet]] = {}
|
||||
self.unclaimed: PointND_Array | None = None
|
||||
self.internal: PointND | None = None
|
||||
self.tolerance = tolerance
|
||||
|
||||
def initialize(self, points: PointND_Array) -> None:
|
||||
# Sample Points
|
||||
simplex = points[
|
||||
np.random.choice(points.shape[0], points.shape[1] + 1, replace=False)
|
||||
]
|
||||
self.unclaimed = points
|
||||
self.internal = np.mean(simplex, axis=0)
|
||||
|
||||
# Build Simplex
|
||||
for c in range(simplex.shape[0]):
|
||||
facet = Facet(np.delete(simplex, c, axis=0), internal=self.internal)
|
||||
self.classify(facet)
|
||||
self.facets.append(facet)
|
||||
|
||||
# Attach Neighbors
|
||||
for f in self.facets:
|
||||
for sf in f.subfacets:
|
||||
self.neighbors.setdefault(sf, set()).add(f)
|
||||
|
||||
def classify(self, facet: Facet) -> None:
|
||||
if not self.unclaimed.size:
|
||||
self.outside[facet] = (None, None)
|
||||
return
|
||||
|
||||
# Compute Projections
|
||||
projections = (self.unclaimed - facet.center) @ facet.normal
|
||||
arg = np.argmax(projections)
|
||||
mask = projections > self.tolerance
|
||||
|
||||
# Identify Eye and Outside Set
|
||||
eye = self.unclaimed[arg] if projections[arg] > self.tolerance else None
|
||||
outside = self.unclaimed[mask]
|
||||
self.outside[facet] = (outside, eye)
|
||||
self.unclaimed = self.unclaimed[~mask]
|
||||
|
||||
def compute_horizon(self, eye: PointND, start_facet: Facet) -> Horizon:
|
||||
horizon = Horizon()
|
||||
self._recursive_horizon(eye, start_facet, horizon)
|
||||
return horizon
|
||||
|
||||
def _recursive_horizon(self, eye: PointND, facet: Facet, horizon: Horizon) -> int:
|
||||
visible = np.dot(facet.normal, eye - facet.center) > 0
|
||||
if not visible:
|
||||
return False
|
||||
# If the eye is visible from the facet...
|
||||
else:
|
||||
# Label the facet as visible and cross each edge
|
||||
horizon.facets.add(facet)
|
||||
for subfacet in facet.subfacets:
|
||||
neighbor = (self.neighbors[subfacet] - {facet}).pop()
|
||||
# If the neighbor is not visible, then the edge shared must be on the boundary
|
||||
if neighbor not in horizon.facets and not self._recursive_horizon(
|
||||
eye, neighbor, horizon
|
||||
):
|
||||
horizon.boundary.append(subfacet)
|
||||
return True
|
||||
|
||||
def build(self, points: PointND_Array):
|
||||
num, dim = points.shape
|
||||
if (dim == 0) or (num < dim + 1):
|
||||
raise ValueError("Not enough points supplied to build Convex Hull!")
|
||||
if dim == 1:
|
||||
raise ValueError("The Convex Hull of 1D data is its min-max!")
|
||||
|
||||
self.initialize(points)
|
||||
while True:
|
||||
updated = False
|
||||
for facet in self.facets:
|
||||
if facet in self.removed:
|
||||
continue
|
||||
outside, eye = self.outside[facet]
|
||||
if eye is not None:
|
||||
updated = True
|
||||
horizon = self.compute_horizon(eye, facet)
|
||||
for f in horizon.facets:
|
||||
self.unclaimed = np.vstack((self.unclaimed, self.outside[f][0]))
|
||||
self.removed.add(f)
|
||||
for sf in f.subfacets:
|
||||
self.neighbors[sf].discard(f)
|
||||
if self.neighbors[sf] == set():
|
||||
del self.neighbors[sf]
|
||||
for sf in horizon.boundary:
|
||||
nf = Facet(
|
||||
np.vstack((sf.coordinates, eye)), internal=self.internal
|
||||
)
|
||||
self.classify(nf)
|
||||
self.facets.append(nf)
|
||||
for nsf in nf.subfacets:
|
||||
self.neighbors.setdefault(nsf, set()).add(nf)
|
||||
if not updated:
|
||||
break
|
||||
11
mypy.ini
11
mypy.ini
|
|
@ -59,10 +59,10 @@ ignore_errors = True
|
|||
ignore_errors = True
|
||||
|
||||
[mypy-manim.cli.*]
|
||||
ignore_errors = True
|
||||
ignore_errors = False
|
||||
|
||||
[mypy-manim.cli.cfg.*]
|
||||
ignore_errors = True
|
||||
ignore_errors = False
|
||||
|
||||
[mypy-manim.gui.*]
|
||||
ignore_errors = True
|
||||
|
|
@ -70,8 +70,8 @@ ignore_errors = True
|
|||
[mypy-manim.mobject.*]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.plugins.*]
|
||||
ignore_errors = True
|
||||
[mypy-manim.mobject.geometry.*]
|
||||
ignore_errors = False
|
||||
|
||||
[mypy-manim.renderer.*]
|
||||
ignore_errors = True
|
||||
|
|
@ -86,9 +86,6 @@ ignore_errors = True
|
|||
ignore_errors = False
|
||||
warn_return_any = False
|
||||
|
||||
[mypy-manim.__main__]
|
||||
ignore_errors = True
|
||||
|
||||
|
||||
# ---------------- We can't properly type this ------------------------
|
||||
|
||||
|
|
|
|||
26
poetry.lock
generated
26
poetry.lock
generated
|
|
@ -4188,22 +4188,22 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "tornado"
|
||||
version = "6.4.1"
|
||||
version = "6.4.2"
|
||||
description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
|
||||
optional = true
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"},
|
||||
{file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"},
|
||||
{file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"},
|
||||
{file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"},
|
||||
{file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"},
|
||||
{file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"},
|
||||
{file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"},
|
||||
{file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"},
|
||||
{file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"},
|
||||
{file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"},
|
||||
{file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-win32.whl", hash = "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38"},
|
||||
{file = "tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -5,17 +5,24 @@ import pytest
|
|||
from manim import FadeIn, Manager, Scene
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"run_time",
|
||||
[0, -1],
|
||||
)
|
||||
def test_animation_forbidden_run_time(run_time):
|
||||
def test_animation_zero_total_run_time():
|
||||
manager = Manager(Scene)
|
||||
test_scene = manager.scene
|
||||
with pytest.raises(
|
||||
ValueError, match="Please set the run_time to a positive number."
|
||||
ValueError, match="The total run_time must be a positive number."
|
||||
):
|
||||
test_scene.play(FadeIn(None, run_time=run_time))
|
||||
test_scene.play(FadeIn(None, run_time=0))
|
||||
|
||||
|
||||
def test_single_animation_zero_run_time_with_more_animations():
|
||||
manager = Manager(Scene)
|
||||
test_scene = manager.scene
|
||||
test_scene.play(FadeIn(None, run_time=0), FadeIn(None, run_time=1))
|
||||
|
||||
|
||||
def test_animation_negative_run_time():
|
||||
with pytest.raises(ValueError, match="The run_time of FadeIn cannot be negative."):
|
||||
FadeIn(None, run_time=-1)
|
||||
|
||||
|
||||
def test_animation_run_time_shorter_than_frame_rate(manim_caplog, config):
|
||||
|
|
@ -25,8 +32,32 @@ def test_animation_run_time_shorter_than_frame_rate(manim_caplog, config):
|
|||
assert "too short for the current frame rate" in manim_caplog.text
|
||||
|
||||
|
||||
def test_wait_run_time_shorter_than_frame_rate(manim_caplog):
|
||||
@pytest.mark.parametrize("duration", [0, -1])
|
||||
def test_wait_invalid_duration(duration):
|
||||
manager = Manager(Scene)
|
||||
test_scene = manager.scene
|
||||
test_scene.wait(1e-9)
|
||||
with pytest.raises(ValueError, match="The duration must be a positive number."):
|
||||
test_scene.wait(duration)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("frozen_frame", [False, True])
|
||||
def test_wait_duration_shorter_than_frame_rate(manim_caplog, frozen_frame):
|
||||
manager = Manager(Scene)
|
||||
test_scene = manager.scene
|
||||
test_scene.wait(1e-9, frozen_frame=frozen_frame)
|
||||
assert "too short for the current frame rate" in manim_caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("duration", [0, -1])
|
||||
def test_pause_invalid_duration(duration):
|
||||
manager = Manager(Scene)
|
||||
test_scene = manager.scene
|
||||
with pytest.raises(ValueError, match="The duration must be a positive number."):
|
||||
test_scene.pause(duration)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("max_time", [0, -1])
|
||||
def test_wait_until_invalid_max_time(max_time):
|
||||
test_scene = Manager(Scene)
|
||||
with pytest.raises(ValueError, match="The max_time must be a positive number."):
|
||||
test_scene.wait_until(lambda: True, max_time)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import logging
|
|||
|
||||
import numpy as np
|
||||
|
||||
from manim import BackgroundRectangle, Circle, Sector, Square
|
||||
from manim import BackgroundRectangle, Circle, Sector, Square, SurroundingRectangle
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -15,9 +15,18 @@ def test_get_arc_center():
|
|||
)
|
||||
|
||||
|
||||
def test_SurroundingRectangle():
|
||||
circle = Circle()
|
||||
square = Square()
|
||||
sr = SurroundingRectangle(circle, square)
|
||||
sr.set_style(fill_opacity=0.42)
|
||||
assert sr.get_fill_opacity() == 0.42
|
||||
|
||||
|
||||
def test_BackgroundRectangle(manim_caplog):
|
||||
c = Circle()
|
||||
bg = BackgroundRectangle(c)
|
||||
circle = Circle()
|
||||
square = Square()
|
||||
bg = BackgroundRectangle(circle, square)
|
||||
bg.set_style(fill_opacity=0.42)
|
||||
assert bg.get_fill_opacity() == 0.42
|
||||
bg.set_style(fill_opacity=1, hello="world")
|
||||
|
|
|
|||
|
|
@ -39,3 +39,25 @@ def test_streamline_attributes_for_single_color():
|
|||
)
|
||||
assert vector_field[0].stroke_width == 1.0
|
||||
assert vector_field[0].stroke_opacity == 0.2
|
||||
|
||||
|
||||
def test_stroke_scale():
|
||||
a = VMobject()
|
||||
b = VMobject()
|
||||
a.set_stroke(width=50)
|
||||
b.set_stroke(width=50)
|
||||
a.scale(0.5)
|
||||
b.scale(0.5, scale_stroke=True)
|
||||
assert a.get_stroke_width() == 50
|
||||
assert b.get_stroke_width() == 25
|
||||
|
||||
|
||||
def test_background_stroke_scale():
|
||||
a = VMobject()
|
||||
b = VMobject()
|
||||
a.set_stroke(width=50, background=True)
|
||||
b.set_stroke(width=50, background=True)
|
||||
a.scale(0.5)
|
||||
b.scale(0.5, scale_stroke=True)
|
||||
assert a.get_stroke_width(background=True) == 50
|
||||
assert b.get_stroke_width(background=True) == 25
|
||||
|
|
|
|||
|
|
@ -26,6 +26,24 @@ def test_scene_add_remove(dry_run):
|
|||
|
||||
# Check that Scene.add() returns the Scene (for chained calls)
|
||||
assert scene.add(Mobject()) is scene
|
||||
|
||||
manager = Manager(Scene)
|
||||
scene = manager.scene
|
||||
to_remove = Mobject()
|
||||
scene = Scene()
|
||||
scene.add(to_remove)
|
||||
scene.add(*(Mobject() for _ in range(10)))
|
||||
assert len(scene.mobjects) == 11
|
||||
scene.remove(to_remove)
|
||||
assert len(scene.mobjects) == 10
|
||||
scene.remove(to_remove)
|
||||
assert len(scene.mobjects) == 10
|
||||
|
||||
# Check that Scene.remove() returns the instance (for chained calls)
|
||||
assert scene.add(Mobject()) is scene
|
||||
|
||||
|
||||
def test_scene_time(dry_run):
|
||||
manager = Manager(Scene)
|
||||
scene = manager.scene
|
||||
assert scene.time == 0
|
||||
|
|
@ -35,7 +53,7 @@ def test_scene_add_remove(dry_run):
|
|||
assert pytest.approx(scene.time) == 2.5
|
||||
scene._original_skipping_status = True
|
||||
scene.play(FadeIn(Square()), run_time=5) # this animation gets skipped.
|
||||
assert pytest.approx(scene.renderer.time) == 7.5
|
||||
assert pytest.approx(scene.time) == 7.5
|
||||
|
||||
|
||||
def test_subcaption(dry_run):
|
||||
|
|
|
|||
|
|
@ -173,3 +173,32 @@ def test_hsv_init() -> None:
|
|||
|
||||
def test_into_HSV() -> None:
|
||||
nt.assert_equal(RED.into(HSV).into(ManimColor), RED)
|
||||
|
||||
|
||||
def test_contrasting() -> None:
|
||||
nt.assert_equal(BLACK.contrasting(), WHITE)
|
||||
nt.assert_equal(WHITE.contrasting(), BLACK)
|
||||
nt.assert_equal(RED.contrasting(0.1), BLACK)
|
||||
nt.assert_equal(RED.contrasting(0.9), WHITE)
|
||||
nt.assert_equal(BLACK.contrasting(dark=GREEN, light=RED), RED)
|
||||
nt.assert_equal(WHITE.contrasting(dark=GREEN, light=RED), GREEN)
|
||||
|
||||
|
||||
def test_lighter() -> None:
|
||||
c = RED.opacity(0.42)
|
||||
cl = c.lighter(0.2)
|
||||
nt.assert_array_equal(
|
||||
cl._internal_value[:3],
|
||||
0.8 * c._internal_value[:3] + 0.2 * WHITE._internal_value[:3],
|
||||
)
|
||||
nt.assert_equal(cl[-1], c[-1])
|
||||
|
||||
|
||||
def test_darker() -> None:
|
||||
c = RED.opacity(0.42)
|
||||
cd = c.darker(0.2)
|
||||
nt.assert_array_equal(
|
||||
cd._internal_value[:3],
|
||||
0.8 * c._internal_value[:3] + 0.2 * BLACK._internal_value[:3],
|
||||
)
|
||||
nt.assert_equal(cd[-1], c[-1])
|
||||
|
|
|
|||
|
|
@ -244,3 +244,24 @@ def test_tex_template_file(tmp_path):
|
|||
|
||||
assert Path(custom_config.tex_template_file) == tex_file
|
||||
assert custom_config.tex_template.body == "Hello World!"
|
||||
|
||||
|
||||
def test_from_to_animations_only_first_animation(config):
|
||||
config: ManimConfig
|
||||
config.from_animation_number = 0
|
||||
config.upto_animation_number = 0
|
||||
|
||||
class SceneWithTwoAnimations(Scene):
|
||||
def construct(self):
|
||||
self.after_first_animation = False
|
||||
s = Square()
|
||||
self.add(s)
|
||||
self.play(s.animate.scale(2))
|
||||
self.renderer.update_skipping_status()
|
||||
self.after_first_animation = True
|
||||
self.play(s.animate.scale(2))
|
||||
|
||||
scene = SceneWithTwoAnimations()
|
||||
scene.render()
|
||||
|
||||
assert scene.after_first_animation is False
|
||||
|
|
|
|||
BIN
tests/test_graphical_units/control_data/geometry/ConvexHull.npz
Normal file
BIN
tests/test_graphical_units/control_data/geometry/ConvexHull.npz
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -138,6 +138,20 @@ def test_RoundedRectangle(scene):
|
|||
scene.add(a)
|
||||
|
||||
|
||||
@frames_comparison
|
||||
def test_ConvexHull(scene):
|
||||
a = ConvexHull(
|
||||
*[
|
||||
[-2.7, -0.6, 0],
|
||||
[0.2, -1.7, 0],
|
||||
[1.9, 1.2, 0],
|
||||
[-2.7, 0.9, 0],
|
||||
[1.6, 2.2, 0],
|
||||
]
|
||||
)
|
||||
scene.add(a)
|
||||
|
||||
|
||||
@frames_comparison
|
||||
def test_Arrange(scene):
|
||||
s1 = Square()
|
||||
|
|
@ -254,9 +268,7 @@ def test_LabeledLine(scene):
|
|||
line = LabeledLine(
|
||||
label="0.5",
|
||||
label_position=0.8,
|
||||
font_size=20,
|
||||
label_color=WHITE,
|
||||
label_frame=True,
|
||||
label_config={"font_size": 20},
|
||||
start=LEFT + DOWN,
|
||||
end=RIGHT + UP,
|
||||
)
|
||||
|
|
@ -266,6 +278,27 @@ def test_LabeledLine(scene):
|
|||
@frames_comparison
|
||||
def test_LabeledArrow(scene):
|
||||
l_arrow = LabeledArrow(
|
||||
"0.5", start=LEFT * 3, end=RIGHT * 3 + UP * 2, label_position=0.5, font_size=15
|
||||
label="0.5",
|
||||
label_position=0.5,
|
||||
label_config={"font_size": 15},
|
||||
start=LEFT * 3,
|
||||
end=RIGHT * 3 + UP * 2,
|
||||
)
|
||||
scene.add(l_arrow)
|
||||
|
||||
|
||||
@frames_comparison
|
||||
def test_LabeledPolygram(scene):
|
||||
polygram = LabeledPolygram(
|
||||
[
|
||||
[-2.5, -2.5, 0],
|
||||
[2.5, -2.5, 0],
|
||||
[2.5, 2.5, 0],
|
||||
[-2.5, 2.5, 0],
|
||||
[-2.5, -2.5, 0],
|
||||
],
|
||||
[[-1, -1, 0], [0.5, -1, 0], [0.5, 0.5, 0], [-1, 0.5, 0], [-1, -1, 0]],
|
||||
[[1, 1, 0], [2, 1, 0], [2, 2, 0], [1, 2, 0], [1, 1, 0]],
|
||||
label="C",
|
||||
)
|
||||
scene.add(polygram)
|
||||
|
|
|
|||
|
|
@ -24,3 +24,17 @@ def test_Icosahedron(scene):
|
|||
@frames_comparison
|
||||
def test_Dodecahedron(scene):
|
||||
scene.add(Dodecahedron())
|
||||
|
||||
|
||||
@frames_comparison
|
||||
def test_ConvexHull3D(scene):
|
||||
a = ConvexHull3D(
|
||||
*[
|
||||
[-2.7, -0.6, 3.5],
|
||||
[0.2, -1.7, -2.8],
|
||||
[1.9, 1.2, 0.7],
|
||||
[-2.7, 0.9, 1.9],
|
||||
[1.6, 2.2, -4.2],
|
||||
]
|
||||
)
|
||||
scene.add(a)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue