mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-22 10:01:47 +00:00
fix: add type annotations to space_out_submobjects
This commit is contained in:
commit
ff34ecb0ef
44 changed files with 942 additions and 371 deletions
|
|
@ -1 +1,20 @@
|
|||
.git
|
||||
|
||||
# Development / test artifacts
|
||||
__pycache__
|
||||
**/__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.egg-info
|
||||
dist/
|
||||
build/
|
||||
coverage.xml
|
||||
|
||||
# Not needed to install the package
|
||||
docs/
|
||||
tests/
|
||||
example_scenes/
|
||||
media/
|
||||
logo/
|
||||
scripts/
|
||||
|
|
|
|||
36
.github/workflows/python-publish.yml
vendored
36
.github/workflows/python-publish.yml
vendored
|
|
@ -11,6 +11,7 @@ jobs:
|
|||
environment: release
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
|
@ -28,7 +29,6 @@ jobs:
|
|||
|
||||
- name: Build and push release to PyPI
|
||||
run: |
|
||||
uv sync
|
||||
uv build
|
||||
uv publish
|
||||
|
||||
|
|
@ -37,37 +37,11 @@ jobs:
|
|||
with:
|
||||
path: dist/*.tar.gz
|
||||
name: manim.tar.gz
|
||||
- name: Install Dependency
|
||||
run: pip install requests
|
||||
- name: Get Upload URL
|
||||
id: create_release
|
||||
shell: python
|
||||
env:
|
||||
access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag_act: ${{ github.ref }}
|
||||
run: |
|
||||
import requests
|
||||
import os
|
||||
ref_tag = os.getenv('tag_act').split('/')[-1]
|
||||
access_token = os.getenv('access_token')
|
||||
headers = {
|
||||
"Accept":"application/vnd.github.v3+json",
|
||||
"Authorization": f"token {access_token}"
|
||||
}
|
||||
url = f"https://api.github.com/repos/ManimCommunity/manim/releases/tags/{ref_tag}"
|
||||
c = requests.get(url,headers=headers)
|
||||
upload_url=c.json()['upload_url']
|
||||
with open(os.getenv('GITHUB_OUTPUT'), 'w') as f:
|
||||
print(f"upload_url={upload_url}", file=f)
|
||||
print(f"tag_name={ref_tag[1:]}", file=f)
|
||||
|
||||
- name: Upload Release Asset
|
||||
id: upload-release
|
||||
uses: actions/upload-release-asset@v1
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: dist/manim-${{ steps.create_release.outputs.tag_name }}.tar.gz
|
||||
asset_name: manim-${{ steps.create_release.outputs.tag_name }}.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
run: |
|
||||
TAG=${{ github.event.release.tag_name }}
|
||||
gh release upload "$TAG" "dist/manim-${TAG#v}.tar.gz"
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ authors:
|
|||
-
|
||||
name: "The Manim Community Developers"
|
||||
cff-version: "1.2.0"
|
||||
date-released: 2026-02-20
|
||||
date-released: 2026-02-27
|
||||
license: MIT
|
||||
message: "We acknowledge the importance of good software to support research, and we note that research becomes more valuable when it is communicated effectively. To demonstrate the value of Manim, we ask that you cite Manim in your work."
|
||||
title: Manim – Mathematical Animation Framework
|
||||
url: "https://www.manim.community/"
|
||||
version: "v0.20.0"
|
||||
version: "v0.20.1"
|
||||
...
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<p align="center">
|
||||
<a href="https://www.manim.community/"><img src="https://raw.githubusercontent.com/ManimCommunity/manim/main/logo/cropped.png"></a>
|
||||
<a href="https://www.manim.community/"><img src="https://raw.githubusercontent.com/ManimCommunity/manim/main/logo/cropped.png" alt="Manim Community logo"></a>
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://pypi.org/project/manim/"><img src="https://img.shields.io/pypi/v/manim.svg?style=flat&logo=pypi" alt="PyPI Latest Release"></a>
|
||||
<a href="https://hub.docker.com/r/manimcommunity/manim"><img src="https://img.shields.io/docker/v/manimcommunity/manim?color=%23099cec&label=docker%20image&logo=docker" alt="Docker image"> </a>
|
||||
<a href="https://mybinder.org/v2/gh/ManimCommunity/jupyter_examples/HEAD?filepath=basic_example_scenes.ipynb"><img src="https://mybinder.org/badge_logo.svg"></a>
|
||||
<a href="https://mybinder.org/v2/gh/ManimCommunity/jupyter_examples/HEAD?filepath=basic_example_scenes.ipynb"><img src="https://mybinder.org/badge_logo.svg" alt="Launch Binder"></a>
|
||||
<a href="http://choosealicense.com/licenses/mit/"><img src="https://img.shields.io/badge/license-MIT-red.svg?style=flat" alt="MIT License"></a>
|
||||
<a href="https://www.reddit.com/r/manim/"><img src="https://img.shields.io/reddit/subreddit-subscribers/manim.svg?color=orange&label=reddit&logo=reddit" alt="Reddit" href=></a>
|
||||
<a href="https://twitter.com/manimcommunity/"><img src="https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=Follow%20%40manimcommunity" alt="Twitter">
|
||||
|
|
|
|||
|
|
@ -1,42 +1,74 @@
|
|||
FROM python:3.11-slim
|
||||
# ── Stage 1: builder ─────────────────────────────────────────────────────────
|
||||
FROM python:3.14-slim AS builder
|
||||
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install --no-install-recommends -y \
|
||||
build-essential \
|
||||
gcc \
|
||||
cmake \
|
||||
make \
|
||||
pkg-config \
|
||||
wget \
|
||||
libcairo2-dev \
|
||||
libffi-dev \
|
||||
libpango1.0-dev \
|
||||
freeglut3-dev \
|
||||
ffmpeg \
|
||||
pkg-config \
|
||||
make \
|
||||
wget \
|
||||
libegl-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Setup a minimal TeX Live installation (no ctex: drops ~100 MB of CJK fonts/packages)
|
||||
COPY docker/texlive-profile.txt /tmp/
|
||||
ENV PATH=/usr/local/texlive/bin/armhf-linux:/usr/local/texlive/bin/aarch64-linux:/usr/local/texlive/bin/x86_64-linux:$PATH
|
||||
RUN wget -O /tmp/install-tl-unx.tar.gz http://mirror.ctan.org/systems/texlive/tlnet/install-tl-unx.tar.gz \
|
||||
&& mkdir /tmp/install-tl \
|
||||
&& tar -xzf /tmp/install-tl-unx.tar.gz -C /tmp/install-tl --strip-components=1 \
|
||||
&& /tmp/install-tl/install-tl --profile=/tmp/texlive-profile.txt \
|
||||
&& tlmgr install \
|
||||
amsmath babel-english cbfonts-fd cm-super count1to doublestroke dvisvgm everysel \
|
||||
fontspec frcursive fundus-calligra gnu-freefont jknapltx latex-bin \
|
||||
mathastext microtype multitoc physics prelim2e preview ragged2e relsize rsfs \
|
||||
setspace standalone tipa wasy wasysym xcolor xetex xkeyval \
|
||||
&& rm -rf /tmp/install-tl /tmp/install-tl-unx.tar.gz
|
||||
|
||||
# Install manim into an isolated virtualenv
|
||||
ENV VIRTUAL_ENV=/opt/venv
|
||||
RUN python -m venv $VIRTUAL_ENV
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
COPY . /opt/manim
|
||||
WORKDIR /opt/manim
|
||||
RUN pip install --no-cache-dir .[jupyterlab]
|
||||
|
||||
# ── Stage 2: runtime ─────────────────────────────────────────────────────────
|
||||
FROM python:3.14-slim
|
||||
|
||||
# Runtime libs only:
|
||||
# - no ffmpeg: PyAV (av package) bundles its own ffmpeg libraries in av.libs/
|
||||
# - OpenGL: keep EGL for headless rendering and libGL as required by moderngl/glcontext
|
||||
# - fonts-noto-core instead of fonts-noto (drops CJK noto fonts)
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install --no-install-recommends -y \
|
||||
libcairo2 \
|
||||
libpango-1.0-0 \
|
||||
libpangocairo-1.0-0 \
|
||||
libpangoft2-1.0-0 \
|
||||
libffi8 \
|
||||
libegl1 \
|
||||
libgl1 \
|
||||
ghostscript \
|
||||
fonts-noto
|
||||
fonts-noto-core \
|
||||
fontconfig \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN fc-cache -fv
|
||||
|
||||
# setup a minimal texlive installation
|
||||
COPY docker/texlive-profile.txt /tmp/
|
||||
# Copy TeX Live from builder
|
||||
ENV PATH=/usr/local/texlive/bin/armhf-linux:/usr/local/texlive/bin/aarch64-linux:/usr/local/texlive/bin/x86_64-linux:$PATH
|
||||
RUN wget -O /tmp/install-tl-unx.tar.gz http://mirror.ctan.org/systems/texlive/tlnet/install-tl-unx.tar.gz && \
|
||||
mkdir /tmp/install-tl && \
|
||||
tar -xzf /tmp/install-tl-unx.tar.gz -C /tmp/install-tl --strip-components=1 && \
|
||||
/tmp/install-tl/install-tl --profile=/tmp/texlive-profile.txt \
|
||||
&& tlmgr install \
|
||||
amsmath babel-english cbfonts-fd cm-super count1to ctex doublestroke dvisvgm everysel \
|
||||
fontspec frcursive fundus-calligra gnu-freefont jknapltx latex-bin \
|
||||
mathastext microtype multitoc physics prelim2e preview ragged2e relsize rsfs \
|
||||
setspace standalone tipa wasy wasysym xcolor xetex xkeyval
|
||||
COPY --from=builder /usr/local/texlive /usr/local/texlive
|
||||
|
||||
# clone and build manim
|
||||
COPY . /opt/manim
|
||||
WORKDIR /opt/manim
|
||||
RUN pip install --no-cache .[jupyterlab]
|
||||
|
||||
RUN pip install -r docs/requirements.txt
|
||||
# Copy the pre-built virtualenv from builder
|
||||
ENV VIRTUAL_ENV=/opt/venv
|
||||
COPY --from=builder /opt/venv /opt/venv
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
ARG NB_USER=manimuser
|
||||
ARG NB_UID=1000
|
||||
|
|
@ -49,11 +81,8 @@ RUN adduser --disabled-password \
|
|||
--uid ${NB_UID} \
|
||||
${NB_USER}
|
||||
|
||||
# create working directory for user to mount local directory into
|
||||
WORKDIR ${HOME}
|
||||
USER root
|
||||
RUN chown -R ${NB_USER}:${NB_USER} ${HOME}
|
||||
RUN chmod 777 ${HOME}
|
||||
RUN chown -R ${NB_USER}:${NB_USER} ${HOME} && chmod 777 ${HOME}
|
||||
USER ${NB_USER}
|
||||
|
||||
CMD [ "/bin/bash" ]
|
||||
CMD ["/bin/bash"]
|
||||
|
|
|
|||
|
|
@ -13,3 +13,11 @@ Multi-platform builds are possible by running
|
|||
docker buildx build --push --platform linux/arm64/v8,linux/amd64 --tag manimcommunity/manim:TAG -f docker/Dockerfile .
|
||||
```
|
||||
from the root directory of the repository.
|
||||
|
||||
# Runtime notes
|
||||
- The image is built via a multi-stage Dockerfile (build dependencies are not
|
||||
carried into the runtime stage).
|
||||
- The image does not include the `ffmpeg` CLI binary.
|
||||
- The default TeX installation is minimal and does not include `ctex`.
|
||||
- Headless OpenGL rendering relies on EGL/GL runtime libraries available in the
|
||||
image.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ This page contains a list of changes made between releases.
|
|||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
changelog/0.20.1-changelog
|
||||
changelog/0.20.0-changelog
|
||||
changelog/0.19.2-changelog
|
||||
changelog/0.19.1-changelog
|
||||
|
|
|
|||
41
docs/source/changelog/0.20.1-changelog.md
Normal file
41
docs/source/changelog/0.20.1-changelog.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
short-title: v0.20.1
|
||||
description: Changelog for v0.20.1
|
||||
---
|
||||
|
||||
# v0.20.1
|
||||
|
||||
Date
|
||||
: February 27, 2026
|
||||
|
||||
|
||||
## What's Changed
|
||||
### Enhancements 🚀
|
||||
* Cleanup `TipableVMobject`: avoid mutable default and fix `assign_tip_attr` typo by {user}`josiest` in {pr}`4503`
|
||||
* enhancement: optimize Docker image build and runtime footprint by {user}`behackl` in {pr}`4604`
|
||||
|
||||
### Bug Fixes 🐛
|
||||
* fix: MathTex double-brace splitting no longer fires on natural LaTeX `}}` by {user}`behackl` in {pr}`4602`
|
||||
* Fix creation or animation of a zero-length `DashedLine` by {user}`SORVER` in {pr}`4606`
|
||||
* Fix moving-object detection for nested AnimationGroups with z-indexed mobjects by {user}`Merzlikin-Matvey` in {pr}`4389`
|
||||
* Fix unintended propagation of `kwargs` in `LaggedStartMap` by {user}`irvanalhaq9` in {pr}`4613`
|
||||
|
||||
### Documentation 📚
|
||||
* Documentation: manual installation of manim as a local package by {user}`u7920349` in {pr}`4456`
|
||||
* Add alt text to all images in `README.md` by {user}`VerisimilitudeX` in {pr}`4064`
|
||||
|
||||
### Code Quality & Refactoring 🧹
|
||||
* Fix publish release workflow by {user}`behackl` in {pr}`4600`
|
||||
* Silence pydub ffmpeg/avconv import warning when ffmpeg CLI is absent by {user}`behackl` in {pr}`4603`
|
||||
|
||||
### Type Hints 📝
|
||||
* Add type annotations to `manim/_config/utils.py` by {user}`henrikmidtiby` in {pr}`4230`
|
||||
|
||||
## New Contributors
|
||||
* {user}`SORVER` made their first contribution in {pr}`4606`
|
||||
* {user}`josiest` made their first contribution in {pr}`4503`
|
||||
* {user}`u7920349` made their first contribution in {pr}`4456`
|
||||
* {user}`Merzlikin-Matvey` made their first contribution in {pr}`4389`
|
||||
* {user}`VerisimilitudeX` made their first contribution in {pr}`4064`
|
||||
|
||||
**Full Changelog**: [Compare view](https://github.com/ManimCommunity/manim/compare/v0.20.0...v0.20.1)
|
||||
|
|
@ -389,8 +389,9 @@ Substrings and parts
|
|||
|
||||
The TeX mobject can accept multiple strings as arguments. Afterwards you can
|
||||
refer to the individual parts either by their index (like ``tex[1]``), or by
|
||||
selecting parts of the tex code. In this example, we set the color
|
||||
of the ``\bigstar`` using :func:`~.set_color_by_tex`:
|
||||
using :func:`~.set_color_by_tex`, which matches the argument exactly against
|
||||
the strings passed to the constructor. In this example, we color the
|
||||
``\bigstar`` part:
|
||||
|
||||
.. manim:: LaTeXSubstrings
|
||||
:save_last_frame:
|
||||
|
|
@ -398,25 +399,13 @@ of the ``\bigstar`` using :func:`~.set_color_by_tex`:
|
|||
class LaTeXSubstrings(Scene):
|
||||
def construct(self):
|
||||
tex = Tex('Hello', r'$\bigstar$', r'\LaTeX', font_size=144)
|
||||
tex.set_color_by_tex('igsta', RED)
|
||||
tex.set_color_by_tex(r'$\bigstar$', RED)
|
||||
self.add(tex)
|
||||
|
||||
Note that :func:`~.set_color_by_tex` colors the entire substring containing
|
||||
the Tex, not just the specific symbol or Tex expression. Consider the following example:
|
||||
|
||||
.. manim:: IncorrectLaTeXSubstringColoring
|
||||
:save_last_frame:
|
||||
|
||||
class IncorrectLaTeXSubstringColoring(Scene):
|
||||
def construct(self):
|
||||
equation = MathTex(
|
||||
r"e^x = x^0 + x^1 + \frac{1}{2} x^2 + \frac{1}{6} x^3 + \cdots + \frac{1}{n!} x^n + \cdots"
|
||||
)
|
||||
equation.set_color_by_tex("x", YELLOW)
|
||||
self.add(equation)
|
||||
|
||||
As you can see, this colors the entire equation yellow, contrary to what
|
||||
may be expected. To color only ``x`` yellow, we have to do the following:
|
||||
Because :func:`~.set_color_by_tex` requires an exact match, it cannot directly
|
||||
target a token inside a string that was passed as a single argument. To color
|
||||
every ``x`` in a formula, use ``substrings_to_isolate`` to split the string at
|
||||
each occurrence first:
|
||||
|
||||
.. manim:: CorrectLaTeXSubstringColoring
|
||||
:save_last_frame:
|
||||
|
|
@ -430,21 +419,27 @@ may be expected. To color only ``x`` yellow, we have to do the following:
|
|||
equation.set_color_by_tex("x", YELLOW)
|
||||
self.add(equation)
|
||||
|
||||
By setting ``substrings_to_isolate`` to ``x``, we split up the
|
||||
:class:`~.MathTex` into substrings automatically and isolate the ``x`` components
|
||||
into individual substrings. Only then can :meth:`~.set_color_by_tex` be used
|
||||
to achieve the desired result.
|
||||
Each isolated occurrence of ``x`` becomes its own sub-mobject that
|
||||
:meth:`~.set_color_by_tex` can match exactly.
|
||||
If one of the ``substrings_to_isolate`` is in a sub or superscript, it needs
|
||||
to be enclosed by curly brackets.
|
||||
|
||||
Note that Manim also supports a custom syntax that allows splitting
|
||||
a TeX string into substrings easily: simply enclose parts of your formula
|
||||
that you want to isolate with double braces. In the string
|
||||
``MathTex(r"{{ a^2 }} + {{ b^2 }} = {{ c^2 }}")``, the rendered mobject
|
||||
``MathTex(r"{{ a^2 }} + {{ b^2 }} = {{ c^2 }}")``, the rendered mobject
|
||||
will consist of the substrings ``a^2``, ``+``, ``b^2``, ``=``, and ``c^2``.
|
||||
This makes transformations between similar text fragments easy
|
||||
to write using :class:`~.TransformMatchingTex`.
|
||||
|
||||
For Manim to recognise a ``{{`` as a group opener, it must appear either
|
||||
at the very start of the string or be immediately preceded by a whitespace
|
||||
character. This means that ``{{`` embedded directly after non-whitespace
|
||||
LaTeX — such as ``\frac{{{n}}}{k}`` or ``a^{{2}}`` — is left untouched,
|
||||
which prevents accidental splitting of ordinary nested-brace expressions.
|
||||
To stop a leading ``{{`` from being treated as a group opener, insert a
|
||||
space between the two braces: ``{{ ... }}`` → ``{ { ... } }``.
|
||||
|
||||
Using ``index_labels`` to work with complicated strings
|
||||
=======================================================
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,13 @@ For our image ``manimcommunity/manim``, there are the following tags:
|
|||
``-p`` (preview file) and ``-f`` (show output file in the file browser)
|
||||
are not supported.
|
||||
|
||||
.. note::
|
||||
|
||||
The Docker image ships with a minimal TeX Live installation. In particular,
|
||||
``ctex`` is not installed by default. If your scenes rely on
|
||||
``TexTemplateLibrary.ctex``, install it in the container via
|
||||
``tlmgr install ctex``.
|
||||
|
||||
|
||||
Basic usage of the Docker container
|
||||
-----------------------------------
|
||||
|
|
|
|||
|
|
@ -329,3 +329,13 @@ version satisfies the requirement. Change the line to, for example
|
|||
to pin the python version to `3.12`. Finally, run `uv sync`, and your
|
||||
environment is updated!
|
||||
:::
|
||||
|
||||
:::{dropdown} Installing the latest development version
|
||||
If you want to install the latest (potentially unstable!)
|
||||
development version of Manim from our source repository
|
||||
[on GitHub](https://github.com/ManimCommunity/manim), then
|
||||
simply run
|
||||
```bash
|
||||
uv add git+https://github.com/ManimCommunity/manim.git@main
|
||||
```
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ Positioning ``Mobject``\s
|
|||
|
||||
Next, let's go over some basic techniques for positioning ``Mobject``\s.
|
||||
|
||||
1. Open ``scene.py``, and add the following code snippet below the ``SquareToCircle`` method:
|
||||
1. Open ``scene.py``, and add the following code snippet below the ``SquareToCircle`` class:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import logging
|
|||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import Iterable, Iterator, Mapping, MutableMapping
|
||||
from collections.abc import Iterator, Mapping, MutableMapping
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, NoReturn
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ def make_config_parser(
|
|||
return parser
|
||||
|
||||
|
||||
def _determine_quality(qual: str) -> str:
|
||||
def _determine_quality(qual: str | None) -> str:
|
||||
for quality, values in constants.QUALITIES.items():
|
||||
if values["flag"] is not None and values["flag"] == qual:
|
||||
return quality
|
||||
|
|
@ -338,6 +338,7 @@ class ManimConfig(MutableMapping):
|
|||
|
||||
def __contains__(self, key: object) -> bool:
|
||||
try:
|
||||
assert isinstance(key, str)
|
||||
self.__getitem__(key)
|
||||
return True
|
||||
except AttributeError:
|
||||
|
|
@ -428,7 +429,7 @@ class ManimConfig(MutableMapping):
|
|||
# Deepcopying the underlying dict is enough because all properties
|
||||
# either read directly from it or compute their value on the fly from
|
||||
# values read directly from it.
|
||||
c._d = copy.deepcopy(self._d, memo)
|
||||
c._d = copy.deepcopy(self._d, memo) # type: ignore[arg-type]
|
||||
return c
|
||||
|
||||
# helper type-checking methods
|
||||
|
|
@ -655,13 +656,15 @@ class ManimConfig(MutableMapping):
|
|||
"window_size"
|
||||
] # if not "default", get a tuple of the position
|
||||
if window_size != "default":
|
||||
window_size = tuple(map(int, re.split(r"[;,\-]", window_size)))
|
||||
self.window_size = window_size
|
||||
window_size_numbers = tuple(map(int, re.split(r"[;,\-]", window_size)))
|
||||
self.window_size = window_size_numbers
|
||||
else:
|
||||
self.window_size = window_size
|
||||
|
||||
# plugins
|
||||
plugins = parser["CLI"].get("plugins", fallback="", raw=True)
|
||||
plugins = [] if plugins == "" else plugins.split(",")
|
||||
self.plugins = plugins
|
||||
plugin_list = [] if plugins is None or plugins == "" else plugins.split(",")
|
||||
self.plugins = plugin_list
|
||||
# the next two must be set AFTER digesting pixel_width and pixel_height
|
||||
self["frame_height"] = parser["CLI"].getfloat("frame_height", 8.0)
|
||||
width = parser["CLI"].getfloat("frame_width", None)
|
||||
|
|
@ -671,31 +674,31 @@ class ManimConfig(MutableMapping):
|
|||
self["frame_width"] = width
|
||||
|
||||
# other logic
|
||||
val = parser["CLI"].get("tex_template_file")
|
||||
if val:
|
||||
self.tex_template_file = val
|
||||
tex_template_file = parser["CLI"].get("tex_template_file")
|
||||
if tex_template_file:
|
||||
self.tex_template_file = Path(tex_template_file)
|
||||
|
||||
val = parser["CLI"].get("progress_bar")
|
||||
if val:
|
||||
self.progress_bar = val
|
||||
progress_bar = parser["CLI"].get("progress_bar")
|
||||
if progress_bar:
|
||||
self.progress_bar = progress_bar
|
||||
|
||||
val = parser["ffmpeg"].get("loglevel")
|
||||
if val:
|
||||
self.ffmpeg_loglevel = val
|
||||
ffmpeg_loglevel = parser["ffmpeg"].get("loglevel")
|
||||
if ffmpeg_loglevel:
|
||||
self.ffmpeg_loglevel = ffmpeg_loglevel
|
||||
|
||||
try:
|
||||
val = parser["jupyter"].getboolean("media_embed")
|
||||
media_embed = parser["jupyter"].getboolean("media_embed")
|
||||
except ValueError:
|
||||
val = None
|
||||
self.media_embed = val
|
||||
media_embed = None
|
||||
self.media_embed = media_embed
|
||||
|
||||
val = parser["jupyter"].get("media_width")
|
||||
if val:
|
||||
self.media_width = val
|
||||
media_width = parser["jupyter"].get("media_width")
|
||||
if media_width:
|
||||
self.media_width = media_width
|
||||
|
||||
val = parser["CLI"].get("quality", fallback="", raw=True)
|
||||
if val:
|
||||
self.quality = _determine_quality(val)
|
||||
quality = parser["CLI"].get("quality", fallback="", raw=True)
|
||||
if quality:
|
||||
self.quality = _determine_quality(quality)
|
||||
|
||||
return self
|
||||
|
||||
|
|
@ -1044,7 +1047,7 @@ class ManimConfig(MutableMapping):
|
|||
logger.setLevel(val)
|
||||
|
||||
@property
|
||||
def format(self) -> str:
|
||||
def format(self) -> str | None:
|
||||
"""File format; "png", "gif", "mp4", "webm" or "mov"."""
|
||||
return self._d["format"]
|
||||
|
||||
|
|
@ -1076,7 +1079,7 @@ class ManimConfig(MutableMapping):
|
|||
logging.getLogger("libav").setLevel(self.ffmpeg_loglevel)
|
||||
|
||||
@property
|
||||
def media_embed(self) -> bool:
|
||||
def media_embed(self) -> bool | None:
|
||||
"""Whether to embed videos in Jupyter notebook."""
|
||||
return self._d["media_embed"]
|
||||
|
||||
|
|
@ -1112,8 +1115,10 @@ class ManimConfig(MutableMapping):
|
|||
self._set_pos_number("pixel_height", value, False)
|
||||
|
||||
@property
|
||||
def aspect_ratio(self) -> int:
|
||||
def aspect_ratio(self) -> float:
|
||||
"""Aspect ratio (width / height) in pixels (--resolution, -r)."""
|
||||
assert isinstance(self._d["pixel_width"], int)
|
||||
assert isinstance(self._d["pixel_height"], int)
|
||||
return self._d["pixel_width"] / self._d["pixel_height"]
|
||||
|
||||
@property
|
||||
|
|
@ -1137,22 +1142,22 @@ class ManimConfig(MutableMapping):
|
|||
@property
|
||||
def frame_y_radius(self) -> float:
|
||||
"""Half the frame height (no flag)."""
|
||||
return self._d["frame_height"] / 2
|
||||
return self._d["frame_height"] / 2 # type: ignore[operator]
|
||||
|
||||
@frame_y_radius.setter
|
||||
def frame_y_radius(self, value: float) -> None:
|
||||
self._d.__setitem__("frame_y_radius", value) or self._d.__setitem__(
|
||||
self._d.__setitem__("frame_y_radius", value) or self._d.__setitem__( # type: ignore[func-returns-value]
|
||||
"frame_height", 2 * value
|
||||
)
|
||||
|
||||
@property
|
||||
def frame_x_radius(self) -> float:
|
||||
"""Half the frame width (no flag)."""
|
||||
return self._d["frame_width"] / 2
|
||||
return self._d["frame_width"] / 2 # type: ignore[operator]
|
||||
|
||||
@frame_x_radius.setter
|
||||
def frame_x_radius(self, value: float) -> None:
|
||||
self._d.__setitem__("frame_x_radius", value) or self._d.__setitem__(
|
||||
self._d.__setitem__("frame_x_radius", value) or self._d.__setitem__( # type: ignore[func-returns-value]
|
||||
"frame_width", 2 * value
|
||||
)
|
||||
|
||||
|
|
@ -1285,7 +1290,7 @@ class ManimConfig(MutableMapping):
|
|||
|
||||
@frame_size.setter
|
||||
def frame_size(self, value: tuple[int, int]) -> None:
|
||||
self._d.__setitem__("pixel_width", value[0]) or self._d.__setitem__(
|
||||
self._d.__setitem__("pixel_width", value[0]) or self._d.__setitem__( # type: ignore[func-returns-value]
|
||||
"pixel_height", value[1]
|
||||
)
|
||||
|
||||
|
|
@ -1295,7 +1300,7 @@ class ManimConfig(MutableMapping):
|
|||
keys = ["pixel_width", "pixel_height", "frame_rate"]
|
||||
q = {k: self[k] for k in keys}
|
||||
for qual in constants.QUALITIES:
|
||||
if all(q[k] == constants.QUALITIES[qual][k] for k in keys):
|
||||
if all(q[k] == constants.QUALITIES[qual][k] for k in keys): # type: ignore[literal-required]
|
||||
return qual
|
||||
return None
|
||||
|
||||
|
|
@ -1312,6 +1317,7 @@ class ManimConfig(MutableMapping):
|
|||
@property
|
||||
def transparent(self) -> bool:
|
||||
"""Whether the background opacity is less than 1.0 (-t)."""
|
||||
assert isinstance(self._d["background_opacity"], float)
|
||||
return self._d["background_opacity"] < 1.0
|
||||
|
||||
@transparent.setter
|
||||
|
|
@ -1421,12 +1427,12 @@ class ManimConfig(MutableMapping):
|
|||
self._d.__setitem__("window_position", value)
|
||||
|
||||
@property
|
||||
def window_size(self) -> str:
|
||||
"""The size of the opengl window as 'width,height' or 'default' to automatically scale the window based on the display monitor."""
|
||||
def window_size(self) -> str | tuple[int, ...]:
|
||||
"""The size of the opengl window. 'default' to automatically scale the window based on the display monitor."""
|
||||
return self._d["window_size"]
|
||||
|
||||
@window_size.setter
|
||||
def window_size(self, value: str) -> None:
|
||||
def window_size(self, value: str | tuple[int, ...]) -> None:
|
||||
self._d.__setitem__("window_size", value)
|
||||
|
||||
def resolve_movie_file_extension(self, is_transparent: bool) -> None:
|
||||
|
|
@ -1455,7 +1461,7 @@ class ManimConfig(MutableMapping):
|
|||
self._set_boolean("enable_gui", value)
|
||||
|
||||
@property
|
||||
def gui_location(self) -> tuple[Any]:
|
||||
def gui_location(self) -> tuple[int, ...]:
|
||||
"""Location parameters for the GUI window (e.g., screen coordinates or layout settings)."""
|
||||
return self._d["gui_location"]
|
||||
|
||||
|
|
@ -1639,6 +1645,7 @@ class ManimConfig(MutableMapping):
|
|||
all_args["quality"] = f"{self.pixel_height}p{self.frame_rate:g}"
|
||||
|
||||
path = self._d[key]
|
||||
assert isinstance(path, str)
|
||||
while "{" in path:
|
||||
try:
|
||||
path = path.format(**all_args)
|
||||
|
|
@ -1738,7 +1745,7 @@ class ManimConfig(MutableMapping):
|
|||
self._set_dir("custom_folders", value)
|
||||
|
||||
@property
|
||||
def input_file(self) -> str:
|
||||
def input_file(self) -> str | Path:
|
||||
"""Input file name."""
|
||||
return self._d["input_file"]
|
||||
|
||||
|
|
@ -1767,7 +1774,7 @@ class ManimConfig(MutableMapping):
|
|||
@property
|
||||
def tex_template(self) -> TexTemplate:
|
||||
"""Template used when rendering Tex. See :class:`.TexTemplate`."""
|
||||
if not hasattr(self, "_tex_template") or not self._tex_template:
|
||||
if not hasattr(self, "_tex_template") or not self._tex_template: # type: ignore[has-type]
|
||||
fn = self._d["tex_template_file"]
|
||||
if fn:
|
||||
self._tex_template = TexTemplate.from_file(fn)
|
||||
|
|
@ -1803,7 +1810,7 @@ class ManimConfig(MutableMapping):
|
|||
return self._d["plugins"]
|
||||
|
||||
@plugins.setter
|
||||
def plugins(self, value: list[str]):
|
||||
def plugins(self, value: list[str]) -> None:
|
||||
self._d["plugins"] = value
|
||||
|
||||
@property
|
||||
|
|
@ -1861,7 +1868,7 @@ class ManimFrame(Mapping):
|
|||
self.__dict__["_c"] = c
|
||||
|
||||
# there are required by parent class Mapping to behave like a dict
|
||||
def __getitem__(self, key: str | int) -> Any:
|
||||
def __getitem__(self, key: str) -> Any:
|
||||
if key in self._OPTS:
|
||||
return self._c[key]
|
||||
elif key in self._CONSTANTS:
|
||||
|
|
@ -1869,7 +1876,7 @@ class ManimFrame(Mapping):
|
|||
else:
|
||||
raise KeyError(key)
|
||||
|
||||
def __iter__(self) -> Iterable[str]:
|
||||
def __iter__(self) -> Iterator[Any]:
|
||||
return iter(list(self._OPTS) + list(self._CONSTANTS))
|
||||
|
||||
def __len__(self) -> int:
|
||||
|
|
@ -1887,4 +1894,4 @@ class ManimFrame(Mapping):
|
|||
|
||||
|
||||
for opt in list(ManimFrame._OPTS) + list(ManimFrame._CONSTANTS):
|
||||
setattr(ManimFrame, opt, property(lambda self, o=opt: self[o]))
|
||||
setattr(ManimFrame, opt, property(lambda self, o=opt: self[o])) # type: ignore[misc]
|
||||
|
|
|
|||
|
|
@ -353,7 +353,7 @@ class LaggedStartMap(LaggedStart):
|
|||
|
||||
Parameters
|
||||
----------
|
||||
AnimationClass
|
||||
animation_class
|
||||
:class:`~.Animation` to apply to mobject.
|
||||
mobject
|
||||
:class:`~.Mobject` whose submobjects the animation, and optionally the function,
|
||||
|
|
@ -362,6 +362,17 @@ class LaggedStartMap(LaggedStart):
|
|||
Function which will be applied to :class:`~.Mobject`.
|
||||
run_time
|
||||
The duration of the animation in seconds.
|
||||
lag_ratio
|
||||
Defines the delay after which the animation is applied to submobjects. A lag_ratio of
|
||||
``n.nn`` means the next animation will play when ``nnn%`` of the current animation has played.
|
||||
Defaults to 0.05, meaning that the next animation will begin when 5% of the current
|
||||
animation has played.
|
||||
|
||||
This does not influence the total runtime of the animation. Instead the runtime
|
||||
of individual animations is adjusted so that the complete animation has the defined
|
||||
run time.
|
||||
kwargs
|
||||
Further keyword arguments that are passed to `animation_class`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
|
@ -392,6 +403,7 @@ class LaggedStartMap(LaggedStart):
|
|||
mobject: Mobject,
|
||||
arg_creator: Callable[[Mobject], Iterable[Any]] | None = None,
|
||||
run_time: float = 2,
|
||||
lag_ratio: float = DEFAULT_LAGGED_START_LAG_RATIO,
|
||||
**kwargs: Any,
|
||||
):
|
||||
if arg_creator is None:
|
||||
|
|
@ -406,4 +418,4 @@ class LaggedStartMap(LaggedStart):
|
|||
if "lag_ratio" in anim_kwargs:
|
||||
anim_kwargs.pop("lag_ratio")
|
||||
animations = [animation_class(*args, **anim_kwargs) for args in args_list]
|
||||
super().__init__(*animations, run_time=run_time, **kwargs)
|
||||
super().__init__(*animations, run_time=run_time, lag_ratio=lag_ratio)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from __future__ import annotations
|
|||
__all__ = ["MovingCamera"]
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
from typing import Any, Literal, overload
|
||||
|
||||
from cairo import Context
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ 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 ..mobject.mobject import Mobject, _AnimationBuilder
|
||||
from ..utils.color import WHITE, ManimColor
|
||||
|
||||
|
||||
|
|
@ -166,13 +166,31 @@ class MovingCamera(Camera):
|
|||
"""
|
||||
return [self.frame]
|
||||
|
||||
@overload
|
||||
def auto_zoom(
|
||||
self,
|
||||
mobjects: Iterable[Mobject],
|
||||
margin: float,
|
||||
only_mobjects_in_frame: bool,
|
||||
animate: Literal[False],
|
||||
) -> Mobject: ...
|
||||
|
||||
@overload
|
||||
def auto_zoom(
|
||||
self,
|
||||
mobjects: Iterable[Mobject],
|
||||
margin: float,
|
||||
only_mobjects_in_frame: bool,
|
||||
animate: Literal[True],
|
||||
) -> _AnimationBuilder: ...
|
||||
|
||||
def auto_zoom(
|
||||
self,
|
||||
mobjects: Iterable[Mobject],
|
||||
margin: float = 0,
|
||||
only_mobjects_in_frame: bool = False,
|
||||
animate: bool = True,
|
||||
) -> Mobject:
|
||||
) -> _AnimationBuilder | Mobject:
|
||||
"""Zooms on to a given array of mobjects (or a singular mobject)
|
||||
and automatically resizes to frame all the mobjects.
|
||||
|
||||
|
|
|
|||
|
|
@ -101,12 +101,12 @@ class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
self,
|
||||
tip_length: float = DEFAULT_ARROW_TIP_LENGTH,
|
||||
normal_vector: Vector3DLike = OUT,
|
||||
tip_style: dict = {},
|
||||
tip_style: dict | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.tip_length: float = tip_length
|
||||
self.normal_vector = normal_vector
|
||||
self.tip_style: dict = tip_style
|
||||
self.tip_style: dict = tip_style if tip_style is not None else {}
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Adding, Creating, Modifying tips
|
||||
|
|
@ -128,7 +128,7 @@ class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
else:
|
||||
self.position_tip(tip, at_start)
|
||||
self.reset_endpoints_based_on_tip(tip, at_start)
|
||||
self.asign_tip_attr(tip, at_start)
|
||||
self.assign_tip_attr(tip, at_start)
|
||||
self.add(tip)
|
||||
return self
|
||||
|
||||
|
|
@ -201,6 +201,7 @@ class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
axis=axis,
|
||||
) # Rotates the tip along the vertical wrt the axis
|
||||
self._init_positioning_axis = axis
|
||||
|
||||
tip.shift(anchor - tip.tip_point)
|
||||
return tip
|
||||
|
||||
|
|
@ -215,7 +216,7 @@ class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
self.put_start_and_end_on(self.get_start(), tip.base)
|
||||
return self
|
||||
|
||||
def asign_tip_attr(self, tip: tips.ArrowTip, at_start: bool) -> Self:
|
||||
def assign_tip_attr(self, tip: tips.ArrowTip, at_start: bool) -> Self:
|
||||
if at_start:
|
||||
self.start_tip = tip
|
||||
else:
|
||||
|
|
@ -241,7 +242,8 @@ class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
if self.has_start_tip():
|
||||
result.add(self.start_tip)
|
||||
self.remove(self.start_tip)
|
||||
self.put_start_and_end_on(start, end)
|
||||
if result.submobjects:
|
||||
self.put_start_and_end_on(start, end)
|
||||
return result
|
||||
|
||||
def get_tips(self) -> VGroup:
|
||||
|
|
|
|||
|
|
@ -14,14 +14,14 @@ __all__ = [
|
|||
"RightAngle",
|
||||
]
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim import config
|
||||
from manim.constants import *
|
||||
from manim.mobject.geometry.arc import Arc, ArcBetweenPoints, Dot, TipableVMobject
|
||||
from manim.mobject.geometry.tips import ArrowTriangleFilledTip
|
||||
from manim.mobject.geometry.tips import ArrowTip, ArrowTriangleFilledTip
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
|
|
@ -648,9 +648,11 @@ class Arrow(Line):
|
|||
self._set_stroke_width_from_length()
|
||||
|
||||
if has_tip:
|
||||
self.add_tip(tip=old_tips[0])
|
||||
# error: Argument "tip" to "add_tip" of "TipableVMobject" has incompatible type "VMobject"; expected "ArrowTip | None" [arg-type]
|
||||
self.add_tip(tip=cast(ArrowTip, old_tips[0]))
|
||||
if has_start_tip:
|
||||
self.add_tip(tip=old_tips[1], at_start=True)
|
||||
# error: Argument "tip" to "add_tip" of "TipableVMobject" has incompatible type "VMobject"; expected "ArrowTip | None" [arg-type]
|
||||
self.add_tip(tip=cast(ArrowTip, old_tips[1]), at_start=True)
|
||||
return self
|
||||
|
||||
def get_normal_vector(self) -> Vector3D:
|
||||
|
|
|
|||
|
|
@ -207,13 +207,11 @@ class SampleSpace(Rectangle):
|
|||
if hasattr(parts, subattr):
|
||||
self.add(getattr(parts, subattr))
|
||||
|
||||
def __getitem__(self, index: int) -> SampleSpace:
|
||||
def __getitem__(self, index: int) -> VMobject:
|
||||
if hasattr(self, "horizontal_parts"):
|
||||
val: SampleSpace = self.horizontal_parts[index]
|
||||
return val
|
||||
return self.horizontal_parts[index]
|
||||
elif hasattr(self, "vertical_parts"):
|
||||
val = self.vertical_parts[index]
|
||||
return val
|
||||
return self.vertical_parts[index]
|
||||
return self.split()[index]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -40,15 +40,15 @@ __all__ = [
|
|||
|
||||
|
||||
import itertools as it
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Any, Self
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.text.numbers import DecimalNumber, Integer
|
||||
from manim.mobject.text.tex_mobject import MathTex, Tex
|
||||
from manim.typing import Vector2DLike, Vector3DLike
|
||||
|
||||
from ..constants import *
|
||||
from ..mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
|
|
@ -164,16 +164,16 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
matrix: Iterable,
|
||||
matrix: Iterable[Iterable[Any] | Vector2DLike],
|
||||
v_buff: float = 0.8,
|
||||
h_buff: float = 1.3,
|
||||
bracket_h_buff: float = MED_SMALL_BUFF,
|
||||
bracket_v_buff: float = MED_SMALL_BUFF,
|
||||
add_background_rectangles_to_entries: bool = False,
|
||||
include_background_rectangle: bool = False,
|
||||
element_to_mobject: type[Mobject] | Callable[..., Mobject] = MathTex,
|
||||
element_to_mobject_config: dict = {},
|
||||
element_alignment_corner: Sequence[float] = DR,
|
||||
element_to_mobject: type[VMobject] | Callable[..., VMobject] = MathTex,
|
||||
element_to_mobject_config: dict[str, Any] = {},
|
||||
element_alignment_corner: Vector3DLike = DR,
|
||||
left_bracket: str = "[",
|
||||
right_bracket: str = "]",
|
||||
stretch_brackets: bool = True,
|
||||
|
|
@ -206,7 +206,9 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
|
|||
if self.include_background_rectangle:
|
||||
self.add_background_rectangle()
|
||||
|
||||
def _matrix_to_mob_matrix(self, matrix: np.ndarray) -> list[list[Mobject]]:
|
||||
def _matrix_to_mob_matrix(
|
||||
self, matrix: Iterable[Iterable[Any]]
|
||||
) -> list[list[VMobject]]:
|
||||
return [
|
||||
[
|
||||
self.element_to_mobject(item, **self.element_to_mobject_config)
|
||||
|
|
@ -215,7 +217,7 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
|
|||
for row in matrix
|
||||
]
|
||||
|
||||
def _organize_mob_matrix(self, matrix: list[list[Mobject]]) -> Self:
|
||||
def _organize_mob_matrix(self, matrix: list[list[VMobject]]) -> Self:
|
||||
for i, row in enumerate(matrix):
|
||||
for j, _ in enumerate(row):
|
||||
mob = matrix[i][j]
|
||||
|
|
@ -401,7 +403,7 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
|
|||
mob.add_background_rectangle()
|
||||
return self
|
||||
|
||||
def get_mob_matrix(self) -> list[list[Mobject]]:
|
||||
def get_mob_matrix(self) -> list[list[VMobject]]:
|
||||
"""Return the underlying mob matrix mobjects.
|
||||
|
||||
Returns
|
||||
|
|
@ -483,8 +485,8 @@ class DecimalMatrix(Matrix):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
matrix: Iterable,
|
||||
element_to_mobject: type[Mobject] = DecimalNumber,
|
||||
matrix: Iterable[Iterable[Any]],
|
||||
element_to_mobject: type[VMobject] | Callable[..., VMobject] = DecimalNumber,
|
||||
element_to_mobject_config: dict[str, Any] = {"num_decimal_places": 1},
|
||||
**kwargs: Any,
|
||||
):
|
||||
|
|
@ -528,8 +530,8 @@ class IntegerMatrix(Matrix):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
matrix: Iterable,
|
||||
element_to_mobject: type[Mobject] = Integer,
|
||||
matrix: Iterable[Iterable[Any]],
|
||||
element_to_mobject: type[VMobject] | Callable[..., VMobject] = Integer,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
|
|
@ -566,8 +568,8 @@ class MobjectMatrix(Matrix):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
matrix: Iterable,
|
||||
element_to_mobject: type[Mobject] | Callable[..., Mobject] = lambda m: m,
|
||||
matrix: Iterable[Iterable[Any]],
|
||||
element_to_mobject: type[VMobject] | Callable[..., VMobject] = lambda m: m,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(matrix, element_to_mobject=element_to_mobject, **kwargs)
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ import random
|
|||
import sys
|
||||
import types
|
||||
import warnings
|
||||
from collections.abc import Callable, Iterable
|
||||
from collections.abc import Callable, Iterable, Iterator, Sequence
|
||||
from functools import partialmethod, reduce
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -43,24 +43,30 @@ from ..utils.space_ops import angle_between_vectors, normalize, rotation_matrix
|
|||
if TYPE_CHECKING:
|
||||
from typing import Self, TypeAlias
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from manim.mobject.types.point_cloud_mobject import Point
|
||||
from manim.typing import (
|
||||
FunctionOverride,
|
||||
MappingFunction,
|
||||
MatrixMN,
|
||||
MultiMappingFunction,
|
||||
PathFuncType,
|
||||
PixelArray,
|
||||
Point3D,
|
||||
Point3D_Array,
|
||||
Point3DLike,
|
||||
Point3DLike_Array,
|
||||
Vector3D,
|
||||
Vector3DLike,
|
||||
)
|
||||
|
||||
from ..animation.animation import Animation
|
||||
from ..camera.camera import Camera
|
||||
|
||||
TimeBasedUpdater: TypeAlias = Callable[["Mobject", float], object]
|
||||
NonTimeBasedUpdater: TypeAlias = Callable[["Mobject"], object]
|
||||
Updater: TypeAlias = NonTimeBasedUpdater | TimeBasedUpdater
|
||||
|
||||
_TimeBasedUpdater: TypeAlias = Callable[["Mobject", float], object]
|
||||
_NonTimeBasedUpdater: TypeAlias = Callable[["Mobject"], object]
|
||||
_Updater: TypeAlias = _NonTimeBasedUpdater | _TimeBasedUpdater
|
||||
|
||||
|
||||
class Mobject:
|
||||
|
|
@ -83,16 +89,18 @@ class Mobject:
|
|||
|
||||
"""
|
||||
|
||||
animation_overrides = {}
|
||||
original_id: str
|
||||
_original__init__: Callable[..., None]
|
||||
animation_overrides: dict[
|
||||
type[Animation],
|
||||
FunctionOverride,
|
||||
] = {}
|
||||
|
||||
@classmethod
|
||||
def __init_subclass__(cls, **kwargs) -> None:
|
||||
def __init_subclass__(cls, **kwargs: Any) -> None:
|
||||
super().__init_subclass__(**kwargs)
|
||||
|
||||
cls.animation_overrides: dict[
|
||||
type[Animation],
|
||||
FunctionOverride,
|
||||
] = {}
|
||||
cls.animation_overrides = {}
|
||||
cls._add_intrinsic_animation_overrides()
|
||||
cls._original__init__ = cls.__init__
|
||||
|
||||
|
|
@ -101,16 +109,16 @@ class Mobject:
|
|||
color: ParsableManimColor | list[ParsableManimColor] = WHITE,
|
||||
name: str | None = None,
|
||||
dim: int = 3,
|
||||
target=None,
|
||||
target: Mobject | None = None,
|
||||
z_index: float = 0,
|
||||
) -> None:
|
||||
):
|
||||
self.name = self.__class__.__name__ if name is None else name
|
||||
self.dim = dim
|
||||
self.target = target
|
||||
self.z_index = z_index
|
||||
self.point_hash = None
|
||||
self.submobjects = []
|
||||
self.updaters: list[Updater] = []
|
||||
self.submobjects: list[Mobject] = []
|
||||
self.updaters: list[_Updater] = []
|
||||
self.updating_suspended = False
|
||||
self.color = ManimColor.parse(color)
|
||||
|
||||
|
|
@ -151,7 +159,7 @@ class Mobject:
|
|||
return self._assert_valid_submobjects_internal(submobjects, Mobject)
|
||||
|
||||
def _assert_valid_submobjects_internal(
|
||||
self, submobjects: list[Mobject], mob_class: type[Mobject]
|
||||
self, submobjects: Iterable[Mobject], mob_class: type[Mobject]
|
||||
) -> Self:
|
||||
for i, submob in enumerate(submobjects):
|
||||
if not isinstance(submob, mob_class):
|
||||
|
|
@ -247,7 +255,7 @@ class Mobject:
|
|||
)
|
||||
|
||||
@classmethod
|
||||
def set_default(cls, **kwargs) -> None:
|
||||
def set_default(cls, **kwargs: Any) -> None:
|
||||
"""Sets the default values of keyword arguments.
|
||||
|
||||
If this method is called without any additional keyword
|
||||
|
|
@ -290,8 +298,11 @@ class Mobject:
|
|||
|
||||
"""
|
||||
if kwargs:
|
||||
cls.__init__ = partialmethod(cls.__init__, **kwargs)
|
||||
# Apparently mypy does not correctly understand `partialmethod`:
|
||||
# see https://github.com/python/mypy/issues/8619
|
||||
cls.__init__ = partialmethod(cls.__init__, **kwargs) # type: ignore[assignment]
|
||||
else:
|
||||
# error: Cannot assign to a method [method-assign]
|
||||
cls.__init__ = cls._original__init__
|
||||
|
||||
@property
|
||||
|
|
@ -430,7 +441,7 @@ class Mobject:
|
|||
# can't use typing.cast because Self is under TYPE_CHECKING
|
||||
return _UpdaterBuilder(self) # type: ignore[return-value]
|
||||
|
||||
def __deepcopy__(self, clone_from_id) -> Self:
|
||||
def __deepcopy__(self, clone_from_id: dict[int, Mobject]) -> Self:
|
||||
cls = self.__class__
|
||||
result = cls.__new__(cls)
|
||||
clone_from_id[id(self)] = result
|
||||
|
|
@ -447,7 +458,7 @@ class Mobject:
|
|||
self.points = np.zeros((0, self.dim))
|
||||
return self
|
||||
|
||||
def init_colors(self) -> object:
|
||||
def init_colors(self, propagate_colors: bool = True) -> object:
|
||||
"""Initializes the colors.
|
||||
|
||||
Gets called upon creation. This is an empty method that can be implemented by
|
||||
|
|
@ -567,10 +578,10 @@ class Mobject:
|
|||
self._assert_valid_submobjects([mobject])
|
||||
self.submobjects.insert(index, mobject)
|
||||
|
||||
def __add__(self, mobject: Mobject):
|
||||
def __add__(self, mobject: Mobject) -> Self:
|
||||
raise NotImplementedError
|
||||
|
||||
def __iadd__(self, mobject: Mobject):
|
||||
def __iadd__(self, mobject: Mobject) -> Self:
|
||||
raise NotImplementedError
|
||||
|
||||
def add_to_back(self, *mobjects: Mobject) -> Self:
|
||||
|
|
@ -650,13 +661,13 @@ class Mobject:
|
|||
self.submobjects.remove(mobject)
|
||||
return self
|
||||
|
||||
def __sub__(self, other):
|
||||
def __sub__(self, other: Mobject) -> Self:
|
||||
raise NotImplementedError
|
||||
|
||||
def __isub__(self, other):
|
||||
def __isub__(self, other: Mobject) -> Self:
|
||||
raise NotImplementedError
|
||||
|
||||
def set(self, **kwargs) -> Self:
|
||||
def set(self, **kwargs: Any) -> Self:
|
||||
"""Sets attributes.
|
||||
|
||||
I.e. ``my_mobject.set(foo=1)`` applies ``my_mobject.foo = 1``.
|
||||
|
|
@ -724,7 +735,7 @@ class Mobject:
|
|||
# Remove the "get_" prefix
|
||||
to_get = attr[4:]
|
||||
|
||||
def getter(self):
|
||||
def getter(self: Mobject) -> Any:
|
||||
warnings.warn(
|
||||
"This method is not guaranteed to stay around. Please prefer "
|
||||
"getting the attribute normally.",
|
||||
|
|
@ -741,7 +752,7 @@ class Mobject:
|
|||
# Remove the "set_" prefix
|
||||
to_set = attr[4:]
|
||||
|
||||
def setter(self, value):
|
||||
def setter(self: Mobject, value: Any) -> Mobject:
|
||||
warnings.warn(
|
||||
"This method is not guaranteed to stay around. Please prefer "
|
||||
"setting the attribute normally or with Mobject.set().",
|
||||
|
|
@ -792,7 +803,7 @@ class Mobject:
|
|||
return self.length_over_dim(0)
|
||||
|
||||
@width.setter
|
||||
def width(self, value: float):
|
||||
def width(self, value: float) -> None:
|
||||
self.scale_to_fit_width(value)
|
||||
|
||||
@property
|
||||
|
|
@ -828,7 +839,7 @@ class Mobject:
|
|||
return self.length_over_dim(1)
|
||||
|
||||
@height.setter
|
||||
def height(self, value: float):
|
||||
def height(self, value: float) -> None:
|
||||
self.scale_to_fit_height(value)
|
||||
|
||||
@property
|
||||
|
|
@ -848,7 +859,7 @@ class Mobject:
|
|||
return self.length_over_dim(2)
|
||||
|
||||
@depth.setter
|
||||
def depth(self, value: float):
|
||||
def depth(self, value: float) -> None:
|
||||
self.scale_to_fit_depth(value)
|
||||
|
||||
# Can't be staticmethod because of point_cloud_mobject.py
|
||||
|
|
@ -861,16 +872,13 @@ class Mobject:
|
|||
return self
|
||||
|
||||
# Displaying
|
||||
|
||||
def get_image(self, camera=None) -> PixelArray:
|
||||
def get_image(self, camera: Camera | None = None) -> Image.Image:
|
||||
if camera is None:
|
||||
from ..camera.camera import Camera
|
||||
|
||||
camera = Camera()
|
||||
camera.capture_mobject(self)
|
||||
return camera.get_image()
|
||||
|
||||
def show(self, camera=None) -> None:
|
||||
def show(self, camera: Camera | None = None) -> None:
|
||||
self.get_image(camera=camera).show()
|
||||
|
||||
def save_image(self, name: str | None = None) -> None:
|
||||
|
|
@ -930,18 +938,21 @@ class Mobject:
|
|||
:meth:`get_updaters`
|
||||
|
||||
"""
|
||||
if not self.updating_suspended:
|
||||
for updater in self.updaters:
|
||||
if "dt" in inspect.signature(updater).parameters:
|
||||
updater(self, dt)
|
||||
else:
|
||||
updater(self)
|
||||
if self.updating_suspended:
|
||||
return self
|
||||
for updater in self.updaters:
|
||||
if "dt" in inspect.signature(updater).parameters:
|
||||
time_based_updater = cast(_TimeBasedUpdater, updater)
|
||||
time_based_updater(self, dt)
|
||||
else:
|
||||
non_time_based_updater = cast(_NonTimeBasedUpdater, updater)
|
||||
non_time_based_updater(self)
|
||||
if recursive:
|
||||
for submob in self.submobjects:
|
||||
submob.update(dt, recursive=recursive)
|
||||
return self
|
||||
|
||||
def get_time_based_updaters(self) -> list[TimeBasedUpdater]:
|
||||
def get_time_based_updaters(self) -> list[_TimeBasedUpdater]:
|
||||
"""Return all updaters using the ``dt`` parameter.
|
||||
|
||||
The updaters use this parameter as the input for difference in time.
|
||||
|
|
@ -957,11 +968,12 @@ class Mobject:
|
|||
:meth:`has_time_based_updater`
|
||||
|
||||
"""
|
||||
return [
|
||||
updater
|
||||
for updater in self.updaters
|
||||
if "dt" in inspect.signature(updater).parameters
|
||||
]
|
||||
rv: list[_TimeBasedUpdater] = []
|
||||
for updater in self.updaters:
|
||||
if "dt" in inspect.signature(updater).parameters:
|
||||
time_based_updater = cast(_TimeBasedUpdater, updater)
|
||||
rv.append(time_based_updater)
|
||||
return rv
|
||||
|
||||
def has_time_based_updater(self) -> bool:
|
||||
"""Test if ``self`` has a time based updater.
|
||||
|
|
@ -981,7 +993,7 @@ class Mobject:
|
|||
"dt" in inspect.signature(updater).parameters for updater in self.updaters
|
||||
)
|
||||
|
||||
def get_updaters(self) -> list[Updater]:
|
||||
def get_updaters(self) -> list[_Updater]:
|
||||
"""Return all updaters.
|
||||
|
||||
Returns
|
||||
|
|
@ -997,12 +1009,12 @@ class Mobject:
|
|||
"""
|
||||
return self.updaters
|
||||
|
||||
def get_family_updaters(self) -> list[Updater]:
|
||||
def get_family_updaters(self) -> list[_Updater]:
|
||||
return list(it.chain(*(sm.get_updaters() for sm in self.get_family())))
|
||||
|
||||
def add_updater(
|
||||
self,
|
||||
update_function: Updater,
|
||||
update_function: _Updater,
|
||||
index: int | None = None,
|
||||
call_updater: bool = False,
|
||||
) -> Self:
|
||||
|
|
@ -1076,12 +1088,15 @@ class Mobject:
|
|||
if call_updater:
|
||||
parameters = inspect.signature(update_function).parameters
|
||||
if "dt" in parameters:
|
||||
update_function(self, 0)
|
||||
time_based_updater = cast(_TimeBasedUpdater, update_function)
|
||||
time_based_updater(self, 0)
|
||||
else:
|
||||
update_function(self)
|
||||
non_time_based_updater = cast(_NonTimeBasedUpdater, update_function)
|
||||
non_time_based_updater(self)
|
||||
|
||||
return self
|
||||
|
||||
def remove_updater(self, update_function: Updater) -> Self:
|
||||
def remove_updater(self, update_function: _Updater) -> Self:
|
||||
"""Remove an updater.
|
||||
|
||||
If the same updater is applied multiple times, every instance gets removed.
|
||||
|
|
@ -1328,6 +1343,7 @@ class Mobject:
|
|||
*,
|
||||
about_point: Point3DLike | None = None,
|
||||
about_edge: Vector3DLike | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Self:
|
||||
"""Rotates the :class:`~.Mobject` around a specified axis and point.
|
||||
|
||||
|
|
@ -1516,10 +1532,10 @@ class Mobject:
|
|||
self.play(t.animate.set_value(TAU), run_time=3)
|
||||
"""
|
||||
|
||||
def R3_func(point):
|
||||
def R3_func(point: Point3D) -> Point3D:
|
||||
x, y, z = point
|
||||
xy_complex = function(complex(x, y))
|
||||
return [xy_complex.real, xy_complex.imag, z]
|
||||
return np.array([xy_complex.real, xy_complex.imag, z])
|
||||
|
||||
return self.apply_function(
|
||||
R3_func, about_point=about_point, about_edge=about_edge
|
||||
|
|
@ -1533,7 +1549,7 @@ class Mobject:
|
|||
def repeat(self, count: int) -> Self:
|
||||
"""This can make transition animations nicer"""
|
||||
|
||||
def repeat_array(array):
|
||||
def repeat_array(array: Point3D_Array) -> Point3D_Array:
|
||||
return reduce(lambda a1, a2: np.append(a1, a2, axis=0), [array] * count)
|
||||
|
||||
for mob in self.family_members_with_points():
|
||||
|
|
@ -1563,7 +1579,7 @@ class Mobject:
|
|||
mob.points += about_point
|
||||
return self
|
||||
|
||||
def pose_at_angle(self, **kwargs):
|
||||
def pose_at_angle(self, **kwargs: Any) -> Self:
|
||||
self.rotate(TAU / 14, RIGHT + UP, **kwargs)
|
||||
return self
|
||||
|
||||
|
|
@ -1709,7 +1725,7 @@ class Mobject:
|
|||
self.shift((target_point - point_to_align + buff * np_direction) * coor_mask)
|
||||
return self
|
||||
|
||||
def shift_onto_screen(self, **kwargs) -> Self:
|
||||
def shift_onto_screen(self, **kwargs: Any) -> Self:
|
||||
space_lengths = [config["frame_x_radius"], config["frame_y_radius"]]
|
||||
for vect in UP, DOWN, LEFT, RIGHT:
|
||||
dim = np.argmax(np.abs(vect))
|
||||
|
|
@ -1720,20 +1736,21 @@ class Mobject:
|
|||
self.to_edge(vect, **kwargs)
|
||||
return self
|
||||
|
||||
def is_off_screen(self):
|
||||
def is_off_screen(self) -> bool:
|
||||
if self.get_left()[0] > config["frame_x_radius"]:
|
||||
return True
|
||||
if self.get_right()[0] < -config["frame_x_radius"]:
|
||||
return True
|
||||
if self.get_bottom()[1] > config["frame_y_radius"]:
|
||||
return True
|
||||
return self.get_top()[1] < -config["frame_y_radius"]
|
||||
rv: bool = self.get_top()[1] < -config["frame_y_radius"]
|
||||
return rv
|
||||
|
||||
def stretch_about_point(self, factor: float, dim: int, point: Point3DLike) -> Self:
|
||||
return self.stretch(factor, dim, about_point=point)
|
||||
|
||||
def rescale_to_fit(
|
||||
self, length: float, dim: int, stretch: bool = False, **kwargs
|
||||
self, length: float, dim: int, stretch: bool = False, **kwargs: Any
|
||||
) -> Self:
|
||||
old_length = self.length_over_dim(dim)
|
||||
if old_length == 0:
|
||||
|
|
@ -1744,7 +1761,7 @@ class Mobject:
|
|||
self.scale(length / old_length, **kwargs)
|
||||
return self
|
||||
|
||||
def scale_to_fit_width(self, width: float, **kwargs) -> Self:
|
||||
def scale_to_fit_width(self, width: float, **kwargs: Any) -> Self:
|
||||
"""Scales the :class:`~.Mobject` to fit a width while keeping height/depth proportional.
|
||||
|
||||
Returns
|
||||
|
|
@ -1769,7 +1786,7 @@ class Mobject:
|
|||
"""
|
||||
return self.rescale_to_fit(width, 0, stretch=False, **kwargs)
|
||||
|
||||
def stretch_to_fit_width(self, width: float, **kwargs) -> Self:
|
||||
def stretch_to_fit_width(self, width: float, **kwargs: Any) -> Self:
|
||||
"""Stretches the :class:`~.Mobject` to fit a width, not keeping height/depth proportional.
|
||||
|
||||
Returns
|
||||
|
|
@ -1794,7 +1811,7 @@ class Mobject:
|
|||
"""
|
||||
return self.rescale_to_fit(width, 0, stretch=True, **kwargs)
|
||||
|
||||
def scale_to_fit_height(self, height: float, **kwargs) -> Self:
|
||||
def scale_to_fit_height(self, height: float, **kwargs: Any) -> Self:
|
||||
"""Scales the :class:`~.Mobject` to fit a height while keeping width/depth proportional.
|
||||
|
||||
Returns
|
||||
|
|
@ -1819,7 +1836,7 @@ class Mobject:
|
|||
"""
|
||||
return self.rescale_to_fit(height, 1, stretch=False, **kwargs)
|
||||
|
||||
def stretch_to_fit_height(self, height: float, **kwargs) -> Self:
|
||||
def stretch_to_fit_height(self, height: float, **kwargs: Any) -> Self:
|
||||
"""Stretches the :class:`~.Mobject` to fit a height, not keeping width/depth proportional.
|
||||
|
||||
Returns
|
||||
|
|
@ -1844,15 +1861,17 @@ class Mobject:
|
|||
"""
|
||||
return self.rescale_to_fit(height, 1, stretch=True, **kwargs)
|
||||
|
||||
def scale_to_fit_depth(self, depth: float, **kwargs) -> Self:
|
||||
def scale_to_fit_depth(self, depth: float, **kwargs: Any) -> Self:
|
||||
"""Scales the :class:`~.Mobject` to fit a depth while keeping width/height proportional."""
|
||||
return self.rescale_to_fit(depth, 2, stretch=False, **kwargs)
|
||||
|
||||
def stretch_to_fit_depth(self, depth: float, **kwargs) -> Self:
|
||||
def stretch_to_fit_depth(self, depth: float, **kwargs: Any) -> Self:
|
||||
"""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: Vector3DLike = ORIGIN) -> Self:
|
||||
def set_coord(
|
||||
self, value: float, dim: int, direction: Vector3DLike = ORIGIN
|
||||
) -> Self:
|
||||
curr = self.get_coord(dim, direction)
|
||||
shift_vect = np.zeros(self.dim)
|
||||
shift_vect[dim] = value - curr
|
||||
|
|
@ -1874,18 +1893,22 @@ class Mobject:
|
|||
def space_out_submobjects(self, factor: float = 2.5, direction: np.ndarray = RIGHT, **kwargs) -> Self:
|
||||
mobject_centre = self.get_center() # store the coordinate of centre of the Mobject
|
||||
|
||||
actual_submobjects = [submobject for submobject in self.get_family() if len(submobject.submobjects)==0] # list of all the submobjects which do not have submobjectes of their own
|
||||
actual_submobjects = [
|
||||
submobject
|
||||
for submobject in self.get_family()
|
||||
if len(submobject.submobjects) == 0
|
||||
] # list of all the submobjects which do not have submobjectes of their own
|
||||
|
||||
# store the coordinates of all the submobjects before they are spaced out.
|
||||
submobject_center = [submobject.get_center() for submobject in actual_submobjects]
|
||||
|
||||
for i,submobject in enumerate(actual_submobjects):
|
||||
# How far is this submobject from the group center
|
||||
offset = submobject_center[i] - mobject_centre
|
||||
|
||||
offset = submobject_center[i] - mobject_centre
|
||||
|
||||
# Calculate how much to shift it from it's current position, but only in x direction, since we are multiplying offset by direction: RIGHT, i.e. by [1,0,0]
|
||||
submobject.shift(direction * offset * (factor - 1))
|
||||
|
||||
|
||||
return self
|
||||
|
||||
def move_to(
|
||||
|
|
@ -1961,7 +1984,10 @@ class Mobject:
|
|||
|
||||
# Background rectangle
|
||||
def add_background_rectangle(
|
||||
self, color: ParsableManimColor | None = None, opacity: float = 0.75, **kwargs
|
||||
self,
|
||||
color: ParsableManimColor | None = None,
|
||||
opacity: float = 0.75,
|
||||
**kwargs: Any,
|
||||
) -> Self:
|
||||
"""Add a BackgroundRectangle as submobject.
|
||||
|
||||
|
|
@ -2000,12 +2026,14 @@ class Mobject:
|
|||
self.add_to_back(self.background_rectangle)
|
||||
return self
|
||||
|
||||
def add_background_rectangle_to_submobjects(self, **kwargs) -> Self:
|
||||
def add_background_rectangle_to_submobjects(self, **kwargs: Any) -> Self:
|
||||
for submobject in self.submobjects:
|
||||
submobject.add_background_rectangle(**kwargs)
|
||||
return self
|
||||
|
||||
def add_background_rectangle_to_family_members_with_points(self, **kwargs) -> Self:
|
||||
def add_background_rectangle_to_family_members_with_points(
|
||||
self, **kwargs: Any
|
||||
) -> Self:
|
||||
for mob in self.family_members_with_points():
|
||||
mob.add_background_rectangle(**kwargs)
|
||||
return self
|
||||
|
|
@ -2013,7 +2041,10 @@ class Mobject:
|
|||
# Color functions
|
||||
|
||||
def set_color(
|
||||
self, color: ParsableManimColor = PURE_YELLOW, family: bool = True
|
||||
self,
|
||||
color: ParsableManimColor = PURE_YELLOW,
|
||||
alpha: Any = None,
|
||||
family: bool = True,
|
||||
) -> Self:
|
||||
"""Condition is function which takes in one arguments, (x, y, z).
|
||||
Here it just recurses to submobjects, but in subclasses this
|
||||
|
|
@ -2055,7 +2086,7 @@ class Mobject:
|
|||
)
|
||||
return self
|
||||
|
||||
def set_submobject_colors_by_gradient(self, *colors: Iterable[ParsableManimColor]):
|
||||
def set_submobject_colors_by_gradient(self, *colors: ParsableManimColor) -> Self:
|
||||
if len(colors) == 0:
|
||||
raise ValueError("Need at least one color")
|
||||
elif len(colors) == 1:
|
||||
|
|
@ -2081,7 +2112,9 @@ class Mobject:
|
|||
for mob in self.family_members_with_points():
|
||||
t = np.linalg.norm(mob.get_center() - center) / radius
|
||||
t = min(t, 1)
|
||||
mob_color = interpolate_color(inner_color, outer_color, t)
|
||||
mob_color = interpolate_color(
|
||||
ManimColor(inner_color), ManimColor(outer_color), t
|
||||
)
|
||||
mob.set_color(mob_color, family=False)
|
||||
|
||||
return self
|
||||
|
|
@ -2094,7 +2127,7 @@ class Mobject:
|
|||
self, color: ParsableManimColor, alpha: float, family: bool = True
|
||||
) -> Self:
|
||||
if self.get_num_points() > 0:
|
||||
new_color = interpolate_color(self.get_color(), color, alpha)
|
||||
new_color = interpolate_color(self.get_color(), ManimColor(color), alpha)
|
||||
self.set_color(new_color, family=False)
|
||||
if family:
|
||||
for submob in self.submobjects:
|
||||
|
|
@ -2134,12 +2167,14 @@ class Mobject:
|
|||
|
||||
def restore(self) -> Self:
|
||||
"""Restores the state that was previously saved with :meth:`~.Mobject.save_state`."""
|
||||
if not hasattr(self, "saved_state") or self.save_state is None:
|
||||
if not hasattr(self, "saved_state") or self.saved_state is None:
|
||||
raise Exception("Trying to restore without having saved")
|
||||
self.become(self.saved_state)
|
||||
return self
|
||||
|
||||
def reduce_across_dimension(self, reduce_func: Callable, dim: int):
|
||||
def reduce_across_dimension(
|
||||
self, reduce_func: Callable[[Iterable[float]], float], dim: int
|
||||
) -> float:
|
||||
"""Find the min or max value from a dimension across all points in this and submobjects."""
|
||||
assert dim >= 0
|
||||
assert dim <= 2
|
||||
|
|
@ -2159,9 +2194,10 @@ class Mobject:
|
|||
for mobj in self.submobjects:
|
||||
value = mobj.reduce_across_dimension(reduce_func, dim)
|
||||
rv = value if rv is None else reduce_func([value, rv])
|
||||
assert rv is not None
|
||||
return rv
|
||||
|
||||
def nonempty_submobjects(self) -> list[Self]:
|
||||
def nonempty_submobjects(self) -> Sequence[Mobject]:
|
||||
return [
|
||||
submob
|
||||
for submob in self.submobjects
|
||||
|
|
@ -2205,11 +2241,14 @@ class Mobject:
|
|||
)
|
||||
values = np_points[:, dim]
|
||||
if key < 0:
|
||||
return np.min(values)
|
||||
rv: float = np.min(values)
|
||||
return rv
|
||||
elif key == 0:
|
||||
return (np.min(values) + np.max(values)) / 2
|
||||
rv = (np.min(values) + np.max(values)) / 2
|
||||
return rv
|
||||
else:
|
||||
return np.max(values)
|
||||
rv = np.max(values)
|
||||
return rv
|
||||
|
||||
def get_critical_point(self, direction: Vector3DLike) -> Point3D:
|
||||
"""Picture a box bounding the :class:`~.Mobject`. Such a box has
|
||||
|
|
@ -2234,7 +2273,7 @@ class Mobject:
|
|||
result[dim] = self.get_extremum_along_dim(
|
||||
all_points,
|
||||
dim=dim,
|
||||
key=direction[dim],
|
||||
key=np.array(direction[dim]),
|
||||
)
|
||||
return result
|
||||
|
||||
|
|
@ -2309,14 +2348,16 @@ class Mobject:
|
|||
|
||||
def length_over_dim(self, dim: int) -> float:
|
||||
"""Measure the length of an :class:`~.Mobject` in a certain direction."""
|
||||
return self.reduce_across_dimension(
|
||||
max_coord: float = self.reduce_across_dimension(
|
||||
max,
|
||||
dim,
|
||||
) - self.reduce_across_dimension(min, dim)
|
||||
)
|
||||
min_coord: float = self.reduce_across_dimension(min, dim)
|
||||
return max_coord - min_coord
|
||||
|
||||
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])
|
||||
return self.get_extremum_along_dim(dim=dim, key=np.array(direction)[dim])
|
||||
|
||||
def get_x(self, direction: Vector3DLike = ORIGIN) -> float:
|
||||
"""Returns x Point3D of the center of the :class:`~.Mobject` as ``float``"""
|
||||
|
|
@ -2380,19 +2421,19 @@ class Mobject:
|
|||
"""Match the color with the color of another :class:`~.Mobject`."""
|
||||
return self.set_color(mobject.get_color())
|
||||
|
||||
def match_dim_size(self, mobject: Mobject, dim: int, **kwargs) -> Self:
|
||||
def match_dim_size(self, mobject: Mobject, dim: int, **kwargs: Any) -> Self:
|
||||
"""Match the specified dimension with the dimension of another :class:`~.Mobject`."""
|
||||
return self.rescale_to_fit(mobject.length_over_dim(dim), dim, **kwargs)
|
||||
|
||||
def match_width(self, mobject: Mobject, **kwargs) -> Self:
|
||||
def match_width(self, mobject: Mobject, **kwargs: Any) -> Self:
|
||||
"""Match the width with the width of another :class:`~.Mobject`."""
|
||||
return self.match_dim_size(mobject, 0, **kwargs)
|
||||
|
||||
def match_height(self, mobject: Mobject, **kwargs) -> Self:
|
||||
def match_height(self, mobject: Mobject, **kwargs: Any) -> Self:
|
||||
"""Match the height with the height of another :class:`~.Mobject`."""
|
||||
return self.match_dim_size(mobject, 1, **kwargs)
|
||||
|
||||
def match_depth(self, mobject: Mobject, **kwargs) -> Self:
|
||||
def match_depth(self, mobject: Mobject, **kwargs: Any) -> Self:
|
||||
"""Match the depth with the depth of another :class:`~.Mobject`."""
|
||||
return self.match_dim_size(mobject, 2, **kwargs)
|
||||
|
||||
|
|
@ -2406,15 +2447,15 @@ class Mobject:
|
|||
direction=direction,
|
||||
)
|
||||
|
||||
def match_x(self, mobject: Mobject, direction=ORIGIN) -> Self:
|
||||
def match_x(self, mobject: Mobject, direction: Vector3DLike = ORIGIN) -> Self:
|
||||
"""Match x coord. to the x coord. of another :class:`~.Mobject`."""
|
||||
return self.match_coord(mobject, 0, direction)
|
||||
|
||||
def match_y(self, mobject: Mobject, direction=ORIGIN) -> Self:
|
||||
def match_y(self, mobject: Mobject, direction: Vector3DLike = ORIGIN) -> Self:
|
||||
"""Match y coord. to the x coord. of another :class:`~.Mobject`."""
|
||||
return self.match_coord(mobject, 1, direction)
|
||||
|
||||
def match_z(self, mobject: Mobject, direction=ORIGIN) -> Self:
|
||||
def match_z(self, mobject: Mobject, direction: Vector3DLike = ORIGIN) -> Self:
|
||||
"""Match z coord. to the x coord. of another :class:`~.Mobject`."""
|
||||
return self.match_coord(mobject, 2, direction)
|
||||
|
||||
|
|
@ -2441,14 +2482,15 @@ class Mobject:
|
|||
|
||||
# Family matters
|
||||
|
||||
def __getitem__(self, value):
|
||||
def __getitem__(self, value: Any) -> Mobject | Group:
|
||||
self_list = self.split()
|
||||
if isinstance(value, slice):
|
||||
GroupClass = self.get_group_class()
|
||||
return GroupClass(*self_list.__getitem__(value))
|
||||
return self_list.__getitem__(value)
|
||||
rv: Mobject | Group = self_list.__getitem__(value)
|
||||
return rv
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> Iterator[Mobject]:
|
||||
return iter(self.split())
|
||||
|
||||
def __len__(self) -> int:
|
||||
|
|
@ -2462,11 +2504,11 @@ class Mobject:
|
|||
"""Return the base class of this mobject type."""
|
||||
return Mobject
|
||||
|
||||
def split(self) -> list[Self]:
|
||||
result = [self] if len(self.points) > 0 else []
|
||||
def split(self) -> list[Mobject]:
|
||||
result: list[Mobject] = [self] if len(self.points) > 0 else []
|
||||
return result + self.submobjects
|
||||
|
||||
def get_family(self, recurse: bool = True) -> list[Self]:
|
||||
def get_family(self, recurse: bool = True) -> list[Mobject]:
|
||||
"""Lists all mobjects in the hierarchy (family) of the given mobject,
|
||||
including the mobject itself and all its submobjects recursively.
|
||||
|
||||
|
|
@ -2500,7 +2542,7 @@ class Mobject:
|
|||
all_mobjects = [self] + list(it.chain(*sub_families))
|
||||
return remove_list_redundancies(all_mobjects)
|
||||
|
||||
def family_members_with_points(self) -> list[Self]:
|
||||
def family_members_with_points(self) -> list[Mobject]:
|
||||
"""Filters the list of family members (generated by :meth:`.get_family`) to include only mobjects with points.
|
||||
|
||||
Returns
|
||||
|
|
@ -2531,7 +2573,7 @@ class Mobject:
|
|||
direction: Vector3DLike = RIGHT,
|
||||
buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFFER,
|
||||
center: bool = True,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> Self:
|
||||
"""Sorts :class:`~.Mobject` next to each other on screen.
|
||||
|
||||
|
|
@ -2567,7 +2609,7 @@ class Mobject:
|
|||
row_heights: Iterable[float | None] | None = None,
|
||||
col_widths: Iterable[float | None] | None = None,
|
||||
flow_order: str = "rd",
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> Self:
|
||||
"""Arrange submobjects in a grid.
|
||||
|
||||
|
|
@ -2661,13 +2703,18 @@ class Mobject:
|
|||
start_pos = self.get_center()
|
||||
|
||||
# get cols / rows values if given (implicitly)
|
||||
def init_size(num, alignments, sizes):
|
||||
def init_size(
|
||||
num: int | None,
|
||||
alignments: str | None,
|
||||
sizes: Iterable[float | None] | None,
|
||||
) -> int | None:
|
||||
if num is not None:
|
||||
return num
|
||||
if alignments is not None:
|
||||
return len(alignments)
|
||||
if sizes is not None:
|
||||
return len(sizes)
|
||||
return len(list(sizes))
|
||||
return None
|
||||
|
||||
cols = init_size(cols, col_alignments, col_widths)
|
||||
rows = init_size(rows, row_alignments, row_heights)
|
||||
|
|
@ -2678,8 +2725,9 @@ class Mobject:
|
|||
# make the grid as close to quadratic as possible.
|
||||
# choosing cols first can results in cols>rows.
|
||||
# This is favored over rows>cols since in general
|
||||
# the sceene is wider than high.
|
||||
# the scene is wider than high.
|
||||
if rows is None:
|
||||
assert isinstance(cols, int)
|
||||
rows = math.ceil(len(mobs) / cols)
|
||||
if cols is None:
|
||||
cols = math.ceil(len(mobs) / rows)
|
||||
|
|
@ -2694,25 +2742,29 @@ class Mobject:
|
|||
buff_x = buff_y = buff
|
||||
|
||||
# Initialize alignments correctly
|
||||
def init_alignments(alignments, num, mapping, name, dir_):
|
||||
def init_alignments(
|
||||
alignments: str | None,
|
||||
num: int,
|
||||
char_to_direction: dict[str, Vector3D],
|
||||
name: str,
|
||||
dir_: Vector3D,
|
||||
) -> list[Vector3D]:
|
||||
if alignments is None:
|
||||
# Use cell_alignment as fallback
|
||||
return [cell_alignment * dir_] * num
|
||||
if len(alignments) != num:
|
||||
raise ValueError(f"{name}_alignments has a mismatching size.")
|
||||
alignments = list(alignments)
|
||||
for i in range(num):
|
||||
alignments[i] = mapping[alignments[i]]
|
||||
return alignments
|
||||
alignment_directions = [char_to_direction[char] for char in alignments]
|
||||
return alignment_directions
|
||||
|
||||
row_alignments = init_alignments(
|
||||
row_alignment_directions = init_alignments(
|
||||
row_alignments,
|
||||
rows,
|
||||
{"u": UP, "c": ORIGIN, "d": DOWN},
|
||||
"row",
|
||||
RIGHT,
|
||||
)
|
||||
col_alignments = init_alignments(
|
||||
col_alignment_directions = init_alignments(
|
||||
col_alignments,
|
||||
cols,
|
||||
{"l": LEFT, "c": ORIGIN, "r": RIGHT},
|
||||
|
|
@ -2721,7 +2773,7 @@ class Mobject:
|
|||
)
|
||||
# Now row_alignment[r] + col_alignment[c] is the alignment in cell [r][c]
|
||||
|
||||
mapper = {
|
||||
mapper: dict[str, Callable[[int, int], int]] = {
|
||||
"dr": lambda r, c: (rows - r - 1) + c * rows,
|
||||
"dl": lambda r, c: (rows - r - 1) + (cols - c - 1) * rows,
|
||||
"ur": lambda r, c: r + c * rows,
|
||||
|
|
@ -2735,18 +2787,14 @@ class Mobject:
|
|||
raise ValueError(
|
||||
'flow_order must be one of the following values: "dr", "rd", "ld" "dl", "ru", "ur", "lu", "ul".',
|
||||
)
|
||||
flow_order = mapper[flow_order]
|
||||
get_mob_index_by_position = mapper[flow_order]
|
||||
|
||||
# Reverse row_alignments and row_heights. Necessary since the
|
||||
# Reverse row_alignment_directions and row_heights. Necessary since the
|
||||
# grid filling is handled bottom up for simplicity reasons.
|
||||
def reverse(maybe_list):
|
||||
if maybe_list is not None:
|
||||
maybe_list = list(maybe_list)
|
||||
maybe_list.reverse()
|
||||
return maybe_list
|
||||
|
||||
row_alignments = reverse(row_alignments)
|
||||
row_heights = reverse(row_heights)
|
||||
row_alignment_directions.reverse()
|
||||
row_heights_list = list(row_heights) if row_heights is not None else []
|
||||
row_heights_list.reverse()
|
||||
col_widths_list = list(col_widths) if col_widths is not None else []
|
||||
|
||||
placeholder = Mobject()
|
||||
# Used to fill up the grid temporarily, doesn't get added to the scene.
|
||||
|
|
@ -2754,7 +2802,10 @@ class Mobject:
|
|||
# properties of 0.
|
||||
|
||||
mobs.extend([placeholder] * (rows * cols - len(mobs)))
|
||||
grid = [[mobs[flow_order(r, c)] for c in range(cols)] for r in range(rows)]
|
||||
grid = [
|
||||
[mobs[get_mob_index_by_position(r, c)] for c in range(cols)]
|
||||
for r in range(rows)
|
||||
]
|
||||
|
||||
measured_heigths = [
|
||||
max(grid[r][c].height for c in range(cols)) for r in range(rows)
|
||||
|
|
@ -2764,24 +2815,29 @@ class Mobject:
|
|||
]
|
||||
|
||||
# Initialize row_heights / col_widths correctly using measurements as fallback
|
||||
def init_sizes(sizes, num, measures, name):
|
||||
if sizes is None:
|
||||
def init_sizes(
|
||||
sizes: list[float | None] | None, num: int, measures: list[float], name: str
|
||||
) -> list[float]:
|
||||
if sizes is None or len(sizes) == 0:
|
||||
sizes = [None] * num
|
||||
if len(sizes) != num:
|
||||
raise ValueError(f"{name} has a mismatching size.")
|
||||
return [
|
||||
sizes[i] if sizes[i] is not None else measures[i] for i in range(num)
|
||||
size if size is not None else measure
|
||||
for size, measure in zip(sizes, measures, strict=False)
|
||||
]
|
||||
|
||||
heights = init_sizes(row_heights, rows, measured_heigths, "row_heights")
|
||||
widths = init_sizes(col_widths, cols, measured_widths, "col_widths")
|
||||
heights = init_sizes(row_heights_list, rows, measured_heigths, "row_heights")
|
||||
widths = init_sizes(col_widths_list, cols, measured_widths, "col_widths")
|
||||
|
||||
x, y = 0, 0
|
||||
x, y = 0.0, 0.0
|
||||
for r in range(rows):
|
||||
x = 0
|
||||
for c in range(cols):
|
||||
if grid[r][c] is not placeholder:
|
||||
alignment = row_alignments[r] + col_alignments[c]
|
||||
alignment = (
|
||||
row_alignment_directions[r] + col_alignment_directions[c]
|
||||
)
|
||||
line = Line(
|
||||
x * RIGHT + y * UP,
|
||||
(x + widths[c]) * RIGHT + (y + heights[r]) * UP,
|
||||
|
|
@ -2845,7 +2901,7 @@ class Mobject:
|
|||
self.submobjects.reverse()
|
||||
|
||||
# Just here to keep from breaking old scenes.
|
||||
def arrange_submobjects(self, *args, **kwargs) -> Self:
|
||||
def arrange_submobjects(self, *args: Any, **kwargs: Any) -> Self:
|
||||
"""Arrange the position of :attr:`submobjects` with a small buffer.
|
||||
|
||||
Examples
|
||||
|
|
@ -2866,11 +2922,11 @@ class Mobject:
|
|||
"""
|
||||
return self.arrange(*args, **kwargs)
|
||||
|
||||
def sort_submobjects(self, *args, **kwargs) -> Self:
|
||||
def sort_submobjects(self, *args: Any, **kwargs: Any) -> Self:
|
||||
"""Sort the :attr:`submobjects`"""
|
||||
return self.sort(*args, **kwargs)
|
||||
|
||||
def shuffle_submobjects(self, *args, **kwargs) -> None:
|
||||
def shuffle_submobjects(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Shuffles the order of :attr:`submobjects`
|
||||
|
||||
Examples
|
||||
|
|
@ -2938,7 +2994,7 @@ class Mobject:
|
|||
for m1, m2 in zip(self.submobjects, mobject.submobjects, strict=True):
|
||||
m1.align_data(m2)
|
||||
|
||||
def get_point_mobject(self, center=None):
|
||||
def get_point_mobject(self, center: Point3DLike | None = None) -> Point:
|
||||
"""The simplest :class:`~.Mobject` to be transformed to or from self.
|
||||
Should by a point of the appropriate type
|
||||
"""
|
||||
|
|
@ -2954,7 +3010,7 @@ class Mobject:
|
|||
mobject.align_points_with_larger(self)
|
||||
return self
|
||||
|
||||
def align_points_with_larger(self, larger_mobject: Mobject):
|
||||
def align_points_with_larger(self, larger_mobject: Mobject) -> None:
|
||||
raise NotImplementedError("Please override in a child class.")
|
||||
|
||||
def align_submobjects(self, mobject: Mobject) -> Self:
|
||||
|
|
@ -2966,7 +3022,7 @@ class Mobject:
|
|||
mob2.add_n_more_submobjects(max(0, n1 - n2))
|
||||
return self
|
||||
|
||||
def null_point_align(self, mobject: Mobject):
|
||||
def null_point_align(self, mobject: Mobject) -> Self:
|
||||
"""If a :class:`~.Mobject` with points is being aligned to
|
||||
one without, treat both as groups, and push
|
||||
the one with points into its own submobjects
|
||||
|
|
@ -3011,7 +3067,7 @@ class Mobject:
|
|||
self.submobjects = new_submobs
|
||||
return self
|
||||
|
||||
def repeat_submobject(self, submob: Mobject) -> Self:
|
||||
def repeat_submobject(self, submob: Mobject) -> Mobject:
|
||||
return submob.copy()
|
||||
|
||||
def interpolate(
|
||||
|
|
@ -3087,7 +3143,9 @@ class Mobject:
|
|||
self.interpolate_color(mobject1, mobject2, alpha)
|
||||
return self
|
||||
|
||||
def interpolate_color(self, mobject1: Mobject, mobject2: Mobject, alpha: float):
|
||||
def interpolate_color(
|
||||
self, mobject1: Mobject, mobject2: Mobject, alpha: float
|
||||
) -> None:
|
||||
raise NotImplementedError("Please override in a child class.")
|
||||
|
||||
def become(
|
||||
|
|
@ -3318,25 +3376,25 @@ class Group(Mobject, metaclass=ConvertToOpenGL):
|
|||
be added to the group.
|
||||
"""
|
||||
|
||||
def __init__(self, *mobjects, **kwargs) -> None:
|
||||
def __init__(self, *mobjects: Any, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.add(*mobjects)
|
||||
|
||||
|
||||
class _AnimationBuilder:
|
||||
def __init__(self, mobject) -> None:
|
||||
def __init__(self, mobject: Mobject) -> None:
|
||||
self.mobject = mobject
|
||||
self.mobject.generate_target()
|
||||
|
||||
self.overridden_animation = None
|
||||
self.overridden_animation: Animation | None = None
|
||||
self.is_chaining = False
|
||||
self.methods: list[MethodWithArgs] = []
|
||||
|
||||
# Whether animation args can be passed
|
||||
self.cannot_pass_args = False
|
||||
self.anim_args = {}
|
||||
self.anim_args: dict[str, Any] = {}
|
||||
|
||||
def __call__(self, **kwargs) -> Self:
|
||||
def __call__(self, **kwargs: Any) -> Self:
|
||||
if self.cannot_pass_args:
|
||||
raise ValueError(
|
||||
"Animation arguments must be passed before accessing methods and can only be passed once",
|
||||
|
|
@ -3347,7 +3405,7 @@ class _AnimationBuilder:
|
|||
|
||||
return self
|
||||
|
||||
def __getattr__(self, method_name) -> types.MethodType:
|
||||
def __getattr__(self, method_name: str) -> Callable[..., _AnimationBuilder]:
|
||||
method = getattr(self.mobject.target, method_name)
|
||||
has_overridden_animation = hasattr(method, "_override_animate")
|
||||
|
||||
|
|
@ -3356,7 +3414,7 @@ class _AnimationBuilder:
|
|||
"Method chaining is currently not supported for overridden animations",
|
||||
)
|
||||
|
||||
def update_target(*method_args, **method_kwargs):
|
||||
def update_target(*method_args: Any, **method_kwargs: Any) -> _AnimationBuilder:
|
||||
if has_overridden_animation:
|
||||
self.overridden_animation = method._override_animate(
|
||||
self.mobject,
|
||||
|
|
@ -3375,9 +3433,7 @@ class _AnimationBuilder:
|
|||
return update_target
|
||||
|
||||
def build(self) -> Animation:
|
||||
from ..animation.transform import ( # is this to prevent circular import?
|
||||
_MethodAnimation,
|
||||
)
|
||||
from ..animation.transform import _MethodAnimation
|
||||
|
||||
anim = self.overridden_animation or _MethodAnimation(self.mobject, self.methods)
|
||||
|
||||
|
|
@ -3393,9 +3449,9 @@ class _UpdaterBuilder:
|
|||
def __init__(self, mobject: Mobject):
|
||||
self._mobject = mobject
|
||||
|
||||
def __getattr__(self, name: str, /) -> Callable[..., Self]:
|
||||
def __getattr__(self, name: str, /) -> Callable[..., _UpdaterBuilder]:
|
||||
# just return a function that will add the updater
|
||||
def add_updater(*method_args, **method_kwargs) -> Self:
|
||||
def add_updater(*method_args: Any, **method_kwargs: Any) -> _UpdaterBuilder:
|
||||
self._mobject.add_updater(
|
||||
lambda m: getattr(m, name)(*method_args, **method_kwargs),
|
||||
call_updater=True,
|
||||
|
|
@ -3405,7 +3461,9 @@ class _UpdaterBuilder:
|
|||
return add_updater
|
||||
|
||||
|
||||
def override_animate(method) -> types.FunctionType:
|
||||
def override_animate(
|
||||
method: types.MethodType,
|
||||
) -> Callable[[types.MethodType], types.MethodType]:
|
||||
r"""Decorator for overriding method animations.
|
||||
|
||||
This allows to specify a method (returning an :class:`~.Animation`)
|
||||
|
|
@ -3456,9 +3514,11 @@ def override_animate(method) -> types.FunctionType:
|
|||
self.wait()
|
||||
|
||||
"""
|
||||
temp_method = cast(_AnimationBuilder, method)
|
||||
|
||||
def decorator(animation_method):
|
||||
method._override_animate = animation_method
|
||||
def decorator(animation_method: types.MethodType) -> types.MethodType:
|
||||
# error: "Callable[..., Animation]" has no attribute "_override_animate" [attr-defined]
|
||||
temp_method._override_animate = animation_method # type: ignore[attr-defined]
|
||||
return animation_method
|
||||
|
||||
return decorator
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ class OpenGLTipableVMobject(OpenGLVMobject):
|
|||
"""
|
||||
tip = self.create_tip(at_start, **kwargs)
|
||||
self.reset_endpoints_based_on_tip(tip, at_start)
|
||||
self.asign_tip_attr(tip, at_start)
|
||||
self.assign_tip_attr(tip, at_start)
|
||||
self.add(tip)
|
||||
return self
|
||||
|
||||
|
|
@ -160,7 +160,7 @@ class OpenGLTipableVMobject(OpenGLVMobject):
|
|||
self.put_start_and_end_on(start, end)
|
||||
return self
|
||||
|
||||
def asign_tip_attr(self, tip: OpenGLArrowTip, at_start: bool) -> Self:
|
||||
def assign_tip_attr(self, tip: OpenGLArrowTip, at_start: bool) -> Self:
|
||||
if at_start:
|
||||
self.start_tip = tip
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1222,7 +1222,7 @@ class OpenGLMobject:
|
|||
) -> Sequence[Vector3D]:
|
||||
if str_alignments is None:
|
||||
# Use cell_alignment as fallback
|
||||
return [cast(Vector3D, cell_alignment * direction)] * num
|
||||
return [cast("Vector3D", cell_alignment * direction)] * num
|
||||
if len(str_alignments) != num:
|
||||
raise ValueError(f"{name}_alignments has a mismatching size.")
|
||||
return [mapping[letter] for letter in str_alignments]
|
||||
|
|
@ -3037,7 +3037,7 @@ class OpenGLPoint(OpenGLMobject):
|
|||
return self.artificial_height
|
||||
|
||||
def get_location(self) -> Point3D:
|
||||
return cast(Point3D, self.points[0]).copy()
|
||||
return cast("Point3D", self.points[0]).copy()
|
||||
|
||||
@override
|
||||
def get_bounding_box_point(self, *args: object, **kwargs: Any) -> Point3D:
|
||||
|
|
|
|||
|
|
@ -1227,7 +1227,9 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
def get_nth_subpath(path_list, n):
|
||||
if n >= len(path_list):
|
||||
# Create a null path at the very end
|
||||
return [path_list[-1][-1]] * nppc
|
||||
if len(path_list) == 0:
|
||||
return np.tile(np.zeros(3), (nppc, 1))
|
||||
return np.tile(path_list[-1][-1], (nppc, 1))
|
||||
path = path_list[n]
|
||||
# Check for useless points at the end of the path and remove them
|
||||
# https://github.com/ManimCommunity/manim/issues/1959
|
||||
|
|
|
|||
|
|
@ -237,6 +237,26 @@ class MathTex(SingleStringMathTex):
|
|||
t = MathTex(r"\int_a^b f'(x) dx = f(b)- f(a)")
|
||||
self.add(t)
|
||||
|
||||
Notes
|
||||
-----
|
||||
Double-brace notation ``{{ ... }}`` can be used to split a single
|
||||
string argument into multiple submobjects without having to pass
|
||||
separate strings::
|
||||
|
||||
MathTex(r"{{ a^2 }} + {{ b^2 }} = {{ c^2 }}")
|
||||
|
||||
Each ``{{ ... }}`` group and every piece of text between groups
|
||||
becomes its own submobject, which is useful for
|
||||
:class:`~.TransformMatchingTex` animations.
|
||||
|
||||
For ``{{`` to be recognised as a group opener it must appear either
|
||||
at the very start of the string or be immediately preceded by a
|
||||
whitespace character. ``{{`` that follows non-whitespace — such as
|
||||
in ``\frac{{{n}}}{k}`` or ``a^{{2}}`` — is left untouched, so
|
||||
ordinary nested-brace LaTeX is not accidentally split. To prevent
|
||||
an unintentional split, insert a space between the two braces:
|
||||
``{{ ... }}`` → ``{ { ... } }``.
|
||||
|
||||
Tests
|
||||
-----
|
||||
Check that creating a :class:`~.MathTex` works::
|
||||
|
|
@ -318,15 +338,94 @@ class MathTex(SingleStringMathTex):
|
|||
tex_strings_validated = [
|
||||
string if isinstance(string, str) else str(string) for string in tex_strings
|
||||
]
|
||||
# Locate double curly bracers
|
||||
# Locate double curly bracers and split on them.
|
||||
tex_strings_validated_two = []
|
||||
for tex_string in tex_strings_validated:
|
||||
split = re.split(r"{{|}}", tex_string)
|
||||
split = self._split_double_braces(tex_string)
|
||||
tex_strings_validated_two.extend(split)
|
||||
if len(tex_strings_validated_two) > len(tex_strings_validated):
|
||||
self.brace_notation_split_occurred = True
|
||||
return [string for string in tex_strings_validated_two if len(string) > 0]
|
||||
|
||||
@staticmethod
|
||||
def _split_double_braces(tex_string: str) -> list[str]:
|
||||
r"""Split *tex_string* on Manim's ``{{ ... }}`` double-brace notation.
|
||||
|
||||
Rules that avoid false positives on ordinary LaTeX source:
|
||||
|
||||
* ``{{`` is only treated as a group opener when it appears at the very
|
||||
start of the string or is immediately preceded by a whitespace
|
||||
character. Naturally-occurring ``{{`` in LaTeX is usually preceded
|
||||
by non-whitespace (e.g. ``\frac{{{n}}}{k}`` or ``a^{{2}}``), so
|
||||
the whitespace guard eliminates the most common false positives
|
||||
without any brace-depth bookkeeping on the outer string.
|
||||
|
||||
* Inside an open group the depth of *real* LaTeX braces is tracked.
|
||||
``}}`` only closes the Manim group when the inner depth is zero,
|
||||
so ``{{ a^{b^{c}} }}`` is handled correctly.
|
||||
|
||||
* Escape sequences are consumed as two-character units in priority
|
||||
order: ``\\`` first (escaped backslash), then ``\{`` / ``\}``
|
||||
(escaped braces). This ensures e.g. ``\\}}`` is read as an
|
||||
escaped backslash followed by a real ``}}`` rather than as
|
||||
``\`` + ``\}`` + lone ``}``.
|
||||
"""
|
||||
segments: list[str] = []
|
||||
current = ""
|
||||
i = 0
|
||||
inside_manim = False
|
||||
inner_depth = 0
|
||||
|
||||
while i < len(tex_string):
|
||||
# --- consume escape sequences as atomic units ---
|
||||
if tex_string[i] == "\\" and i + 1 < len(tex_string):
|
||||
next_ch = tex_string[i + 1]
|
||||
if next_ch == "\\" or next_ch in "{}":
|
||||
# \\ (escaped backslash) checked before \{ / \} so that
|
||||
# the second \ in \\ is never mistaken for an escape prefix.
|
||||
current += tex_string[i : i + 2]
|
||||
i += 2
|
||||
continue
|
||||
|
||||
if not inside_manim:
|
||||
# {{ opens a Manim group only at start-of-string or after whitespace.
|
||||
if tex_string[i : i + 2] == "{{" and (
|
||||
i == 0 or tex_string[i - 1].isspace()
|
||||
):
|
||||
segments.append(current)
|
||||
current = ""
|
||||
inside_manim = True
|
||||
inner_depth = 0
|
||||
i += 2
|
||||
else:
|
||||
current += tex_string[i]
|
||||
i += 1
|
||||
else:
|
||||
if tex_string[i] == "{":
|
||||
inner_depth += 1
|
||||
current += tex_string[i]
|
||||
i += 1
|
||||
elif (
|
||||
tex_string[i] == "}"
|
||||
and inner_depth == 0
|
||||
and tex_string[i : i + 2] == "}}"
|
||||
):
|
||||
# }} at inner depth 0 closes the Manim group.
|
||||
segments.append(current)
|
||||
current = ""
|
||||
inside_manim = False
|
||||
i += 2
|
||||
elif tex_string[i] == "}":
|
||||
inner_depth -= 1
|
||||
current += tex_string[i]
|
||||
i += 1
|
||||
else:
|
||||
current += tex_string[i]
|
||||
i += 1
|
||||
|
||||
segments.append(current)
|
||||
return segments
|
||||
|
||||
def _join_tex_strings_with_unique_deliminters(
|
||||
self, tex_strings: list[str], substrings_to_isolate: Iterable[str]
|
||||
) -> str:
|
||||
|
|
@ -488,7 +587,7 @@ class MathTex(SingleStringMathTex):
|
|||
self.id_to_vgroup_dict[match[1]].set_color(color)
|
||||
return self
|
||||
|
||||
def index_of_part(self, part: MathTex) -> int:
|
||||
def index_of_part(self, part: VMobject) -> int:
|
||||
split_self = self.split()
|
||||
if part not in split_self:
|
||||
raise ValueError("Trying to get index of part not in MathTex")
|
||||
|
|
|
|||
|
|
@ -166,9 +166,12 @@ class Paragraph(VGroup):
|
|||
lines_str_list = lines_str.split("\n")
|
||||
self.chars = self._gen_chars(lines_str_list)
|
||||
|
||||
self.lines = [list(self.chars), [self.alignment] * len(self.chars)]
|
||||
self.lines_initial_positions = [line.get_center() for line in self.lines[0]]
|
||||
self.add(*self.lines[0])
|
||||
# TODO: If possible get rid of self.lines_chars, as it seems to be a
|
||||
# listified duplicate of self.chars.
|
||||
self.lines_chars = list(self.chars)
|
||||
self.lines_alignments = [self.alignment] * len(self.chars)
|
||||
self.lines_initial_positions = [line.get_center() for line in self.lines_chars]
|
||||
self.add(*self.lines_chars)
|
||||
self.move_to(np.array([0, 0, 0]))
|
||||
if self.alignment:
|
||||
self._set_all_lines_alignments(self.alignment)
|
||||
|
|
@ -221,7 +224,7 @@ class Paragraph(VGroup):
|
|||
alignment
|
||||
Defines the alignment of paragraph. Possible values are "left", "right", "center".
|
||||
"""
|
||||
for line_no in range(len(self.lines[0])):
|
||||
for line_no in range(len(self.lines_chars)):
|
||||
self._change_alignment_for_a_line(alignment, line_no)
|
||||
return self
|
||||
|
||||
|
|
@ -240,8 +243,8 @@ class Paragraph(VGroup):
|
|||
|
||||
def _set_all_lines_to_initial_positions(self) -> Paragraph:
|
||||
"""Set all lines to their initial positions."""
|
||||
self.lines[1] = [None] * len(self.lines[0])
|
||||
for line_no in range(len(self.lines[0])):
|
||||
self.lines_alignments = [None] * len(self.lines_chars)
|
||||
for line_no in range(len(self.lines_chars)):
|
||||
self[line_no].move_to(
|
||||
self.get_center() + self.lines_initial_positions[line_no],
|
||||
)
|
||||
|
|
@ -255,7 +258,7 @@ class Paragraph(VGroup):
|
|||
line_no
|
||||
Defines the line number for which we want to set given alignment.
|
||||
"""
|
||||
self.lines[1][line_no] = None
|
||||
self.lines_alignments[line_no] = None
|
||||
self[line_no].move_to(self.get_center() + self.lines_initial_positions[line_no])
|
||||
return self
|
||||
|
||||
|
|
@ -269,12 +272,12 @@ class Paragraph(VGroup):
|
|||
line_no
|
||||
Defines the line number for which we want to set given alignment.
|
||||
"""
|
||||
self.lines[1][line_no] = alignment
|
||||
if self.lines[1][line_no] == "center":
|
||||
self.lines_alignments[line_no] = alignment
|
||||
if self.lines_alignments[line_no] == "center":
|
||||
self[line_no].move_to(
|
||||
np.array([self.get_center()[0], self[line_no].get_center()[1], 0]),
|
||||
)
|
||||
elif self.lines[1][line_no] == "right":
|
||||
elif self.lines_alignments[line_no] == "right":
|
||||
self[line_no].move_to(
|
||||
np.array(
|
||||
[
|
||||
|
|
@ -284,7 +287,7 @@ class Paragraph(VGroup):
|
|||
],
|
||||
),
|
||||
)
|
||||
elif self.lines[1][line_no] == "left":
|
||||
elif self.lines_alignments[line_no] == "left":
|
||||
self[line_no].move_to(
|
||||
np.array(
|
||||
[
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
|
|||
self.pre_function_handle_to_anchor_scale_factor = (
|
||||
pre_function_handle_to_anchor_scale_factor
|
||||
)
|
||||
self.list_of_faces: list[ThreeDVMobject] = []
|
||||
self._func = func
|
||||
self._setup_in_uv_space()
|
||||
self.apply_function(lambda p: func(p[0], p[1]))
|
||||
|
|
@ -172,6 +173,7 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
|
|||
def _setup_in_uv_space(self) -> None:
|
||||
u_values, v_values = self._get_u_values_and_v_values()
|
||||
faces = VGroup()
|
||||
self.list_of_faces = []
|
||||
for i in range(len(u_values) - 1):
|
||||
for j in range(len(v_values) - 1):
|
||||
u1, u2 = u_values[i : i + 2]
|
||||
|
|
@ -193,6 +195,7 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
|
|||
face.u2 = u2
|
||||
face.v1 = v1
|
||||
face.v2 = v2
|
||||
self.list_of_faces.append(face)
|
||||
faces.set_fill(color=self.fill_color, opacity=self.fill_opacity)
|
||||
faces.set_stroke(
|
||||
color=self.stroke_color,
|
||||
|
|
@ -223,7 +226,7 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
|
|||
The parametric surface with an alternating pattern.
|
||||
"""
|
||||
n_colors = len(colors)
|
||||
for face in self:
|
||||
for face in self.list_of_faces:
|
||||
c_index = (face.u_index + face.v_index) % n_colors
|
||||
face.set_fill(colors[c_index], opacity=opacity)
|
||||
return self
|
||||
|
|
@ -376,13 +379,7 @@ class Sphere(Surface):
|
|||
class ExampleSphere(ThreeDScene):
|
||||
def construct(self):
|
||||
self.set_camera_orientation(phi=PI / 6, theta=PI / 6)
|
||||
sphere1 = Sphere(
|
||||
center=(3, 0, 0),
|
||||
radius=1,
|
||||
resolution=(20, 20),
|
||||
u_range=[0.001, PI - 0.001],
|
||||
v_range=[0, TAU]
|
||||
)
|
||||
sphere1 = Sphere(center=(3, 0, 0), radius=1, resolution=(20, 20))
|
||||
sphere1.set_color(RED)
|
||||
self.add(sphere1)
|
||||
sphere2 = Sphere(center=(-1, -3, 0), radius=2, resolution=(18, 18))
|
||||
|
|
@ -391,6 +388,57 @@ class Sphere(Surface):
|
|||
sphere3 = Sphere(center=(-1, 2, 0), radius=2, resolution=(16, 16))
|
||||
sphere3.set_color(BLUE)
|
||||
self.add(sphere3)
|
||||
|
||||
This example shows that overlapping spheres can intersect with rough transitions.
|
||||
|
||||
.. manim:: ExampleSphereOverlap
|
||||
:save_last_frame:
|
||||
|
||||
class ExampleSphereOverlap(ThreeDScene):
|
||||
def construct(self):
|
||||
self.set_camera_orientation(phi=PI / 4, theta=PI / 4)
|
||||
sphere1 = Sphere(center=(0, 0, 0), radius=1, resolution=(20, 20))
|
||||
sphere1.set_color(RED)
|
||||
self.add(sphere1)
|
||||
sphere2 = Sphere(center=(-0.5, -1, 0.5), radius=1.2, resolution=(20, 20))
|
||||
sphere2.set_color(GREEN)
|
||||
self.add(sphere2)
|
||||
sphere3 = Sphere(center=(1, -1, 0), radius=1.1, resolution=(20, 20))
|
||||
sphere3.set_color(BLUE)
|
||||
self.add(sphere3)
|
||||
|
||||
In this example, by modifying ``u_range`` (the range of the azimuthal angle) and
|
||||
``v_range`` (the range of the polar angle), it is possible to obtain a portion of a
|
||||
sphere:
|
||||
|
||||
.. manim:: ExamplePartialSpheres
|
||||
:save_last_frame:
|
||||
|
||||
class ExamplePartialSpheres(ThreeDScene):
|
||||
def construct(self):
|
||||
self.set_camera_orientation(phi=PI / 4)
|
||||
sphere1 = Sphere(
|
||||
center=(-3, 0, 0),
|
||||
resolution=(10, 20),
|
||||
u_range=[TAU / 4, 3 * TAU / 4],
|
||||
)
|
||||
sphere1.set_color(RED)
|
||||
self.add(sphere1)
|
||||
sphere2 = Sphere(
|
||||
center=(0, 0, 0),
|
||||
resolution=(20, 10),
|
||||
v_range=[0, TAU / 4],
|
||||
)
|
||||
sphere2.set_color(GREEN)
|
||||
self.add(sphere2)
|
||||
sphere3 = Sphere(
|
||||
center=(3, 0, 0),
|
||||
resolution=(5, 10),
|
||||
u_range=[3 * TAU / 4, TAU],
|
||||
v_range=[TAU / 4, TAU / 2],
|
||||
)
|
||||
sphere3.set_color(BLUE)
|
||||
self.add(sphere3)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ class AbstractImageMobject(Mobject):
|
|||
def get_pixel_array(self) -> PixelArray:
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_color( # type: ignore[override]
|
||||
def set_color(
|
||||
self,
|
||||
color: ParsableManimColor = YELLOW_C,
|
||||
alpha: Any = None,
|
||||
|
|
@ -217,7 +217,7 @@ class ImageMobject(AbstractImageMobject):
|
|||
"""A simple getter method."""
|
||||
return self.pixel_array
|
||||
|
||||
def set_color( # type: ignore[override]
|
||||
def set_color(
|
||||
self,
|
||||
color: ParsableManimColor = YELLOW_C,
|
||||
alpha: Any = None,
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ from manim.utils.iterables import (
|
|||
from manim.utils.space_ops import rotate_vector, shoelace_direction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
from typing import Self
|
||||
|
||||
import numpy.typing as npt
|
||||
|
|
@ -103,6 +104,7 @@ class VMobject(Mobject):
|
|||
"""
|
||||
|
||||
sheen_factor = 0.0
|
||||
target: VMobject
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -172,6 +174,9 @@ class VMobject(Mobject):
|
|||
def _assert_valid_submobjects(self, submobjects: Iterable[VMobject]) -> Self:
|
||||
return self._assert_valid_submobjects_internal(submobjects, VMobject)
|
||||
|
||||
def __iter__(self) -> Iterator[VMobject]:
|
||||
return iter(self.split())
|
||||
|
||||
# OpenGL compatibility
|
||||
@property
|
||||
def n_points_per_curve(self) -> int:
|
||||
|
|
@ -630,6 +635,17 @@ class VMobject(Mobject):
|
|||
|
||||
color: ManimColor = property(get_color, set_color)
|
||||
|
||||
def nonempty_submobjects(self) -> Sequence[VMobject]:
|
||||
return [
|
||||
submob
|
||||
for submob in self.submobjects
|
||||
if len(submob.submobjects) != 0 or len(submob.points) != 0
|
||||
]
|
||||
|
||||
def split(self) -> list[VMobject]:
|
||||
result: list[VMobject] = [self] if len(self.points) > 0 else []
|
||||
return result + self.submobjects
|
||||
|
||||
def set_sheen_direction(self, direction: Vector3DLike, family: bool = True) -> Self:
|
||||
"""Sets the direction of the applied sheen.
|
||||
|
||||
|
|
@ -1769,7 +1785,9 @@ class VMobject(Mobject):
|
|||
def get_nth_subpath(path_list, n):
|
||||
if n >= len(path_list):
|
||||
# Create a null path at the very end
|
||||
return [path_list[-1][-1]] * nppcc
|
||||
if len(path_list) == 0:
|
||||
return np.tile(np.zeros(3), (nppcc, 1))
|
||||
return np.tile(path_list[-1][-1], (nppcc, 1))
|
||||
path = path_list[n]
|
||||
# Check for useless points at the end of the path and remove them
|
||||
# https://github.com/ManimCommunity/manim/issues/1959
|
||||
|
|
@ -2303,6 +2321,11 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
|
|||
self._assert_valid_submobjects(tuplify(value))
|
||||
self.submobjects[key] = value
|
||||
|
||||
def __getitem__(self, key: int | slice) -> VMobject:
|
||||
if isinstance(key, slice):
|
||||
return VGroup(self.submobjects[key])
|
||||
return self.submobjects[key]
|
||||
|
||||
|
||||
class VDict(VMobject, metaclass=ConvertToOpenGL):
|
||||
"""A VGroup-like class, also offering submobject access by
|
||||
|
|
|
|||
|
|
@ -25,14 +25,23 @@ class Window(PygletWindow):
|
|||
def __init__(
|
||||
self,
|
||||
renderer: OpenGLRenderer,
|
||||
window_size: str = config.window_size,
|
||||
window_size: str | tuple[int, ...] = config.window_size,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
monitors = get_monitors()
|
||||
mon_index = config.window_monitor
|
||||
monitor = monitors[min(mon_index, len(monitors) - 1)]
|
||||
|
||||
if window_size == "default":
|
||||
invalid_window_size_error_message = (
|
||||
"window_size must be specified either as 'default', a string of the form "
|
||||
"'width,height', or a tuple of 2 ints of the form (width, height)."
|
||||
)
|
||||
|
||||
if isinstance(window_size, tuple):
|
||||
if len(window_size) != 2:
|
||||
raise ValueError(invalid_window_size_error_message)
|
||||
size = window_size
|
||||
elif window_size == "default":
|
||||
# make window_width half the width of the monitor
|
||||
# but make it full screen if --fullscreen
|
||||
window_width = monitor.width
|
||||
|
|
@ -48,9 +57,7 @@ class Window(PygletWindow):
|
|||
(window_width, window_height) = tuple(map(int, window_size.split(",")))
|
||||
size = (window_width, window_height)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Window_size must be specified as 'width,height' or 'default'.",
|
||||
)
|
||||
raise ValueError(invalid_window_size_error_message)
|
||||
|
||||
super().__init__(size=size)
|
||||
|
||||
|
|
|
|||
|
|
@ -903,7 +903,24 @@ class Scene:
|
|||
# as soon as there's one that needs updating of
|
||||
# some kind per frame, return the list from that
|
||||
# point forward.
|
||||
animation_mobjects = [anim.mobject for anim in animations]
|
||||
# Imported inside the method to avoid cyclic import.
|
||||
from ..animation.composition import AnimationGroup
|
||||
|
||||
def _collect_animation_mobjects(
|
||||
nested_animations: Iterable[Animation],
|
||||
) -> list[Mobject | OpenGLMobject]:
|
||||
animation_mobjects: list[Mobject | OpenGLMobject] = []
|
||||
for anim in nested_animations:
|
||||
if isinstance(anim, AnimationGroup):
|
||||
animation_mobjects.extend(
|
||||
_collect_animation_mobjects(anim.animations),
|
||||
)
|
||||
else:
|
||||
animation_mobjects.extend(anim.mobject.get_family())
|
||||
return animation_mobjects
|
||||
|
||||
animation_mobjects = _collect_animation_mobjects(animations)
|
||||
|
||||
mobjects = self.get_mobject_family_members()
|
||||
for i, mob in enumerate(mobjects):
|
||||
update_possibilities = [
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ __all__ = ["SceneFileWriter"]
|
|||
|
||||
import json
|
||||
import shutil
|
||||
import warnings
|
||||
from fractions import Fraction
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
|
|
@ -17,7 +18,17 @@ import av
|
|||
import numpy as np
|
||||
import srt
|
||||
from PIL import Image
|
||||
from pydub import AudioSegment
|
||||
|
||||
# Manim handles audio conversion through PyAV directly. Importing pydub emits a
|
||||
# RuntimeWarning if ffmpeg/avconv is not on PATH, even when only WAV code paths
|
||||
# are used (which do not need ffmpeg). Silence this specific warning.
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings(
|
||||
"ignore",
|
||||
message=r".*ffmpeg or avconv.*",
|
||||
category=RuntimeWarning,
|
||||
)
|
||||
from pydub import AudioSegment
|
||||
|
||||
from manim import __version__
|
||||
|
||||
|
|
|
|||
|
|
@ -281,15 +281,19 @@ class VectorScene(Scene):
|
|||
color (str),
|
||||
label_scale_factor=VECTOR_LABEL_SCALE_FACTOR (int, float),
|
||||
"""
|
||||
i_hat, j_hat = self.get_basis_vectors()
|
||||
i_hat = self.get_basis_vectors().submobjects[0]
|
||||
j_hat = self.get_basis_vectors().submobjects[1]
|
||||
return VGroup(
|
||||
*(
|
||||
self.get_vector_label(
|
||||
vect, label, color=color, label_scale_factor=1, **kwargs
|
||||
)
|
||||
for vect, label, color in [
|
||||
(i_hat, "\\hat{\\imath}", X_COLOR),
|
||||
(j_hat, "\\hat{\\jmath}", Y_COLOR),
|
||||
# Casting i_hat and j_hat to Vector, as the VGroup from
|
||||
# self.get_basis_vectors() contains two vectors, but the
|
||||
# type checker is currently not aware of that.
|
||||
(cast(Vector, i_hat), "\\hat{\\imath}", X_COLOR),
|
||||
(cast(Vector, j_hat), "\\hat{\\jmath}", Y_COLOR),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
|
@ -517,7 +521,9 @@ 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 = cast(VGroup, array.get_entries())
|
||||
temp = array.get_entries()
|
||||
x_coord = temp.submobjects[0]
|
||||
y_coord = temp.submobjects[1]
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ def bezier(
|
|||
containing :math:`n` values to evaluate the Bézier curve at, returning instead
|
||||
an :math:`(n, 3)`-shaped :class:`~.Point3D_Array` containing the points
|
||||
resulting from evaluating the Bézier at each of the :math:`n` values.
|
||||
|
||||
.. warning::
|
||||
If passing a vector of :math:`t`-values to ``bezier_func``, it **must**
|
||||
be a column vector/matrix of shape :math:`(n, 1)`. Passing an 1D array of
|
||||
|
|
@ -106,6 +107,7 @@ def bezier(
|
|||
Bézier curve defined by ``points`` is evaluated at the corresponding :math:`i`-th
|
||||
value in ``t``, returning again an :math:`(M, 3)`-shaped :class:`~.Point3D_Array`
|
||||
containing those :math:`M` evaluations.
|
||||
|
||||
.. warning::
|
||||
Unlike the previous case, if you pass a :class:`~.ColVector` to ``bezier_func``,
|
||||
it **must** contain exactly :math:`M` values, each value for each of the :math:`M`
|
||||
|
|
|
|||
|
|
@ -250,6 +250,9 @@ def deprecated(
|
|||
|
||||
if type(func).__name__ != "function":
|
||||
deprecate_docs(func)
|
||||
# The following line raises this mypy error:
|
||||
# Accessing "__init__" on an instance is unsound, since instance.__init__
|
||||
# could be from an incompatible subclass [misc]</pre>
|
||||
func.__init__ = decorate(func.__init__, deprecate)
|
||||
return func
|
||||
|
||||
|
|
|
|||
6
mypy.ini
6
mypy.ini
|
|
@ -85,9 +85,6 @@ ignore_errors = True
|
|||
[mypy-manim.mobject.logo]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.mobject]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.opengl.opengl_point_cloud_mobject]
|
||||
ignore_errors = True
|
||||
|
||||
|
|
@ -100,6 +97,9 @@ ignore_errors = True
|
|||
[mypy-manim.mobject.table]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.types.point_cloud_mobject]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.types.vectorized_mobject]
|
||||
ignore_errors = True
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "manim"
|
||||
version = "0.20.0"
|
||||
version = "0.20.1"
|
||||
description = "Animation engine for explanatory math videos."
|
||||
authors = [
|
||||
{name = "The Manim Community Developers", email = "contact@manim.community"},
|
||||
|
|
@ -62,7 +62,7 @@ documentation = "https://docs.manim.community/"
|
|||
homepage = "https://www.manim.community/"
|
||||
"Bug Tracker" = "https://github.com/ManimCommunity/manim/issues"
|
||||
"Changelog" = "https://docs.manim.community/en/stable/changelog.html"
|
||||
"X / Twitter" = "https://x.com/manim_community"
|
||||
"X / Twitter" = "https://x.com/manimcommunity"
|
||||
"Bluesky" = "https://bsky.app/profile/manim.community"
|
||||
"Discord" = "https://www.manim.community/discord/"
|
||||
|
||||
|
|
|
|||
|
|
@ -5,14 +5,16 @@ from unittest.mock import MagicMock
|
|||
import pytest
|
||||
|
||||
from manim.animation.animation import Animation, Wait
|
||||
from manim.animation.composition import AnimationGroup, Succession
|
||||
from manim.animation.composition import AnimationGroup, LaggedStartMap, Succession
|
||||
from manim.animation.creation import Create, Write
|
||||
from manim.animation.fading import FadeIn, FadeOut
|
||||
from manim.constants import DOWN, UP
|
||||
from manim.mobject.geometry.arc import Circle
|
||||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.geometry.polygram import RegularPolygon, Square
|
||||
from manim.mobject.types.vectorized_mobject import VGroup
|
||||
from manim.scene.scene import Scene
|
||||
from manim.utils.rate_functions import linear, there_and_back
|
||||
|
||||
|
||||
def test_succession_timing():
|
||||
|
|
@ -189,6 +191,23 @@ def test_animationgroup_calls_finish():
|
|||
assert circ_animation.finished
|
||||
|
||||
|
||||
def test_laggedstartmap_only_passes_kwargs_to_subanimations():
|
||||
mobject = VGroup(Square(), Circle())
|
||||
animation = LaggedStartMap(
|
||||
FadeIn,
|
||||
mobject,
|
||||
rate_func=there_and_back,
|
||||
lag_ratio=0.3,
|
||||
)
|
||||
|
||||
assert animation.rate_func is linear
|
||||
assert animation.lag_ratio == 0.3
|
||||
assert all(
|
||||
subanimation.rate_func is there_and_back
|
||||
for subanimation in animation.animations
|
||||
)
|
||||
|
||||
|
||||
def test_empty_animation_group_fails():
|
||||
with pytest.raises(ValueError, match="Please add at least one subanimation."):
|
||||
AnimationGroup().begin()
|
||||
|
|
|
|||
|
|
@ -20,13 +20,122 @@ def test_SingleStringMathTex(config):
|
|||
|
||||
@pytest.mark.parametrize( # : PT006
|
||||
("text_input", "length_sub"),
|
||||
[("{{ a }} + {{ b }} = {{ c }}", 5), (r"\frac{1}{a+b\sqrt{2}}", 1)],
|
||||
[
|
||||
("{{ a }} + {{ b }} = {{ c }}", 5),
|
||||
(r"\frac{1}{a+b\sqrt{2}}", 1),
|
||||
# Regression test for https://github.com/ManimCommunity/manim/issues/4601:
|
||||
# a string whose only }} comes from closing two nested LaTeX brace groups
|
||||
# (not from the {{ }} notation) must not be split.
|
||||
(r"\\+\int_{0}^{\frac{Mq}{M+m}}", 1),
|
||||
],
|
||||
)
|
||||
def test_double_braces_testing(text_input, length_sub):
|
||||
t1 = MathTex(text_input)
|
||||
assert len(t1.submobjects) == length_sub
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Unit tests for MathTex._split_double_braces — no LaTeX compilation needed.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("tex_string", "expected_segments"),
|
||||
[
|
||||
# ---- intended notation ----
|
||||
# Basic split: each {{ }} group and the text between become segments.
|
||||
(
|
||||
"{{ a }} + {{ b }}",
|
||||
["", " a ", " + ", " b ", ""],
|
||||
),
|
||||
# {{ }} at the very start of the string (no preceding character).
|
||||
(
|
||||
"{{x}}",
|
||||
["", "x", ""],
|
||||
),
|
||||
# Content with arbitrarily nested LaTeX braces: inner }} must NOT close
|
||||
# the Manim group early.
|
||||
(
|
||||
r"{{ a^{b^{c}} }}",
|
||||
["", r" a^{b^{c}} ", ""],
|
||||
),
|
||||
# \frac inside a Manim group — the }} from {1}{a} are at inner_depth > 0.
|
||||
(
|
||||
r"{{ \frac{1}{a} }}",
|
||||
["", r" \frac{1}{a} ", ""],
|
||||
),
|
||||
# ---- false-positive guards: {{ not preceded by whitespace ----
|
||||
# \text{{word}}: {{ preceded by {, must not split.
|
||||
(
|
||||
r"\text{{word}}",
|
||||
[r"\text{{word}}"],
|
||||
),
|
||||
# ^{{\alpha}}: {{ preceded by {, must not split.
|
||||
(
|
||||
r"^{{\alpha}}",
|
||||
[r"^{{\alpha}}"],
|
||||
),
|
||||
# +{{a}}: {{ preceded by non-whitespace, must not split.
|
||||
(
|
||||
r"+{{a}}",
|
||||
[r"+{{a}}"],
|
||||
),
|
||||
# ---- bug case: }} without any {{ must not split ----
|
||||
(
|
||||
r"\\+\int_{0}^{\frac{Mq}{M+m}}",
|
||||
[r"\\+\int_{0}^{\frac{Mq}{M+m}}"],
|
||||
),
|
||||
# ---- backslash escape handling ----
|
||||
# \}} — \} consumed as unit, remaining } is a lone close, not }}.
|
||||
(
|
||||
r"\}}",
|
||||
[r"\}}"],
|
||||
),
|
||||
# \\}} — \\ consumed as unit, leaving real }} which is not inside any
|
||||
# Manim group so it passes through unchanged.
|
||||
(
|
||||
r"\\}}",
|
||||
[r"\\}}"],
|
||||
),
|
||||
# \\\}} — \\ consumed, then \} consumed; lone } passes through.
|
||||
(
|
||||
r"\\\}}",
|
||||
[r"\\\}}"],
|
||||
),
|
||||
# \\\\}} — two \\ consumed; lone }} passes through (no Manim group open).
|
||||
(
|
||||
r"\\\\}}",
|
||||
[r"\\\\}}"],
|
||||
),
|
||||
# Same backslash cases *inside* a Manim group.
|
||||
# The escape sequence is placed right before the Manim }} close.
|
||||
#
|
||||
# {{ a \}}} — \} consumed as escaped brace (content), }} closes the group.
|
||||
(
|
||||
r"{{ a \}}}",
|
||||
["", r" a \}", ""],
|
||||
),
|
||||
# {{ a \\}} — \\ consumed as escaped backslash (content), }} closes.
|
||||
(
|
||||
r"{{ a \\}}",
|
||||
["", r" a \\", ""],
|
||||
),
|
||||
# {{ a \\\}}} — \\ then \} consumed (content), }} closes.
|
||||
(
|
||||
r"{{ a \\\}}}",
|
||||
["", r" a \\\}", ""],
|
||||
),
|
||||
# {{ a \\\\}} — \\ then \\ consumed (content), }} closes.
|
||||
(
|
||||
r"{{ a \\\\}}",
|
||||
["", r" a \\\\", ""],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_split_double_braces(tex_string, expected_segments):
|
||||
assert MathTex._split_double_braces(tex_string) == expected_segments
|
||||
|
||||
|
||||
def test_tex(config):
|
||||
Tex("The horse does not eat cucumber salad.")
|
||||
assert Path(config.media_dir, "Tex", "5384b41741a246bd.svg").exists()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from manim import ORIGIN, UR, Arrow, DashedVMobject, VGroup
|
||||
from manim import ORIGIN, UR, Arrow, DashedLine, DashedVMobject, VGroup
|
||||
from manim.mobject.geometry.tips import ArrowTip, StealthTip
|
||||
|
||||
|
||||
|
|
@ -41,3 +41,19 @@ def test_dashed_arrow_with_start_tip_has_two_tips():
|
|||
tips = _collect_tips(dashed)
|
||||
|
||||
assert len(tips) == 2
|
||||
|
||||
|
||||
def test_zero_length_dashed_line_submobjects_have_2d_points():
|
||||
"""Submobjects of a zero-length DashedLine must have 2-D point arrays."""
|
||||
line = DashedLine(ORIGIN, ORIGIN)
|
||||
for sub in line.submobjects:
|
||||
assert sub.points.ndim == 2, (
|
||||
f"Expected 2-D points array, got shape {sub.points.shape}"
|
||||
)
|
||||
|
||||
|
||||
def test_become_nonzero_to_zero_dashed_line_does_not_crash():
|
||||
"""become() from a normal DashedLine to a zero-length one should not crash."""
|
||||
normal = DashedLine(ORIGIN, 2 * UR)
|
||||
zero = DashedLine(ORIGIN, ORIGIN)
|
||||
normal.become(zero)
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -174,6 +174,27 @@ def test_ZIndex(scene):
|
|||
scene.play(ApplyMethod(triangle.shift, 2 * UP))
|
||||
|
||||
|
||||
@frames_comparison(last_frame=False)
|
||||
def test_negative_z_index_AnimationGroup(scene):
|
||||
# https://github.com/ManimCommunity/manim/issues/3334
|
||||
s = Square().set_z_index(-1)
|
||||
scene.play(AnimationGroup(GrowFromCenter(s)))
|
||||
|
||||
|
||||
@frames_comparison(last_frame=False)
|
||||
def test_negative_z_index_LaggedStart(scene):
|
||||
# https://github.com/ManimCommunity/manim/issues/3914
|
||||
line_1 = Line(LEFT, RIGHT, color=BLUE)
|
||||
line_2 = Line(UP + LEFT, UP + RIGHT, color=RED).set_z_index(-1)
|
||||
scene.play(LaggedStart(FadeIn(line_1), FadeIn(line_2), lag_ratio=0.5))
|
||||
|
||||
|
||||
@frames_comparison(last_frame=False)
|
||||
def test_nested_animation_groups_with_negative_z_index(scene):
|
||||
line = Line(LEFT, RIGHT, color=BLUE).set_z_index(-1)
|
||||
scene.play(AnimationGroup(AnimationGroup(AnimationGroup(FadeIn(line)))))
|
||||
|
||||
|
||||
@frames_comparison
|
||||
def test_Angle(scene):
|
||||
l1 = Line(ORIGIN, RIGHT)
|
||||
|
|
|
|||
2
uv.lock
generated
2
uv.lock
generated
|
|
@ -1396,7 +1396,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "manim"
|
||||
version = "0.20.0"
|
||||
version = "0.20.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "audioop-lts", marker = "python_full_version >= '3.13'" },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue