mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-22 10:01:47 +00:00
Merge branch 'main' into fix/code-transform-animation
This commit is contained in:
commit
f01f7d16b1
136 changed files with 5102 additions and 2836 deletions
6
.github/codeql.yml
vendored
6
.github/codeql.yml
vendored
|
|
@ -9,6 +9,12 @@ query-filters:
|
|||
id: py/multiple-calls-to-init
|
||||
- exclude:
|
||||
id: py/missing-call-to-init
|
||||
- exclude:
|
||||
id: py/method-first-arg-is-not-self
|
||||
- exclude:
|
||||
id: py/cyclic-import
|
||||
- exclude:
|
||||
id: py/unsafe-cyclic-import
|
||||
paths:
|
||||
- manim
|
||||
paths-ignore:
|
||||
|
|
|
|||
4
.github/scripts/ci_build_cairo.py
vendored
4
.github/scripts/ci_build_cairo.py
vendored
|
|
@ -14,8 +14,8 @@ import subprocess
|
|||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import typing
|
||||
import urllib.request
|
||||
from collections.abc import Generator
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from sys import stdout
|
||||
|
|
@ -67,7 +67,7 @@ def run_command(command, cwd=None, env=None):
|
|||
|
||||
|
||||
@contextmanager
|
||||
def gha_group(title: str) -> typing.Generator:
|
||||
def gha_group(title: str) -> Generator:
|
||||
if not is_ci():
|
||||
yield
|
||||
return
|
||||
|
|
|
|||
2
.github/workflows/cffconvert.yml
vendored
2
.github/workflows/cffconvert.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out a copy of the repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Check whether the citation metadata from CITATION.cff is valid
|
||||
uses: citation-file-format/cffconvert-github-action@2.0.0
|
||||
|
|
|
|||
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
|
|
@ -27,15 +27,15 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Python ${{ matrix.python }}
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
|
|
@ -54,10 +54,13 @@ jobs:
|
|||
|
||||
- name: Install Texlive (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
uses: teatimeguest/setup-texlive-action@v3
|
||||
uses: zauguin/install-texlive@v4
|
||||
with:
|
||||
cache: true
|
||||
packages: scheme-basic fontspec inputenc fontenc tipa mathrsfs calligra xcolor standalone preview doublestroke ms everysel setspace rsfs relsize ragged2e fundus-calligra microtype wasysym physics dvisvgm jknapltx wasy cm-super babel-english gnu-freefont mathastext cbfonts-fd xetex
|
||||
packages: >
|
||||
scheme-basic latex fontspec tipa calligra xcolor
|
||||
standalone preview doublestroke setspace rsfs relsize
|
||||
ragged2e fundus-calligra microtype wasysym physics dvisvgm jknapltx
|
||||
wasy cm-super babel-english gnu-freefont mathastext cbfonts-fd xetex
|
||||
|
||||
- name: Start virtual display (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
|
|
|
|||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
|
|
|
|||
6
.github/workflows/python-publish.yml
vendored
6
.github/workflows/python-publish.yml
vendored
|
|
@ -13,15 +13,15 @@ jobs:
|
|||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python 3.13
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.13
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
uses: astral-sh/setup-uv@v6
|
||||
|
||||
- name: Build and push release to PyPI
|
||||
run: |
|
||||
|
|
|
|||
|
|
@ -9,15 +9,15 @@ jobs:
|
|||
build-and-publish-htmldocs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.13
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
uses: astral-sh/setup-uv@v6
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ fail_fast: false
|
|||
exclude: ^(manim/grpc/gen/|docs/i18n/)
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-ast
|
||||
name: Validate Python
|
||||
|
|
@ -13,7 +13,7 @@ repos:
|
|||
- id: check-toml
|
||||
name: Validate pyproject.toml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.11.0
|
||||
rev: v0.14.2
|
||||
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.15.0
|
||||
rev: v1.18.2
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies:
|
||||
|
|
|
|||
22
README.md
22
README.md
|
|
@ -22,17 +22,17 @@
|
|||
Manim is an animation engine for explanatory math videos. It's used to create precise animations programmatically, as demonstrated in the videos of [3Blue1Brown](https://www.3blue1brown.com/).
|
||||
|
||||
> [!NOTE]
|
||||
> The community edition of Manim has been forked from 3b1b/manim, a tool originally created and open-sourced by Grant Sanderson, also creator of the 3Blue1Brown educational math videos. While Grant Sanderson’s repository continues to be maintained separately by him, he is not among the maintainers of the community edition. We recommend this version for its continued development, improved features, enhanced documentation, and more active community-driven maintenance. If you would like to study how Grant makes his videos, head over to his repository ([3b1b/manim](https://github.com/3b1b/manim)).
|
||||
> The community edition of Manim (ManimCE) is a version maintained and developed by the community. It was forked from 3b1b/manim, a tool originally created and open-sourced by Grant Sanderson, also creator of the 3Blue1Brown educational math videos. While Grant Sanderson continues to maintain his own repository, we recommend this version for its continued development, improved features, enhanced documentation, and more active community-driven maintenance. If you would like to study how Grant makes his videos, head over to his repository ([3b1b/manim](https://github.com/3b1b/manim)).
|
||||
|
||||
## Table of Contents:
|
||||
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Documentation](#documentation)
|
||||
- [Docker](#docker)
|
||||
- [Help with Manim](#help-with-manim)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Documentation](#documentation)
|
||||
- [Docker](#docker)
|
||||
- [Help with Manim](#help-with-manim)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -90,9 +90,9 @@ The `-p` flag in the command above is for previewing, meaning the video file wil
|
|||
|
||||
Some other useful flags include:
|
||||
|
||||
- `-s` to skip to the end and just show the final frame.
|
||||
- `-n <number>` to skip ahead to the `n`'th animation of a scene.
|
||||
- `-f` show the file in the file browser.
|
||||
- `-s` to skip to the end and just show the final frame.
|
||||
- `-n <number>` to skip ahead to the `n`'th animation of a scene.
|
||||
- `-f` show the file in the file browser.
|
||||
|
||||
For a thorough list of command line arguments, visit the [documentation](https://docs.manim.community/en/stable/guides/configuration.html).
|
||||
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ html_title = f"Manim Community v{manim.__version__}"
|
|||
# This specifies any additional css files that will override the theme's
|
||||
html_css_files = ["custom.css"]
|
||||
|
||||
latex_engine = "lualatex"
|
||||
|
||||
# external links
|
||||
extlinks = {
|
||||
|
|
|
|||
|
|
@ -85,14 +85,8 @@ typed as a :class:`~.Point3D`, because it represents a direction along
|
|||
which to shift a :class:`~.Mobject`, not a position in space.
|
||||
|
||||
As a general rule, if a parameter is called ``direction`` or ``axis``,
|
||||
it should be type hinted as some form of :class:`~.VectorND`.
|
||||
|
||||
.. warning::
|
||||
|
||||
This is not always true. For example, as of Manim 0.18.0, the direction
|
||||
parameter of the :class:`.Vector` Mobject should be
|
||||
``Point2DLike | Point3DLike``, as it can also accept ``tuple[float, float]``
|
||||
and ``tuple[float, float, float]``.
|
||||
it should be type hinted as some form of :class:`~.VectorND` or
|
||||
:class:`~.VectorNDLike`.
|
||||
|
||||
Colors
|
||||
------
|
||||
|
|
|
|||
|
|
@ -299,7 +299,7 @@ Animations
|
|||
path.become(previous_path)
|
||||
path.add_updater(update_path)
|
||||
self.add(path, dot)
|
||||
self.play(Rotating(dot, radians=PI, about_point=RIGHT, run_time=2))
|
||||
self.play(Rotating(dot, angle=PI, about_point=RIGHT, run_time=2))
|
||||
self.wait()
|
||||
self.play(dot.animate.shift(UP))
|
||||
self.play(dot.animate.shift(LEFT))
|
||||
|
|
|
|||
|
|
@ -94,6 +94,21 @@ or `Discord <https://www.manim.community/discord/>`_. If you're using Manim in a
|
|||
context, instructions on how to cite a particular release can be found
|
||||
`in our README <https://github.com/ManimCommunity/manim/blob/main/README.md>`_.
|
||||
|
||||
License Information
|
||||
-------------------
|
||||
|
||||
Manim is an open-source library licensed under the **MIT License**, which applies to both the
|
||||
original and the community editions of the software. This means you are free to use, modify,
|
||||
and distribute the code in accordance with the MIT License terms. However, there are some
|
||||
additional points to be aware of:
|
||||
|
||||
- **Copyrighted Assets:** Specific assets, such as the "Pi creatures" in Grant Sanderson's
|
||||
(3Blue1Brown) videos, are copyrighted and protected. Please avoid using these characters in
|
||||
any derivative works.
|
||||
- **Content Creation and Sharing:** Videos and animations created with Manim can be freely
|
||||
shared, and no attribution to Manim is required—although it is much appreciated! You are
|
||||
encouraged to showcase your work online and share it with the Manim community.
|
||||
|
||||
Index
|
||||
-----
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ Module Index
|
|||
~utils.commands
|
||||
~utils.config_ops
|
||||
constants
|
||||
data_structures
|
||||
~utils.debug
|
||||
~utils.deprecation
|
||||
~utils.docbuild
|
||||
|
|
|
|||
|
|
@ -327,6 +327,13 @@ Generally, you start with the starting number and add only some part of the valu
|
|||
So, the logic of calculating the number to display at each step will be ``50 + alpha * (100 - 50)``.
|
||||
Once you set the calculated value for the :class:`~.DecimalNumber`, you are done.
|
||||
|
||||
.. note::
|
||||
|
||||
If you're creating a custom animation and want to use a ``rate_func``, you must explicitly apply
|
||||
``self.rate_func(alpha)`` to the parameter you're animating. For example, try switching the rate
|
||||
function to ``rate_functions.there_and_back`` to observe how it affects the counting behavior.
|
||||
|
||||
|
||||
Once you have defined your ``Count`` animation, you can play it in your :class:`~.Scene` for any duration you want for any :class:`~.DecimalNumber` with any rate function.
|
||||
|
||||
.. manim:: CountingScene
|
||||
|
|
@ -343,7 +350,7 @@ Once you have defined your ``Count`` animation, you can play it in your :class:`
|
|||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
# Set value of DecimalNumber according to alpha
|
||||
value = self.start + (alpha * (self.end - self.start))
|
||||
value = self.start + (self.rate_func(alpha) * (self.end - self.start))
|
||||
self.mobject.set_value(value)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class TexFontTemplateLibrary(Scene):
|
|||
Many of the in the TexFontTemplates collection require that specific fonts
|
||||
are installed on your local machine.
|
||||
For example, choosing the template TexFontTemplates.comic_sans will
|
||||
not compile if the Comic Sans Micrososft font is not installed.
|
||||
not compile if the Comic Sans Microsoft font is not installed.
|
||||
|
||||
This scene will only render those Templates that do not cause a TeX
|
||||
compilation error on your system. Furthermore, some of the ones that do render,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
#!/usr/bin/env python
|
||||
from __future__ import annotations
|
||||
|
||||
from importlib.metadata import version
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
|
||||
__version__ = version(__name__)
|
||||
# Use installed distribution version if available; otherwise fall back to a
|
||||
# sensible default so that importing from a source checkout works without an
|
||||
# editable install (pip install -e .).
|
||||
try:
|
||||
__version__ = version(__name__)
|
||||
except PackageNotFoundError:
|
||||
# Package is not installed; provide a fallback version string.
|
||||
__version__ = "0.0.0+unknown"
|
||||
|
||||
|
||||
# isort: off
|
||||
|
|
|
|||
|
|
@ -23,10 +23,9 @@ __all__ = [
|
|||
|
||||
parser = make_config_parser()
|
||||
|
||||
# The logger can be accessed from anywhere as manim.logger, or as
|
||||
# logging.getLogger("manim"). The console must be accessed as manim.console.
|
||||
# Throughout the codebase, use manim.console.print() instead of print().
|
||||
# Use error_console to print errors so that it outputs to stderr.
|
||||
# Logger usage: accessible globally as `manim.logger` or via `logging.getLogger("manim")`.
|
||||
# For printing, use `manim.console.print()` instead of the built-in `print()`.
|
||||
# For error output, use `error_console`, which prints to stderr.
|
||||
logger, console, error_console = make_logger(
|
||||
parser["logger"],
|
||||
parser["CLI"]["verbosity"],
|
||||
|
|
@ -45,7 +44,7 @@ frame = ManimFrame(config)
|
|||
# This has to go here because it needs access to this module's config
|
||||
@contextmanager
|
||||
def tempconfig(temp: ManimConfig | dict[str, Any]) -> Generator[None, None, None]:
|
||||
"""Context manager that temporarily modifies the global ``config`` object.
|
||||
"""Temporarily modifies the global ``config`` object using a context manager.
|
||||
|
||||
Inside the ``with`` statement, the modified config will be used. After
|
||||
context manager exits, the config will be restored to its original state.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
"""Parses CLI context settings from the configuration file and returns a Cloup Context settings dictionary.
|
||||
|
||||
This module reads configuration values for help formatting, theme styles, and alignment options
|
||||
used when rendering command-line interfaces in Manim.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
|
|
@ -9,7 +15,7 @@ __all__ = ["parse_cli_ctx"]
|
|||
|
||||
|
||||
def parse_cli_ctx(parser: configparser.SectionProxy) -> dict[str, Any]:
|
||||
formatter_settings: dict[str, str | int] = {
|
||||
formatter_settings: dict[str, str | int | None] = {
|
||||
"indent_increment": int(parser["indent_increment"]),
|
||||
"width": int(parser["width"]),
|
||||
"col1_max_width": int(parser["col1_max_width"]),
|
||||
|
|
@ -28,6 +34,7 @@ def parse_cli_ctx(parser: configparser.SectionProxy) -> dict[str, Any]:
|
|||
"col2",
|
||||
"epilog",
|
||||
}
|
||||
# Extract and apply any style-related keys defined in the config section.
|
||||
for k, v in parser.items():
|
||||
if k in theme_keys and v:
|
||||
theme_settings.update({k: Style(v)})
|
||||
|
|
@ -37,22 +44,24 @@ def parse_cli_ctx(parser: configparser.SectionProxy) -> dict[str, Any]:
|
|||
if theme is None:
|
||||
formatter = HelpFormatter.settings(
|
||||
theme=HelpTheme(**theme_settings),
|
||||
**formatter_settings, # type: ignore[arg-type]
|
||||
**formatter_settings,
|
||||
)
|
||||
elif theme.lower() == "dark":
|
||||
formatter = HelpFormatter.settings(
|
||||
theme=HelpTheme.dark().with_(**theme_settings),
|
||||
**formatter_settings, # type: ignore[arg-type]
|
||||
**formatter_settings,
|
||||
)
|
||||
elif theme.lower() == "light":
|
||||
formatter = HelpFormatter.settings(
|
||||
theme=HelpTheme.light().with_(**theme_settings),
|
||||
**formatter_settings, # type: ignore[arg-type]
|
||||
**formatter_settings,
|
||||
)
|
||||
|
||||
return Context.settings(
|
||||
return_val: dict[str, Any] = Context.settings(
|
||||
align_option_groups=parser["align_option_groups"].lower() == "true",
|
||||
align_sections=parser["align_sections"].lower() == "true",
|
||||
show_constraints=True,
|
||||
formatter_settings=formatter,
|
||||
)
|
||||
|
||||
return return_val
|
||||
|
|
|
|||
|
|
@ -122,10 +122,14 @@ def make_config_parser(
|
|||
# read_file() before calling read() for any optional files."
|
||||
# https://docs.python.org/3/library/configparser.html#configparser.ConfigParser.read
|
||||
parser = configparser.ConfigParser()
|
||||
logger.info(f"Reading config file: {library_wide}")
|
||||
with library_wide.open() as file:
|
||||
parser.read_file(file) # necessary file
|
||||
|
||||
other_files = [user_wide, Path(custom_file) if custom_file else folder_wide]
|
||||
for path in other_files:
|
||||
if path.exists():
|
||||
logger.info(f"Reading config file: {path}")
|
||||
parser.read(other_files) # optional files
|
||||
|
||||
return parser
|
||||
|
|
@ -591,6 +595,7 @@ class ManimConfig(MutableMapping):
|
|||
"enable_wireframe",
|
||||
"force_window",
|
||||
"no_latex_cleanup",
|
||||
"dry_run",
|
||||
]:
|
||||
setattr(self, key, parser["CLI"].getboolean(key, fallback=False))
|
||||
|
||||
|
|
@ -625,6 +630,7 @@ class ManimConfig(MutableMapping):
|
|||
"background_color",
|
||||
"renderer",
|
||||
"window_position",
|
||||
"preview_command",
|
||||
]:
|
||||
setattr(self, key, parser["CLI"].get(key, fallback="", raw=True))
|
||||
|
||||
|
|
@ -1414,7 +1420,7 @@ class ManimConfig(MutableMapping):
|
|||
|
||||
@property
|
||||
def window_size(self) -> str:
|
||||
"""The size of the opengl window. 'default' to automatically scale the window based on the display monitor."""
|
||||
"""The size of the opengl window as 'width,height' or 'default' to automatically scale the window based on the display monitor."""
|
||||
return self._d["window_size"]
|
||||
|
||||
@window_size.setter
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ from ..utils.rate_functions import linear, smooth
|
|||
__all__ = ["Animation", "Wait", "Add", "override_animation"]
|
||||
|
||||
|
||||
from collections.abc import Iterable, Sequence
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from copy import deepcopy
|
||||
from functools import partialmethod
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
|
|
@ -54,7 +54,6 @@ class Animation:
|
|||
For example ``rate_func(0.5)`` is the proportion of the animation that is done
|
||||
after half of the animations run time.
|
||||
|
||||
|
||||
reverse_rate_function
|
||||
Reverses the rate function of the animation. Setting ``reverse_rate_function``
|
||||
does not have any effect on ``remover`` or ``introducer``. These need to be
|
||||
|
|
@ -121,7 +120,7 @@ class Animation:
|
|||
if func is not None:
|
||||
anim = func(mobject, *args, **kwargs)
|
||||
logger.debug(
|
||||
f"The {cls.__name__} animation has been is overridden for "
|
||||
f"The {cls.__name__} animation has been overridden for "
|
||||
f"{type(mobject).__name__} mobjects. use_override = False can "
|
||||
f" be used as keyword argument to prevent animation overriding.",
|
||||
)
|
||||
|
|
@ -130,7 +129,7 @@ class Animation:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject | None,
|
||||
mobject: Mobject | OpenGLMobject | None,
|
||||
lag_ratio: float = DEFAULT_ANIMATION_LAG_RATIO,
|
||||
run_time: float = DEFAULT_ANIMATION_RUN_TIME,
|
||||
rate_func: Callable[[float], float] = smooth,
|
||||
|
|
@ -141,7 +140,7 @@ class Animation:
|
|||
introducer: bool = False,
|
||||
*,
|
||||
_on_finish: Callable[[], None] = lambda _: None,
|
||||
**kwargs,
|
||||
use_override: bool = True, # included here to avoid TypeError if passed from a subclass' constructor
|
||||
) -> None:
|
||||
self._typecheck_input(mobject)
|
||||
self.run_time: float = run_time
|
||||
|
|
@ -161,8 +160,6 @@ class Animation:
|
|||
else:
|
||||
self.starting_mobject: Mobject = Mobject()
|
||||
self.mobject: Mobject = mobject if mobject is not None else Mobject()
|
||||
if kwargs:
|
||||
logger.debug("Animation received extra kwargs: %s", kwargs)
|
||||
|
||||
if hasattr(self, "CONFIG"):
|
||||
logger.error(
|
||||
|
|
@ -265,11 +262,11 @@ class Animation:
|
|||
):
|
||||
scene.add(self.mobject)
|
||||
|
||||
def create_starting_mobject(self) -> Mobject:
|
||||
def create_starting_mobject(self) -> Mobject | OpenGLMobject:
|
||||
# Keep track of where the mobject starts
|
||||
return self.mobject.copy()
|
||||
|
||||
def get_all_mobjects(self) -> Sequence[Mobject]:
|
||||
def get_all_mobjects(self) -> Sequence[Mobject | OpenGLMobject]:
|
||||
"""Get all mobjects involved in the animation.
|
||||
|
||||
Ordering must match the ordering of arguments to interpolate_submobject
|
||||
|
|
@ -499,6 +496,8 @@ class Animation:
|
|||
|
||||
cls._original__init__ = cls.__init__
|
||||
|
||||
_original__init__ = __init__ # needed if set_default() is called with no kwargs directly from Animation
|
||||
|
||||
@classmethod
|
||||
def set_default(cls, **kwargs) -> None:
|
||||
"""Sets the default values of keyword arguments.
|
||||
|
|
@ -541,7 +540,7 @@ class Animation:
|
|||
|
||||
|
||||
def prepare_animation(
|
||||
anim: Animation | mobject._AnimationBuilder,
|
||||
anim: Animation | mobject._AnimationBuilder | opengl_mobject._AnimationBuilder,
|
||||
) -> Animation:
|
||||
r"""Returns either an unchanged animation, or the animation built
|
||||
from a passed animation factory.
|
||||
|
|
|
|||
|
|
@ -4,8 +4,12 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["AnimatedBoundary", "TracedPath"]
|
||||
|
||||
from typing import Callable
|
||||
from collections.abc import Callable, Sequence
|
||||
from typing import Any
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.utils.color import (
|
||||
|
|
@ -16,7 +20,7 @@ from manim.utils.color import (
|
|||
WHITE,
|
||||
ParsableManimColor,
|
||||
)
|
||||
from manim.utils.rate_functions import smooth
|
||||
from manim.utils.rate_functions import RateFunction, smooth
|
||||
|
||||
|
||||
class AnimatedBoundary(VGroup):
|
||||
|
|
@ -38,14 +42,14 @@ class AnimatedBoundary(VGroup):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
vmobject,
|
||||
colors=[BLUE_D, BLUE_B, BLUE_E, GREY_BROWN],
|
||||
max_stroke_width=3,
|
||||
cycle_rate=0.5,
|
||||
back_and_forth=True,
|
||||
draw_rate_func=smooth,
|
||||
fade_rate_func=smooth,
|
||||
**kwargs,
|
||||
vmobject: VMobject,
|
||||
colors: Sequence[ParsableManimColor] = [BLUE_D, BLUE_B, BLUE_E, GREY_BROWN],
|
||||
max_stroke_width: float = 3,
|
||||
cycle_rate: float = 0.5,
|
||||
back_and_forth: bool = True,
|
||||
draw_rate_func: RateFunction = smooth,
|
||||
fade_rate_func: RateFunction = smooth,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.colors = colors
|
||||
|
|
@ -59,10 +63,10 @@ class AnimatedBoundary(VGroup):
|
|||
vmobject.copy().set_style(stroke_width=0, fill_opacity=0) for x in range(2)
|
||||
]
|
||||
self.add(*self.boundary_copies)
|
||||
self.total_time = 0
|
||||
self.total_time = 0.0
|
||||
self.add_updater(lambda m, dt: self.update_boundary_copies(dt))
|
||||
|
||||
def update_boundary_copies(self, dt):
|
||||
def update_boundary_copies(self, dt: float) -> None:
|
||||
# Not actual time, but something which passes at
|
||||
# an altered rate to make the implementation below
|
||||
# cleaner
|
||||
|
|
@ -78,9 +82,9 @@ class AnimatedBoundary(VGroup):
|
|||
fade_alpha = self.fade_rate_func(alpha)
|
||||
|
||||
if self.back_and_forth and int(time) % 2 == 1:
|
||||
bounds = (1 - draw_alpha, 1)
|
||||
bounds = (1.0 - draw_alpha, 1.0)
|
||||
else:
|
||||
bounds = (0, draw_alpha)
|
||||
bounds = (0.0, draw_alpha)
|
||||
self.full_family_become_partial(growing, vmobject, *bounds)
|
||||
growing.set_stroke(colors[index], width=msw)
|
||||
|
||||
|
|
@ -90,7 +94,9 @@ class AnimatedBoundary(VGroup):
|
|||
|
||||
self.total_time += dt
|
||||
|
||||
def full_family_become_partial(self, mob1, mob2, a, b):
|
||||
def full_family_become_partial(
|
||||
self, mob1: VMobject, mob2: VMobject, a: float, b: float
|
||||
) -> Self:
|
||||
family1 = mob1.family_members_with_points()
|
||||
family2 = mob2.family_members_with_points()
|
||||
for sm1, sm2 in zip(family1, family2):
|
||||
|
|
@ -146,20 +152,21 @@ class TracedPath(VMobject, metaclass=ConvertToOpenGL):
|
|||
stroke_width: float = 2,
|
||||
stroke_color: ParsableManimColor | None = WHITE,
|
||||
dissipating_time: float | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(stroke_color=stroke_color, stroke_width=stroke_width, **kwargs)
|
||||
self.traced_point_func = traced_point_func
|
||||
self.dissipating_time = dissipating_time
|
||||
self.time = 1 if self.dissipating_time else None
|
||||
self.time = 1.0 if self.dissipating_time else None
|
||||
self.add_updater(self.update_path)
|
||||
|
||||
def update_path(self, mob, dt):
|
||||
def update_path(self, mob: Mobject, dt: float) -> None:
|
||||
new_point = self.traced_point_func()
|
||||
if not self.has_points():
|
||||
self.start_new_path(new_point)
|
||||
self.add_line_to(new_point)
|
||||
if self.dissipating_time:
|
||||
assert self.time is not None
|
||||
self.time += dt
|
||||
if self.time - 1 > self.dissipating_time:
|
||||
nppcc = self.n_points_per_curve
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import types
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -12,7 +11,7 @@ from manim._config import config
|
|||
from manim.animation.animation import Animation, prepare_animation
|
||||
from manim.constants import RendererType
|
||||
from manim.mobject.mobject import Group, Mobject
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLGroup
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLGroup, OpenGLMobject
|
||||
from manim.scene.scene import Scene
|
||||
from manim.utils.iterables import remove_list_redundancies
|
||||
from manim.utils.parameter_parsing import flatten_iterable_parameters
|
||||
|
|
@ -54,31 +53,34 @@ class AnimationGroup(Animation):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
*animations: Animation | Iterable[Animation] | types.GeneratorType[Animation],
|
||||
group: Group | VGroup | OpenGLGroup | OpenGLVGroup = None,
|
||||
*animations: Animation | Iterable[Animation],
|
||||
group: Group | VGroup | OpenGLGroup | OpenGLVGroup | None = None,
|
||||
run_time: float | None = None,
|
||||
rate_func: Callable[[float], float] = linear,
|
||||
lag_ratio: float = 0,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
**kwargs: Any,
|
||||
):
|
||||
arg_anim = flatten_iterable_parameters(animations)
|
||||
self.animations = [prepare_animation(anim) for anim in arg_anim]
|
||||
self.rate_func = rate_func
|
||||
self.group = group
|
||||
if self.group is None:
|
||||
if group is None:
|
||||
mobjects = remove_list_redundancies(
|
||||
[anim.mobject for anim in self.animations if not anim.is_introducer()],
|
||||
)
|
||||
if config["renderer"] == RendererType.OPENGL:
|
||||
self.group = OpenGLGroup(*mobjects)
|
||||
self.group: Group | VGroup | OpenGLGroup | OpenGLVGroup = OpenGLGroup(
|
||||
*mobjects
|
||||
)
|
||||
else:
|
||||
self.group = Group(*mobjects)
|
||||
else:
|
||||
self.group = group
|
||||
super().__init__(
|
||||
self.group, rate_func=self.rate_func, lag_ratio=lag_ratio, **kwargs
|
||||
)
|
||||
self.run_time: float = self.init_run_time(run_time)
|
||||
|
||||
def get_all_mobjects(self) -> Sequence[Mobject]:
|
||||
def get_all_mobjects(self) -> Sequence[Mobject | OpenGLMobject]:
|
||||
return list(self.group)
|
||||
|
||||
def begin(self) -> None:
|
||||
|
|
@ -93,7 +95,7 @@ class AnimationGroup(Animation):
|
|||
for anim in self.animations:
|
||||
anim.begin()
|
||||
|
||||
def _setup_scene(self, scene) -> None:
|
||||
def _setup_scene(self, scene: Scene) -> None:
|
||||
for anim in self.animations:
|
||||
anim._setup_scene(scene)
|
||||
|
||||
|
|
@ -118,7 +120,7 @@ class AnimationGroup(Animation):
|
|||
]:
|
||||
anim.update_mobjects(dt)
|
||||
|
||||
def init_run_time(self, run_time) -> float:
|
||||
def init_run_time(self, run_time: float | None) -> float:
|
||||
"""Calculates the run time of the animation, if different from ``run_time``.
|
||||
|
||||
Parameters
|
||||
|
|
@ -146,9 +148,9 @@ class AnimationGroup(Animation):
|
|||
run_times = np.array([anim.run_time for anim in self.animations])
|
||||
num_animations = run_times.shape[0]
|
||||
dtype = [("anim", "O"), ("start", "f8"), ("end", "f8")]
|
||||
self.anims_with_timings = np.zeros(num_animations, dtype=dtype)
|
||||
self.anims_begun = np.zeros(num_animations, dtype=bool)
|
||||
self.anims_finished = np.zeros(num_animations, dtype=bool)
|
||||
self.anims_with_timings: np.ndarray = np.zeros(num_animations, dtype=dtype)
|
||||
self.anims_begun: np.ndarray = np.zeros(num_animations, dtype=bool)
|
||||
self.anims_finished: np.ndarray = np.zeros(num_animations, dtype=bool)
|
||||
if num_animations == 0:
|
||||
return
|
||||
|
||||
|
|
@ -228,7 +230,7 @@ class Succession(AnimationGroup):
|
|||
))
|
||||
"""
|
||||
|
||||
def __init__(self, *animations: Animation, lag_ratio: float = 1, **kwargs) -> None:
|
||||
def __init__(self, *animations: Animation, lag_ratio: float = 1, **kwargs: Any):
|
||||
super().__init__(*animations, lag_ratio=lag_ratio, **kwargs)
|
||||
|
||||
def begin(self) -> None:
|
||||
|
|
@ -247,7 +249,7 @@ class Succession(AnimationGroup):
|
|||
if self.active_animation:
|
||||
self.active_animation.update_mobjects(dt)
|
||||
|
||||
def _setup_scene(self, scene) -> None:
|
||||
def _setup_scene(self, scene: Scene | None) -> None:
|
||||
if scene is None:
|
||||
return
|
||||
if self.is_introducer():
|
||||
|
|
@ -339,7 +341,7 @@ class LaggedStart(AnimationGroup):
|
|||
self,
|
||||
*animations: Animation,
|
||||
lag_ratio: float = DEFAULT_LAGGED_START_LAG_RATIO,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(*animations, lag_ratio=lag_ratio, **kwargs)
|
||||
|
||||
|
|
@ -384,20 +386,22 @@ class LaggedStartMap(LaggedStart):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
AnimationClass: Callable[..., Animation],
|
||||
animation_class: type[Animation],
|
||||
mobject: Mobject,
|
||||
arg_creator: Callable[[Mobject], str] = None,
|
||||
arg_creator: Callable[[Mobject], Iterable[Any]] | None = None,
|
||||
run_time: float = 2,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
args_list = []
|
||||
for submob in mobject:
|
||||
if arg_creator:
|
||||
args_list.append(arg_creator(submob))
|
||||
else:
|
||||
args_list.append((submob,))
|
||||
**kwargs: Any,
|
||||
):
|
||||
if arg_creator is None:
|
||||
|
||||
def identity(mob: Mobject) -> Mobject:
|
||||
return mob
|
||||
|
||||
arg_creator = identity
|
||||
|
||||
args_list = [arg_creator(submob) for submob in mobject]
|
||||
anim_kwargs = dict(kwargs)
|
||||
if "lag_ratio" in anim_kwargs:
|
||||
anim_kwargs.pop("lag_ratio")
|
||||
animations = [AnimationClass(*args, **anim_kwargs) for args in args_list]
|
||||
animations = [animation_class(*args, **anim_kwargs) for args in args_list]
|
||||
super().__init__(*animations, run_time=run_time, **kwargs)
|
||||
|
|
|
|||
|
|
@ -76,8 +76,8 @@ __all__ = [
|
|||
|
||||
|
||||
import itertools as it
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -120,7 +120,7 @@ class ShowPartial(Animation):
|
|||
):
|
||||
pointwise = getattr(mobject, "pointwise_become_partial", None)
|
||||
if not callable(pointwise):
|
||||
raise NotImplementedError("This animation is not defined for this Mobject.")
|
||||
raise TypeError(f"{self.__class__.__name__} only works for VMobjects.")
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def interpolate_submobject(
|
||||
|
|
@ -133,7 +133,7 @@ class ShowPartial(Animation):
|
|||
starting_submobject, *self._get_bounds(alpha)
|
||||
)
|
||||
|
||||
def _get_bounds(self, alpha: float) -> None:
|
||||
def _get_bounds(self, alpha: float) -> tuple[float, float]:
|
||||
raise NotImplementedError("Please use Create or ShowPassingFlash")
|
||||
|
||||
|
||||
|
|
@ -173,7 +173,7 @@ class Create(ShowPartial):
|
|||
) -> None:
|
||||
super().__init__(mobject, lag_ratio=lag_ratio, introducer=introducer, **kwargs)
|
||||
|
||||
def _get_bounds(self, alpha: float) -> tuple[int, float]:
|
||||
def _get_bounds(self, alpha: float) -> tuple[float, float]:
|
||||
return (0, alpha)
|
||||
|
||||
|
||||
|
|
@ -229,8 +229,6 @@ class DrawBorderThenFill(Animation):
|
|||
rate_func: Callable[[float], float] = double_smooth,
|
||||
stroke_width: float = 2,
|
||||
stroke_color: str = None,
|
||||
draw_border_animation_config: dict = {}, # what does this dict accept?
|
||||
fill_animation_config: dict = {},
|
||||
introducer: bool = True,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
|
|
@ -244,8 +242,6 @@ class DrawBorderThenFill(Animation):
|
|||
)
|
||||
self.stroke_width = stroke_width
|
||||
self.stroke_color = stroke_color
|
||||
self.draw_border_animation_config = draw_border_animation_config
|
||||
self.fill_animation_config = fill_animation_config
|
||||
self.outline = self.get_outline()
|
||||
|
||||
def _typecheck_input(self, vmobject: VMobject | OpenGLVMobject) -> None:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ __all__ = [
|
|||
"FadeIn",
|
||||
]
|
||||
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
|
|
@ -53,7 +55,7 @@ class _Fade(Transform):
|
|||
shift: np.ndarray | None = None,
|
||||
target_position: np.ndarray | Mobject | None = None,
|
||||
scale: float = 1,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
if not mobjects:
|
||||
raise ValueError("At least one mobject must be passed.")
|
||||
|
|
@ -85,7 +87,7 @@ class _Fade(Transform):
|
|||
Mobject
|
||||
The faded, shifted and scaled copy of the mobject.
|
||||
"""
|
||||
faded_mobject = self.mobject.copy()
|
||||
faded_mobject: Mobject = self.mobject.copy() # type: ignore[assignment]
|
||||
faded_mobject.fade(1)
|
||||
direction_modifier = -1 if fadeIn and not self.point_target else 1
|
||||
faded_mobject.shift(self.shift_vector * direction_modifier)
|
||||
|
|
@ -131,13 +133,13 @@ class FadeIn(_Fade):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, *mobjects: Mobject, **kwargs) -> None:
|
||||
def __init__(self, *mobjects: Mobject, **kwargs: Any) -> None:
|
||||
super().__init__(*mobjects, introducer=True, **kwargs)
|
||||
|
||||
def create_target(self):
|
||||
return self.mobject
|
||||
def create_target(self) -> Mobject:
|
||||
return self.mobject # type: ignore[return-value]
|
||||
|
||||
def create_starting_mobject(self):
|
||||
def create_starting_mobject(self) -> Mobject:
|
||||
return self._create_faded_mobject(fadeIn=True)
|
||||
|
||||
|
||||
|
|
@ -179,12 +181,12 @@ class FadeOut(_Fade):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, *mobjects: Mobject, **kwargs) -> None:
|
||||
def __init__(self, *mobjects: Mobject, **kwargs: Any) -> None:
|
||||
super().__init__(*mobjects, remover=True, **kwargs)
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Mobject:
|
||||
return self._create_faded_mobject(fadeIn=False)
|
||||
|
||||
def clean_up_from_scene(self, scene: Scene = None) -> None:
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
super().clean_up_from_scene(scene)
|
||||
self.interpolate(0)
|
||||
|
|
|
|||
|
|
@ -31,16 +31,17 @@ __all__ = [
|
|||
"SpinInFromNothing",
|
||||
]
|
||||
|
||||
import typing
|
||||
|
||||
import numpy as np
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ..animation.transform import Transform
|
||||
from ..constants import PI
|
||||
from ..utils.paths import spiral_path
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.geometry.line import Arrow
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.typing import Point3DLike, Vector3DLike
|
||||
from manim.utils.color import ParsableManimColor
|
||||
|
||||
from ..mobject.mobject import Mobject
|
||||
|
||||
|
|
@ -76,16 +77,20 @@ class GrowFromPoint(Transform):
|
|||
"""
|
||||
|
||||
def __init__(
|
||||
self, mobject: Mobject, point: np.ndarray, point_color: str = None, **kwargs
|
||||
) -> None:
|
||||
self,
|
||||
mobject: Mobject,
|
||||
point: Point3DLike,
|
||||
point_color: ParsableManimColor | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.point = point
|
||||
self.point_color = point_color
|
||||
super().__init__(mobject, introducer=True, **kwargs)
|
||||
|
||||
def create_target(self) -> Mobject:
|
||||
def create_target(self) -> Mobject | OpenGLMobject:
|
||||
return self.mobject
|
||||
|
||||
def create_starting_mobject(self) -> Mobject:
|
||||
def create_starting_mobject(self) -> Mobject | OpenGLMobject:
|
||||
start = super().create_starting_mobject()
|
||||
start.scale(0)
|
||||
start.move_to(self.point)
|
||||
|
|
@ -118,7 +123,12 @@ class GrowFromCenter(GrowFromPoint):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, mobject: Mobject, point_color: str = None, **kwargs) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
point_color: ParsableManimColor | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
point = mobject.get_center()
|
||||
super().__init__(mobject, point, point_color=point_color, **kwargs)
|
||||
|
||||
|
|
@ -153,8 +163,12 @@ class GrowFromEdge(GrowFromPoint):
|
|||
"""
|
||||
|
||||
def __init__(
|
||||
self, mobject: Mobject, edge: np.ndarray, point_color: str = None, **kwargs
|
||||
) -> None:
|
||||
self,
|
||||
mobject: Mobject,
|
||||
edge: Vector3DLike,
|
||||
point_color: ParsableManimColor | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
point = mobject.get_critical_point(edge)
|
||||
super().__init__(mobject, point, point_color=point_color, **kwargs)
|
||||
|
||||
|
|
@ -183,11 +197,13 @@ class GrowArrow(GrowFromPoint):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, arrow: Arrow, point_color: str = None, **kwargs) -> None:
|
||||
def __init__(
|
||||
self, arrow: Arrow, point_color: ParsableManimColor | None = None, **kwargs: Any
|
||||
):
|
||||
point = arrow.get_start()
|
||||
super().__init__(arrow, point, point_color=point_color, **kwargs)
|
||||
|
||||
def create_starting_mobject(self) -> Mobject:
|
||||
def create_starting_mobject(self) -> Mobject | OpenGLMobject:
|
||||
start_arrow = self.mobject.copy()
|
||||
start_arrow.scale(0, scale_tips=True, about_point=self.point)
|
||||
if self.point_color:
|
||||
|
|
@ -224,8 +240,12 @@ class SpinInFromNothing(GrowFromCenter):
|
|||
"""
|
||||
|
||||
def __init__(
|
||||
self, mobject: Mobject, angle: float = PI / 2, point_color: str = None, **kwargs
|
||||
) -> None:
|
||||
self,
|
||||
mobject: Mobject,
|
||||
angle: float = PI / 2,
|
||||
point_color: ParsableManimColor | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.angle = angle
|
||||
super().__init__(
|
||||
mobject, path_func=spiral_path(angle), point_color=point_color, **kwargs
|
||||
|
|
|
|||
|
|
@ -40,14 +40,16 @@ __all__ = [
|
|||
]
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import Callable
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim.mobject.geometry.arc import Circle, Dot
|
||||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.geometry.polygram import Rectangle
|
||||
from manim.mobject.geometry.shape_matchers import SurroundingRectangle
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.scene.scene import Scene
|
||||
|
||||
from .. import config
|
||||
|
|
@ -61,9 +63,10 @@ from ..animation.updaters.update import UpdateFromFunc
|
|||
from ..constants import *
|
||||
from ..mobject.mobject import Mobject
|
||||
from ..mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from ..typing import Point3D, Point3DLike, Vector3DLike
|
||||
from ..utils.bezier import interpolate, inverse_interpolate
|
||||
from ..utils.color import GREY, YELLOW, ParsableManimColor
|
||||
from ..utils.rate_functions import smooth, there_and_back, wiggle
|
||||
from ..utils.rate_functions import RateFunction, smooth, there_and_back, wiggle
|
||||
from ..utils.space_ops import normalize
|
||||
|
||||
|
||||
|
|
@ -95,12 +98,12 @@ class FocusOn(Transform):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
focus_point: np.ndarray | Mobject,
|
||||
focus_point: Point3DLike | Mobject,
|
||||
opacity: float = 0.2,
|
||||
color: str = GREY,
|
||||
color: ParsableManimColor = GREY,
|
||||
run_time: float = 2,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.focus_point = focus_point
|
||||
self.color = color
|
||||
self.opacity = opacity
|
||||
|
|
@ -151,15 +154,15 @@ class Indicate(Transform):
|
|||
self,
|
||||
mobject: Mobject,
|
||||
scale_factor: float = 1.2,
|
||||
color: str = YELLOW,
|
||||
rate_func: Callable[[float, float | None], np.ndarray] = there_and_back,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
color: ParsableManimColor = YELLOW,
|
||||
rate_func: RateFunction = there_and_back,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.color = color
|
||||
self.scale_factor = scale_factor
|
||||
super().__init__(mobject, rate_func=rate_func, **kwargs)
|
||||
|
||||
def create_target(self) -> Mobject:
|
||||
def create_target(self) -> Mobject | OpenGLMobject:
|
||||
target = self.mobject.copy()
|
||||
target.scale(self.scale_factor)
|
||||
target.set_color(self.color)
|
||||
|
|
@ -219,20 +222,20 @@ class Flash(AnimationGroup):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
point: np.ndarray | Mobject,
|
||||
point: Point3DLike | Mobject,
|
||||
line_length: float = 0.2,
|
||||
num_lines: int = 12,
|
||||
flash_radius: float = 0.1,
|
||||
line_stroke_width: int = 3,
|
||||
color: str = YELLOW,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
time_width: float = 1,
|
||||
run_time: float = 1.0,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
**kwargs: Any,
|
||||
):
|
||||
if isinstance(point, Mobject):
|
||||
self.point = point.get_center()
|
||||
self.point: Point3D = point.get_center()
|
||||
else:
|
||||
self.point = point
|
||||
self.point = np.asarray(point)
|
||||
self.color = color
|
||||
self.line_length = line_length
|
||||
self.num_lines = num_lines
|
||||
|
|
@ -303,11 +306,13 @@ class ShowPassingFlash(ShowPartial):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, mobject: VMobject, time_width: float = 0.1, **kwargs) -> None:
|
||||
def __init__(
|
||||
self, mobject: VMobject, time_width: float = 0.1, **kwargs: Any
|
||||
) -> None:
|
||||
self.time_width = time_width
|
||||
super().__init__(mobject, remover=True, introducer=True, **kwargs)
|
||||
|
||||
def _get_bounds(self, alpha: float) -> tuple[float]:
|
||||
def _get_bounds(self, alpha: float) -> tuple[float, float]:
|
||||
tw = self.time_width
|
||||
upper = interpolate(0, 1 + tw, alpha)
|
||||
lower = upper - tw
|
||||
|
|
@ -322,7 +327,14 @@ class ShowPassingFlash(ShowPartial):
|
|||
|
||||
|
||||
class ShowPassingFlashWithThinningStrokeWidth(AnimationGroup):
|
||||
def __init__(self, vmobject, n_segments=10, time_width=0.1, remover=True, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
vmobject: VMobject,
|
||||
n_segments: int = 10,
|
||||
time_width: float = 0.1,
|
||||
remover: bool = True,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.n_segments = n_segments
|
||||
self.time_width = time_width
|
||||
self.remover = remover
|
||||
|
|
@ -389,19 +401,19 @@ class ApplyWave(Homotopy):
|
|||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
direction: np.ndarray = UP,
|
||||
direction: Vector3DLike = UP,
|
||||
amplitude: float = 0.2,
|
||||
wave_func: Callable[[float], float] = smooth,
|
||||
wave_func: RateFunction = smooth,
|
||||
time_width: float = 1,
|
||||
ripples: int = 1,
|
||||
run_time: float = 2,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
**kwargs: Any,
|
||||
):
|
||||
x_min = mobject.get_left()[0]
|
||||
x_max = mobject.get_right()[0]
|
||||
vect = amplitude * normalize(direction)
|
||||
|
||||
def wave(t):
|
||||
def wave(t: float) -> float:
|
||||
# Creates a wave with n ripples from a simple rate_func
|
||||
# This wave is build up as follows:
|
||||
# The time is split into 2*ripples phases. In every phase the amplitude
|
||||
|
|
@ -467,7 +479,8 @@ class ApplyWave(Homotopy):
|
|||
relative_x = inverse_interpolate(x_min, x_max, x)
|
||||
wave_phase = inverse_interpolate(lower, upper, relative_x)
|
||||
nudge = wave(wave_phase) * vect
|
||||
return np.array([x, y, z]) + nudge
|
||||
return_value: tuple[float, float, float] = np.array([x, y, z]) + nudge
|
||||
return return_value
|
||||
|
||||
super().__init__(homotopy, mobject, run_time=run_time, **kwargs)
|
||||
|
||||
|
|
@ -511,24 +524,28 @@ class Wiggle(Animation):
|
|||
scale_value: float = 1.1,
|
||||
rotation_angle: float = 0.01 * TAU,
|
||||
n_wiggles: int = 6,
|
||||
scale_about_point: np.ndarray | None = None,
|
||||
rotate_about_point: np.ndarray | None = None,
|
||||
scale_about_point: Point3DLike | None = None,
|
||||
rotate_about_point: Point3DLike | None = None,
|
||||
run_time: float = 2,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.scale_value = scale_value
|
||||
self.rotation_angle = rotation_angle
|
||||
self.n_wiggles = n_wiggles
|
||||
self.scale_about_point = scale_about_point
|
||||
if scale_about_point is not None:
|
||||
self.scale_about_point = np.array(scale_about_point)
|
||||
self.rotate_about_point = rotate_about_point
|
||||
if rotate_about_point is not None:
|
||||
self.rotate_about_point = np.array(rotate_about_point)
|
||||
super().__init__(mobject, run_time=run_time, **kwargs)
|
||||
|
||||
def get_scale_about_point(self) -> np.ndarray:
|
||||
def get_scale_about_point(self) -> Point3D:
|
||||
if self.scale_about_point is None:
|
||||
return self.mobject.get_center()
|
||||
return self.scale_about_point
|
||||
|
||||
def get_rotate_about_point(self) -> np.ndarray:
|
||||
def get_rotate_about_point(self) -> Point3D:
|
||||
if self.rotate_about_point is None:
|
||||
return self.mobject.get_center()
|
||||
return self.rotate_about_point
|
||||
|
|
@ -538,7 +555,7 @@ class Wiggle(Animation):
|
|||
submobject: Mobject,
|
||||
starting_submobject: Mobject,
|
||||
alpha: float,
|
||||
) -> None:
|
||||
) -> Self:
|
||||
submobject.points[:, :] = starting_submobject.points
|
||||
submobject.scale(
|
||||
interpolate(1, self.scale_value, there_and_back(alpha)),
|
||||
|
|
@ -548,6 +565,7 @@ class Wiggle(Animation):
|
|||
wiggle(alpha, self.n_wiggles) * self.rotation_angle,
|
||||
about_point=self.get_rotate_about_point(),
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class Circumscribe(Succession):
|
||||
|
|
@ -595,18 +613,18 @@ class Circumscribe(Succession):
|
|||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
shape: type = Rectangle,
|
||||
fade_in=False,
|
||||
fade_out=False,
|
||||
time_width=0.3,
|
||||
shape: type[Rectangle] | type[Circle] = Rectangle,
|
||||
fade_in: bool = False,
|
||||
fade_out: bool = False,
|
||||
time_width: float = 0.3,
|
||||
buff: float = SMALL_BUFF,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
run_time=1,
|
||||
stroke_width=DEFAULT_STROKE_WIDTH,
|
||||
**kwargs,
|
||||
run_time: float = 1,
|
||||
stroke_width: float = DEFAULT_STROKE_WIDTH,
|
||||
**kwargs: Any,
|
||||
):
|
||||
if shape is Rectangle:
|
||||
frame = SurroundingRectangle(
|
||||
frame: SurroundingRectangle | Circle = SurroundingRectangle(
|
||||
mobject,
|
||||
color=color,
|
||||
buff=buff,
|
||||
|
|
@ -685,7 +703,7 @@ class Blink(Succession):
|
|||
time_off: float = 0.5,
|
||||
blinks: int = 1,
|
||||
hide_at_end: bool = False,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
animations = [
|
||||
UpdateFromFunc(
|
||||
|
|
|
|||
|
|
@ -18,7 +18,13 @@ from ..animation.animation import Animation
|
|||
from ..utils.rate_functions import linear
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..mobject.mobject import Mobject, VMobject
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
from manim.typing import MappingFunction, Point3D
|
||||
from manim.utils.rate_functions import RateFunction
|
||||
|
||||
from ..mobject.mobject import Mobject
|
||||
|
||||
|
||||
class Homotopy(Animation):
|
||||
|
|
@ -72,27 +78,33 @@ class Homotopy(Animation):
|
|||
mobject: Mobject,
|
||||
run_time: float = 3,
|
||||
apply_function_kwargs: dict[str, Any] | None = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.homotopy = homotopy
|
||||
self.apply_function_kwargs = (
|
||||
apply_function_kwargs if apply_function_kwargs is not None else {}
|
||||
)
|
||||
super().__init__(mobject, run_time=run_time, **kwargs)
|
||||
|
||||
def function_at_time_t(self, t: float) -> tuple[float, float, float]:
|
||||
return lambda p: self.homotopy(*p, t)
|
||||
def function_at_time_t(self, t: float) -> MappingFunction:
|
||||
def mapping_function(p: Point3D) -> Point3D:
|
||||
x, y, z = p
|
||||
return np.array(self.homotopy(x, y, z, t))
|
||||
|
||||
return mapping_function
|
||||
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
submobject: Mobject,
|
||||
starting_submobject: Mobject,
|
||||
alpha: float,
|
||||
) -> None:
|
||||
) -> Self:
|
||||
submobject.points = starting_submobject.points
|
||||
submobject.apply_function(
|
||||
self.function_at_time_t(alpha), **self.apply_function_kwargs
|
||||
self.function_at_time_t(alpha),
|
||||
**self.apply_function_kwargs,
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class SmoothedVectorizedHomotopy(Homotopy):
|
||||
|
|
@ -101,15 +113,20 @@ class SmoothedVectorizedHomotopy(Homotopy):
|
|||
submobject: Mobject,
|
||||
starting_submobject: Mobject,
|
||||
alpha: float,
|
||||
) -> None:
|
||||
) -> Self:
|
||||
assert isinstance(submobject, VMobject)
|
||||
super().interpolate_submobject(submobject, starting_submobject, alpha)
|
||||
submobject.make_smooth()
|
||||
return self
|
||||
|
||||
|
||||
class ComplexHomotopy(Homotopy):
|
||||
def __init__(
|
||||
self, complex_homotopy: Callable[[complex], float], mobject: Mobject, **kwargs
|
||||
) -> None:
|
||||
self,
|
||||
complex_homotopy: Callable[[complex, float], float],
|
||||
mobject: Mobject,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Complex Homotopy a function Cx[0, 1] to C"""
|
||||
|
||||
def homotopy(
|
||||
|
|
@ -131,9 +148,9 @@ class PhaseFlow(Animation):
|
|||
mobject: Mobject,
|
||||
virtual_time: float = 1,
|
||||
suspend_mobject_updating: bool = False,
|
||||
rate_func: Callable[[float], float] = linear,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
rate_func: RateFunction = linear,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.virtual_time = virtual_time
|
||||
self.function = function
|
||||
super().__init__(
|
||||
|
|
@ -149,7 +166,7 @@ class PhaseFlow(Animation):
|
|||
self.rate_func(alpha) - self.rate_func(self.last_alpha)
|
||||
)
|
||||
self.mobject.apply_function(lambda p: p + dt * self.function(p))
|
||||
self.last_alpha = alpha
|
||||
self.last_alpha: float = alpha
|
||||
|
||||
|
||||
class MoveAlongPath(Animation):
|
||||
|
|
@ -171,9 +188,9 @@ class MoveAlongPath(Animation):
|
|||
self,
|
||||
mobject: Mobject,
|
||||
path: VMobject,
|
||||
suspend_mobject_updating: bool | None = False,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
suspend_mobject_updating: bool = False,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.path = path
|
||||
super().__init__(
|
||||
mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ from __future__ import annotations
|
|||
__all__ = ["ChangingDecimal", "ChangeDecimalToValue"]
|
||||
|
||||
|
||||
import typing
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from manim.mobject.text.numbers import DecimalNumber
|
||||
|
||||
|
|
@ -14,12 +15,47 @@ from ..utils.bezier import interpolate
|
|||
|
||||
|
||||
class ChangingDecimal(Animation):
|
||||
"""Animate a :class:`~.DecimalNumber` to values specified by a user-supplied function.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
decimal_mob
|
||||
The :class:`~.DecimalNumber` instance to animate.
|
||||
number_update_func
|
||||
A function that returns the number to display at each point in the animation.
|
||||
suspend_mobject_updating
|
||||
If ``True``, the mobject is not updated outside this animation.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
If ``decimal_mob`` is not an instance of :class:`~.DecimalNumber`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim:: ChangingDecimalExample
|
||||
|
||||
class ChangingDecimalExample(Scene):
|
||||
def construct(self):
|
||||
number = DecimalNumber(0)
|
||||
self.add(number)
|
||||
self.play(
|
||||
ChangingDecimal(
|
||||
number,
|
||||
lambda a: 5 * a,
|
||||
run_time=3
|
||||
)
|
||||
)
|
||||
self.wait()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
decimal_mob: DecimalNumber,
|
||||
number_update_func: typing.Callable[[float], float],
|
||||
suspend_mobject_updating: bool | None = False,
|
||||
**kwargs,
|
||||
number_update_func: Callable[[float], float],
|
||||
suspend_mobject_updating: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.check_validity_of_input(decimal_mob)
|
||||
self.number_update_func = number_update_func
|
||||
|
|
@ -32,12 +68,34 @@ class ChangingDecimal(Animation):
|
|||
raise TypeError("ChangingDecimal can only take in a DecimalNumber")
|
||||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
self.mobject.set_value(self.number_update_func(self.rate_func(alpha)))
|
||||
self.mobject.set_value(self.number_update_func(self.rate_func(alpha))) # type: ignore[attr-defined]
|
||||
|
||||
|
||||
class ChangeDecimalToValue(ChangingDecimal):
|
||||
"""Animate a :class:`~.DecimalNumber` to a target value using linear interpolation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
decimal_mob
|
||||
The :class:`~.DecimalNumber` instance to animate.
|
||||
target_number
|
||||
The target value to transition to.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim:: ChangeDecimalToValueExample
|
||||
|
||||
class ChangeDecimalToValueExample(Scene):
|
||||
def construct(self):
|
||||
number = DecimalNumber(0)
|
||||
self.add(number)
|
||||
self.play(ChangeDecimalToValue(number, 10, run_time=3))
|
||||
self.wait()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, decimal_mob: DecimalNumber, target_number: int, **kwargs
|
||||
self, decimal_mob: DecimalNumber, target_number: int, **kwargs: Any
|
||||
) -> None:
|
||||
start_number = decimal_mob.number
|
||||
super().__init__(
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["Rotating", "Rotate"]
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
from collections.abc import Callable, Sequence
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -19,19 +19,85 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class Rotating(Animation):
|
||||
"""Animation that rotates a Mobject.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mobject
|
||||
The mobject to be rotated.
|
||||
angle
|
||||
The rotation angle in radians. Predefined constants such as ``DEGREES``
|
||||
can also be used to specify the angle in degrees.
|
||||
axis
|
||||
The rotation axis as a numpy vector.
|
||||
about_point
|
||||
The rotation center.
|
||||
about_edge
|
||||
If ``about_point`` is ``None``, this argument specifies
|
||||
the direction of the bounding box point to be taken as
|
||||
the rotation center.
|
||||
run_time
|
||||
The duration of the animation in seconds.
|
||||
rate_func
|
||||
The function defining the animation progress based on the relative
|
||||
runtime (see :mod:`~.rate_functions`) .
|
||||
**kwargs
|
||||
Additional keyword arguments passed to :class:`~.Animation`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. manim:: RotatingDemo
|
||||
|
||||
class RotatingDemo(Scene):
|
||||
def construct(self):
|
||||
circle = Circle(radius=1, color=BLUE)
|
||||
line = Line(start=ORIGIN, end=RIGHT)
|
||||
arrow = Arrow(start=ORIGIN, end=RIGHT, buff=0, color=GOLD)
|
||||
vg = VGroup(circle,line,arrow)
|
||||
self.add(vg)
|
||||
anim_kw = {"about_point": arrow.get_start(), "run_time": 1}
|
||||
self.play(Rotating(arrow, 180*DEGREES, **anim_kw))
|
||||
self.play(Rotating(arrow, PI, **anim_kw))
|
||||
self.play(Rotating(vg, PI, about_point=RIGHT))
|
||||
self.play(Rotating(vg, PI, axis=UP, about_point=ORIGIN))
|
||||
self.play(Rotating(vg, PI, axis=RIGHT, about_edge=UP))
|
||||
self.play(vg.animate.move_to(ORIGIN))
|
||||
|
||||
.. manim:: RotatingDifferentAxis
|
||||
|
||||
class RotatingDifferentAxis(ThreeDScene):
|
||||
def construct(self):
|
||||
axes = ThreeDAxes()
|
||||
cube = Cube()
|
||||
arrow2d = Arrow(start=[0, -1.2, 1], end=[0, 1.2, 1], color=YELLOW_E)
|
||||
cube_group = VGroup(cube,arrow2d)
|
||||
self.set_camera_orientation(gamma=0, phi=40*DEGREES, theta=40*DEGREES)
|
||||
self.add(axes, cube_group)
|
||||
play_kw = {"run_time": 1.5}
|
||||
self.play(Rotating(cube_group, PI), **play_kw)
|
||||
self.play(Rotating(cube_group, PI, axis=UP), **play_kw)
|
||||
self.play(Rotating(cube_group, 180*DEGREES, axis=RIGHT), **play_kw)
|
||||
self.wait(0.5)
|
||||
|
||||
See also
|
||||
--------
|
||||
:class:`~.Rotate`, :meth:`~.Mobject.rotate`
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
angle: float = TAU,
|
||||
axis: np.ndarray = OUT,
|
||||
radians: np.ndarray = TAU,
|
||||
about_point: np.ndarray | None = None,
|
||||
about_edge: np.ndarray | None = None,
|
||||
run_time: float = 5,
|
||||
rate_func: Callable[[float], float] = linear,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.angle = angle
|
||||
self.axis = axis
|
||||
self.radians = radians
|
||||
self.about_point = about_point
|
||||
self.about_edge = about_edge
|
||||
super().__init__(mobject, run_time=run_time, rate_func=rate_func, **kwargs)
|
||||
|
|
@ -39,7 +105,7 @@ class Rotating(Animation):
|
|||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
self.mobject.become(self.starting_mobject)
|
||||
self.mobject.rotate(
|
||||
self.rate_func(alpha) * self.radians,
|
||||
self.rate_func(alpha) * self.angle,
|
||||
axis=self.axis,
|
||||
about_point=self.about_point,
|
||||
about_edge=self.about_edge,
|
||||
|
|
@ -80,6 +146,10 @@ class Rotate(Transform):
|
|||
Rotate(Square(side_length=0.5), angle=2*PI, rate_func=linear),
|
||||
)
|
||||
|
||||
See also
|
||||
--------
|
||||
:class:`~.Rotating`, :meth:`~.Mobject.rotate`
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -89,7 +159,7 @@ class Rotate(Transform):
|
|||
axis: np.ndarray = OUT,
|
||||
about_point: Sequence[float] | None = None,
|
||||
about_edge: Sequence[float] | None = None,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
if "path_arc" not in kwargs:
|
||||
kwargs["path_arc"] = angle
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from collections.abc import Sequence
|
|||
from typing import Any
|
||||
|
||||
from manim.animation.transform import Restore
|
||||
from manim.mobject.mobject import Mobject
|
||||
|
||||
from ..constants import *
|
||||
from .composition import LaggedStart
|
||||
|
|
@ -50,7 +51,7 @@ class Broadcast(LaggedStart):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
mobject,
|
||||
mobject: Mobject,
|
||||
focal_point: Sequence[float] = ORIGIN,
|
||||
n_mobs: int = 5,
|
||||
initial_opacity: float = 1,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ from __future__ import annotations
|
|||
|
||||
import inspect
|
||||
import types
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from numpy import piecewise
|
||||
|
||||
|
|
|
|||
|
|
@ -28,11 +28,12 @@ __all__ = [
|
|||
|
||||
import inspect
|
||||
import types
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.data_structures import MethodWithArgs
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLGroup, OpenGLMobject
|
||||
|
||||
from .. import config
|
||||
|
|
@ -208,7 +209,7 @@ class Transform(Animation):
|
|||
self.mobject.align_data(self.target_copy)
|
||||
super().begin()
|
||||
|
||||
def create_target(self) -> Mobject:
|
||||
def create_target(self) -> Mobject | OpenGLMobject:
|
||||
# Has no meaningful effect here, but may be useful
|
||||
# in subclasses
|
||||
return self.target_mobject
|
||||
|
|
@ -438,13 +439,13 @@ class MoveToTarget(Transform):
|
|||
|
||||
|
||||
class _MethodAnimation(MoveToTarget):
|
||||
def __init__(self, mobject, methods):
|
||||
def __init__(self, mobject: Mobject, methods: list[MethodWithArgs]) -> None:
|
||||
self.methods = methods
|
||||
super().__init__(mobject)
|
||||
|
||||
def finish(self) -> None:
|
||||
for method, method_args, method_kwargs in self.methods:
|
||||
method.__func__(self.mobject, *method_args, **method_kwargs)
|
||||
for item in self.methods:
|
||||
item.method.__func__(self.mobject, *item.args, **item.kwargs)
|
||||
super().finish()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,6 @@ class TransformMatchingAbstractBase(AnimationGroup):
|
|||
# target_map
|
||||
transform_source = group_type()
|
||||
transform_target = group_type()
|
||||
kwargs["final_alpha_value"] = 0
|
||||
for key in set(source_map).intersection(target_map):
|
||||
transform_source.add(source_map[key])
|
||||
transform_target.add(target_map[key])
|
||||
|
|
@ -226,7 +225,8 @@ class TransformMatchingShapes(TransformMatchingAbstractBase):
|
|||
mobject.save_state()
|
||||
mobject.center()
|
||||
mobject.set(height=1)
|
||||
result = hash(np.round(mobject.points, 3).tobytes())
|
||||
rounded_points = np.round(mobject.points, 3) + 0.0
|
||||
result = hash(rounded_points.tobytes())
|
||||
mobject.restore()
|
||||
return result
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ __all__ = [
|
|||
|
||||
|
||||
import inspect
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ __all__ = ["UpdateFromFunc", "UpdateFromAlphaFunc", "MaintainPositionRelativeTo"
|
|||
|
||||
|
||||
import operator as op
|
||||
import typing
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from manim.animation.animation import Animation
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.mobject import Mobject
|
||||
|
||||
|
||||
|
|
@ -24,9 +25,9 @@ class UpdateFromFunc(Animation):
|
|||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
update_function: typing.Callable[[Mobject], typing.Any],
|
||||
update_function: Callable[[Mobject], Any],
|
||||
suspend_mobject_updating: bool = False,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.update_function = update_function
|
||||
super().__init__(
|
||||
|
|
@ -34,16 +35,18 @@ class UpdateFromFunc(Animation):
|
|||
)
|
||||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
self.update_function(self.mobject)
|
||||
self.update_function(self.mobject) # type: ignore[arg-type]
|
||||
|
||||
|
||||
class UpdateFromAlphaFunc(UpdateFromFunc):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
self.update_function(self.mobject, self.rate_func(alpha))
|
||||
self.update_function(self.mobject, self.rate_func(alpha)) # type: ignore[call-arg, arg-type]
|
||||
|
||||
|
||||
class MaintainPositionRelativeTo(Animation):
|
||||
def __init__(self, mobject: Mobject, tracked_mobject: Mobject, **kwargs) -> None:
|
||||
def __init__(
|
||||
self, mobject: Mobject, tracked_mobject: Mobject, **kwargs: Any
|
||||
) -> None:
|
||||
self.tracked_mobject = tracked_mobject
|
||||
self.diff = op.sub(
|
||||
mobject.get_center(),
|
||||
|
|
|
|||
|
|
@ -8,19 +8,29 @@ import copy
|
|||
import itertools as it
|
||||
import operator as op
|
||||
import pathlib
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Callable, Iterable
|
||||
from functools import reduce
|
||||
from typing import Any, Callable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import cairo
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
from PIL import Image
|
||||
from scipy.spatial.distance import pdist
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim.typing import (
|
||||
FloatRGBA_Array,
|
||||
FloatRGBALike_Array,
|
||||
ManimInt,
|
||||
PixelArray,
|
||||
Point3D,
|
||||
Point3D_Array,
|
||||
)
|
||||
|
||||
from .. import config, logger
|
||||
from ..constants import *
|
||||
from ..mobject.mobject import Mobject
|
||||
from ..mobject.types.image_mobject import AbstractImageMobject
|
||||
from ..mobject.types.point_cloud_mobject import PMobject
|
||||
from ..mobject.types.vectorized_mobject import VMobject
|
||||
from ..utils.color import ManimColor, ParsableManimColor, color_to_int_rgba
|
||||
|
|
@ -29,6 +39,10 @@ from ..utils.images import get_full_raster_image_path
|
|||
from ..utils.iterables import list_difference_update
|
||||
from ..utils.space_ops import angle_of_vector
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..mobject.types.image_mobject import AbstractImageMobject
|
||||
|
||||
|
||||
LINE_JOIN_MAP = {
|
||||
LineJointType.AUTO: None, # TODO: this could be improved
|
||||
LineJointType.ROUND: cairo.LineJoin.ROUND,
|
||||
|
|
@ -70,13 +84,13 @@ class Camera:
|
|||
def __init__(
|
||||
self,
|
||||
background_image: str | None = None,
|
||||
frame_center: np.ndarray = ORIGIN,
|
||||
frame_center: Point3D = ORIGIN,
|
||||
image_mode: str = "RGBA",
|
||||
n_channels: int = 4,
|
||||
pixel_array_dtype: str = "uint8",
|
||||
cairo_line_width_multiple: float = 0.01,
|
||||
use_z_index: bool = True,
|
||||
background: np.ndarray | None = None,
|
||||
background: PixelArray | None = None,
|
||||
pixel_height: int | None = None,
|
||||
pixel_width: int | None = None,
|
||||
frame_height: float | None = None,
|
||||
|
|
@ -84,8 +98,8 @@ class Camera:
|
|||
frame_rate: float | None = None,
|
||||
background_color: ParsableManimColor | None = None,
|
||||
background_opacity: float | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.background_image = background_image
|
||||
self.frame_center = frame_center
|
||||
self.image_mode = image_mode
|
||||
|
|
@ -94,6 +108,9 @@ class Camera:
|
|||
self.cairo_line_width_multiple = cairo_line_width_multiple
|
||||
self.use_z_index = use_z_index
|
||||
self.background = background
|
||||
self.background_colored_vmobject_displayer: (
|
||||
BackgroundColoredVMobjectDisplayer | None
|
||||
) = None
|
||||
|
||||
if pixel_height is None:
|
||||
pixel_height = config["pixel_height"]
|
||||
|
|
@ -116,11 +133,13 @@ class Camera:
|
|||
self.frame_rate = frame_rate
|
||||
|
||||
if background_color is None:
|
||||
self._background_color = ManimColor.parse(config["background_color"])
|
||||
self._background_color: ManimColor = ManimColor.parse(
|
||||
config["background_color"]
|
||||
)
|
||||
else:
|
||||
self._background_color = ManimColor.parse(background_color)
|
||||
if background_opacity is None:
|
||||
self._background_opacity = config["background_opacity"]
|
||||
self._background_opacity: float = config["background_opacity"]
|
||||
else:
|
||||
self._background_opacity = background_opacity
|
||||
|
||||
|
|
@ -129,7 +148,7 @@ class Camera:
|
|||
self.max_allowable_norm = config["frame_width"]
|
||||
|
||||
self.rgb_max_val = np.iinfo(self.pixel_array_dtype).max
|
||||
self.pixel_array_to_cairo_context = {}
|
||||
self.pixel_array_to_cairo_context: dict[int, cairo.Context] = {}
|
||||
|
||||
# Contains the correct method to process a list of Mobjects of the
|
||||
# corresponding class. If a Mobject is not an instance of a class in
|
||||
|
|
@ -140,7 +159,7 @@ class Camera:
|
|||
self.resize_frame_shape()
|
||||
self.reset()
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
def __deepcopy__(self, memo: Any) -> Camera:
|
||||
# This is to address a strange bug where deepcopying
|
||||
# will result in a segfault, which is somehow related
|
||||
# to the aggdraw library
|
||||
|
|
@ -148,24 +167,26 @@ class Camera:
|
|||
return copy.copy(self)
|
||||
|
||||
@property
|
||||
def background_color(self):
|
||||
def background_color(self) -> ManimColor:
|
||||
return self._background_color
|
||||
|
||||
@background_color.setter
|
||||
def background_color(self, color):
|
||||
def background_color(self, color: ManimColor) -> None:
|
||||
self._background_color = color
|
||||
self.init_background()
|
||||
|
||||
@property
|
||||
def background_opacity(self):
|
||||
def background_opacity(self) -> float:
|
||||
return self._background_opacity
|
||||
|
||||
@background_opacity.setter
|
||||
def background_opacity(self, alpha):
|
||||
def background_opacity(self, alpha: float) -> None:
|
||||
self._background_opacity = alpha
|
||||
self.init_background()
|
||||
|
||||
def type_or_raise(self, mobject: Mobject):
|
||||
def type_or_raise(
|
||||
self, mobject: Mobject
|
||||
) -> type[VMobject] | type[PMobject] | type[AbstractImageMobject] | type[Mobject]:
|
||||
"""Return the type of mobject, if it is a type that can be rendered.
|
||||
|
||||
If `mobject` is an instance of a class that inherits from a class that
|
||||
|
|
@ -192,10 +213,14 @@ class Camera:
|
|||
:exc:`TypeError`
|
||||
When mobject is not an instance of a class that can be rendered.
|
||||
"""
|
||||
self.display_funcs = {
|
||||
VMobject: self.display_multiple_vectorized_mobjects,
|
||||
PMobject: self.display_multiple_point_cloud_mobjects,
|
||||
AbstractImageMobject: self.display_multiple_image_mobjects,
|
||||
from ..mobject.types.image_mobject import AbstractImageMobject
|
||||
|
||||
self.display_funcs: dict[
|
||||
type[Mobject], Callable[[list[Mobject], PixelArray], Any]
|
||||
] = {
|
||||
VMobject: self.display_multiple_vectorized_mobjects, # type: ignore[dict-item]
|
||||
PMobject: self.display_multiple_point_cloud_mobjects, # type: ignore[dict-item]
|
||||
AbstractImageMobject: self.display_multiple_image_mobjects, # type: ignore[dict-item]
|
||||
Mobject: lambda batch, pa: batch, # Do nothing
|
||||
}
|
||||
# We have to check each type in turn because we are dealing with
|
||||
|
|
@ -206,7 +231,7 @@ class Camera:
|
|||
return _type
|
||||
raise TypeError(f"Displaying an object of class {_type} is not supported")
|
||||
|
||||
def reset_pixel_shape(self, new_height: float, new_width: float):
|
||||
def reset_pixel_shape(self, new_height: float, new_width: float) -> None:
|
||||
"""This method resets the height and width
|
||||
of a single pixel to the passed new_height and new_width.
|
||||
|
||||
|
|
@ -223,7 +248,7 @@ class Camera:
|
|||
self.resize_frame_shape()
|
||||
self.reset()
|
||||
|
||||
def resize_frame_shape(self, fixed_dimension: int = 0):
|
||||
def resize_frame_shape(self, fixed_dimension: int = 0) -> None:
|
||||
"""
|
||||
Changes frame_shape to match the aspect ratio
|
||||
of the pixels, where fixed_dimension determines
|
||||
|
|
@ -248,7 +273,7 @@ class Camera:
|
|||
self.frame_height = frame_height
|
||||
self.frame_width = frame_width
|
||||
|
||||
def init_background(self):
|
||||
def init_background(self) -> None:
|
||||
"""Initialize the background.
|
||||
If self.background_image is the path of an image
|
||||
the image is set as background; else, the default
|
||||
|
|
@ -274,7 +299,9 @@ class Camera:
|
|||
)
|
||||
self.background[:, :] = background_rgba
|
||||
|
||||
def get_image(self, pixel_array: np.ndarray | list | tuple | None = None):
|
||||
def get_image(
|
||||
self, pixel_array: PixelArray | list | tuple | None = None
|
||||
) -> Image.Image:
|
||||
"""Returns an image from the passed
|
||||
pixel array, or from the current frame
|
||||
if the passed pixel array is none.
|
||||
|
|
@ -286,7 +313,7 @@ class Camera:
|
|||
|
||||
Returns
|
||||
-------
|
||||
PIL.Image
|
||||
PIL.Image.Image
|
||||
The PIL image of the array.
|
||||
"""
|
||||
if pixel_array is None:
|
||||
|
|
@ -294,8 +321,8 @@ class Camera:
|
|||
return Image.fromarray(pixel_array, mode=self.image_mode)
|
||||
|
||||
def convert_pixel_array(
|
||||
self, pixel_array: np.ndarray | list | tuple, convert_from_floats: bool = False
|
||||
):
|
||||
self, pixel_array: PixelArray | list | tuple, convert_from_floats: bool = False
|
||||
) -> PixelArray:
|
||||
"""Converts a pixel array from values that have floats in then
|
||||
to proper RGB values.
|
||||
|
||||
|
|
@ -321,8 +348,8 @@ class Camera:
|
|||
return retval
|
||||
|
||||
def set_pixel_array(
|
||||
self, pixel_array: np.ndarray | list | tuple, convert_from_floats: bool = False
|
||||
):
|
||||
self, pixel_array: PixelArray | list | tuple, convert_from_floats: bool = False
|
||||
) -> None:
|
||||
"""Sets the pixel array of the camera to the passed pixel array.
|
||||
|
||||
Parameters
|
||||
|
|
@ -332,19 +359,21 @@ class Camera:
|
|||
convert_from_floats
|
||||
Whether or not to convert float values to proper RGB values, by default False
|
||||
"""
|
||||
converted_array = self.convert_pixel_array(pixel_array, convert_from_floats)
|
||||
converted_array: PixelArray = self.convert_pixel_array(
|
||||
pixel_array, convert_from_floats
|
||||
)
|
||||
if not (
|
||||
hasattr(self, "pixel_array")
|
||||
and self.pixel_array.shape == converted_array.shape
|
||||
):
|
||||
self.pixel_array = converted_array
|
||||
self.pixel_array: PixelArray = converted_array
|
||||
else:
|
||||
# Set in place
|
||||
self.pixel_array[:, :, :] = converted_array[:, :, :]
|
||||
|
||||
def set_background(
|
||||
self, pixel_array: np.ndarray | list | tuple, convert_from_floats: bool = False
|
||||
):
|
||||
self, pixel_array: PixelArray | list | tuple, convert_from_floats: bool = False
|
||||
) -> None:
|
||||
"""Sets the background to the passed pixel_array after converting
|
||||
to valid RGB values.
|
||||
|
||||
|
|
@ -360,7 +389,7 @@ class Camera:
|
|||
# TODO, this should live in utils, not as a method of Camera
|
||||
def make_background_from_func(
|
||||
self, coords_to_colors_func: Callable[[np.ndarray], np.ndarray]
|
||||
):
|
||||
) -> PixelArray:
|
||||
"""
|
||||
Makes a pixel array for the background by using coords_to_colors_func to determine each pixel's color. Each input
|
||||
pixel's color. Each input to coords_to_colors_func is an (x, y) pair in space (in ordinary space coordinates; not
|
||||
|
|
@ -386,7 +415,7 @@ class Camera:
|
|||
|
||||
def set_background_from_func(
|
||||
self, coords_to_colors_func: Callable[[np.ndarray], np.ndarray]
|
||||
):
|
||||
) -> None:
|
||||
"""
|
||||
Sets the background to a pixel array using coords_to_colors_func to determine each pixel's color. Each input
|
||||
pixel's color. Each input to coords_to_colors_func is an (x, y) pair in space (in ordinary space coordinates; not
|
||||
|
|
@ -400,7 +429,7 @@ class Camera:
|
|||
"""
|
||||
self.set_background(self.make_background_from_func(coords_to_colors_func))
|
||||
|
||||
def reset(self):
|
||||
def reset(self) -> Self:
|
||||
"""Resets the camera's pixel array
|
||||
to that of the background
|
||||
|
||||
|
|
@ -412,7 +441,7 @@ class Camera:
|
|||
self.set_pixel_array(self.background)
|
||||
return self
|
||||
|
||||
def set_frame_to_background(self, background):
|
||||
def set_frame_to_background(self, background: PixelArray) -> None:
|
||||
self.set_pixel_array(background)
|
||||
|
||||
####
|
||||
|
|
@ -422,7 +451,7 @@ class Camera:
|
|||
mobjects: Iterable[Mobject],
|
||||
include_submobjects: bool = True,
|
||||
excluded_mobjects: list | None = None,
|
||||
):
|
||||
) -> list[Mobject]:
|
||||
"""Used to get the list of mobjects to display
|
||||
with the camera.
|
||||
|
||||
|
|
@ -454,7 +483,7 @@ class Camera:
|
|||
mobjects = list_difference_update(mobjects, all_excluded)
|
||||
return list(mobjects)
|
||||
|
||||
def is_in_frame(self, mobject: Mobject):
|
||||
def is_in_frame(self, mobject: Mobject) -> bool:
|
||||
"""Checks whether the passed mobject is in
|
||||
frame or not.
|
||||
|
||||
|
|
@ -481,7 +510,7 @@ class Camera:
|
|||
],
|
||||
)
|
||||
|
||||
def capture_mobject(self, mobject: Mobject, **kwargs: Any):
|
||||
def capture_mobject(self, mobject: Mobject, **kwargs: Any) -> None:
|
||||
"""Capture mobjects by storing it in :attr:`pixel_array`.
|
||||
|
||||
This is a single-mobject version of :meth:`capture_mobjects`.
|
||||
|
|
@ -497,7 +526,7 @@ class Camera:
|
|||
"""
|
||||
return self.capture_mobjects([mobject], **kwargs)
|
||||
|
||||
def capture_mobjects(self, mobjects: Iterable[Mobject], **kwargs):
|
||||
def capture_mobjects(self, mobjects: Iterable[Mobject], **kwargs: Any) -> None:
|
||||
"""Capture mobjects by printing them on :attr:`pixel_array`.
|
||||
|
||||
This is the essential function that converts the contents of a Scene
|
||||
|
|
@ -532,7 +561,7 @@ class Camera:
|
|||
# NOTE: None of the methods below have been mentioned outside of their definitions. Their DocStrings are not as
|
||||
# detailed as possible.
|
||||
|
||||
def get_cached_cairo_context(self, pixel_array: np.ndarray):
|
||||
def get_cached_cairo_context(self, pixel_array: PixelArray) -> cairo.Context | None:
|
||||
"""Returns the cached cairo context of the passed
|
||||
pixel array if it exists, and None if it doesn't.
|
||||
|
||||
|
|
@ -548,7 +577,7 @@ class Camera:
|
|||
"""
|
||||
return self.pixel_array_to_cairo_context.get(id(pixel_array), None)
|
||||
|
||||
def cache_cairo_context(self, pixel_array: np.ndarray, ctx: cairo.Context):
|
||||
def cache_cairo_context(self, pixel_array: PixelArray, ctx: cairo.Context) -> None:
|
||||
"""Caches the passed Pixel array into a Cairo Context
|
||||
|
||||
Parameters
|
||||
|
|
@ -560,7 +589,7 @@ class Camera:
|
|||
"""
|
||||
self.pixel_array_to_cairo_context[id(pixel_array)] = ctx
|
||||
|
||||
def get_cairo_context(self, pixel_array: np.ndarray):
|
||||
def get_cairo_context(self, pixel_array: PixelArray) -> cairo.Context:
|
||||
"""Returns the cairo context for a pixel array after
|
||||
caching it to self.pixel_array_to_cairo_context
|
||||
If that array has already been cached, it returns the
|
||||
|
|
@ -585,7 +614,7 @@ class Camera:
|
|||
fh = self.frame_height
|
||||
fc = self.frame_center
|
||||
surface = cairo.ImageSurface.create_for_data(
|
||||
pixel_array,
|
||||
pixel_array.data,
|
||||
cairo.FORMAT_ARGB32,
|
||||
pw,
|
||||
ph,
|
||||
|
|
@ -606,8 +635,8 @@ class Camera:
|
|||
return ctx
|
||||
|
||||
def display_multiple_vectorized_mobjects(
|
||||
self, vmobjects: list, pixel_array: np.ndarray
|
||||
):
|
||||
self, vmobjects: list[VMobject], pixel_array: PixelArray
|
||||
) -> None:
|
||||
"""Displays multiple VMobjects in the pixel_array
|
||||
|
||||
Parameters
|
||||
|
|
@ -630,8 +659,8 @@ class Camera:
|
|||
)
|
||||
|
||||
def display_multiple_non_background_colored_vmobjects(
|
||||
self, vmobjects: list, pixel_array: np.ndarray
|
||||
):
|
||||
self, vmobjects: Iterable[VMobject], pixel_array: PixelArray
|
||||
) -> None:
|
||||
"""Displays multiple VMobjects in the cairo context, as long as they don't have
|
||||
background colors.
|
||||
|
||||
|
|
@ -646,7 +675,7 @@ class Camera:
|
|||
for vmobject in vmobjects:
|
||||
self.display_vectorized(vmobject, ctx)
|
||||
|
||||
def display_vectorized(self, vmobject: VMobject, ctx: cairo.Context):
|
||||
def display_vectorized(self, vmobject: VMobject, ctx: cairo.Context) -> Self:
|
||||
"""Displays a VMobject in the cairo context
|
||||
|
||||
Parameters
|
||||
|
|
@ -667,7 +696,7 @@ class Camera:
|
|||
self.apply_stroke(ctx, vmobject)
|
||||
return self
|
||||
|
||||
def set_cairo_context_path(self, ctx: cairo.Context, vmobject: VMobject):
|
||||
def set_cairo_context_path(self, ctx: cairo.Context, vmobject: VMobject) -> Self:
|
||||
"""Sets a path for the cairo context with the vmobject passed
|
||||
|
||||
Parameters
|
||||
|
|
@ -686,7 +715,7 @@ class Camera:
|
|||
# TODO, shouldn't this be handled in transform_points_pre_display?
|
||||
# points = points - self.get_frame_center()
|
||||
if len(points) == 0:
|
||||
return
|
||||
return self
|
||||
|
||||
ctx.new_path()
|
||||
subpaths = vmobject.gen_subpaths_from_points_2d(points)
|
||||
|
|
@ -702,8 +731,8 @@ class Camera:
|
|||
return self
|
||||
|
||||
def set_cairo_context_color(
|
||||
self, ctx: cairo.Context, rgbas: np.ndarray, vmobject: VMobject
|
||||
):
|
||||
self, ctx: cairo.Context, rgbas: FloatRGBALike_Array, vmobject: VMobject
|
||||
) -> Self:
|
||||
"""Sets the color of the cairo context
|
||||
|
||||
Parameters
|
||||
|
|
@ -735,7 +764,7 @@ class Camera:
|
|||
ctx.set_source(pat)
|
||||
return self
|
||||
|
||||
def apply_fill(self, ctx: cairo.Context, vmobject: VMobject):
|
||||
def apply_fill(self, ctx: cairo.Context, vmobject: VMobject) -> Self:
|
||||
"""Fills the cairo context
|
||||
|
||||
Parameters
|
||||
|
|
@ -756,7 +785,7 @@ class Camera:
|
|||
|
||||
def apply_stroke(
|
||||
self, ctx: cairo.Context, vmobject: VMobject, background: bool = False
|
||||
):
|
||||
) -> Self:
|
||||
"""Applies a stroke to the VMobject in the cairo context.
|
||||
|
||||
Parameters
|
||||
|
|
@ -795,7 +824,9 @@ class Camera:
|
|||
ctx.stroke_preserve()
|
||||
return self
|
||||
|
||||
def get_stroke_rgbas(self, vmobject: VMobject, background: bool = False):
|
||||
def get_stroke_rgbas(
|
||||
self, vmobject: VMobject, background: bool = False
|
||||
) -> FloatRGBA_Array:
|
||||
"""Gets the RGBA array for the stroke of the passed
|
||||
VMobject.
|
||||
|
||||
|
|
@ -814,7 +845,7 @@ class Camera:
|
|||
"""
|
||||
return vmobject.get_stroke_rgbas(background)
|
||||
|
||||
def get_fill_rgbas(self, vmobject: VMobject):
|
||||
def get_fill_rgbas(self, vmobject: VMobject) -> FloatRGBA_Array:
|
||||
"""Returns the RGBA array of the fill of the passed VMobject
|
||||
|
||||
Parameters
|
||||
|
|
@ -829,25 +860,27 @@ class Camera:
|
|||
"""
|
||||
return vmobject.get_fill_rgbas()
|
||||
|
||||
def get_background_colored_vmobject_displayer(self):
|
||||
def get_background_colored_vmobject_displayer(
|
||||
self,
|
||||
) -> BackgroundColoredVMobjectDisplayer:
|
||||
"""Returns the background_colored_vmobject_displayer
|
||||
if it exists or makes one and returns it if not.
|
||||
|
||||
Returns
|
||||
-------
|
||||
BackGroundColoredVMobjectDisplayer
|
||||
BackgroundColoredVMobjectDisplayer
|
||||
Object that displays VMobjects that have the same color
|
||||
as the background.
|
||||
"""
|
||||
# Quite wordy to type out a bunch
|
||||
bcvd = "background_colored_vmobject_displayer"
|
||||
if not hasattr(self, bcvd):
|
||||
setattr(self, bcvd, BackgroundColoredVMobjectDisplayer(self))
|
||||
return getattr(self, bcvd)
|
||||
if self.background_colored_vmobject_displayer is None:
|
||||
self.background_colored_vmobject_displayer = (
|
||||
BackgroundColoredVMobjectDisplayer(self)
|
||||
)
|
||||
return self.background_colored_vmobject_displayer
|
||||
|
||||
def display_multiple_background_colored_vmobjects(
|
||||
self, cvmobjects: list, pixel_array: np.ndarray
|
||||
):
|
||||
self, cvmobjects: Iterable[VMobject], pixel_array: PixelArray
|
||||
) -> Self:
|
||||
"""Displays multiple vmobjects that have the same color as the background.
|
||||
|
||||
Parameters
|
||||
|
|
@ -873,8 +906,8 @@ class Camera:
|
|||
# As a result, the other methods do not have as detailed docstrings as would be preferred.
|
||||
|
||||
def display_multiple_point_cloud_mobjects(
|
||||
self, pmobjects: list, pixel_array: np.ndarray
|
||||
):
|
||||
self, pmobjects: Iterable[PMobject], pixel_array: PixelArray
|
||||
) -> None:
|
||||
"""Displays multiple PMobjects by modifying the passed pixel array.
|
||||
|
||||
Parameters
|
||||
|
|
@ -896,11 +929,11 @@ class Camera:
|
|||
def display_point_cloud(
|
||||
self,
|
||||
pmobject: PMobject,
|
||||
points: list,
|
||||
rgbas: np.ndarray,
|
||||
points: Point3D_Array,
|
||||
rgbas: FloatRGBA_Array,
|
||||
thickness: float,
|
||||
pixel_array: np.ndarray,
|
||||
):
|
||||
pixel_array: PixelArray,
|
||||
) -> None:
|
||||
"""Displays a PMobject by modifying the pixel array suitably.
|
||||
|
||||
TODO: Write a description for the rgbas argument.
|
||||
|
|
@ -947,8 +980,10 @@ class Camera:
|
|||
pixel_array[:, :] = new_pa.reshape((ph, pw, rgba_len))
|
||||
|
||||
def display_multiple_image_mobjects(
|
||||
self, image_mobjects: list, pixel_array: np.ndarray
|
||||
):
|
||||
self,
|
||||
image_mobjects: Iterable[AbstractImageMobject],
|
||||
pixel_array: PixelArray,
|
||||
) -> None:
|
||||
"""Displays multiple image mobjects by modifying the passed pixel_array.
|
||||
|
||||
Parameters
|
||||
|
|
@ -963,7 +998,7 @@ class Camera:
|
|||
|
||||
def display_image_mobject(
|
||||
self, image_mobject: AbstractImageMobject, pixel_array: np.ndarray
|
||||
):
|
||||
) -> None:
|
||||
"""Displays an ImageMobject by changing the pixel_array suitably.
|
||||
|
||||
Parameters
|
||||
|
|
@ -1020,7 +1055,9 @@ class Camera:
|
|||
# Paint on top of existing pixel array
|
||||
self.overlay_PIL_image(pixel_array, full_image)
|
||||
|
||||
def overlay_rgba_array(self, pixel_array: np.ndarray, new_array: np.ndarray):
|
||||
def overlay_rgba_array(
|
||||
self, pixel_array: np.ndarray, new_array: np.ndarray
|
||||
) -> None:
|
||||
"""Overlays an RGBA array on top of the given Pixel array.
|
||||
|
||||
Parameters
|
||||
|
|
@ -1032,7 +1069,7 @@ class Camera:
|
|||
"""
|
||||
self.overlay_PIL_image(pixel_array, self.get_image(new_array))
|
||||
|
||||
def overlay_PIL_image(self, pixel_array: np.ndarray, image: Image):
|
||||
def overlay_PIL_image(self, pixel_array: np.ndarray, image: Image) -> None:
|
||||
"""Overlays a PIL image on the passed pixel array.
|
||||
|
||||
Parameters
|
||||
|
|
@ -1047,7 +1084,7 @@ class Camera:
|
|||
dtype="uint8",
|
||||
)
|
||||
|
||||
def adjust_out_of_range_points(self, points: np.ndarray):
|
||||
def adjust_out_of_range_points(self, points: np.ndarray) -> np.ndarray:
|
||||
"""If any of the points in the passed array are out of
|
||||
the viable range, they are adjusted suitably.
|
||||
|
||||
|
|
@ -1078,9 +1115,9 @@ class Camera:
|
|||
|
||||
def transform_points_pre_display(
|
||||
self,
|
||||
mobject,
|
||||
points,
|
||||
): # TODO: Write more detailed docstrings for this method.
|
||||
mobject: Mobject,
|
||||
points: Point3D_Array,
|
||||
) -> Point3D_Array: # TODO: Write more detailed docstrings for this method.
|
||||
# NOTE: There seems to be an unused argument `mobject`.
|
||||
|
||||
# Subclasses (like ThreeDCamera) may want to
|
||||
|
|
@ -1093,9 +1130,9 @@ class Camera:
|
|||
|
||||
def points_to_pixel_coords(
|
||||
self,
|
||||
mobject,
|
||||
points,
|
||||
): # TODO: Write more detailed docstrings for this method.
|
||||
mobject: Mobject,
|
||||
points: Point3D_Array,
|
||||
) -> npt.NDArray[ManimInt]: # TODO: Write more detailed docstrings for this method.
|
||||
points = self.transform_points_pre_display(mobject, points)
|
||||
shifted_points = points - self.frame_center
|
||||
|
||||
|
|
@ -1115,7 +1152,7 @@ class Camera:
|
|||
result[:, 1] = shifted_points[:, 1] * height_mult + height_add
|
||||
return result.astype("int")
|
||||
|
||||
def on_screen_pixels(self, pixel_coords: np.ndarray):
|
||||
def on_screen_pixels(self, pixel_coords: np.ndarray) -> PixelArray:
|
||||
"""Returns array of pixels that are on the screen from a given
|
||||
array of pixel_coordinates
|
||||
|
||||
|
|
@ -1154,12 +1191,12 @@ class Camera:
|
|||
the camera.
|
||||
"""
|
||||
# TODO: This seems...unsystematic
|
||||
big_sum = op.add(config["pixel_height"], config["pixel_width"])
|
||||
this_sum = op.add(self.pixel_height, self.pixel_width)
|
||||
big_sum: float = op.add(config["pixel_height"], config["pixel_width"])
|
||||
this_sum: float = op.add(self.pixel_height, self.pixel_width)
|
||||
factor = big_sum / this_sum
|
||||
return 1 + (thickness - 1) * factor
|
||||
|
||||
def get_thickening_nudges(self, thickness: float):
|
||||
def get_thickening_nudges(self, thickness: float) -> PixelArray:
|
||||
"""Determine a list of vectors used to nudge
|
||||
two-dimensional pixel coordinates.
|
||||
|
||||
|
|
@ -1176,7 +1213,9 @@ class Camera:
|
|||
_range = list(range(-thickness // 2 + 1, thickness // 2 + 1))
|
||||
return np.array(list(it.product(_range, _range)))
|
||||
|
||||
def thickened_coordinates(self, pixel_coords: np.ndarray, thickness: float):
|
||||
def thickened_coordinates(
|
||||
self, pixel_coords: np.ndarray, thickness: float
|
||||
) -> PixelArray:
|
||||
"""Returns thickened coordinates for a passed array of pixel coords and
|
||||
a thickness to thicken by.
|
||||
|
||||
|
|
@ -1198,7 +1237,7 @@ class Camera:
|
|||
return pixel_coords.reshape((size // 2, 2))
|
||||
|
||||
# TODO, reimplement using cairo matrix
|
||||
def get_coords_of_all_pixels(self):
|
||||
def get_coords_of_all_pixels(self) -> PixelArray:
|
||||
"""Returns the cartesian coordinates of each pixel.
|
||||
|
||||
Returns
|
||||
|
|
@ -1246,20 +1285,20 @@ class BackgroundColoredVMobjectDisplayer:
|
|||
|
||||
def __init__(self, camera: Camera):
|
||||
self.camera = camera
|
||||
self.file_name_to_pixel_array_map = {}
|
||||
self.file_name_to_pixel_array_map: dict[str, PixelArray] = {}
|
||||
self.pixel_array = np.array(camera.pixel_array)
|
||||
self.reset_pixel_array()
|
||||
|
||||
def reset_pixel_array(self):
|
||||
def reset_pixel_array(self) -> None:
|
||||
self.pixel_array[:, :] = 0
|
||||
|
||||
def resize_background_array(
|
||||
self,
|
||||
background_array: np.ndarray,
|
||||
background_array: PixelArray,
|
||||
new_width: float,
|
||||
new_height: float,
|
||||
mode: str = "RGBA",
|
||||
):
|
||||
) -> PixelArray:
|
||||
"""Resizes the pixel array representing the background.
|
||||
|
||||
Parameters
|
||||
|
|
@ -1284,8 +1323,8 @@ class BackgroundColoredVMobjectDisplayer:
|
|||
return np.array(resized_image)
|
||||
|
||||
def resize_background_array_to_match(
|
||||
self, background_array: np.ndarray, pixel_array: np.ndarray
|
||||
):
|
||||
self, background_array: PixelArray, pixel_array: PixelArray
|
||||
) -> PixelArray:
|
||||
"""Resizes the background array to match the passed pixel array.
|
||||
|
||||
Parameters
|
||||
|
|
@ -1304,7 +1343,9 @@ class BackgroundColoredVMobjectDisplayer:
|
|||
mode = "RGBA" if pixel_array.shape[2] == 4 else "RGB"
|
||||
return self.resize_background_array(background_array, width, height, mode)
|
||||
|
||||
def get_background_array(self, image: Image.Image | pathlib.Path | str):
|
||||
def get_background_array(
|
||||
self, image: Image.Image | pathlib.Path | str
|
||||
) -> PixelArray:
|
||||
"""Gets the background array that has the passed file_name.
|
||||
|
||||
Parameters
|
||||
|
|
@ -1333,7 +1374,7 @@ class BackgroundColoredVMobjectDisplayer:
|
|||
self.file_name_to_pixel_array_map[image_key] = back_array
|
||||
return back_array
|
||||
|
||||
def display(self, *cvmobjects: VMobject):
|
||||
def display(self, *cvmobjects: VMobject) -> PixelArray | None:
|
||||
"""Displays the colored VMobjects.
|
||||
|
||||
Parameters
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""A camera that allows mapping between objects."""
|
||||
"""A camera module that supports spatial mapping between objects for distortion effects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -17,8 +17,16 @@ from ..utils.config_ops import DictAsObject
|
|||
|
||||
|
||||
class MappingCamera(Camera):
|
||||
"""Camera object that allows mapping
|
||||
between objects.
|
||||
"""Parameters
|
||||
----------
|
||||
mapping_func : callable
|
||||
Function to map 3D points to new 3D points (identity by default).
|
||||
min_num_curves : int
|
||||
Minimum number of curves for VMobjects to avoid visual glitches.
|
||||
allow_object_intrusion : bool
|
||||
If True, modifies original mobjects; else works on copies.
|
||||
kwargs : dict
|
||||
Additional arguments passed to Camera base class.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -34,12 +42,18 @@ class MappingCamera(Camera):
|
|||
super().__init__(**kwargs)
|
||||
|
||||
def points_to_pixel_coords(self, mobject, points):
|
||||
# Map points with custom function before converting to pixels
|
||||
return super().points_to_pixel_coords(
|
||||
mobject,
|
||||
np.apply_along_axis(self.mapping_func, 1, points),
|
||||
)
|
||||
|
||||
def capture_mobjects(self, mobjects, **kwargs):
|
||||
"""Capture mobjects for rendering after applying the spatial mapping.
|
||||
|
||||
Copies mobjects unless intrusion is allowed, and ensures
|
||||
vector objects have enough curves for smooth distortion.
|
||||
"""
|
||||
mobjects = self.get_mobjects_to_display(mobjects, **kwargs)
|
||||
if self.allow_object_intrusion:
|
||||
mobject_copies = mobjects
|
||||
|
|
@ -67,6 +81,13 @@ class MappingCamera(Camera):
|
|||
|
||||
# TODO, the classes below should likely be deleted
|
||||
class OldMultiCamera(Camera):
|
||||
"""Parameters
|
||||
----------
|
||||
cameras_with_start_positions : tuple
|
||||
Tuples of (Camera, (start_y, start_x)) indicating camera and
|
||||
its pixel offset on the final frame.
|
||||
"""
|
||||
|
||||
def __init__(self, *cameras_with_start_positions, **kwargs):
|
||||
self.shifted_cameras = [
|
||||
DictAsObject(
|
||||
|
|
@ -125,6 +146,15 @@ class OldMultiCamera(Camera):
|
|||
|
||||
|
||||
class SplitScreenCamera(OldMultiCamera):
|
||||
"""Initializes a split screen camera setup with two side-by-side cameras.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
left_camera : Camera
|
||||
right_camera : Camera
|
||||
kwargs : dict
|
||||
"""
|
||||
|
||||
def __init__(self, left_camera, right_camera, **kwargs):
|
||||
Camera.__init__(self, **kwargs) # to set attributes such as pixel_width
|
||||
self.left_camera = left_camera
|
||||
|
|
|
|||
|
|
@ -1,45 +1,48 @@
|
|||
"""A camera able to move through a scene.
|
||||
"""Defines the MovingCamera class, a camera that can pan and zoom through a scene.
|
||||
|
||||
.. SEEALSO::
|
||||
|
||||
:mod:`.moving_camera_scene`
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["MovingCamera"]
|
||||
|
||||
import numpy as np
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
from cairo import Context
|
||||
|
||||
from manim.typing import PixelArray, Point3D, Point3DLike
|
||||
|
||||
from .. import config
|
||||
from ..camera.camera import Camera
|
||||
from ..constants import DOWN, LEFT, RIGHT, UP
|
||||
from ..mobject.frame import ScreenRectangle
|
||||
from ..mobject.mobject import Mobject
|
||||
from ..utils.color import WHITE
|
||||
from ..utils.color import WHITE, ManimColor
|
||||
|
||||
|
||||
class MovingCamera(Camera):
|
||||
"""
|
||||
Stays in line with the height, width and position of it's 'frame', which is a Rectangle
|
||||
"""A camera that follows and matches the size and position of its 'frame', a Rectangle (or similar Mobject).
|
||||
|
||||
The frame defines the region of space the camera displays and can move or resize dynamically.
|
||||
|
||||
.. SEEALSO::
|
||||
|
||||
:class:`.MovingCameraScene`
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
frame=None,
|
||||
fixed_dimension=0, # width
|
||||
default_frame_stroke_color=WHITE,
|
||||
default_frame_stroke_width=0,
|
||||
**kwargs,
|
||||
frame: Mobject | None = None,
|
||||
fixed_dimension: int = 0, # width
|
||||
default_frame_stroke_color: ManimColor = WHITE,
|
||||
default_frame_stroke_width: int = 0,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
Frame is a Mobject, (should almost certainly be a rectangle)
|
||||
"""Frame is a Mobject, (should almost certainly be a rectangle)
|
||||
determining which region of space the camera displays
|
||||
"""
|
||||
self.fixed_dimension = fixed_dimension
|
||||
|
|
@ -56,7 +59,7 @@ class MovingCamera(Camera):
|
|||
|
||||
# TODO, make these work for a rotated frame
|
||||
@property
|
||||
def frame_height(self):
|
||||
def frame_height(self) -> float:
|
||||
"""Returns the height of the frame.
|
||||
|
||||
Returns
|
||||
|
|
@ -66,30 +69,8 @@ class MovingCamera(Camera):
|
|||
"""
|
||||
return self.frame.height
|
||||
|
||||
@property
|
||||
def frame_width(self):
|
||||
"""Returns the width of the frame
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The width of the frame.
|
||||
"""
|
||||
return self.frame.width
|
||||
|
||||
@property
|
||||
def frame_center(self):
|
||||
"""Returns the centerpoint of the frame in cartesian coordinates.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.array
|
||||
The cartesian coordinates of the center of the frame.
|
||||
"""
|
||||
return self.frame.get_center()
|
||||
|
||||
@frame_height.setter
|
||||
def frame_height(self, frame_height: float):
|
||||
def frame_height(self, frame_height: float) -> None:
|
||||
"""Sets the height of the frame in MUnits.
|
||||
|
||||
Parameters
|
||||
|
|
@ -99,8 +80,19 @@ class MovingCamera(Camera):
|
|||
"""
|
||||
self.frame.stretch_to_fit_height(frame_height)
|
||||
|
||||
@property
|
||||
def frame_width(self) -> float:
|
||||
"""Returns the width of the frame
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The width of the frame.
|
||||
"""
|
||||
return self.frame.width
|
||||
|
||||
@frame_width.setter
|
||||
def frame_width(self, frame_width: float):
|
||||
def frame_width(self, frame_width: float) -> None:
|
||||
"""Sets the width of the frame in MUnits.
|
||||
|
||||
Parameters
|
||||
|
|
@ -110,8 +102,19 @@ class MovingCamera(Camera):
|
|||
"""
|
||||
self.frame.stretch_to_fit_width(frame_width)
|
||||
|
||||
@property
|
||||
def frame_center(self) -> Point3D:
|
||||
"""Returns the centerpoint of the frame in cartesian coordinates.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.array
|
||||
The cartesian coordinates of the center of the frame.
|
||||
"""
|
||||
return self.frame.get_center()
|
||||
|
||||
@frame_center.setter
|
||||
def frame_center(self, frame_center: np.ndarray | list | tuple | Mobject):
|
||||
def frame_center(self, frame_center: Point3DLike | Mobject) -> None:
|
||||
"""Sets the centerpoint of the frame.
|
||||
|
||||
Parameters
|
||||
|
|
@ -123,25 +126,20 @@ class MovingCamera(Camera):
|
|||
"""
|
||||
self.frame.move_to(frame_center)
|
||||
|
||||
def capture_mobjects(self, mobjects, **kwargs):
|
||||
def capture_mobjects(self, mobjects: Iterable[Mobject], **kwargs: Any) -> None:
|
||||
# self.reset_frame_center()
|
||||
# self.realign_frame_shape()
|
||||
super().capture_mobjects(mobjects, **kwargs)
|
||||
|
||||
# Since the frame can be moving around, the cairo
|
||||
# context used for updating should be regenerated
|
||||
# at each frame. So no caching.
|
||||
def get_cached_cairo_context(self, pixel_array):
|
||||
"""
|
||||
Since the frame can be moving around, the cairo
|
||||
def get_cached_cairo_context(self, pixel_array: PixelArray) -> None:
|
||||
"""Since the frame can be moving around, the cairo
|
||||
context used for updating should be regenerated
|
||||
at each frame. So no caching.
|
||||
"""
|
||||
return None
|
||||
|
||||
def cache_cairo_context(self, pixel_array, ctx):
|
||||
"""
|
||||
Since the frame can be moving around, the cairo
|
||||
def cache_cairo_context(self, pixel_array: PixelArray, ctx: Context) -> None:
|
||||
"""Since the frame can be moving around, the cairo
|
||||
context used for updating should be regenerated
|
||||
at each frame. So no caching.
|
||||
"""
|
||||
|
|
@ -158,24 +156,23 @@ class MovingCamera(Camera):
|
|||
# self.frame_shape = (self.frame.height, width)
|
||||
# self.resize_frame_shape(fixed_dimension=self.fixed_dimension)
|
||||
|
||||
def get_mobjects_indicating_movement(self):
|
||||
"""
|
||||
Returns all mobjects whose movement implies that the camera
|
||||
def get_mobjects_indicating_movement(self) -> list[Mobject]:
|
||||
"""Returns all mobjects whose movement implies that the camera
|
||||
should think of all other mobjects on the screen as moving
|
||||
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
list[Mobject]
|
||||
"""
|
||||
return [self.frame]
|
||||
|
||||
def auto_zoom(
|
||||
self,
|
||||
mobjects: list[Mobject],
|
||||
mobjects: Iterable[Mobject],
|
||||
margin: float = 0,
|
||||
only_mobjects_in_frame: bool = False,
|
||||
animate: bool = True,
|
||||
):
|
||||
) -> Mobject:
|
||||
"""Zooms on to a given array of mobjects (or a singular mobject)
|
||||
and automatically resizes to frame all the mobjects.
|
||||
|
||||
|
|
@ -205,37 +202,12 @@ class MovingCamera(Camera):
|
|||
or ScreenRectangle with position and size updated to zoomed position.
|
||||
|
||||
"""
|
||||
scene_critical_x_left = None
|
||||
scene_critical_x_right = None
|
||||
scene_critical_y_up = None
|
||||
scene_critical_y_down = None
|
||||
|
||||
for m in mobjects:
|
||||
if (m == self.frame) or (
|
||||
only_mobjects_in_frame and not self.is_in_frame(m)
|
||||
):
|
||||
# detected camera frame, should not be used to calculate final position of camera
|
||||
continue
|
||||
|
||||
# initialize scene critical points with first mobjects critical points
|
||||
if scene_critical_x_left is None:
|
||||
scene_critical_x_left = m.get_critical_point(LEFT)[0]
|
||||
scene_critical_x_right = m.get_critical_point(RIGHT)[0]
|
||||
scene_critical_y_up = m.get_critical_point(UP)[1]
|
||||
scene_critical_y_down = m.get_critical_point(DOWN)[1]
|
||||
|
||||
else:
|
||||
if m.get_critical_point(LEFT)[0] < scene_critical_x_left:
|
||||
scene_critical_x_left = m.get_critical_point(LEFT)[0]
|
||||
|
||||
if m.get_critical_point(RIGHT)[0] > scene_critical_x_right:
|
||||
scene_critical_x_right = m.get_critical_point(RIGHT)[0]
|
||||
|
||||
if m.get_critical_point(UP)[1] > scene_critical_y_up:
|
||||
scene_critical_y_up = m.get_critical_point(UP)[1]
|
||||
|
||||
if m.get_critical_point(DOWN)[1] < scene_critical_y_down:
|
||||
scene_critical_y_down = m.get_critical_point(DOWN)[1]
|
||||
(
|
||||
scene_critical_x_left,
|
||||
scene_critical_x_right,
|
||||
scene_critical_y_up,
|
||||
scene_critical_y_down,
|
||||
) = self._get_bounding_box(mobjects, only_mobjects_in_frame)
|
||||
|
||||
# calculate center x and y
|
||||
x = (scene_critical_x_left + scene_critical_x_right) / 2
|
||||
|
|
@ -251,3 +223,52 @@ class MovingCamera(Camera):
|
|||
return m_target.set_x(x).set_y(y).set(width=new_width + margin)
|
||||
else:
|
||||
return m_target.set_x(x).set_y(y).set(height=new_height + margin)
|
||||
|
||||
def _get_bounding_box(
|
||||
self, mobjects: Iterable[Mobject], only_mobjects_in_frame: bool
|
||||
) -> tuple[float, float, float, float]:
|
||||
bounding_box_located = False
|
||||
scene_critical_x_left: float = 0
|
||||
scene_critical_x_right: float = 1
|
||||
scene_critical_y_up: float = 1
|
||||
scene_critical_y_down: float = 0
|
||||
|
||||
for m in mobjects:
|
||||
if (m == self.frame) or (
|
||||
only_mobjects_in_frame and not self.is_in_frame(m)
|
||||
):
|
||||
# detected camera frame, should not be used to calculate final position of camera
|
||||
continue
|
||||
|
||||
# initialize scene critical points with first mobjects critical points
|
||||
if not bounding_box_located:
|
||||
scene_critical_x_left = m.get_critical_point(LEFT)[0]
|
||||
scene_critical_x_right = m.get_critical_point(RIGHT)[0]
|
||||
scene_critical_y_up = m.get_critical_point(UP)[1]
|
||||
scene_critical_y_down = m.get_critical_point(DOWN)[1]
|
||||
bounding_box_located = True
|
||||
|
||||
else:
|
||||
if m.get_critical_point(LEFT)[0] < scene_critical_x_left:
|
||||
scene_critical_x_left = m.get_critical_point(LEFT)[0]
|
||||
|
||||
if m.get_critical_point(RIGHT)[0] > scene_critical_x_right:
|
||||
scene_critical_x_right = m.get_critical_point(RIGHT)[0]
|
||||
|
||||
if m.get_critical_point(UP)[1] > scene_critical_y_up:
|
||||
scene_critical_y_up = m.get_critical_point(UP)[1]
|
||||
|
||||
if m.get_critical_point(DOWN)[1] < scene_critical_y_down:
|
||||
scene_critical_y_down = m.get_critical_point(DOWN)[1]
|
||||
|
||||
if not bounding_box_located:
|
||||
raise Exception(
|
||||
"Could not determine bounding box of the mobjects given to 'auto_zoom'."
|
||||
)
|
||||
|
||||
return (
|
||||
scene_critical_x_left,
|
||||
scene_critical_x_right,
|
||||
scene_critical_y_up,
|
||||
scene_critical_y_down,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,13 @@ from __future__ import annotations
|
|||
__all__ = ["MultiCamera"]
|
||||
|
||||
|
||||
from manim.mobject.types.image_mobject import ImageMobject
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.types.image_mobject import ImageMobjectFromCamera
|
||||
|
||||
from ..camera.moving_camera import MovingCamera
|
||||
from ..utils.iterables import list_difference_update
|
||||
|
|
@ -16,10 +22,10 @@ class MultiCamera(MovingCamera):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
image_mobjects_from_cameras: ImageMobject | None = None,
|
||||
allow_cameras_to_capture_their_own_display=False,
|
||||
**kwargs,
|
||||
):
|
||||
image_mobjects_from_cameras: Iterable[ImageMobjectFromCamera] | None = None,
|
||||
allow_cameras_to_capture_their_own_display: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialises the MultiCamera
|
||||
|
||||
Parameters
|
||||
|
|
@ -29,7 +35,7 @@ class MultiCamera(MovingCamera):
|
|||
kwargs
|
||||
Any valid keyword arguments of MovingCamera.
|
||||
"""
|
||||
self.image_mobjects_from_cameras = []
|
||||
self.image_mobjects_from_cameras: list[ImageMobjectFromCamera] = []
|
||||
if image_mobjects_from_cameras is not None:
|
||||
for imfc in image_mobjects_from_cameras:
|
||||
self.add_image_mobject_from_camera(imfc)
|
||||
|
|
@ -38,7 +44,9 @@ class MultiCamera(MovingCamera):
|
|||
)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def add_image_mobject_from_camera(self, image_mobject_from_camera: ImageMobject):
|
||||
def add_image_mobject_from_camera(
|
||||
self, image_mobject_from_camera: ImageMobjectFromCamera
|
||||
) -> None:
|
||||
"""Adds an ImageMobject that's been obtained from the camera
|
||||
into the list ``self.image_mobject_from_cameras``
|
||||
|
||||
|
|
@ -53,20 +61,20 @@ class MultiCamera(MovingCamera):
|
|||
assert isinstance(imfc.camera, MovingCamera)
|
||||
self.image_mobjects_from_cameras.append(imfc)
|
||||
|
||||
def update_sub_cameras(self):
|
||||
def update_sub_cameras(self) -> None:
|
||||
"""Reshape sub_camera pixel_arrays"""
|
||||
for imfc in self.image_mobjects_from_cameras:
|
||||
pixel_height, pixel_width = self.pixel_array.shape[:2]
|
||||
imfc.camera.frame_shape = (
|
||||
imfc.camera.frame.height,
|
||||
imfc.camera.frame.width,
|
||||
)
|
||||
# imfc.camera.frame_shape = (
|
||||
# imfc.camera.frame.height,
|
||||
# imfc.camera.frame.width,
|
||||
# )
|
||||
imfc.camera.reset_pixel_shape(
|
||||
int(pixel_height * imfc.height / self.frame_height),
|
||||
int(pixel_width * imfc.width / self.frame_width),
|
||||
)
|
||||
|
||||
def reset(self):
|
||||
def reset(self) -> Self:
|
||||
"""Resets the MultiCamera.
|
||||
|
||||
Returns
|
||||
|
|
@ -79,7 +87,7 @@ class MultiCamera(MovingCamera):
|
|||
super().reset()
|
||||
return self
|
||||
|
||||
def capture_mobjects(self, mobjects, **kwargs):
|
||||
def capture_mobjects(self, mobjects: Iterable[Mobject], **kwargs: Any) -> None:
|
||||
self.update_sub_cameras()
|
||||
for imfc in self.image_mobjects_from_cameras:
|
||||
to_add = list(mobjects)
|
||||
|
|
@ -88,7 +96,7 @@ class MultiCamera(MovingCamera):
|
|||
imfc.camera.capture_mobjects(to_add, **kwargs)
|
||||
super().capture_mobjects(mobjects, **kwargs)
|
||||
|
||||
def get_mobjects_indicating_movement(self):
|
||||
def get_mobjects_indicating_movement(self) -> list[Mobject]:
|
||||
"""Returns all mobjects whose movement implies that the camera
|
||||
should think of all other mobjects on the screen as moving
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ from __future__ import annotations
|
|||
__all__ = ["ThreeDCamera"]
|
||||
|
||||
|
||||
from typing import Callable
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -16,7 +17,15 @@ from manim.mobject.three_d.three_d_utils import (
|
|||
get_3d_vmob_start_corner,
|
||||
get_3d_vmob_start_corner_unit_normal,
|
||||
)
|
||||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
from manim.mobject.value_tracker import ValueTracker
|
||||
from manim.typing import (
|
||||
FloatRGBA_Array,
|
||||
MatrixMN,
|
||||
Point3D,
|
||||
Point3D_Array,
|
||||
Point3DLike,
|
||||
)
|
||||
|
||||
from .. import config
|
||||
from ..camera.camera import Camera
|
||||
|
|
@ -30,17 +39,17 @@ from ..utils.space_ops import rotation_about_z, rotation_matrix
|
|||
class ThreeDCamera(Camera):
|
||||
def __init__(
|
||||
self,
|
||||
focal_distance=20.0,
|
||||
shading_factor=0.2,
|
||||
default_distance=5.0,
|
||||
light_source_start_point=9 * DOWN + 7 * LEFT + 10 * OUT,
|
||||
should_apply_shading=True,
|
||||
exponential_projection=False,
|
||||
phi=0,
|
||||
theta=-90 * DEGREES,
|
||||
gamma=0,
|
||||
zoom=1,
|
||||
**kwargs,
|
||||
focal_distance: float = 20.0,
|
||||
shading_factor: float = 0.2,
|
||||
default_distance: float = 5.0,
|
||||
light_source_start_point: Point3DLike = 9 * DOWN + 7 * LEFT + 10 * OUT,
|
||||
should_apply_shading: bool = True,
|
||||
exponential_projection: bool = False,
|
||||
phi: float = 0,
|
||||
theta: float = -90 * DEGREES,
|
||||
gamma: float = 0,
|
||||
zoom: float = 1,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Initializes the ThreeDCamera
|
||||
|
||||
|
|
@ -68,23 +77,23 @@ class ThreeDCamera(Camera):
|
|||
self.focal_distance_tracker = ValueTracker(self.focal_distance)
|
||||
self.gamma_tracker = ValueTracker(self.gamma)
|
||||
self.zoom_tracker = ValueTracker(self.zoom)
|
||||
self.fixed_orientation_mobjects = {}
|
||||
self.fixed_in_frame_mobjects = set()
|
||||
self.fixed_orientation_mobjects: dict[Mobject, Callable[[], Point3D]] = {}
|
||||
self.fixed_in_frame_mobjects: set[Mobject] = set()
|
||||
self.reset_rotation_matrix()
|
||||
|
||||
@property
|
||||
def frame_center(self):
|
||||
def frame_center(self) -> Point3D:
|
||||
return self._frame_center.points[0]
|
||||
|
||||
@frame_center.setter
|
||||
def frame_center(self, point):
|
||||
def frame_center(self, point: Point3DLike) -> None:
|
||||
self._frame_center.move_to(point)
|
||||
|
||||
def capture_mobjects(self, mobjects, **kwargs):
|
||||
def capture_mobjects(self, mobjects: Iterable[Mobject], **kwargs: Any) -> None:
|
||||
self.reset_rotation_matrix()
|
||||
super().capture_mobjects(mobjects, **kwargs)
|
||||
|
||||
def get_value_trackers(self):
|
||||
def get_value_trackers(self) -> list[ValueTracker]:
|
||||
"""A list of :class:`ValueTrackers <.ValueTracker>` of phi, theta, focal_distance,
|
||||
gamma and zoom.
|
||||
|
||||
|
|
@ -101,7 +110,9 @@ class ThreeDCamera(Camera):
|
|||
self.zoom_tracker,
|
||||
]
|
||||
|
||||
def modified_rgbas(self, vmobject, rgbas):
|
||||
def modified_rgbas(
|
||||
self, vmobject: VMobject, rgbas: FloatRGBA_Array
|
||||
) -> FloatRGBA_Array:
|
||||
if not self.should_apply_shading:
|
||||
return rgbas
|
||||
if vmobject.shade_in_3d and (vmobject.get_num_points() > 0):
|
||||
|
|
@ -127,28 +138,33 @@ class ThreeDCamera(Camera):
|
|||
|
||||
def get_stroke_rgbas(
|
||||
self,
|
||||
vmobject,
|
||||
background=False,
|
||||
): # NOTE : DocStrings From parent
|
||||
vmobject: VMobject,
|
||||
background: bool = False,
|
||||
) -> FloatRGBA_Array: # NOTE : DocStrings From parent
|
||||
return self.modified_rgbas(vmobject, vmobject.get_stroke_rgbas(background))
|
||||
|
||||
def get_fill_rgbas(self, vmobject): # NOTE : DocStrings From parent
|
||||
def get_fill_rgbas(
|
||||
self, vmobject: VMobject
|
||||
) -> FloatRGBA_Array: # NOTE : DocStrings From parent
|
||||
return self.modified_rgbas(vmobject, vmobject.get_fill_rgbas())
|
||||
|
||||
def get_mobjects_to_display(self, *args, **kwargs): # NOTE : DocStrings From parent
|
||||
def get_mobjects_to_display(
|
||||
self, *args: Any, **kwargs: Any
|
||||
) -> list[Mobject]: # NOTE : DocStrings From parent
|
||||
mobjects = super().get_mobjects_to_display(*args, **kwargs)
|
||||
rot_matrix = self.get_rotation_matrix()
|
||||
|
||||
def z_key(mob):
|
||||
def z_key(mob: Mobject) -> float:
|
||||
if not (hasattr(mob, "shade_in_3d") and mob.shade_in_3d):
|
||||
return np.inf
|
||||
return np.inf # type: ignore[no-any-return]
|
||||
# Assign a number to a three dimensional mobjects
|
||||
# based on how close it is to the camera
|
||||
return np.dot(mob.get_z_index_reference_point(), rot_matrix.T)[2]
|
||||
distance: float = np.dot(mob.get_z_index_reference_point(), rot_matrix.T)[2]
|
||||
return distance
|
||||
|
||||
return sorted(mobjects, key=z_key)
|
||||
|
||||
def get_phi(self):
|
||||
def get_phi(self) -> float:
|
||||
"""Returns the Polar angle (the angle off Z_AXIS) phi.
|
||||
|
||||
Returns
|
||||
|
|
@ -158,7 +174,7 @@ class ThreeDCamera(Camera):
|
|||
"""
|
||||
return self.phi_tracker.get_value()
|
||||
|
||||
def get_theta(self):
|
||||
def get_theta(self) -> float:
|
||||
"""Returns the Azimuthal i.e the angle that spins the camera around the Z_AXIS.
|
||||
|
||||
Returns
|
||||
|
|
@ -168,7 +184,7 @@ class ThreeDCamera(Camera):
|
|||
"""
|
||||
return self.theta_tracker.get_value()
|
||||
|
||||
def get_focal_distance(self):
|
||||
def get_focal_distance(self) -> float:
|
||||
"""Returns focal_distance of the Camera.
|
||||
|
||||
Returns
|
||||
|
|
@ -178,7 +194,7 @@ class ThreeDCamera(Camera):
|
|||
"""
|
||||
return self.focal_distance_tracker.get_value()
|
||||
|
||||
def get_gamma(self):
|
||||
def get_gamma(self) -> float:
|
||||
"""Returns the rotation of the camera about the vector from the ORIGIN to the Camera.
|
||||
|
||||
Returns
|
||||
|
|
@ -189,7 +205,7 @@ class ThreeDCamera(Camera):
|
|||
"""
|
||||
return self.gamma_tracker.get_value()
|
||||
|
||||
def get_zoom(self):
|
||||
def get_zoom(self) -> float:
|
||||
"""Returns the zoom amount of the camera.
|
||||
|
||||
Returns
|
||||
|
|
@ -199,7 +215,7 @@ class ThreeDCamera(Camera):
|
|||
"""
|
||||
return self.zoom_tracker.get_value()
|
||||
|
||||
def set_phi(self, value: float):
|
||||
def set_phi(self, value: float) -> None:
|
||||
"""Sets the polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians.
|
||||
|
||||
Parameters
|
||||
|
|
@ -209,7 +225,7 @@ class ThreeDCamera(Camera):
|
|||
"""
|
||||
self.phi_tracker.set_value(value)
|
||||
|
||||
def set_theta(self, value: float):
|
||||
def set_theta(self, value: float) -> None:
|
||||
"""Sets the azimuthal angle i.e the angle that spins the camera around Z_AXIS in radians.
|
||||
|
||||
Parameters
|
||||
|
|
@ -219,7 +235,7 @@ class ThreeDCamera(Camera):
|
|||
"""
|
||||
self.theta_tracker.set_value(value)
|
||||
|
||||
def set_focal_distance(self, value: float):
|
||||
def set_focal_distance(self, value: float) -> None:
|
||||
"""Sets the focal_distance of the Camera.
|
||||
|
||||
Parameters
|
||||
|
|
@ -229,7 +245,7 @@ class ThreeDCamera(Camera):
|
|||
"""
|
||||
self.focal_distance_tracker.set_value(value)
|
||||
|
||||
def set_gamma(self, value: float):
|
||||
def set_gamma(self, value: float) -> None:
|
||||
"""Sets the angle of rotation of the camera about the vector from the ORIGIN to the Camera.
|
||||
|
||||
Parameters
|
||||
|
|
@ -239,7 +255,7 @@ class ThreeDCamera(Camera):
|
|||
"""
|
||||
self.gamma_tracker.set_value(value)
|
||||
|
||||
def set_zoom(self, value: float):
|
||||
def set_zoom(self, value: float) -> None:
|
||||
"""Sets the zoom amount of the camera.
|
||||
|
||||
Parameters
|
||||
|
|
@ -249,13 +265,13 @@ class ThreeDCamera(Camera):
|
|||
"""
|
||||
self.zoom_tracker.set_value(value)
|
||||
|
||||
def reset_rotation_matrix(self):
|
||||
def reset_rotation_matrix(self) -> None:
|
||||
"""Sets the value of self.rotation_matrix to
|
||||
the matrix corresponding to the current position of the camera
|
||||
"""
|
||||
self.rotation_matrix = self.generate_rotation_matrix()
|
||||
|
||||
def get_rotation_matrix(self):
|
||||
def get_rotation_matrix(self) -> MatrixMN:
|
||||
"""Returns the matrix corresponding to the current position of the camera.
|
||||
|
||||
Returns
|
||||
|
|
@ -265,7 +281,7 @@ class ThreeDCamera(Camera):
|
|||
"""
|
||||
return self.rotation_matrix
|
||||
|
||||
def generate_rotation_matrix(self):
|
||||
def generate_rotation_matrix(self) -> MatrixMN:
|
||||
"""Generates a rotation matrix based off the current position of the camera.
|
||||
|
||||
Returns
|
||||
|
|
@ -286,7 +302,7 @@ class ThreeDCamera(Camera):
|
|||
result = np.dot(matrix, result)
|
||||
return result
|
||||
|
||||
def project_points(self, points: np.ndarray | list):
|
||||
def project_points(self, points: Point3D_Array) -> Point3D_Array:
|
||||
"""Applies the current rotation_matrix as a projection
|
||||
matrix to the passed array of points.
|
||||
|
||||
|
|
@ -323,7 +339,7 @@ class ThreeDCamera(Camera):
|
|||
points[:, i] *= factor * zoom
|
||||
return points
|
||||
|
||||
def project_point(self, point: list | np.ndarray):
|
||||
def project_point(self, point: Point3D) -> Point3D:
|
||||
"""Applies the current rotation_matrix as a projection
|
||||
matrix to the passed point.
|
||||
|
||||
|
|
@ -341,9 +357,9 @@ class ThreeDCamera(Camera):
|
|||
|
||||
def transform_points_pre_display(
|
||||
self,
|
||||
mobject,
|
||||
points,
|
||||
): # TODO: Write Docstrings for this Method.
|
||||
mobject: Mobject,
|
||||
points: Point3D_Array,
|
||||
) -> Point3D_Array: # TODO: Write Docstrings for this Method.
|
||||
points = super().transform_points_pre_display(mobject, points)
|
||||
fixed_orientation = mobject in self.fixed_orientation_mobjects
|
||||
fixed_in_frame = mobject in self.fixed_in_frame_mobjects
|
||||
|
|
@ -362,8 +378,8 @@ class ThreeDCamera(Camera):
|
|||
self,
|
||||
*mobjects: Mobject,
|
||||
use_static_center_func: bool = False,
|
||||
center_func: Callable[[], np.ndarray] | None = None,
|
||||
):
|
||||
center_func: Callable[[], Point3D] | None = None,
|
||||
) -> None:
|
||||
"""This method allows the mobject to have a fixed orientation,
|
||||
even when the camera moves around.
|
||||
E.G If it was passed through this method, facing the camera, it
|
||||
|
|
@ -384,7 +400,7 @@ class ThreeDCamera(Camera):
|
|||
|
||||
# This prevents the computation of mobject.get_center
|
||||
# every single time a projection happens
|
||||
def get_static_center_func(mobject):
|
||||
def get_static_center_func(mobject: Mobject) -> Callable[[], Point3D]:
|
||||
point = mobject.get_center()
|
||||
return lambda: point
|
||||
|
||||
|
|
@ -398,7 +414,7 @@ class ThreeDCamera(Camera):
|
|||
for submob in mobject.get_family():
|
||||
self.fixed_orientation_mobjects[submob] = func
|
||||
|
||||
def add_fixed_in_frame_mobjects(self, *mobjects: Mobject):
|
||||
def add_fixed_in_frame_mobjects(self, *mobjects: Mobject) -> None:
|
||||
"""This method allows the mobject to have a fixed position,
|
||||
even when the camera moves around.
|
||||
E.G If it was passed through this method, at the top of the frame, it
|
||||
|
|
@ -414,7 +430,7 @@ class ThreeDCamera(Camera):
|
|||
for mobject in extract_mobject_family_members(mobjects):
|
||||
self.fixed_in_frame_mobjects.add(mobject)
|
||||
|
||||
def remove_fixed_orientation_mobjects(self, *mobjects: Mobject):
|
||||
def remove_fixed_orientation_mobjects(self, *mobjects: Mobject) -> None:
|
||||
"""If a mobject was fixed in its orientation by passing it through
|
||||
:meth:`.add_fixed_orientation_mobjects`, then this undoes that fixing.
|
||||
The Mobject will no longer have a fixed orientation.
|
||||
|
|
@ -428,7 +444,7 @@ class ThreeDCamera(Camera):
|
|||
if mobject in self.fixed_orientation_mobjects:
|
||||
del self.fixed_orientation_mobjects[mobject]
|
||||
|
||||
def remove_fixed_in_frame_mobjects(self, *mobjects: Mobject):
|
||||
def remove_fixed_in_frame_mobjects(self, *mobjects: Mobject) -> None:
|
||||
"""If a mobject was fixed in frame by passing it through
|
||||
:meth:`.add_fixed_in_frame_mobjects`, then this undoes that fixing.
|
||||
The Mobject will no longer be fixed in frame.
|
||||
|
|
|
|||
|
|
@ -267,6 +267,12 @@ modify write_cfg_subcmd_input to account for it.""",
|
|||
|
||||
@cfg.command(context_settings=cli_ctx_settings)
|
||||
def show() -> None:
|
||||
console.print("CONFIG FILES READ", style="bold green underline")
|
||||
for path in config_file_paths():
|
||||
if path.exists():
|
||||
console.print(f"{path}")
|
||||
console.print()
|
||||
|
||||
parser = make_config_parser()
|
||||
rich_non_style_entries = [a.replace(".", "_") for a in RICH_NON_STYLE_ENTRIES]
|
||||
for category in parser:
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ from __future__ import annotations
|
|||
|
||||
import os
|
||||
import shutil
|
||||
from typing import Callable, Protocol, cast
|
||||
from collections.abc import Callable
|
||||
from typing import Protocol, cast
|
||||
|
||||
__all__ = ["HEALTH_CHECKS"]
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ In particular, this class is what allows ``manim`` to act as ``manim render``.
|
|||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import cloup
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ CFG_DEFAULTS = {
|
|||
"background_color": "BLACK",
|
||||
"background_opacity": 1,
|
||||
"scene_names": "Default",
|
||||
"resolution": (854, 480),
|
||||
"resolution": (1920, 1080),
|
||||
}
|
||||
|
||||
__all__ = ["select_resolution", "update_cfg", "project", "scene"]
|
||||
|
|
|
|||
|
|
@ -125,23 +125,23 @@ global_options = option_group(
|
|||
"--force_window",
|
||||
is_flag=True,
|
||||
help="Force window to open when using the opengl renderer, intended for debugging as it may impact performance",
|
||||
default=False,
|
||||
default=None,
|
||||
),
|
||||
option(
|
||||
"--dry_run",
|
||||
is_flag=True,
|
||||
help="Renders animations without outputting image or video files and disables the window",
|
||||
default=False,
|
||||
default=None,
|
||||
),
|
||||
option(
|
||||
"--no_latex_cleanup",
|
||||
is_flag=True,
|
||||
help="Prevents deletion of .aux, .dvi, and .log files produced by Tex and MathTex.",
|
||||
default=False,
|
||||
default=None,
|
||||
),
|
||||
option(
|
||||
"--preview_command",
|
||||
help="The command used to preview the output file (for example vlc for video files)",
|
||||
default="",
|
||||
default=None,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ SCENE_NOT_FOUND_MESSAGE = """
|
|||
"""
|
||||
CHOOSE_NUMBER_MESSAGE = """
|
||||
Choose number corresponding to desired scene/arguments.
|
||||
(Use comma separated list for multiple entries)
|
||||
(Use comma separated list for multiple entries or use "*" to select all scenes.)
|
||||
Choice(s): """
|
||||
INVALID_NUMBER_MESSAGE = "Invalid scene numbers have been specified. Aborting."
|
||||
NO_SCENE_MESSAGE = """
|
||||
|
|
|
|||
31
manim/data_structures.py
Normal file
31
manim/data_structures.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
"""Data classes and other necessary data structures for use in Manim."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from types import MethodType
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class MethodWithArgs:
|
||||
"""Object containing a :attr:`method` which is intended to be called later
|
||||
with the positional arguments :attr:`args` and the keyword arguments
|
||||
:attr:`kwargs`.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
method : MethodType
|
||||
A callable representing a method of some class.
|
||||
args : Iterable[Any]
|
||||
Positional arguments for :attr:`method`.
|
||||
kwargs : dict[str, Any]
|
||||
Keyword arguments for :attr:`method`.
|
||||
"""
|
||||
|
||||
__slots__ = ["method", "args", "kwargs"]
|
||||
|
||||
method: MethodType
|
||||
args: Iterable[Any]
|
||||
kwargs: dict[str, Any]
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import dearpygui.dearpygui as dpg
|
||||
|
||||
dearpygui_imported = True
|
||||
except ImportError:
|
||||
dearpygui_imported = False
|
||||
|
||||
|
||||
from .. import __version__, config
|
||||
from ..utils.module_ops import scene_classes_from_file
|
||||
|
||||
__all__ = ["configure_pygui"]
|
||||
|
||||
if dearpygui_imported:
|
||||
dpg.create_context()
|
||||
window = dpg.generate_uuid()
|
||||
|
||||
|
||||
def configure_pygui(renderer, widgets, update=True):
|
||||
if not dearpygui_imported:
|
||||
raise RuntimeError("Attempted to use DearPyGUI when it isn't imported.")
|
||||
if update:
|
||||
dpg.delete_item(window)
|
||||
else:
|
||||
dpg.create_viewport()
|
||||
dpg.setup_dearpygui()
|
||||
dpg.show_viewport()
|
||||
|
||||
dpg.set_viewport_title(title=f"Manim Community v{__version__}")
|
||||
dpg.set_viewport_width(1015)
|
||||
dpg.set_viewport_height(540)
|
||||
|
||||
def rerun_callback(sender, data):
|
||||
renderer.scene.queue.put(("rerun_gui", [], {}))
|
||||
|
||||
def continue_callback(sender, data):
|
||||
renderer.scene.queue.put(("exit_gui", [], {}))
|
||||
|
||||
def scene_selection_callback(sender, data):
|
||||
config["scene_names"] = (dpg.get_value(sender),)
|
||||
renderer.scene.queue.put(("rerun_gui", [], {}))
|
||||
|
||||
scene_classes = scene_classes_from_file(Path(config["input_file"]), full_list=True)
|
||||
scene_names = [scene_class.__name__ for scene_class in scene_classes]
|
||||
|
||||
with dpg.window(
|
||||
id=window,
|
||||
label="Manim GUI",
|
||||
pos=[config["gui_location"][0], config["gui_location"][1]],
|
||||
width=1000,
|
||||
height=500,
|
||||
):
|
||||
dpg.set_global_font_scale(2)
|
||||
dpg.add_button(label="Rerun", callback=rerun_callback)
|
||||
dpg.add_button(label="Continue", callback=continue_callback)
|
||||
dpg.add_combo(
|
||||
label="Selected scene",
|
||||
items=scene_names,
|
||||
callback=scene_selection_callback,
|
||||
default_value=config["scene_names"][0],
|
||||
)
|
||||
dpg.add_separator()
|
||||
if len(widgets) != 0:
|
||||
with dpg.collapsing_header(
|
||||
label=f"{config['scene_names'][0]} widgets",
|
||||
default_open=True,
|
||||
):
|
||||
for widget_config in widgets:
|
||||
widget_config_copy = widget_config.copy()
|
||||
name = widget_config_copy["name"]
|
||||
widget = widget_config_copy["widget"]
|
||||
if widget != "separator":
|
||||
del widget_config_copy["name"]
|
||||
del widget_config_copy["widget"]
|
||||
getattr(dpg, f"add_{widget}")(label=name, **widget_config_copy)
|
||||
else:
|
||||
dpg.add_separator()
|
||||
|
||||
if not update:
|
||||
dpg.start_dearpygui()
|
||||
|
|
@ -8,17 +8,21 @@ __all__ = [
|
|||
]
|
||||
|
||||
|
||||
from typing import Any
|
||||
|
||||
from manim.mobject.geometry.polygram import Rectangle
|
||||
|
||||
from .. import config
|
||||
|
||||
|
||||
class ScreenRectangle(Rectangle):
|
||||
def __init__(self, aspect_ratio=16.0 / 9.0, height=4, **kwargs):
|
||||
def __init__(
|
||||
self, aspect_ratio: float = 16.0 / 9.0, height: float = 4, **kwargs: Any
|
||||
) -> None:
|
||||
super().__init__(width=aspect_ratio * height, height=height, **kwargs)
|
||||
|
||||
@property
|
||||
def aspect_ratio(self):
|
||||
def aspect_ratio(self) -> float:
|
||||
"""The aspect ratio.
|
||||
|
||||
When set, the width is stretched to accommodate
|
||||
|
|
@ -27,11 +31,11 @@ class ScreenRectangle(Rectangle):
|
|||
return self.width / self.height
|
||||
|
||||
@aspect_ratio.setter
|
||||
def aspect_ratio(self, value):
|
||||
def aspect_ratio(self, value: float) -> None:
|
||||
self.stretch_to_fit_width(value * self.height)
|
||||
|
||||
|
||||
class FullScreenRectangle(ScreenRectangle):
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.height = config["frame_height"]
|
||||
|
|
|
|||
|
|
@ -40,11 +40,12 @@ __all__ = [
|
|||
"CubicBezier",
|
||||
"ArcPolygon",
|
||||
"ArcPolygonFromArcs",
|
||||
"TangentialArc",
|
||||
]
|
||||
|
||||
import itertools
|
||||
import warnings
|
||||
from typing import TYPE_CHECKING, cast
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import numpy as np
|
||||
from typing_extensions import Self
|
||||
|
|
@ -55,6 +56,7 @@ from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
|||
from manim.utils.color import BLACK, BLUE, RED, WHITE, ParsableManimColor
|
||||
from manim.utils.iterables import adjacent_pairs
|
||||
from manim.utils.space_ops import (
|
||||
angle_between_vectors,
|
||||
angle_of_vector,
|
||||
cartesian_to_spherical,
|
||||
line_intersection,
|
||||
|
|
@ -64,9 +66,9 @@ 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.geometry.line import Line
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.text.tex_mobject import SingleStringMathTex, Tex
|
||||
from manim.mobject.text.text_mobject import Text
|
||||
|
|
@ -74,7 +76,7 @@ if TYPE_CHECKING:
|
|||
Point3D,
|
||||
Point3DLike,
|
||||
QuadraticSpline,
|
||||
Vector3D,
|
||||
Vector3DLike,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -99,12 +101,12 @@ class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
def __init__(
|
||||
self,
|
||||
tip_length: float = DEFAULT_ARROW_TIP_LENGTH,
|
||||
normal_vector: Vector3D = OUT,
|
||||
normal_vector: Vector3DLike = OUT,
|
||||
tip_style: dict = {},
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.tip_length: float = tip_length
|
||||
self.normal_vector: Vector3D = normal_vector
|
||||
self.normal_vector = normal_vector
|
||||
self.tip_style: dict = tip_style
|
||||
super().__init__(**kwargs)
|
||||
|
||||
|
|
@ -497,6 +499,71 @@ class ArcBetweenPoints(Arc):
|
|||
self.radius = np.inf
|
||||
|
||||
|
||||
class TangentialArc(ArcBetweenPoints):
|
||||
"""
|
||||
Construct an arc that is tangent to two intersecting lines.
|
||||
You can choose any of the 4 possible corner arcs via the `corner` tuple.
|
||||
corner = (s1, s2) where each si is ±1 to control direction along each line.
|
||||
|
||||
Example
|
||||
-------
|
||||
.. manim:: TangentialArcExample
|
||||
|
||||
class TangentialArcExample(Scene):
|
||||
def construct(self):
|
||||
line1 = DashedLine(start=3 * LEFT, end=3 * RIGHT)
|
||||
line1.rotate(angle=31 * DEGREES, about_point=ORIGIN)
|
||||
line2 = DashedLine(start=3 * UP, end=3 * DOWN)
|
||||
line2.rotate(angle=12 * DEGREES, about_point=ORIGIN)
|
||||
|
||||
arc = TangentialArc(line1, line2, radius=2.25, corner=(1, 1), color=TEAL)
|
||||
self.add(arc, line1, line2)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
line1: Line,
|
||||
line2: Line,
|
||||
radius: float,
|
||||
corner: Any = (1, 1),
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.line1 = line1
|
||||
self.line2 = line2
|
||||
|
||||
intersection_point = line_intersection(
|
||||
[line1.get_start(), line1.get_end()], [line2.get_start(), line2.get_end()]
|
||||
)
|
||||
|
||||
s1, s2 = corner
|
||||
# Get unit vector for specified directions
|
||||
unit_vector1 = s1 * line1.get_unit_vector()
|
||||
unit_vector2 = s2 * line2.get_unit_vector()
|
||||
|
||||
corner_angle = angle_between_vectors(unit_vector1, unit_vector2)
|
||||
tangent_point_distance = radius / np.tan(corner_angle / 2)
|
||||
|
||||
# tangent points
|
||||
tangent_point1 = intersection_point + tangent_point_distance * unit_vector1
|
||||
tangent_point2 = intersection_point + tangent_point_distance * unit_vector2
|
||||
|
||||
cross_product = (
|
||||
unit_vector1[0] * unit_vector2[1] - unit_vector1[1] * unit_vector2[0]
|
||||
)
|
||||
|
||||
# Determine start and end points based on orientation
|
||||
if cross_product < 0:
|
||||
# Counterclockwise orientation - standard order
|
||||
start_point = tangent_point1
|
||||
end_point = tangent_point2
|
||||
else:
|
||||
# Clockwise orientation - reverse the points
|
||||
start_point = tangent_point2
|
||||
end_point = tangent_point1
|
||||
|
||||
super().__init__(start=start_point, end=end_point, radius=radius, **kwargs)
|
||||
|
||||
|
||||
class CurvedArrow(ArcBetweenPoints):
|
||||
def __init__(
|
||||
self, start_point: Point3DLike, end_point: Point3DLike, **kwargs: Any
|
||||
|
|
@ -916,7 +983,8 @@ class AnnularSector(Arc):
|
|||
self.append_points(outer_arc.points)
|
||||
self.add_line_to(inner_arc.points[0])
|
||||
|
||||
init_points = generate_points
|
||||
def init_points(self) -> None:
|
||||
self.generate_points()
|
||||
|
||||
|
||||
class Sector(AnnularSector):
|
||||
|
|
@ -990,7 +1058,8 @@ class Annulus(Circle):
|
|||
self.append_points(inner_circle.points)
|
||||
self.shift(self.arc_center)
|
||||
|
||||
init_points = generate_points
|
||||
def init_points(self) -> None:
|
||||
self.generate_points()
|
||||
|
||||
|
||||
class CubicBezier(VMobject, metaclass=ConvertToOpenGL):
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
from pathops import Path as SkiaPath
|
||||
|
|
@ -13,8 +13,6 @@ from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
|||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from manim.typing import Point2DLike_Array, Point3D_Array, Point3DLike_Array
|
||||
|
||||
from ...constants import RendererType
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["Label", "LabeledLine", "LabeledArrow", "LabeledPolygram"]
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -22,8 +22,6 @@ from manim.utils.color import WHITE
|
|||
from manim.utils.polylabel import polylabel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from manim.typing import Point3DLike_Array
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ __all__ = [
|
|||
"RightAngle",
|
||||
]
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -30,11 +30,9 @@ 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, TypeAlias
|
||||
|
||||
from typing_extensions import Literal, Self, TypeAlias
|
||||
|
||||
from manim.typing import Point2DLike, Point3D, Point3DLike, Vector3D
|
||||
from manim.typing import Point3D, Point3DLike, Vector2DLike, Vector3D, Vector3DLike
|
||||
from manim.utils.color import ParsableManimColor
|
||||
|
||||
from ..matrix import Matrix # Avoid circular import
|
||||
|
|
@ -147,7 +145,8 @@ class Line(TipableVMobject):
|
|||
|
||||
self._account_for_buff(buff)
|
||||
|
||||
init_points = generate_points
|
||||
def init_points(self) -> None:
|
||||
self.generate_points()
|
||||
|
||||
def _account_for_buff(self, buff: float) -> None:
|
||||
if buff <= 0:
|
||||
|
|
@ -175,7 +174,7 @@ class Line(TipableVMobject):
|
|||
def _pointify(
|
||||
self,
|
||||
mob_or_point: Mobject | Point3DLike,
|
||||
direction: Vector3D | None = None,
|
||||
direction: Vector3DLike | None = None,
|
||||
) -> Point3D:
|
||||
"""Transforms a mobject into its corresponding point. Does nothing if a point is passed.
|
||||
|
||||
|
|
@ -738,7 +737,7 @@ class Vector(Arrow):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
direction: Point2DLike | Point3DLike = RIGHT,
|
||||
direction: Vector2DLike | Vector3DLike = RIGHT,
|
||||
buff: float = 0,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ __all__ = [
|
|||
|
||||
|
||||
from math import ceil
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -32,8 +32,6 @@ 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
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class SurroundingRectangle(RoundedRectangle):
|
|||
self,
|
||||
*mobjects: Mobject,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
buff: float = SMALL_BUFF,
|
||||
buff: float | tuple[float, float] = SMALL_BUFF,
|
||||
corner_radius: float = 0.0,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
|
|
@ -64,11 +64,17 @@ class SurroundingRectangle(RoundedRectangle):
|
|||
"Expected all inputs for parameter mobjects to be a Mobjects"
|
||||
)
|
||||
|
||||
if isinstance(buff, tuple):
|
||||
buff_x = buff[0]
|
||||
buff_y = buff[1]
|
||||
else:
|
||||
buff_x = buff_y = buff
|
||||
|
||||
group = Group(*mobjects)
|
||||
super().__init__(
|
||||
color=color,
|
||||
width=group.width + 2 * buff,
|
||||
height=group.height + 2 * buff,
|
||||
width=group.width + 2 * buff_x,
|
||||
height=group.height + 2 * buff_y,
|
||||
corner_radius=corner_radius,
|
||||
**kwargs,
|
||||
)
|
||||
|
|
@ -108,7 +114,7 @@ class BackgroundRectangle(SurroundingRectangle):
|
|||
stroke_width: float = 0,
|
||||
stroke_opacity: float = 0,
|
||||
fill_opacity: float = 0.75,
|
||||
buff: float = 0,
|
||||
buff: float | tuple[float, float] = 0,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
if color is None:
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ __all__ = [
|
|||
"StealthTip",
|
||||
]
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -25,8 +25,6 @@ from manim.mobject.types.vectorized_mobject import VMobject
|
|||
from manim.utils.space_ops import angle_of_vector
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from manim.typing import Point3D, Vector3D
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ __all__ = [
|
|||
|
||||
import fractions as fr
|
||||
import numbers
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Any, Callable, TypeVar, overload
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Any, TypeVar, overload
|
||||
|
||||
import numpy as np
|
||||
from typing_extensions import Self
|
||||
|
|
@ -64,6 +64,7 @@ if TYPE_CHECKING:
|
|||
Point3D,
|
||||
Point3DLike,
|
||||
Vector3D,
|
||||
Vector3DLike,
|
||||
)
|
||||
|
||||
LineType = TypeVar("LineType", bound=Line)
|
||||
|
|
@ -126,7 +127,7 @@ class CoordinateSystem:
|
|||
x_length: float | None = None,
|
||||
y_length: float | None = None,
|
||||
dimension: int = 2,
|
||||
) -> None:
|
||||
):
|
||||
self.dimension = dimension
|
||||
|
||||
default_step = 1
|
||||
|
|
@ -153,11 +154,14 @@ class CoordinateSystem:
|
|||
self.x_length = x_length
|
||||
self.y_length = y_length
|
||||
self.num_sampled_graph_points_per_tick = 10
|
||||
self.x_axis: NumberLine
|
||||
|
||||
def coords_to_point(self, *coords: ManimFloat):
|
||||
def coords_to_point(self, *coords: ManimFloat) -> Point3D:
|
||||
# TODO: I think the method should be able to return more than just a single point.
|
||||
# E.g. see the implementation of it on line 2065.
|
||||
raise NotImplementedError()
|
||||
|
||||
def point_to_coords(self, point: Point3DLike):
|
||||
def point_to_coords(self, point: Point3DLike) -> list[ManimFloat]:
|
||||
raise NotImplementedError()
|
||||
|
||||
def polar_to_point(self, radius: float, azimuth: float) -> Point2D:
|
||||
|
|
@ -201,7 +205,7 @@ class CoordinateSystem:
|
|||
|
||||
Returns
|
||||
-------
|
||||
Tuple[:class:`float`, :class:`float`]
|
||||
Point2D
|
||||
The coordinate radius (:math:`r`) and the coordinate azimuth (:math:`\theta`).
|
||||
"""
|
||||
x, y = self.point_to_coords(point)
|
||||
|
|
@ -213,7 +217,7 @@ class CoordinateSystem:
|
|||
"""Abbreviation for :meth:`coords_to_point`"""
|
||||
return self.coords_to_point(*coords)
|
||||
|
||||
def p2c(self, point: Point3DLike):
|
||||
def p2c(self, point: Point3DLike) -> list[ManimFloat]:
|
||||
"""Abbreviation for :meth:`point_to_coords`"""
|
||||
return self.point_to_coords(point)
|
||||
|
||||
|
|
@ -221,17 +225,18 @@ class CoordinateSystem:
|
|||
"""Abbreviation for :meth:`polar_to_point`"""
|
||||
return self.polar_to_point(radius, azimuth)
|
||||
|
||||
def pt2pr(self, point: np.ndarray) -> tuple[float, float]:
|
||||
def pt2pr(self, point: np.ndarray) -> Point2D:
|
||||
"""Abbreviation for :meth:`point_to_polar`"""
|
||||
return self.point_to_polar(point)
|
||||
|
||||
def get_axes(self):
|
||||
def get_axes(self) -> VGroup:
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_axis(self, index: int) -> Mobject:
|
||||
return self.get_axes()[index]
|
||||
def get_axis(self, index: int) -> NumberLine:
|
||||
val: NumberLine = self.get_axes()[index]
|
||||
return val
|
||||
|
||||
def get_origin(self) -> np.ndarray:
|
||||
def get_origin(self) -> Point3D:
|
||||
"""Gets the origin of :class:`~.Axes`.
|
||||
|
||||
Returns
|
||||
|
|
@ -241,13 +246,13 @@ class CoordinateSystem:
|
|||
"""
|
||||
return self.coords_to_point(0, 0)
|
||||
|
||||
def get_x_axis(self) -> Mobject:
|
||||
def get_x_axis(self) -> NumberLine:
|
||||
return self.get_axis(0)
|
||||
|
||||
def get_y_axis(self) -> Mobject:
|
||||
def get_y_axis(self) -> NumberLine:
|
||||
return self.get_axis(1)
|
||||
|
||||
def get_z_axis(self) -> Mobject:
|
||||
def get_z_axis(self) -> NumberLine:
|
||||
return self.get_axis(2)
|
||||
|
||||
def get_x_unit_size(self) -> float:
|
||||
|
|
@ -258,11 +263,11 @@ class CoordinateSystem:
|
|||
|
||||
def get_x_axis_label(
|
||||
self,
|
||||
label: float | str | Mobject,
|
||||
edge: Sequence[float] = UR,
|
||||
direction: Sequence[float] = UR,
|
||||
label: float | str | VMobject,
|
||||
edge: Vector3D = UR,
|
||||
direction: Vector3D = UR,
|
||||
buff: float = SMALL_BUFF,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> Mobject:
|
||||
"""Generate an x-axis label.
|
||||
|
||||
|
|
@ -301,11 +306,11 @@ class CoordinateSystem:
|
|||
|
||||
def get_y_axis_label(
|
||||
self,
|
||||
label: float | str | Mobject,
|
||||
edge: Sequence[float] = UR,
|
||||
direction: Sequence[float] = UP * 0.5 + RIGHT,
|
||||
label: float | str | VMobject,
|
||||
edge: Vector3D = UR,
|
||||
direction: Vector3D = UP * 0.5 + RIGHT,
|
||||
buff: float = SMALL_BUFF,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> Mobject:
|
||||
"""Generate a y-axis label.
|
||||
|
||||
|
|
@ -347,10 +352,10 @@ class CoordinateSystem:
|
|||
|
||||
def _get_axis_label(
|
||||
self,
|
||||
label: float | str | Mobject,
|
||||
label: float | str | VMobject,
|
||||
axis: Mobject,
|
||||
edge: Sequence[float],
|
||||
direction: Sequence[float],
|
||||
edge: Vector3DLike,
|
||||
direction: Vector3DLike,
|
||||
buff: float = SMALL_BUFF,
|
||||
) -> Mobject:
|
||||
"""Gets the label for an axis.
|
||||
|
|
@ -373,12 +378,14 @@ class CoordinateSystem:
|
|||
:class:`~.Mobject`
|
||||
The positioned label along the given axis.
|
||||
"""
|
||||
label = self.x_axis._create_label_tex(label)
|
||||
label.next_to(axis.get_edge_center(edge), direction=direction, buff=buff)
|
||||
label.shift_onto_screen(buff=MED_SMALL_BUFF)
|
||||
return label
|
||||
label_mobject: Mobject = self.x_axis._create_label_tex(label)
|
||||
label_mobject.next_to(
|
||||
axis.get_edge_center(edge), direction=direction, buff=buff
|
||||
)
|
||||
label_mobject.shift_onto_screen(buff=MED_SMALL_BUFF)
|
||||
return label_mobject
|
||||
|
||||
def get_axis_labels(self):
|
||||
def get_axis_labels(self) -> VGroup:
|
||||
raise NotImplementedError()
|
||||
|
||||
def add_coordinates(
|
||||
|
|
@ -453,7 +460,7 @@ class CoordinateSystem:
|
|||
def get_line_from_axis_to_point(
|
||||
self,
|
||||
index: int,
|
||||
point: Sequence[float],
|
||||
point: Point3DLike,
|
||||
line_config: dict | None = ...,
|
||||
color: ParsableManimColor | None = ...,
|
||||
stroke_width: float = ...,
|
||||
|
|
@ -463,7 +470,7 @@ class CoordinateSystem:
|
|||
def get_line_from_axis_to_point(
|
||||
self,
|
||||
index: int,
|
||||
point: Sequence[float],
|
||||
point: Point3DLike,
|
||||
line_func: type[LineType],
|
||||
line_config: dict | None = ...,
|
||||
color: ParsableManimColor | None = ...,
|
||||
|
|
@ -518,7 +525,7 @@ class CoordinateSystem:
|
|||
line = line_func(axis.get_projection(point), point, **line_config)
|
||||
return line
|
||||
|
||||
def get_vertical_line(self, point: Sequence[float], **kwargs: Any) -> Line:
|
||||
def get_vertical_line(self, point: Point3DLike, **kwargs: Any) -> Line:
|
||||
"""A vertical line from the x-axis to a given point in the scene.
|
||||
|
||||
Parameters
|
||||
|
|
@ -552,7 +559,7 @@ class CoordinateSystem:
|
|||
"""
|
||||
return self.get_line_from_axis_to_point(0, point, **kwargs)
|
||||
|
||||
def get_horizontal_line(self, point: Sequence[float], **kwargs) -> Line:
|
||||
def get_horizontal_line(self, point: Point3DLike, **kwargs: Any) -> Line:
|
||||
"""A horizontal line from the y-axis to a given point in the scene.
|
||||
|
||||
Parameters
|
||||
|
|
@ -584,7 +591,7 @@ class CoordinateSystem:
|
|||
"""
|
||||
return self.get_line_from_axis_to_point(1, point, **kwargs)
|
||||
|
||||
def get_lines_to_point(self, point: Sequence[float], **kwargs) -> VGroup:
|
||||
def get_lines_to_point(self, point: Point3DLike, **kwargs: Any) -> VGroup:
|
||||
"""Generate both horizontal and vertical lines from the axis to a point.
|
||||
|
||||
Parameters
|
||||
|
|
@ -630,7 +637,9 @@ class CoordinateSystem:
|
|||
function: Callable[[float], float],
|
||||
x_range: Sequence[float] | None = None,
|
||||
use_vectorized: bool = False,
|
||||
colorscale: Union[Iterable[Color], Iterable[Color, float]] | None = None,
|
||||
colorscale: Iterable[ParsableManimColor]
|
||||
| Iterable[ParsableManimColor, float]
|
||||
| None = None,
|
||||
colorscale_axis: int = 1,
|
||||
**kwargs: Any,
|
||||
) -> ParametricFunction:
|
||||
|
|
@ -1093,7 +1102,7 @@ class CoordinateSystem:
|
|||
def get_graph_label(
|
||||
self,
|
||||
graph: ParametricFunction,
|
||||
label: float | str | Mobject = "f(x)",
|
||||
label: float | str | VMobject = "f(x)",
|
||||
x_val: float | None = None,
|
||||
direction: Sequence[float] = RIGHT,
|
||||
buff: float = MED_SMALL_BUFF,
|
||||
|
|
@ -1150,7 +1159,7 @@ class CoordinateSystem:
|
|||
dot_config = {}
|
||||
if color is None:
|
||||
color = graph.get_color()
|
||||
label = self.x_axis._create_label_tex(label).set_color(color)
|
||||
label_object: Mobject = self.x_axis._create_label_tex(label).set_color(color)
|
||||
|
||||
if x_val is None:
|
||||
# Search from right to left
|
||||
|
|
@ -1161,14 +1170,14 @@ class CoordinateSystem:
|
|||
else:
|
||||
point = self.input_to_graph_point(x_val, graph)
|
||||
|
||||
label.next_to(point, direction, buff=buff)
|
||||
label.shift_onto_screen()
|
||||
label_object.next_to(point, direction, buff=buff)
|
||||
label_object.shift_onto_screen()
|
||||
|
||||
if dot:
|
||||
dot = Dot(point=point, **dot_config)
|
||||
label.add(dot)
|
||||
label.dot = dot
|
||||
return label
|
||||
label_object.add(dot)
|
||||
label_object.dot = dot
|
||||
return label_object
|
||||
|
||||
# calculus
|
||||
|
||||
|
|
@ -1176,14 +1185,14 @@ class CoordinateSystem:
|
|||
self,
|
||||
graph: ParametricFunction,
|
||||
x_range: Sequence[float] | None = None,
|
||||
dx: float | None = 0.1,
|
||||
dx: float = 0.1,
|
||||
input_sample_type: str = "left",
|
||||
stroke_width: float = 1,
|
||||
stroke_color: ParsableManimColor = BLACK,
|
||||
fill_opacity: float = 1,
|
||||
color: Iterable[ParsableManimColor] | ParsableManimColor = (BLUE, GREEN),
|
||||
show_signed_area: bool = True,
|
||||
bounded_graph: ParametricFunction = None,
|
||||
bounded_graph: ParametricFunction | None = None,
|
||||
blend: bool = False,
|
||||
width_scale_factor: float = 1.001,
|
||||
) -> VGroup:
|
||||
|
|
@ -1277,16 +1286,16 @@ class CoordinateSystem:
|
|||
x_range = [*x_range[:2], dx]
|
||||
|
||||
rectangles = VGroup()
|
||||
x_range = np.arange(*x_range)
|
||||
x_range_array = np.arange(*x_range)
|
||||
|
||||
if isinstance(color, (list, tuple)):
|
||||
color = [ManimColor(c) for c in color]
|
||||
else:
|
||||
color = [ManimColor(color)]
|
||||
|
||||
colors = color_gradient(color, len(x_range))
|
||||
colors = color_gradient(color, len(x_range_array))
|
||||
|
||||
for x, color in zip(x_range, colors):
|
||||
for x, color in zip(x_range_array, colors):
|
||||
if input_sample_type == "left":
|
||||
sample_input = x
|
||||
elif input_sample_type == "right":
|
||||
|
|
@ -1341,7 +1350,7 @@ class CoordinateSystem:
|
|||
x_range: tuple[float, float] | None = None,
|
||||
color: ParsableManimColor | Iterable[ParsableManimColor] = (BLUE, GREEN),
|
||||
opacity: float = 0.3,
|
||||
bounded_graph: ParametricFunction = None,
|
||||
bounded_graph: ParametricFunction | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Polygon:
|
||||
"""Returns a :class:`~.Polygon` representing the area under the graph passed.
|
||||
|
|
@ -1485,10 +1494,14 @@ class CoordinateSystem:
|
|||
ax.slope_of_tangent(x=-2, graph=curve)
|
||||
# -3.5000000259052038
|
||||
"""
|
||||
return np.tan(self.angle_of_tangent(x, graph, **kwargs))
|
||||
val: float = np.tan(self.angle_of_tangent(x, graph, **kwargs))
|
||||
return val
|
||||
|
||||
def plot_derivative_graph(
|
||||
self, graph: ParametricFunction, color: ParsableManimColor = GREEN, **kwargs
|
||||
self,
|
||||
graph: ParametricFunction,
|
||||
color: ParsableManimColor = GREEN,
|
||||
**kwargs: Any,
|
||||
) -> ParametricFunction:
|
||||
"""Returns the curve of the derivative of the passed graph.
|
||||
|
||||
|
|
@ -1526,7 +1539,7 @@ class CoordinateSystem:
|
|||
self.add(ax, curves, labels)
|
||||
"""
|
||||
|
||||
def deriv(x):
|
||||
def deriv(x: float) -> float:
|
||||
return self.slope_of_tangent(x, graph)
|
||||
|
||||
return self.plot(deriv, color=color, **kwargs)
|
||||
|
|
@ -1587,7 +1600,7 @@ class CoordinateSystem:
|
|||
x_vals = np.linspace(0, x, samples, axis=1 if use_vectorized else 0)
|
||||
f_vec = np.vectorize(graph.underlying_function)
|
||||
y_vals = f_vec(x_vals)
|
||||
return np.trapz(y_vals, x_vals) + y_intercept
|
||||
return np.trapezoid(y_vals, x_vals) + y_intercept
|
||||
|
||||
return self.plot(antideriv, use_vectorized=use_vectorized, **kwargs)
|
||||
|
||||
|
|
@ -1843,14 +1856,17 @@ class CoordinateSystem:
|
|||
|
||||
return T_label_group
|
||||
|
||||
def __matmul__(self, coord: Point3DLike | Mobject):
|
||||
def __matmul__(self, coord: Point3DLike | Mobject) -> Point3DLike:
|
||||
if isinstance(coord, Mobject):
|
||||
coord = coord.get_center()
|
||||
return self.coords_to_point(*coord)
|
||||
|
||||
def __rmatmul__(self, point: Point3DLike):
|
||||
def __rmatmul__(self, point: Point3DLike) -> Point3DLike:
|
||||
return self.point_to_coords(point)
|
||||
|
||||
@staticmethod
|
||||
def _origin_shift(axis_range: Sequence[float]) -> float: ...
|
||||
|
||||
|
||||
class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
|
||||
"""Creates a set of axes.
|
||||
|
|
@ -1918,7 +1934,7 @@ class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
|
|||
y_axis_config: dict | None = None,
|
||||
tips: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
):
|
||||
VGroup.__init__(self, **kwargs)
|
||||
CoordinateSystem.__init__(self, x_range, y_range, x_length, y_length)
|
||||
|
||||
|
|
@ -1926,8 +1942,11 @@ class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
|
|||
"include_tip": tips,
|
||||
"numbers_to_exclude": [0],
|
||||
}
|
||||
self.x_axis_config = {}
|
||||
self.y_axis_config = {"rotation": 90 * DEGREES, "label_direction": LEFT}
|
||||
self.x_axis_config: dict[str, Any] = {}
|
||||
self.y_axis_config: dict[str, Any] = {
|
||||
"rotation": 90 * DEGREES,
|
||||
"label_direction": LEFT,
|
||||
}
|
||||
|
||||
self._update_default_configs(
|
||||
(self.axis_config, self.x_axis_config, self.y_axis_config),
|
||||
|
|
@ -2414,14 +2433,14 @@ class ThreeDAxes(Axes):
|
|||
y_length: float | None = config.frame_height + 2.5,
|
||||
z_length: float | None = config.frame_height - 1.5,
|
||||
z_axis_config: dict[str, Any] | None = None,
|
||||
z_normal: Vector3D = DOWN,
|
||||
z_normal: Vector3DLike = DOWN,
|
||||
num_axis_pieces: int = 20,
|
||||
light_source: Sequence[float] = 9 * DOWN + 7 * LEFT + 10 * OUT,
|
||||
light_source: Point3DLike = 9 * DOWN + 7 * LEFT + 10 * OUT,
|
||||
# opengl stuff (?)
|
||||
depth=None,
|
||||
gloss=0.5,
|
||||
depth: Any = None,
|
||||
gloss: float = 0.5,
|
||||
**kwargs: dict[str, Any],
|
||||
) -> None:
|
||||
):
|
||||
super().__init__(
|
||||
x_range=x_range,
|
||||
x_length=x_length,
|
||||
|
|
@ -2433,7 +2452,7 @@ class ThreeDAxes(Axes):
|
|||
self.z_range = z_range
|
||||
self.z_length = z_length
|
||||
|
||||
self.z_axis_config = {}
|
||||
self.z_axis_config: dict[str, Any] = {}
|
||||
self._update_default_configs((self.z_axis_config,), (z_axis_config,))
|
||||
self.z_axis_config = merge_dicts_recursively(
|
||||
self.axis_config,
|
||||
|
|
@ -2443,7 +2462,7 @@ class ThreeDAxes(Axes):
|
|||
self.z_normal = z_normal
|
||||
self.num_axis_pieces = num_axis_pieces
|
||||
|
||||
self.light_source = light_source
|
||||
self.light_source = np.array(light_source)
|
||||
|
||||
self.dimension = 3
|
||||
|
||||
|
|
@ -2500,13 +2519,13 @@ class ThreeDAxes(Axes):
|
|||
|
||||
def get_y_axis_label(
|
||||
self,
|
||||
label: float | str | Mobject,
|
||||
edge: Sequence[float] = UR,
|
||||
direction: Sequence[float] = UR,
|
||||
label: float | str | VMobject,
|
||||
edge: Vector3DLike = UR,
|
||||
direction: Vector3DLike = UR,
|
||||
buff: float = SMALL_BUFF,
|
||||
rotation: float = PI / 2,
|
||||
rotation_axis: Vector3D = OUT,
|
||||
**kwargs,
|
||||
rotation_axis: Vector3DLike = OUT,
|
||||
**kwargs: dict[str, Any],
|
||||
) -> Mobject:
|
||||
"""Generate a y-axis label.
|
||||
|
||||
|
|
@ -2550,12 +2569,12 @@ class ThreeDAxes(Axes):
|
|||
|
||||
def get_z_axis_label(
|
||||
self,
|
||||
label: float | str | Mobject,
|
||||
edge: Vector3D = OUT,
|
||||
direction: Vector3D = RIGHT,
|
||||
label: float | str | VMobject,
|
||||
edge: Vector3DLike = OUT,
|
||||
direction: Vector3DLike = RIGHT,
|
||||
buff: float = SMALL_BUFF,
|
||||
rotation: float = PI / 2,
|
||||
rotation_axis: Vector3D = RIGHT,
|
||||
rotation_axis: Vector3DLike = RIGHT,
|
||||
**kwargs: Any,
|
||||
) -> Mobject:
|
||||
"""Generate a z-axis label.
|
||||
|
|
@ -2600,9 +2619,9 @@ class ThreeDAxes(Axes):
|
|||
|
||||
def get_axis_labels(
|
||||
self,
|
||||
x_label: float | str | Mobject = "x",
|
||||
y_label: float | str | Mobject = "y",
|
||||
z_label: float | str | Mobject = "z",
|
||||
x_label: float | str | VMobject = "x",
|
||||
y_label: float | str | VMobject = "y",
|
||||
z_label: float | str | VMobject = "z",
|
||||
) -> VGroup:
|
||||
"""Defines labels for the x_axis and y_axis of the graph.
|
||||
|
||||
|
|
@ -2741,7 +2760,7 @@ class NumberPlane(Axes):
|
|||
**kwargs: dict[str, Any],
|
||||
):
|
||||
# configs
|
||||
self.axis_config = {
|
||||
self.axis_config: dict[str, Any] = {
|
||||
"stroke_width": 2,
|
||||
"include_ticks": False,
|
||||
"include_tip": False,
|
||||
|
|
@ -2749,8 +2768,8 @@ class NumberPlane(Axes):
|
|||
"label_direction": DR,
|
||||
"font_size": 24,
|
||||
}
|
||||
self.y_axis_config = {"label_direction": DR}
|
||||
self.background_line_style = {
|
||||
self.y_axis_config: dict[str, Any] = {"label_direction": DR}
|
||||
self.background_line_style: dict[str, Any] = {
|
||||
"stroke_color": BLUE_D,
|
||||
"stroke_width": 2,
|
||||
"stroke_opacity": 1,
|
||||
|
|
@ -2997,7 +3016,7 @@ class PolarPlane(Axes):
|
|||
size: float | None = None,
|
||||
radius_step: float = 1,
|
||||
azimuth_step: float | None = None,
|
||||
azimuth_units: str | None = "PI radians",
|
||||
azimuth_units: str = "PI radians",
|
||||
azimuth_compact_fraction: bool = True,
|
||||
azimuth_offset: float = 0,
|
||||
azimuth_direction: str = "CCW",
|
||||
|
|
@ -3009,7 +3028,7 @@ class PolarPlane(Axes):
|
|||
faded_line_ratio: int = 1,
|
||||
make_smooth_after_applying_functions: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
):
|
||||
# error catching
|
||||
if azimuth_units in ["PI radians", "TAU radians", "degrees", "gradians", None]:
|
||||
self.azimuth_units = azimuth_units
|
||||
|
|
@ -3130,11 +3149,11 @@ class PolarPlane(Axes):
|
|||
unit_vector = self.x_axis.get_unit_vector()[0]
|
||||
|
||||
for k, x in enumerate(rinput):
|
||||
new_line = Circle(radius=x * unit_vector)
|
||||
new_circle = Circle(radius=x * unit_vector)
|
||||
if k % ratio_faded_lines == 0:
|
||||
alines1.add(new_line)
|
||||
alines1.add(new_circle)
|
||||
else:
|
||||
alines2.add(new_line)
|
||||
alines2.add(new_circle)
|
||||
|
||||
line = Line(center, self.get_x_axis().get_end())
|
||||
|
||||
|
|
@ -3292,7 +3311,9 @@ class PolarPlane(Axes):
|
|||
self.add(self.get_coordinate_labels(r_values, a_values))
|
||||
return self
|
||||
|
||||
def get_radian_label(self, number, font_size: float = 24, **kwargs: Any) -> MathTex:
|
||||
def get_radian_label(
|
||||
self, number: float, font_size: float = 24, **kwargs: Any
|
||||
) -> MathTex:
|
||||
constant_label = {"PI radians": r"\pi", "TAU radians": r"\tau"}[
|
||||
self.azimuth_units
|
||||
]
|
||||
|
|
@ -3361,7 +3382,7 @@ class ComplexPlane(NumberPlane):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
def __init__(self, **kwargs: Any):
|
||||
super().__init__(
|
||||
**kwargs,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ from __future__ import annotations
|
|||
__all__ = ["ParametricFunction", "FunctionGraph", "ImplicitFunction"]
|
||||
|
||||
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
from isosurfaces import plot_isoline
|
||||
|
|
@ -17,9 +17,12 @@ from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
|||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim.typing import Point3D, Point3DLike
|
||||
from manim.utils.color import ParsableManimColor
|
||||
|
||||
from manim.utils.color import YELLOW
|
||||
|
||||
|
|
@ -111,7 +114,7 @@ class ParametricFunction(VMobject, metaclass=ConvertToOpenGL):
|
|||
discontinuities: Iterable[float] | None = None,
|
||||
use_smoothing: bool = True,
|
||||
use_vectorized: bool = False,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
def internal_parametric_function(t: float) -> Point3D:
|
||||
"""Wrap ``function``'s output inside a NumPy array."""
|
||||
|
|
@ -143,13 +146,13 @@ class ParametricFunction(VMobject, metaclass=ConvertToOpenGL):
|
|||
lambda t: self.t_min <= t <= self.t_max,
|
||||
self.discontinuities,
|
||||
)
|
||||
discontinuities = np.array(list(discontinuities))
|
||||
discontinuities_array = np.array(list(discontinuities))
|
||||
boundary_times = np.array(
|
||||
[
|
||||
self.t_min,
|
||||
self.t_max,
|
||||
*(discontinuities - self.dt),
|
||||
*(discontinuities + self.dt),
|
||||
*(discontinuities_array - self.dt),
|
||||
*(discontinuities_array + self.dt),
|
||||
],
|
||||
)
|
||||
boundary_times.sort()
|
||||
|
|
@ -179,7 +182,8 @@ class ParametricFunction(VMobject, metaclass=ConvertToOpenGL):
|
|||
self.make_smooth()
|
||||
return self
|
||||
|
||||
init_points = generate_points
|
||||
def init_points(self) -> None:
|
||||
self.generate_points()
|
||||
|
||||
|
||||
class FunctionGraph(ParametricFunction):
|
||||
|
|
@ -211,19 +215,27 @@ class FunctionGraph(ParametricFunction):
|
|||
self.add(cos_func, sin_func_1, sin_func_2)
|
||||
"""
|
||||
|
||||
def __init__(self, function, x_range=None, color=YELLOW, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
function: Callable[[float], Any],
|
||||
x_range: tuple[float, float] | tuple[float, float, float] | None = None,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
if x_range is None:
|
||||
x_range = np.array([-config["frame_x_radius"], config["frame_x_radius"]])
|
||||
x_range = (-config["frame_x_radius"], config["frame_x_radius"])
|
||||
|
||||
self.x_range = x_range
|
||||
self.parametric_function = lambda t: np.array([t, function(t), 0])
|
||||
self.function = function
|
||||
self.parametric_function: Callable[[float], Point3D] = lambda t: np.array(
|
||||
[t, function(t), 0]
|
||||
)
|
||||
self.function = function # type: ignore[assignment]
|
||||
super().__init__(self.parametric_function, self.x_range, color=color, **kwargs)
|
||||
|
||||
def get_function(self):
|
||||
def get_function(self) -> Callable[[float], Any]:
|
||||
return self.function
|
||||
|
||||
def get_point_from_function(self, x):
|
||||
def get_point_from_function(self, x: float) -> Point3D:
|
||||
return self.parametric_function(x)
|
||||
|
||||
|
||||
|
|
@ -236,7 +248,7 @@ class ImplicitFunction(VMobject, metaclass=ConvertToOpenGL):
|
|||
min_depth: int = 5,
|
||||
max_quads: int = 1500,
|
||||
use_smoothing: bool = True,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""An implicit function.
|
||||
|
||||
|
|
@ -295,7 +307,7 @@ class ImplicitFunction(VMobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def generate_points(self):
|
||||
def generate_points(self) -> Self:
|
||||
p_min, p_max = (
|
||||
np.array([self.x_range[0], self.y_range[0]]),
|
||||
np.array([self.x_range[1], self.y_range[1]]),
|
||||
|
|
@ -317,4 +329,5 @@ class ImplicitFunction(VMobject, metaclass=ConvertToOpenGL):
|
|||
self.make_smooth()
|
||||
return self
|
||||
|
||||
init_points = generate_points
|
||||
def init_points(self) -> None:
|
||||
self.generate_points()
|
||||
|
|
|
|||
|
|
@ -8,12 +8,16 @@ from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
|
|||
__all__ = ["NumberLine", "UnitInterval"]
|
||||
|
||||
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim.mobject.geometry.tips import ArrowTip
|
||||
from manim.typing import Point3DLike
|
||||
from manim.typing import Point3D, Point3DLike, Vector3D
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -21,8 +25,9 @@ from manim import config
|
|||
from manim.constants import *
|
||||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.graphing.scale import LinearBase, _ScaleBase
|
||||
from manim.mobject.text.numbers import DecimalNumber
|
||||
from manim.mobject.text.numbers import DecimalNumber, Integer
|
||||
from manim.mobject.text.tex_mobject import MathTex, Tex
|
||||
from manim.mobject.text.text_mobject import Text
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.utils.bezier import interpolate
|
||||
from manim.utils.config_ops import merge_dicts_recursively
|
||||
|
|
@ -157,14 +162,14 @@ class NumberLine(Line):
|
|||
# numbers/labels
|
||||
include_numbers: bool = False,
|
||||
font_size: float = 36,
|
||||
label_direction: Sequence[float] = DOWN,
|
||||
label_constructor: VMobject = MathTex,
|
||||
label_direction: Point3DLike = DOWN,
|
||||
label_constructor: type[MathTex] = MathTex,
|
||||
scaling: _ScaleBase = LinearBase(),
|
||||
line_to_number_buff: float = MED_SMALL_BUFF,
|
||||
decimal_number_config: dict | None = None,
|
||||
numbers_to_exclude: Iterable[float] | None = None,
|
||||
numbers_to_include: Iterable[float] | None = None,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
# avoid mutable arguments in defaults
|
||||
if numbers_to_exclude is None:
|
||||
|
|
@ -189,6 +194,9 @@ class NumberLine(Line):
|
|||
|
||||
# turn into a NumPy array to scale by just applying the function
|
||||
self.x_range = np.array(x_range, dtype=float)
|
||||
self.x_min: float
|
||||
self.x_max: float
|
||||
self.x_step: float
|
||||
self.x_min, self.x_max, self.x_step = scaling.function(self.x_range)
|
||||
self.length = length
|
||||
self.unit_size = unit_size
|
||||
|
|
@ -246,16 +254,16 @@ class NumberLine(Line):
|
|||
if self.scaling.custom_labels:
|
||||
tick_range = self.get_tick_range()
|
||||
|
||||
custom_labels = self.scaling.get_custom_labels(
|
||||
tick_range,
|
||||
unit_decimal_places=decimal_number_config["num_decimal_places"],
|
||||
)
|
||||
|
||||
self.add_labels(
|
||||
dict(
|
||||
zip(
|
||||
tick_range,
|
||||
self.scaling.get_custom_labels(
|
||||
tick_range,
|
||||
unit_decimal_places=decimal_number_config[
|
||||
"num_decimal_places"
|
||||
],
|
||||
),
|
||||
custom_labels,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
|
@ -267,21 +275,25 @@ class NumberLine(Line):
|
|||
font_size=self.font_size,
|
||||
)
|
||||
|
||||
def rotate_about_zero(self, angle: float, axis: Sequence[float] = OUT, **kwargs):
|
||||
def rotate_about_zero(
|
||||
self, angle: float, axis: Vector3D = OUT, **kwargs: Any
|
||||
) -> Self:
|
||||
return self.rotate_about_number(0, angle, axis, **kwargs)
|
||||
|
||||
def rotate_about_number(
|
||||
self, number: float, angle: float, axis: Sequence[float] = OUT, **kwargs
|
||||
):
|
||||
self, number: float, angle: float, axis: Vector3D = OUT, **kwargs: Any
|
||||
) -> Self:
|
||||
return self.rotate(angle, axis, about_point=self.n2p(number), **kwargs)
|
||||
|
||||
def add_ticks(self):
|
||||
def add_ticks(self) -> None:
|
||||
"""Adds ticks to the number line. Ticks can be accessed after creation
|
||||
via ``self.ticks``.
|
||||
"""
|
||||
ticks = VGroup()
|
||||
elongated_tick_size = self.tick_size * self.longer_tick_multiple
|
||||
elongated_tick_offsets = self.numbers_with_elongated_ticks - self.x_min
|
||||
elongated_tick_offsets = (
|
||||
np.array(self.numbers_with_elongated_ticks) - self.x_min
|
||||
)
|
||||
for x in self.get_tick_range():
|
||||
size = self.tick_size
|
||||
if np.any(np.isclose(x - self.x_min, elongated_tick_offsets)):
|
||||
|
|
@ -413,31 +425,34 @@ class NumberLine(Line):
|
|||
point = np.asarray(point)
|
||||
start, end = self.get_start_and_end()
|
||||
unit_vect = normalize(end - start)
|
||||
proportion = np.dot(point - start, unit_vect) / np.dot(end - start, unit_vect)
|
||||
proportion: float = np.dot(point - start, unit_vect) / np.dot(
|
||||
end - start, unit_vect
|
||||
)
|
||||
return interpolate(self.x_min, self.x_max, proportion)
|
||||
|
||||
def n2p(self, number: float | np.ndarray) -> np.ndarray:
|
||||
def n2p(self, number: float | np.ndarray) -> Point3D:
|
||||
"""Abbreviation for :meth:`~.NumberLine.number_to_point`."""
|
||||
return self.number_to_point(number)
|
||||
|
||||
def p2n(self, point: Sequence[float]) -> float:
|
||||
def p2n(self, point: Point3DLike) -> float:
|
||||
"""Abbreviation for :meth:`~.NumberLine.point_to_number`."""
|
||||
return self.point_to_number(point)
|
||||
|
||||
def get_unit_size(self) -> float:
|
||||
return self.get_length() / (self.x_range[1] - self.x_range[0])
|
||||
val: float = self.get_length() / (self.x_range[1] - self.x_range[0])
|
||||
return val
|
||||
|
||||
def get_unit_vector(self) -> np.ndarray:
|
||||
def get_unit_vector(self) -> Vector3D:
|
||||
return super().get_unit_vector() * self.unit_size
|
||||
|
||||
def get_number_mobject(
|
||||
self,
|
||||
x: float,
|
||||
direction: Sequence[float] | None = None,
|
||||
direction: Vector3D | None = None,
|
||||
buff: float | None = None,
|
||||
font_size: float | None = None,
|
||||
label_constructor: VMobject | None = None,
|
||||
**number_config,
|
||||
label_constructor: type[MathTex] | None = None,
|
||||
**number_config: dict[str, Any],
|
||||
) -> VMobject:
|
||||
"""Generates a positioned :class:`~.DecimalNumber` mobject
|
||||
generated according to ``label_constructor``.
|
||||
|
|
@ -462,7 +477,7 @@ class NumberLine(Line):
|
|||
:class:`~.DecimalNumber`
|
||||
The positioned mobject.
|
||||
"""
|
||||
number_config = merge_dicts_recursively(
|
||||
number_config_merged = merge_dicts_recursively(
|
||||
self.decimal_number_config,
|
||||
number_config,
|
||||
)
|
||||
|
|
@ -476,7 +491,10 @@ class NumberLine(Line):
|
|||
label_constructor = self.label_constructor
|
||||
|
||||
num_mob = DecimalNumber(
|
||||
x, font_size=font_size, mob_class=label_constructor, **number_config
|
||||
x,
|
||||
font_size=font_size,
|
||||
mob_class=label_constructor,
|
||||
**number_config_merged,
|
||||
)
|
||||
|
||||
num_mob.next_to(self.number_to_point(x), direction=direction, buff=buff)
|
||||
|
|
@ -485,7 +503,7 @@ class NumberLine(Line):
|
|||
num_mob.shift(num_mob[0].width * LEFT / 2)
|
||||
return num_mob
|
||||
|
||||
def get_number_mobjects(self, *numbers, **kwargs) -> VGroup:
|
||||
def get_number_mobjects(self, *numbers: float, **kwargs: Any) -> VGroup:
|
||||
if len(numbers) == 0:
|
||||
numbers = self.default_numbers_to_display()
|
||||
return VGroup([self.get_number_mobject(number, **kwargs) for number in numbers])
|
||||
|
|
@ -498,9 +516,9 @@ class NumberLine(Line):
|
|||
x_values: Iterable[float] | None = None,
|
||||
excluding: Iterable[float] | None = None,
|
||||
font_size: float | None = None,
|
||||
label_constructor: VMobject | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
label_constructor: type[MathTex] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Self:
|
||||
"""Adds :class:`~.DecimalNumber` mobjects representing their position
|
||||
at each tick of the number line. The numbers can be accessed after creation
|
||||
via ``self.numbers``.
|
||||
|
|
@ -551,11 +569,11 @@ class NumberLine(Line):
|
|||
def add_labels(
|
||||
self,
|
||||
dict_values: dict[float, str | float | VMobject],
|
||||
direction: Sequence[float] = None,
|
||||
direction: Point3DLike | None = None,
|
||||
buff: float | None = None,
|
||||
font_size: float | None = None,
|
||||
label_constructor: VMobject | None = None,
|
||||
):
|
||||
label_constructor: type[MathTex] | None = None,
|
||||
) -> Self:
|
||||
"""Adds specifically positioned labels to the :class:`~.NumberLine` using a ``dict``.
|
||||
The labels can be accessed after creation via ``self.labels``.
|
||||
|
||||
|
|
@ -598,6 +616,7 @@ class NumberLine(Line):
|
|||
label = self._create_label_tex(label, label_constructor)
|
||||
|
||||
if hasattr(label, "font_size"):
|
||||
assert isinstance(label, (MathTex, Tex, Text, Integer)), label
|
||||
label.font_size = font_size
|
||||
else:
|
||||
raise AttributeError(f"{label} is not compatible with add_labels.")
|
||||
|
|
@ -612,7 +631,7 @@ class NumberLine(Line):
|
|||
self,
|
||||
label_tex: str | float | VMobject,
|
||||
label_constructor: Callable | None = None,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> VMobject:
|
||||
"""Checks if the label is a :class:`~.VMobject`, otherwise, creates a
|
||||
label by passing ``label_tex`` to ``label_constructor``.
|
||||
|
|
@ -633,24 +652,25 @@ class NumberLine(Line):
|
|||
:class:`~.VMobject`
|
||||
The label.
|
||||
"""
|
||||
if label_constructor is None:
|
||||
label_constructor = self.label_constructor
|
||||
if isinstance(label_tex, (VMobject, OpenGLVMobject)):
|
||||
return label_tex
|
||||
else:
|
||||
if label_constructor is None:
|
||||
label_constructor = self.label_constructor
|
||||
if isinstance(label_tex, str):
|
||||
return label_constructor(label_tex, **kwargs)
|
||||
return label_constructor(str(label_tex), **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _decimal_places_from_step(step) -> int:
|
||||
step = str(step)
|
||||
if "." not in step:
|
||||
def _decimal_places_from_step(step: float) -> int:
|
||||
step_str = str(step)
|
||||
if "." not in step_str:
|
||||
return 0
|
||||
return len(step.split(".")[-1])
|
||||
return len(step_str.split(".")[-1])
|
||||
|
||||
def __matmul__(self, other: float):
|
||||
def __matmul__(self, other: float) -> Point3D:
|
||||
return self.n2p(other)
|
||||
|
||||
def __rmatmul__(self, other: Point3DLike | Mobject):
|
||||
def __rmatmul__(self, other: Point3DLike | Mobject) -> float:
|
||||
if isinstance(other, Mobject):
|
||||
other = other.get_center()
|
||||
return self.p2n(other)
|
||||
|
|
@ -659,10 +679,10 @@ class NumberLine(Line):
|
|||
class UnitInterval(NumberLine):
|
||||
def __init__(
|
||||
self,
|
||||
unit_size=10,
|
||||
numbers_with_elongated_ticks=None,
|
||||
decimal_number_config=None,
|
||||
**kwargs,
|
||||
unit_size: float = 10,
|
||||
numbers_with_elongated_ticks: list[float] | None = None,
|
||||
decimal_number_config: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
numbers_with_elongated_ticks = (
|
||||
[0, 1]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ __all__ = ["SampleSpace", "BarChart"]
|
|||
|
||||
|
||||
from collections.abc import Iterable, MutableSequence, Sequence
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -13,11 +14,11 @@ from manim import config, logger
|
|||
from manim.constants import *
|
||||
from manim.mobject.geometry.polygram import Rectangle
|
||||
from manim.mobject.graphing.coordinate_systems import Axes
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
|
||||
from manim.mobject.svg.brace import Brace
|
||||
from manim.mobject.text.tex_mobject import MathTex, Tex
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.typing import Vector3D
|
||||
from manim.utils.color import (
|
||||
BLUE_E,
|
||||
DARK_GREY,
|
||||
|
|
@ -54,13 +55,13 @@ class SampleSpace(Rectangle):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
height=3,
|
||||
width=3,
|
||||
fill_color=DARK_GREY,
|
||||
fill_opacity=1,
|
||||
stroke_width=0.5,
|
||||
stroke_color=LIGHT_GREY,
|
||||
default_label_scale_val=1,
|
||||
height: float = 3,
|
||||
width: float = 3,
|
||||
fill_color: ParsableManimColor = DARK_GREY,
|
||||
fill_opacity: float = 1,
|
||||
stroke_width: float = 0.5,
|
||||
stroke_color: ParsableManimColor = LIGHT_GREY,
|
||||
default_label_scale_val: float = 1,
|
||||
):
|
||||
super().__init__(
|
||||
height=height,
|
||||
|
|
@ -72,7 +73,9 @@ class SampleSpace(Rectangle):
|
|||
)
|
||||
self.default_label_scale_val = default_label_scale_val
|
||||
|
||||
def add_title(self, title="Sample space", buff=MED_SMALL_BUFF):
|
||||
def add_title(
|
||||
self, title: str = "Sample space", buff: float = MED_SMALL_BUFF
|
||||
) -> None:
|
||||
# TODO, should this really exist in SampleSpaceScene
|
||||
title_mob = Tex(title)
|
||||
if title_mob.width > self.width:
|
||||
|
|
@ -81,23 +84,30 @@ class SampleSpace(Rectangle):
|
|||
self.title = title_mob
|
||||
self.add(title_mob)
|
||||
|
||||
def add_label(self, label):
|
||||
def add_label(self, label: str) -> None:
|
||||
self.label = label
|
||||
|
||||
def complete_p_list(self, p_list):
|
||||
new_p_list = list(tuplify(p_list))
|
||||
def complete_p_list(self, p_list: float | Iterable[float]) -> list[float]:
|
||||
p_list_tuplified: tuple[float] = tuplify(p_list)
|
||||
new_p_list = list(p_list_tuplified)
|
||||
remainder = 1.0 - sum(new_p_list)
|
||||
if abs(remainder) > EPSILON:
|
||||
new_p_list.append(remainder)
|
||||
return new_p_list
|
||||
|
||||
def get_division_along_dimension(self, p_list, dim, colors, vect):
|
||||
p_list = self.complete_p_list(p_list)
|
||||
colors = color_gradient(colors, len(p_list))
|
||||
def get_division_along_dimension(
|
||||
self,
|
||||
p_list: float | Iterable[float],
|
||||
dim: int,
|
||||
colors: Sequence[ParsableManimColor],
|
||||
vect: Vector3D,
|
||||
) -> VGroup:
|
||||
p_list_complete = self.complete_p_list(p_list)
|
||||
colors_in_gradient = color_gradient(colors, len(p_list_complete))
|
||||
|
||||
last_point = self.get_edge_center(-vect)
|
||||
parts = VGroup()
|
||||
for factor, color in zip(p_list, colors):
|
||||
for factor, color in zip(p_list_complete, colors_in_gradient):
|
||||
part = SampleSpace()
|
||||
part.set_fill(color, 1)
|
||||
part.replace(self, stretch=True)
|
||||
|
|
@ -107,33 +117,43 @@ class SampleSpace(Rectangle):
|
|||
parts.add(part)
|
||||
return parts
|
||||
|
||||
def get_horizontal_division(self, p_list, colors=[GREEN_E, BLUE_E], vect=DOWN):
|
||||
def get_horizontal_division(
|
||||
self,
|
||||
p_list: float | Iterable[float],
|
||||
colors: Sequence[ParsableManimColor] = [GREEN_E, BLUE_E],
|
||||
vect: Vector3D = DOWN,
|
||||
) -> VGroup:
|
||||
return self.get_division_along_dimension(p_list, 1, colors, vect)
|
||||
|
||||
def get_vertical_division(self, p_list, colors=[MAROON_B, YELLOW], vect=RIGHT):
|
||||
def get_vertical_division(
|
||||
self,
|
||||
p_list: float | Iterable[float],
|
||||
colors: Sequence[ParsableManimColor] = [MAROON_B, YELLOW],
|
||||
vect: Vector3D = RIGHT,
|
||||
) -> VGroup:
|
||||
return self.get_division_along_dimension(p_list, 0, colors, vect)
|
||||
|
||||
def divide_horizontally(self, *args, **kwargs):
|
||||
def divide_horizontally(self, *args: Any, **kwargs: Any) -> None:
|
||||
self.horizontal_parts = self.get_horizontal_division(*args, **kwargs)
|
||||
self.add(self.horizontal_parts)
|
||||
|
||||
def divide_vertically(self, *args, **kwargs):
|
||||
def divide_vertically(self, *args: Any, **kwargs: Any) -> None:
|
||||
self.vertical_parts = self.get_vertical_division(*args, **kwargs)
|
||||
self.add(self.vertical_parts)
|
||||
|
||||
def get_subdivision_braces_and_labels(
|
||||
self,
|
||||
parts,
|
||||
labels,
|
||||
direction,
|
||||
buff=SMALL_BUFF,
|
||||
min_num_quads=1,
|
||||
):
|
||||
parts: VGroup,
|
||||
labels: list[str | VMobject | OpenGLVMobject],
|
||||
direction: Vector3D,
|
||||
buff: float = SMALL_BUFF,
|
||||
min_num_quads: int = 1,
|
||||
) -> VGroup:
|
||||
label_mobs = VGroup()
|
||||
braces = VGroup()
|
||||
for label, part in zip(labels, parts):
|
||||
brace = Brace(part, direction, min_num_quads=min_num_quads, buff=buff)
|
||||
if isinstance(label, (Mobject, OpenGLMobject)):
|
||||
if isinstance(label, (VMobject, OpenGLVMobject)):
|
||||
label_mob = label
|
||||
else:
|
||||
label_mob = MathTex(label)
|
||||
|
|
@ -141,34 +161,44 @@ class SampleSpace(Rectangle):
|
|||
label_mob.next_to(brace, direction, buff)
|
||||
|
||||
braces.add(brace)
|
||||
assert isinstance(label_mob, VMobject)
|
||||
label_mobs.add(label_mob)
|
||||
parts.braces = braces
|
||||
parts.labels = label_mobs
|
||||
parts.label_kwargs = {
|
||||
parts.braces = braces # type: ignore[attr-defined]
|
||||
parts.labels = label_mobs # type: ignore[attr-defined]
|
||||
parts.label_kwargs = { # type: ignore[attr-defined]
|
||||
"labels": label_mobs.copy(),
|
||||
"direction": direction,
|
||||
"buff": buff,
|
||||
}
|
||||
return VGroup(parts.braces, parts.labels)
|
||||
|
||||
def get_side_braces_and_labels(self, labels, direction=LEFT, **kwargs):
|
||||
def get_side_braces_and_labels(
|
||||
self,
|
||||
labels: list[str | VMobject | OpenGLVMobject],
|
||||
direction: Vector3D = LEFT,
|
||||
**kwargs: Any,
|
||||
) -> VGroup:
|
||||
assert hasattr(self, "horizontal_parts")
|
||||
parts = self.horizontal_parts
|
||||
return self.get_subdivision_braces_and_labels(
|
||||
parts, labels, direction, **kwargs
|
||||
)
|
||||
|
||||
def get_top_braces_and_labels(self, labels, **kwargs):
|
||||
def get_top_braces_and_labels(
|
||||
self, labels: list[str | VMobject | OpenGLVMobject], **kwargs: Any
|
||||
) -> VGroup:
|
||||
assert hasattr(self, "vertical_parts")
|
||||
parts = self.vertical_parts
|
||||
return self.get_subdivision_braces_and_labels(parts, labels, UP, **kwargs)
|
||||
|
||||
def get_bottom_braces_and_labels(self, labels, **kwargs):
|
||||
def get_bottom_braces_and_labels(
|
||||
self, labels: list[str | VMobject | OpenGLVMobject], **kwargs: Any
|
||||
) -> VGroup:
|
||||
assert hasattr(self, "vertical_parts")
|
||||
parts = self.vertical_parts
|
||||
return self.get_subdivision_braces_and_labels(parts, labels, DOWN, **kwargs)
|
||||
|
||||
def add_braces_and_labels(self):
|
||||
def add_braces_and_labels(self) -> None:
|
||||
for attr in "horizontal_parts", "vertical_parts":
|
||||
if not hasattr(self, attr):
|
||||
continue
|
||||
|
|
@ -177,11 +207,13 @@ class SampleSpace(Rectangle):
|
|||
if hasattr(parts, subattr):
|
||||
self.add(getattr(parts, subattr))
|
||||
|
||||
def __getitem__(self, index):
|
||||
def __getitem__(self, index: int) -> SampleSpace:
|
||||
if hasattr(self, "horizontal_parts"):
|
||||
return self.horizontal_parts[index]
|
||||
val: SampleSpace = self.horizontal_parts[index]
|
||||
return val
|
||||
elif hasattr(self, "vertical_parts"):
|
||||
return self.vertical_parts[index]
|
||||
val = self.vertical_parts[index]
|
||||
return val
|
||||
return self.split()[index]
|
||||
|
||||
|
||||
|
|
@ -253,7 +285,7 @@ class BarChart(Axes):
|
|||
bar_width: float = 0.6,
|
||||
bar_fill_opacity: float = 0.7,
|
||||
bar_stroke_width: float = 3,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
if isinstance(bar_colors, str):
|
||||
logger.warning(
|
||||
|
|
@ -311,7 +343,7 @@ class BarChart(Axes):
|
|||
|
||||
self.y_axis.add_numbers()
|
||||
|
||||
def _update_colors(self):
|
||||
def _update_colors(self) -> None:
|
||||
"""Initialize the colors of the bars of the chart.
|
||||
|
||||
Sets the color of ``self.bars`` via ``self.bar_colors``.
|
||||
|
|
@ -321,13 +353,14 @@ class BarChart(Axes):
|
|||
"""
|
||||
self.bars.set_color_by_gradient(*self.bar_colors)
|
||||
|
||||
def _add_x_axis_labels(self):
|
||||
def _add_x_axis_labels(self) -> None:
|
||||
"""Essentially :meth`:~.NumberLine.add_labels`, but differs in that
|
||||
the direction of the label with respect to the x_axis changes to UP or DOWN
|
||||
depending on the value.
|
||||
|
||||
UP for negative values and DOWN for positive values.
|
||||
"""
|
||||
assert isinstance(self.bar_names, list)
|
||||
val_range = np.arange(
|
||||
0.5, len(self.bar_names), 1
|
||||
) # 0.5 shifted so that labels are centered, not on ticks
|
||||
|
|
@ -338,7 +371,7 @@ class BarChart(Axes):
|
|||
# to accommodate negative bars, the label may need to be
|
||||
# below or above the x_axis depending on the value of the bar
|
||||
direction = UP if self.values[i] < 0 else DOWN
|
||||
bar_name_label = self.x_axis.label_constructor(bar_name)
|
||||
bar_name_label: MathTex = self.x_axis.label_constructor(bar_name)
|
||||
|
||||
bar_name_label.font_size = self.x_axis.font_size
|
||||
bar_name_label.next_to(
|
||||
|
|
@ -398,8 +431,8 @@ class BarChart(Axes):
|
|||
color: ParsableManimColor | None = None,
|
||||
font_size: float = 24,
|
||||
buff: float = MED_SMALL_BUFF,
|
||||
label_constructor: type[VMobject] = Tex,
|
||||
):
|
||||
label_constructor: type[MathTex] = Tex,
|
||||
) -> VGroup:
|
||||
"""Annotates each bar with its corresponding value. Use ``self.bar_labels`` to access the
|
||||
labels after creation.
|
||||
|
||||
|
|
@ -431,7 +464,7 @@ class BarChart(Axes):
|
|||
"""
|
||||
bar_labels = VGroup()
|
||||
for bar, value in zip(self.bars, self.values):
|
||||
bar_lbl = label_constructor(str(value))
|
||||
bar_lbl: MathTex = label_constructor(str(value))
|
||||
|
||||
if color is None:
|
||||
bar_lbl.set_color(bar.get_fill_color())
|
||||
|
|
@ -446,7 +479,9 @@ class BarChart(Axes):
|
|||
|
||||
return bar_labels
|
||||
|
||||
def change_bar_values(self, values: Iterable[float], update_colors: bool = True):
|
||||
def change_bar_values(
|
||||
self, values: Iterable[float], update_colors: bool = True
|
||||
) -> None:
|
||||
"""Updates the height of the bars of the chart.
|
||||
|
||||
Parameters
|
||||
|
|
@ -512,4 +547,4 @@ class BarChart(Axes):
|
|||
if update_colors:
|
||||
self._update_colors()
|
||||
|
||||
self.values[: len(values)] = values
|
||||
self.values[: len(list(values))] = values
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import math
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, overload
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -11,7 +11,9 @@ __all__ = ["LogBase", "LinearBase"]
|
|||
from manim.mobject.text.numbers import Integer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.mobject import Mobject
|
||||
from typing import Callable
|
||||
|
||||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
|
||||
|
||||
class _ScaleBase:
|
||||
|
|
@ -26,6 +28,12 @@ class _ScaleBase:
|
|||
def __init__(self, custom_labels: bool = False):
|
||||
self.custom_labels = custom_labels
|
||||
|
||||
@overload
|
||||
def function(self, value: float) -> float: ...
|
||||
|
||||
@overload
|
||||
def function(self, value: np.ndarray) -> np.ndarray: ...
|
||||
|
||||
def function(self, value: float) -> float:
|
||||
"""The function that will be used to scale the values.
|
||||
|
||||
|
|
@ -59,7 +67,8 @@ class _ScaleBase:
|
|||
def get_custom_labels(
|
||||
self,
|
||||
val_range: Iterable[float],
|
||||
) -> Iterable[Mobject]:
|
||||
**kw_args: Any,
|
||||
) -> Iterable[VMobject]:
|
||||
"""Custom instructions for generating labels along an axis.
|
||||
|
||||
Parameters
|
||||
|
|
@ -139,15 +148,19 @@ class LogBase(_ScaleBase):
|
|||
|
||||
def function(self, value: float) -> float:
|
||||
"""Scales the value to fit it to a logarithmic scale.``self.function(5)==10**5``"""
|
||||
return self.base**value
|
||||
return_value: float = self.base**value
|
||||
return return_value
|
||||
|
||||
def inverse_function(self, value: float) -> float:
|
||||
"""Inverse of ``function``. The value must be greater than 0"""
|
||||
if isinstance(value, np.ndarray):
|
||||
condition = value.any() <= 0
|
||||
|
||||
def func(value, base):
|
||||
return np.log(value) / np.log(base)
|
||||
func: Callable[[float, float], float]
|
||||
|
||||
def func(value: float, base: float) -> float:
|
||||
return_value: float = np.log(value) / np.log(base)
|
||||
return return_value
|
||||
else:
|
||||
condition = value <= 0
|
||||
func = math.log
|
||||
|
|
@ -163,8 +176,8 @@ class LogBase(_ScaleBase):
|
|||
self,
|
||||
val_range: Iterable[float],
|
||||
unit_decimal_places: int = 0,
|
||||
**base_config: dict[str, Any],
|
||||
) -> list[Mobject]:
|
||||
**base_config: Any,
|
||||
) -> list[Integer]:
|
||||
"""Produces custom :class:`~.Integer` labels in the form of ``10^2``.
|
||||
|
||||
Parameters
|
||||
|
|
@ -177,7 +190,7 @@ class LogBase(_ScaleBase):
|
|||
Additional arguments to be passed to :class:`~.Integer`.
|
||||
"""
|
||||
# uses `format` syntax to control the number of decimal places.
|
||||
tex_labels = [
|
||||
tex_labels: list[Integer] = [
|
||||
Integer(
|
||||
self.base,
|
||||
unit="^{%s}" % (f"{self.inverse_function(i):.{unit_decimal_places}f}"), # noqa: UP031
|
||||
|
|
|
|||
|
|
@ -4,11 +4,15 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["ManimBanner"]
|
||||
|
||||
from typing import Any
|
||||
|
||||
import svgelements as se
|
||||
|
||||
from manim.animation.updaters.update import UpdateFromAlphaFunc
|
||||
from manim.mobject.geometry.arc import Circle
|
||||
from manim.mobject.geometry.polygram import Square, Triangle
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.typing import Vector3D
|
||||
|
||||
from .. import constants as cst
|
||||
from ..animation.animation import override_animation
|
||||
|
|
@ -146,7 +150,7 @@ class ManimBanner(VGroup):
|
|||
m_height_over_anim_height = 0.75748
|
||||
|
||||
self.font_color = "#ece6e2" if dark_theme else "#343434"
|
||||
self.scale_factor = 1
|
||||
self.scale_factor = 1.0
|
||||
|
||||
self.M = VMobjectFromSVGPath(MANIM_SVG_PATHS[0]).flip(cst.RIGHT).center()
|
||||
self.M.set(stroke_width=0).scale(
|
||||
|
|
@ -180,7 +184,7 @@ class ManimBanner(VGroup):
|
|||
# and thus not yet added to the submobjects of self.
|
||||
self.anim = anim
|
||||
|
||||
def scale(self, scale_factor: float, **kwargs) -> ManimBanner:
|
||||
def scale(self, scale_factor: float, **kwargs: Any) -> ManimBanner:
|
||||
"""Scale the banner by the specified scale factor.
|
||||
|
||||
Parameters
|
||||
|
|
@ -219,7 +223,7 @@ class ManimBanner(VGroup):
|
|||
lag_ratio=0.1,
|
||||
)
|
||||
|
||||
def expand(self, run_time: float = 1.5, direction="center") -> Succession:
|
||||
def expand(self, run_time: float = 1.5, direction: str = "center") -> Succession:
|
||||
"""An animation that expands Manim's logo into its banner.
|
||||
|
||||
The returned animation transforms the banner from its initial
|
||||
|
|
@ -277,7 +281,7 @@ class ManimBanner(VGroup):
|
|||
self.M.save_state()
|
||||
left_group = VGroup(self.M, self.anim, m_clone)
|
||||
|
||||
def shift(vector):
|
||||
def shift(vector: Vector3D) -> None:
|
||||
self.shapes.restore()
|
||||
left_group.align_to(self.M.saved_state, cst.LEFT)
|
||||
if direction == "right":
|
||||
|
|
@ -288,7 +292,7 @@ class ManimBanner(VGroup):
|
|||
elif direction == "left":
|
||||
left_group.shift(-vector)
|
||||
|
||||
def slide_and_uncover(mob, alpha):
|
||||
def slide_and_uncover(mob: Mobject, alpha: float) -> None:
|
||||
shift(alpha * (m_shape_offset + shape_sliding_overshoot) * cst.RIGHT)
|
||||
|
||||
# Add letters when they are covered
|
||||
|
|
@ -305,7 +309,7 @@ class ManimBanner(VGroup):
|
|||
mob.shapes.save_state()
|
||||
mob.M.save_state()
|
||||
|
||||
def slide_back(mob, alpha):
|
||||
def slide_back(mob: Mobject, alpha: float) -> None:
|
||||
if alpha == 0:
|
||||
m_clone.set_opacity(1)
|
||||
m_clone.move_to(mob.anim[-1])
|
||||
|
|
|
|||
|
|
@ -40,9 +40,11 @@ __all__ = [
|
|||
|
||||
|
||||
import itertools as it
|
||||
from collections.abc import Iterable, Sequence
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
|
|
@ -56,7 +58,7 @@ from ..mobject.types.vectorized_mobject import VGroup, VMobject
|
|||
# Not sure if we should keep it or not.
|
||||
|
||||
|
||||
def matrix_to_tex_string(matrix):
|
||||
def matrix_to_tex_string(matrix: np.ndarray) -> str:
|
||||
matrix = np.array(matrix).astype("str")
|
||||
if matrix.ndim == 1:
|
||||
matrix = matrix.reshape((matrix.size, 1))
|
||||
|
|
@ -67,7 +69,7 @@ def matrix_to_tex_string(matrix):
|
|||
return prefix + " \\\\ ".join(rows) + suffix
|
||||
|
||||
|
||||
def matrix_to_mobject(matrix):
|
||||
def matrix_to_mobject(matrix: np.ndarray) -> MathTex:
|
||||
return MathTex(matrix_to_tex_string(matrix))
|
||||
|
||||
|
||||
|
|
@ -170,14 +172,14 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
|
|||
bracket_v_buff: float = MED_SMALL_BUFF,
|
||||
add_background_rectangles_to_entries: bool = False,
|
||||
include_background_rectangle: bool = False,
|
||||
element_to_mobject: type[MathTex] = MathTex,
|
||||
element_to_mobject: type[Mobject] | Callable[..., Mobject] = MathTex,
|
||||
element_to_mobject_config: dict = {},
|
||||
element_alignment_corner: Sequence[float] = DR,
|
||||
left_bracket: str = "[",
|
||||
right_bracket: str = "]",
|
||||
stretch_brackets: bool = True,
|
||||
bracket_config: dict = {},
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.v_buff = v_buff
|
||||
self.h_buff = h_buff
|
||||
|
|
@ -205,7 +207,7 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
|
|||
if self.include_background_rectangle:
|
||||
self.add_background_rectangle()
|
||||
|
||||
def _matrix_to_mob_matrix(self, matrix):
|
||||
def _matrix_to_mob_matrix(self, matrix: np.ndarray) -> list[list[Mobject]]:
|
||||
return [
|
||||
[
|
||||
self.element_to_mobject(item, **self.element_to_mobject_config)
|
||||
|
|
@ -214,7 +216,7 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
|
|||
for row in matrix
|
||||
]
|
||||
|
||||
def _organize_mob_matrix(self, matrix):
|
||||
def _organize_mob_matrix(self, matrix: list[list[Mobject]]) -> Self:
|
||||
for i, row in enumerate(matrix):
|
||||
for j, _ in enumerate(row):
|
||||
mob = matrix[i][j]
|
||||
|
|
@ -224,7 +226,7 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
|
|||
)
|
||||
return self
|
||||
|
||||
def _add_brackets(self, left: str = "[", right: str = "]", **kwargs):
|
||||
def _add_brackets(self, left: str = "[", right: str = "]", **kwargs: Any) -> Self:
|
||||
"""Adds the brackets to the Matrix mobject.
|
||||
|
||||
See Latex document for various bracket types.
|
||||
|
|
@ -278,13 +280,13 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
|
|||
self.add(l_bracket, r_bracket)
|
||||
return self
|
||||
|
||||
def get_columns(self):
|
||||
def get_columns(self) -> VGroup:
|
||||
r"""Return columns of the matrix as VGroups.
|
||||
|
||||
Returns
|
||||
--------
|
||||
List[:class:`~.VGroup`]
|
||||
Each VGroup contains a column of the matrix.
|
||||
:class:`~.VGroup`
|
||||
The VGroup contains a nested VGroup for each column of the matrix.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
|
@ -305,7 +307,7 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
|
|||
)
|
||||
)
|
||||
|
||||
def set_column_colors(self, *colors: str):
|
||||
def set_column_colors(self, *colors: str) -> Self:
|
||||
r"""Set individual colors for each columns of the matrix.
|
||||
|
||||
Parameters
|
||||
|
|
@ -335,13 +337,13 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
|
|||
column.set_color(color)
|
||||
return self
|
||||
|
||||
def get_rows(self):
|
||||
def get_rows(self) -> VGroup:
|
||||
r"""Return rows of the matrix as VGroups.
|
||||
|
||||
Returns
|
||||
--------
|
||||
List[:class:`~.VGroup`]
|
||||
Each VGroup contains a row of the matrix.
|
||||
:class:`~.VGroup`
|
||||
The VGroup contains a nested VGroup for each row of the matrix.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
|
@ -357,7 +359,7 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
|
|||
"""
|
||||
return VGroup(*(VGroup(*row) for row in self.mob_matrix))
|
||||
|
||||
def set_row_colors(self, *colors: str):
|
||||
def set_row_colors(self, *colors: str) -> Self:
|
||||
r"""Set individual colors for each row of the matrix.
|
||||
|
||||
Parameters
|
||||
|
|
@ -387,7 +389,7 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
|
|||
row.set_color(color)
|
||||
return self
|
||||
|
||||
def add_background_to_entries(self):
|
||||
def add_background_to_entries(self) -> Self:
|
||||
"""Add a black background rectangle to the matrix,
|
||||
see above for an example.
|
||||
|
||||
|
|
@ -400,7 +402,7 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
|
|||
mob.add_background_rectangle()
|
||||
return self
|
||||
|
||||
def get_mob_matrix(self):
|
||||
def get_mob_matrix(self) -> list[list[Mobject]]:
|
||||
"""Return the underlying mob matrix mobjects.
|
||||
|
||||
Returns
|
||||
|
|
@ -410,7 +412,7 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
|
|||
"""
|
||||
return self.mob_matrix
|
||||
|
||||
def get_entries(self):
|
||||
def get_entries(self) -> VGroup:
|
||||
"""Return the individual entries of the matrix.
|
||||
|
||||
Returns
|
||||
|
|
@ -435,13 +437,13 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
|
|||
"""
|
||||
return self.elements
|
||||
|
||||
def get_brackets(self):
|
||||
def get_brackets(self) -> VGroup:
|
||||
r"""Return the bracket mobjects.
|
||||
|
||||
Returns
|
||||
--------
|
||||
List[:class:`~.VGroup`]
|
||||
Each VGroup contains a bracket
|
||||
:class:`~.VGroup`
|
||||
A VGroup containing the left and right bracket.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
|
@ -483,9 +485,9 @@ class DecimalMatrix(Matrix):
|
|||
def __init__(
|
||||
self,
|
||||
matrix: Iterable,
|
||||
element_to_mobject: Mobject = DecimalNumber,
|
||||
element_to_mobject_config: dict[str, Mobject] = {"num_decimal_places": 1},
|
||||
**kwargs,
|
||||
element_to_mobject: type[Mobject] = DecimalNumber,
|
||||
element_to_mobject_config: dict[str, Any] = {"num_decimal_places": 1},
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
Will round/truncate the decimal places as per the provided config.
|
||||
|
|
@ -526,7 +528,10 @@ class IntegerMatrix(Matrix):
|
|||
"""
|
||||
|
||||
def __init__(
|
||||
self, matrix: Iterable, element_to_mobject: Mobject = Integer, **kwargs
|
||||
self,
|
||||
matrix: Iterable,
|
||||
element_to_mobject: type[Mobject] = Integer,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
Will round if there are decimal entries in the matrix.
|
||||
|
|
@ -560,7 +565,12 @@ class MobjectMatrix(Matrix):
|
|||
self.add(m0)
|
||||
"""
|
||||
|
||||
def __init__(self, matrix, element_to_mobject=lambda m: m, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
matrix: Iterable,
|
||||
element_to_mobject: type[Mobject] | Callable[..., Mobject] = lambda m: m,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(matrix, element_to_mobject=element_to_mobject, **kwargs)
|
||||
|
||||
|
||||
|
|
@ -569,7 +579,7 @@ def get_det_text(
|
|||
determinant: int | str | None = None,
|
||||
background_rect: bool = False,
|
||||
initial_scale_factor: float = 2,
|
||||
):
|
||||
) -> VGroup:
|
||||
r"""Helper function to create determinant.
|
||||
|
||||
Parameters
|
||||
|
|
|
|||
|
|
@ -14,13 +14,14 @@ import random
|
|||
import sys
|
||||
import types
|
||||
import warnings
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Callable, Iterable
|
||||
from functools import partialmethod, reduce
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.data_structures import MethodWithArgs
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
|
||||
from .. import config, logger
|
||||
|
|
@ -40,8 +41,6 @@ from ..utils.paths import straight_path
|
|||
from ..utils.space_ops import angle_between_vectors, normalize, rotation_matrix
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Callable, Literal
|
||||
|
||||
from typing_extensions import Self, TypeAlias
|
||||
|
||||
from manim.typing import (
|
||||
|
|
@ -53,7 +52,7 @@ if TYPE_CHECKING:
|
|||
Point3D,
|
||||
Point3DLike,
|
||||
Point3DLike_Array,
|
||||
Vector3D,
|
||||
Vector3DLike,
|
||||
)
|
||||
|
||||
from ..animation.animation import Animation
|
||||
|
|
@ -387,9 +386,9 @@ class Mobject:
|
|||
will interpolate the :class:`~.Mobject` between its points prior to
|
||||
``.animate`` and its points after applying ``.animate`` to it. This may
|
||||
result in unexpected behavior when attempting to interpolate along paths,
|
||||
or rotations.
|
||||
or rotations (see :meth:`.rotate`).
|
||||
If you want animations to consider the points between, consider using
|
||||
:class:`~.ValueTracker` with updaters instead.
|
||||
:class:`~.ValueTracker` with updaters instead (see :meth:`.add_updater`).
|
||||
|
||||
"""
|
||||
return _AnimationBuilder(self)
|
||||
|
|
@ -1002,16 +1001,16 @@ class Mobject:
|
|||
|
||||
class NextToUpdater(Scene):
|
||||
def construct(self):
|
||||
def dot_position(mobject):
|
||||
def update_label(mobject):
|
||||
mobject.set_value(dot.get_center()[0])
|
||||
mobject.next_to(dot)
|
||||
|
||||
dot = Dot(RIGHT*3)
|
||||
label = DecimalNumber()
|
||||
label.add_updater(dot_position)
|
||||
label.add_updater(update_label)
|
||||
self.add(dot, label)
|
||||
|
||||
self.play(Rotating(dot, about_point=ORIGIN, angle=TAU, run_time=TAU, rate_func=linear))
|
||||
self.play(Rotating(dot, angle=TAU, about_point=ORIGIN, run_time=TAU, rate_func=linear))
|
||||
|
||||
.. manim:: DtUpdater
|
||||
|
||||
|
|
@ -1029,6 +1028,9 @@ class Mobject:
|
|||
:meth:`get_updaters`
|
||||
:meth:`remove_updater`
|
||||
:class:`~.UpdateFromFunc`
|
||||
:class:`~.Rotating`
|
||||
:meth:`rotate`
|
||||
:attr:`~.Mobject.animate`
|
||||
"""
|
||||
if index is None:
|
||||
self.updaters.append(update_function)
|
||||
|
|
@ -1200,7 +1202,7 @@ class Mobject:
|
|||
for mob in self.family_members_with_points():
|
||||
func(mob)
|
||||
|
||||
def shift(self, *vectors: Vector3D) -> Self:
|
||||
def shift(self, *vectors: Vector3DLike) -> Self:
|
||||
"""Shift by the given vectors.
|
||||
|
||||
Parameters
|
||||
|
|
@ -1271,25 +1273,82 @@ class Mobject:
|
|||
)
|
||||
return self
|
||||
|
||||
def rotate_about_origin(self, angle: float, axis: Vector3D = OUT, axes=[]) -> Self:
|
||||
def rotate_about_origin(self, angle: float, axis: Vector3DLike = OUT) -> Self:
|
||||
"""Rotates the :class:`~.Mobject` about the ORIGIN, which is at [0,0,0]."""
|
||||
return self.rotate(angle, axis, about_point=ORIGIN)
|
||||
|
||||
def rotate(
|
||||
self,
|
||||
angle: float,
|
||||
axis: Vector3D = OUT,
|
||||
axis: Vector3DLike = OUT,
|
||||
about_point: Point3DLike | None = None,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
"""Rotates the :class:`~.Mobject` about a certain point."""
|
||||
"""Rotates the :class:`~.Mobject` around a specified axis and point.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
angle
|
||||
The angle of rotation in radians. Predefined constants such as ``DEGREES``
|
||||
can also be used to specify the angle in degrees.
|
||||
axis
|
||||
The rotation axis (see :class:`~.Rotating` for more).
|
||||
about_point
|
||||
The point about which the mobject rotates. If ``None``, rotation occurs around
|
||||
the center of the mobject.
|
||||
**kwargs
|
||||
Additional keyword arguments passed to :meth:`apply_points_function_about_point`,
|
||||
such as ``about_edge``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`Mobject`
|
||||
``self`` (for method chaining)
|
||||
|
||||
|
||||
.. note::
|
||||
To animate a rotation, use :class:`~.Rotating` or :class:`~.Rotate`
|
||||
instead of ``.animate.rotate(...)``.
|
||||
The ``.animate.rotate(...)`` syntax only applies a transformation
|
||||
from the initial state to the final rotated state
|
||||
(interpolation between the two states), without showing proper rotational motion
|
||||
based on the angle (from 0 to the given angle).
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim:: RotateMethodExample
|
||||
:save_last_frame:
|
||||
|
||||
class RotateMethodExample(Scene):
|
||||
def construct(self):
|
||||
circle = Circle(radius=1, color=BLUE)
|
||||
line = Line(start=ORIGIN, end=RIGHT)
|
||||
arrow1 = Arrow(start=ORIGIN, end=RIGHT, buff=0, color=GOLD)
|
||||
group1 = VGroup(circle, line, arrow1)
|
||||
|
||||
group2 = group1.copy()
|
||||
arrow2 = group2[2]
|
||||
arrow2.rotate(angle=PI / 4, about_point=arrow2.get_start())
|
||||
|
||||
group3 = group1.copy()
|
||||
arrow3 = group3[2]
|
||||
arrow3.rotate(angle=120 * DEGREES, about_point=arrow3.get_start())
|
||||
|
||||
self.add(VGroup(group1, group2, group3).arrange(RIGHT, buff=1))
|
||||
|
||||
See also
|
||||
--------
|
||||
:class:`~.Rotating`, :class:`~.Rotate`, :attr:`~.Mobject.animate`, :meth:`apply_points_function_about_point`
|
||||
|
||||
"""
|
||||
rot_matrix = rotation_matrix(angle, axis)
|
||||
self.apply_points_function_about_point(
|
||||
lambda points: np.dot(points, rot_matrix.T), about_point, **kwargs
|
||||
)
|
||||
return self
|
||||
|
||||
def flip(self, axis: Vector3D = UP, **kwargs) -> Self:
|
||||
def flip(self, axis: Vector3DLike = UP, **kwargs) -> Self:
|
||||
"""Flips/Mirrors an mobject about its center.
|
||||
|
||||
Examples
|
||||
|
|
@ -1409,7 +1468,7 @@ class Mobject:
|
|||
self,
|
||||
func: MultiMappingFunction,
|
||||
about_point: Point3DLike | None = None,
|
||||
about_edge: Vector3D | None = None,
|
||||
about_edge: Vector3DLike | None = None,
|
||||
) -> Self:
|
||||
if about_point is None:
|
||||
if about_edge is None:
|
||||
|
|
@ -1439,7 +1498,7 @@ class Mobject:
|
|||
return self
|
||||
|
||||
def align_on_border(
|
||||
self, direction: Vector3D, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER
|
||||
self, direction: Vector3DLike, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER
|
||||
) -> Self:
|
||||
"""Direction just needs to be a vector pointing towards side or
|
||||
corner in the 2d plane.
|
||||
|
|
@ -1456,7 +1515,7 @@ class Mobject:
|
|||
return self
|
||||
|
||||
def to_corner(
|
||||
self, corner: Vector3D = DL, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER
|
||||
self, corner: Vector3DLike = DL, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER
|
||||
) -> Self:
|
||||
"""Moves this :class:`~.Mobject` to the given corner of the screen.
|
||||
|
||||
|
|
@ -1484,7 +1543,7 @@ class Mobject:
|
|||
return self.align_on_border(corner, buff)
|
||||
|
||||
def to_edge(
|
||||
self, edge: Vector3D = LEFT, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER
|
||||
self, edge: Vector3DLike = LEFT, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER
|
||||
) -> Self:
|
||||
"""Moves this :class:`~.Mobject` to the given edge of the screen,
|
||||
without affecting its position in the other dimension.
|
||||
|
|
@ -1516,12 +1575,12 @@ class Mobject:
|
|||
def next_to(
|
||||
self,
|
||||
mobject_or_point: Mobject | Point3DLike,
|
||||
direction: Vector3D = RIGHT,
|
||||
direction: Vector3DLike = RIGHT,
|
||||
buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFFER,
|
||||
aligned_edge: Vector3D = ORIGIN,
|
||||
aligned_edge: Vector3DLike = ORIGIN,
|
||||
submobject_to_align: Mobject | None = None,
|
||||
index_of_submobject_to_align: int | None = None,
|
||||
coor_mask: Vector3D = np.array([1, 1, 1]),
|
||||
coor_mask: Vector3DLike = np.array([1, 1, 1]),
|
||||
) -> Self:
|
||||
"""Move this :class:`~.Mobject` next to another's :class:`~.Mobject` or Point3D.
|
||||
|
||||
|
|
@ -1543,13 +1602,18 @@ class Mobject:
|
|||
self.add(d, c, s, t)
|
||||
|
||||
"""
|
||||
np_direction = np.asarray(direction)
|
||||
np_aligned_edge = np.asarray(aligned_edge)
|
||||
|
||||
if isinstance(mobject_or_point, Mobject):
|
||||
mob = mobject_or_point
|
||||
if index_of_submobject_to_align is not None:
|
||||
target_aligner = mob[index_of_submobject_to_align]
|
||||
else:
|
||||
target_aligner = mob
|
||||
target_point = target_aligner.get_critical_point(aligned_edge + direction)
|
||||
target_point = target_aligner.get_critical_point(
|
||||
np_aligned_edge + np_direction
|
||||
)
|
||||
else:
|
||||
target_point = mobject_or_point
|
||||
if submobject_to_align is not None:
|
||||
|
|
@ -1558,8 +1622,8 @@ class Mobject:
|
|||
aligner = self[index_of_submobject_to_align]
|
||||
else:
|
||||
aligner = self
|
||||
point_to_align = aligner.get_critical_point(aligned_edge - direction)
|
||||
self.shift((target_point - point_to_align + buff * direction) * coor_mask)
|
||||
point_to_align = aligner.get_critical_point(np_aligned_edge - np_direction)
|
||||
self.shift((target_point - point_to_align + buff * np_direction) * coor_mask)
|
||||
return self
|
||||
|
||||
def shift_onto_screen(self, **kwargs) -> Self:
|
||||
|
|
@ -1705,22 +1769,22 @@ class Mobject:
|
|||
"""Stretches the :class:`~.Mobject` to fit a depth, not keeping width/height proportional."""
|
||||
return self.rescale_to_fit(depth, 2, stretch=True, **kwargs)
|
||||
|
||||
def set_coord(self, value, dim: int, direction: Vector3D = ORIGIN) -> Self:
|
||||
def set_coord(self, value, dim: int, direction: Vector3DLike = ORIGIN) -> Self:
|
||||
curr = self.get_coord(dim, direction)
|
||||
shift_vect = np.zeros(self.dim)
|
||||
shift_vect[dim] = value - curr
|
||||
self.shift(shift_vect)
|
||||
return self
|
||||
|
||||
def set_x(self, x: float, direction: Vector3D = ORIGIN) -> Self:
|
||||
def set_x(self, x: float, direction: Vector3DLike = ORIGIN) -> Self:
|
||||
"""Set x value of the center of the :class:`~.Mobject` (``int`` or ``float``)"""
|
||||
return self.set_coord(x, 0, direction)
|
||||
|
||||
def set_y(self, y: float, direction: Vector3D = ORIGIN) -> Self:
|
||||
def set_y(self, y: float, direction: Vector3DLike = ORIGIN) -> Self:
|
||||
"""Set y value of the center of the :class:`~.Mobject` (``int`` or ``float``)"""
|
||||
return self.set_coord(y, 1, direction)
|
||||
|
||||
def set_z(self, z: float, direction: Vector3D = ORIGIN) -> Self:
|
||||
def set_z(self, z: float, direction: Vector3DLike = ORIGIN) -> Self:
|
||||
"""Set z value of the center of the :class:`~.Mobject` (``int`` or ``float``)"""
|
||||
return self.set_coord(z, 2, direction)
|
||||
|
||||
|
|
@ -1733,8 +1797,8 @@ class Mobject:
|
|||
def move_to(
|
||||
self,
|
||||
point_or_mobject: Point3DLike | Mobject,
|
||||
aligned_edge: Vector3D = ORIGIN,
|
||||
coor_mask: Vector3D = np.array([1, 1, 1]),
|
||||
aligned_edge: Vector3DLike = ORIGIN,
|
||||
coor_mask: Vector3DLike = np.array([1, 1, 1]),
|
||||
) -> Self:
|
||||
"""Move center of the :class:`~.Mobject` to certain Point3D."""
|
||||
if isinstance(point_or_mobject, Mobject):
|
||||
|
|
@ -2053,7 +2117,7 @@ class Mobject:
|
|||
else:
|
||||
return np.max(values)
|
||||
|
||||
def get_critical_point(self, direction: Vector3D) -> Point3D:
|
||||
def get_critical_point(self, direction: Vector3DLike) -> Point3D:
|
||||
"""Picture a box bounding the :class:`~.Mobject`. Such a box has
|
||||
9 'critical points': 4 corners, 4 edge center, the
|
||||
center. This returns one of them, along the given direction.
|
||||
|
|
@ -2082,11 +2146,11 @@ class Mobject:
|
|||
|
||||
# Pseudonyms for more general get_critical_point method
|
||||
|
||||
def get_edge_center(self, direction: Vector3D) -> Point3D:
|
||||
def get_edge_center(self, direction: Vector3DLike) -> Point3D:
|
||||
"""Get edge Point3Ds for certain direction."""
|
||||
return self.get_critical_point(direction)
|
||||
|
||||
def get_corner(self, direction: Vector3D) -> Point3D:
|
||||
def get_corner(self, direction: Vector3DLike) -> Point3D:
|
||||
"""Get corner Point3Ds for certain direction."""
|
||||
return self.get_critical_point(direction)
|
||||
|
||||
|
|
@ -2097,9 +2161,9 @@ class Mobject:
|
|||
def get_center_of_mass(self) -> Point3D:
|
||||
return np.apply_along_axis(np.mean, 0, self.get_all_points())
|
||||
|
||||
def get_boundary_point(self, direction: Vector3D) -> Point3D:
|
||||
def get_boundary_point(self, direction: Vector3DLike) -> Point3D:
|
||||
all_points = self.get_points_defining_boundary()
|
||||
index = np.argmax(np.dot(all_points, np.array(direction).T))
|
||||
index = np.argmax(np.dot(all_points, direction))
|
||||
return all_points[index]
|
||||
|
||||
def get_midpoint(self) -> Point3D:
|
||||
|
|
@ -2156,19 +2220,19 @@ class Mobject:
|
|||
dim,
|
||||
) - self.reduce_across_dimension(min, dim)
|
||||
|
||||
def get_coord(self, dim: int, direction: Vector3D = ORIGIN):
|
||||
def get_coord(self, dim: int, direction: Vector3DLike = ORIGIN) -> float:
|
||||
"""Meant to generalize ``get_x``, ``get_y`` and ``get_z``"""
|
||||
return self.get_extremum_along_dim(dim=dim, key=direction[dim])
|
||||
|
||||
def get_x(self, direction: Vector3D = ORIGIN) -> float:
|
||||
def get_x(self, direction: Vector3DLike = ORIGIN) -> float:
|
||||
"""Returns x Point3D of the center of the :class:`~.Mobject` as ``float``"""
|
||||
return self.get_coord(0, direction)
|
||||
|
||||
def get_y(self, direction: Vector3D = ORIGIN) -> float:
|
||||
def get_y(self, direction: Vector3DLike = ORIGIN) -> float:
|
||||
"""Returns y Point3D of the center of the :class:`~.Mobject` as ``float``"""
|
||||
return self.get_coord(1, direction)
|
||||
|
||||
def get_z(self, direction: Vector3D = ORIGIN) -> float:
|
||||
def get_z(self, direction: Vector3DLike = ORIGIN) -> float:
|
||||
"""Returns z Point3D of the center of the :class:`~.Mobject` as ``float``"""
|
||||
return self.get_coord(2, direction)
|
||||
|
||||
|
|
@ -2239,7 +2303,7 @@ class Mobject:
|
|||
return self.match_dim_size(mobject, 2, **kwargs)
|
||||
|
||||
def match_coord(
|
||||
self, mobject: Mobject, dim: int, direction: Vector3D = ORIGIN
|
||||
self, mobject: Mobject, dim: int, direction: Vector3DLike = ORIGIN
|
||||
) -> Self:
|
||||
"""Match the Point3Ds with the Point3Ds of another :class:`~.Mobject`."""
|
||||
return self.set_coord(
|
||||
|
|
@ -2263,7 +2327,7 @@ class Mobject:
|
|||
def align_to(
|
||||
self,
|
||||
mobject_or_point: Mobject | Point3DLike,
|
||||
direction: Vector3D = ORIGIN,
|
||||
direction: Vector3DLike = ORIGIN,
|
||||
) -> Self:
|
||||
"""Aligns mobject to another :class:`~.Mobject` in a certain direction.
|
||||
|
||||
|
|
@ -2293,7 +2357,7 @@ class Mobject:
|
|||
def __iter__(self):
|
||||
return iter(self.split())
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
return len(self.split())
|
||||
|
||||
def get_group_class(self) -> type[Group]:
|
||||
|
|
@ -2370,7 +2434,7 @@ class Mobject:
|
|||
|
||||
def arrange(
|
||||
self,
|
||||
direction: Vector3D = RIGHT,
|
||||
direction: Vector3DLike = RIGHT,
|
||||
buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFFER,
|
||||
center: bool = True,
|
||||
**kwargs,
|
||||
|
|
@ -2403,7 +2467,7 @@ class Mobject:
|
|||
rows: int | None = None,
|
||||
cols: int | None = None,
|
||||
buff: float | tuple[float, float] = MED_SMALL_BUFF,
|
||||
cell_alignment: Vector3D = ORIGIN,
|
||||
cell_alignment: Vector3DLike = ORIGIN,
|
||||
row_alignments: str | None = None, # "ucd"
|
||||
col_alignments: str | None = None, # "lcr"
|
||||
row_heights: Iterable[float | None] | None = None,
|
||||
|
|
@ -3042,21 +3106,22 @@ class Mobject:
|
|||
--------
|
||||
:meth:`~.Mobject.align_data`, :meth:`~.VMobject.interpolate_color`
|
||||
"""
|
||||
mobject = mobject.copy()
|
||||
if stretch:
|
||||
mobject.stretch_to_fit_height(self.height)
|
||||
mobject.stretch_to_fit_width(self.width)
|
||||
mobject.stretch_to_fit_depth(self.depth)
|
||||
else:
|
||||
if match_height:
|
||||
mobject.match_height(self)
|
||||
if match_width:
|
||||
mobject.match_width(self)
|
||||
if match_depth:
|
||||
mobject.match_depth(self)
|
||||
if stretch or match_height or match_width or match_depth or match_center:
|
||||
mobject = mobject.copy()
|
||||
if stretch:
|
||||
mobject.stretch_to_fit_height(self.height)
|
||||
mobject.stretch_to_fit_width(self.width)
|
||||
mobject.stretch_to_fit_depth(self.depth)
|
||||
else:
|
||||
if match_height:
|
||||
mobject.match_height(self)
|
||||
if match_width:
|
||||
mobject.match_width(self)
|
||||
if match_depth:
|
||||
mobject.match_depth(self)
|
||||
|
||||
if match_center:
|
||||
mobject.move_to(self.get_center())
|
||||
if match_center:
|
||||
mobject.move_to(self.get_center())
|
||||
|
||||
self.align_data(mobject, skip_point_alignment=True)
|
||||
for sm1, sm2 in zip(self.get_family(), mobject.get_family()):
|
||||
|
|
@ -3172,7 +3237,7 @@ class _AnimationBuilder:
|
|||
|
||||
self.overridden_animation = None
|
||||
self.is_chaining = False
|
||||
self.methods = []
|
||||
self.methods: list[MethodWithArgs] = []
|
||||
|
||||
# Whether animation args can be passed
|
||||
self.cannot_pass_args = False
|
||||
|
|
@ -3207,7 +3272,7 @@ class _AnimationBuilder:
|
|||
**method_kwargs,
|
||||
)
|
||||
else:
|
||||
self.methods.append([method, method_args, method_kwargs])
|
||||
self.methods.append(MethodWithArgs(method, method_args, method_kwargs))
|
||||
method(*method_args, **method_kwargs)
|
||||
return self
|
||||
|
||||
|
|
@ -3221,10 +3286,7 @@ class _AnimationBuilder:
|
|||
_MethodAnimation,
|
||||
)
|
||||
|
||||
if self.overridden_animation:
|
||||
anim = self.overridden_animation
|
||||
else:
|
||||
anim = _MethodAnimation(self.mobject, self.methods)
|
||||
anim = self.overridden_animation or _MethodAnimation(self.mobject, self.methods)
|
||||
|
||||
for attr, value in self.anim_args.items():
|
||||
setattr(anim, attr, value)
|
||||
|
|
@ -3241,7 +3303,7 @@ def override_animate(method) -> types.FunctionType:
|
|||
|
||||
.. seealso::
|
||||
|
||||
:attr:`Mobject.animate`
|
||||
:attr:`~.Mobject.animate`
|
||||
|
||||
.. note::
|
||||
|
||||
|
|
|
|||
|
|
@ -2,16 +2,25 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["TrueDot", "DotCloud"]
|
||||
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim.constants import ORIGIN, RIGHT, UP
|
||||
from manim.mobject.opengl.opengl_point_cloud_mobject import OpenGLPMobject
|
||||
from manim.utils.color import YELLOW
|
||||
from manim.typing import Point3DLike
|
||||
from manim.utils.color import YELLOW, ParsableManimColor
|
||||
|
||||
|
||||
class DotCloud(OpenGLPMobject):
|
||||
def __init__(
|
||||
self, color=YELLOW, stroke_width=2.0, radius=2.0, density=10, **kwargs
|
||||
self,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
stroke_width: float = 2.0,
|
||||
radius: float = 2.0,
|
||||
density: float = 10,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.radius = radius
|
||||
self.epsilon = 1.0 / density
|
||||
|
|
@ -19,7 +28,7 @@ class DotCloud(OpenGLPMobject):
|
|||
stroke_width=stroke_width, density=density, color=color, **kwargs
|
||||
)
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
self.points = np.array(
|
||||
[
|
||||
r * (np.cos(theta) * RIGHT + np.sin(theta) * UP)
|
||||
|
|
@ -34,7 +43,7 @@ class DotCloud(OpenGLPMobject):
|
|||
dtype=np.float32,
|
||||
)
|
||||
|
||||
def make_3d(self, gloss=0.5, shadow=0.2):
|
||||
def make_3d(self, gloss: float = 0.5, shadow: float = 0.2) -> Self:
|
||||
self.set_gloss(gloss)
|
||||
self.set_shadow(shadow)
|
||||
self.apply_depth_test()
|
||||
|
|
@ -42,6 +51,8 @@ class DotCloud(OpenGLPMobject):
|
|||
|
||||
|
||||
class TrueDot(DotCloud):
|
||||
def __init__(self, center=ORIGIN, stroke_width=2.0, **kwargs):
|
||||
def __init__(
|
||||
self, center: Point3DLike = ORIGIN, stroke_width: float = 2.0, **kwargs: Any
|
||||
):
|
||||
self.radius = stroke_width
|
||||
super().__init__(points=[center], stroke_width=stroke_width, **kwargs)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class ConvertToOpenGL(ABCMeta):
|
|||
|
||||
_converted_classes = []
|
||||
|
||||
def __new__(mcls, name, bases, namespace): # noqa: B902
|
||||
def __new__(mcls, name, bases, namespace):
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
# Must check class names to prevent
|
||||
# cyclic importing.
|
||||
|
|
@ -40,6 +40,6 @@ class ConvertToOpenGL(ABCMeta):
|
|||
|
||||
return super().__new__(mcls, name, bases, namespace)
|
||||
|
||||
def __init__(cls, name, bases, namespace): # noqa: B902
|
||||
def __init__(cls, name, bases, namespace):
|
||||
super().__init__(name, bases, namespace)
|
||||
cls._converted_classes.append(cls)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,27 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any, cast
|
||||
|
||||
import numpy as np
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim.constants import *
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLDashedVMobject,
|
||||
OpenGLMobject,
|
||||
OpenGLVGroup,
|
||||
OpenGLVMobject,
|
||||
)
|
||||
from manim.typing import (
|
||||
Point3D,
|
||||
Point3D_Array,
|
||||
Point3DLike,
|
||||
QuadraticSpline,
|
||||
Vector2DLike,
|
||||
Vector3D,
|
||||
Vector3DLike,
|
||||
)
|
||||
from manim.utils.color import *
|
||||
from manim.utils.iterables import adjacent_n_tuples, adjacent_pairs
|
||||
from manim.utils.simple_functions import clip
|
||||
|
|
@ -77,17 +90,17 @@ class OpenGLTipableVMobject(OpenGLVMobject):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
tip_length=DEFAULT_ARROW_TIP_LENGTH,
|
||||
normal_vector=OUT,
|
||||
tip_config={},
|
||||
**kwargs,
|
||||
tip_length: float = DEFAULT_ARROW_TIP_LENGTH,
|
||||
normal_vector: Vector3DLike = OUT,
|
||||
tip_config: dict[str, Any] = {},
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.tip_length = tip_length
|
||||
self.normal_vector = normal_vector
|
||||
self.tip_config = tip_config
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def add_tip(self, at_start=False, **kwargs):
|
||||
def add_tip(self, at_start: bool = False, **kwargs: Any) -> Self:
|
||||
"""
|
||||
Adds a tip to the TipableVMobject instance, recognising
|
||||
that the endpoints might need to be switched if it's
|
||||
|
|
@ -99,7 +112,7 @@ class OpenGLTipableVMobject(OpenGLVMobject):
|
|||
self.add(tip)
|
||||
return self
|
||||
|
||||
def create_tip(self, at_start=False, **kwargs):
|
||||
def create_tip(self, at_start: bool = False, **kwargs: Any) -> OpenGLArrowTip:
|
||||
"""
|
||||
Stylises the tip, positions it spacially, and returns
|
||||
the newly instantiated tip to the caller.
|
||||
|
|
@ -108,7 +121,7 @@ class OpenGLTipableVMobject(OpenGLVMobject):
|
|||
self.position_tip(tip, at_start)
|
||||
return tip
|
||||
|
||||
def get_unpositioned_tip(self, **kwargs):
|
||||
def get_unpositioned_tip(self, **kwargs: Any) -> OpenGLArrowTip:
|
||||
"""
|
||||
Returns a tip that has been stylistically configured,
|
||||
but has not yet been given a position in space.
|
||||
|
|
@ -118,7 +131,9 @@ class OpenGLTipableVMobject(OpenGLVMobject):
|
|||
config.update(kwargs)
|
||||
return OpenGLArrowTip(**config)
|
||||
|
||||
def position_tip(self, tip, at_start=False):
|
||||
def position_tip(
|
||||
self, tip: OpenGLArrowTip, at_start: bool = False
|
||||
) -> OpenGLArrowTip:
|
||||
# Last two control points, defining both
|
||||
# the end, and the tangency direction
|
||||
if at_start:
|
||||
|
|
@ -131,7 +146,7 @@ class OpenGLTipableVMobject(OpenGLVMobject):
|
|||
tip.shift(anchor - tip.get_tip_point())
|
||||
return tip
|
||||
|
||||
def reset_endpoints_based_on_tip(self, tip, at_start):
|
||||
def reset_endpoints_based_on_tip(self, tip: OpenGLArrowTip, at_start: bool) -> Self:
|
||||
if self.get_length() == 0:
|
||||
# Zero length, put_start_and_end_on wouldn't
|
||||
# work
|
||||
|
|
@ -146,7 +161,7 @@ class OpenGLTipableVMobject(OpenGLVMobject):
|
|||
self.put_start_and_end_on(start, end)
|
||||
return self
|
||||
|
||||
def asign_tip_attr(self, tip, at_start):
|
||||
def asign_tip_attr(self, tip: OpenGLArrowTip, at_start: bool) -> Self:
|
||||
if at_start:
|
||||
self.start_tip = tip
|
||||
else:
|
||||
|
|
@ -154,14 +169,14 @@ class OpenGLTipableVMobject(OpenGLVMobject):
|
|||
return self
|
||||
|
||||
# Checking for tips
|
||||
def has_tip(self):
|
||||
def has_tip(self) -> bool:
|
||||
return hasattr(self, "tip") and self.tip in self
|
||||
|
||||
def has_start_tip(self):
|
||||
def has_start_tip(self) -> bool:
|
||||
return hasattr(self, "start_tip") and self.start_tip in self
|
||||
|
||||
# Getters
|
||||
def pop_tips(self):
|
||||
def pop_tips(self) -> OpenGLVGroup:
|
||||
start, end = self.get_start_and_end()
|
||||
result = OpenGLVGroup()
|
||||
if self.has_tip():
|
||||
|
|
@ -173,7 +188,7 @@ class OpenGLTipableVMobject(OpenGLVMobject):
|
|||
self.put_start_and_end_on(start, end)
|
||||
return result
|
||||
|
||||
def get_tips(self):
|
||||
def get_tips(self) -> OpenGLVGroup:
|
||||
"""
|
||||
Returns a VGroup (collection of VMobjects) containing
|
||||
the TipableVMObject instance's tips.
|
||||
|
|
@ -185,7 +200,7 @@ class OpenGLTipableVMobject(OpenGLVMobject):
|
|||
result.add(self.start_tip)
|
||||
return result
|
||||
|
||||
def get_tip(self):
|
||||
def get_tip(self) -> OpenGLArrowTip:
|
||||
"""Returns the TipableVMobject instance's (first) tip,
|
||||
otherwise throws an exception.
|
||||
"""
|
||||
|
|
@ -193,53 +208,55 @@ class OpenGLTipableVMobject(OpenGLVMobject):
|
|||
if len(tips) == 0:
|
||||
raise Exception("tip not found")
|
||||
else:
|
||||
return tips[0]
|
||||
rv = cast(OpenGLArrowTip, tips[0])
|
||||
return rv
|
||||
|
||||
def get_default_tip_length(self):
|
||||
def get_default_tip_length(self) -> float:
|
||||
return self.tip_length
|
||||
|
||||
def get_first_handle(self):
|
||||
def get_first_handle(self) -> Point3D:
|
||||
return self.points[1]
|
||||
|
||||
def get_last_handle(self):
|
||||
def get_last_handle(self) -> Point3D:
|
||||
return self.points[-2]
|
||||
|
||||
def get_end(self):
|
||||
def get_end(self) -> Point3D:
|
||||
if self.has_tip():
|
||||
return self.tip.get_start()
|
||||
else:
|
||||
return super().get_end()
|
||||
|
||||
def get_start(self):
|
||||
def get_start(self) -> Point3D:
|
||||
if self.has_start_tip():
|
||||
return self.start_tip.get_start()
|
||||
else:
|
||||
return super().get_start()
|
||||
|
||||
def get_length(self):
|
||||
def get_length(self) -> float:
|
||||
start, end = self.get_start_and_end()
|
||||
return np.linalg.norm(start - end)
|
||||
rv: float = np.linalg.norm(start - end)
|
||||
return rv
|
||||
|
||||
|
||||
class OpenGLArc(OpenGLTipableVMobject):
|
||||
def __init__(
|
||||
self,
|
||||
start_angle=0,
|
||||
angle=TAU / 4,
|
||||
radius=1.0,
|
||||
n_components=8,
|
||||
arc_center=ORIGIN,
|
||||
**kwargs,
|
||||
start_angle: float = 0,
|
||||
angle: float = TAU / 4,
|
||||
radius: float = 1.0,
|
||||
n_components: int = 8,
|
||||
arc_center: Point3DLike = ORIGIN,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.start_angle = start_angle
|
||||
self.angle = angle
|
||||
self.radius = radius
|
||||
self.n_components = n_components
|
||||
self.arc_center = arc_center
|
||||
super().__init__(self, **kwargs)
|
||||
super().__init__(**kwargs)
|
||||
self.orientation = -1
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
self.set_points(
|
||||
OpenGLArc.create_quadratic_bezier_points(
|
||||
angle=self.angle,
|
||||
|
|
@ -252,7 +269,9 @@ class OpenGLArc(OpenGLTipableVMobject):
|
|||
self.shift(self.arc_center)
|
||||
|
||||
@staticmethod
|
||||
def create_quadratic_bezier_points(angle, start_angle=0, n_components=8):
|
||||
def create_quadratic_bezier_points(
|
||||
angle: float, start_angle: float = 0, n_components: int = 8
|
||||
) -> QuadraticSpline:
|
||||
samples = np.array(
|
||||
[
|
||||
[np.cos(a), np.sin(a), 0]
|
||||
|
|
@ -272,7 +291,7 @@ class OpenGLArc(OpenGLTipableVMobject):
|
|||
points[2::3] = samples[2::2]
|
||||
return points
|
||||
|
||||
def get_arc_center(self):
|
||||
def get_arc_center(self) -> Point3D:
|
||||
"""
|
||||
Looks at the normals to the first two
|
||||
anchors, and finds their intersection points
|
||||
|
|
@ -287,21 +306,29 @@ class OpenGLArc(OpenGLTipableVMobject):
|
|||
n2 = rotate_vector(t2, TAU / 4)
|
||||
return find_intersection(a1, n1, a2, n2)
|
||||
|
||||
def get_start_angle(self):
|
||||
def get_start_angle(self) -> float:
|
||||
angle = angle_of_vector(self.get_start() - self.get_arc_center())
|
||||
return angle % TAU
|
||||
rv: float = angle % TAU
|
||||
return rv
|
||||
|
||||
def get_stop_angle(self):
|
||||
def get_stop_angle(self) -> float:
|
||||
angle = angle_of_vector(self.get_end() - self.get_arc_center())
|
||||
return angle % TAU
|
||||
rv: float = angle % TAU
|
||||
return rv
|
||||
|
||||
def move_arc_center_to(self, point):
|
||||
def move_arc_center_to(self, point: Point3DLike) -> Self:
|
||||
self.shift(point - self.get_arc_center())
|
||||
return self
|
||||
|
||||
|
||||
class OpenGLArcBetweenPoints(OpenGLArc):
|
||||
def __init__(self, start, end, angle=TAU / 4, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
start: Point3DLike,
|
||||
end: Point3DLike,
|
||||
angle: float = TAU / 4,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(angle=angle, **kwargs)
|
||||
if angle == 0:
|
||||
self.set_points_as_corners([LEFT, RIGHT])
|
||||
|
|
@ -309,30 +336,37 @@ class OpenGLArcBetweenPoints(OpenGLArc):
|
|||
|
||||
|
||||
class OpenGLCurvedArrow(OpenGLArcBetweenPoints):
|
||||
def __init__(self, start_point, end_point, **kwargs):
|
||||
def __init__(self, start_point: Point3DLike, end_point: Point3DLike, **kwargs: Any):
|
||||
super().__init__(start_point, end_point, **kwargs)
|
||||
self.add_tip()
|
||||
|
||||
|
||||
class OpenGLCurvedDoubleArrow(OpenGLCurvedArrow):
|
||||
def __init__(self, start_point, end_point, **kwargs):
|
||||
def __init__(self, start_point: Point3DLike, end_point: Point3DLike, **kwargs: Any):
|
||||
super().__init__(start_point, end_point, **kwargs)
|
||||
self.add_tip(at_start=True)
|
||||
|
||||
|
||||
class OpenGLCircle(OpenGLArc):
|
||||
def __init__(self, color=RED, **kwargs):
|
||||
def __init__(self, color: ParsableManimColor = RED, **kwargs: Any):
|
||||
super().__init__(0, TAU, color=color, **kwargs)
|
||||
|
||||
def surround(self, mobject, dim_to_match=0, stretch=False, buff=MED_SMALL_BUFF):
|
||||
def surround(
|
||||
self,
|
||||
mobject: OpenGLMobject,
|
||||
dim_to_match: int = 0,
|
||||
stretch: bool = False,
|
||||
buff: float = MED_SMALL_BUFF,
|
||||
) -> Self:
|
||||
# Ignores dim_to_match and stretch; result will always be a circle
|
||||
# TODO: Perhaps create an ellipse class to handle singele-dimension stretching
|
||||
|
||||
self.replace(mobject, dim_to_match, stretch)
|
||||
self.stretch((self.get_width() + 2 * buff) / self.get_width(), 0)
|
||||
self.stretch((self.get_height() + 2 * buff) / self.get_height(), 1)
|
||||
return self
|
||||
|
||||
def point_at_angle(self, angle):
|
||||
def point_at_angle(self, angle: float) -> Point3D:
|
||||
start_angle = self.get_start_angle()
|
||||
return self.point_from_proportion((angle - start_angle) / TAU)
|
||||
|
||||
|
|
@ -340,12 +374,12 @@ class OpenGLCircle(OpenGLArc):
|
|||
class OpenGLDot(OpenGLCircle):
|
||||
def __init__(
|
||||
self,
|
||||
point=ORIGIN,
|
||||
radius=DEFAULT_DOT_RADIUS,
|
||||
stroke_width=0,
|
||||
fill_opacity=1.0,
|
||||
color=WHITE,
|
||||
**kwargs,
|
||||
point: Point3DLike = ORIGIN,
|
||||
radius: float = DEFAULT_DOT_RADIUS,
|
||||
stroke_width: float = 0,
|
||||
fill_opacity: float = 1.0,
|
||||
color: ParsableManimColor = WHITE,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(
|
||||
arc_center=point,
|
||||
|
|
@ -358,7 +392,7 @@ class OpenGLDot(OpenGLCircle):
|
|||
|
||||
|
||||
class OpenGLEllipse(OpenGLCircle):
|
||||
def __init__(self, width=2, height=1, **kwargs):
|
||||
def __init__(self, width: float = 2, height: float = 1, **kwargs: Any):
|
||||
super().__init__(**kwargs)
|
||||
self.set_width(width, stretch=True)
|
||||
self.set_height(height, stretch=True)
|
||||
|
|
@ -367,14 +401,14 @@ class OpenGLEllipse(OpenGLCircle):
|
|||
class OpenGLAnnularSector(OpenGLArc):
|
||||
def __init__(
|
||||
self,
|
||||
inner_radius=1,
|
||||
outer_radius=2,
|
||||
angle=TAU / 4,
|
||||
start_angle=0,
|
||||
fill_opacity=1,
|
||||
stroke_width=0,
|
||||
color=WHITE,
|
||||
**kwargs,
|
||||
inner_radius: float = 1,
|
||||
outer_radius: float = 2,
|
||||
angle: float = TAU / 4,
|
||||
start_angle: float = 0,
|
||||
fill_opacity: float = 1,
|
||||
stroke_width: float = 0,
|
||||
color: ParsableManimColor = WHITE,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.inner_radius = inner_radius
|
||||
self.outer_radius = outer_radius
|
||||
|
|
@ -387,7 +421,7 @@ class OpenGLAnnularSector(OpenGLArc):
|
|||
**kwargs,
|
||||
)
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
inner_arc, outer_arc = (
|
||||
OpenGLArc(
|
||||
start_angle=self.start_angle,
|
||||
|
|
@ -405,20 +439,20 @@ class OpenGLAnnularSector(OpenGLArc):
|
|||
|
||||
|
||||
class OpenGLSector(OpenGLAnnularSector):
|
||||
def __init__(self, outer_radius=1, inner_radius=0, **kwargs):
|
||||
def __init__(self, outer_radius: float = 1, inner_radius: float = 0, **kwargs: Any):
|
||||
super().__init__(inner_radius=inner_radius, outer_radius=outer_radius, **kwargs)
|
||||
|
||||
|
||||
class OpenGLAnnulus(OpenGLCircle):
|
||||
def __init__(
|
||||
self,
|
||||
inner_radius=1,
|
||||
outer_radius=2,
|
||||
fill_opacity=1,
|
||||
stroke_width=0,
|
||||
color=WHITE,
|
||||
mark_paths_closed=False,
|
||||
**kwargs,
|
||||
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: Any,
|
||||
):
|
||||
self.mark_paths_closed = mark_paths_closed # is this even used?
|
||||
self.inner_radius = inner_radius
|
||||
|
|
@ -427,7 +461,7 @@ class OpenGLAnnulus(OpenGLCircle):
|
|||
fill_opacity=fill_opacity, stroke_width=stroke_width, color=color, **kwargs
|
||||
)
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
self.radius = self.outer_radius
|
||||
outer_circle = OpenGLCircle(radius=self.outer_radius)
|
||||
inner_circle = OpenGLCircle(radius=self.inner_radius)
|
||||
|
|
@ -438,17 +472,26 @@ class OpenGLAnnulus(OpenGLCircle):
|
|||
|
||||
|
||||
class OpenGLLine(OpenGLTipableVMobject):
|
||||
def __init__(self, start=LEFT, end=RIGHT, buff=0, path_arc=0, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
start: Point3DLike = LEFT,
|
||||
end: Point3DLike = RIGHT,
|
||||
buff: float = 0,
|
||||
path_arc: float = 0,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.dim = 3
|
||||
self.buff = buff
|
||||
self.path_arc = path_arc
|
||||
self.set_start_and_end_attrs(start, end)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
self.set_points_by_ends(self.start, self.end, self.buff, self.path_arc)
|
||||
|
||||
def set_points_by_ends(self, start, end, buff=0, path_arc=0):
|
||||
def set_points_by_ends(
|
||||
self, start: Point3DLike, end: Point3DLike, buff: float = 0, path_arc: float = 0
|
||||
) -> None:
|
||||
if path_arc:
|
||||
self.set_points(OpenGLArc.create_quadratic_bezier_points(path_arc))
|
||||
self.put_start_and_end_on(start, end)
|
||||
|
|
@ -456,23 +499,25 @@ class OpenGLLine(OpenGLTipableVMobject):
|
|||
self.set_points_as_corners([start, end])
|
||||
self.account_for_buff(self.buff)
|
||||
|
||||
def set_path_arc(self, new_value):
|
||||
def set_path_arc(self, new_value: float) -> None:
|
||||
self.path_arc = new_value
|
||||
self.init_points()
|
||||
|
||||
def account_for_buff(self, buff):
|
||||
def account_for_buff(self, buff: float) -> Self:
|
||||
if buff == 0:
|
||||
return
|
||||
return self
|
||||
#
|
||||
length = self.get_length() if self.path_arc == 0 else self.get_arc_length()
|
||||
#
|
||||
if length < 2 * buff:
|
||||
return
|
||||
return self
|
||||
buff_prop = buff / length
|
||||
self.pointwise_become_partial(self, buff_prop, 1 - buff_prop)
|
||||
return self
|
||||
|
||||
def set_start_and_end_attrs(self, start, end):
|
||||
def set_start_and_end_attrs(
|
||||
self, start: Mobject | Point3DLike, end: Mobject | Point3DLike
|
||||
) -> None:
|
||||
# If either start or end are Mobjects, this
|
||||
# gives their centers
|
||||
rough_start = self.pointify(start)
|
||||
|
|
@ -484,7 +529,9 @@ class OpenGLLine(OpenGLTipableVMobject):
|
|||
self.start = self.pointify(start, vect) + self.buff * vect
|
||||
self.end = self.pointify(end, -vect) - self.buff * vect
|
||||
|
||||
def pointify(self, mob_or_point, direction=None):
|
||||
def pointify(
|
||||
self, mob_or_point: Mobject | Point3DLike, direction: Vector3DLike = None
|
||||
) -> Point3D:
|
||||
"""
|
||||
Take an argument passed into Line (or subclass) and turn
|
||||
it into a 3d point.
|
||||
|
|
@ -501,31 +548,32 @@ class OpenGLLine(OpenGLTipableVMobject):
|
|||
result[: len(point)] = point
|
||||
return result
|
||||
|
||||
def put_start_and_end_on(self, start, end):
|
||||
def put_start_and_end_on(self, start: Point3DLike, end: Point3DLike) -> Self:
|
||||
curr_start, curr_end = self.get_start_and_end()
|
||||
if (curr_start == curr_end).all():
|
||||
self.set_points_by_ends(start, end, self.path_arc)
|
||||
return super().put_start_and_end_on(start, end)
|
||||
|
||||
def get_vector(self):
|
||||
def get_vector(self) -> Vector3D:
|
||||
return self.get_end() - self.get_start()
|
||||
|
||||
def get_unit_vector(self):
|
||||
def get_unit_vector(self) -> Vector3D:
|
||||
return normalize(self.get_vector())
|
||||
|
||||
def get_angle(self):
|
||||
def get_angle(self) -> float:
|
||||
return angle_of_vector(self.get_vector())
|
||||
|
||||
def get_projection(self, point):
|
||||
def get_projection(self, point: Point3DLike) -> Point3D:
|
||||
"""Return projection of a point onto the line"""
|
||||
unit_vect = self.get_unit_vector()
|
||||
start = self.get_start()
|
||||
return start + np.dot(point - start, unit_vect) * unit_vect
|
||||
|
||||
def get_slope(self):
|
||||
return np.tan(self.get_angle())
|
||||
def get_slope(self) -> float:
|
||||
rv: float = np.tan(self.get_angle())
|
||||
return rv
|
||||
|
||||
def set_angle(self, angle, about_point=None):
|
||||
def set_angle(self, angle: float, about_point: Point3DLike | None = None) -> Self:
|
||||
if about_point is None:
|
||||
about_point = self.get_start()
|
||||
self.rotate(
|
||||
|
|
@ -534,13 +582,17 @@ class OpenGLLine(OpenGLTipableVMobject):
|
|||
)
|
||||
return self
|
||||
|
||||
def set_length(self, length):
|
||||
def set_length(self, length: float) -> None:
|
||||
self.scale(length / self.get_length())
|
||||
|
||||
|
||||
class OpenGLDashedLine(OpenGLLine):
|
||||
def __init__(
|
||||
self, *args, dash_length=DEFAULT_DASH_LENGTH, dashed_ratio=0.5, **kwargs
|
||||
self,
|
||||
*args: Any,
|
||||
dash_length: float = DEFAULT_DASH_LENGTH,
|
||||
dashed_ratio: float = 0.5,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.dashed_ratio = dashed_ratio
|
||||
self.dash_length = dash_length
|
||||
|
|
@ -555,33 +607,40 @@ class OpenGLDashedLine(OpenGLLine):
|
|||
self.clear_points()
|
||||
self.add(*dashes)
|
||||
|
||||
def calculate_num_dashes(self, dashed_ratio):
|
||||
def calculate_num_dashes(self, dashed_ratio: float) -> int:
|
||||
return max(
|
||||
2,
|
||||
int(np.ceil((self.get_length() / self.dash_length) * dashed_ratio)),
|
||||
)
|
||||
|
||||
def get_start(self):
|
||||
def get_start(self) -> Point3D:
|
||||
if len(self.submobjects) > 0:
|
||||
return self.submobjects[0].get_start()
|
||||
else:
|
||||
return super().get_start()
|
||||
|
||||
def get_end(self):
|
||||
def get_end(self) -> Point3D:
|
||||
if len(self.submobjects) > 0:
|
||||
return self.submobjects[-1].get_end()
|
||||
else:
|
||||
return super().get_end()
|
||||
|
||||
def get_first_handle(self):
|
||||
def get_first_handle(self) -> Point3D:
|
||||
return self.submobjects[0].points[1]
|
||||
|
||||
def get_last_handle(self):
|
||||
def get_last_handle(self) -> Point3D:
|
||||
return self.submobjects[-1].points[-2]
|
||||
|
||||
|
||||
class OpenGLTangentLine(OpenGLLine):
|
||||
def __init__(self, vmob, alpha, length=1, d_alpha=1e-6, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
vmob: OpenGLVMobject,
|
||||
alpha: float,
|
||||
length: float = 1,
|
||||
d_alpha: float = 1e-6,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.length = length
|
||||
self.d_alpha = d_alpha
|
||||
da = self.d_alpha
|
||||
|
|
@ -592,7 +651,7 @@ class OpenGLTangentLine(OpenGLLine):
|
|||
|
||||
|
||||
class OpenGLElbow(OpenGLVMobject):
|
||||
def __init__(self, width=0.2, angle=0, **kwargs):
|
||||
def __init__(self, width: float = 0.2, angle: float = 0, **kwargs: Any):
|
||||
self.angle = angle
|
||||
super().__init__(self, **kwargs)
|
||||
self.set_points_as_corners([UP, UP + RIGHT, RIGHT])
|
||||
|
|
@ -603,19 +662,19 @@ class OpenGLElbow(OpenGLVMobject):
|
|||
class OpenGLArrow(OpenGLLine):
|
||||
def __init__(
|
||||
self,
|
||||
start=LEFT,
|
||||
end=RIGHT,
|
||||
path_arc=0,
|
||||
fill_color=GREY_A,
|
||||
fill_opacity=1,
|
||||
stroke_width=0,
|
||||
buff=MED_SMALL_BUFF,
|
||||
thickness=0.05,
|
||||
tip_width_ratio=5,
|
||||
tip_angle=PI / 3,
|
||||
max_tip_length_to_length_ratio=0.5,
|
||||
max_width_to_length_ratio=0.1,
|
||||
**kwargs,
|
||||
start: Point3DLike = LEFT,
|
||||
end: Point3DLike = RIGHT,
|
||||
path_arc: float = 0,
|
||||
fill_color: ParsableManimColor = GREY_A,
|
||||
fill_opacity: float = 1,
|
||||
stroke_width: float = 0,
|
||||
buff: float = MED_SMALL_BUFF,
|
||||
thickness: float = 0.05,
|
||||
tip_width_ratio: float = 5,
|
||||
tip_angle: float = PI / 3,
|
||||
max_tip_length_to_length_ratio: float = 0.5,
|
||||
max_width_to_length_ratio: float = 0.1,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.thickness = thickness
|
||||
self.tip_width_ratio = tip_width_ratio
|
||||
|
|
@ -633,9 +692,11 @@ class OpenGLArrow(OpenGLLine):
|
|||
**kwargs,
|
||||
)
|
||||
|
||||
def set_points_by_ends(self, start, end, buff=0, path_arc=0):
|
||||
def set_points_by_ends(
|
||||
self, start: Point3DLike, end: Point3DLike, buff: float = 0, path_arc: float = 0
|
||||
) -> None:
|
||||
# Find the right tip length and thickness
|
||||
vect = end - start
|
||||
vect = np.asarray(end) - np.asarray(start)
|
||||
length = max(np.linalg.norm(vect), 1e-8)
|
||||
thickness = self.thickness
|
||||
w_ratio = self.max_width_to_length_ratio / (thickness / length)
|
||||
|
|
@ -696,7 +757,7 @@ class OpenGLArrow(OpenGLLine):
|
|||
self.shift(start - self.get_start())
|
||||
self.refresh_triangulation()
|
||||
|
||||
def reset_points_around_ends(self):
|
||||
def reset_points_around_ends(self) -> Self:
|
||||
self.set_points_by_ends(
|
||||
self.get_start(),
|
||||
self.get_end(),
|
||||
|
|
@ -704,36 +765,41 @@ class OpenGLArrow(OpenGLLine):
|
|||
)
|
||||
return self
|
||||
|
||||
def get_start(self):
|
||||
def get_start(self) -> Point3D:
|
||||
nppc = self.n_points_per_curve
|
||||
points = self.points
|
||||
return (points[0] + points[-nppc]) / 2
|
||||
|
||||
def get_end(self):
|
||||
def get_end(self) -> Point3D:
|
||||
return self.points[self.tip_index]
|
||||
|
||||
def put_start_and_end_on(self, start, end):
|
||||
def put_start_and_end_on(self, start: Point3DLike, end: Point3DLike) -> Self:
|
||||
self.set_points_by_ends(start, end, buff=0, path_arc=self.path_arc)
|
||||
return self
|
||||
|
||||
def scale(self, *args, **kwargs):
|
||||
def scale(self, *args: Any, **kwargs: Any) -> Self:
|
||||
super().scale(*args, **kwargs)
|
||||
self.reset_points_around_ends()
|
||||
return self
|
||||
|
||||
def set_thickness(self, thickness):
|
||||
def set_thickness(self, thickness: float) -> Self:
|
||||
self.thickness = thickness
|
||||
self.reset_points_around_ends()
|
||||
return self
|
||||
|
||||
def set_path_arc(self, path_arc):
|
||||
def set_path_arc(self, path_arc: float) -> None:
|
||||
self.path_arc = path_arc
|
||||
self.reset_points_around_ends()
|
||||
return self
|
||||
# return self
|
||||
|
||||
|
||||
class OpenGLVector(OpenGLArrow):
|
||||
def __init__(self, direction=RIGHT, buff=0, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
direction: Vector2DLike | Vector3DLike = RIGHT,
|
||||
buff: float = 0,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.buff = buff
|
||||
if len(direction) == 2:
|
||||
direction = np.hstack([direction, 0])
|
||||
|
|
@ -741,30 +807,37 @@ class OpenGLVector(OpenGLArrow):
|
|||
|
||||
|
||||
class OpenGLDoubleArrow(OpenGLArrow):
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args: Any, **kwargs: Any):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.add_tip(at_start=True)
|
||||
|
||||
|
||||
class OpenGLCubicBezier(OpenGLVMobject):
|
||||
def __init__(self, a0, h0, h1, a1, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
a0: Point3DLike,
|
||||
h0: Point3DLike,
|
||||
h1: Point3DLike,
|
||||
a1: Point3DLike,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.add_cubic_bezier_curve(a0, h0, h1, a1)
|
||||
|
||||
|
||||
class OpenGLPolygon(OpenGLVMobject):
|
||||
def __init__(self, *vertices, **kwargs):
|
||||
self.vertices = vertices
|
||||
def __init__(self, *vertices: Point3DLike, **kwargs: Any):
|
||||
self.vertices: Point3D_Array = np.array(vertices)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
verts = self.vertices
|
||||
self.set_points_as_corners([*verts, verts[0]])
|
||||
|
||||
def get_vertices(self):
|
||||
def get_vertices(self) -> Point3D_Array:
|
||||
return self.get_start_anchors()
|
||||
|
||||
def round_corners(self, radius=0.5):
|
||||
def round_corners(self, radius: float = 0.5) -> Self:
|
||||
vertices = self.get_vertices()
|
||||
arcs = []
|
||||
for v1, v2, v3 in adjacent_n_tuples(vertices, 3):
|
||||
|
|
@ -801,7 +874,7 @@ class OpenGLPolygon(OpenGLVMobject):
|
|||
|
||||
|
||||
class OpenGLRegularPolygon(OpenGLPolygon):
|
||||
def __init__(self, n=6, start_angle=None, **kwargs):
|
||||
def __init__(self, n: int = 6, start_angle: float | None = None, **kwargs: Any):
|
||||
self.start_angle = start_angle
|
||||
if self.start_angle is None:
|
||||
if n % 2 == 0:
|
||||
|
|
@ -814,20 +887,20 @@ class OpenGLRegularPolygon(OpenGLPolygon):
|
|||
|
||||
|
||||
class OpenGLTriangle(OpenGLRegularPolygon):
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, **kwargs: Any):
|
||||
super().__init__(n=3, **kwargs)
|
||||
|
||||
|
||||
class OpenGLArrowTip(OpenGLTriangle):
|
||||
def __init__(
|
||||
self,
|
||||
fill_opacity=1,
|
||||
fill_color=WHITE,
|
||||
stroke_width=0,
|
||||
width=DEFAULT_ARROW_TIP_WIDTH,
|
||||
length=DEFAULT_ARROW_TIP_LENGTH,
|
||||
angle=0,
|
||||
**kwargs,
|
||||
fill_opacity: float = 1,
|
||||
fill_color: ParsableManimColor = WHITE,
|
||||
stroke_width: float = 0,
|
||||
width: float = DEFAULT_ARROW_TIP_WIDTH,
|
||||
length: float = DEFAULT_ARROW_TIP_LENGTH,
|
||||
angle: float = 0,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(
|
||||
start_angle=0,
|
||||
|
|
@ -839,24 +912,31 @@ class OpenGLArrowTip(OpenGLTriangle):
|
|||
self.set_width(width, stretch=True)
|
||||
self.set_height(length, stretch=True)
|
||||
|
||||
def get_base(self):
|
||||
def get_base(self) -> Point3D:
|
||||
return self.point_from_proportion(0.5)
|
||||
|
||||
def get_tip_point(self):
|
||||
def get_tip_point(self) -> Point3D:
|
||||
return self.points[0]
|
||||
|
||||
def get_vector(self):
|
||||
def get_vector(self) -> Vector3D:
|
||||
return self.get_tip_point() - self.get_base()
|
||||
|
||||
def get_angle(self):
|
||||
def get_angle(self) -> float:
|
||||
return angle_of_vector(self.get_vector())
|
||||
|
||||
def get_length(self):
|
||||
return np.linalg.norm(self.get_vector())
|
||||
def get_length(self) -> float:
|
||||
rv: float = np.linalg.norm(self.get_vector())
|
||||
return rv
|
||||
|
||||
|
||||
class OpenGLRectangle(OpenGLPolygon):
|
||||
def __init__(self, color=WHITE, width=4.0, height=2.0, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
color: ParsableManimColor = WHITE,
|
||||
width: float = 4.0,
|
||||
height: float = 2.0,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(UR, UL, DL, DR, color=color, **kwargs)
|
||||
|
||||
self.set_width(width, stretch=True)
|
||||
|
|
@ -864,14 +944,14 @@ class OpenGLRectangle(OpenGLPolygon):
|
|||
|
||||
|
||||
class OpenGLSquare(OpenGLRectangle):
|
||||
def __init__(self, side_length=2.0, **kwargs):
|
||||
def __init__(self, side_length: float = 2.0, **kwargs: Any):
|
||||
self.side_length = side_length
|
||||
|
||||
super().__init__(height=side_length, width=side_length, **kwargs)
|
||||
|
||||
|
||||
class OpenGLRoundedRectangle(OpenGLRectangle):
|
||||
def __init__(self, corner_radius=0.5, **kwargs):
|
||||
def __init__(self, corner_radius: float = 0.5, **kwargs: Any):
|
||||
self.corner_radius = corner_radius
|
||||
super().__init__(**kwargs)
|
||||
self.round_corners(self.corner_radius)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -2,16 +2,35 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["OpenGLPMobject", "OpenGLPGroup", "OpenGLPMPoint"]
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import moderngl
|
||||
import numpy as np
|
||||
|
||||
from manim.constants import *
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.utils.bezier import interpolate
|
||||
from manim.utils.color import BLACK, WHITE, YELLOW, color_gradient, color_to_rgba
|
||||
from manim.utils.color import (
|
||||
BLACK,
|
||||
WHITE,
|
||||
YELLOW,
|
||||
ParsableManimColor,
|
||||
color_gradient,
|
||||
color_to_rgba,
|
||||
)
|
||||
from manim.utils.config_ops import _Uniforms
|
||||
from manim.utils.iterables import resize_with_interpolation
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim.typing import (
|
||||
FloatRGBA_Array,
|
||||
FloatRGBALike_Array,
|
||||
Point3D_Array,
|
||||
Point3DLike_Array,
|
||||
)
|
||||
|
||||
__all__ = ["OpenGLPMobject", "OpenGLPGroup", "OpenGLPMPoint"]
|
||||
|
||||
|
||||
|
|
@ -27,7 +46,11 @@ class OpenGLPMobject(OpenGLMobject):
|
|||
point_radius = _Uniforms()
|
||||
|
||||
def __init__(
|
||||
self, stroke_width=2.0, color=YELLOW, render_primitive=moderngl.POINTS, **kwargs
|
||||
self,
|
||||
stroke_width: float = 2.0,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
render_primitive: int = moderngl.POINTS,
|
||||
**kwargs,
|
||||
):
|
||||
self.stroke_width = stroke_width
|
||||
super().__init__(color=color, render_primitive=render_primitive, **kwargs)
|
||||
|
|
@ -35,15 +58,21 @@ class OpenGLPMobject(OpenGLMobject):
|
|||
self.stroke_width * OpenGLPMobject.OPENGL_POINT_RADIUS_SCALE_FACTOR
|
||||
)
|
||||
|
||||
def reset_points(self):
|
||||
self.rgbas = np.zeros((1, 4))
|
||||
self.points = np.zeros((0, 3))
|
||||
def reset_points(self) -> Self:
|
||||
self.rgbas: FloatRGBA_Array = np.zeros((1, 4))
|
||||
self.points: Point3D_Array = np.zeros((0, 3))
|
||||
return self
|
||||
|
||||
def get_array_attrs(self):
|
||||
return ["points", "rgbas"]
|
||||
|
||||
def add_points(self, points, rgbas=None, color=None, opacity=None):
|
||||
def add_points(
|
||||
self,
|
||||
points: Point3DLike_Array,
|
||||
rgbas: FloatRGBALike_Array | None = None,
|
||||
color: ParsableManimColor | None = None,
|
||||
opacity: float | None = None,
|
||||
) -> Self:
|
||||
"""Add points.
|
||||
|
||||
Points must be a Nx3 numpy array.
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from PIL import Image
|
|||
|
||||
from manim.constants import *
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.typing import Point3D_Array, Vector3D_Array
|
||||
from manim.utils.bezier import integer_interpolate, interpolate
|
||||
from manim.utils.color import *
|
||||
from manim.utils.config_ops import _Data, _Uniforms
|
||||
|
|
@ -160,12 +161,14 @@ class OpenGLSurface(OpenGLMobject):
|
|||
def get_triangle_indices(self):
|
||||
return self.triangle_indices
|
||||
|
||||
def get_surface_points_and_nudged_points(self):
|
||||
def get_surface_points_and_nudged_points(
|
||||
self,
|
||||
) -> tuple[Point3D_Array, Point3D_Array, Point3D_Array]:
|
||||
points = self.points
|
||||
k = len(points) // 3
|
||||
return points[:k], points[k : 2 * k], points[2 * k :]
|
||||
|
||||
def get_unit_normals(self):
|
||||
def get_unit_normals(self) -> Vector3D_Array:
|
||||
s_points, du_points, dv_points = self.get_surface_points_and_nudged_points()
|
||||
normals = np.cross(
|
||||
(du_points - s_points) / self.epsilon,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.mobject.opengl.opengl_surface import OpenGLSurface
|
||||
|
|
@ -11,13 +13,13 @@ __all__ = ["OpenGLSurfaceMesh"]
|
|||
class OpenGLSurfaceMesh(OpenGLVGroup):
|
||||
def __init__(
|
||||
self,
|
||||
uv_surface,
|
||||
resolution=None,
|
||||
stroke_width=1,
|
||||
normal_nudge=1e-2,
|
||||
depth_test=True,
|
||||
flat_stroke=False,
|
||||
**kwargs,
|
||||
uv_surface: OpenGLSurface,
|
||||
resolution: tuple[int, int] | None = None,
|
||||
stroke_width: float = 1,
|
||||
normal_nudge: float = 1e-2,
|
||||
depth_test: bool = True,
|
||||
flat_stroke: bool = False,
|
||||
**kwargs: Any,
|
||||
):
|
||||
if not isinstance(uv_surface, OpenGLSurface):
|
||||
raise Exception("uv_surface must be of type OpenGLSurface")
|
||||
|
|
@ -31,7 +33,7 @@ class OpenGLSurfaceMesh(OpenGLVGroup):
|
|||
**kwargs,
|
||||
)
|
||||
|
||||
def init_points(self):
|
||||
def init_points(self) -> None:
|
||||
uv_surface = self.uv_surface
|
||||
|
||||
full_nu, full_nv = uv_surface.resolution
|
||||
|
|
|
|||
|
|
@ -2,17 +2,19 @@ from __future__ import annotations
|
|||
|
||||
import itertools as it
|
||||
import operator as op
|
||||
from collections.abc import Iterable, Sequence
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from functools import reduce, wraps
|
||||
from typing import Callable
|
||||
from typing import Any
|
||||
|
||||
import moderngl
|
||||
import numpy as np
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim import config
|
||||
from manim.constants import *
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject, OpenGLPoint
|
||||
from manim.renderer.shader_wrapper import ShaderWrapper
|
||||
from manim.typing import Point3D, Point3DLike, Point3DLike_Array
|
||||
from manim.utils.bezier import (
|
||||
bezier,
|
||||
bezier_remap,
|
||||
|
|
@ -83,6 +85,9 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
stroke_shader_folder = "quadratic_bezier_stroke"
|
||||
fill_shader_folder = "quadratic_bezier_fill"
|
||||
|
||||
# TODO: although these are called "rgba" in singular, they are used as
|
||||
# FloatRGBA_Arrays and should be called instead "rgbas" in plural for consistency.
|
||||
# The same should probably apply for "stroke_width" and "unit_normal".
|
||||
fill_rgba = _Data()
|
||||
stroke_rgba = _Data()
|
||||
stroke_width = _Data()
|
||||
|
|
@ -171,6 +176,15 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
def get_mobject_type_class():
|
||||
return OpenGLVMobject
|
||||
|
||||
@property
|
||||
def submobjects(self) -> Sequence[OpenGLVMobject]:
|
||||
return self._submobjects if hasattr(self, "_submobjects") else []
|
||||
|
||||
@submobjects.setter
|
||||
def submobjects(self, submobject_list: Iterable[OpenGLVMobject]) -> None:
|
||||
self.remove(*self.submobjects)
|
||||
self.add(*submobject_list)
|
||||
|
||||
def init_data(self):
|
||||
super().init_data()
|
||||
self.data.pop("rgbas")
|
||||
|
|
@ -269,7 +283,10 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
|
||||
if width is not None:
|
||||
for mob in self.get_family(recurse):
|
||||
mob.stroke_width = np.array([[width] for width in tuplify(width)])
|
||||
if isinstance(width, np.ndarray):
|
||||
mob.stroke_width = width
|
||||
else:
|
||||
mob.stroke_width = np.array([[width] for width in tuplify(width)])
|
||||
|
||||
if background is not None:
|
||||
for mob in self.get_family(recurse):
|
||||
|
|
@ -462,7 +479,13 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
self.append_points([point])
|
||||
return self
|
||||
|
||||
def add_cubic_bezier_curve(self, anchor1, handle1, handle2, anchor2):
|
||||
def add_cubic_bezier_curve(
|
||||
self,
|
||||
anchor1: Point3DLike,
|
||||
handle1: Point3DLike,
|
||||
handle2: Point3DLike,
|
||||
anchor2: Point3DLike,
|
||||
):
|
||||
new_points = get_quadratic_approximation_of_cubic(
|
||||
anchor1,
|
||||
handle1,
|
||||
|
|
@ -571,7 +594,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
self.add_line_to(point)
|
||||
return points
|
||||
|
||||
def set_points_as_corners(self, points: Iterable[float]) -> OpenGLVMobject:
|
||||
def set_points_as_corners(self, points: Point3DLike_Array) -> OpenGLVMobject:
|
||||
"""Given an array of points, set them as corner of the vmobject.
|
||||
|
||||
To achieve that, this algorithm sets handles aligned with the anchors such that the resultant bezier curve will be the segment
|
||||
|
|
@ -594,7 +617,9 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
)
|
||||
return self
|
||||
|
||||
def set_points_smoothly(self, points, true_smooth=False):
|
||||
def set_points_smoothly(
|
||||
self, points: Point3DLike_Array, true_smooth: bool = False
|
||||
) -> Self:
|
||||
self.set_points_as_corners(points)
|
||||
self.make_smooth()
|
||||
return self
|
||||
|
|
@ -922,7 +947,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
for n in range(num_curves):
|
||||
yield self.get_nth_curve_function_with_length(n, **kwargs)
|
||||
|
||||
def point_from_proportion(self, alpha: float) -> np.ndarray:
|
||||
def point_from_proportion(self, alpha: float) -> Point3D:
|
||||
"""Gets the point at a proportion along the path of the :class:`OpenGLVMobject`.
|
||||
|
||||
Parameters
|
||||
|
|
@ -932,7 +957,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
|
||||
Returns
|
||||
-------
|
||||
:class:`numpy.ndarray`
|
||||
:class:`Point3D`
|
||||
The point on the :class:`OpenGLVMobject`.
|
||||
|
||||
Raises
|
||||
|
|
@ -969,7 +994,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
|
||||
def proportion_from_point(
|
||||
self,
|
||||
point: Iterable[float | int],
|
||||
point: Point3DLike,
|
||||
) -> float:
|
||||
"""Returns the proportion along the path of the :class:`OpenGLVMobject`
|
||||
a particular given point is at.
|
||||
|
|
@ -1375,7 +1400,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
|
||||
# Related to triangulation
|
||||
|
||||
def refresh_triangulation(self):
|
||||
def refresh_triangulation(self) -> Self:
|
||||
for mob in self.get_family():
|
||||
mob.needs_new_triangulation = True
|
||||
return self
|
||||
|
|
@ -1654,7 +1679,7 @@ class OpenGLVGroup(OpenGLVMobject):
|
|||
self.add(circles_group)
|
||||
"""
|
||||
|
||||
def __init__(self, *vmobjects, **kwargs):
|
||||
def __init__(self, *vmobjects: OpenGLVMobject, **kwargs: Any):
|
||||
super().__init__(**kwargs)
|
||||
self.add(*vmobjects)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,19 +4,21 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["Brace", "BraceLabel", "ArcBrace", "BraceText", "BraceBetweenPoints"]
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
import svgelements as se
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim._config import config
|
||||
from manim.mobject.geometry.arc import Arc
|
||||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.text.tex_mobject import MathTex, Tex
|
||||
from manim.mobject.text.tex_mobject import MathTex, SingleStringMathTex, Tex
|
||||
from manim.mobject.text.text_mobject import Text
|
||||
|
||||
from ...animation.animation import Animation
|
||||
from ...animation.composition import AnimationGroup
|
||||
from ...animation.fading import FadeIn
|
||||
from ...animation.growing import GrowFromCenter
|
||||
|
|
@ -26,11 +28,9 @@ from ...utils.color import BLACK
|
|||
from ..svg.svg_mobject import VMobjectFromSVGPath
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.typing import Point3DLike, Vector3D
|
||||
from manim.typing import Point3D, Point3DLike, Vector3D, Vector3DLike
|
||||
from manim.utils.color.core import ParsableManimColor
|
||||
|
||||
__all__ = ["Brace", "BraceBetweenPoints", "BraceLabel", "ArcBrace"]
|
||||
|
||||
|
||||
class Brace(VMobjectFromSVGPath):
|
||||
"""Takes a mobject and draws a brace adjacent to it.
|
||||
|
|
@ -70,14 +70,14 @@ class Brace(VMobjectFromSVGPath):
|
|||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
direction: Vector3D | None = DOWN,
|
||||
direction: Vector3DLike = DOWN,
|
||||
buff: float = 0.2,
|
||||
sharpness: float = 2,
|
||||
stroke_width: float = 0,
|
||||
fill_opacity: float = 1.0,
|
||||
background_stroke_width: float = 0,
|
||||
background_stroke_color: ParsableManimColor = BLACK,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
path_string_template = (
|
||||
"m0.01216 0c-0.01152 0-0.01216 6.103e-4 -0.01216 0.01311v0.007762c0.06776 "
|
||||
|
|
@ -130,7 +130,7 @@ class Brace(VMobjectFromSVGPath):
|
|||
for mob in mobject, self:
|
||||
mob.rotate(angle, about_point=ORIGIN)
|
||||
|
||||
def put_at_tip(self, mob: Mobject, use_next_to: bool = True, **kwargs):
|
||||
def put_at_tip(self, mob: Mobject, use_next_to: bool = True, **kwargs: Any) -> Self:
|
||||
"""Puts the given mobject at the brace tip.
|
||||
|
||||
Parameters
|
||||
|
|
@ -153,7 +153,7 @@ class Brace(VMobjectFromSVGPath):
|
|||
mob.shift(self.get_direction() * shift_distance)
|
||||
return self
|
||||
|
||||
def get_text(self, *text, **kwargs):
|
||||
def get_text(self, *text: str, **kwargs: Any) -> Tex:
|
||||
"""Places the text at the brace tip.
|
||||
|
||||
Parameters
|
||||
|
|
@ -172,7 +172,7 @@ class Brace(VMobjectFromSVGPath):
|
|||
self.put_at_tip(text_mob, **kwargs)
|
||||
return text_mob
|
||||
|
||||
def get_tex(self, *tex, **kwargs):
|
||||
def get_tex(self, *tex: str, **kwargs: Any) -> MathTex:
|
||||
"""Places the tex at the brace tip.
|
||||
|
||||
Parameters
|
||||
|
|
@ -191,7 +191,7 @@ class Brace(VMobjectFromSVGPath):
|
|||
self.put_at_tip(tex_mob, **kwargs)
|
||||
return tex_mob
|
||||
|
||||
def get_tip(self):
|
||||
def get_tip(self) -> Point3D:
|
||||
"""Returns the point at the brace tip."""
|
||||
# Returns the position of the seventh point in the path, which is the tip.
|
||||
if config["renderer"] == "opengl":
|
||||
|
|
@ -199,7 +199,7 @@ class Brace(VMobjectFromSVGPath):
|
|||
|
||||
return self.points[28] # = 7*4
|
||||
|
||||
def get_direction(self):
|
||||
def get_direction(self) -> Vector3D:
|
||||
"""Returns the direction from the center to the brace tip."""
|
||||
vect = self.get_tip() - self.get_center()
|
||||
return vect / np.linalg.norm(vect)
|
||||
|
|
@ -233,12 +233,12 @@ class BraceLabel(VMobject, metaclass=ConvertToOpenGL):
|
|||
self,
|
||||
obj: Mobject,
|
||||
text: str,
|
||||
brace_direction: np.ndarray = DOWN,
|
||||
label_constructor: type = MathTex,
|
||||
brace_direction: Vector3DLike = DOWN,
|
||||
label_constructor: type[SingleStringMathTex | Text] = MathTex,
|
||||
font_size: float = DEFAULT_FONT_SIZE,
|
||||
buff: float = 0.2,
|
||||
brace_config: dict | None = None,
|
||||
**kwargs,
|
||||
brace_config: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.label_constructor = label_constructor
|
||||
super().__init__(**kwargs)
|
||||
|
|
@ -249,37 +249,94 @@ class BraceLabel(VMobject, metaclass=ConvertToOpenGL):
|
|||
self.brace = Brace(obj, brace_direction, buff, **brace_config)
|
||||
|
||||
if isinstance(text, (tuple, list)):
|
||||
self.label = self.label_constructor(*text, font_size=font_size, **kwargs)
|
||||
self.label: VMobject = self.label_constructor(
|
||||
*text, font_size=font_size, **kwargs
|
||||
)
|
||||
else:
|
||||
self.label = self.label_constructor(str(text), font_size=font_size)
|
||||
|
||||
self.brace.put_at_tip(self.label)
|
||||
self.add(self.brace, self.label)
|
||||
|
||||
def creation_anim(self, label_anim=FadeIn, brace_anim=GrowFromCenter):
|
||||
def creation_anim(
|
||||
self,
|
||||
label_anim: type[Animation] = FadeIn,
|
||||
brace_anim: type[Animation] = GrowFromCenter,
|
||||
) -> AnimationGroup:
|
||||
return AnimationGroup(brace_anim(self.brace), label_anim(self.label))
|
||||
|
||||
def shift_brace(self, obj, **kwargs):
|
||||
def shift_brace(self, obj: Mobject, **kwargs: Any) -> Self:
|
||||
if isinstance(obj, list):
|
||||
obj = self.get_group_class()(*obj)
|
||||
self.brace = Brace(obj, self.brace_direction, **kwargs)
|
||||
self.brace.put_at_tip(self.label)
|
||||
return self
|
||||
|
||||
def change_label(self, *text, **kwargs):
|
||||
self.label = self.label_constructor(*text, **kwargs)
|
||||
|
||||
def change_label(self, *text: str, **kwargs: Any) -> Self:
|
||||
self.remove(self.label)
|
||||
self.label = self.label_constructor(*text, **kwargs) # type: ignore[arg-type]
|
||||
self.brace.put_at_tip(self.label)
|
||||
self.add(self.label)
|
||||
return self
|
||||
|
||||
def change_brace_label(self, obj, *text, **kwargs):
|
||||
def change_brace_label(self, obj: Mobject, *text: str, **kwargs: Any) -> Self:
|
||||
self.shift_brace(obj)
|
||||
self.change_label(*text, **kwargs)
|
||||
return self
|
||||
|
||||
|
||||
class BraceText(BraceLabel):
|
||||
def __init__(self, obj, text, label_constructor=Tex, **kwargs):
|
||||
"""Create a brace with a text label attached.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj
|
||||
The mobject adjacent to which the brace is placed.
|
||||
text
|
||||
The label text.
|
||||
brace_direction
|
||||
The direction of the brace. By default ``DOWN``.
|
||||
label_constructor
|
||||
A class or function used to construct a mobject representing
|
||||
the label. By default :class:`~.Text`.
|
||||
font_size
|
||||
The font size of the label, passed to the ``label_constructor``.
|
||||
buff
|
||||
The buffer between the mobject and the brace.
|
||||
brace_config
|
||||
Arguments to be passed to :class:`.Brace`.
|
||||
kwargs
|
||||
Additional arguments to be passed to :class:`~.VMobject`.
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. manim:: BraceTextExample
|
||||
:save_last_frame:
|
||||
|
||||
class BraceTextExample(Scene):
|
||||
def construct(self):
|
||||
s1 = Square().move_to(2*LEFT)
|
||||
self.add(s1)
|
||||
br1 = BraceText(s1, "Label")
|
||||
self.add(br1)
|
||||
|
||||
s2 = Square().move_to(2*RIGHT)
|
||||
self.add(s2)
|
||||
br2 = BraceText(s2, "Label")
|
||||
|
||||
br2.change_label("new")
|
||||
self.add(br2)
|
||||
self.wait(0.1)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
obj: Mobject,
|
||||
text: str,
|
||||
label_constructor: type[SingleStringMathTex | Text] = Text,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(obj, text, label_constructor=label_constructor, **kwargs)
|
||||
|
||||
|
||||
|
|
@ -317,10 +374,10 @@ class BraceBetweenPoints(Brace):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
point_1: Point3DLike | None,
|
||||
point_2: Point3DLike | None,
|
||||
direction: Vector3D | None = ORIGIN,
|
||||
**kwargs,
|
||||
point_1: Point3DLike,
|
||||
point_2: Point3DLike,
|
||||
direction: Vector3DLike = ORIGIN,
|
||||
**kwargs: Any,
|
||||
):
|
||||
if all(direction == ORIGIN):
|
||||
line_vector = np.array(point_2) - np.array(point_1)
|
||||
|
|
@ -386,8 +443,8 @@ class ArcBrace(Brace):
|
|||
def __init__(
|
||||
self,
|
||||
arc: Arc | None = None,
|
||||
direction: Sequence[float] = RIGHT,
|
||||
**kwargs,
|
||||
direction: Vector3DLike = RIGHT,
|
||||
**kwargs: Any,
|
||||
):
|
||||
if arc is None:
|
||||
arc = Arc(start_angle=-1, angle=2, radius=1)
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ from __future__ import annotations
|
|||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
import numpy as np
|
||||
import svgelements as se
|
||||
|
||||
from manim import config, logger
|
||||
from manim.utils.color import ManimColor, ParsableManimColor
|
||||
|
||||
from ...constants import RIGHT
|
||||
from ...utils.bezier import get_quadratic_approximation_of_cubic
|
||||
|
|
@ -98,17 +100,17 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
should_center: bool = True,
|
||||
height: float | None = 2,
|
||||
width: float | None = None,
|
||||
color: str | None = None,
|
||||
color: ParsableManimColor | None = None,
|
||||
opacity: float | None = None,
|
||||
fill_color: str | None = None,
|
||||
fill_color: ParsableManimColor | None = None,
|
||||
fill_opacity: float | None = None,
|
||||
stroke_color: str | None = None,
|
||||
stroke_color: ParsableManimColor | None = None,
|
||||
stroke_opacity: float | None = None,
|
||||
stroke_width: float | None = None,
|
||||
svg_default: dict | None = None,
|
||||
path_string_config: dict | None = None,
|
||||
use_svg_cache: bool = True,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(color=None, stroke_color=None, fill_color=None, **kwargs)
|
||||
|
||||
|
|
@ -118,13 +120,15 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
self.should_center = should_center
|
||||
self.svg_height = height
|
||||
self.svg_width = width
|
||||
self.color = color
|
||||
self.color = ManimColor(color)
|
||||
self.opacity = opacity
|
||||
self.fill_color = fill_color
|
||||
self.fill_opacity = fill_opacity
|
||||
self.fill_opacity = fill_opacity # type: ignore[assignment]
|
||||
self.stroke_color = stroke_color
|
||||
self.stroke_opacity = stroke_opacity
|
||||
self.stroke_width = stroke_width
|
||||
self.stroke_opacity = stroke_opacity # type: ignore[assignment]
|
||||
self.stroke_width = stroke_width # type: ignore[assignment]
|
||||
if self.stroke_width is None:
|
||||
self.stroke_width = 0
|
||||
|
||||
if svg_default is None:
|
||||
svg_default = {
|
||||
|
|
@ -191,7 +195,7 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
"""Parse the SVG and translate its elements to submobjects."""
|
||||
file_path = self.get_file_path()
|
||||
element_tree = ET.parse(file_path)
|
||||
new_tree = self.modify_xml_tree(element_tree)
|
||||
new_tree = self.modify_xml_tree(element_tree) # type: ignore[arg-type]
|
||||
# Create a temporary svg file to dump modified svg to be parsed
|
||||
modified_file_path = file_path.with_name(f"{file_path.stem}_{file_path.suffix}")
|
||||
new_tree.write(modified_file_path)
|
||||
|
|
@ -228,12 +232,12 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
"style",
|
||||
)
|
||||
root = element_tree.getroot()
|
||||
root_style_dict = {k: v for k, v in root.attrib.items() if k in style_keys}
|
||||
root_style_dict = {k: v for k, v in root.attrib.items() if k in style_keys} # type: ignore[union-attr]
|
||||
|
||||
new_root = ET.Element("svg", {})
|
||||
config_style_node = ET.SubElement(new_root, "g", config_style_dict)
|
||||
root_style_node = ET.SubElement(config_style_node, "g", root_style_dict)
|
||||
root_style_node.extend(root)
|
||||
root_style_node.extend(root) # type: ignore[arg-type]
|
||||
return ET.ElementTree(new_root)
|
||||
|
||||
def generate_config_style_dict(self) -> dict[str, str]:
|
||||
|
|
@ -262,13 +266,13 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
svg
|
||||
The parsed SVG file.
|
||||
"""
|
||||
result = []
|
||||
result: list[VMobject] = []
|
||||
for shape in svg.elements():
|
||||
# can we combine the two continue cases into one?
|
||||
if isinstance(shape, se.Group): # noqa: SIM114
|
||||
continue
|
||||
elif isinstance(shape, se.Path):
|
||||
mob = self.path_to_mobject(shape)
|
||||
mob: VMobject = self.path_to_mobject(shape)
|
||||
elif isinstance(shape, se.SimpleLine):
|
||||
mob = self.line_to_mobject(shape)
|
||||
elif isinstance(shape, se.Rect):
|
||||
|
|
@ -422,7 +426,7 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
return vmobject_class().set_points_as_corners(points)
|
||||
|
||||
@staticmethod
|
||||
def text_to_mobject(text: se.Text):
|
||||
def text_to_mobject(text: se.Text) -> VMobject:
|
||||
"""Convert a text element to a vectorized mobject.
|
||||
|
||||
.. warning::
|
||||
|
|
@ -435,7 +439,7 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
The parsed SVG text.
|
||||
"""
|
||||
logger.warning(f"Unsupported element type: {type(text)}")
|
||||
return
|
||||
return # type: ignore[return-value]
|
||||
|
||||
def move_into_position(self) -> None:
|
||||
"""Scale and move the generated mobject into position."""
|
||||
|
|
@ -480,7 +484,7 @@ class VMobjectFromSVGPath(VMobject, metaclass=ConvertToOpenGL):
|
|||
long_lines: bool = False,
|
||||
should_subdivide_sharp_curves: bool = False,
|
||||
should_remove_null_curves: bool = False,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
# Get rid of arcs
|
||||
path_obj.approximate_arcs_with_quads()
|
||||
|
|
@ -492,7 +496,7 @@ class VMobjectFromSVGPath(VMobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_points(self) -> None:
|
||||
def generate_points(self) -> None:
|
||||
# TODO: cache mobject in a re-importable way
|
||||
|
||||
self.handle_commands()
|
||||
|
|
@ -505,15 +509,16 @@ class VMobjectFromSVGPath(VMobject, metaclass=ConvertToOpenGL):
|
|||
# Get rid of any null curves
|
||||
self.set_points(self.get_points_without_null_curves())
|
||||
|
||||
generate_points = init_points
|
||||
def init_points(self) -> None:
|
||||
self.generate_points()
|
||||
|
||||
def handle_commands(self) -> None:
|
||||
all_points: list[np.ndarray] = []
|
||||
last_move = None
|
||||
last_move: np.ndarray = None
|
||||
curve_start = None
|
||||
last_true_move = None
|
||||
|
||||
def move_pen(pt, *, true_move: bool = False):
|
||||
def move_pen(pt: np.ndarray, *, true_move: bool = False) -> None:
|
||||
nonlocal last_move, curve_start, last_true_move
|
||||
last_move = pt
|
||||
if curve_start is None:
|
||||
|
|
@ -523,17 +528,19 @@ class VMobjectFromSVGPath(VMobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
if self.n_points_per_curve == 4:
|
||||
|
||||
def add_cubic(start, cp1, cp2, end):
|
||||
def add_cubic(
|
||||
start: np.ndarray, cp1: np.ndarray, cp2: np.ndarray, end: np.ndarray
|
||||
) -> None:
|
||||
nonlocal all_points
|
||||
assert len(all_points) % 4 == 0, len(all_points)
|
||||
all_points += [start, cp1, cp2, end]
|
||||
move_pen(end)
|
||||
|
||||
def add_quad(start, cp, end):
|
||||
def add_quad(start: np.ndarray, cp: np.ndarray, end: np.ndarray) -> None:
|
||||
add_cubic(start, (start + cp + cp) / 3, (cp + cp + end) / 3, end)
|
||||
move_pen(end)
|
||||
|
||||
def add_line(start, end):
|
||||
def add_line(start: np.ndarray, end: np.ndarray) -> None:
|
||||
add_cubic(
|
||||
start, (start + start + end) / 3, (start + end + end) / 3, end
|
||||
)
|
||||
|
|
@ -541,7 +548,9 @@ class VMobjectFromSVGPath(VMobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
else:
|
||||
|
||||
def add_cubic(start, cp1, cp2, end):
|
||||
def add_cubic(
|
||||
start: np.ndarray, cp1: np.ndarray, cp2: np.ndarray, end: np.ndarray
|
||||
) -> None:
|
||||
nonlocal all_points
|
||||
assert len(all_points) % 3 == 0, len(all_points)
|
||||
two_quads = get_quadratic_approximation_of_cubic(
|
||||
|
|
@ -554,13 +563,13 @@ class VMobjectFromSVGPath(VMobject, metaclass=ConvertToOpenGL):
|
|||
all_points += two_quads[3:].tolist()
|
||||
move_pen(end)
|
||||
|
||||
def add_quad(start, cp, end):
|
||||
def add_quad(start: np.ndarray, cp: np.ndarray, end: np.ndarray) -> None:
|
||||
nonlocal all_points
|
||||
assert len(all_points) % 3 == 0, len(all_points)
|
||||
all_points += [start, cp, end]
|
||||
move_pen(end)
|
||||
|
||||
def add_line(start, end):
|
||||
def add_line(start: np.ndarray, end: np.ndarray) -> None:
|
||||
add_quad(start, (start + end) / 2, end)
|
||||
move_pen(end)
|
||||
|
||||
|
|
|
|||
|
|
@ -65,8 +65,7 @@ __all__ = [
|
|||
|
||||
|
||||
import itertools as it
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import Callable
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
|
||||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.geometry.polygram import Polygon
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ __all__ = [
|
|||
"Code",
|
||||
]
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Literal
|
||||
|
||||
|
|
@ -23,7 +24,6 @@ from manim.mobject.geometry.arc import Dot
|
|||
from manim.mobject.geometry.shape_matchers import SurroundingRectangle
|
||||
from manim.mobject.mobject import override_animate
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.text.text_mobject import Paragraph
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.typing import StrPath
|
||||
from manim.utils.color import WHITE, ManimColor
|
||||
|
|
@ -126,6 +126,7 @@ class Code(VMobject, metaclass=ConvertToOpenGL):
|
|||
"line_spacing": 0.5,
|
||||
"disable_ligatures": True,
|
||||
}
|
||||
code: VMobject
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -177,10 +178,10 @@ class Code(VMobject, metaclass=ConvertToOpenGL):
|
|||
if child.name == "span":
|
||||
try:
|
||||
child_style = child["style"]
|
||||
if isinstance(child_style, str):
|
||||
color = child_style.removeprefix("color: ")
|
||||
else:
|
||||
color = None
|
||||
match_ = re.match(
|
||||
r"color: (#[A-Fa-f0-9]{6}|#[A-Fa-f0-9]{3})", child_style
|
||||
)
|
||||
color = None if match_ is None else match_.group(1)
|
||||
except KeyError:
|
||||
color = None
|
||||
current_line_color_ranges.append(
|
||||
|
|
@ -208,6 +209,8 @@ class Code(VMobject, metaclass=ConvertToOpenGL):
|
|||
base_paragraph_config = self.default_paragraph_config.copy()
|
||||
base_paragraph_config.update(paragraph_config)
|
||||
|
||||
from manim.mobject.text.text_mobject import Paragraph
|
||||
|
||||
self.code_lines = Paragraph(
|
||||
*code_lines,
|
||||
**base_paragraph_config,
|
||||
|
|
@ -232,6 +235,8 @@ class Code(VMobject, metaclass=ConvertToOpenGL):
|
|||
)
|
||||
self.add(self.line_numbers)
|
||||
|
||||
for line in self.code_lines:
|
||||
line.submobjects = [c for c in line if not isinstance(c, Dot)]
|
||||
self.add(self.code_lines)
|
||||
|
||||
if background_config is None:
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["DecimalNumber", "Integer", "Variable"]
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim import config
|
||||
from manim.constants import *
|
||||
|
|
@ -16,10 +16,9 @@ from manim.mobject.text.tex_mobject import MathTex, SingleStringMathTex, Tex
|
|||
from manim.mobject.text.text_mobject import Text
|
||||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
from manim.mobject.value_tracker import ValueTracker
|
||||
from manim.typing import Vector3DLike
|
||||
|
||||
string_to_mob_map = {}
|
||||
|
||||
__all__ = ["DecimalNumber", "Integer", "Variable"]
|
||||
string_to_mob_map: dict[str, SingleStringMathTex] = {}
|
||||
|
||||
|
||||
class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
|
||||
|
|
@ -86,7 +85,7 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
|
|||
self,
|
||||
number: float = 0,
|
||||
num_decimal_places: int = 2,
|
||||
mob_class: VMobject = MathTex,
|
||||
mob_class: type[SingleStringMathTex] = MathTex,
|
||||
include_sign: bool = False,
|
||||
group_with_commas: bool = True,
|
||||
digit_buff_per_font_unit: float = 0.001,
|
||||
|
|
@ -94,13 +93,13 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
|
|||
unit: str | None = None, # Aligned to bottom unless it starts with "^"
|
||||
unit_buff_per_font_unit: float = 0,
|
||||
include_background_rectangle: bool = False,
|
||||
edge_to_fix: Sequence[float] = LEFT,
|
||||
edge_to_fix: Vector3DLike = LEFT,
|
||||
font_size: float = DEFAULT_FONT_SIZE,
|
||||
stroke_width: float = 0,
|
||||
fill_opacity: float = 1.0,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(**kwargs, stroke_width=stroke_width)
|
||||
super().__init__(**kwargs, fill_opacity=fill_opacity, stroke_width=stroke_width)
|
||||
self.number = number
|
||||
self.num_decimal_places = num_decimal_places
|
||||
self.include_sign = include_sign
|
||||
|
|
@ -137,12 +136,13 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
|
|||
self.init_colors()
|
||||
|
||||
@property
|
||||
def font_size(self):
|
||||
def font_size(self) -> float:
|
||||
"""The font size of the tex mobject."""
|
||||
return self.height / self.initial_height * self._font_size
|
||||
return_value: float = self.height / self.initial_height * self._font_size
|
||||
return return_value
|
||||
|
||||
@font_size.setter
|
||||
def font_size(self, font_val):
|
||||
def font_size(self, font_val: float) -> None:
|
||||
if font_val <= 0:
|
||||
raise ValueError("font_size must be greater than 0.")
|
||||
elif self.height > 0:
|
||||
|
|
@ -153,7 +153,7 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
|
|||
# font_size does not depend on current size.
|
||||
self.scale(font_val / self.font_size)
|
||||
|
||||
def _set_submobjects_from_number(self, number):
|
||||
def _set_submobjects_from_number(self, number: float) -> None:
|
||||
self.number = number
|
||||
self.submobjects = []
|
||||
|
||||
|
|
@ -197,12 +197,12 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
|
|||
self.unit_sign.align_to(self, UP)
|
||||
|
||||
# track the initial height to enable scaling via font_size
|
||||
self.initial_height = self.height
|
||||
self.initial_height: float = self.height
|
||||
|
||||
if self.include_background_rectangle:
|
||||
self.add_background_rectangle()
|
||||
|
||||
def _get_num_string(self, number):
|
||||
def _get_num_string(self, number: float | complex) -> str:
|
||||
if isinstance(number, complex):
|
||||
formatter = self._get_complex_formatter()
|
||||
else:
|
||||
|
|
@ -215,7 +215,12 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
return num_string
|
||||
|
||||
def _string_to_mob(self, string: str, mob_class: VMobject | None = None, **kwargs):
|
||||
def _string_to_mob(
|
||||
self,
|
||||
string: str,
|
||||
mob_class: type[SingleStringMathTex] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> VMobject:
|
||||
if mob_class is None:
|
||||
mob_class = self.mob_class
|
||||
|
||||
|
|
@ -225,7 +230,7 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
|
|||
mob.font_size = self._font_size
|
||||
return mob
|
||||
|
||||
def _get_formatter(self, **kwargs):
|
||||
def _get_formatter(self, **kwargs: Any) -> str:
|
||||
"""
|
||||
Configuration is based first off instance attributes,
|
||||
but overwritten by any kew word argument. Relevant
|
||||
|
|
@ -258,7 +263,7 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
|
|||
],
|
||||
)
|
||||
|
||||
def _get_complex_formatter(self):
|
||||
def _get_complex_formatter(self) -> str:
|
||||
return "".join(
|
||||
[
|
||||
self._get_formatter(field_name="0.real"),
|
||||
|
|
@ -267,7 +272,7 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
|
|||
],
|
||||
)
|
||||
|
||||
def set_value(self, number: float):
|
||||
def set_value(self, number: float) -> Self:
|
||||
"""Set the value of the :class:`~.DecimalNumber` to a new number.
|
||||
|
||||
Parameters
|
||||
|
|
@ -304,10 +309,10 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
|
|||
self.init_colors()
|
||||
return self
|
||||
|
||||
def get_value(self):
|
||||
def get_value(self) -> float:
|
||||
return self.number
|
||||
|
||||
def increment_value(self, delta_t=1):
|
||||
def increment_value(self, delta_t: float = 1) -> None:
|
||||
self.set_value(self.get_value() + delta_t)
|
||||
|
||||
|
||||
|
|
@ -333,7 +338,7 @@ class Integer(DecimalNumber):
|
|||
) -> None:
|
||||
super().__init__(number=number, num_decimal_places=num_decimal_places, **kwargs)
|
||||
|
||||
def get_value(self):
|
||||
def get_value(self) -> int:
|
||||
return int(np.round(super().get_value()))
|
||||
|
||||
|
||||
|
|
@ -444,9 +449,9 @@ class Variable(VMobject, metaclass=ConvertToOpenGL):
|
|||
self,
|
||||
var: float,
|
||||
label: str | Tex | MathTex | Text | SingleStringMathTex,
|
||||
var_type: DecimalNumber | Integer = DecimalNumber,
|
||||
var_type: type[DecimalNumber | Integer] = DecimalNumber,
|
||||
num_decimal_places: int = 2,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.label = MathTex(label) if isinstance(label, str) else label
|
||||
equals = MathTex("=").next_to(self.label, RIGHT)
|
||||
|
|
|
|||
|
|
@ -26,9 +26,12 @@ __all__ = [
|
|||
import itertools as it
|
||||
import operator as op
|
||||
import re
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Iterable, Sequence
|
||||
from functools import reduce
|
||||
from textwrap import dedent
|
||||
from typing import Any
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim import config, logger
|
||||
from manim.constants import *
|
||||
|
|
@ -38,8 +41,6 @@ from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
|||
from manim.utils.tex import TexTemplate
|
||||
from manim.utils.tex_file_writing import tex_to_svg_file
|
||||
|
||||
tex_string_to_mob_map = {}
|
||||
|
||||
|
||||
class SingleStringMathTex(SVGMobject):
|
||||
"""Elementary building block for rendering text with LaTeX.
|
||||
|
|
@ -59,11 +60,11 @@ class SingleStringMathTex(SVGMobject):
|
|||
should_center: bool = True,
|
||||
height: float | None = None,
|
||||
organize_left_to_right: bool = False,
|
||||
tex_environment: str = "align*",
|
||||
tex_environment: str | None = "align*",
|
||||
tex_template: TexTemplate | None = None,
|
||||
font_size: float = DEFAULT_FONT_SIZE,
|
||||
color: ParsableManimColor | None = None,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
if color is None:
|
||||
color = VMobject().color
|
||||
|
|
@ -73,9 +74,8 @@ class SingleStringMathTex(SVGMobject):
|
|||
self.tex_environment = tex_environment
|
||||
if tex_template is None:
|
||||
tex_template = config["tex_template"]
|
||||
self.tex_template = tex_template
|
||||
self.tex_template: TexTemplate = tex_template
|
||||
|
||||
assert isinstance(tex_string, str)
|
||||
self.tex_string = tex_string
|
||||
file_name = tex_to_svg_file(
|
||||
self._get_modified_expression(tex_string),
|
||||
|
|
@ -105,16 +105,16 @@ class SingleStringMathTex(SVGMobject):
|
|||
if self.organize_left_to_right:
|
||||
self._organize_submobjects_left_to_right()
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f"{type(self).__name__}({repr(self.tex_string)})"
|
||||
|
||||
@property
|
||||
def font_size(self):
|
||||
def font_size(self) -> float:
|
||||
"""The font size of the tex mobject."""
|
||||
return self.height / self.initial_height / SCALE_FACTOR_PER_FONT_POINT
|
||||
|
||||
@font_size.setter
|
||||
def font_size(self, font_val):
|
||||
def font_size(self, font_val: float) -> None:
|
||||
if font_val <= 0:
|
||||
raise ValueError("font_size must be greater than 0.")
|
||||
elif self.height > 0:
|
||||
|
|
@ -125,13 +125,13 @@ class SingleStringMathTex(SVGMobject):
|
|||
# font_size does not depend on current size.
|
||||
self.scale(font_val / self.font_size)
|
||||
|
||||
def _get_modified_expression(self, tex_string):
|
||||
def _get_modified_expression(self, tex_string: str) -> str:
|
||||
result = tex_string
|
||||
result = result.strip()
|
||||
result = self._modify_special_strings(result)
|
||||
return result
|
||||
|
||||
def _modify_special_strings(self, tex):
|
||||
def _modify_special_strings(self, tex: str) -> str:
|
||||
tex = tex.strip()
|
||||
should_add_filler = reduce(
|
||||
op.or_,
|
||||
|
|
@ -184,7 +184,7 @@ class SingleStringMathTex(SVGMobject):
|
|||
tex = ""
|
||||
return tex
|
||||
|
||||
def _remove_stray_braces(self, tex):
|
||||
def _remove_stray_braces(self, tex: str) -> str:
|
||||
r"""
|
||||
Makes :class:`~.MathTex` resilient to unmatched braces.
|
||||
|
||||
|
|
@ -202,14 +202,14 @@ class SingleStringMathTex(SVGMobject):
|
|||
num_rights += 1
|
||||
return tex
|
||||
|
||||
def _organize_submobjects_left_to_right(self):
|
||||
def _organize_submobjects_left_to_right(self) -> Self:
|
||||
self.sort(lambda p: p[0])
|
||||
return self
|
||||
|
||||
def get_tex_string(self):
|
||||
def get_tex_string(self) -> str:
|
||||
return self.tex_string
|
||||
|
||||
def init_colors(self, propagate_colors=True):
|
||||
def init_colors(self, propagate_colors: bool = True) -> Self:
|
||||
for submobject in self.submobjects:
|
||||
# needed to preserve original (non-black)
|
||||
# TeX colors of individual submobjects
|
||||
|
|
@ -220,6 +220,7 @@ class SingleStringMathTex(SVGMobject):
|
|||
submobject.init_colors()
|
||||
elif config.renderer == RendererType.CAIRO:
|
||||
submobject.init_colors(propagate_colors=propagate_colors)
|
||||
return self
|
||||
|
||||
|
||||
class MathTex(SingleStringMathTex):
|
||||
|
|
@ -255,21 +256,22 @@ class MathTex(SingleStringMathTex):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
*tex_strings,
|
||||
*tex_strings: str,
|
||||
arg_separator: str = " ",
|
||||
substrings_to_isolate: Iterable[str] | None = None,
|
||||
tex_to_color_map: dict[str, ManimColor] = None,
|
||||
tex_environment: str = "align*",
|
||||
**kwargs,
|
||||
tex_to_color_map: dict[str, ParsableManimColor] | None = None,
|
||||
tex_environment: str | None = "align*",
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.tex_template = kwargs.pop("tex_template", config["tex_template"])
|
||||
self.arg_separator = arg_separator
|
||||
self.substrings_to_isolate = (
|
||||
[] if substrings_to_isolate is None else substrings_to_isolate
|
||||
)
|
||||
self.tex_to_color_map = tex_to_color_map
|
||||
if self.tex_to_color_map is None:
|
||||
self.tex_to_color_map = {}
|
||||
if tex_to_color_map is None:
|
||||
self.tex_to_color_map: dict[str, ParsableManimColor] = {}
|
||||
else:
|
||||
self.tex_to_color_map = tex_to_color_map
|
||||
self.tex_environment = tex_environment
|
||||
self.brace_notation_split_occurred = False
|
||||
self.tex_strings = self._break_up_tex_strings(tex_strings)
|
||||
|
|
@ -301,12 +303,14 @@ class MathTex(SingleStringMathTex):
|
|||
if self.organize_left_to_right:
|
||||
self._organize_submobjects_left_to_right()
|
||||
|
||||
def _break_up_tex_strings(self, tex_strings):
|
||||
def _break_up_tex_strings(self, tex_strings: Sequence[str]) -> list[str]:
|
||||
# Separate out anything surrounded in double braces
|
||||
pre_split_length = len(tex_strings)
|
||||
tex_strings = [re.split("{{(.*?)}}", str(t)) for t in tex_strings]
|
||||
tex_strings = sum(tex_strings, [])
|
||||
if len(tex_strings) > pre_split_length:
|
||||
tex_strings_brace_splitted = [
|
||||
re.split("{{(.*?)}}", str(t)) for t in tex_strings
|
||||
]
|
||||
tex_strings_combined = sum(tex_strings_brace_splitted, [])
|
||||
if len(tex_strings_combined) > pre_split_length:
|
||||
self.brace_notation_split_occurred = True
|
||||
|
||||
# Separate out any strings specified in the isolate
|
||||
|
|
@ -324,19 +328,19 @@ class MathTex(SingleStringMathTex):
|
|||
pattern = "|".join(patterns)
|
||||
if pattern:
|
||||
pieces = []
|
||||
for s in tex_strings:
|
||||
for s in tex_strings_combined:
|
||||
pieces.extend(re.split(pattern, s))
|
||||
else:
|
||||
pieces = tex_strings
|
||||
pieces = tex_strings_combined
|
||||
return [p for p in pieces if p]
|
||||
|
||||
def _break_up_by_substrings(self):
|
||||
def _break_up_by_substrings(self) -> Self:
|
||||
"""
|
||||
Reorganize existing submobjects one layer
|
||||
deeper based on the structure of tex_strings (as a list
|
||||
of tex_strings)
|
||||
"""
|
||||
new_submobjects = []
|
||||
new_submobjects: list[VMobject] = []
|
||||
curr_index = 0
|
||||
for tex_string in self.tex_strings:
|
||||
sub_tex_mob = SingleStringMathTex(
|
||||
|
|
@ -358,8 +362,10 @@ class MathTex(SingleStringMathTex):
|
|||
self.submobjects = new_submobjects
|
||||
return self
|
||||
|
||||
def get_parts_by_tex(self, tex, substring=True, case_sensitive=True):
|
||||
def test(tex1, tex2):
|
||||
def get_parts_by_tex(
|
||||
self, tex: str, substring: bool = True, case_sensitive: bool = True
|
||||
) -> VGroup:
|
||||
def test(tex1: str, tex2: str) -> bool:
|
||||
if not case_sensitive:
|
||||
tex1 = tex1.lower()
|
||||
tex2 = tex2.lower()
|
||||
|
|
@ -370,19 +376,25 @@ class MathTex(SingleStringMathTex):
|
|||
|
||||
return VGroup(*(m for m in self.submobjects if test(tex, m.get_tex_string())))
|
||||
|
||||
def get_part_by_tex(self, tex, **kwargs):
|
||||
def get_part_by_tex(self, tex: str, **kwargs: Any) -> MathTex | None:
|
||||
all_parts = self.get_parts_by_tex(tex, **kwargs)
|
||||
return all_parts[0] if all_parts else None
|
||||
|
||||
def set_color_by_tex(self, tex, color, **kwargs):
|
||||
def set_color_by_tex(
|
||||
self, tex: str, color: ParsableManimColor, **kwargs: Any
|
||||
) -> Self:
|
||||
parts_to_color = self.get_parts_by_tex(tex, **kwargs)
|
||||
for part in parts_to_color:
|
||||
part.set_color(color)
|
||||
return self
|
||||
|
||||
def set_opacity_by_tex(
|
||||
self, tex: str, opacity: float = 0.5, remaining_opacity: float = None, **kwargs
|
||||
):
|
||||
self,
|
||||
tex: str,
|
||||
opacity: float = 0.5,
|
||||
remaining_opacity: float | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Self:
|
||||
"""
|
||||
Sets the opacity of the tex specified. If 'remaining_opacity' is specified,
|
||||
then the remaining tex will be set to that opacity.
|
||||
|
|
@ -403,29 +415,33 @@ class MathTex(SingleStringMathTex):
|
|||
part.set_opacity(opacity)
|
||||
return self
|
||||
|
||||
def set_color_by_tex_to_color_map(self, texs_to_color_map, **kwargs):
|
||||
def set_color_by_tex_to_color_map(
|
||||
self, texs_to_color_map: dict[str, ParsableManimColor], **kwargs: Any
|
||||
) -> Self:
|
||||
for texs, color in list(texs_to_color_map.items()):
|
||||
try:
|
||||
# If the given key behaves like tex_strings
|
||||
texs + ""
|
||||
self.set_color_by_tex(texs, color, **kwargs)
|
||||
self.set_color_by_tex(texs, ManimColor(color), **kwargs)
|
||||
except TypeError:
|
||||
# If the given key is a tuple
|
||||
for tex in texs:
|
||||
self.set_color_by_tex(tex, color, **kwargs)
|
||||
self.set_color_by_tex(tex, ManimColor(color), **kwargs)
|
||||
return self
|
||||
|
||||
def index_of_part(self, part):
|
||||
def index_of_part(self, part: MathTex) -> int:
|
||||
split_self = self.split()
|
||||
if part not in split_self:
|
||||
raise ValueError("Trying to get index of part not in MathTex")
|
||||
return split_self.index(part)
|
||||
|
||||
def index_of_part_by_tex(self, tex, **kwargs):
|
||||
def index_of_part_by_tex(self, tex: str, **kwargs: Any) -> int:
|
||||
part = self.get_part_by_tex(tex, **kwargs)
|
||||
if part is None:
|
||||
return -1
|
||||
return self.index_of_part(part)
|
||||
|
||||
def sort_alphabetically(self):
|
||||
def sort_alphabetically(self) -> None:
|
||||
self.submobjects.sort(key=lambda m: m.get_tex_string())
|
||||
|
||||
|
||||
|
|
@ -447,7 +463,11 @@ class Tex(MathTex):
|
|||
"""
|
||||
|
||||
def __init__(
|
||||
self, *tex_strings, arg_separator="", tex_environment="center", **kwargs
|
||||
self,
|
||||
*tex_strings: str,
|
||||
arg_separator: str = "",
|
||||
tex_environment: str | None = "center",
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(
|
||||
*tex_strings,
|
||||
|
|
@ -477,18 +497,20 @@ class BulletedList(Tex):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
*items,
|
||||
buff=MED_LARGE_BUFF,
|
||||
dot_scale_factor=2,
|
||||
tex_environment=None,
|
||||
**kwargs,
|
||||
*items: str,
|
||||
buff: float = MED_LARGE_BUFF,
|
||||
dot_scale_factor: float = 2,
|
||||
tex_environment: str | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.buff = buff
|
||||
self.dot_scale_factor = dot_scale_factor
|
||||
self.tex_environment = tex_environment
|
||||
line_separated_items = [s + "\\\\" for s in items]
|
||||
super().__init__(
|
||||
*line_separated_items, tex_environment=tex_environment, **kwargs
|
||||
*line_separated_items,
|
||||
tex_environment=tex_environment,
|
||||
**kwargs,
|
||||
)
|
||||
for part in self:
|
||||
dot = MathTex("\\cdot").scale(self.dot_scale_factor)
|
||||
|
|
@ -496,10 +518,14 @@ class BulletedList(Tex):
|
|||
part.add_to_back(dot)
|
||||
self.arrange(DOWN, aligned_edge=LEFT, buff=self.buff)
|
||||
|
||||
def fade_all_but(self, index_or_string, opacity=0.5):
|
||||
def fade_all_but(self, index_or_string: int | str, opacity: float = 0.5) -> None:
|
||||
arg = index_or_string
|
||||
if isinstance(arg, str):
|
||||
part = self.get_part_by_tex(arg)
|
||||
part: VGroup | VMobject | None = self.get_part_by_tex(arg)
|
||||
if part is None:
|
||||
raise Exception(
|
||||
f"Could not locate part by provided tex string '{arg}'."
|
||||
)
|
||||
elif isinstance(arg, int):
|
||||
part = self.submobjects[arg]
|
||||
else:
|
||||
|
|
@ -531,11 +557,11 @@ class Title(Tex):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
*text_parts,
|
||||
include_underline=True,
|
||||
match_underline_width_to_text=False,
|
||||
underline_buff=MED_SMALL_BUFF,
|
||||
**kwargs,
|
||||
*text_parts: str,
|
||||
include_underline: bool = True,
|
||||
match_underline_width_to_text: bool = False,
|
||||
underline_buff: float = MED_SMALL_BUFF,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.include_underline = include_underline
|
||||
self.match_underline_width_to_text = match_underline_width_to_text
|
||||
|
|
|
|||
|
|
@ -57,10 +57,11 @@ __all__ = ["Text", "Paragraph", "MarkupText", "register_font"]
|
|||
import copy
|
||||
import hashlib
|
||||
import re
|
||||
from collections.abc import Iterable, Sequence
|
||||
from collections.abc import Iterable, Iterator, Sequence
|
||||
from contextlib import contextmanager
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import manimpango
|
||||
import numpy as np
|
||||
|
|
@ -71,8 +72,13 @@ from manim.constants import *
|
|||
from manim.mobject.geometry.arc import Dot
|
||||
from manim.mobject.svg.svg_mobject import SVGMobject
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.typing import Point3D
|
||||
from manim.utils.color import ManimColor, ParsableManimColor, color_gradient
|
||||
from manim.utils.deprecation import deprecated
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim.typing import Point3D
|
||||
|
||||
TEXT_MOB_SCALE_FACTOR = 0.05
|
||||
DEFAULT_LINE_SPACING_SCALE = 0.3
|
||||
|
|
@ -81,7 +87,7 @@ TEXT2SVG_ADJUSTMENT_FACTOR = 4.8
|
|||
__all__ = ["Text", "Paragraph", "MarkupText", "register_font"]
|
||||
|
||||
|
||||
def remove_invisible_chars(mobject: SVGMobject) -> SVGMobject:
|
||||
def remove_invisible_chars(mobject: VMobject) -> VMobject:
|
||||
"""Function to remove unwanted invisible characters from some mobjects.
|
||||
|
||||
Parameters
|
||||
|
|
@ -94,24 +100,14 @@ def remove_invisible_chars(mobject: SVGMobject) -> SVGMobject:
|
|||
:class:`~.SVGMobject`
|
||||
The SVGMobject without unwanted invisible characters.
|
||||
"""
|
||||
# TODO: Refactor needed
|
||||
iscode = False
|
||||
if mobject.__class__.__name__ == "Text":
|
||||
mobject = mobject[:]
|
||||
elif mobject.__class__.__name__ == "Code":
|
||||
iscode = True
|
||||
code = mobject
|
||||
mobject = mobject.code
|
||||
mobject_without_dots = VGroup()
|
||||
if mobject[0].__class__ == VGroup:
|
||||
for i in range(len(mobject)):
|
||||
mobject_without_dots.add(VGroup())
|
||||
mobject_without_dots[i].add(*(k for k in mobject[i] if k.__class__ != Dot))
|
||||
if isinstance(mobject[0], VGroup):
|
||||
for submob in mobject:
|
||||
mobject_without_dots.add(
|
||||
VGroup(k for k in submob if not isinstance(k, Dot))
|
||||
)
|
||||
else:
|
||||
mobject_without_dots.add(*(k for k in mobject if k.__class__ != Dot))
|
||||
if iscode:
|
||||
code.code = mobject_without_dots
|
||||
return code
|
||||
mobject_without_dots.add(*(k for k in mobject if not isinstance(k, Dot)))
|
||||
return mobject_without_dots
|
||||
|
||||
|
||||
|
|
@ -155,11 +151,11 @@ class Paragraph(VGroup):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
*text: Sequence[str],
|
||||
*text: str,
|
||||
line_spacing: float = -1,
|
||||
alignment: str | None = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.line_spacing = line_spacing
|
||||
self.alignment = alignment
|
||||
self.consider_spaces_as_chars = kwargs.get("disable_ligatures", False)
|
||||
|
|
@ -420,7 +416,8 @@ class Text(SVGMobject):
|
|||
@staticmethod
|
||||
@functools.cache
|
||||
def font_list() -> list[str]:
|
||||
return manimpango.list_fonts()
|
||||
value: list[str] = manimpango.list_fonts()
|
||||
return value
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -433,22 +430,22 @@ class Text(SVGMobject):
|
|||
font: str = "",
|
||||
slant: str = NORMAL,
|
||||
weight: str = NORMAL,
|
||||
t2c: dict[str, str] = None,
|
||||
t2f: dict[str, str] = None,
|
||||
t2g: dict[str, tuple] = None,
|
||||
t2s: dict[str, str] = None,
|
||||
t2w: dict[str, str] = None,
|
||||
gradient: tuple = None,
|
||||
t2c: dict[str, str] | None = None,
|
||||
t2f: dict[str, str] | None = None,
|
||||
t2g: dict[str, Iterable[ParsableManimColor]] | None = None,
|
||||
t2s: dict[str, str] | None = None,
|
||||
t2w: dict[str, str] | None = None,
|
||||
gradient: Iterable[ParsableManimColor] | None = None,
|
||||
tab_width: int = 4,
|
||||
warn_missing_font: bool = True,
|
||||
# Mobject
|
||||
height: float = None,
|
||||
width: float = None,
|
||||
height: float | None = None,
|
||||
width: float | None = None,
|
||||
should_center: bool = True,
|
||||
disable_ligatures: bool = False,
|
||||
use_svg_cache: bool = False,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.line_spacing = line_spacing
|
||||
if font and warn_missing_font:
|
||||
fonts_list = Text.font_list()
|
||||
|
|
@ -489,11 +486,16 @@ class Text(SVGMobject):
|
|||
t2g = kwargs.pop("text2gradient", t2g)
|
||||
t2s = kwargs.pop("text2slant", t2s)
|
||||
t2w = kwargs.pop("text2weight", t2w)
|
||||
self.t2c = {k: ManimColor(v).to_hex() for k, v in t2c.items()}
|
||||
self.t2f = t2f
|
||||
self.t2g = t2g
|
||||
self.t2s = t2s
|
||||
self.t2w = t2w
|
||||
assert t2c is not None
|
||||
assert t2f is not None
|
||||
assert t2g is not None
|
||||
assert t2s is not None
|
||||
assert t2w is not None
|
||||
self.t2c: dict[str, str] = {k: ManimColor(v).to_hex() for k, v in t2c.items()}
|
||||
self.t2f: dict[str, str] = t2f
|
||||
self.t2g: dict[str, Iterable[ParsableManimColor]] = t2g
|
||||
self.t2s: dict[str, str] = t2s
|
||||
self.t2w: dict[str, str] = t2w
|
||||
|
||||
self.original_text = text
|
||||
self.disable_ligatures = disable_ligatures
|
||||
|
|
@ -508,8 +510,8 @@ class Text(SVGMobject):
|
|||
else:
|
||||
self.line_spacing = self._font_size + self._font_size * self.line_spacing
|
||||
|
||||
color: ManimColor = ManimColor(color) if color else VMobject().color
|
||||
file_name = self._text2svg(color.to_hex())
|
||||
parsed_color: ManimColor = ManimColor(color) if color else VMobject().color
|
||||
file_name = self._text2svg(parsed_color.to_hex())
|
||||
PangoUtils.remove_last_M(file_name)
|
||||
super().__init__(
|
||||
file_name,
|
||||
|
|
@ -541,12 +543,12 @@ class Text(SVGMobject):
|
|||
# into a numpy array at the end, rather than creating
|
||||
# new numpy arrays every time a point or fixing line
|
||||
# is added (which is O(n^2) for numpy arrays).
|
||||
closed_curve_points = []
|
||||
closed_curve_points: list[Point3D] = []
|
||||
# OpenGL has points be part of quadratic Bezier curves;
|
||||
# Cairo uses cubic Bezier curves.
|
||||
if nppc == 3: # RendererType.OPENGL
|
||||
|
||||
def add_line_to(end):
|
||||
def add_line_to(end: Point3D) -> None:
|
||||
nonlocal closed_curve_points
|
||||
start = closed_curve_points[-1]
|
||||
closed_curve_points += [
|
||||
|
|
@ -557,7 +559,7 @@ class Text(SVGMobject):
|
|||
|
||||
else: # RendererType.CAIRO
|
||||
|
||||
def add_line_to(end):
|
||||
def add_line_to(end: Point3D) -> None:
|
||||
nonlocal closed_curve_points
|
||||
start = closed_curve_points[-1]
|
||||
closed_curve_points += [
|
||||
|
|
@ -588,11 +590,11 @@ class Text(SVGMobject):
|
|||
self.scale(TEXT_MOB_SCALE_FACTOR)
|
||||
self.initial_height = self.height
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f"Text({repr(self.original_text)})"
|
||||
|
||||
@property
|
||||
def font_size(self):
|
||||
def font_size(self) -> float:
|
||||
return (
|
||||
self.height
|
||||
/ self.initial_height
|
||||
|
|
@ -603,14 +605,14 @@ class Text(SVGMobject):
|
|||
)
|
||||
|
||||
@font_size.setter
|
||||
def font_size(self, font_val):
|
||||
def font_size(self, font_val: float) -> None:
|
||||
# TODO: use pango's font size scaling.
|
||||
if font_val <= 0:
|
||||
raise ValueError("font_size must be greater than 0.")
|
||||
else:
|
||||
self.scale(font_val / self.font_size)
|
||||
|
||||
def _gen_chars(self):
|
||||
def _gen_chars(self) -> VGroup:
|
||||
chars = self.get_group_class()()
|
||||
submobjects_char_index = 0
|
||||
for char_index in range(len(self.text)):
|
||||
|
|
@ -628,7 +630,7 @@ class Text(SVGMobject):
|
|||
submobjects_char_index += 1
|
||||
return chars
|
||||
|
||||
def _find_indexes(self, word: str, text: str):
|
||||
def _find_indexes(self, word: str, text: str) -> list[tuple[int, int]]:
|
||||
"""Finds the indexes of ``text`` in ``word``."""
|
||||
temp = re.match(r"\[([0-9\-]{0,}):([0-9\-]{0,})\]", word)
|
||||
if temp:
|
||||
|
|
@ -636,7 +638,9 @@ class Text(SVGMobject):
|
|||
end = int(temp.group(2)) if temp.group(2) != "" else len(text)
|
||||
start = len(text) + start if start < 0 else start
|
||||
end = len(text) + end if end < 0 else end
|
||||
return [(start, end)]
|
||||
return [
|
||||
(start, end),
|
||||
]
|
||||
indexes = []
|
||||
index = text.find(word)
|
||||
while index != -1:
|
||||
|
|
@ -644,33 +648,7 @@ class Text(SVGMobject):
|
|||
index = text.find(word, index + len(word))
|
||||
return indexes
|
||||
|
||||
@deprecated(
|
||||
since="v0.14.0",
|
||||
until="v0.15.0",
|
||||
message="This was internal function, you shouldn't be using it anyway.",
|
||||
)
|
||||
def _set_color_by_t2c(self, t2c=None):
|
||||
"""Sets color for specified strings."""
|
||||
t2c = t2c if t2c else self.t2c
|
||||
for word, color in list(t2c.items()):
|
||||
for start, end in self._find_indexes(word, self.text):
|
||||
self.chars[start:end].set_color(color)
|
||||
|
||||
@deprecated(
|
||||
since="v0.14.0",
|
||||
until="v0.15.0",
|
||||
message="This was internal function, you shouldn't be using it anyway.",
|
||||
)
|
||||
def _set_color_by_t2g(self, t2g=None):
|
||||
"""Sets gradient colors for specified
|
||||
strings. Behaves similarly to ``set_color_by_t2c``.
|
||||
"""
|
||||
t2g = t2g if t2g else self.t2g
|
||||
for word, gradient in list(t2g.items()):
|
||||
for start, end in self._find_indexes(word, self.text):
|
||||
self.chars[start:end].set_color_by_gradient(*gradient)
|
||||
|
||||
def _text2hash(self, color: ManimColor):
|
||||
def _text2hash(self, color: ParsableManimColor) -> str:
|
||||
"""Generates ``sha256`` hash for file name."""
|
||||
settings = (
|
||||
"PANGO" + self.font + self.slant + self.weight + str(color)
|
||||
|
|
@ -678,6 +656,7 @@ class Text(SVGMobject):
|
|||
settings += str(self.t2f) + str(self.t2s) + str(self.t2w) + str(self.t2c)
|
||||
settings += str(self.line_spacing) + str(self._font_size)
|
||||
settings += str(self.disable_ligatures)
|
||||
settings += str(self.gradient)
|
||||
id_str = self.text + settings
|
||||
hasher = hashlib.sha256()
|
||||
hasher.update(id_str.encode())
|
||||
|
|
@ -713,7 +692,7 @@ class Text(SVGMobject):
|
|||
self,
|
||||
t2xs: Sequence[tuple[dict[str, str], str]],
|
||||
default_args: dict[str, Iterable[str]],
|
||||
) -> Sequence[TextSetting]:
|
||||
) -> list[TextSetting]:
|
||||
settings = []
|
||||
t2xwords = set(chain(*([*t2x.keys()] for t2x, _ in t2xs)))
|
||||
for word in t2xwords:
|
||||
|
|
@ -729,34 +708,27 @@ class Text(SVGMobject):
|
|||
return settings
|
||||
|
||||
def _get_settings_from_gradient(
|
||||
self, default_args: dict[str, Iterable[str]]
|
||||
) -> Sequence[TextSetting]:
|
||||
self, default_args: dict[str, Any]
|
||||
) -> list[TextSetting]:
|
||||
settings = []
|
||||
args = copy.copy(default_args)
|
||||
if self.gradient:
|
||||
colors = color_gradient(self.gradient, len(self.text))
|
||||
colors: list[ManimColor] = color_gradient(self.gradient, len(self.text))
|
||||
for i in range(len(self.text)):
|
||||
args["color"] = colors[i].to_hex()
|
||||
settings.append(TextSetting(i, i + 1, **args))
|
||||
|
||||
for word, gradient in self.t2g.items():
|
||||
if isinstance(gradient, str) or len(gradient) == 1:
|
||||
color = gradient if isinstance(gradient, str) else gradient[0]
|
||||
gradient = [ManimColor(color)]
|
||||
colors = (
|
||||
color_gradient(gradient, len(word))
|
||||
if len(gradient) != 1
|
||||
else len(word) * gradient
|
||||
)
|
||||
colors = color_gradient(gradient, len(word))
|
||||
for start, end in self._find_indexes(word, self.text):
|
||||
for i in range(start, end):
|
||||
args["color"] = colors[i - start].to_hex()
|
||||
settings.append(TextSetting(i, i + 1, **args))
|
||||
return settings
|
||||
|
||||
def _text2settings(self, color: str):
|
||||
def _text2settings(self, color: ParsableManimColor) -> list[TextSetting]:
|
||||
"""Converts the texts and styles to a setting for parsing."""
|
||||
t2xs = [
|
||||
t2xs: list[tuple[dict[str, str], str]] = [
|
||||
(self.t2f, "font"),
|
||||
(self.t2s, "slant"),
|
||||
(self.t2w, "weight"),
|
||||
|
|
@ -764,7 +736,7 @@ class Text(SVGMobject):
|
|||
]
|
||||
# setting_args requires values to be strings
|
||||
|
||||
default_args = {
|
||||
default_args: dict[str, Any] = {
|
||||
arg: getattr(self, arg) if arg != "color" else color for _, arg in t2xs
|
||||
}
|
||||
|
||||
|
|
@ -802,15 +774,15 @@ class Text(SVGMobject):
|
|||
|
||||
line_num = 0
|
||||
if re.search(r"\n", self.text):
|
||||
for start, end in self._find_indexes("\n", self.text):
|
||||
for for_start, for_end in self._find_indexes("\n", self.text):
|
||||
for setting in settings:
|
||||
if setting.line_num == -1:
|
||||
setting.line_num = line_num
|
||||
if start < setting.end:
|
||||
if for_start < setting.end:
|
||||
line_num += 1
|
||||
new_setting = copy.copy(setting)
|
||||
setting.end = end
|
||||
new_setting.start = end
|
||||
setting.end = for_end
|
||||
new_setting.start = for_end
|
||||
new_setting.line_num = line_num
|
||||
settings.append(new_setting)
|
||||
settings.sort(key=lambda setting: setting.start)
|
||||
|
|
@ -821,7 +793,7 @@ class Text(SVGMobject):
|
|||
|
||||
return settings
|
||||
|
||||
def _text2svg(self, color: ManimColor):
|
||||
def _text2svg(self, color: ParsableManimColor) -> str:
|
||||
"""Convert the text to SVG using Pango."""
|
||||
size = self._font_size
|
||||
line_spacing = self.line_spacing
|
||||
|
|
@ -856,11 +828,12 @@ class Text(SVGMobject):
|
|||
|
||||
return svg_file
|
||||
|
||||
def init_colors(self, propagate_colors=True):
|
||||
def init_colors(self, propagate_colors: bool = True) -> Self:
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
super().init_colors()
|
||||
elif config.renderer == RendererType.CAIRO:
|
||||
super().init_colors(propagate_colors=propagate_colors)
|
||||
return self
|
||||
|
||||
|
||||
class MarkupText(SVGMobject):
|
||||
|
|
@ -1164,7 +1137,8 @@ class MarkupText(SVGMobject):
|
|||
@staticmethod
|
||||
@functools.cache
|
||||
def font_list() -> list[str]:
|
||||
return manimpango.list_fonts()
|
||||
value: list[str] = manimpango.list_fonts()
|
||||
return value
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -1173,22 +1147,22 @@ class MarkupText(SVGMobject):
|
|||
stroke_width: float = 0,
|
||||
color: ParsableManimColor | None = None,
|
||||
font_size: float = DEFAULT_FONT_SIZE,
|
||||
line_spacing: int = -1,
|
||||
line_spacing: float = -1,
|
||||
font: str = "",
|
||||
slant: str = NORMAL,
|
||||
weight: str = NORMAL,
|
||||
justify: bool = False,
|
||||
gradient: tuple = None,
|
||||
gradient: Iterable[ParsableManimColor] | None = None,
|
||||
tab_width: int = 4,
|
||||
height: int = None,
|
||||
width: int = None,
|
||||
height: int | None = None,
|
||||
width: int | None = None,
|
||||
should_center: bool = True,
|
||||
disable_ligatures: bool = False,
|
||||
warn_missing_font: bool = True,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.text = text
|
||||
self.line_spacing = line_spacing
|
||||
self.line_spacing: float = line_spacing
|
||||
if font and warn_missing_font:
|
||||
fonts_list = Text.font_list()
|
||||
# handle special case of sans/sans-serif
|
||||
|
|
@ -1235,8 +1209,8 @@ class MarkupText(SVGMobject):
|
|||
else:
|
||||
self.line_spacing = self._font_size + self._font_size * self.line_spacing
|
||||
|
||||
color: ManimColor = ManimColor(color) if color else VMobject().color
|
||||
file_name = self._text2svg(color)
|
||||
parsed_color: ManimColor = ManimColor(color) if color else VMobject().color
|
||||
file_name = self._text2svg(parsed_color)
|
||||
|
||||
PangoUtils.remove_last_M(file_name)
|
||||
super().__init__(
|
||||
|
|
@ -1267,12 +1241,12 @@ class MarkupText(SVGMobject):
|
|||
# into a numpy array at the end, rather than creating
|
||||
# new numpy arrays every time a point or fixing line
|
||||
# is added (which is O(n^2) for numpy arrays).
|
||||
closed_curve_points = []
|
||||
closed_curve_points: list[Point3D] = []
|
||||
# OpenGL has points be part of quadratic Bezier curves;
|
||||
# Cairo uses cubic Bezier curves.
|
||||
if nppc == 3: # RendererType.OPENGL
|
||||
|
||||
def add_line_to(end):
|
||||
def add_line_to(end: Point3D) -> None:
|
||||
nonlocal closed_curve_points
|
||||
start = closed_curve_points[-1]
|
||||
closed_curve_points += [
|
||||
|
|
@ -1283,7 +1257,7 @@ class MarkupText(SVGMobject):
|
|||
|
||||
else: # RendererType.CAIRO
|
||||
|
||||
def add_line_to(end):
|
||||
def add_line_to(end: Point3D) -> None:
|
||||
nonlocal closed_curve_points
|
||||
start = closed_curve_points[-1]
|
||||
closed_curve_points += [
|
||||
|
|
@ -1331,7 +1305,7 @@ class MarkupText(SVGMobject):
|
|||
self.initial_height = self.height
|
||||
|
||||
@property
|
||||
def font_size(self):
|
||||
def font_size(self) -> float:
|
||||
return (
|
||||
self.height
|
||||
/ self.initial_height
|
||||
|
|
@ -1342,14 +1316,14 @@ class MarkupText(SVGMobject):
|
|||
)
|
||||
|
||||
@font_size.setter
|
||||
def font_size(self, font_val):
|
||||
def font_size(self, font_val: float) -> None:
|
||||
# TODO: use pango's font size scaling.
|
||||
if font_val <= 0:
|
||||
raise ValueError("font_size must be greater than 0.")
|
||||
else:
|
||||
self.scale(font_val / self.font_size)
|
||||
|
||||
def _text2hash(self, color: ParsableManimColor):
|
||||
def _text2hash(self, color: ParsableManimColor) -> str:
|
||||
"""Generates ``sha256`` hash for file name."""
|
||||
settings = (
|
||||
"MARKUPPANGO"
|
||||
|
|
@ -1366,11 +1340,11 @@ class MarkupText(SVGMobject):
|
|||
hasher.update(id_str.encode())
|
||||
return hasher.hexdigest()[:16]
|
||||
|
||||
def _text2svg(self, color: ParsableManimColor | None):
|
||||
def _text2svg(self, color: ParsableManimColor | None) -> str:
|
||||
"""Convert the text to SVG using Pango."""
|
||||
color = ManimColor(color)
|
||||
size = self._font_size
|
||||
line_spacing = self.line_spacing
|
||||
line_spacing: float = self.line_spacing
|
||||
size /= TEXT2SVG_ADJUSTMENT_FACTOR
|
||||
line_spacing /= TEXT2SVG_ADJUSTMENT_FACTOR
|
||||
|
||||
|
|
@ -1381,7 +1355,7 @@ class MarkupText(SVGMobject):
|
|||
file_name = dir_name / (hash_name + ".svg")
|
||||
|
||||
if file_name.exists():
|
||||
svg_file = str(file_name.resolve())
|
||||
svg_file: str = str(file_name.resolve())
|
||||
else:
|
||||
final_text = (
|
||||
f'<span foreground="{color.to_hex()}">{self.text}</span>'
|
||||
|
|
@ -1407,7 +1381,7 @@ class MarkupText(SVGMobject):
|
|||
)
|
||||
return svg_file
|
||||
|
||||
def _count_real_chars(self, s):
|
||||
def _count_real_chars(self, s: str) -> int:
|
||||
"""Counts characters that will be displayed.
|
||||
|
||||
This is needed for partial coloring or gradients, because space
|
||||
|
|
@ -1426,7 +1400,7 @@ class MarkupText(SVGMobject):
|
|||
count += 1
|
||||
return count
|
||||
|
||||
def _extract_gradient_tags(self):
|
||||
def _extract_gradient_tags(self) -> list[dict[str, Any]]:
|
||||
"""Used to determine which parts (if any) of the string should be formatted
|
||||
with a gradient.
|
||||
|
||||
|
|
@ -1437,7 +1411,7 @@ class MarkupText(SVGMobject):
|
|||
self.original_text,
|
||||
re.S,
|
||||
)
|
||||
gradientmap = []
|
||||
gradientmap: list[dict[str, Any]] = []
|
||||
for tag in tags:
|
||||
start = self._count_real_chars(self.original_text[: tag.start(0)])
|
||||
end = start + self._count_real_chars(tag.group(5))
|
||||
|
|
@ -1460,14 +1434,14 @@ class MarkupText(SVGMobject):
|
|||
)
|
||||
return gradientmap
|
||||
|
||||
def _parse_color(self, col):
|
||||
def _parse_color(self, col: str) -> str:
|
||||
"""Parse color given in ``<color>`` or ``<gradient>`` tags."""
|
||||
if re.match("#[0-9a-f]{6}", col):
|
||||
return col
|
||||
else:
|
||||
return ManimColor(col).to_hex()
|
||||
|
||||
def _extract_color_tags(self):
|
||||
def _extract_color_tags(self) -> list[dict[str, Any]]:
|
||||
"""Used to determine which parts (if any) of the string should be formatted
|
||||
with a custom color.
|
||||
|
||||
|
|
@ -1482,7 +1456,7 @@ class MarkupText(SVGMobject):
|
|||
re.S,
|
||||
)
|
||||
|
||||
colormap = []
|
||||
colormap: list[dict[str, Any]] = []
|
||||
for tag in tags:
|
||||
start = self._count_real_chars(self.original_text[: tag.start(0)])
|
||||
end = start + self._count_real_chars(tag.group(4))
|
||||
|
|
@ -1504,12 +1478,12 @@ class MarkupText(SVGMobject):
|
|||
)
|
||||
return colormap
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f"MarkupText({repr(self.original_text)})"
|
||||
|
||||
|
||||
@contextmanager
|
||||
def register_font(font_file: str | Path):
|
||||
def register_font(font_file: str | Path) -> Iterator[None]:
|
||||
"""Temporarily add a font file to Pango's search path.
|
||||
|
||||
This searches for the font_file at various places. The order it searches it described below.
|
||||
|
|
@ -1561,7 +1535,7 @@ def register_font(font_file: str | Path):
|
|||
logger.debug("Found file at %s", file_path.absolute())
|
||||
break
|
||||
else:
|
||||
error = f"Can't find {font_file}.Tried these : {possible_paths}"
|
||||
error = f"Can't find {font_file}. Checked paths: {possible_paths}"
|
||||
raise FileNotFoundError(error)
|
||||
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from collections.abc import Hashable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -14,7 +15,7 @@ from manim.utils.qhull import QuickHull
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.typing import Point3D
|
||||
from manim.typing import Point3D, Point3DLike_Array
|
||||
|
||||
__all__ = [
|
||||
"Polyhedron",
|
||||
|
|
@ -96,10 +97,10 @@ class Polyhedron(VGroup):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
vertex_coords: list[list[float] | np.ndarray],
|
||||
vertex_coords: Point3DLike_Array,
|
||||
faces_list: list[list[int]],
|
||||
faces_config: dict[str, str | int | float | bool] = {},
|
||||
graph_config: dict[str, str | int | float | bool] = {},
|
||||
graph_config: dict[str, Any] = {},
|
||||
):
|
||||
super().__init__()
|
||||
self.faces_config = dict(
|
||||
|
|
@ -116,7 +117,7 @@ class Polyhedron(VGroup):
|
|||
)
|
||||
self.vertex_coords = vertex_coords
|
||||
self.vertex_indices = list(range(len(self.vertex_coords)))
|
||||
self.layout = dict(enumerate(self.vertex_coords))
|
||||
self.layout: dict[Hashable, Any] = dict(enumerate(self.vertex_coords))
|
||||
self.faces_list = faces_list
|
||||
self.face_coords = [[self.layout[j] for j in i] for i in faces_list]
|
||||
self.edges = self.get_edges(self.faces_list)
|
||||
|
|
@ -129,14 +130,14 @@ class Polyhedron(VGroup):
|
|||
|
||||
def get_edges(self, faces_list: list[list[int]]) -> list[tuple[int, int]]:
|
||||
"""Creates list of cyclic pairwise tuples."""
|
||||
edges = []
|
||||
edges: list[tuple[int, int]] = []
|
||||
for face in faces_list:
|
||||
edges += zip(face, face[1:] + face[:1])
|
||||
return edges
|
||||
|
||||
def create_faces(
|
||||
self,
|
||||
face_coords: list[list[list | np.ndarray]],
|
||||
face_coords: Point3DLike_Array,
|
||||
) -> VGroup:
|
||||
"""Creates VGroup of faces from a list of face coordinates."""
|
||||
face_group = VGroup()
|
||||
|
|
@ -144,12 +145,12 @@ class Polyhedron(VGroup):
|
|||
face_group.add(Polygon(*face, **self.faces_config))
|
||||
return face_group
|
||||
|
||||
def update_faces(self, m: Mobject):
|
||||
def update_faces(self, m: Mobject) -> None:
|
||||
face_coords = self.extract_face_coords()
|
||||
new_faces = self.create_faces(face_coords)
|
||||
self.faces.match_points(new_faces)
|
||||
|
||||
def extract_face_coords(self) -> list[list[np.ndarray]]:
|
||||
def extract_face_coords(self) -> Point3DLike_Array:
|
||||
"""Extracts the coordinates of the vertices in the graph.
|
||||
Used for updating faces.
|
||||
"""
|
||||
|
|
@ -181,7 +182,7 @@ class Tetrahedron(Polyhedron):
|
|||
self.add(obj)
|
||||
"""
|
||||
|
||||
def __init__(self, edge_length: float = 1, **kwargs):
|
||||
def __init__(self, edge_length: float = 1, **kwargs: Any):
|
||||
unit = edge_length * np.sqrt(2) / 4
|
||||
super().__init__(
|
||||
vertex_coords=[
|
||||
|
|
@ -216,7 +217,7 @@ class Octahedron(Polyhedron):
|
|||
self.add(obj)
|
||||
"""
|
||||
|
||||
def __init__(self, edge_length: float = 1, **kwargs):
|
||||
def __init__(self, edge_length: float = 1, **kwargs: Any):
|
||||
unit = edge_length * np.sqrt(2) / 2
|
||||
super().__init__(
|
||||
vertex_coords=[
|
||||
|
|
@ -262,7 +263,7 @@ class Icosahedron(Polyhedron):
|
|||
self.add(obj)
|
||||
"""
|
||||
|
||||
def __init__(self, edge_length: float = 1, **kwargs):
|
||||
def __init__(self, edge_length: float = 1, **kwargs: Any):
|
||||
unit_a = edge_length * ((1 + np.sqrt(5)) / 4)
|
||||
unit_b = edge_length * (1 / 2)
|
||||
super().__init__(
|
||||
|
|
@ -327,7 +328,7 @@ class Dodecahedron(Polyhedron):
|
|||
self.add(obj)
|
||||
"""
|
||||
|
||||
def __init__(self, edge_length: float = 1, **kwargs):
|
||||
def __init__(self, edge_length: float = 1, **kwargs: Any):
|
||||
unit_a = edge_length * ((1 + np.sqrt(5)) / 4)
|
||||
unit_b = edge_length * ((3 + np.sqrt(5)) / 4)
|
||||
unit_c = edge_length * (1 / 2)
|
||||
|
|
@ -427,7 +428,7 @@ class ConvexHull3D(Polyhedron):
|
|||
self.add(dots)
|
||||
"""
|
||||
|
||||
def __init__(self, *points: Point3D, tolerance: float = 1e-5, **kwargs):
|
||||
def __init__(self, *points: Point3D, tolerance: float = 1e-5, **kwargs: Any):
|
||||
# Build Convex Hull
|
||||
array = np.array(points)
|
||||
hull = QuickHull(tolerance)
|
||||
|
|
|
|||
|
|
@ -24,35 +24,39 @@ from manim.utils.space_ops import get_unit_normal
|
|||
if TYPE_CHECKING:
|
||||
from manim.typing import Point3D, Vector3D
|
||||
|
||||
from ..types.vectorized_mobject import VMobject
|
||||
|
||||
def get_3d_vmob_gradient_start_and_end_points(vmob) -> tuple[Point3D, Point3D]:
|
||||
|
||||
def get_3d_vmob_gradient_start_and_end_points(
|
||||
vmob: VMobject,
|
||||
) -> tuple[Point3D, Point3D]:
|
||||
return (
|
||||
get_3d_vmob_start_corner(vmob),
|
||||
get_3d_vmob_end_corner(vmob),
|
||||
)
|
||||
|
||||
|
||||
def get_3d_vmob_start_corner_index(vmob) -> Literal[0]:
|
||||
def get_3d_vmob_start_corner_index(vmob: VMobject) -> Literal[0]:
|
||||
return 0
|
||||
|
||||
|
||||
def get_3d_vmob_end_corner_index(vmob) -> int:
|
||||
def get_3d_vmob_end_corner_index(vmob: VMobject) -> int:
|
||||
return ((len(vmob.points) - 1) // 6) * 3
|
||||
|
||||
|
||||
def get_3d_vmob_start_corner(vmob) -> Point3D:
|
||||
def get_3d_vmob_start_corner(vmob: VMobject) -> Point3D:
|
||||
if vmob.get_num_points() == 0:
|
||||
return np.array(ORIGIN)
|
||||
return vmob.points[get_3d_vmob_start_corner_index(vmob)]
|
||||
|
||||
|
||||
def get_3d_vmob_end_corner(vmob) -> Point3D:
|
||||
def get_3d_vmob_end_corner(vmob: VMobject) -> Point3D:
|
||||
if vmob.get_num_points() == 0:
|
||||
return np.array(ORIGIN)
|
||||
return vmob.points[get_3d_vmob_end_corner_index(vmob)]
|
||||
|
||||
|
||||
def get_3d_vmob_unit_normal(vmob, point_index: int) -> Vector3D:
|
||||
def get_3d_vmob_unit_normal(vmob: VMobject, point_index: int) -> Vector3D:
|
||||
n_points = vmob.get_num_points()
|
||||
if len(vmob.get_anchors()) <= 2:
|
||||
return np.array(UP)
|
||||
|
|
@ -68,9 +72,9 @@ def get_3d_vmob_unit_normal(vmob, point_index: int) -> Vector3D:
|
|||
return unit_normal
|
||||
|
||||
|
||||
def get_3d_vmob_start_corner_unit_normal(vmob) -> Vector3D:
|
||||
def get_3d_vmob_start_corner_unit_normal(vmob: VMobject) -> Vector3D:
|
||||
return get_3d_vmob_unit_normal(vmob, get_3d_vmob_start_corner_index(vmob))
|
||||
|
||||
|
||||
def get_3d_vmob_end_corner_unit_normal(vmob) -> Vector3D:
|
||||
def get_3d_vmob_end_corner_unit_normal(vmob: VMobject) -> Vector3D:
|
||||
return get_3d_vmob_unit_normal(vmob, get_3d_vmob_end_corner_index(vmob))
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from manim.typing import Point3DLike, Vector3D
|
||||
from manim.utils.color import BLUE, BLUE_D, BLUE_E, LIGHT_GREY, WHITE, interpolate_color
|
||||
|
||||
__all__ = [
|
||||
"ThreeDVMobject",
|
||||
"Surface",
|
||||
|
|
@ -19,8 +16,8 @@ __all__ = [
|
|||
"Torus",
|
||||
]
|
||||
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import Any, Callable
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
from typing_extensions import Self
|
||||
|
|
@ -34,12 +31,21 @@ from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
|||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.mobject.types.vectorized_mobject import VectorizedPoint, VGroup, VMobject
|
||||
from manim.utils.color import (
|
||||
BLUE,
|
||||
BLUE_D,
|
||||
BLUE_E,
|
||||
LIGHT_GREY,
|
||||
WHITE,
|
||||
ManimColor,
|
||||
ParsableManimColor,
|
||||
interpolate_color,
|
||||
)
|
||||
from manim.utils.iterables import tuplify
|
||||
from manim.utils.space_ops import normalize, perpendicular_bisector, z_to_vector
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.typing import Point3D, Point3DLike, Vector3DLike
|
||||
|
||||
|
||||
class ThreeDVMobject(VMobject, metaclass=ConvertToOpenGL):
|
||||
def __init__(self, shade_in_3d: bool = True, **kwargs):
|
||||
|
|
@ -116,19 +122,21 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
|
|||
) -> None:
|
||||
self.u_range = u_range
|
||||
self.v_range = v_range
|
||||
super().__init__(**kwargs)
|
||||
super().__init__(
|
||||
fill_color=fill_color,
|
||||
fill_opacity=fill_opacity,
|
||||
stroke_color=stroke_color,
|
||||
stroke_width=stroke_width,
|
||||
**kwargs,
|
||||
)
|
||||
self.resolution = resolution
|
||||
self.surface_piece_config = surface_piece_config
|
||||
self.fill_color: ManimColor = ManimColor(fill_color)
|
||||
self.fill_opacity = fill_opacity
|
||||
if checkerboard_colors:
|
||||
self.checkerboard_colors: list[ManimColor] = [
|
||||
ManimColor(x) for x in checkerboard_colors
|
||||
]
|
||||
else:
|
||||
self.checkerboard_colors = checkerboard_colors
|
||||
self.stroke_color: ManimColor = ManimColor(stroke_color)
|
||||
self.stroke_width = stroke_width
|
||||
self.should_make_jagged = should_make_jagged
|
||||
self.pre_function_handle_to_anchor_scale_factor = (
|
||||
pre_function_handle_to_anchor_scale_factor
|
||||
|
|
@ -510,6 +518,7 @@ class Cube(VGroup):
|
|||
face = Square(
|
||||
side_length=self.side_length,
|
||||
shade_in_3d=True,
|
||||
joint_type=LineJointType.BEVEL,
|
||||
)
|
||||
face.flip()
|
||||
face.shift(self.side_length * OUT / 2.0)
|
||||
|
|
@ -517,7 +526,8 @@ class Cube(VGroup):
|
|||
|
||||
self.add(face)
|
||||
|
||||
init_points = generate_points
|
||||
def init_points(self) -> None:
|
||||
self.generate_points()
|
||||
|
||||
|
||||
class Prism(Cube):
|
||||
|
|
@ -926,6 +936,10 @@ class Line3D(Cylinder):
|
|||
):
|
||||
self.thickness = thickness
|
||||
self.resolution = (2, resolution) if isinstance(resolution, int) else resolution
|
||||
|
||||
start = np.array(start, dtype=np.float64)
|
||||
end = np.array(end, dtype=np.float64)
|
||||
|
||||
self.set_start_and_end_attrs(start, end, **kwargs)
|
||||
if color is not None:
|
||||
self.set_color(color)
|
||||
|
|
@ -967,8 +981,8 @@ class Line3D(Cylinder):
|
|||
def pointify(
|
||||
self,
|
||||
mob_or_point: Mobject | Point3DLike,
|
||||
direction: Vector3D = None,
|
||||
) -> np.ndarray:
|
||||
direction: Vector3DLike | None = None,
|
||||
) -> Point3D:
|
||||
"""Gets a point representing the center of the :class:`Mobjects <.Mobject>`.
|
||||
|
||||
Parameters
|
||||
|
|
@ -1015,7 +1029,7 @@ class Line3D(Cylinder):
|
|||
def parallel_to(
|
||||
cls,
|
||||
line: Line3D,
|
||||
point: Vector3D = ORIGIN,
|
||||
point: Point3DLike = ORIGIN,
|
||||
length: float = 5,
|
||||
**kwargs,
|
||||
) -> Line3D:
|
||||
|
|
@ -1051,11 +1065,11 @@ class Line3D(Cylinder):
|
|||
line2 = Line3D.parallel_to(line1, color=YELLOW)
|
||||
self.add(ax, line1, line2)
|
||||
"""
|
||||
point = np.array(point)
|
||||
np_point = np.asarray(point)
|
||||
vect = normalize(line.vect)
|
||||
return cls(
|
||||
point + vect * length / 2,
|
||||
point - vect * length / 2,
|
||||
np_point + vect * length / 2,
|
||||
np_point - vect * length / 2,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
|
@ -1063,7 +1077,7 @@ class Line3D(Cylinder):
|
|||
def perpendicular_to(
|
||||
cls,
|
||||
line: Line3D,
|
||||
point: Vector3D = ORIGIN,
|
||||
point: Vector3DLike = ORIGIN,
|
||||
length: float = 5,
|
||||
**kwargs,
|
||||
) -> Line3D:
|
||||
|
|
@ -1099,17 +1113,17 @@ class Line3D(Cylinder):
|
|||
line2 = Line3D.perpendicular_to(line1, color=BLUE)
|
||||
self.add(ax, line1, line2)
|
||||
"""
|
||||
point = np.array(point)
|
||||
np_point = np.asarray(point)
|
||||
|
||||
norm = np.cross(line.vect, point - line.start)
|
||||
norm = np.cross(line.vect, np_point - line.start)
|
||||
if all(np.linalg.norm(norm) == np.zeros(3)):
|
||||
raise ValueError("Could not find the perpendicular.")
|
||||
|
||||
start, end = perpendicular_bisector([line.start, line.end], norm)
|
||||
vect = normalize(end - start)
|
||||
return cls(
|
||||
point + vect * length / 2,
|
||||
point - vect * length / 2,
|
||||
np_point + vect * length / 2,
|
||||
np_point - vect * length / 2,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
|
@ -1183,8 +1197,9 @@ class Arrow3D(Line3D):
|
|||
height=height,
|
||||
**kwargs,
|
||||
)
|
||||
self.cone.shift(end)
|
||||
self.end_point = VectorizedPoint(end)
|
||||
np_end = np.asarray(end, dtype=np.float64)
|
||||
self.cone.shift(np_end)
|
||||
self.end_point = VectorizedPoint(np_end)
|
||||
self.add(self.end_point, self.cone)
|
||||
self.set_color(color)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
|||
__all__ = ["AbstractImageMobject", "ImageMobject", "ImageMobjectFromCamera"]
|
||||
|
||||
import pathlib
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
|
@ -14,6 +14,7 @@ from PIL.Image import Resampling
|
|||
from manim.mobject.geometry.shape_matchers import SurroundingRectangle
|
||||
|
||||
from ... import config
|
||||
from ...camera.moving_camera import MovingCamera
|
||||
from ...constants import *
|
||||
from ...mobject.mobject import Mobject
|
||||
from ...utils.bezier import interpolate
|
||||
|
|
@ -23,12 +24,12 @@ 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
|
||||
from manim.typing import PixelArray, StrPath
|
||||
|
||||
from ...camera.moving_camera import MovingCamera
|
||||
|
||||
|
||||
class AbstractImageMobject(Mobject):
|
||||
|
|
@ -57,7 +58,7 @@ class AbstractImageMobject(Mobject):
|
|||
self.set_resampling_algorithm(resampling_algorithm)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def get_pixel_array(self) -> None:
|
||||
def get_pixel_array(self) -> PixelArray:
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_color(self, color, alpha=None, family=True):
|
||||
|
|
@ -205,6 +206,7 @@ class ImageMobject(AbstractImageMobject):
|
|||
self.pixel_array[:, :, :3] = (
|
||||
np.iinfo(self.pixel_array_dtype).max - self.pixel_array[:, :, :3]
|
||||
)
|
||||
self.orig_alpha_pixel_array = self.pixel_array[:, :, 3].copy()
|
||||
super().__init__(scale_to_resolution, **kwargs)
|
||||
|
||||
def get_pixel_array(self):
|
||||
|
|
@ -230,8 +232,7 @@ class ImageMobject(AbstractImageMobject):
|
|||
The alpha value of the object, 1 being opaque and 0 being
|
||||
transparent.
|
||||
"""
|
||||
self.pixel_array[:, :, 3] = int(255 * alpha)
|
||||
self.fill_opacity = alpha
|
||||
self.pixel_array[:, :, 3] = self.orig_alpha_pixel_array * alpha
|
||||
self.stroke_opacity = alpha
|
||||
return self
|
||||
|
||||
|
|
@ -303,7 +304,7 @@ class ImageMobject(AbstractImageMobject):
|
|||
class ImageMobjectFromCamera(AbstractImageMobject):
|
||||
def __init__(
|
||||
self,
|
||||
camera,
|
||||
camera: MovingCamera,
|
||||
default_display_frame_config: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["PMobject", "Mobject1D", "Mobject2D", "PGroup", "PointCloudDot", "Point"]
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -29,13 +30,17 @@ from ...utils.iterables import stretch_array_to_length
|
|||
__all__ = ["PMobject", "Mobject1D", "Mobject2D", "PGroup", "PointCloudDot", "Point"]
|
||||
|
||||
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, Point3DLike, Vector3D
|
||||
from manim.typing import (
|
||||
FloatRGBA_Array,
|
||||
FloatRGBALike_Array,
|
||||
ManimFloat,
|
||||
Point3D_Array,
|
||||
Point3DLike,
|
||||
Point3DLike_Array,
|
||||
)
|
||||
|
||||
|
||||
class PMobject(Mobject, metaclass=ConvertToOpenGL):
|
||||
|
|
@ -72,8 +77,8 @@ class PMobject(Mobject, metaclass=ConvertToOpenGL):
|
|||
super().__init__(**kwargs)
|
||||
|
||||
def reset_points(self) -> Self:
|
||||
self.rgbas = np.zeros((0, 4))
|
||||
self.points = np.zeros((0, 3))
|
||||
self.rgbas: FloatRGBA_Array = np.zeros((0, 4))
|
||||
self.points: Point3D_Array = np.zeros((0, 3))
|
||||
return self
|
||||
|
||||
def get_array_attrs(self) -> list[str]:
|
||||
|
|
@ -81,10 +86,10 @@ class PMobject(Mobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
def add_points(
|
||||
self,
|
||||
points: npt.NDArray,
|
||||
rgbas: npt.NDArray | None = None,
|
||||
points: Point3DLike_Array,
|
||||
rgbas: FloatRGBALike_Array | None = None,
|
||||
color: ParsableManimColor | None = None,
|
||||
alpha: float = 1,
|
||||
alpha: float = 1.0,
|
||||
) -> Self:
|
||||
"""Add points.
|
||||
|
||||
|
|
@ -349,7 +354,7 @@ class PointCloudDot(Mobject1D):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
center: Vector3D = ORIGIN,
|
||||
center: Point3DLike = ORIGIN,
|
||||
radius: float = 2.0,
|
||||
stroke_width: int = 2,
|
||||
density: int = DEFAULT_POINT_DENSITY_1D,
|
||||
|
|
@ -406,7 +411,7 @@ class Point(PMobject):
|
|||
"""
|
||||
|
||||
def __init__(
|
||||
self, location: Vector3D = ORIGIN, color: ManimColor = BLACK, **kwargs: Any
|
||||
self, location: Point3DLike = ORIGIN, color: ManimColor = BLACK, **kwargs: Any
|
||||
) -> None:
|
||||
self.location = location
|
||||
super().__init__(color=color, **kwargs)
|
||||
|
|
|
|||
|
|
@ -11,11 +11,10 @@ __all__ = [
|
|||
"DashedVMobject",
|
||||
]
|
||||
|
||||
|
||||
import itertools as it
|
||||
import sys
|
||||
from collections.abc import Hashable, Iterable, Mapping, Sequence
|
||||
from typing import TYPE_CHECKING, Callable, Literal
|
||||
from collections.abc import Callable, Hashable, Iterable, Mapping, Sequence
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
import numpy as np
|
||||
from PIL.Image import Image
|
||||
|
|
@ -48,8 +47,6 @@ 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
|
||||
|
||||
|
|
@ -57,6 +54,8 @@ if TYPE_CHECKING:
|
|||
CubicBezierPath,
|
||||
CubicBezierPointsLike,
|
||||
CubicSpline,
|
||||
FloatRGBA,
|
||||
FloatRGBA_Array,
|
||||
ManimFloat,
|
||||
MappingFunction,
|
||||
Point2DLike,
|
||||
|
|
@ -64,9 +63,8 @@ if TYPE_CHECKING:
|
|||
Point3D_Array,
|
||||
Point3DLike,
|
||||
Point3DLike_Array,
|
||||
RGBA_Array_Float,
|
||||
Vector3D,
|
||||
Zeros,
|
||||
Vector3DLike,
|
||||
)
|
||||
|
||||
# TODO
|
||||
|
|
@ -117,7 +115,7 @@ class VMobject(Mobject):
|
|||
background_stroke_width: float = 0,
|
||||
sheen_factor: float = 0.0,
|
||||
joint_type: LineJointType | None = None,
|
||||
sheen_direction: Vector3D = UL,
|
||||
sheen_direction: Vector3DLike = UL,
|
||||
close_new_points: bool = False,
|
||||
pre_function_handle_to_anchor_scale_factor: float = 0.01,
|
||||
make_smooth_after_applying_functions: bool = False,
|
||||
|
|
@ -142,7 +140,7 @@ class VMobject(Mobject):
|
|||
self.joint_type: LineJointType = (
|
||||
LineJointType.AUTO if joint_type is None else joint_type
|
||||
)
|
||||
self.sheen_direction: Vector3D = sheen_direction
|
||||
self.sheen_direction = sheen_direction
|
||||
self.close_new_points: bool = close_new_points
|
||||
self.pre_function_handle_to_anchor_scale_factor: float = (
|
||||
pre_function_handle_to_anchor_scale_factor
|
||||
|
|
@ -217,8 +215,10 @@ class VMobject(Mobject):
|
|||
return self
|
||||
|
||||
def generate_rgbas_array(
|
||||
self, color: ManimColor | list[ManimColor], opacity: float | Iterable[float]
|
||||
) -> RGBA_Array_Float:
|
||||
self,
|
||||
color: ParsableManimColor | Iterable[ManimColor] | None,
|
||||
opacity: float | Iterable[float],
|
||||
) -> FloatRGBA:
|
||||
"""
|
||||
First arg can be either a color, or a tuple/list of colors.
|
||||
Likewise, opacity can either be a float, or a tuple of floats.
|
||||
|
|
@ -232,7 +232,7 @@ class VMobject(Mobject):
|
|||
opacities: list[float] = [
|
||||
o if (o is not None) else 0.0 for o in tuplify(opacity)
|
||||
]
|
||||
rgbas: npt.NDArray[RGBA_Array_Float] = np.array(
|
||||
rgbas: FloatRGBA_Array = np.array(
|
||||
[c.to_rgba_with_alpha(o) for c, o in zip(*make_even(colors, opacities))],
|
||||
)
|
||||
|
||||
|
|
@ -247,7 +247,7 @@ class VMobject(Mobject):
|
|||
def update_rgbas_array(
|
||||
self,
|
||||
array_name: str,
|
||||
color: ManimColor | None = None,
|
||||
color: ParsableManimColor | Iterable[ManimColor] | None = None,
|
||||
opacity: float | None = None,
|
||||
) -> Self:
|
||||
rgbas = self.generate_rgbas_array(color, opacity)
|
||||
|
|
@ -315,7 +315,7 @@ class VMobject(Mobject):
|
|||
for submobject in self.submobjects:
|
||||
submobject.set_fill(color, opacity, family)
|
||||
self.update_rgbas_array("fill_rgbas", color, opacity)
|
||||
self.fill_rgbas: RGBA_Array_Float
|
||||
self.fill_rgbas: FloatRGBA_Array
|
||||
if opacity is not None:
|
||||
self.fill_opacity = opacity
|
||||
return self
|
||||
|
|
@ -395,7 +395,7 @@ class VMobject(Mobject):
|
|||
background_stroke_width: float | None = None,
|
||||
background_stroke_opacity: float | None = None,
|
||||
sheen_factor: float | None = None,
|
||||
sheen_direction: Vector3D | None = None,
|
||||
sheen_direction: Vector3DLike | None = None,
|
||||
background_image: Image | str | None = None,
|
||||
family: bool = True,
|
||||
) -> Self:
|
||||
|
|
@ -541,7 +541,7 @@ class VMobject(Mobject):
|
|||
super().fade(darkness, family)
|
||||
return self
|
||||
|
||||
def get_fill_rgbas(self) -> RGBA_Array_Float | Zeros:
|
||||
def get_fill_rgbas(self) -> FloatRGBA_Array:
|
||||
try:
|
||||
return self.fill_rgbas
|
||||
except AttributeError:
|
||||
|
|
@ -574,13 +574,13 @@ class VMobject(Mobject):
|
|||
def get_fill_opacities(self) -> npt.NDArray[ManimFloat]:
|
||||
return self.get_fill_rgbas()[:, 3]
|
||||
|
||||
def get_stroke_rgbas(self, background: bool = False) -> RGBA_Array_float | Zeros:
|
||||
def get_stroke_rgbas(self, background: bool = False) -> FloatRGBA_Array:
|
||||
try:
|
||||
if background:
|
||||
self.background_stroke_rgbas: RGBA_Array_Float
|
||||
self.background_stroke_rgbas: FloatRGBA_Array
|
||||
rgbas = self.background_stroke_rgbas
|
||||
else:
|
||||
self.stroke_rgbas: RGBA_Array_Float
|
||||
self.stroke_rgbas: FloatRGBA_Array
|
||||
rgbas = self.stroke_rgbas
|
||||
return rgbas
|
||||
except AttributeError:
|
||||
|
|
@ -618,9 +618,9 @@ class VMobject(Mobject):
|
|||
return self.get_stroke_color()
|
||||
return self.get_fill_color()
|
||||
|
||||
color = property(get_color, set_color)
|
||||
color: ManimColor = property(get_color, set_color)
|
||||
|
||||
def set_sheen_direction(self, direction: Vector3D, family: bool = True) -> Self:
|
||||
def set_sheen_direction(self, direction: Vector3DLike, family: bool = True) -> Self:
|
||||
"""Sets the direction of the applied sheen.
|
||||
|
||||
Parameters
|
||||
|
|
@ -639,16 +639,16 @@ class VMobject(Mobject):
|
|||
:meth:`~.VMobject.set_sheen`
|
||||
:meth:`~.VMobject.rotate_sheen_direction`
|
||||
"""
|
||||
direction = np.array(direction)
|
||||
direction_copy = np.array(direction)
|
||||
if family:
|
||||
for submob in self.get_family():
|
||||
submob.sheen_direction = direction
|
||||
submob.sheen_direction = direction_copy.copy()
|
||||
else:
|
||||
self.sheen_direction: Vector3D = direction
|
||||
self.sheen_direction = direction_copy
|
||||
return self
|
||||
|
||||
def rotate_sheen_direction(
|
||||
self, angle: float, axis: Vector3D = OUT, family: bool = True
|
||||
self, angle: float, axis: Vector3DLike = OUT, family: bool = True
|
||||
) -> Self:
|
||||
"""Rotates the direction of the applied sheen.
|
||||
|
||||
|
|
@ -681,7 +681,7 @@ class VMobject(Mobject):
|
|||
return self
|
||||
|
||||
def set_sheen(
|
||||
self, factor: float, direction: Vector3D | None = None, family: bool = True
|
||||
self, factor: float, direction: Vector3DLike | None = None, family: bool = True
|
||||
) -> Self:
|
||||
"""Applies a color gradient from a direction.
|
||||
|
||||
|
|
@ -1189,7 +1189,7 @@ class VMobject(Mobject):
|
|||
def rotate(
|
||||
self,
|
||||
angle: float,
|
||||
axis: Vector3D = OUT,
|
||||
axis: Vector3DLike = OUT,
|
||||
about_point: Point3DLike | None = None,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
|
|
@ -1916,7 +1916,6 @@ class VMobject(Mobject):
|
|||
return self
|
||||
num_curves = vmobject.get_num_curves()
|
||||
if num_curves == 0:
|
||||
self.clear_points()
|
||||
return self
|
||||
|
||||
# The following two lines will compute which Bézier curves of the given Mobject must be processed.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["ValueTracker", "ComplexValueTracker"]
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -11,6 +12,11 @@ from manim.mobject.mobject import Mobject
|
|||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.utils.paths import straight_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim.typing import PathFuncType
|
||||
|
||||
|
||||
class ValueTracker(Mobject, metaclass=ConvertToOpenGL):
|
||||
"""A mobject that can be used for tracking (real-valued) parameters.
|
||||
|
|
@ -69,69 +75,131 @@ class ValueTracker(Mobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, value=0, **kwargs):
|
||||
def __init__(self, value: float = 0, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.set(points=np.zeros((1, 3)))
|
||||
self.set_value(value)
|
||||
|
||||
def get_value(self) -> float:
|
||||
"""Get the current value of this ValueTracker."""
|
||||
return self.points[0, 0]
|
||||
value: float = self.points[0, 0]
|
||||
return value
|
||||
|
||||
def set_value(self, value: float):
|
||||
"""Sets a new scalar value to the ValueTracker"""
|
||||
def set_value(self, value: float) -> Self:
|
||||
"""Sets a new scalar value to the ValueTracker."""
|
||||
self.points[0, 0] = value
|
||||
return self
|
||||
|
||||
def increment_value(self, d_value: float):
|
||||
"""Increments (adds) a scalar value to the ValueTracker"""
|
||||
def increment_value(self, d_value: float) -> Self:
|
||||
"""Increments (adds) a scalar value to the ValueTracker."""
|
||||
self.set_value(self.get_value() + d_value)
|
||||
return self
|
||||
|
||||
def __bool__(self):
|
||||
"""Return whether the value of this value tracker evaluates as true."""
|
||||
def __bool__(self) -> bool:
|
||||
"""Return whether the value of this ValueTracker evaluates as true."""
|
||||
return bool(self.get_value())
|
||||
|
||||
def __iadd__(self, d_value: float):
|
||||
"""adds ``+=`` syntax to increment the value of the ValueTracker"""
|
||||
def __add__(self, d_value: float | Mobject) -> ValueTracker:
|
||||
"""Return a new :class:`ValueTracker` whose value is the current tracker's value plus
|
||||
``d_value``.
|
||||
"""
|
||||
if isinstance(d_value, Mobject):
|
||||
raise ValueError(
|
||||
"Cannot increment ValueTracker by a Mobject. Please provide a scalar value."
|
||||
)
|
||||
return ValueTracker(self.get_value() + d_value)
|
||||
|
||||
def __iadd__(self, d_value: float | Mobject) -> Self:
|
||||
"""adds ``+=`` syntax to increment the value of the ValueTracker."""
|
||||
if isinstance(d_value, Mobject):
|
||||
raise ValueError(
|
||||
"Cannot increment ValueTracker by a Mobject. Please provide a scalar value."
|
||||
)
|
||||
self.increment_value(d_value)
|
||||
return self
|
||||
|
||||
def __ifloordiv__(self, d_value: float):
|
||||
"""Set the value of this value tracker to the floor division of the current value by ``d_value``."""
|
||||
def __floordiv__(self, d_value: float) -> ValueTracker:
|
||||
"""Return a new :class:`ValueTracker` whose value is the floor division of the current
|
||||
tracker's value by ``d_value``.
|
||||
"""
|
||||
return ValueTracker(self.get_value() // d_value)
|
||||
|
||||
def __ifloordiv__(self, d_value: float) -> Self:
|
||||
"""Set the value of this ValueTracker to the floor division of the current value by ``d_value``."""
|
||||
self.set_value(self.get_value() // d_value)
|
||||
return self
|
||||
|
||||
def __imod__(self, d_value: float):
|
||||
"""Set the value of this value tracker to the current value modulo ``d_value``."""
|
||||
def __mod__(self, d_value: float) -> ValueTracker:
|
||||
"""Return a new :class:`ValueTracker` whose value is the current tracker's value
|
||||
modulo ``d_value``.
|
||||
"""
|
||||
return ValueTracker(self.get_value() % d_value)
|
||||
|
||||
def __imod__(self, d_value: float) -> Self:
|
||||
"""Set the value of this ValueTracker to the current value modulo ``d_value``."""
|
||||
self.set_value(self.get_value() % d_value)
|
||||
return self
|
||||
|
||||
def __imul__(self, d_value: float):
|
||||
"""Set the value of this value tracker to the product of the current value and ``d_value``."""
|
||||
def __mul__(self, d_value: float) -> ValueTracker:
|
||||
"""Return a new :class:`ValueTracker` whose value is the current tracker's value multiplied by
|
||||
``d_value``.
|
||||
"""
|
||||
return ValueTracker(self.get_value() * d_value)
|
||||
|
||||
def __imul__(self, d_value: float) -> Self:
|
||||
"""Set the value of this ValueTracker to the product of the current value and ``d_value``."""
|
||||
self.set_value(self.get_value() * d_value)
|
||||
return self
|
||||
|
||||
def __ipow__(self, d_value: float):
|
||||
"""Set the value of this value tracker to the current value raised to the power of ``d_value``."""
|
||||
def __pow__(self, d_value: float) -> ValueTracker:
|
||||
"""Return a new :class:`ValueTracker` whose value is the current tracker's value raised to the
|
||||
power of ``d_value``.
|
||||
"""
|
||||
return ValueTracker(self.get_value() ** d_value)
|
||||
|
||||
def __ipow__(self, d_value: float) -> Self:
|
||||
"""Set the value of this ValueTracker to the current value raised to the power of ``d_value``."""
|
||||
self.set_value(self.get_value() ** d_value)
|
||||
return self
|
||||
|
||||
def __isub__(self, d_value: float):
|
||||
"""adds ``-=`` syntax to decrement the value of the ValueTracker"""
|
||||
def __sub__(self, d_value: float | Mobject) -> ValueTracker:
|
||||
"""Return a new :class:`ValueTracker` whose value is the current tracker's value minus
|
||||
``d_value``.
|
||||
"""
|
||||
if isinstance(d_value, Mobject):
|
||||
raise ValueError(
|
||||
"Cannot decrement ValueTracker by a Mobject. Please provide a scalar value."
|
||||
)
|
||||
return ValueTracker(self.get_value() - d_value)
|
||||
|
||||
def __isub__(self, d_value: float | Mobject) -> Self:
|
||||
"""Adds ``-=`` syntax to decrement the value of the ValueTracker."""
|
||||
if isinstance(d_value, Mobject):
|
||||
raise ValueError(
|
||||
"Cannot decrement ValueTracker by a Mobject. Please provide a scalar value."
|
||||
)
|
||||
self.increment_value(-d_value)
|
||||
return self
|
||||
|
||||
def __itruediv__(self, d_value: float):
|
||||
"""Sets the value of this value tracker to the current value divided by ``d_value``."""
|
||||
def __truediv__(self, d_value: float) -> ValueTracker:
|
||||
"""Return a new :class:`ValueTracker` whose value is the current tracker's value
|
||||
divided by ``d_value``.
|
||||
"""
|
||||
return ValueTracker(self.get_value() / d_value)
|
||||
|
||||
def __itruediv__(self, d_value: float) -> Self:
|
||||
"""Sets the value of this ValueTracker to the current value divided by ``d_value``."""
|
||||
self.set_value(self.get_value() / d_value)
|
||||
return self
|
||||
|
||||
def interpolate(self, mobject1, mobject2, alpha, path_func=straight_path()):
|
||||
"""
|
||||
Turns self into an interpolation between mobject1
|
||||
and mobject2.
|
||||
"""
|
||||
def interpolate(
|
||||
self,
|
||||
mobject1: Mobject,
|
||||
mobject2: Mobject,
|
||||
alpha: float,
|
||||
path_func: PathFuncType = straight_path(),
|
||||
) -> Self:
|
||||
"""Turns ``self`` into an interpolation between ``mobject1`` and ``mobject2``."""
|
||||
self.set(points=path_func(mobject1.points, mobject2.points, alpha))
|
||||
return self
|
||||
|
||||
|
|
@ -139,6 +207,8 @@ class ValueTracker(Mobject, metaclass=ConvertToOpenGL):
|
|||
class ComplexValueTracker(ValueTracker):
|
||||
"""Tracks a complex-valued parameter.
|
||||
|
||||
The value is internally stored as a points array [a, b, 0]. This can be accessed directly
|
||||
to represent the value geometrically, see the usage example.
|
||||
When the value is set through :attr:`animate`, the value will take a straight path from the
|
||||
source point to the destination point.
|
||||
|
||||
|
|
@ -161,16 +231,12 @@ class ComplexValueTracker(ValueTracker):
|
|||
self.play(tracker.animate.set_value(tracker.get_value() / (-2 + 3j)))
|
||||
"""
|
||||
|
||||
def get_value(self):
|
||||
"""Get the current value of this value tracker as a complex number.
|
||||
|
||||
The value is internally stored as a points array [a, b, 0]. This can be accessed directly
|
||||
to represent the value geometrically, see the usage example.
|
||||
"""
|
||||
def get_value(self) -> complex: # type: ignore [override]
|
||||
"""Get the current value of this ComplexValueTracker as a complex number."""
|
||||
return complex(*self.points[0, :2])
|
||||
|
||||
def set_value(self, z):
|
||||
"""Sets a new complex value to the ComplexValueTracker"""
|
||||
z = complex(z)
|
||||
def set_value(self, value: complex | float) -> Self:
|
||||
"""Sets a new complex value to the ComplexValueTracker."""
|
||||
z = complex(value)
|
||||
self.points[0, :2] = (z.real, z.imag)
|
||||
return self
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ __all__ = [
|
|||
|
||||
import itertools as it
|
||||
import random
|
||||
from collections.abc import Iterable, Sequence
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from math import ceil, floor
|
||||
from typing import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
|
@ -43,6 +43,15 @@ from ..utils.color import (
|
|||
from ..utils.rate_functions import ease_out_sine, linear
|
||||
from ..utils.simple_functions import sigmoid
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.typing import (
|
||||
FloatRGB,
|
||||
FloatRGB_Array,
|
||||
FloatRGBA_Array,
|
||||
Point3D,
|
||||
Vector3D,
|
||||
)
|
||||
|
||||
DEFAULT_SCALAR_FIELD_COLORS: list = [BLUE_E, GREEN, YELLOW, RED]
|
||||
|
||||
|
||||
|
|
@ -74,9 +83,9 @@ class VectorField(VGroup):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[np.ndarray], np.ndarray],
|
||||
func: Callable[[Point3D], Vector3D],
|
||||
color: ParsableManimColor | None = None,
|
||||
color_scheme: Callable[[np.ndarray], float] | None = None,
|
||||
color_scheme: Callable[[Vector3D], float] | None = None,
|
||||
min_color_scheme_value: float = 0,
|
||||
max_color_scheme_value: float = 2,
|
||||
colors: Sequence[ParsableManimColor] = DEFAULT_SCALAR_FIELD_COLORS,
|
||||
|
|
@ -88,13 +97,13 @@ class VectorField(VGroup):
|
|||
self.single_color = False
|
||||
if color_scheme is None:
|
||||
|
||||
def color_scheme(p):
|
||||
return np.linalg.norm(p)
|
||||
def color_scheme(vec: Vector3D) -> float:
|
||||
return np.linalg.norm(vec)
|
||||
|
||||
self.color_scheme = color_scheme # TODO maybe other default for direction?
|
||||
self.rgbs = np.array(list(map(color_to_rgb, colors)))
|
||||
self.rgbs: FloatRGB_Array = np.array(list(map(color_to_rgb, colors)))
|
||||
|
||||
def pos_to_rgb(pos: np.ndarray) -> tuple[float, float, float, float]:
|
||||
def pos_to_rgb(pos: Point3D) -> FloatRGB:
|
||||
vec = self.func(pos)
|
||||
color_value = np.clip(
|
||||
self.color_scheme(vec),
|
||||
|
|
@ -107,8 +116,8 @@ class VectorField(VGroup):
|
|||
color_value,
|
||||
)
|
||||
alpha *= len(self.rgbs) - 1
|
||||
c1 = self.rgbs[int(alpha)]
|
||||
c2 = self.rgbs[min(int(alpha + 1), len(self.rgbs) - 1)]
|
||||
c1: FloatRGB = self.rgbs[int(alpha)]
|
||||
c2: FloatRGB = self.rgbs[min(int(alpha + 1), len(self.rgbs) - 1)]
|
||||
alpha %= 1
|
||||
return interpolate(c1, c2, alpha)
|
||||
|
||||
|
|
@ -418,7 +427,7 @@ class VectorField(VGroup):
|
|||
start: float,
|
||||
end: float,
|
||||
colors: Iterable[ParsableManimColor],
|
||||
):
|
||||
) -> Callable[[Sequence[float], float], FloatRGBA_Array]:
|
||||
"""
|
||||
Generates a gradient of rgbas as a numpy array
|
||||
|
||||
|
|
@ -435,9 +444,9 @@ class VectorField(VGroup):
|
|||
-------
|
||||
function to generate the gradients as numpy arrays representing rgba values
|
||||
"""
|
||||
rgbs = np.array([color_to_rgb(c) for c in colors])
|
||||
rgbs: FloatRGB_Array = np.array([color_to_rgb(c) for c in colors])
|
||||
|
||||
def func(values, opacity=1):
|
||||
def func(values: Sequence[float], opacity: float = 1.0) -> FloatRGBA_Array:
|
||||
alphas = inverse_interpolate(start, end, np.array(values))
|
||||
alphas = np.clip(alphas, 0, 1)
|
||||
scaled_alphas = alphas * (len(rgbs) - 1)
|
||||
|
|
@ -445,12 +454,14 @@ class VectorField(VGroup):
|
|||
next_indices = np.clip(indices + 1, 0, len(rgbs) - 1)
|
||||
inter_alphas = scaled_alphas % 1
|
||||
inter_alphas = inter_alphas.repeat(3).reshape((len(indices), 3))
|
||||
result = interpolate(rgbs[indices], rgbs[next_indices], inter_alphas)
|
||||
result = np.concatenate(
|
||||
(result, np.full([len(result), 1], opacity)),
|
||||
new_rgbs: FloatRGB_Array = interpolate(
|
||||
rgbs[indices], rgbs[next_indices], inter_alphas
|
||||
)
|
||||
new_rgbas: FloatRGBA_Array = np.concatenate(
|
||||
(new_rgbs, np.full([len(new_rgbs), 1], opacity)),
|
||||
axis=1,
|
||||
)
|
||||
return result
|
||||
return new_rgbas
|
||||
|
||||
return func
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -13,9 +14,7 @@ from ..scene.scene_file_writer import SceneFileWriter
|
|||
from ..utils.exceptions import EndSceneEarlyException
|
||||
from ..utils.iterables import list_update
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.animation.animation import Animation
|
||||
from manim.scene.scene import Scene
|
||||
|
||||
|
|
@ -27,16 +26,21 @@ __all__ = ["CairoRenderer"]
|
|||
class CairoRenderer:
|
||||
"""A renderer using Cairo.
|
||||
|
||||
num_plays : Number of play() functions in the scene.
|
||||
time: time elapsed since initialisation of scene.
|
||||
Attributes
|
||||
----------
|
||||
num_plays : int
|
||||
Number of play() functions in the scene.
|
||||
|
||||
time : float
|
||||
Time elapsed since initialisation of scene.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_writer_class=SceneFileWriter,
|
||||
camera_class=None,
|
||||
skip_animations=False,
|
||||
**kwargs,
|
||||
file_writer_class: type[SceneFileWriter] = SceneFileWriter,
|
||||
camera_class: type[Camera] | None = None,
|
||||
skip_animations: bool = False,
|
||||
**kwargs: Any,
|
||||
):
|
||||
# All of the following are set to EITHER the value passed via kwargs,
|
||||
# OR the value stored in the global config dict at the time of
|
||||
|
|
@ -46,12 +50,12 @@ class CairoRenderer:
|
|||
self.camera = camera_cls()
|
||||
self._original_skipping_status = skip_animations
|
||||
self.skip_animations = skip_animations
|
||||
self.animations_hashes = []
|
||||
self.animations_hashes: list[str | None] = []
|
||||
self.num_plays = 0
|
||||
self.time = 0
|
||||
self.static_image = None
|
||||
self.time = 0.0
|
||||
self.static_image: PixelArray | None = None
|
||||
|
||||
def init_scene(self, scene):
|
||||
def init_scene(self, scene: Scene) -> None:
|
||||
self.file_writer: Any = self._file_writer_class(
|
||||
self,
|
||||
scene.__class__.__name__,
|
||||
|
|
@ -61,8 +65,8 @@ class CairoRenderer:
|
|||
self,
|
||||
scene: Scene,
|
||||
*args: Animation | Mobject | _AnimationBuilder,
|
||||
**kwargs,
|
||||
):
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
# Reset skip_animations to the original state.
|
||||
# Needed when rendering only some animations, and skipping others.
|
||||
self.skip_animations = self._original_skipping_status
|
||||
|
|
@ -79,6 +83,7 @@ class CairoRenderer:
|
|||
logger.info("Caching disabled.")
|
||||
hash_current_animation = f"uncached_{self.num_plays:05}"
|
||||
else:
|
||||
assert scene.animations is not None
|
||||
hash_current_animation = get_hash_from_play_call(
|
||||
scene,
|
||||
self.camera,
|
||||
|
|
@ -119,12 +124,12 @@ class CairoRenderer:
|
|||
|
||||
def update_frame( # TODO Description in Docstring
|
||||
self,
|
||||
scene,
|
||||
mobjects: typing.Iterable[Mobject] | None = None,
|
||||
scene: Scene,
|
||||
mobjects: Iterable[Mobject] | None = None,
|
||||
include_submobjects: bool = True,
|
||||
ignore_skipping: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Update the frame.
|
||||
|
||||
Parameters
|
||||
|
|
@ -139,7 +144,6 @@ class CairoRenderer:
|
|||
ignore_skipping
|
||||
|
||||
**kwargs
|
||||
|
||||
"""
|
||||
if self.skip_animations and not ignore_skipping:
|
||||
return
|
||||
|
|
@ -156,25 +160,28 @@ class CairoRenderer:
|
|||
kwargs["include_submobjects"] = include_submobjects
|
||||
self.camera.capture_mobjects(mobjects, **kwargs)
|
||||
|
||||
def render(self, scene, time, moving_mobjects):
|
||||
def render(
|
||||
self,
|
||||
scene: Scene,
|
||||
time: float,
|
||||
moving_mobjects: Iterable[Mobject] | None = None,
|
||||
) -> None:
|
||||
self.update_frame(scene, moving_mobjects)
|
||||
self.add_frame(self.get_frame())
|
||||
|
||||
def get_frame(self) -> PixelArray:
|
||||
"""
|
||||
Gets the current frame as NumPy array.
|
||||
"""Gets the current frame as NumPy array.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.array
|
||||
PixelArray
|
||||
NumPy array of pixel values of each pixel in screen.
|
||||
The shape of the array is height x width x 3
|
||||
The shape of the array is height x width x 3.
|
||||
"""
|
||||
return np.array(self.camera.pixel_array)
|
||||
|
||||
def add_frame(self, frame: np.ndarray, num_frames: int = 1):
|
||||
"""
|
||||
Adds a frame to the video_file_stream
|
||||
def add_frame(self, frame: PixelArray, num_frames: int = 1) -> None:
|
||||
"""Adds a frame to the video_file_stream
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
|
@ -189,7 +196,7 @@ class CairoRenderer:
|
|||
self.time += num_frames * dt
|
||||
self.file_writer.write_frame(frame, num_frames=num_frames)
|
||||
|
||||
def freeze_current_frame(self, duration: float):
|
||||
def freeze_current_frame(self, duration: float) -> None:
|
||||
"""Adds a static frame to the movie for a given duration. The static frame is the current frame.
|
||||
|
||||
Parameters
|
||||
|
|
@ -203,19 +210,18 @@ class CairoRenderer:
|
|||
num_frames=int(duration / dt),
|
||||
)
|
||||
|
||||
def show_frame(self):
|
||||
"""
|
||||
Opens the current frame in the Default Image Viewer
|
||||
def show_frame(self, scene: Scene) -> None:
|
||||
"""Opens the current frame in the Default Image Viewer
|
||||
of your system.
|
||||
"""
|
||||
self.update_frame(ignore_skipping=True)
|
||||
self.update_frame(scene, ignore_skipping=True)
|
||||
self.camera.get_image().show()
|
||||
|
||||
def save_static_frame_data(
|
||||
self,
|
||||
scene: Scene,
|
||||
static_mobjects: typing.Iterable[Mobject],
|
||||
) -> typing.Iterable[Mobject] | None:
|
||||
static_mobjects: Iterable[Mobject],
|
||||
) -> PixelArray | None:
|
||||
"""Compute and save the static frame, that will be reused at each frame
|
||||
to avoid unnecessarily computing static mobjects.
|
||||
|
||||
|
|
@ -224,12 +230,12 @@ class CairoRenderer:
|
|||
scene
|
||||
The scene played.
|
||||
static_mobjects
|
||||
Static mobjects of the scene. If None, self.static_image is set to None
|
||||
Static mobjects of the scene. If None, self.static_image is set to None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
typing.Iterable[Mobject]
|
||||
The static image computed.
|
||||
PixelArray | None
|
||||
The static image computed. The return value is None if there are no static mobjects in the scene.
|
||||
"""
|
||||
self.static_image = None
|
||||
if not static_mobjects:
|
||||
|
|
@ -238,9 +244,8 @@ class CairoRenderer:
|
|||
self.static_image = self.get_frame()
|
||||
return self.static_image
|
||||
|
||||
def update_skipping_status(self):
|
||||
"""
|
||||
This method is used internally to check if the current
|
||||
def update_skipping_status(self) -> None:
|
||||
"""This method is used internally to check if the current
|
||||
animation needs to be skipped or not. It also checks if
|
||||
the number of animations that were played correspond to
|
||||
the number of animations that need to be played, and
|
||||
|
|
@ -263,7 +268,7 @@ class CairoRenderer:
|
|||
self.skip_animations = True
|
||||
raise EndSceneEarlyException()
|
||||
|
||||
def scene_finished(self, scene):
|
||||
def scene_finished(self, scene: Scene) -> None:
|
||||
# If no animations in scene, render an image instead
|
||||
if self.num_plays:
|
||||
self.file_writer.finish()
|
||||
|
|
@ -277,4 +282,4 @@ class CairoRenderer:
|
|||
if config["save_last_frame"]:
|
||||
self.static_image = None
|
||||
self.update_frame(scene)
|
||||
self.file_writer.save_final_image(self.camera.get_image())
|
||||
self.file_writer.save_image(self.camera.get_image())
|
||||
|
|
|
|||
|
|
@ -4,14 +4,18 @@ import contextlib
|
|||
import itertools as it
|
||||
import time
|
||||
from functools import cached_property
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import moderngl
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from manim import config, logger
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject, OpenGLPoint
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
OpenGLMobject,
|
||||
OpenGLPoint,
|
||||
_AnimationBuilder,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
|
||||
from manim.utils.caching import handle_caching_play
|
||||
from manim.utils.color import color_to_rgba
|
||||
|
|
@ -35,6 +39,15 @@ from .vectorized_mobject_rendering import (
|
|||
render_opengl_vectorized_mobject_stroke,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim.animation.animation import Animation
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.scene.scene import Scene
|
||||
from manim.typing import Point3D
|
||||
|
||||
|
||||
__all__ = ["OpenGLCamera", "OpenGLRenderer"]
|
||||
|
||||
|
||||
|
|
@ -102,7 +115,7 @@ class OpenGLCamera(OpenGLMobject):
|
|||
self.euler_angles = euler_angles
|
||||
self.refresh_rotation_matrix()
|
||||
|
||||
def get_position(self):
|
||||
def get_position(self) -> Point3D:
|
||||
return self.model_matrix[:, 3][:3]
|
||||
|
||||
def set_position(self, position):
|
||||
|
|
@ -123,7 +136,7 @@ class OpenGLCamera(OpenGLMobject):
|
|||
self.set_height(self.frame_shape[1], stretch=True)
|
||||
self.move_to(self.center_point)
|
||||
|
||||
def to_default_state(self):
|
||||
def to_default_state(self) -> Self:
|
||||
self.center()
|
||||
self.set_height(config["frame_height"])
|
||||
self.set_width(config["frame_width"])
|
||||
|
|
@ -166,28 +179,28 @@ class OpenGLCamera(OpenGLMobject):
|
|||
self.refresh_rotation_matrix()
|
||||
return self
|
||||
|
||||
def set_theta(self, theta):
|
||||
def set_theta(self, theta: float) -> Self:
|
||||
return self.set_euler_angles(theta=theta)
|
||||
|
||||
def set_phi(self, phi):
|
||||
def set_phi(self, phi: float) -> Self:
|
||||
return self.set_euler_angles(phi=phi)
|
||||
|
||||
def set_gamma(self, gamma):
|
||||
def set_gamma(self, gamma: float) -> Self:
|
||||
return self.set_euler_angles(gamma=gamma)
|
||||
|
||||
def increment_theta(self, dtheta):
|
||||
def increment_theta(self, dtheta: float) -> Self:
|
||||
self.euler_angles[0] += dtheta
|
||||
self.refresh_rotation_matrix()
|
||||
return self
|
||||
|
||||
def increment_phi(self, dphi):
|
||||
def increment_phi(self, dphi: float) -> Self:
|
||||
phi = self.euler_angles[1]
|
||||
new_phi = clip(phi + dphi, -PI / 2, PI / 2)
|
||||
self.euler_angles[1] = new_phi
|
||||
self.refresh_rotation_matrix()
|
||||
return self
|
||||
|
||||
def increment_gamma(self, dgamma):
|
||||
def increment_gamma(self, dgamma: float) -> Self:
|
||||
self.euler_angles[2] += dgamma
|
||||
self.refresh_rotation_matrix()
|
||||
return self
|
||||
|
|
@ -199,15 +212,15 @@ class OpenGLCamera(OpenGLMobject):
|
|||
# Assumes first point is at the center
|
||||
return self.points[0]
|
||||
|
||||
def get_width(self):
|
||||
def get_width(self) -> float:
|
||||
points = self.points
|
||||
return points[2, 0] - points[1, 0]
|
||||
|
||||
def get_height(self):
|
||||
def get_height(self) -> float:
|
||||
points = self.points
|
||||
return points[4, 1] - points[3, 1]
|
||||
|
||||
def get_focal_distance(self):
|
||||
def get_focal_distance(self) -> float:
|
||||
return self.focal_distance * self.get_height()
|
||||
|
||||
def interpolate(self, *args, **kwargs):
|
||||
|
|
@ -236,12 +249,14 @@ class OpenGLRenderer:
|
|||
self.camera = OpenGLCamera()
|
||||
self.pressed_keys = set()
|
||||
|
||||
self.window = None
|
||||
|
||||
# Initialize texture map.
|
||||
self.path_to_texture_id = {}
|
||||
|
||||
self.background_color = config["background_color"]
|
||||
|
||||
def init_scene(self, scene):
|
||||
def init_scene(self, scene: Scene) -> None:
|
||||
self.partial_movie_files = []
|
||||
self.file_writer: Any = self._file_writer_class(
|
||||
self,
|
||||
|
|
@ -249,32 +264,31 @@ class OpenGLRenderer:
|
|||
)
|
||||
self.scene = scene
|
||||
self.background_color = config["background_color"]
|
||||
if not hasattr(self, "window"):
|
||||
if self.should_create_window():
|
||||
from .opengl_renderer_window import Window
|
||||
if self.should_create_window():
|
||||
from .opengl_renderer_window import Window
|
||||
|
||||
self.window = Window(self)
|
||||
self.context = self.window.ctx
|
||||
self.frame_buffer_object = self.context.detect_framebuffer()
|
||||
else:
|
||||
self.window = None
|
||||
try:
|
||||
self.context = moderngl.create_context(standalone=True)
|
||||
except Exception:
|
||||
self.context = moderngl.create_context(
|
||||
standalone=True,
|
||||
backend="egl",
|
||||
)
|
||||
self.frame_buffer_object = self.get_frame_buffer_object(self.context, 0)
|
||||
self.frame_buffer_object.use()
|
||||
self.context.enable(moderngl.BLEND)
|
||||
self.context.wireframe = config["enable_wireframe"]
|
||||
self.context.blend_func = (
|
||||
moderngl.SRC_ALPHA,
|
||||
moderngl.ONE_MINUS_SRC_ALPHA,
|
||||
moderngl.ONE,
|
||||
moderngl.ONE,
|
||||
)
|
||||
self.window = Window(self)
|
||||
self.context = self.window.ctx
|
||||
self.frame_buffer_object = self.context.detect_framebuffer()
|
||||
else:
|
||||
# self.window = None
|
||||
try:
|
||||
self.context = moderngl.create_context(standalone=True)
|
||||
except Exception:
|
||||
self.context = moderngl.create_context(
|
||||
standalone=True,
|
||||
backend="egl",
|
||||
)
|
||||
self.frame_buffer_object = self.get_frame_buffer_object(self.context, 0)
|
||||
self.frame_buffer_object.use()
|
||||
self.context.enable(moderngl.BLEND)
|
||||
self.context.wireframe = config["enable_wireframe"]
|
||||
self.context.blend_func = (
|
||||
moderngl.SRC_ALPHA,
|
||||
moderngl.ONE_MINUS_SRC_ALPHA,
|
||||
moderngl.ONE,
|
||||
moderngl.ONE,
|
||||
)
|
||||
|
||||
def should_create_window(self):
|
||||
if config["force_window"]:
|
||||
|
|
@ -389,8 +403,7 @@ class OpenGLRenderer:
|
|||
return self.path_to_texture_id[repr(path)]
|
||||
|
||||
def update_skipping_status(self) -> None:
|
||||
"""
|
||||
This method is used internally to check if the current
|
||||
"""This method is used internally to check if the current
|
||||
animation needs to be skipped or not. It also checks if
|
||||
the number of animations that were played correspond to
|
||||
the number of animations that need to be played, and
|
||||
|
|
@ -412,7 +425,12 @@ class OpenGLRenderer:
|
|||
raise EndSceneEarlyException()
|
||||
|
||||
@handle_caching_play
|
||||
def play(self, scene, *args, **kwargs):
|
||||
def play(
|
||||
self,
|
||||
scene: Scene,
|
||||
*args: Animation | Mobject | _AnimationBuilder,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
# TODO: Handle data locking / unlocking.
|
||||
self.animation_start_time = time.time()
|
||||
self.file_writer.begin_animation(not self.skip_animations)
|
||||
|
|
@ -440,11 +458,13 @@ class OpenGLRenderer:
|
|||
self.time += scene.duration
|
||||
self.num_plays += 1
|
||||
|
||||
def clear_screen(self):
|
||||
def clear_screen(self) -> None:
|
||||
self.frame_buffer_object.clear(*self.background_color)
|
||||
self.window.swap_buffers()
|
||||
|
||||
def render(self, scene, frame_offset, moving_mobjects):
|
||||
def render(
|
||||
self, scene: Scene, frame_offset, moving_mobjects: list[Mobject]
|
||||
) -> None:
|
||||
self.update_frame(scene)
|
||||
|
||||
if self.skip_animations:
|
||||
|
|
@ -485,7 +505,7 @@ class OpenGLRenderer:
|
|||
if self.should_save_last_frame():
|
||||
config.save_last_frame = True
|
||||
self.update_frame(scene)
|
||||
self.file_writer.save_final_image(self.get_image())
|
||||
self.file_writer.save_image(self.get_image())
|
||||
|
||||
def should_save_last_frame(self):
|
||||
if config["save_last_frame"]:
|
||||
|
|
@ -566,7 +586,9 @@ class OpenGLRenderer:
|
|||
# Returns offset from the bottom left corner in pixels.
|
||||
# top_left flag should be set to True when using a GUI framework
|
||||
# where the (0,0) is at the top left: e.g. PySide6
|
||||
def pixel_coords_to_space_coords(self, px, py, relative=False, top_left=False):
|
||||
def pixel_coords_to_space_coords(
|
||||
self, px: float, py: float, relative: bool = False, top_left: bool = False
|
||||
) -> Point3D:
|
||||
pixel_shape = self.get_pixel_shape()
|
||||
if pixel_shape is None:
|
||||
return np.array([0, 0, 0])
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import moderngl_window as mglw
|
||||
from moderngl_window.context.pyglet.window import Window as PygletWindow
|
||||
from moderngl_window.timers.clock import Timer
|
||||
from screeninfo import get_monitors
|
||||
from screeninfo import Monitor, get_monitors
|
||||
|
||||
from .. import __version__, config
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .opengl_renderer import OpenGLRenderer
|
||||
|
||||
__all__ = ["Window"]
|
||||
|
||||
|
||||
|
|
@ -17,15 +22,19 @@ class Window(PygletWindow):
|
|||
vsync = True
|
||||
cursor = True
|
||||
|
||||
def __init__(self, renderer, size=config.window_size, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
renderer: OpenGLRenderer,
|
||||
window_size: str = config.window_size,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
monitors = get_monitors()
|
||||
mon_index = config.window_monitor
|
||||
monitor = monitors[min(mon_index, len(monitors) - 1)]
|
||||
|
||||
if size == "default":
|
||||
if window_size == "default":
|
||||
# make window_width half the width of the monitor
|
||||
# but make it full screen if --fullscreen
|
||||
|
||||
window_width = monitor.width
|
||||
if not config.fullscreen:
|
||||
window_width //= 2
|
||||
|
|
@ -35,8 +44,13 @@ class Window(PygletWindow):
|
|||
window_width * config.frame_height // config.frame_width,
|
||||
)
|
||||
size = (window_width, window_height)
|
||||
elif len(window_size.split(",")) == 2:
|
||||
(window_width, window_height) = tuple(map(int, window_size.split(",")))
|
||||
size = (window_width, window_height)
|
||||
else:
|
||||
size = tuple(size)
|
||||
raise ValueError(
|
||||
"Window_size must be specified as 'width,height' or 'default'.",
|
||||
)
|
||||
|
||||
super().__init__(size=size)
|
||||
|
||||
|
|
@ -55,13 +69,13 @@ class Window(PygletWindow):
|
|||
self.position = initial_position
|
||||
|
||||
# Delegate event handling to scene.
|
||||
def on_mouse_motion(self, x, y, dx, dy):
|
||||
def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None:
|
||||
super().on_mouse_motion(x, y, dx, dy)
|
||||
point = self.renderer.pixel_coords_to_space_coords(x, y)
|
||||
d_point = self.renderer.pixel_coords_to_space_coords(dx, dy, relative=True)
|
||||
self.renderer.scene.on_mouse_motion(point, d_point)
|
||||
|
||||
def on_mouse_scroll(self, x, y, x_offset: float, y_offset: float):
|
||||
def on_mouse_scroll(self, x: int, y: int, x_offset: float, y_offset: float) -> None:
|
||||
super().on_mouse_scroll(x, y, x_offset, y_offset)
|
||||
point = self.renderer.pixel_coords_to_space_coords(x, y)
|
||||
offset = self.renderer.pixel_coords_to_space_coords(
|
||||
|
|
@ -71,28 +85,32 @@ class Window(PygletWindow):
|
|||
)
|
||||
self.renderer.scene.on_mouse_scroll(point, offset)
|
||||
|
||||
def on_key_press(self, symbol, modifiers):
|
||||
def on_key_press(self, symbol: int, modifiers: int) -> bool:
|
||||
self.renderer.pressed_keys.add(symbol)
|
||||
super().on_key_press(symbol, modifiers)
|
||||
event_handled: bool = super().on_key_press(symbol, modifiers)
|
||||
self.renderer.scene.on_key_press(symbol, modifiers)
|
||||
return event_handled
|
||||
|
||||
def on_key_release(self, symbol, modifiers):
|
||||
def on_key_release(self, symbol: int, modifiers: int) -> None:
|
||||
if symbol in self.renderer.pressed_keys:
|
||||
self.renderer.pressed_keys.remove(symbol)
|
||||
super().on_key_release(symbol, modifiers)
|
||||
self.renderer.scene.on_key_release(symbol, modifiers)
|
||||
|
||||
def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
|
||||
def on_mouse_drag(
|
||||
self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int
|
||||
) -> None:
|
||||
super().on_mouse_drag(x, y, dx, dy, buttons, modifiers)
|
||||
point = self.renderer.pixel_coords_to_space_coords(x, y)
|
||||
d_point = self.renderer.pixel_coords_to_space_coords(dx, dy, relative=True)
|
||||
self.renderer.scene.on_mouse_drag(point, d_point, buttons, modifiers)
|
||||
|
||||
def find_initial_position(self, size, monitor):
|
||||
def find_initial_position(
|
||||
self, size: tuple[int, int], monitor: Monitor
|
||||
) -> tuple[int, int]:
|
||||
custom_position = config.window_position
|
||||
window_width, window_height = size
|
||||
# Position might be specified with a string of the form
|
||||
# x,y for integers x and y
|
||||
# Position might be specified with a string of the form x,y for integers x and y
|
||||
if len(custom_position) == 1:
|
||||
raise ValueError(
|
||||
"window_position must specify both Y and X positions (Y/X -> UR). Also accepts LEFT/RIGHT/ORIGIN/UP/DOWN.",
|
||||
|
|
@ -105,20 +123,21 @@ class Window(PygletWindow):
|
|||
elif custom_position == "ORIGIN":
|
||||
custom_position = "O" * 2
|
||||
elif "," in custom_position:
|
||||
return tuple(map(int, custom_position.split(",")))
|
||||
pos_y, pos_x = tuple(map(int, custom_position.split(",")))
|
||||
return (pos_x, pos_y)
|
||||
|
||||
# Alternatively, it might be specified with a string like
|
||||
# UR, OO, DL, etc. specifying what corner it should go to
|
||||
char_to_n = {"L": 0, "U": 0, "O": 1, "R": 2, "D": 2}
|
||||
width_diff = monitor.width - window_width
|
||||
height_diff = monitor.height - window_height
|
||||
width_diff: int = monitor.width - window_width
|
||||
height_diff: int = monitor.height - window_height
|
||||
|
||||
return (
|
||||
monitor.x + char_to_n[custom_position[1]] * width_diff // 2,
|
||||
-monitor.y + char_to_n[custom_position[0]] * height_diff // 2,
|
||||
)
|
||||
|
||||
def on_mouse_press(self, x, y, button, modifiers):
|
||||
def on_mouse_press(self, x: int, y: int, button: int, modifiers: int) -> None:
|
||||
super().on_mouse_press(x, y, button, modifiers)
|
||||
point = self.renderer.pixel_coords_to_space_coords(x, y)
|
||||
mouse_button_map = {
|
||||
|
|
|
|||
|
|
@ -4,17 +4,30 @@ import contextlib
|
|||
import inspect
|
||||
import re
|
||||
import textwrap
|
||||
from collections.abc import Callable, Iterator, Sequence
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import moderngl
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
from typing_extensions import Self, TypeAlias
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.renderer.opengl_renderer import OpenGLRenderer
|
||||
|
||||
MeshTimeBasedUpdater: TypeAlias = Callable[["Object3D", float], None]
|
||||
MeshNonTimeBasedUpdater: TypeAlias = Callable[["Object3D"], None]
|
||||
MeshUpdater: TypeAlias = MeshNonTimeBasedUpdater | MeshTimeBasedUpdater
|
||||
|
||||
from manim.typing import MatrixMN, Point3D
|
||||
|
||||
from .. import config
|
||||
from ..utils import opengl
|
||||
|
||||
SHADER_FOLDER = Path(__file__).parent / "shaders"
|
||||
shader_program_cache: dict = {}
|
||||
file_path_to_code_map: dict = {}
|
||||
shader_program_cache: dict[str, moderngl.Program] = {}
|
||||
file_path_to_code_map: dict[Path, str] = {}
|
||||
|
||||
__all__ = [
|
||||
"Object3D",
|
||||
|
|
@ -43,7 +56,9 @@ def get_shader_code_from_file(file_path: Path) -> str:
|
|||
return source
|
||||
|
||||
|
||||
def filter_attributes(unfiltered_attributes, attributes):
|
||||
def filter_attributes(
|
||||
unfiltered_attributes: npt.NDArray, attributes: Sequence[str]
|
||||
) -> npt.NDArray:
|
||||
# Construct attributes for only those needed by the shader.
|
||||
filtered_attributes_dtype = []
|
||||
for i, dtype_name in enumerate(unfiltered_attributes.dtype.names):
|
||||
|
|
@ -69,28 +84,28 @@ def filter_attributes(unfiltered_attributes, attributes):
|
|||
|
||||
|
||||
class Object3D:
|
||||
def __init__(self, *children):
|
||||
def __init__(self, *children: Object3D):
|
||||
self.model_matrix = np.eye(4)
|
||||
self.normal_matrix = np.eye(4)
|
||||
self.children = []
|
||||
self.parent = None
|
||||
self.children: list[Object3D] = []
|
||||
self.parent: Object3D | None = None
|
||||
self.add(*children)
|
||||
self.init_updaters()
|
||||
|
||||
# TODO: Use path_func.
|
||||
def interpolate(self, start, end, alpha, _):
|
||||
def interpolate(self, start: Object3D, end: Object3D, alpha: float, _: Any) -> None:
|
||||
self.model_matrix = (1 - alpha) * start.model_matrix + alpha * end.model_matrix
|
||||
self.normal_matrix = (
|
||||
1 - alpha
|
||||
) * start.normal_matrix + alpha * end.normal_matrix
|
||||
|
||||
def single_copy(self):
|
||||
def single_copy(self) -> Object3D:
|
||||
copy = Object3D()
|
||||
copy.model_matrix = self.model_matrix.copy()
|
||||
copy.normal_matrix = self.normal_matrix.copy()
|
||||
return copy
|
||||
|
||||
def copy(self):
|
||||
def copy(self) -> Object3D:
|
||||
node_to_copy = {}
|
||||
|
||||
bfs = [self]
|
||||
|
|
@ -106,7 +121,7 @@ class Object3D:
|
|||
node_to_copy[node.parent].add(node_copy)
|
||||
return node_to_copy[self]
|
||||
|
||||
def add(self, *children):
|
||||
def add(self, *children: Object3D) -> None:
|
||||
for child in children:
|
||||
if child.parent is not None:
|
||||
raise Exception(
|
||||
|
|
@ -117,7 +132,7 @@ class Object3D:
|
|||
for child in children:
|
||||
child.parent = self
|
||||
|
||||
def remove(self, *children, current_children_only=True):
|
||||
def remove(self, *children: Object3D, current_children_only: bool = True) -> None:
|
||||
if current_children_only:
|
||||
for child in children:
|
||||
if child.parent != self:
|
||||
|
|
@ -128,14 +143,14 @@ class Object3D:
|
|||
for child in children:
|
||||
child.parent = None
|
||||
|
||||
def get_position(self):
|
||||
def get_position(self) -> Point3D:
|
||||
return self.model_matrix[:, 3][:3]
|
||||
|
||||
def set_position(self, position):
|
||||
def set_position(self, position: Point3D) -> Self:
|
||||
self.model_matrix[:, 3][:3] = position
|
||||
return self
|
||||
|
||||
def get_meshes(self):
|
||||
def get_meshes(self) -> Iterator[Mesh]:
|
||||
dfs = [self]
|
||||
while dfs:
|
||||
parent = dfs.pop()
|
||||
|
|
@ -143,17 +158,17 @@ class Object3D:
|
|||
yield parent
|
||||
dfs.extend(parent.children)
|
||||
|
||||
def get_family(self):
|
||||
def get_family(self) -> Iterator[Object3D]:
|
||||
dfs = [self]
|
||||
while dfs:
|
||||
parent = dfs.pop()
|
||||
yield parent
|
||||
dfs.extend(parent.children)
|
||||
|
||||
def align_data_and_family(self, _):
|
||||
def align_data_and_family(self, _: Any) -> None:
|
||||
pass
|
||||
|
||||
def hierarchical_model_matrix(self):
|
||||
def hierarchical_model_matrix(self) -> MatrixMN:
|
||||
if self.parent is None:
|
||||
return self.model_matrix
|
||||
|
||||
|
|
@ -164,7 +179,7 @@ class Object3D:
|
|||
current_object = current_object.parent
|
||||
return np.linalg.multi_dot(list(reversed(model_matrices)))
|
||||
|
||||
def hierarchical_normal_matrix(self):
|
||||
def hierarchical_normal_matrix(self) -> MatrixMN:
|
||||
if self.parent is None:
|
||||
return self.normal_matrix[:3, :3]
|
||||
|
||||
|
|
@ -175,76 +190,93 @@ class Object3D:
|
|||
current_object = current_object.parent
|
||||
return np.linalg.multi_dot(list(reversed(normal_matrices)))[:3, :3]
|
||||
|
||||
def init_updaters(self):
|
||||
self.time_based_updaters = []
|
||||
self.non_time_updaters = []
|
||||
def init_updaters(self) -> None:
|
||||
self.time_based_updaters: list[MeshTimeBasedUpdater] = []
|
||||
self.non_time_updaters: list[MeshNonTimeBasedUpdater] = []
|
||||
self.has_updaters = False
|
||||
self.updating_suspended = False
|
||||
|
||||
def update(self, dt=0):
|
||||
def update(self, dt: float = 0) -> Self:
|
||||
if not self.has_updaters or self.updating_suspended:
|
||||
return self
|
||||
for updater in self.time_based_updaters:
|
||||
updater(self, dt)
|
||||
for updater in self.non_time_updaters:
|
||||
updater(self)
|
||||
for time_based_updater in self.time_based_updaters:
|
||||
time_based_updater(self, dt)
|
||||
for non_time_based_updater in self.non_time_updaters:
|
||||
non_time_based_updater(self)
|
||||
return self
|
||||
|
||||
def get_time_based_updaters(self):
|
||||
def get_time_based_updaters(self) -> list[MeshTimeBasedUpdater]:
|
||||
return self.time_based_updaters
|
||||
|
||||
def has_time_based_updater(self):
|
||||
def has_time_based_updater(self) -> bool:
|
||||
return len(self.time_based_updaters) > 0
|
||||
|
||||
def get_updaters(self):
|
||||
def get_updaters(self) -> list[MeshUpdater]:
|
||||
return self.time_based_updaters + self.non_time_updaters
|
||||
|
||||
def add_updater(self, update_function, index=None, call_updater=True):
|
||||
def add_updater(
|
||||
self,
|
||||
update_function: MeshUpdater,
|
||||
index: int | None = None,
|
||||
call_updater: bool = True,
|
||||
) -> Self:
|
||||
if "dt" in inspect.signature(update_function).parameters:
|
||||
updater_list = self.time_based_updaters
|
||||
self._add_time_based_updater(update_function, index) # type: ignore[arg-type]
|
||||
else:
|
||||
updater_list = self.non_time_updaters
|
||||
|
||||
if index is None:
|
||||
updater_list.append(update_function)
|
||||
else:
|
||||
updater_list.insert(index, update_function)
|
||||
self._add_non_time_updater(update_function, index) # type: ignore[arg-type]
|
||||
|
||||
self.refresh_has_updater_status()
|
||||
if call_updater:
|
||||
self.update()
|
||||
return self
|
||||
|
||||
def remove_updater(self, update_function):
|
||||
for updater_list in [self.time_based_updaters, self.non_time_updaters]:
|
||||
while update_function in updater_list:
|
||||
updater_list.remove(update_function)
|
||||
def _add_time_based_updater(
|
||||
self, update_function: MeshTimeBasedUpdater, index: int | None = None
|
||||
) -> None:
|
||||
if index is None:
|
||||
self.time_based_updaters.append(update_function)
|
||||
else:
|
||||
self.time_based_updaters.insert(index, update_function)
|
||||
|
||||
def _add_non_time_updater(
|
||||
self, update_function: MeshNonTimeBasedUpdater, index: int | None = None
|
||||
) -> None:
|
||||
if index is None:
|
||||
self.non_time_updaters.append(update_function)
|
||||
else:
|
||||
self.non_time_updaters.insert(index, update_function)
|
||||
|
||||
def remove_updater(self, update_function: MeshUpdater) -> Self:
|
||||
while update_function in self.time_based_updaters:
|
||||
self.time_based_updaters.remove(update_function) # type: ignore[arg-type]
|
||||
while update_function in self.non_time_updaters:
|
||||
self.non_time_updaters.remove(update_function) # type: ignore[arg-type]
|
||||
self.refresh_has_updater_status()
|
||||
return self
|
||||
|
||||
def clear_updaters(self):
|
||||
def clear_updaters(self) -> Self:
|
||||
self.time_based_updaters = []
|
||||
self.non_time_updaters = []
|
||||
self.refresh_has_updater_status()
|
||||
return self
|
||||
|
||||
def match_updaters(self, mobject):
|
||||
def match_updaters(self, mesh: Object3D) -> Self:
|
||||
self.clear_updaters()
|
||||
for updater in mobject.get_updaters():
|
||||
for updater in mesh.get_updaters():
|
||||
self.add_updater(updater)
|
||||
return self
|
||||
|
||||
def suspend_updating(self):
|
||||
def suspend_updating(self) -> Self:
|
||||
self.updating_suspended = True
|
||||
return self
|
||||
|
||||
def resume_updating(self, call_updater=True):
|
||||
def resume_updating(self, call_updater: bool = True) -> Self:
|
||||
self.updating_suspended = False
|
||||
if call_updater:
|
||||
self.update(dt=0)
|
||||
return self
|
||||
|
||||
def refresh_has_updater_status(self):
|
||||
def refresh_has_updater_status(self) -> Self:
|
||||
self.has_updaters = len(self.get_updaters()) > 0
|
||||
return self
|
||||
|
||||
|
|
@ -252,23 +284,23 @@ class Object3D:
|
|||
class Mesh(Object3D):
|
||||
def __init__(
|
||||
self,
|
||||
shader=None,
|
||||
attributes=None,
|
||||
geometry=None,
|
||||
material=None,
|
||||
indices=None,
|
||||
use_depth_test=True,
|
||||
primitive=moderngl.TRIANGLES,
|
||||
shader: Shader | None = None,
|
||||
attributes: npt.NDArray | None = None,
|
||||
geometry: Mesh | None = None,
|
||||
material: Shader | None = None,
|
||||
indices: npt.NDArray | None = None,
|
||||
use_depth_test: bool = True,
|
||||
primitive: int = moderngl.TRIANGLES,
|
||||
):
|
||||
super().__init__()
|
||||
if shader is not None and attributes is not None:
|
||||
self.shader = shader
|
||||
self.shader: Shader = shader
|
||||
self.attributes = attributes
|
||||
self.indices = indices
|
||||
elif geometry is not None and material is not None:
|
||||
self.shader = material
|
||||
self.attributes = geometry.attributes
|
||||
self.indices = geometry.index
|
||||
self.indices = geometry.indices
|
||||
else:
|
||||
raise Exception(
|
||||
"Mesh requires either attributes and a Shader or a Geometry and a "
|
||||
|
|
@ -276,10 +308,10 @@ class Mesh(Object3D):
|
|||
)
|
||||
self.use_depth_test = use_depth_test
|
||||
self.primitive = primitive
|
||||
self.skip_render = False
|
||||
self.skip_render: bool = False
|
||||
self.init_updaters()
|
||||
|
||||
def single_copy(self):
|
||||
def single_copy(self) -> Mesh:
|
||||
copy = Mesh(
|
||||
attributes=self.attributes.copy(),
|
||||
shader=self.shader,
|
||||
|
|
@ -293,7 +325,7 @@ class Mesh(Object3D):
|
|||
# TODO: Copy updaters?
|
||||
return copy
|
||||
|
||||
def set_uniforms(self, renderer):
|
||||
def set_uniforms(self, renderer: OpenGLRenderer) -> None:
|
||||
self.shader.set_uniform(
|
||||
"u_model_matrix",
|
||||
opengl.matrix_to_shader_input(self.model_matrix),
|
||||
|
|
@ -304,7 +336,7 @@ class Mesh(Object3D):
|
|||
renderer.camera.projection_matrix,
|
||||
)
|
||||
|
||||
def render(self):
|
||||
def render(self) -> None:
|
||||
if self.skip_render:
|
||||
return
|
||||
|
||||
|
|
@ -313,15 +345,17 @@ class Mesh(Object3D):
|
|||
else:
|
||||
self.shader.context.disable(moderngl.DEPTH_TEST)
|
||||
|
||||
from moderngl import Attribute
|
||||
shader_attribute_names: list[str] = []
|
||||
for member_name, member in self.shader.shader_program._members.items():
|
||||
if isinstance(member, moderngl.Attribute):
|
||||
shader_attribute_names.append(member_name)
|
||||
filtered_shader_attributes = filter_attributes(
|
||||
self.attributes, shader_attribute_names
|
||||
)
|
||||
|
||||
shader_attributes = []
|
||||
for k, v in self.shader.shader_program._members.items():
|
||||
if isinstance(v, Attribute):
|
||||
shader_attributes.append(k)
|
||||
shader_attributes = filter_attributes(self.attributes, shader_attributes)
|
||||
|
||||
vertex_buffer_object = self.shader.context.buffer(shader_attributes.tobytes())
|
||||
vertex_buffer_object = self.shader.context.buffer(
|
||||
filtered_shader_attributes.tobytes()
|
||||
)
|
||||
if self.indices is None:
|
||||
index_buffer_object = None
|
||||
else:
|
||||
|
|
@ -333,7 +367,7 @@ class Mesh(Object3D):
|
|||
vertex_array_object = self.shader.context.simple_vertex_array(
|
||||
self.shader.shader_program,
|
||||
vertex_buffer_object,
|
||||
*shader_attributes.dtype.names,
|
||||
*filtered_shader_attributes.dtype.names,
|
||||
index_buffer=index_buffer_object,
|
||||
)
|
||||
vertex_array_object.render(self.primitive)
|
||||
|
|
@ -346,13 +380,14 @@ class Mesh(Object3D):
|
|||
class Shader:
|
||||
def __init__(
|
||||
self,
|
||||
context,
|
||||
name=None,
|
||||
source=None,
|
||||
context: moderngl.Context,
|
||||
name: str | None = None,
|
||||
source: dict[str, Any] | None = None,
|
||||
):
|
||||
global shader_program_cache
|
||||
self.context = context
|
||||
self.name = name
|
||||
self.source = source
|
||||
|
||||
# See if the program is cached.
|
||||
if (
|
||||
|
|
@ -360,10 +395,10 @@ class Shader:
|
|||
and shader_program_cache[self.name].ctx == self.context
|
||||
):
|
||||
self.shader_program = shader_program_cache[self.name]
|
||||
elif source is not None:
|
||||
elif self.source is not None:
|
||||
# Generate the shader from inline code if it was passed.
|
||||
self.shader_program = context.program(**source)
|
||||
else:
|
||||
self.shader_program = context.program(**self.source)
|
||||
elif self.name is not None:
|
||||
# Search for a file containing the shader.
|
||||
source_dict = {}
|
||||
source_dict_key = {
|
||||
|
|
@ -371,18 +406,20 @@ class Shader:
|
|||
"frag": "fragment_shader",
|
||||
"geom": "geometry_shader",
|
||||
}
|
||||
shader_folder = SHADER_FOLDER / name
|
||||
shader_folder = SHADER_FOLDER / self.name
|
||||
for shader_file in shader_folder.iterdir():
|
||||
shader_file_path = shader_folder / shader_file
|
||||
shader_source = get_shader_code_from_file(shader_file_path)
|
||||
source_dict[source_dict_key[shader_file_path.stem]] = shader_source
|
||||
self.shader_program = context.program(**source_dict)
|
||||
else:
|
||||
raise Exception("Must either pass shader name or shader source.")
|
||||
|
||||
# Cache the shader.
|
||||
if name is not None and name not in shader_program_cache:
|
||||
if self.name is not None and self.name not in shader_program_cache:
|
||||
shader_program_cache[self.name] = self.shader_program
|
||||
|
||||
def set_uniform(self, name, value):
|
||||
def set_uniform(self, name: str, value: Any) -> None:
|
||||
with contextlib.suppress(KeyError):
|
||||
self.shader_program[name] = value
|
||||
|
||||
|
|
@ -390,9 +427,9 @@ class Shader:
|
|||
class FullScreenQuad(Mesh):
|
||||
def __init__(
|
||||
self,
|
||||
context,
|
||||
fragment_shader_source=None,
|
||||
fragment_shader_name=None,
|
||||
context: moderngl.Context,
|
||||
fragment_shader_source: str | None = None,
|
||||
fragment_shader_name: str | None = None,
|
||||
):
|
||||
if fragment_shader_source is None and fragment_shader_name is None:
|
||||
raise Exception("Must either pass shader name or shader source.")
|
||||
|
|
@ -439,5 +476,5 @@ class FullScreenQuad(Mesh):
|
|||
)
|
||||
super().__init__(shader, attributes)
|
||||
|
||||
def render(self):
|
||||
def render(self) -> None:
|
||||
super().render()
|
||||
|
|
|
|||
|
|
@ -3,10 +3,17 @@ from __future__ import annotations
|
|||
import copy
|
||||
import logging
|
||||
import re
|
||||
from collections.abc import Mapping, Sequence
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import moderngl
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
from typing_extensions import Self, TypeAlias
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.typing import FloatRGBLike_Array
|
||||
|
||||
# Mobjects that should be rendered with
|
||||
# the same shader will be organized and
|
||||
|
|
@ -36,25 +43,31 @@ def find_file(file_name: Path, directories: list[Path]) -> Path:
|
|||
raise OSError(f"{file_name} not Found")
|
||||
|
||||
|
||||
_ShaderDType: TypeAlias = np.void
|
||||
_ShaderData: TypeAlias = npt.NDArray[_ShaderDType]
|
||||
|
||||
|
||||
class ShaderWrapper:
|
||||
def __init__(
|
||||
self,
|
||||
vert_data=None,
|
||||
vert_indices=None,
|
||||
shader_folder=None,
|
||||
uniforms=None, # A dictionary mapping names of uniform variables
|
||||
texture_paths=None, # A dictionary mapping names to filepaths for textures.
|
||||
depth_test=False,
|
||||
render_primitive=moderngl.TRIANGLE_STRIP,
|
||||
vert_data: _ShaderData = None,
|
||||
vert_indices: Sequence[int] | None = None,
|
||||
shader_folder: Path | str | None = None,
|
||||
# A dictionary mapping names of uniform variables
|
||||
uniforms: dict[str, float | tuple[float, ...]] | None = None,
|
||||
# A dictionary mapping names to filepaths for textures.
|
||||
texture_paths: Mapping[str, Path | str] | None = None,
|
||||
depth_test: bool = False,
|
||||
render_primitive: int | str = moderngl.TRIANGLE_STRIP,
|
||||
):
|
||||
self.vert_data = vert_data
|
||||
self.vert_indices = vert_indices
|
||||
self.vert_attributes = vert_data.dtype.names
|
||||
self.shader_folder = Path(shader_folder or "")
|
||||
self.uniforms = uniforms or {}
|
||||
self.texture_paths = texture_paths or {}
|
||||
self.depth_test = depth_test
|
||||
self.render_primitive = str(render_primitive)
|
||||
self.vert_data: _ShaderData = vert_data
|
||||
self.vert_indices: Sequence[int] | None = vert_indices
|
||||
self.vert_attributes: tuple[str, ...] | None = vert_data.dtype.names
|
||||
self.shader_folder: Path = Path(shader_folder or "")
|
||||
self.uniforms: dict[str, float | tuple[float, ...]] = uniforms or {}
|
||||
self.texture_paths: Mapping[str, str | Path] = texture_paths or {}
|
||||
self.depth_test: bool = depth_test
|
||||
self.render_primitive: str = str(render_primitive)
|
||||
self.init_program_code()
|
||||
self.refresh_id()
|
||||
|
||||
|
|
@ -69,7 +82,7 @@ class ShaderWrapper:
|
|||
result.texture_paths = dict(self.texture_paths)
|
||||
return result
|
||||
|
||||
def is_valid(self):
|
||||
def is_valid(self) -> bool:
|
||||
return all(
|
||||
[
|
||||
self.vert_data is not None,
|
||||
|
|
@ -78,10 +91,10 @@ class ShaderWrapper:
|
|||
],
|
||||
)
|
||||
|
||||
def get_id(self):
|
||||
def get_id(self) -> str:
|
||||
return self.id
|
||||
|
||||
def get_program_id(self):
|
||||
def get_program_id(self) -> int:
|
||||
return self.program_id
|
||||
|
||||
def create_id(self):
|
||||
|
|
@ -99,9 +112,9 @@ class ShaderWrapper:
|
|||
),
|
||||
)
|
||||
|
||||
def refresh_id(self):
|
||||
self.program_id = self.create_program_id()
|
||||
self.id = self.create_id()
|
||||
def refresh_id(self) -> None:
|
||||
self.program_id: int = self.create_program_id()
|
||||
self.id: str = self.create_id()
|
||||
|
||||
def create_program_id(self):
|
||||
return hash(
|
||||
|
|
@ -117,7 +130,7 @@ class ShaderWrapper:
|
|||
self.shader_folder / f"{name}.glsl",
|
||||
)
|
||||
|
||||
self.program_code = {
|
||||
self.program_code: dict[str, str | None] = {
|
||||
"vertex_shader": get_code("vert"),
|
||||
"geometry_shader": get_code("geom"),
|
||||
"fragment_shader": get_code("frag"),
|
||||
|
|
@ -126,7 +139,7 @@ class ShaderWrapper:
|
|||
def get_program_code(self):
|
||||
return self.program_code
|
||||
|
||||
def replace_code(self, old, new):
|
||||
def replace_code(self, old: str, new: str) -> None:
|
||||
code_map = self.program_code
|
||||
for name, _code in code_map.items():
|
||||
if code_map[name] is None:
|
||||
|
|
@ -134,10 +147,10 @@ class ShaderWrapper:
|
|||
code_map[name] = re.sub(old, new, code_map[name])
|
||||
self.refresh_id()
|
||||
|
||||
def combine_with(self, *shader_wrappers):
|
||||
def combine_with(self, *shader_wrappers: "ShaderWrapper") -> Self: # noqa: UP037
|
||||
# Assume they are of the same type
|
||||
if len(shader_wrappers) == 0:
|
||||
return
|
||||
return self
|
||||
if self.vert_indices is not None:
|
||||
num_verts = len(self.vert_data)
|
||||
indices_list = [self.vert_indices]
|
||||
|
|
@ -193,6 +206,6 @@ def get_shader_code_from_file(filename: Path) -> str | None:
|
|||
return result
|
||||
|
||||
|
||||
def get_colormap_code(rgb_list):
|
||||
def get_colormap_code(rgb_list: FloatRGBLike_Array) -> str:
|
||||
data = ",".join("vec3({}, {}, {})".format(*rgb) for rgb in rgb_list)
|
||||
return f"vec3[{len(rgb_list)}]({data})"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,18 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
from collections import defaultdict
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.renderer.opengl_renderer import (
|
||||
OpenGLRenderer,
|
||||
OpenGLVMobject,
|
||||
)
|
||||
from manim.typing import MatrixMN
|
||||
|
||||
from ..utils import opengl
|
||||
from ..utils.space_ops import cross2d, earclip_triangulation
|
||||
from .shader import Shader
|
||||
|
|
@ -14,9 +23,11 @@ __all__ = [
|
|||
]
|
||||
|
||||
|
||||
def build_matrix_lists(mob):
|
||||
def build_matrix_lists(
|
||||
mob: OpenGLVMobject,
|
||||
) -> defaultdict[tuple[float, ...], list[OpenGLVMobject]]:
|
||||
root_hierarchical_matrix = mob.hierarchical_model_matrix()
|
||||
matrix_to_mobject_list = collections.defaultdict(list)
|
||||
matrix_to_mobject_list = defaultdict(list)
|
||||
if mob.has_points():
|
||||
matrix_to_mobject_list[tuple(root_hierarchical_matrix.ravel())].append(mob)
|
||||
mobject_to_hierarchical_matrix = {mob: root_hierarchical_matrix}
|
||||
|
|
@ -36,7 +47,9 @@ def build_matrix_lists(mob):
|
|||
return matrix_to_mobject_list
|
||||
|
||||
|
||||
def render_opengl_vectorized_mobject_fill(renderer, mobject):
|
||||
def render_opengl_vectorized_mobject_fill(
|
||||
renderer: OpenGLRenderer, mobject: OpenGLVMobject
|
||||
) -> None:
|
||||
matrix_to_mobject_list = build_matrix_lists(mobject)
|
||||
|
||||
for matrix_tuple, mobject_list in matrix_to_mobject_list.items():
|
||||
|
|
@ -44,7 +57,11 @@ def render_opengl_vectorized_mobject_fill(renderer, mobject):
|
|||
render_mobject_fills_with_matrix(renderer, model_matrix, mobject_list)
|
||||
|
||||
|
||||
def render_mobject_fills_with_matrix(renderer, model_matrix, mobjects):
|
||||
def render_mobject_fills_with_matrix(
|
||||
renderer: OpenGLRenderer,
|
||||
model_matrix: MatrixMN,
|
||||
mobjects: Iterable[OpenGLVMobject],
|
||||
) -> None:
|
||||
# Precompute the total number of vertices for which to reserve space.
|
||||
# Note that triangulate_mobject() will cache its results.
|
||||
total_size = 0
|
||||
|
|
@ -84,7 +101,7 @@ def render_mobject_fills_with_matrix(renderer, model_matrix, mobjects):
|
|||
)
|
||||
fill_shader.set_uniform(
|
||||
"u_projection_matrix",
|
||||
renderer.scene.camera.projection_matrix,
|
||||
renderer.camera.projection_matrix,
|
||||
)
|
||||
|
||||
vbo = renderer.context.buffer(attributes.tobytes())
|
||||
|
|
@ -98,7 +115,7 @@ def render_mobject_fills_with_matrix(renderer, model_matrix, mobjects):
|
|||
vbo.release()
|
||||
|
||||
|
||||
def triangulate_mobject(mob):
|
||||
def triangulate_mobject(mob: OpenGLVMobject) -> np.ndarray:
|
||||
if not mob.needs_new_triangulation:
|
||||
return mob.triangulation
|
||||
|
||||
|
|
@ -192,14 +209,20 @@ def triangulate_mobject(mob):
|
|||
return attributes
|
||||
|
||||
|
||||
def render_opengl_vectorized_mobject_stroke(renderer, mobject):
|
||||
def render_opengl_vectorized_mobject_stroke(
|
||||
renderer: OpenGLRenderer, mobject: OpenGLVMobject
|
||||
) -> None:
|
||||
matrix_to_mobject_list = build_matrix_lists(mobject)
|
||||
for matrix_tuple, mobject_list in matrix_to_mobject_list.items():
|
||||
model_matrix = np.array(matrix_tuple).reshape((4, 4))
|
||||
render_mobject_strokes_with_matrix(renderer, model_matrix, mobject_list)
|
||||
|
||||
|
||||
def render_mobject_strokes_with_matrix(renderer, model_matrix, mobjects):
|
||||
def render_mobject_strokes_with_matrix(
|
||||
renderer: OpenGLRenderer,
|
||||
model_matrix: MatrixMN,
|
||||
mobjects: Sequence[OpenGLVMobject],
|
||||
) -> None:
|
||||
# Precompute the total number of vertices for which to reserve space.
|
||||
total_size = 0
|
||||
for submob in mobjects:
|
||||
|
|
@ -279,7 +302,7 @@ def render_mobject_strokes_with_matrix(renderer, model_matrix, mobjects):
|
|||
renderer.camera.unformatted_view_matrix @ model_matrix,
|
||||
),
|
||||
)
|
||||
shader.set_uniform("u_projection_matrix", renderer.scene.camera.projection_matrix)
|
||||
shader.set_uniform("u_projection_matrix", renderer.camera.projection_matrix)
|
||||
shader.set_uniform("manim_unit_normal", tuple(-mobjects[0].unit_normal[0]))
|
||||
|
||||
vbo = renderer.context.buffer(stroke_data.tobytes())
|
||||
|
|
|
|||
|
|
@ -89,8 +89,12 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["MovingCameraScene"]
|
||||
|
||||
from manim.animation.animation import Animation
|
||||
from typing import Any
|
||||
|
||||
from manim.animation.animation import Animation
|
||||
from manim.mobject.mobject import Mobject
|
||||
|
||||
from ..camera.camera import Camera
|
||||
from ..camera.moving_camera import MovingCamera
|
||||
from ..scene.scene import Scene
|
||||
from ..utils.family import extract_mobject_family_members
|
||||
|
|
@ -111,10 +115,12 @@ class MovingCameraScene(Scene):
|
|||
:class:`.MovingCamera`
|
||||
"""
|
||||
|
||||
def __init__(self, camera_class=MovingCamera, **kwargs):
|
||||
def __init__(
|
||||
self, camera_class: type[Camera] = MovingCamera, **kwargs: Any
|
||||
) -> None:
|
||||
super().__init__(camera_class=camera_class, **kwargs)
|
||||
|
||||
def get_moving_mobjects(self, *animations: Animation):
|
||||
def get_moving_mobjects(self, *animations: Animation) -> list[Mobject]:
|
||||
"""
|
||||
This method returns a list of all of the Mobjects in the Scene that
|
||||
are moving, that are also in the animations passed.
|
||||
|
|
@ -126,7 +132,7 @@ class MovingCameraScene(Scene):
|
|||
"""
|
||||
moving_mobjects = super().get_moving_mobjects(*animations)
|
||||
all_moving_mobjects = extract_mobject_family_members(moving_mobjects)
|
||||
movement_indicators = self.renderer.camera.get_mobjects_indicating_movement()
|
||||
movement_indicators = self.renderer.camera.get_mobjects_indicating_movement() # type: ignore[union-attr]
|
||||
for movement_indicator in movement_indicators:
|
||||
if movement_indicator in all_moving_mobjects:
|
||||
# When one of these is moving, the camera should
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -9,7 +9,7 @@ import shutil
|
|||
from fractions import Fraction
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
from tempfile import NamedTemporaryFile
|
||||
from tempfile import NamedTemporaryFile, _TemporaryFileWrapper
|
||||
from threading import Thread
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
|
|
@ -20,7 +20,6 @@ from PIL import Image
|
|||
from pydub import AudioSegment
|
||||
|
||||
from manim import __version__
|
||||
from manim.typing import PixelArray, StrPath
|
||||
|
||||
from .. import config, logger
|
||||
from .._config.logger_utils import set_file_logger
|
||||
|
|
@ -38,11 +37,15 @@ from ..utils.sounds import get_full_sound_file_path
|
|||
from .section import DefaultSectionType, Section
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from av.container.output import OutputContainer
|
||||
from av.stream import Stream
|
||||
|
||||
from manim.renderer.cairo_renderer import CairoRenderer
|
||||
from manim.renderer.opengl_renderer import OpenGLRenderer
|
||||
from manim.typing import PixelArray, StrPath
|
||||
|
||||
|
||||
def to_av_frame_rate(fps):
|
||||
def to_av_frame_rate(fps: float) -> Fraction:
|
||||
epsilon1 = 1e-4
|
||||
epsilon2 = 0.02
|
||||
|
||||
|
|
@ -59,7 +62,9 @@ def to_av_frame_rate(fps):
|
|||
return Fraction(num, denom)
|
||||
|
||||
|
||||
def convert_audio(input_path: Path, output_path: Path, codec_name: str):
|
||||
def convert_audio(
|
||||
input_path: Path, output_path: Path | _TemporaryFileWrapper[bytes], codec_name: str
|
||||
) -> None:
|
||||
with (
|
||||
av.open(input_path) as input_audio,
|
||||
av.open(output_path, "w") as output_audio,
|
||||
|
|
@ -75,8 +80,7 @@ def convert_audio(input_path: Path, output_path: Path, codec_name: str):
|
|||
|
||||
|
||||
class SceneFileWriter:
|
||||
"""
|
||||
SceneFileWriter is the object that actually writes the animations
|
||||
"""SceneFileWriter is the object that actually writes the animations
|
||||
played, into video files, using FFMPEG.
|
||||
This is mostly for Manim's internal use. You will rarely, if ever,
|
||||
have to use the methods for this class, unless tinkering with the very
|
||||
|
|
@ -108,14 +112,14 @@ class SceneFileWriter:
|
|||
def __init__(
|
||||
self,
|
||||
renderer: CairoRenderer | OpenGLRenderer,
|
||||
scene_name: StrPath,
|
||||
scene_name: str,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.renderer = renderer
|
||||
self.init_output_directories(scene_name)
|
||||
self.init_audio()
|
||||
self.frame_count = 0
|
||||
self.partial_movie_files: list[str] = []
|
||||
self.partial_movie_files: list[str | None] = []
|
||||
self.subcaptions: list[srt.Subtitle] = []
|
||||
self.sections: list[Section] = []
|
||||
# first section gets automatically created for convenience
|
||||
|
|
@ -124,7 +128,7 @@ class SceneFileWriter:
|
|||
name="autocreated", type_=DefaultSectionType.NORMAL, skip_animations=False
|
||||
)
|
||||
|
||||
def init_output_directories(self, scene_name: StrPath) -> None:
|
||||
def init_output_directories(self, scene_name: str) -> None:
|
||||
"""Initialise output directories.
|
||||
|
||||
Notes
|
||||
|
|
@ -231,9 +235,12 @@ class SceneFileWriter:
|
|||
),
|
||||
)
|
||||
|
||||
def add_partial_movie_file(self, hash_animation: str):
|
||||
"""Adds a new partial movie file path to `scene.partial_movie_files` and current section from a hash.
|
||||
This method will compute the path from the hash. In addition to that it adds the new animation to the current section.
|
||||
def add_partial_movie_file(self, hash_animation: str | None) -> None:
|
||||
"""Adds a new partial movie file path to ``scene.partial_movie_files``
|
||||
and current section from a hash.
|
||||
|
||||
This method will compute the path from the hash. In addition to that it
|
||||
adds the new animation to the current section.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
|
@ -256,7 +263,7 @@ class SceneFileWriter:
|
|||
self.partial_movie_files.append(new_partial_movie_file)
|
||||
self.sections[-1].partial_movie_files.append(new_partial_movie_file)
|
||||
|
||||
def get_resolution_directory(self):
|
||||
def get_resolution_directory(self) -> str:
|
||||
"""Get the name of the resolution directory directly containing
|
||||
the video file.
|
||||
|
||||
|
|
@ -272,9 +279,11 @@ class SceneFileWriter:
|
|||
|--Tex
|
||||
|--texts
|
||||
|--videos
|
||||
|--<name_of_file_containing_scene>
|
||||
|--<height_in_pixels_of_video>p<frame_rate>
|
||||
|--<scene_name>.mp4
|
||||
|--<name_of_file_containing_scene>
|
||||
|--<height_in_pixels_of_video>p<frame_rate>
|
||||
|--partial_movie_files
|
||||
|--<scene_name>.mp4
|
||||
|--<scene_name>.srt
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
|
@ -286,11 +295,11 @@ class SceneFileWriter:
|
|||
return f"{pixel_height}p{frame_rate}"
|
||||
|
||||
# Sound
|
||||
def init_audio(self):
|
||||
def init_audio(self) -> None:
|
||||
"""Preps the writer for adding audio to the movie."""
|
||||
self.includes_sound = False
|
||||
|
||||
def create_audio_segment(self):
|
||||
def create_audio_segment(self) -> None:
|
||||
"""Creates an empty, silent, Audio Segment."""
|
||||
self.audio_segment = AudioSegment.silent()
|
||||
|
||||
|
|
@ -299,10 +308,9 @@ class SceneFileWriter:
|
|||
new_segment: AudioSegment,
|
||||
time: float | None = None,
|
||||
gain_to_background: float | None = None,
|
||||
):
|
||||
"""
|
||||
This method adds an audio segment from an
|
||||
AudioSegment type object and suitable parameters.
|
||||
) -> None:
|
||||
"""This method adds an audio segment from an AudioSegment type object
|
||||
and suitable parameters.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
|
@ -310,8 +318,7 @@ class SceneFileWriter:
|
|||
The audio segment to add
|
||||
|
||||
time
|
||||
the timestamp at which the
|
||||
sound should be added.
|
||||
the timestamp at which the sound should be added.
|
||||
|
||||
gain_to_background
|
||||
The gain of the segment from the background.
|
||||
|
|
@ -341,13 +348,12 @@ class SceneFileWriter:
|
|||
|
||||
def add_sound(
|
||||
self,
|
||||
sound_file: str,
|
||||
sound_file: StrPath,
|
||||
time: float | None = None,
|
||||
gain: float | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
This method adds an audio segment from a sound file.
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""This method adds an audio segment from a sound file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
|
@ -387,8 +393,7 @@ class SceneFileWriter:
|
|||
def begin_animation(
|
||||
self, allow_write: bool = False, file_path: StrPath | None = None
|
||||
) -> None:
|
||||
"""
|
||||
Used internally by manim to stream the animation to FFMPEG for
|
||||
"""Used internally by manim to stream the animation to FFMPEG for
|
||||
displaying or writing to a file.
|
||||
|
||||
Parameters
|
||||
|
|
@ -400,9 +405,7 @@ class SceneFileWriter:
|
|||
self.open_partial_movie_stream(file_path=file_path)
|
||||
|
||||
def end_animation(self, allow_write: bool = False) -> None:
|
||||
"""
|
||||
Internally used by Manim to stop streaming to
|
||||
FFMPEG gracefully.
|
||||
"""Internally used by Manim to stop streaming to FFMPEG gracefully.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
|
@ -412,7 +415,7 @@ class SceneFileWriter:
|
|||
if write_to_movie() and allow_write:
|
||||
self.close_partial_movie_stream()
|
||||
|
||||
def listen_and_write(self):
|
||||
def listen_and_write(self) -> None:
|
||||
"""For internal use only: blocks until new frame is available on the queue."""
|
||||
while True:
|
||||
num_frames, frame_data = self.queue.get()
|
||||
|
|
@ -422,9 +425,8 @@ class SceneFileWriter:
|
|||
self.encode_and_write_frame(frame_data, num_frames)
|
||||
|
||||
def encode_and_write_frame(self, frame: PixelArray, num_frames: int) -> None:
|
||||
"""
|
||||
For internal use only: takes a given frame in ``np.ndarray`` format and
|
||||
write it to the stream
|
||||
"""For internal use only: takes a given frame in ``np.ndarray`` format and
|
||||
writes it to the stream
|
||||
"""
|
||||
for _ in range(num_frames):
|
||||
# Notes: precomputing reusing packets does not work!
|
||||
|
|
@ -438,11 +440,9 @@ class SceneFileWriter:
|
|||
self.video_container.mux(packet)
|
||||
|
||||
def write_frame(
|
||||
self, frame_or_renderer: np.ndarray | OpenGLRenderer, num_frames: int = 1
|
||||
):
|
||||
"""
|
||||
Used internally by Manim to write a frame to
|
||||
the FFMPEG input buffer.
|
||||
self, frame_or_renderer: PixelArray | OpenGLRenderer, num_frames: int = 1
|
||||
) -> None:
|
||||
"""Used internally by Manim to write a frame to the FFMPEG input buffer.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
|
@ -452,21 +452,27 @@ class SceneFileWriter:
|
|||
The number of times to write frame.
|
||||
"""
|
||||
if write_to_movie():
|
||||
frame: np.ndarray = (
|
||||
frame_or_renderer.get_frame()
|
||||
if config.renderer == RendererType.OPENGL
|
||||
else frame_or_renderer
|
||||
)
|
||||
if isinstance(frame_or_renderer, np.ndarray):
|
||||
frame = frame_or_renderer
|
||||
else:
|
||||
frame = (
|
||||
frame_or_renderer.get_frame()
|
||||
if config.renderer == RendererType.OPENGL
|
||||
else frame_or_renderer
|
||||
)
|
||||
|
||||
msg = (num_frames, frame)
|
||||
self.queue.put(msg)
|
||||
|
||||
if is_png_format() and not config["dry_run"]:
|
||||
image: Image = (
|
||||
frame_or_renderer.get_image()
|
||||
if config.renderer == RendererType.OPENGL
|
||||
else Image.fromarray(frame_or_renderer)
|
||||
)
|
||||
if isinstance(frame_or_renderer, np.ndarray):
|
||||
image = Image.fromarray(frame_or_renderer)
|
||||
else:
|
||||
image = (
|
||||
frame_or_renderer.get_image()
|
||||
if config.renderer == RendererType.OPENGL
|
||||
else Image.fromarray(frame_or_renderer)
|
||||
)
|
||||
target_dir = self.image_file_path.parent / self.image_file_path.stem
|
||||
extension = self.image_file_path.suffix
|
||||
self.output_image(
|
||||
|
|
@ -476,17 +482,17 @@ class SceneFileWriter:
|
|||
config["zero_pad"],
|
||||
)
|
||||
|
||||
def output_image(self, image: Image.Image, target_dir, ext, zero_pad: bool):
|
||||
def output_image(
|
||||
self, image: Image.Image, target_dir: StrPath, ext: str, zero_pad: int
|
||||
) -> None:
|
||||
if zero_pad:
|
||||
image.save(f"{target_dir}{str(self.frame_count).zfill(zero_pad)}{ext}")
|
||||
else:
|
||||
image.save(f"{target_dir}{self.frame_count}{ext}")
|
||||
self.frame_count += 1
|
||||
|
||||
def save_final_image(self, image: np.ndarray):
|
||||
"""
|
||||
The name is a misnomer. This method saves the image
|
||||
passed to it as an in the default image directory.
|
||||
def save_image(self, image: Image.Image) -> None:
|
||||
"""This method saves the image passed to it in the default image directory.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
|
@ -502,13 +508,9 @@ class SceneFileWriter:
|
|||
self.print_file_ready_message(self.image_file_path)
|
||||
|
||||
def finish(self) -> None:
|
||||
"""
|
||||
Finishes writing to the FFMPEG buffer or writing images
|
||||
to output directory.
|
||||
Combines the partial movie files into the
|
||||
whole scene.
|
||||
If save_last_frame is True, saves the last
|
||||
frame in the default image directory.
|
||||
"""Finishes writing to the FFMPEG buffer or writing images to output directory.
|
||||
Combines the partial movie files into the whole scene.
|
||||
If save_last_frame is True, saves the last frame in the default image directory.
|
||||
"""
|
||||
if write_to_movie():
|
||||
self.combine_to_movie()
|
||||
|
|
@ -524,7 +526,7 @@ class SceneFileWriter:
|
|||
if self.subcaptions:
|
||||
self.write_subcaption_file()
|
||||
|
||||
def open_partial_movie_stream(self, file_path=None) -> None:
|
||||
def open_partial_movie_stream(self, file_path: StrPath | None = None) -> None:
|
||||
"""Open a container holding a video stream.
|
||||
|
||||
This is used internally by Manim initialize the container holding
|
||||
|
|
@ -563,8 +565,8 @@ class SceneFileWriter:
|
|||
stream.width = config.pixel_width
|
||||
stream.height = config.pixel_height
|
||||
|
||||
self.video_container = video_container
|
||||
self.video_stream = stream
|
||||
self.video_container: OutputContainer = video_container
|
||||
self.video_stream: Stream = stream
|
||||
|
||||
self.queue: Queue[tuple[int, PixelArray | None]] = Queue()
|
||||
self.writer_thread = Thread(target=self.listen_and_write, args=())
|
||||
|
|
@ -590,7 +592,7 @@ class SceneFileWriter:
|
|||
{"path": f"'{self.partial_movie_file_path}'"},
|
||||
)
|
||||
|
||||
def is_already_cached(self, hash_invocation: str):
|
||||
def is_already_cached(self, hash_invocation: str) -> bool:
|
||||
"""Will check if a file named with `hash_invocation` exists.
|
||||
|
||||
Parameters
|
||||
|
|
@ -615,9 +617,9 @@ class SceneFileWriter:
|
|||
self,
|
||||
input_files: list[str],
|
||||
output_file: Path,
|
||||
create_gif=False,
|
||||
includes_sound=False,
|
||||
):
|
||||
create_gif: bool = False,
|
||||
includes_sound: bool = False,
|
||||
) -> None:
|
||||
file_list = self.partial_movie_directory / "partial_movie_file_list.txt"
|
||||
logger.debug(
|
||||
f"Partial movie files to combine ({len(input_files)} files): %(p)s",
|
||||
|
|
@ -651,8 +653,7 @@ class SceneFileWriter:
|
|||
if config.transparent and config.movie_file_extension == ".webm":
|
||||
output_stream.pix_fmt = "yuva420p"
|
||||
if create_gif:
|
||||
"""
|
||||
The following solution was largely inspired from this comment
|
||||
"""The following solution was largely inspired from this comment
|
||||
https://github.com/imageio/imageio/issues/995#issuecomment-1580533018,
|
||||
and the following code
|
||||
https://github.com/imageio/imageio/blob/65d79140018bb7c64c0692ea72cb4093e8d632a0/imageio/plugins/pyav.py#L927-L996.
|
||||
|
|
@ -716,7 +717,7 @@ class SceneFileWriter:
|
|||
partial_movies_input.close()
|
||||
output_container.close()
|
||||
|
||||
def combine_to_movie(self):
|
||||
def combine_to_movie(self) -> None:
|
||||
"""Used internally by Manim to combine the separate
|
||||
partial movie files that make up a Scene into a single
|
||||
video file for that Scene.
|
||||
|
|
@ -836,7 +837,7 @@ class SceneFileWriter:
|
|||
with (self.sections_output_dir / f"{self.output_name}.json").open("w") as file:
|
||||
json.dump(sections_index, file, indent=4)
|
||||
|
||||
def clean_cache(self):
|
||||
def clean_cache(self) -> None:
|
||||
"""Will clean the cache by removing the oldest partial_movie_files."""
|
||||
cached_partial_movies = [
|
||||
(self.partial_movie_directory / file_name)
|
||||
|
|
@ -858,7 +859,7 @@ class SceneFileWriter:
|
|||
" You can change this behaviour by changing max_files_cached in config.",
|
||||
)
|
||||
|
||||
def flush_cache_directory(self):
|
||||
def flush_cache_directory(self) -> None:
|
||||
"""Delete all the cached partial movie files"""
|
||||
cached_partial_movies = [
|
||||
self.partial_movie_directory / file_name
|
||||
|
|
@ -872,7 +873,7 @@ class SceneFileWriter:
|
|||
{"par_dir": self.partial_movie_directory},
|
||||
)
|
||||
|
||||
def write_subcaption_file(self):
|
||||
def write_subcaption_file(self) -> None:
|
||||
"""Writes the subcaption file."""
|
||||
if config.output_file is None:
|
||||
return
|
||||
|
|
@ -880,7 +881,7 @@ class SceneFileWriter:
|
|||
subcaption_file.write_text(srt.compose(self.subcaptions), encoding="utf-8")
|
||||
logger.info(f"Subcaption file has been written as {subcaption_file}")
|
||||
|
||||
def print_file_ready_message(self, file_path):
|
||||
def print_file_ready_message(self, file_path: StrPath) -> None:
|
||||
"""Prints the "File Ready" message to STDOUT."""
|
||||
config["output_file"] = file_path
|
||||
logger.info("\nFile ready at %(file_path)s\n", {"file_path": f"'{file_path}'"})
|
||||
|
|
|
|||
|
|
@ -59,7 +59,9 @@ class Section:
|
|||
:meth:`.OpenGLRenderer.update_skipping_status`
|
||||
"""
|
||||
|
||||
def __init__(self, type_: str, video: str | None, name: str, skip_animations: bool):
|
||||
def __init__(
|
||||
self, type_: str, video: str | None, name: str, skip_animations: bool
|
||||
) -> None:
|
||||
self.type_ = type_
|
||||
# None when not to be saved -> still keeps section alive
|
||||
self.video: str | None = video
|
||||
|
|
@ -100,5 +102,5 @@ class Section:
|
|||
**video_metadata,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f"<Section '{self.name}' stored in '{self.video}'>"
|
||||
|
|
|
|||
|
|
@ -4,10 +4,13 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["VectorScene", "LinearTransformationScene"]
|
||||
|
||||
from typing import Callable
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.animation.creation import DrawBorderThenFill, Group
|
||||
from manim.camera.camera import Camera
|
||||
from manim.mobject.geometry.arc import Dot
|
||||
from manim.mobject.geometry.line import Arrow, Line, Vector
|
||||
from manim.mobject.geometry.polygram import Rectangle
|
||||
|
|
@ -41,6 +44,19 @@ from ..utils.color import (
|
|||
from ..utils.rate_functions import rush_from, rush_into
|
||||
from ..utils.space_ops import angle_of_vector
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim.typing import (
|
||||
MappingFunction,
|
||||
Point3D,
|
||||
Point3DLike,
|
||||
Vector2DLike,
|
||||
Vector3D,
|
||||
Vector3DLike,
|
||||
)
|
||||
|
||||
|
||||
X_COLOR = GREEN_C
|
||||
Y_COLOR = RED_C
|
||||
Z_COLOR = BLUE_D
|
||||
|
|
@ -53,11 +69,11 @@ Z_COLOR = BLUE_D
|
|||
# Also, methods I would have thought of as getters, like coords_to_vector, are
|
||||
# actually doing a lot of animating.
|
||||
class VectorScene(Scene):
|
||||
def __init__(self, basis_vector_stroke_width=6, **kwargs):
|
||||
def __init__(self, basis_vector_stroke_width: float = 6.0, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.basis_vector_stroke_width = basis_vector_stroke_width
|
||||
|
||||
def add_plane(self, animate: bool = False, **kwargs):
|
||||
def add_plane(self, animate: bool = False, **kwargs: Any) -> NumberPlane:
|
||||
"""
|
||||
Adds a NumberPlane object to the background.
|
||||
|
||||
|
|
@ -79,7 +95,11 @@ class VectorScene(Scene):
|
|||
self.add(plane)
|
||||
return plane
|
||||
|
||||
def add_axes(self, animate: bool = False, color: bool = WHITE, **kwargs):
|
||||
def add_axes(
|
||||
self,
|
||||
animate: bool = False,
|
||||
color: ParsableManimColor | Iterable[ParsableManimColor] = WHITE,
|
||||
) -> Axes:
|
||||
"""
|
||||
Adds a pair of Axes to the Scene.
|
||||
|
||||
|
|
@ -96,7 +116,9 @@ class VectorScene(Scene):
|
|||
self.add(axes)
|
||||
return axes
|
||||
|
||||
def lock_in_faded_grid(self, dimness: float = 0.7, axes_dimness: float = 0.5):
|
||||
def lock_in_faded_grid(
|
||||
self, dimness: float = 0.7, axes_dimness: float = 0.5
|
||||
) -> None:
|
||||
"""
|
||||
This method freezes the NumberPlane and Axes that were already
|
||||
in the background, and adds new, manipulatable ones to the foreground.
|
||||
|
|
@ -116,11 +138,13 @@ class VectorScene(Scene):
|
|||
axes.fade(axes_dimness)
|
||||
self.add(axes)
|
||||
|
||||
self.renderer.update_frame()
|
||||
# TODO
|
||||
# error: Missing positional argument "scene" in call to "update_frame" of "CairoRenderer" [call-arg]
|
||||
self.renderer.update_frame() # type: ignore[call-arg]
|
||||
self.renderer.camera = Camera(self.renderer.get_frame())
|
||||
self.clear()
|
||||
|
||||
def get_vector(self, numerical_vector: np.ndarray | list | tuple, **kwargs):
|
||||
def get_vector(self, numerical_vector: Vector3DLike, **kwargs: Any) -> Arrow:
|
||||
"""
|
||||
Returns an arrow on the Plane given an input numerical vector.
|
||||
|
||||
|
|
@ -137,19 +161,21 @@ class VectorScene(Scene):
|
|||
The Arrow representing the Vector.
|
||||
"""
|
||||
return Arrow(
|
||||
self.plane.coords_to_point(0, 0),
|
||||
self.plane.coords_to_point(*numerical_vector[:2]),
|
||||
# TODO
|
||||
# error: "VectorScene" has no attribute "plane" [attr-defined]
|
||||
self.plane.coords_to_point(0, 0), # type: ignore[attr-defined]
|
||||
self.plane.coords_to_point(*numerical_vector[:2]), # type: ignore[attr-defined]
|
||||
buff=0,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def add_vector(
|
||||
self,
|
||||
vector: Arrow | list | tuple | np.ndarray,
|
||||
color: str = YELLOW,
|
||||
vector: Arrow | Vector3DLike,
|
||||
color: ParsableManimColor | Iterable[ParsableManimColor] = YELLOW,
|
||||
animate: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
**kwargs: Any,
|
||||
) -> Arrow:
|
||||
"""
|
||||
Returns the Vector after adding it to the Plane.
|
||||
|
||||
|
|
@ -179,13 +205,13 @@ class VectorScene(Scene):
|
|||
The arrow representing the vector.
|
||||
"""
|
||||
if not isinstance(vector, Arrow):
|
||||
vector = Vector(vector, color=color, **kwargs)
|
||||
vector = Vector(np.asarray(vector), color=color, **kwargs)
|
||||
if animate:
|
||||
self.play(GrowArrow(vector))
|
||||
self.add(vector)
|
||||
return vector
|
||||
|
||||
def write_vector_coordinates(self, vector: Arrow, **kwargs):
|
||||
def write_vector_coordinates(self, vector: Vector, **kwargs: Any) -> Matrix:
|
||||
"""
|
||||
Returns a column matrix indicating the vector coordinates,
|
||||
after writing them to the screen.
|
||||
|
|
@ -203,11 +229,15 @@ class VectorScene(Scene):
|
|||
:class:`.Matrix`
|
||||
The column matrix representing the vector.
|
||||
"""
|
||||
coords = vector.coordinate_label(**kwargs)
|
||||
coords: Matrix = vector.coordinate_label(**kwargs)
|
||||
self.play(Write(coords))
|
||||
return coords
|
||||
|
||||
def get_basis_vectors(self, i_hat_color: str = X_COLOR, j_hat_color: str = Y_COLOR):
|
||||
def get_basis_vectors(
|
||||
self,
|
||||
i_hat_color: ParsableManimColor | Iterable[ParsableManimColor] = X_COLOR,
|
||||
j_hat_color: ParsableManimColor | Iterable[ParsableManimColor] = Y_COLOR,
|
||||
) -> VGroup:
|
||||
"""
|
||||
Returns a VGroup of the Basis Vectors (1,0) and (0,1)
|
||||
|
||||
|
|
@ -226,12 +256,16 @@ class VectorScene(Scene):
|
|||
"""
|
||||
return VGroup(
|
||||
*(
|
||||
Vector(vect, color=color, stroke_width=self.basis_vector_stroke_width)
|
||||
Vector(
|
||||
np.asarray(vect),
|
||||
color=color,
|
||||
stroke_width=self.basis_vector_stroke_width,
|
||||
)
|
||||
for vect, color in [([1, 0], i_hat_color), ([0, 1], j_hat_color)]
|
||||
)
|
||||
)
|
||||
|
||||
def get_basis_vector_labels(self, **kwargs):
|
||||
def get_basis_vector_labels(self, **kwargs: Any) -> VGroup:
|
||||
"""
|
||||
Returns naming labels for the basis vectors.
|
||||
|
||||
|
|
@ -263,13 +297,13 @@ class VectorScene(Scene):
|
|||
def get_vector_label(
|
||||
self,
|
||||
vector: Vector,
|
||||
label,
|
||||
label: MathTex | str,
|
||||
at_tip: bool = False,
|
||||
direction: str = "left",
|
||||
rotate: bool = False,
|
||||
color: str | None = None,
|
||||
color: ParsableManimColor | None = None,
|
||||
label_scale_factor: float = LARGE_BUFF - 0.2,
|
||||
):
|
||||
) -> MathTex:
|
||||
"""
|
||||
Returns naming labels for the passed vector.
|
||||
|
||||
|
|
@ -300,8 +334,11 @@ class VectorScene(Scene):
|
|||
label = "\\vec{\\textbf{%s}}" % label # noqa: UP031
|
||||
label = MathTex(label)
|
||||
if color is None:
|
||||
color = vector.get_color()
|
||||
label.set_color(color)
|
||||
prepared_color: ParsableManimColor = vector.get_color()
|
||||
else:
|
||||
prepared_color = color
|
||||
label.set_color(prepared_color)
|
||||
assert isinstance(label, MathTex)
|
||||
label.scale(label_scale_factor)
|
||||
label.add_background_rectangle()
|
||||
|
||||
|
|
@ -314,16 +351,18 @@ class VectorScene(Scene):
|
|||
if not rotate:
|
||||
label.rotate(-angle, about_point=ORIGIN)
|
||||
if direction == "left":
|
||||
label.shift(-label.get_bottom() + 0.1 * UP)
|
||||
temp_shift_1: Vector3D = np.asarray(label.get_bottom())
|
||||
label.shift(-temp_shift_1 + 0.1 * UP)
|
||||
else:
|
||||
label.shift(-label.get_top() + 0.1 * DOWN)
|
||||
temp_shift_2: Vector3D = np.asarray(label.get_top())
|
||||
label.shift(-temp_shift_2 + 0.1 * DOWN)
|
||||
label.rotate(angle, about_point=ORIGIN)
|
||||
label.shift((vector.get_end() - vector.get_start()) / 2)
|
||||
return label
|
||||
|
||||
def label_vector(
|
||||
self, vector: Vector, label: MathTex | str, animate: bool = True, **kwargs
|
||||
):
|
||||
self, vector: Vector, label: MathTex | str, animate: bool = True, **kwargs: Any
|
||||
) -> MathTex:
|
||||
"""
|
||||
Shortcut method for creating, and animating the addition of
|
||||
a label for the vector.
|
||||
|
|
@ -347,38 +386,38 @@ class VectorScene(Scene):
|
|||
:class:`~.MathTex`
|
||||
The MathTex of the label.
|
||||
"""
|
||||
label = self.get_vector_label(vector, label, **kwargs)
|
||||
mathtex_label = self.get_vector_label(vector, label, **kwargs)
|
||||
if animate:
|
||||
self.play(Write(label, run_time=1))
|
||||
self.add(label)
|
||||
return label
|
||||
self.play(Write(mathtex_label, run_time=1))
|
||||
self.add(mathtex_label)
|
||||
return mathtex_label
|
||||
|
||||
def position_x_coordinate(
|
||||
self,
|
||||
x_coord,
|
||||
x_line,
|
||||
vector,
|
||||
): # TODO Write DocStrings for this.
|
||||
x_coord: MathTex,
|
||||
x_line: Line,
|
||||
vector: Vector3DLike,
|
||||
) -> MathTex: # TODO Write DocStrings for this.
|
||||
x_coord.next_to(x_line, -np.sign(vector[1]) * UP)
|
||||
x_coord.set_color(X_COLOR)
|
||||
return x_coord
|
||||
|
||||
def position_y_coordinate(
|
||||
self,
|
||||
y_coord,
|
||||
y_line,
|
||||
vector,
|
||||
): # TODO Write DocStrings for this.
|
||||
y_coord: MathTex,
|
||||
y_line: Line,
|
||||
vector: Vector3DLike,
|
||||
) -> MathTex: # TODO Write DocStrings for this.
|
||||
y_coord.next_to(y_line, np.sign(vector[0]) * RIGHT)
|
||||
y_coord.set_color(Y_COLOR)
|
||||
return y_coord
|
||||
|
||||
def coords_to_vector(
|
||||
self,
|
||||
vector: np.ndarray | list | tuple,
|
||||
coords_start: np.ndarray | list | tuple = 2 * RIGHT + 2 * UP,
|
||||
vector: Vector2DLike,
|
||||
coords_start: Point3DLike = 2 * RIGHT + 2 * UP,
|
||||
clean_up: bool = True,
|
||||
):
|
||||
) -> None:
|
||||
"""
|
||||
This method writes the vector as a column matrix (henceforth called the label),
|
||||
takes the values in it one by one, and form the corresponding
|
||||
|
|
@ -409,26 +448,29 @@ class VectorScene(Scene):
|
|||
y_line = Line(x_line.get_end(), arrow.get_end())
|
||||
x_line.set_color(X_COLOR)
|
||||
y_line.set_color(Y_COLOR)
|
||||
x_coord, y_coord = array.get_mob_matrix().flatten()
|
||||
mob_matrix = array.get_mob_matrix()
|
||||
x_coord = mob_matrix[0][0]
|
||||
y_coord = mob_matrix[1][0]
|
||||
|
||||
self.play(Write(array, run_time=1))
|
||||
self.wait()
|
||||
self.play(
|
||||
ApplyFunction(
|
||||
lambda x: self.position_x_coordinate(x, x_line, vector),
|
||||
lambda x: self.position_x_coordinate(x, x_line, vector), # type: ignore[arg-type]
|
||||
x_coord,
|
||||
),
|
||||
)
|
||||
self.play(Create(x_line))
|
||||
animations = [
|
||||
ApplyFunction(
|
||||
lambda y: self.position_y_coordinate(y, y_line, vector),
|
||||
lambda y: self.position_y_coordinate(y, y_line, vector), # type: ignore[arg-type]
|
||||
y_coord,
|
||||
),
|
||||
FadeOut(array.get_brackets()),
|
||||
]
|
||||
self.play(*animations)
|
||||
y_coord, _ = (anim.mobject for anim in animations)
|
||||
# TODO: Can we delete the line below? I don't think it have any purpose.
|
||||
# y_coord, _ = (anim.mobject for anim in animations)
|
||||
self.play(Create(y_line))
|
||||
self.play(Create(arrow))
|
||||
self.wait()
|
||||
|
|
@ -438,10 +480,10 @@ class VectorScene(Scene):
|
|||
|
||||
def vector_to_coords(
|
||||
self,
|
||||
vector: np.ndarray | list | tuple,
|
||||
vector: Vector3DLike,
|
||||
integer_labels: bool = True,
|
||||
clean_up: bool = True,
|
||||
):
|
||||
) -> tuple[Matrix, Line, Line]:
|
||||
"""
|
||||
This method displays vector as a Vector() based vector, and then shows
|
||||
the corresponding lines that make up the x and y components of the vector.
|
||||
|
|
@ -475,7 +517,7 @@ class VectorScene(Scene):
|
|||
y_line = Line(x_line.get_end(), arrow.get_end())
|
||||
x_line.set_color(X_COLOR)
|
||||
y_line.set_color(Y_COLOR)
|
||||
x_coord, y_coord = array.get_entries()
|
||||
x_coord, y_coord = cast(VGroup, array.get_entries())
|
||||
x_coord_start = self.position_x_coordinate(x_coord.copy(), x_line, vector)
|
||||
y_coord_start = self.position_y_coordinate(y_coord.copy(), y_line, vector)
|
||||
brackets = array.get_brackets()
|
||||
|
|
@ -499,7 +541,7 @@ class VectorScene(Scene):
|
|||
self.add(*starting_mobjects)
|
||||
return array, x_line, y_line
|
||||
|
||||
def show_ghost_movement(self, vector: Arrow | list | tuple | np.ndarray):
|
||||
def show_ghost_movement(self, vector: Arrow | Vector2DLike | Vector3DLike) -> None:
|
||||
"""
|
||||
This method plays an animation that partially shows the entire plane moving
|
||||
in the direction of a particular vector. This is useful when you wish to
|
||||
|
|
@ -513,20 +555,26 @@ class VectorScene(Scene):
|
|||
"""
|
||||
if isinstance(vector, Arrow):
|
||||
vector = vector.get_end() - vector.get_start()
|
||||
elif len(vector) == 2:
|
||||
vector = np.append(np.array(vector), 0.0)
|
||||
x_max = int(config["frame_x_radius"] + abs(vector[0]))
|
||||
y_max = int(config["frame_y_radius"] + abs(vector[1]))
|
||||
else:
|
||||
vector = np.asarray(vector)
|
||||
if len(vector) == 2:
|
||||
vector = np.append(np.array(vector), 0.0)
|
||||
vector_cleaned: Vector3D = vector
|
||||
|
||||
x_max = int(config["frame_x_radius"] + abs(vector_cleaned[0]))
|
||||
y_max = int(config["frame_y_radius"] + abs(vector_cleaned[1]))
|
||||
# TODO:
|
||||
# I think that this should be a VGroup instead of a VMobject.
|
||||
dots = VMobject(
|
||||
*(
|
||||
*( # type: ignore[arg-type]
|
||||
Dot(x * RIGHT + y * UP)
|
||||
for x in range(-x_max, x_max)
|
||||
for y in range(-y_max, y_max)
|
||||
)
|
||||
)
|
||||
dots.set_fill(BLACK, opacity=0)
|
||||
dots_halfway = dots.copy().shift(vector / 2).set_fill(WHITE, 1)
|
||||
dots_end = dots.copy().shift(vector)
|
||||
dots_halfway = dots.copy().shift(vector_cleaned / 2).set_fill(WHITE, 1)
|
||||
dots_end = dots.copy().shift(vector_cleaned)
|
||||
|
||||
self.play(Transform(dots, dots_halfway, rate_func=rush_into))
|
||||
self.play(Transform(dots, dots_end, rate_func=rush_from))
|
||||
|
|
@ -585,16 +633,16 @@ class LinearTransformationScene(VectorScene):
|
|||
self,
|
||||
include_background_plane: bool = True,
|
||||
include_foreground_plane: bool = True,
|
||||
background_plane_kwargs: dict | None = None,
|
||||
foreground_plane_kwargs: dict | None = None,
|
||||
background_plane_kwargs: dict[str, Any] | None = None,
|
||||
foreground_plane_kwargs: dict[str, Any] | None = None,
|
||||
show_coordinates: bool = False,
|
||||
show_basis_vectors: bool = True,
|
||||
basis_vector_stroke_width: float = 6,
|
||||
i_hat_color: ParsableManimColor = X_COLOR,
|
||||
j_hat_color: ParsableManimColor = Y_COLOR,
|
||||
leave_ghost_vectors: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.include_background_plane = include_background_plane
|
||||
|
|
@ -605,7 +653,7 @@ class LinearTransformationScene(VectorScene):
|
|||
self.i_hat_color = ManimColor(i_hat_color)
|
||||
self.j_hat_color = ManimColor(j_hat_color)
|
||||
self.leave_ghost_vectors = leave_ghost_vectors
|
||||
self.background_plane_kwargs = {
|
||||
self.background_plane_kwargs: dict[str, Any] = {
|
||||
"color": GREY,
|
||||
"axis_config": {
|
||||
"color": GREY,
|
||||
|
|
@ -618,7 +666,7 @@ class LinearTransformationScene(VectorScene):
|
|||
|
||||
self.ghost_vectors = VGroup()
|
||||
|
||||
self.foreground_plane_kwargs = {
|
||||
self.foreground_plane_kwargs: dict[str, Any] = {
|
||||
"x_range": np.array([-config["frame_width"], config["frame_width"], 1.0]),
|
||||
"y_range": np.array([-config["frame_width"], config["frame_width"], 1.0]),
|
||||
"faded_line_ratio": 1,
|
||||
|
|
@ -630,22 +678,25 @@ class LinearTransformationScene(VectorScene):
|
|||
)
|
||||
|
||||
@staticmethod
|
||||
def update_default_configs(default_configs, passed_configs):
|
||||
def update_default_configs(
|
||||
default_configs: Iterable[dict[str, Any]],
|
||||
passed_configs: Iterable[dict[str, Any] | None],
|
||||
) -> None:
|
||||
for default_config, passed_config in zip(default_configs, passed_configs):
|
||||
if passed_config is not None:
|
||||
update_dict_recursively(default_config, passed_config)
|
||||
|
||||
def setup(self):
|
||||
def setup(self) -> None:
|
||||
# The has_already_setup attr is to not break all the old Scenes
|
||||
if hasattr(self, "has_already_setup"):
|
||||
return
|
||||
self.has_already_setup = True
|
||||
self.background_mobjects = []
|
||||
self.foreground_mobjects = []
|
||||
self.transformable_mobjects = []
|
||||
self.moving_vectors = []
|
||||
self.transformable_labels = []
|
||||
self.moving_mobjects = []
|
||||
self.background_mobjects: list[Mobject] = []
|
||||
self.foreground_mobjects: list[Mobject] = []
|
||||
self.transformable_mobjects: list[Mobject] = []
|
||||
self.moving_vectors: list[Mobject] = []
|
||||
self.transformable_labels: list[MathTex] = []
|
||||
self.moving_mobjects: list[Mobject] = []
|
||||
|
||||
self.background_plane = NumberPlane(**self.background_plane_kwargs)
|
||||
|
||||
|
|
@ -665,7 +716,9 @@ class LinearTransformationScene(VectorScene):
|
|||
self.i_hat, self.j_hat = self.basis_vectors
|
||||
self.add(self.basis_vectors)
|
||||
|
||||
def add_special_mobjects(self, mob_list: list, *mobs_to_add: Mobject):
|
||||
def add_special_mobjects(
|
||||
self, mob_list: list[Mobject], *mobs_to_add: Mobject
|
||||
) -> None:
|
||||
"""
|
||||
Adds mobjects to a separate list that can be tracked,
|
||||
if these mobjects have some extra importance.
|
||||
|
|
@ -685,7 +738,7 @@ class LinearTransformationScene(VectorScene):
|
|||
mob_list.append(mobject)
|
||||
self.add(mobject)
|
||||
|
||||
def add_background_mobject(self, *mobjects: Mobject):
|
||||
def add_background_mobject(self, *mobjects: Mobject) -> None:
|
||||
"""
|
||||
Adds the mobjects to the special list
|
||||
self.background_mobjects.
|
||||
|
|
@ -697,8 +750,9 @@ class LinearTransformationScene(VectorScene):
|
|||
"""
|
||||
self.add_special_mobjects(self.background_mobjects, *mobjects)
|
||||
|
||||
# TODO, this conflicts with Scene.add_fore
|
||||
def add_foreground_mobject(self, *mobjects: Mobject):
|
||||
# TODO, this conflicts with Scene.add_foreground_mobject
|
||||
# Please be aware that there is also the method Scene.add_foreground_mobjects.
|
||||
def add_foreground_mobject(self, *mobjects: Mobject) -> None: # type: ignore[override]
|
||||
"""
|
||||
Adds the mobjects to the special list
|
||||
self.foreground_mobjects.
|
||||
|
|
@ -710,7 +764,7 @@ class LinearTransformationScene(VectorScene):
|
|||
"""
|
||||
self.add_special_mobjects(self.foreground_mobjects, *mobjects)
|
||||
|
||||
def add_transformable_mobject(self, *mobjects: Mobject):
|
||||
def add_transformable_mobject(self, *mobjects: Mobject) -> None:
|
||||
"""
|
||||
Adds the mobjects to the special list
|
||||
self.transformable_mobjects.
|
||||
|
|
@ -724,7 +778,7 @@ class LinearTransformationScene(VectorScene):
|
|||
|
||||
def add_moving_mobject(
|
||||
self, mobject: Mobject, target_mobject: Mobject | None = None
|
||||
):
|
||||
) -> None:
|
||||
"""
|
||||
Adds the mobject to the special list
|
||||
self.moving_mobject, and adds a property
|
||||
|
|
@ -751,8 +805,11 @@ class LinearTransformationScene(VectorScene):
|
|||
return self.ghost_vectors
|
||||
|
||||
def get_unit_square(
|
||||
self, color: str = YELLOW, opacity: float = 0.3, stroke_width: float = 3
|
||||
):
|
||||
self,
|
||||
color: ParsableManimColor | Iterable[ParsableManimColor] = YELLOW,
|
||||
opacity: float = 0.3,
|
||||
stroke_width: float = 3,
|
||||
) -> Rectangle:
|
||||
"""
|
||||
Returns a unit square for the current NumberPlane.
|
||||
|
||||
|
|
@ -783,7 +840,7 @@ class LinearTransformationScene(VectorScene):
|
|||
square.move_to(self.plane.coords_to_point(0, 0), DL)
|
||||
return square
|
||||
|
||||
def add_unit_square(self, animate: bool = False, **kwargs):
|
||||
def add_unit_square(self, animate: bool = False, **kwargs: Any) -> Self:
|
||||
"""
|
||||
Adds a unit square to the scene via
|
||||
self.get_unit_square.
|
||||
|
|
@ -814,8 +871,12 @@ class LinearTransformationScene(VectorScene):
|
|||
return self
|
||||
|
||||
def add_vector(
|
||||
self, vector: Arrow | list | tuple | np.ndarray, color: str = YELLOW, **kwargs
|
||||
):
|
||||
self,
|
||||
vector: Arrow | list | tuple | np.ndarray,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
animate: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> Arrow:
|
||||
"""
|
||||
Adds a vector to the scene, and puts it in the special
|
||||
list self.moving_vectors.
|
||||
|
|
@ -839,11 +900,11 @@ class LinearTransformationScene(VectorScene):
|
|||
Arrow
|
||||
The arrow representing the vector.
|
||||
"""
|
||||
vector = super().add_vector(vector, color=color, **kwargs)
|
||||
vector = super().add_vector(vector, color=color, animate=animate, **kwargs)
|
||||
self.moving_vectors.append(vector)
|
||||
return vector
|
||||
|
||||
def write_vector_coordinates(self, vector: Arrow, **kwargs):
|
||||
def write_vector_coordinates(self, vector: Vector, **kwargs: Any) -> Matrix:
|
||||
"""
|
||||
Returns a column matrix indicating the vector coordinates,
|
||||
after writing them to the screen, and adding them to the
|
||||
|
|
@ -872,8 +933,8 @@ class LinearTransformationScene(VectorScene):
|
|||
label: MathTex | str,
|
||||
transformation_name: str | MathTex = "L",
|
||||
new_label: str | MathTex | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
**kwargs: Any,
|
||||
) -> MathTex:
|
||||
"""
|
||||
Method for creating, and animating the addition of
|
||||
a transformable label for the vector.
|
||||
|
|
@ -900,26 +961,27 @@ class LinearTransformationScene(VectorScene):
|
|||
:class:`~.MathTex`
|
||||
The MathTex of the label.
|
||||
"""
|
||||
# TODO: Clear up types in this function. This is currently a mess.
|
||||
label_mob = self.label_vector(vector, label, **kwargs)
|
||||
if new_label:
|
||||
label_mob.target_text = new_label
|
||||
label_mob.target_text = new_label # type: ignore[attr-defined]
|
||||
else:
|
||||
label_mob.target_text = (
|
||||
label_mob.target_text = ( # type: ignore[attr-defined]
|
||||
f"{transformation_name}({label_mob.get_tex_string()})"
|
||||
)
|
||||
label_mob.vector = vector
|
||||
label_mob.kwargs = kwargs
|
||||
label_mob.vector = vector # type: ignore[attr-defined]
|
||||
label_mob.kwargs = kwargs # type: ignore[attr-defined]
|
||||
if "animate" in label_mob.kwargs:
|
||||
label_mob.kwargs.pop("animate")
|
||||
self.transformable_labels.append(label_mob)
|
||||
return label_mob
|
||||
return cast(MathTex, label_mob)
|
||||
|
||||
def add_title(
|
||||
self,
|
||||
title: str | MathTex | Tex,
|
||||
scale_factor: float = 1.5,
|
||||
animate: bool = False,
|
||||
):
|
||||
) -> Self:
|
||||
"""
|
||||
Adds a title, after scaling it, adding a background rectangle,
|
||||
moving it to the top and adding it to foreground_mobjects adding
|
||||
|
|
@ -951,7 +1013,9 @@ class LinearTransformationScene(VectorScene):
|
|||
self.title = title
|
||||
return self
|
||||
|
||||
def get_matrix_transformation(self, matrix: np.ndarray | list | tuple):
|
||||
def get_matrix_transformation(
|
||||
self, matrix: np.ndarray | list | tuple
|
||||
) -> Callable[[Point3D], Point3D]:
|
||||
"""
|
||||
Returns a function corresponding to the linear
|
||||
transformation represented by the matrix passed.
|
||||
|
|
@ -965,7 +1029,7 @@ class LinearTransformationScene(VectorScene):
|
|||
|
||||
def get_transposed_matrix_transformation(
|
||||
self, transposed_matrix: np.ndarray | list | tuple
|
||||
):
|
||||
) -> Callable[[Point3D], Point3D]:
|
||||
"""
|
||||
Returns a function corresponding to the linear
|
||||
transformation represented by the transposed
|
||||
|
|
@ -985,7 +1049,7 @@ class LinearTransformationScene(VectorScene):
|
|||
raise ValueError("Matrix has bad dimensions")
|
||||
return lambda point: np.dot(point, transposed_matrix)
|
||||
|
||||
def get_piece_movement(self, pieces: list | tuple | np.ndarray):
|
||||
def get_piece_movement(self, pieces: Iterable[Mobject]) -> Transform:
|
||||
"""
|
||||
This method returns an animation that moves an arbitrary
|
||||
mobject in "pieces" to its corresponding .target value.
|
||||
|
|
@ -1013,7 +1077,7 @@ class LinearTransformationScene(VectorScene):
|
|||
self.add(self.ghost_vectors[-1])
|
||||
return Transform(start, target, lag_ratio=0)
|
||||
|
||||
def get_moving_mobject_movement(self, func: Callable[[np.ndarray], np.ndarray]):
|
||||
def get_moving_mobject_movement(self, func: MappingFunction) -> Transform:
|
||||
"""
|
||||
This method returns an animation that moves a mobject
|
||||
in "self.moving_mobjects" to its corresponding .target value.
|
||||
|
|
@ -1034,11 +1098,12 @@ class LinearTransformationScene(VectorScene):
|
|||
for m in self.moving_mobjects:
|
||||
if m.target is None:
|
||||
m.target = m.copy()
|
||||
target_point = func(m.get_center())
|
||||
temp: Point3D = m.get_center()
|
||||
target_point = func(temp)
|
||||
m.target.move_to(target_point)
|
||||
return self.get_piece_movement(self.moving_mobjects)
|
||||
|
||||
def get_vector_movement(self, func: Callable[[np.ndarray], np.ndarray]):
|
||||
def get_vector_movement(self, func: MappingFunction) -> Transform:
|
||||
"""
|
||||
This method returns an animation that moves a mobject
|
||||
in "self.moving_vectors" to its corresponding .target value.
|
||||
|
|
@ -1058,12 +1123,12 @@ class LinearTransformationScene(VectorScene):
|
|||
"""
|
||||
for v in self.moving_vectors:
|
||||
v.target = Vector(func(v.get_end()), color=v.get_color())
|
||||
norm = np.linalg.norm(v.target.get_end())
|
||||
norm = float(np.linalg.norm(v.target.get_end()))
|
||||
if norm < 0.1:
|
||||
v.target.get_tip().scale(norm)
|
||||
return self.get_piece_movement(self.moving_vectors)
|
||||
|
||||
def get_transformable_label_movement(self):
|
||||
def get_transformable_label_movement(self) -> Transform:
|
||||
"""
|
||||
This method returns an animation that moves all labels
|
||||
in "self.transformable_labels" to its corresponding .target .
|
||||
|
|
@ -1074,12 +1139,17 @@ class LinearTransformationScene(VectorScene):
|
|||
The animation of the movement.
|
||||
"""
|
||||
for label in self.transformable_labels:
|
||||
# TODO: This location and lines 933 and 335 are the only locations in
|
||||
# the code where the target_text property is referenced.
|
||||
target_text: MathTex | str = label.target_text # type: ignore[assignment]
|
||||
label.target = self.get_vector_label(
|
||||
label.vector.target, label.target_text, **label.kwargs
|
||||
label.vector.target, # type: ignore[attr-defined]
|
||||
target_text,
|
||||
**label.kwargs, # type: ignore[arg-type]
|
||||
)
|
||||
return self.get_piece_movement(self.transformable_labels)
|
||||
|
||||
def apply_matrix(self, matrix: np.ndarray | list | tuple, **kwargs):
|
||||
def apply_matrix(self, matrix: np.ndarray | list | tuple, **kwargs: Any) -> None:
|
||||
"""
|
||||
Applies the transformation represented by the
|
||||
given matrix to the number plane, and each vector/similar
|
||||
|
|
@ -1094,7 +1164,7 @@ class LinearTransformationScene(VectorScene):
|
|||
"""
|
||||
self.apply_transposed_matrix(np.array(matrix).T, **kwargs)
|
||||
|
||||
def apply_inverse(self, matrix: np.ndarray | list | tuple, **kwargs):
|
||||
def apply_inverse(self, matrix: np.ndarray | list | tuple, **kwargs: Any) -> None:
|
||||
"""
|
||||
This method applies the linear transformation
|
||||
represented by the inverse of the passed matrix
|
||||
|
|
@ -1110,8 +1180,8 @@ class LinearTransformationScene(VectorScene):
|
|||
self.apply_matrix(np.linalg.inv(matrix), **kwargs)
|
||||
|
||||
def apply_transposed_matrix(
|
||||
self, transposed_matrix: np.ndarray | list | tuple, **kwargs
|
||||
):
|
||||
self, transposed_matrix: np.ndarray | list | tuple, **kwargs: Any
|
||||
) -> None:
|
||||
"""
|
||||
Applies the transformation represented by the
|
||||
given transposed matrix to the number plane,
|
||||
|
|
@ -1132,7 +1202,9 @@ class LinearTransformationScene(VectorScene):
|
|||
kwargs["path_arc"] = net_rotation
|
||||
self.apply_function(func, **kwargs)
|
||||
|
||||
def apply_inverse_transpose(self, t_matrix: np.ndarray | list | tuple, **kwargs):
|
||||
def apply_inverse_transpose(
|
||||
self, t_matrix: np.ndarray | list | tuple, **kwargs: Any
|
||||
) -> None:
|
||||
"""
|
||||
Applies the inverse of the transformation represented
|
||||
by the given transposed matrix to the number plane and each
|
||||
|
|
@ -1149,8 +1221,8 @@ class LinearTransformationScene(VectorScene):
|
|||
self.apply_transposed_matrix(t_inv, **kwargs)
|
||||
|
||||
def apply_nonlinear_transformation(
|
||||
self, function: Callable[[np.ndarray], np.ndarray], **kwargs
|
||||
):
|
||||
self, function: Callable[[np.ndarray], np.ndarray], **kwargs: Any
|
||||
) -> None:
|
||||
"""
|
||||
Applies the non-linear transformation represented
|
||||
by the given function to the number plane and each
|
||||
|
|
@ -1168,10 +1240,10 @@ class LinearTransformationScene(VectorScene):
|
|||
|
||||
def apply_function(
|
||||
self,
|
||||
function: Callable[[np.ndarray], np.ndarray],
|
||||
added_anims: list = [],
|
||||
**kwargs,
|
||||
):
|
||||
function: MappingFunction,
|
||||
added_anims: list[Animation] = [],
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""
|
||||
Applies the given function to each of the mobjects in
|
||||
self.transformable_mobjects, and plays the animation showing
|
||||
|
|
@ -1194,7 +1266,7 @@ class LinearTransformationScene(VectorScene):
|
|||
kwargs["run_time"] = 3
|
||||
anims = (
|
||||
[
|
||||
ApplyPointwiseFunction(function, t_mob)
|
||||
ApplyPointwiseFunction(function, t_mob) # type: ignore[arg-type]
|
||||
for t_mob in self.transformable_mobjects
|
||||
]
|
||||
+ [
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue