Merge branch 'main' into fix-getarea-arguments

This commit is contained in:
Francisco Manríquez Novoa 2026-06-09 17:50:39 -04:00 committed by GitHub
commit c0170b14fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
202 changed files with 11000 additions and 6626 deletions

View file

@ -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/

View file

@ -1,10 +1,6 @@
<!-- Thank you for contributing to Manim! Learn more about the process in our contributing guidelines: https://docs.manim.community/en/latest/contributing.html -->
## Overview: What does this pull request change?
<!-- If there is more information than the PR title that should be added to our release changelog, add it in the following changelog section. This is optional, but recommended for larger pull requests. -->
<!--changelog-start-->
<!--changelog-end-->
## Motivation and Explanation: Why and how do your changes improve the library?
<!-- Optional for bugfixes, small enhancements, and documentation-related PRs. Otherwise, please give a short reasoning for your changes. -->

6
.github/codeql.yml vendored
View file

@ -10,7 +10,11 @@ query-filters:
- exclude:
id: py/missing-call-to-init
- exclude:
id: py/method-first-arg-is-not-self
id: py/method-first-arg-is-not-self
- exclude:
id: py/cyclic-import
- exclude:
id: py/unsafe-cyclic-import
paths:
- manim
paths-ignore:

67
.github/release.yml vendored Normal file
View file

@ -0,0 +1,67 @@
# https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes
changelog:
exclude:
labels:
- duplicate/wontfix
- invalid
- question
- release
authors:
- dependabot[bot]
- pre-commit-ci[bot]
categories:
# High Impact
- title: "Breaking Changes 🚨"
labels:
- breaking changes
# Highlights
- title: "Highlights 🌟"
labels:
- highlight
# User-facing
- title: "New Features ✨"
labels:
- new feature
- title: "Enhancements 🚀"
labels:
- enhancement
- title: "Bug Fixes 🐛"
labels:
- pr:bugfix
- title: "Deprecations & Removals ⚠️"
labels:
- pr:deprecation
# Developer-facing
- title: "Documentation 📚"
labels:
- documentation
- title: "Testing 🧪"
labels:
- testing
- title: "Infrastructure & Build 🔨"
labels:
- infrastructure
- title: "Code Quality & Refactoring 🧹"
labels:
- maintenance
- refactor
- title: "Type Hints 📝"
labels:
- typehints
# Catch-all (must be last)
- title: "Other Changes"
labels:
- "*"

View file

@ -14,8 +14,8 @@ import subprocess
import sys
import tarfile
import tempfile
import typing
import urllib.request
from collections.abc import Generator
from contextlib import contextmanager
from pathlib import Path
from sys import stdout
@ -67,7 +67,7 @@ def run_command(command, cwd=None, env=None):
@contextmanager
def gha_group(title: str) -> typing.Generator:
def gha_group(title: str) -> Generator:
if not is_ci():
yield
return
@ -144,10 +144,38 @@ def main():
]
)
env_vars = {
# add the venv bin directory to PATH so that meson can find ninja
"PATH": f"{os.path.join(tmpdir, VENV_NAME, 'bin')}{os.pathsep}{os.environ['PATH']}",
}
# Inherit the current environment so PKG_CONFIG_PATH, CFLAGS, LDFLAGS, etc. are preserved.
env_vars = os.environ.copy()
# Prepend the venv bin directory so meson/ninja from the venv are used.
env_vars["PATH"] = f"{os.path.join(tmpdir, VENV_NAME, 'bin')}{os.pathsep}{env_vars.get('PATH','')}"
# Ensure Homebrew-provided pkgconfig and include/lib paths are present on macOS ARM.
if sys.platform == "darwin":
try:
# Try to get specific prefix for lzo (safer for opt path), fall back to generic brew prefix.
brew_prefix = subprocess.check_output(["brew", "--prefix", "lzo"], text=True).strip()
except subprocess.CalledProcessError:
try:
brew_prefix = subprocess.check_output(["brew", "--prefix"], text=True).strip()
except Exception:
brew_prefix = None
if brew_prefix:
# pkg-config files can live in lib/pkgconfig or opt/<pkg>/lib/pkgconfig
pkgconfig_paths = [f"{brew_prefix}/lib/pkgconfig", f"{brew_prefix}/opt/lzo/lib/pkgconfig"]
# merge with any existing PKG_CONFIG_PATH
existing_pc = env_vars.get("PKG_CONFIG_PATH", "")
merged_pc = ":".join([p for p in pkgconfig_paths if p]) + (f":{existing_pc}" if existing_pc else "")
env_vars["PKG_CONFIG_PATH"] = merged_pc
# Ensure compiler & linker flags include brew include/lib
existing_cflags = env_vars.get("CFLAGS", "")
existing_ldflags = env_vars.get("LDFLAGS", "")
env_vars["CFLAGS"] = f"-I{brew_prefix}/include {existing_cflags}".strip()
env_vars["LDFLAGS"] = f"-L{brew_prefix}/lib {existing_ldflags}".strip()
# Debugging: log environment keys relevant to detection
# logger.info(f"env vars for meson: {env_vars}")
with gha_group("Building and Installing Cairo"):
logger.info("Running meson setup")

View file

@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out a copy of the repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Check whether the citation metadata from CITATION.cff is valid
uses: citation-file-format/cffconvert-github-action@2.0.0

View file

@ -22,20 +22,23 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, macos-13, windows-latest]
python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
os: [ubuntu-22.04, macos-latest, windows-latest]
python: ["3.11", "3.12", "3.13", "3.14"]
include:
- os: macos-15-intel
python: "3.13"
steps:
- name: Checkout the repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Python ${{ matrix.python }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python }}
- name: Install uv
uses: astral-sh/setup-uv@v5
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
@ -54,10 +57,13 @@ jobs:
- name: Install Texlive (Linux)
if: runner.os == 'Linux'
uses: teatimeguest/setup-texlive-action@v3
uses: zauguin/install-texlive@v4
with:
cache: true
packages: scheme-basic fontspec inputenc fontenc tipa mathrsfs calligra xcolor standalone preview doublestroke ms everysel setspace rsfs relsize ragged2e fundus-calligra microtype wasysym physics dvisvgm jknapltx wasy cm-super babel-english gnu-freefont mathastext cbfonts-fd xetex
packages: >
scheme-basic latex fontspec tipa calligra xcolor
standalone preview doublestroke setspace rsfs relsize
ragged2e fundus-calligra microtype wasysym physics dvisvgm jknapltx
wasy cm-super babel-english gnu-freefont mathastext cbfonts-fd xetex
- name: Start virtual display (Linux)
if: runner.os == 'Linux'
@ -66,12 +72,12 @@ jobs:
sudo /usr/bin/Xvfb $DISPLAY -screen 0 1280x1024x24 &
- name: Setup Cairo Cache
uses: actions/cache@v4
uses: actions/cache@v5
id: cache-cairo
if: runner.os == 'Linux' || runner.os == 'macOS'
with:
path: ${{ github.workspace }}/third_party
key: ${{ runner.os }}-dependencies-cairo-${{ hashFiles('.github/scripts/ci_build_cairo.py') }}
key: ${{ runner.os }}-${{ runner.arch }}-dependencies-cairo-${{ hashFiles('.github/scripts/ci_build_cairo.py') }}
- name: Build and install Cairo (Linux and macOS)
if: (runner.os == 'Linux' || runner.os == 'macOS') && steps.cache-cairo.outputs.cache-hit != 'true'
@ -82,7 +88,7 @@ jobs:
run: python .github/scripts/ci_build_cairo.py --set-env-vars
- name: Setup macOS cache
uses: actions/cache@v4
uses: actions/cache@v5
id: cache-macos
if: runner.os == 'macOS'
with:
@ -98,8 +104,8 @@ jobs:
oriPath=$PATH
sudo mkdir -p $PWD/macos-cache
echo "Install TinyTeX"
sudo curl -L -o "/tmp/TinyTeX.tgz" "https://github.com/yihui/tinytex-releases/releases/download/daily/TinyTeX-1.tgz"
sudo tar zxf "/tmp/TinyTeX.tgz" -C "$PWD/macos-cache"
sudo curl -L -o "/tmp/TinyTeX.tar.xz" "https://github.com/rstudio/tinytex-releases/releases/download/daily/TinyTeX-1-darwin.tar.xz"
sudo tar xJf "/tmp/TinyTeX.tar.xz" -C "$PWD/macos-cache"
export PATH="$PWD/macos-cache/TinyTeX/bin/universal-darwin:$PATH"
sudo tlmgr update --self
for i in "${ttp[@]}"; do
@ -118,12 +124,12 @@ jobs:
- name: Setup Windows cache
id: cache-windows
if: runner.os == 'Windows'
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ${{ github.workspace }}\ManimCache
key: ${{ runner.os }}-dependencies-tinytex-${{ hashFiles('.github/manimdependency.json') }}-${{ steps.cache-vars.outputs.date }}-1
- uses: ssciwr/setup-mesa-dist-win@v2
- uses: ssciwr/setup-mesa-dist-win@v3
- name: Install system dependencies (Windows)
if: runner.os == 'Windows' && steps.cache-windows.outputs.cache-hit != 'true'
@ -131,8 +137,8 @@ jobs:
$tinyTexPackages = $(python -c "import json;print(' '.join(json.load(open('.github/manimdependency.json'))['windows']['tinytex']))") -Split ' '
$OriPath = $env:PATH
echo "Install Tinytex"
Invoke-WebRequest "https://github.com/yihui/tinytex-releases/releases/download/daily/TinyTeX-1.zip" -OutFile "$($env:TMP)\TinyTex.zip"
Expand-Archive -LiteralPath "$($env:TMP)\TinyTex.zip" -DestinationPath "$($PWD)\ManimCache\LatexWindows"
Invoke-WebRequest "https://github.com/rstudio/tinytex-releases/releases/download/daily/TinyTeX-1-windows.exe" -OutFile "$($env:TMP)\TinyTex.exe"
.$env:TMP\TinyTex.exe -o"$($PWD)\ManimCache\LatexWindows"
$env:Path = "$($PWD)\ManimCache\LatexWindows\TinyTeX\bin\windows;$($env:PATH)"
tlmgr update --self
tlmgr install $tinyTexPackages

View file

@ -24,19 +24,19 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
config-file: ./.github/codeql.yml
queries: +security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{ matrix.language }}"

View file

@ -13,19 +13,19 @@ jobs:
if: github.event_name != 'release'
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
platforms: linux/arm64,linux/amd64
push: true
@ -38,13 +38,13 @@ jobs:
if: github.event_name == 'release'
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@ -61,7 +61,7 @@ jobs:
print(f"tag_name={ref_tag}", file=f)
- name: Build and push
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
platforms: linux/arm64,linux/amd64
push: true

View file

@ -11,60 +11,37 @@ jobs:
environment: release
permissions:
id-token: write
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install dependencies
run: sudo apt-get update && sudo apt-get install -y build-essential python3-dev libcairo2-dev libpango1.0-dev
- name: Set up Python 3.13
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: 3.13
- name: Install uv
uses: astral-sh/setup-uv@v5
uses: astral-sh/setup-uv@v7
- name: Build and push release to PyPI
run: |
uv sync
uv build
uv publish
- name: Store artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
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"

View file

@ -9,15 +9,15 @@ jobs:
build-and-publish-htmldocs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: 3.13
- name: Install uv
uses: astral-sh/setup-uv@v5
uses: astral-sh/setup-uv@v7
- name: Install system dependencies
run: |
@ -43,7 +43,7 @@ jobs:
tar -czvf ../html-docs.tar.gz *
- name: Store artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
path: ${{ github.workspace }}/docs/build/html-docs.tar.gz
name: html-docs.tar.gz

View file

@ -3,7 +3,7 @@ fail_fast: false
exclude: ^(manim/grpc/gen/|docs/i18n/)
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: check-ast
name: Validate Python
@ -12,8 +12,16 @@ repos:
- id: end-of-file-fixer
- id: check-toml
name: Validate pyproject.toml
- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
hooks:
- id: codespell
files: ^.*\.(py|md|rst)$
args: ["-L", "medias,nam"]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.0
rev: v0.14.10
hooks:
- id: ruff
name: ruff lint
@ -21,8 +29,9 @@ repos:
args: [--exit-non-zero-on-fix]
- id: ruff-format
types: [python]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.15.0
rev: v1.19.1
hooks:
- id: mypy
additional_dependencies:
@ -34,10 +43,3 @@ repos:
types-setuptools,
]
files: ^manim/
- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
hooks:
- id: codespell
files: ^.*\.(py|md|rst)$
args: ["-L", "medias,nam"]

View file

@ -4,10 +4,10 @@ authors:
-
name: "The Manim Community Developers"
cff-version: "1.2.0"
date-released: 2025-01-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.19.0"
version: "v0.20.1"
...

View file

@ -1,17 +1,15 @@
<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/manim_community/"><img src="https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=Follow%20%40manim_community" alt="Twitter">
<a href="https://www.manim.community/discord/"><img src="https://img.shields.io/discord/581738731934056449.svg?label=discord&color=yellow&logo=discord" alt="Discord"></a>
<a href="https://github.com/psf/black"><img src="https://img.shields.io/badge/code%20style-black-000000.svg" alt="Code style: black">
<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">
<a href="https://manim.community/discord/"><img src="https://img.shields.io/discord/581738731934056449.svg?label=discord&color=yellow&logo=discord" alt="Discord"></a>
<a href="https://docs.manim.community/"><img src="https://readthedocs.org/projects/manimce/badge/?version=latest" alt="Documentation Status"></a>
<a href="https://pepy.tech/project/manim"><img src="https://pepy.tech/badge/manim/month?" alt="Downloads"> </a>
<img src="https://github.com/ManimCommunity/manim/workflows/CI/badge.svg" alt="CI">
<br />
<br />

View file

@ -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"]

View file

@ -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.

View file

@ -2,14 +2,18 @@
Changelog
#########
This page contains a list of changes made between releases. Changes
from versions that are not listed below (in particular patch-level
releases since v0.18.0) are documented on our
`GitHub release page <https://github.com/ManimCommunity/manim/releases/>`__.
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
changelog/0.19.0-changelog
changelog/0.18.1-changelog
changelog/0.18.0.post0-changelog
changelog/0.18.0-changelog
changelog/0.17.3-changelog
changelog/0.17.2-changelog

View file

@ -0,0 +1,9 @@
*************
v0.18.0.post0
*************
:Date: April 08, 2024
This release is a post-release fixing `#3676
<https://github.com/ManimCommunity/manim/issues/3676>`_, a bug caused by a recent
change introduced to the way how SVG files of text are generated by Pango.

View file

@ -0,0 +1,160 @@
---
short-title: v0.18.1
description: Changelog for Manim v0.18.1
---
# v0.18.1
Date
: April 28, 2024
## What's Changed
### Breaking Changes and Deprecations
* Removed deprecated `manim new` command by {user}`chopan050` in {pr}`3512`
* Removed support for dynamic plugin imports by {user}`Viicos` in {pr}`3524`
* Remove meth:``.Mobject.wag`` by {user}`JasonGrace2282` in {pr}`3539`
* Remove deprecated parameters and animations by {user}`JasonGrace2282` in {pr}`3688`
### New Features
* Added `cap_style` feature to `VMobject` by {user}`MathItYT` in {pr}`3516`
* Allow hiding version splash by {user}`jeertmans` in {pr}`3329`
* Added the ability to pass lists and generators to `Scene.play()` by {user}`MrDiver` in {pr}`3365`
* Added ``--preview_command`` cli flag by {user}`JasonGrace2282` in {pr}`3615`
### Fixed Bugs and Enhancements
* Allow accessing ghost vectors in :class:`.LinearTransformationScene` by {user}`JasonGrace2282` in {pr}`3435`
* Optimized `get_unit_normal()` and replaced `np.cross()` with custom `cross()` in `manim.utils.space_ops` by {user}`chopan050` in {pr}`3494`
* Implement caching of fonts list to improve runtime performance by {user}`MrDiver` in {pr}`3316`
* Reformatting the `--save_sections` output to have the format `<Scene>_<SecNum>_<SecName><extension>` by {user}`doaamuham` in {pr}`3499`
* Account for dtype in the pixel array so the maximum value stays correct in the invert function by {user}`jeertmans` in {pr}`3493`
* Added `grid_lines` attribute to `Rectangle` to add individual styling to the grid lines by {user}`RobinPH` in {pr}`3428`
* Fixed rectangle grid properties (#3082) by {user}`pauluhlenbruck` in {pr}`3513`
* Fixed animations with zero runtime length to give a useful error instead of a broken pipe by {user}`MrDiver` in {pr}`3491`
* Fixed stroke width being ignored by `StreamLines` with a single color by {user}`yashm277` in {pr}`3436`
* Fixed formatting of ``MoveAlongPath`` docs by {user}`JasonGrace2282` in {pr}`3541`
* Added helpful hints to `VGroup.add()` error message by {user}`vvolhejn` in {pr}`3561`
* Made `earclip_triangulation` more robust by {user}`hydromelvictor` in {pr}`3574`
* Refactored `TexTemplate` by {user}`Viicos` in {pr}`3520`
* Fixed `write_subcaption_file` error when using OpenGL renderer by {user}`yuan-xy` in {pr}`3546`
* Fixed `get_arc_center()` returning reference of point by {user}`sparshg` in {pr}`3599`
* Improved handling of specified font name by {user}`staghado` in {pr}`3429`
* Fixing the behavior of `.become` to not modify target mobject via side effects fix color linking by {user}`MrDiver` in {pr}`3508`
* Fixed bug in :class:`.VMobjectFromSVGPath` by {user}`abul4fia` in {pr}`3677`
* Fix for windows cp1252 encoding failure (fix test pipeline) by {user}`JasonGrace2282` in {pr}`3687`
* Fix NameError in try... except by {user}`JasonGrace2282` in {pr}`3694`
* Fix successive calls of :meth:`.LinearTransformationScene.apply_matrix` by {user}`SirJamesClarkMaxwell` in {pr}`3675`
* Fixed `Mobject.put_start_and_end_on` with same start and end point by {user}`MontroyJosh` in {pr}`3718`
* Fixed issue where `SpiralIn` doesn't show elements by {user}`Gixtox` in {pr}`3589`
* Cleaned `Graph` layouts and increase flexibility by {user}`Nikhil-42` in {pr}`3434`
* `AnimationGroup`: optimized `interpolate()` and fixed alpha bug on `finish()` by {user}`chopan050` in {pr}`3542`
* Fixed warning about missing plugin `""` by {user}`behackl` in {pr}`3734`
### Documentation
* Typo in `indication` documentation by {user}`jcep` in {pr}`3477`
* Fixed typo: 360° to 180° in quickstart tutorial by {user}`szchixy` in {pr}`3498`
* Fixed typo in mobject docstring: `line` -> `square` by {user}`yuan-xy` in {pr}`3509`
* Explain ``.Transform`` vs ``.ReplacementTransform`` in quickstart examples by {user}`JasonGrace2282` in {pr}`3500`
* Fixed formatting in building blocks tutorial by {user}`MrDiver` in {pr}`3515`
* Fixed `Indicate` docstring typo by {user}`Lawqup` in {pr}`3461`
* Added Documentation to `.to_edge` and `to_corner` by {user}`TheMathematicFanatic` in {pr}`3408`
* Added some words about Cairo 1.18 by {user}`jeertmans` in {pr}`3530`
* Fixed typo of `get_y_axis_label` parameter documentation by {user}`yuan-xy` in {pr}`3547`
* Added note in docstring of `ManimColor` about class constructors by {user}`JasonGrace2282` in {pr}`3554`
* Improve documentation section about contributing to docs by {user}`chopan050` in {pr}`3555`
* Removed duplicated documentation for -s / --save_last_frame CLI flag by {user}`Gixtox` in {pr}`3528`
* Updated Docker instructions to use bash from the PATH by {user}`NotWearingPants` in {pr}`3582`
* Fixed typo in `value_tracker.py` by {user}`yuan-xy` in {pr}`3594`
* Added `ref_class` for `BooleanOperations` in Example Gallery by {user}`JasonGrace2282` in {pr}`3598`
* Changed `Vector3` to `Vector3D` in contributing docs by {user}`JasonGrace2282` in {pr}`3639`
* Added some examples for `Mobject`/`VMobject` methods by {user}`JasonGrace2282` in {pr}`3641`
* Fixed broken link to Poetry's installation guide in the documentation by {user}`biinnnggggg` in {pr}`3692`
* Fixed minor grammatical errors found in the index page of the documentation by {user}`biinnnggggg` in {pr}`3690`
* Fixed typo on page about translations by {user}`biinnnggggg` in {pr}`3696`
* Fixed outdated description of CLI option in Manim's Output Settings by {user}`HairlessVillager` in {pr}`3674`
* Mention pixi in installation guide by {user}`pavelzw` in {pr}`3678`
* Updated typing guidelines by {user}`JasonGrace2282` in {pr}`3704`
* Updated documentation and typings for `ParametricFunction` by {user}`danielzsh` in {pr}`3703`
* Fixed docstring markup in `Rotate` by {user}`TheCrowned` in {pr}`3721`
* Improve consistency in axis label example by {user}`amrear` in {pr}`3730`
### Maintenance and Testing
* Fixed wrong path in action building downloadable docs by {user}`behackl` in {pr}`3450`
* Add type hints to `_config` by {user}`Viicos` in {pr}`3440`
* Update dependency constraints, fix deprecation warnings by {user}`Viicos` in {pr}`3376`
* Update Docker base image to python3.12-slim (#3458) by {user}`PikaBlue107` in {pr}`3459`
* Fixed `line_join` to `joint_type` in example_scenes/basic.py by {user}`szchixy` in {pr}`3510`
* Fixed :attr:`.Mobject.animate` type-hint to allow LSP autocomplete by {user}`JasonGrace2282` in {pr}`3543`
* Finish TODO's in ``contributing/typings.rst`` by {user}`JasonGrace2282` in {pr}`3545`
* Fixed use of `Mobject`'s deprecated `get_*()` and `set_*()` methods in Cairo tests by {user}`JasonGrace2282` in {pr}`3549`
* Added support for Manim type aliases in Sphinx docs and added new TypeAliases by {user}`chopan050` in {pr}`3484`
* Fixed typing of `Animation` by {user}`dandavison` in {pr}`3568`
* Added some TODOs for future use of `ManimFrame` by {user}`chopan050` in {pr}`3553`
* Fixed typehint of :attr:`InternalPoint2D_Array` by {user}`JasonGrace2282` in {pr}`3592`
* Fixed error in Windows CI pipeline by {user}`behackl` in {pr}`3611`
* Fixed type hint of indication.py by {user}`yuan-xy` in {pr}`3613`
* Revert vector type aliases to NumPy ndarrays by {user}`chopan050` in {pr}`3595`
* Run `poetry lock --no-update` by {user}`JasonGrace2282` in {pr}`3621`
* Code Cleanup: removing unused imports and global variables by {user}`JasonGrace2282` in {pr}`3620`
* Fixed type hint of `Vector` direction parameter by {user}`JasonGrace2282` in {pr}`3640`
* Flake8 rule C901 is about McCabe code complexity by {user}`cclauss` in {pr}`3673`
* Updated year in license by {user}`JasonGrace2282` in {pr}`3689`
* Automated copyright updating for docs by {user}`JasonGrace2282` in {pr}`3708`
* Fixed some typehints in `mobject.py` by {user}`JasonGrace2282` in {pr}`3668`
* Search for type aliases if TYPE_CHECKING by {user}`JasonGrace2282` in {pr}`3671`
* Follow-up to graph layout cleanup: improvements for tests and typing by {user}`behackl` in {pr}`3728`
* GH Actions: Changed from macos-latest to macos-13 by {user}`JasonGrace2282` in {pr}`3729`
* Fixed return type inconsistency for `get_anchors()` by {user}`JinchuLi2002` in {pr}`3214`
* Prepared new release: `v0.18.1` by {user}`behackl` in {pr}`3719`
#### Dependency Version Changes
* Bump jupyter-server from 2.9.1 to 2.11.2 by {user}`dependabot` in {pr}`3497`
* Bump github/codeql-action from 2 to 3 by {user}`dependabot` in {pr}`3567`
* Bump actions/upload-artifact from 3 to 4 by {user}`dependabot` in {pr}`3566`
* Bump actions/setup-python from 4 to 5 by {user}`dependabot` in {pr}`3565`
* updated several packages (pillow, jupyterlab, notebook, jupyterlab-lsp, jinja2, gitpython) by {user}`behackl` in {pr}`3593`
* Update jupyter.rst by {user}`abul4fia` in {pr}`3630`
* Bump black from 23.12.1 to 24.3.0 by {user}`dependabot` in {pr}`3649`
* Bump cryptography from 42.0.0 to 42.0.4 by {user}`dependabot` in {pr}`3629`
* Bump actions/cache from 3 to 4 by {user}`dependabot` in {pr}`3607`
* Bump FedericoCarboni/setup-ffmpeg from 2 to 3 by {user}`dependabot` in {pr}`3608`
* Bump ssciwr/setup-mesa-dist-win from 1 to 2 by {user}`dependabot` in {pr}`3609`
* Bump idna from 3.6 to 3.7 by {user}`dependabot` in {pr}`3693`
* Bump pillow from 10.2.0 to 10.3.0 by {user}`dependabot` in {pr}`3672`
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci` in {pr}`3332`
* Updated sphinx deps by {user}`JasonGrace2282` in {pr}`3720`
## New Contributors
* {user}`Lawqup` made their first contribution in {pr}`3461`
* {user}`jcep` made their first contribution in {pr}`3477`
* {user}`szchixy` made their first contribution in {pr}`3498`
* {user}`PikaBlue107` made their first contribution in {pr}`3459`
* {user}`yuan-xy` made their first contribution in {pr}`3509`
* {user}`MathItYT` made their first contribution in {pr}`3516`
* {user}`doaamuham` made their first contribution in {pr}`3499`
* {user}`RobinPH` made their first contribution in {pr}`3428`
* {user}`pauluhlenbruck` made their first contribution in {pr}`3513`
* {user}`yashm277` made their first contribution in {pr}`3436`
* {user}`TheMathematicFanatic` made their first contribution in {pr}`3408`
* {user}`vvolhejn` made their first contribution in {pr}`3561`
* {user}`hydromelvictor` made their first contribution in {pr}`3574`
* {user}`dandavison` made their first contribution in {pr}`3568`
* {user}`Gixtox` made their first contribution in {pr}`3528`
* {user}`staghado` made their first contribution in {pr}`3429`
* {user}`biinnnggggg` made their first contribution in {pr}`3692`
* {user}`HairlessVillager` made their first contribution in {pr}`3674`
* {user}`SirJamesClarkMaxwell` made their first contribution in {pr}`3675`
* {user}`danielzsh` made their first contribution in {pr}`3703`
* {user}`TheCrowned` made their first contribution in {pr}`3721`
* {user}`MontroyJosh` made their first contribution in {pr}`3718`
* {user}`amrear` made their first contribution in {pr}`3730`
**Full Changelog**: https://github.com/ManimCommunity/manim/compare/v0.18.0.post0...v0.18.1

View file

@ -0,0 +1,197 @@
---
short-title: v0.19.1
description: Changelog for Manim v0.19.1
---
# v0.19.1
Date
: December 01, 2025
## What's Changed
### New Features
* Introduce seed in `random_color` method to produce colors deterministically by {user}`ishu9bansal` in {pr}`4265`
* Add support for arithmetic operators `//`, `%`, `*`, `**` and `/` on `ValueTracker` by {user}`fmuenkel` in {pr}`4351`
* Add `TangentialArc` mobject by {user}`Brainsucker92` in {pr}`4469`
### Fixed Bugs and Enhancements
* Fix environment formatting for Tex() mobject by {user}`fmuenkel` in {pr}`4159`
* Improved consistency of rate_function implementations by {user}`BenKirkels` in {pr}`4144`
* Make new `Code` mobject compatible with OpenGL renderer by {user}`behackl` in {pr}`4164`
* Fix HSL color ordering in ManimColor by {user}`thehugwizard` in {pr}`4202`
* Fix return type of `Polygram.get_vertex_groups()` and rename variables in `.round_corners()` by {user}`chopan050` in {pr}`4063`
* Improve `Mobject.align_data` docstring by {user}`irvanalhaq9` in {pr}`4152`
* Fix :meth:`VMobject.pointwise_become_partial` failing when `vmobject` is `self` by {user}`irvanalhaq9` in {pr}`4193`
* Fix `add_points_as_corners` not connecting single point to existing path by {user}`irvanalhaq9` in {pr}`4219`
* Complete typing for logger_utils.py by {user}`fmuenkel` in {pr}`4134`
* Fix(graph): Allow any Line subclass as edge_type in Graph/DiGraph by {user}`Akshat-Mishra-py` in {pr}`4251`
* Replace exceptions, remove unused parameters, and fix type hints in `Animation`, `ShowPartial`, `Create`, `ShowPassingFlash`, and `DrawBorderThenFill` by {user}`irvanalhaq9` in {pr}`4214`
* Fix: `Axes` submobject colors are not being set properly by {user}`ishu9bansal` in {pr}`4291`
* Refactor `Rotating` and add docstrings to `Mobject.rotate()` and `Rotating` by {user}`irvanalhaq9` in {pr}`4147`
* Fix default config of `manim init project` to use correct `pixel_height` and `pixel_width` by {user}`StevenH34` in {pr}`4213`
* Handle opacity and transparent images by {user}`henrikmidtiby` in {pr}`4313`
* Gracefully fall back when version metadata is missing by {user}`mohiuddin-khan-shiam` in {pr}`4324`
* Fix for issue 4255 - Do not clear points when the number of curves is zero by {user}`henrikmidtiby` in {pr}`4320`
* Use utf-8 encoding to read generated .tex files. by {user}`OliverStrait` in {pr}`4334`
* Add zero to vmobject points to remove negative zeros in `get_mobject_key` by {user}`elshorbagyx` in {pr}`4332`
* Ensure `stroke_width` attribute of `SVGMobject` is not set to `None` by {user}`henrikmidtiby` in {pr}`4319`
* Fix `Prism` incorrectly rendering with `dimensions=[2, 2, 2]` in OpenGL by {user}`ra1u` in {pr}`4003`
* Fix `BraceLabel.change_label()` and document `BraceText` by {user}`henrikmidtiby` in {pr}`4347`
* Include `Text.gradient` in hash to properly regenerate `Text` when its gradient changes by {user}`AbhilashaTandon` in {pr}`4099`
* Fixed surface animations in OpenGL by {user}`nubDotDev` in {pr}`4286`
* Add type hints and support for arithmetic operators `+` and `-` on `ValueTracker` by {user}`fmuenkel` in {pr}`4129`
* Fix duplicate references in `Scene.mobjects` after `ReplacementTransform` with existing target mobject by {user}`irvanalhaq9` in {pr}`4242`
* Optimize `always_redraw()` by reducing `Mobject` copying in `Mobject.become()` by {user}`chopan050` in {pr}`4357`
* Enhance `manim cfg show` output and add info-level logging for config files read by {user}`xnov18` in {pr}`4375`
* Let `Cube` use Bevel type line joints by {user}`nubDotDev` in {pr}`4361`
* Properly define `init_points` methods for use in OpenGL instead of defining `init_points = generate_points` by {user}`chopan050` in {pr}`4360`
* Allow passing a tuple to `buff` in `SurroundingRectangle` to specify buffer in x and y direction independently by {user}`nubDotDev` in {pr}`4390`
* Rewrite `color_gradient` to always return a list of ManimColors by {user}`henrikmidtiby` in {pr}`4380`
* Ensure leading whitespace does not change line height for lines in CodeMobject by {user}`behackl` in {pr}`4392`
* Simplify the function `remove_invisible_chars` in `text_mobject.py` by {user}`henrikmidtiby` in {pr}`4394`
* Fix some config options specified via `--config_file` not being respected properly by {user}`behackl` in {pr}`4401`
* Fix: Correct resolution tuple order to (height, width) by {user}`Nikhil172913832` in {pr}`4440`
* Ensure that start and end points are stored as float values in Line3D by {user}`SirJamesClarkMaxwell` in {pr}`4080`
* OpenGL: Fix iterated nesting in `DecimalNumber.set_value` by {user}`henrikmidtiby` in {pr}`4373`
* Update default resolution in CLI to match Manims 1920x1080 default settings by {user}`SASHAKT1290` in {pr}`4452`
* Better parsing of color styles in CodeMobject by {user}`SirJamesClarkMaxwell` in {pr}`4454`
* Allow selection of all scenes to render using '*' by {user}`NightyStudios` in {pr}`4470`
* Prevent mutation of `about_point` in `apply_points_function_about_point` by {user}`Morkunas` in {pr}`4478`
* Fix behavior of `Mobject.suspend_updating`: when only suspending parent mobject, let children continue updating by {user}`behackl` in {pr}`4402`
* Allow passing a `buff` to `LabeledDot` by {user}`nubDotDev` in {pr}`4403`
* Pass ndarrays to `mapbox_earcut.triangulate_float32()` to fix `TypeError` in `mapbox_earcut==2.0.0` by {user}`GuiCT` in {pr}`4479`
* Fix duplicated arrow tips in DashedVMobject (issue #3220) by {user}`jakekinchen` in {pr}`4484`
### Documentation
* Add docstring to :meth:`.Mobject.get_family` by {user}`irvanalhaq9` in {pr}`4127`
* Fix link formatting and clarify the distinction between Manim versions in index.rst by {user}`irvanalhaq9` in {pr}`4131`
* Add instructions for installing system utilities `cairo` and `pkg-config` via Homebrew on MacOS by {user}`behackl` in {pr}`4146`
* Add missing line break in Code of Conduct's conflict of interest policy by {user}`Hasan-Mesbaul-Ali-Taher` in {pr}`4185`
* Fix links to Pango website by {user}`ragibson` in {pr}`4217`
* Replace poetry with uv in the README by {user}`xinoehp512` in {pr}`4226`
* Improve docstring for `interpolate` method in `Mobject` class by {user}`irvanalhaq9` in {pr}`4149`
* Add docstrings to `Line` and remove `None` handling for `path_arc` parameter by {user}`irvanalhaq9` in {pr}`4223`
* Add docstring to :meth:`Mobject.family_members_with_points` by {user}`irvanalhaq9` in {pr}`4128`
* Update incorrect docstring for :attr:`ManimConfig.gui_location` property by {user}`SAYAN02-DEV` in {pr}`4254`
* Fix formatting of color space documentation by {user}`behackl` in {pr}`4274`
* Enhance and Paraphrase Description of ManimCE in README.md by {user}`irvanalhaq9` in {pr}`4141`
* docs: add explanation about the rate_func in the custom animation by {user}`pedropxoto` in {pr}`4278`
* Fixed artifact in docstring of Animation by {user}`barollet` in {pr}`4283`
* Rename update function `dot_position` to `update_label` in `.add_updater` example by {user}`irvanalhaq9` in {pr}`4196`
* Fix Microsoft typo in `TexFontTemplateLibrary` scene in `example_scenes/advanced_tex_fonts.py` by {user}`alterdim` in {pr}`4305`
* Improved readability, grammar, as well as added docstrings for consistency by {user}`NASAnerd05` in {pr}`4267`
* Add docstrings for `ChangingDecimal` and `ChangeDecimalToValue` by {user}`haveheartt` in {pr}`4346`
* Fix Sphinx exceptions when trying to build documentation via latex / as pdf by {user}`behackl` in {pr}`4370`
* Added license information to documentation landing page by {user}`Nikil-D-Gr8` in {pr}`3986`
* Set the default Python version to 3.13 in the uv installation guide by {user}`henrikmidtiby` in {pr}`4480`
### Maintenance and Testing
* Change project management tool from poetry to uv by {user}`behackl` in {pr}`4138`
* Re-add ffmpeg as dependency within Docker image by {user}`behackl` in {pr}`4150`
* Add tests for Matrix, DecimalMatrix, IntegerMatrix by {user}`pdrzan` in {pr}`4279`
* Add tests for polylabel utility by {user}`giolucasd` in {pr}`4269`
* Add support for `pycodestyle W` rule in Ruff by {user}`KaiqueDultra` in {pr}`4276`
* Fix files with few MyPy typing errors by {user}`henrikmidtiby` in {pr}`4263`
* Explicitly mention all files that mypy should ignore in the `mypy.ini` configuration file by {user}`henrikmidtiby` in {pr}`4306`
* Remove dead code from `scene.py` and `vector_space_scene.py` by {user}`henrikmidtiby` in {pr}`4310`
* Add type annotations to `scene.py` and `vector_space_scene.py` by {user}`henrikmidtiby` in {pr}`4260`
* Replace setup-texlive-action in CI workflow by {user}`behackl` in {pr}`4326`
* Adding type annotations to polyhedra.py and matrix.py by {user}`henrikmidtiby` in {pr}`4322`
* Handling typing errors in text/numbers.py by {user}`henrikmidtiby` in {pr}`4317`
* Move `configure_pygui` into a `Scene` method and remove `manim.gui` by {user}`chopan050` in {pr}`4314`
* Add typing annotations to svg_mobject.py by {user}`henrikmidtiby` in {pr}`4318`
* Add type annotations to `mobject/svg/brace.py` and default to `label_constructor=Text` in `BraceText` by {user}`henrikmidtiby` in {pr}`4309`
* Add classes `MethodWithArgs`, `SceneInteractContinue` and `SceneInteractRerun` inside new module `manim.data_structures` by {user}`chopan050` in {pr}`4315`
* Fix typo in import of OpenGLCamera in `utils/hashing.py` by {user}`fmuenkel` in {pr}`4352`
* Add type annotations to `manim/renderer/shader.py` by {user}`henrikmidtiby` in {pr}`4350`
* Add type annotations to `tex_mobject.py` by {user}`henrikmidtiby` in {pr}`4355`
* Add type annotations to `three_d_camera.py` by {user}`henrikmidtiby` in {pr}`4356`
* Revert change of default value for tex_environment by {user}`henrikmidtiby` in {pr}`4358`
* Add type hints to `scene_file_writer.py`, `section.py`, and `zoomed_scene.py` by {user}`fmuenkel` in {pr}`4133`
* Add type annotations for most of `camera` and `mobject.graphing` by {user}`henrikmidtiby` in {pr}`4125`
* Add `VectorNDLike` type aliases by {user}`chopan050` in {pr}`4068`
* Add type annotations to `dot_cloud.py`, `vectorized_mobject_rendering.py` and `opengl_three_dimensions.py` by {user}`henrikmidtiby` in {pr}`4359`
* Add type annotations to `indication.py` by {user}`henrikmidtiby` in {pr}`4367`
* Add type annotations to `composition.py` by {user}`henrikmidtiby` in {pr}`4366`
* Add type annotations to `growing.py` by {user}`henrikmidtiby` in {pr}`4368`
* Add type annotations to `movement.py` by {user}`henrikmidtiby` in {pr}`4371`
* Exclude check for cyclic imports by CodeQL by {user}`behackl` in {pr}`4384`
* Refactor imports from `collections.abc`, `typing` and `typing_extensions` for Python 3.9 by {user}`chopan050` in {pr}`4353`
* Add type annotations to `opengl_renderer_window.py` by {user}`fmuenkel` in {pr}`4363`
* Rename `SceneFileWriter.save_final_image()` to `save_image()` by {user}`fmuenkel` in {pr}`4378`
* Add type annotations to `text_mobject.py` by {user}`henrikmidtiby` in {pr}`4381`
* Rename types like `RGBA_Array_Float` to `FloatRGBA` and add types like `FloatRGBA_Array` by {user}`chopan050` in {pr}`4386`
* Add type annotations to `opengl_geometry.py` by {user}`henrikmidtiby` in {pr}`4396`
* Add type annotations to `moving_camera.py` by {user}`henrikmidtiby` in {pr}`4397`
* Add type annotations to `opengl_mobject.py` by {user}`RBerga06` in {pr}`4398`
* Fix failing pre-commit tests by {user}`cclauss` in {pr}`4434`
* Add type annotations to `cairo_renderer.py` by {user}`fmuenkel` in {pr}`4393`
* Fix type errors and add typings for `Mobject.apply_function()`, its derivatives, and other utility functions by {user}`godalming123` in {pr}`4228`
* Bump macOS image from deprecated macos-13 to macos-15-intel by {user}`chopan050` in {pr}`4481`
* Prepare new release `v0.19.1` and bump minimum required Python version to 3.10 by {user}`behackl` in {pr}`4490`
### Dependency Version Changes
* Bump typing extensions minimum version by {user}`JasonGrace2282` in {pr}`4121`
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4122`
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4140`
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4148`
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4181`
* Bump astral-sh/setup-uv from 5 to 6 by {user}`dependabot`[bot] in {pr}`4234`
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4204`
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4391`
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4405`
* Bump actions/setup-python from 5 to 6 by {user}`dependabot`[bot] in {pr}`4433`
* Bump actions/checkout from 4 to 5 by {user}`dependabot`[bot] in {pr}`4418`
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4409`
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4460`
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4467`
* Bump github/codeql-action from 3 to 4 by {user}`dependabot`[bot] in {pr}`4466`
* Bump astral-sh/setup-uv from 6 to 7 by {user}`dependabot`[bot] in {pr}`4465`
* Bump actions/upload-artifact from 4 to 5 by {user}`dependabot`[bot] in {pr}`4464`
## New Contributors
* {user}`BenKirkels` made their first contribution in {pr}`4144`
* {user}`Hasan-Mesbaul-Ali-Taher` made their first contribution in {pr}`4185`
* {user}`ragibson` made their first contribution in {pr}`4217`
* {user}`thehugwizard` made their first contribution in {pr}`4202`
* {user}`xinoehp512` made their first contribution in {pr}`4226`
* {user}`SAYAN02-DEV` made their first contribution in {pr}`4254`
* {user}`Akshat-Mishra-py` made their first contribution in {pr}`4251`
* {user}`pdrzan` made their first contribution in {pr}`4279`
* {user}`pedropxoto` made their first contribution in {pr}`4278`
* {user}`giolucasd` made their first contribution in {pr}`4269`
* {user}`KaiqueDultra` made their first contribution in {pr}`4276`
* {user}`ishu9bansal` made their first contribution in {pr}`4291`
* {user}`StevenH34` made their first contribution in {pr}`4213`
* {user}`alterdim` made their first contribution in {pr}`4305`
* {user}`mohiuddin-khan-shiam` made their first contribution in {pr}`4324`
* {user}`elshorbagyx` made their first contribution in {pr}`4332`
* {user}`NASAnerd05` made their first contribution in {pr}`4267`
* {user}`ra1u` made their first contribution in {pr}`4003`
* {user}`AbhilashaTandon` made their first contribution in {pr}`4099`
* {user}`nubDotDev` made their first contribution in {pr}`4286`
* {user}`haveheartt` made their first contribution in {pr}`4346`
* {user}`xnov18` made their first contribution in {pr}`4375`
* {user}`Nikil-D-Gr8` made their first contribution in {pr}`3986`
* {user}`RBerga06` made their first contribution in {pr}`4398`
* {user}`Nikhil172913832` made their first contribution in {pr}`4440`
* {user}`SASHAKT1290` made their first contribution in {pr}`4452`
* {user}`Brainsucker92` made their first contribution in {pr}`4469`
* {user}`NightyStudios` made their first contribution in {pr}`4470`
* {user}`Morkunas` made their first contribution in {pr}`4478`
* {user}`GuiCT` made their first contribution in {pr}`4479`
* {user}`godalming123` made their first contribution in {pr}`4228`
* {user}`jakekinchen` made their first contribution in {pr}`4484`
**Full Changelog**: https://github.com/ManimCommunity/manim/compare/v0.19.0...v0.19.1

View file

@ -0,0 +1,41 @@
---
short-title: v0.19.2
description: Changelog for Manim v0.19.2
---
# v0.19.2
Date
: January 17, 2026
## What's Changed
### Highlights 🌟
* Add support for Python 3.14, bump minimum Python to 3.11 and av to 14.0.1 by {user}`behackl` in {pr}`4385`
### Bug Fixes 🐛
* Fix argument passed to `get_hash_from_play_call` in hashing by {user}`judenimo` in {pr}`4524`
* Fix incorrect `Circle.point_at_angle` calculation by {user}`Swarnlataaa` in {pr}`4438`
### Testing 🧪
* Test on Apple Silicon ARM64 by {user}`cclauss` in {pr}`4496`
### Code Quality & Refactoring 🧹
* Add ruff rules PERF for performance by {user}`cclauss` in {pr}`4492`
* Remove deprecation warning from pytest "np.trapz" -> "np.trapezoid" by {user}`henrikmidtiby` in {pr}`4513`
* Bump Python target versions of both mypy and ruff by {user}`behackl` in {pr}`4520`
* Replace legacy numpy usage -- ruff rule NPY002 by {user}`cclauss` in {pr}`4516`
* Add `.github/release.yml` for improved classifications in automatically generated changelogs by {user}`behackl` in {pr}`4526`
* Check and bump lower version requirements for dependencies by {user}`henrikmidtiby` in {pr}`4529`
### Type Hints 📝
* Add type annotations to `three_dimensions.py` by {user}`henrikmidtiby` in {pr}`4497`
### Other Changes
* Prepare new release `v0.19.2` by {user}`behackl` in {pr}`4528`
## New Contributors
* {user}`judenimo` made their first contribution in {pr}`4524`
* {user}`Swarnlataaa` made their first contribution in {pr}`4438`
**Full Changelog**: https://github.com/ManimCommunity/manim/compare/v0.19.1...v0.19.2

View file

@ -0,0 +1,86 @@
---
short-title: v0.20.0
description: Changelog for v0.20.0
---
# v0.20.0
Date
: February 20, 2026
## What's Changed
### Breaking Changes 🚨
* Fix `ImageMobject` 3D rotation/flipping and remove resampling algorithms `lanczos` (`antialias`), `box` and `hamming` by {user}`chopan050` in {pr}`4266`
* Fix `YELLOW_C` and add `PURE_CYAN`, `PURE_MAGENTA` and `PURE_YELLOW` by {user}`chopan050` in {pr}`4562`
### Highlights 🌟
* Rewrite MathTex to make it more robust regarding splitting by {user}`henrikmidtiby` in {pr}`4515`
The MathTex implementation has been updated to make it more robust and fix a number of issues.
A beneficial side effect is that named groups in svg files can now be accessed through SVGMobject.
* Add new Animation Builder `Mobject.always` by {user}`JasonGrace2282` in {pr}`4594`
This new feature is a convenience wrapper around `add_updater` that allows adding
updaters to a mobject in an intuitive and easy-to-read way. Example usage in a scene:
```python
d = Dot()
s = Square()
d.always.next_to(s, UP)
self.add(s, d)
self.play(s.animate.to_edge(LEFT))
```
### New Features ✨
* Add a `seed` config option + `--seed` CLI option for reproducible randomness in rendered scenes by {user}`arnaud-ma` in {pr}`4532`
### Enhancements 🚀
* Enable `strict=True` for `zip()` where safe by {user}`Oll-iver` in {pr}`4547`
### Bug Fixes 🐛
* using `color` instead of `fill_color` with MathTeX for node labels by {user}`Schefflera-Arboricola` in {pr}`4501`
* fix: infinite recursion caused by accessing color of a highlighted Ta… by {user}`BHearron` in {pr}`4435`
* Prevent potential `UnboundLocalError` in `PolarPlane` by {user}`RinZ27` in {pr}`4557`
* Fixed division by 0 in `turn_animation_into_updater` by {user}`SoldierSacha` in {pr}`4567`
* Fix TOCTOU Race Conditions when creating directories by {user}`SoldierSacha` in {pr}`4587`
* Resolve more race conditions potentially happening during directory creation by {user}`SoldierSacha` in {pr}`4589`
* Fix `c2p`/`coords_to_point` method call with single flat list or 1D array input by {user}`danielalanbates` in {pr}`4596`
### Documentation 📚
* Enable rendered documentation of `RandomColorGenerator` by {user}`arnaud-ma` in {pr}`4533`
* Remove pin to Python 3.13 in installation docs by {user}`chopan050` in {pr}`4534`
* Fix broken aquabeam OpenGL link using Wayback Machine by {user}`behackl` in {pr}`4545`
* Add type annotations and docstrings in `opengl_renderer.py` by {user}`arnaud-ma` in {pr}`4537`
* docs: improve `TransformFromCopy` docstring by {user}`GoThrones` in {pr}`4597`
### Infrastructure & Build 🔨
* Install missing dependencies in release pipeline by {user}`behackl` in {pr}`4531`
### Code Quality & Refactoring 🧹
* Rework and consolidate release changelog script, add previously skipped changelog entries by {user}`behackl` in {pr}`4568`
* Remove `__future__.annotations` from required imports by {user}`JasonGrace2282` in {pr}`4571`
* Cleaned up `mypy.ini` by {user}`henrikmidtiby` in {pr}`4584`
* Add `py.typed` to declare manim as having type hints by {user}`Timmmm` in {pr}`4553`
* Fix assertion in `ImageMobjectFromCamera.interpolate_color()` by {user}`chopan050` in {pr}`4593`
* Reduce dependency on scipy - replace `scipy.special.comb` with `math.comb` by {user}`fmuenkel` in {pr}`4598`
### Type Hints 📝
* Add type annotations to `rotation.py` by {user}`fmuenkel` in {pr}`4535`
* Add type annotations to `opengl_compatibility.py` by {user}`fmuenkel` in {pr}`4585`
* Add type annotations to `image_mobject.py` by {user}`henrikmidtiby` in {pr}`4458`
* Add type annotations to `opengl_image_mobject.py` by {user}`fmuenkel` in {pr}`4536`
* Add type annotations to `point_cloud_mobject.py` by {user}`fmuenkel` in {pr}`4586`
## New Contributors
* {user}`arnaud-ma` made their first contribution in {pr}`4533`
* {user}`Schefflera-Arboricola` made their first contribution in {pr}`4501`
* {user}`BHearron` made their first contribution in {pr}`4435`
* {user}`RinZ27` made their first contribution in {pr}`4557`
* {user}`SoldierSacha` made their first contribution in {pr}`4567`
* {user}`Oll-iver` made their first contribution in {pr}`4547`
* {user}`GoThrones` made their first contribution in {pr}`4597`
* {user}`danielalanbates` made their first contribution in {pr}`4596`
**Full Changelog**: [Compare view](https://github.com/ManimCommunity/manim/compare/v0.19.2...v0.20.0)

View 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)

View file

@ -58,7 +58,7 @@ extensions = [
# Automatically generate stub pages when using the .. autosummary directive
autosummary_generate = True
myst_enable_extensions = ["colon_fence", "amsmath"]
myst_enable_extensions = ["colon_fence", "amsmath", "deflist"]
# redirects (for moved / deleted pages)
redirects = {
@ -156,11 +156,13 @@ html_title = f"Manim Community v{manim.__version__}"
# This specifies any additional css files that will override the theme's
html_css_files = ["custom.css"]
latex_engine = "lualatex"
# external links
extlinks = {
"issue": ("https://github.com/ManimCommunity/manim/issues/%s", "#%s"),
"pr": ("https://github.com/ManimCommunity/manim/pull/%s", "#%s"),
"pr": ("https://github.com/ManimCommunity/manim/pull/%s", "PR #%s"),
"user": ("https://github.com/%s", "@%s"),
}
# opengraph settings

View file

@ -85,14 +85,8 @@ typed as a :class:`~.Point3D`, because it represents a direction along
which to shift a :class:`~.Mobject`, not a position in space.
As a general rule, if a parameter is called ``direction`` or ``axis``,
it should be type hinted as some form of :class:`~.VectorND`.
.. warning::
This is not always true. For example, as of Manim 0.18.0, the direction
parameter of the :class:`.Vector` Mobject should be
``Point2DLike | Point3DLike``, as it can also accept ``tuple[float, float]``
and ``tuple[float, float, float]``.
it should be type hinted as some form of :class:`~.VectorND` or
:class:`~.VectorNDLike`.
Colors
------

View file

@ -8,7 +8,7 @@ or specific OpenGL classes like `OpenGLSurface`, but documentation for some of
them is available in form of docstrings
[in the source code](https://github.com/ManimCommunity/manim/tree/main/manim/mobject/opengl).
Furthermore, [this user guide by *aquabeam*](https://www.aquabeam.me/manim/opengl_guide/)
Furthermore, [this user guide by *aquabeam*](https://web.archive.org/web/20250708135737/https://www.aquabeam.me/manim/opengl_guide/)
can be helpful to get started using the OpenGL renderer.
---

View file

@ -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:
@ -424,25 +413,33 @@ may be expected. To color only ``x`` yellow, we have to do the following:
class CorrectLaTeXSubstringColoring(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",
r"e^{x} = x^0 + x^1 + \frac{1}{2} x^2 + \frac{1}{6} x^3 + \cdots + \frac{1}{n!} x^n + \cdots",
substrings_to_isolate="x"
)
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
=======================================================

View file

@ -94,6 +94,21 @@ or `Discord <https://www.manim.community/discord/>`_. If you're using Manim in a
context, instructions on how to cite a particular release can be found
`in our README <https://github.com/ManimCommunity/manim/blob/main/README.md>`_.
License Information
-------------------
Manim is an open-source library licensed under the **MIT License**, which applies to both the
original and the community editions of the software. This means you are free to use, modify,
and distribute the code in accordance with the MIT License terms. However, there are some
additional points to be aware of:
- **Copyrighted Assets:** Specific assets, such as the "Pi creatures" in Grant Sanderson's
(3Blue1Brown) videos, are copyrighted and protected. Please avoid using these characters in
any derivative works.
- **Content Creation and Sharing:** Videos and animations created with Manim can be freely
shared, and no attribution to Manim is required—although it is much appreciated! You are
encouraged to showcase your work online and share it with the Manim community.
Index
-----

View file

@ -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
-----------------------------------

View file

@ -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
```
:::

View file

@ -15,6 +15,7 @@ Module Index
~utils.commands
~utils.config_ops
constants
data_structures
~utils.debug
~utils.deprecation
~utils.docbuild

View file

@ -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

View file

@ -52,7 +52,7 @@ class TexFontTemplateLibrary(Scene):
Many of the in the TexFontTemplates collection require that specific fonts
are installed on your local machine.
For example, choosing the template TexFontTemplates.comic_sans will
not compile if the Comic Sans Micrososft font is not installed.
not compile if the Comic Sans Microsoft font is not installed.
This scene will only render those Templates that do not cause a TeX
compilation error on your system. Furthermore, some of the ones that do render,

View file

@ -1,9 +1,16 @@
#!/usr/bin/env python
from __future__ import annotations
from importlib.metadata import version
from importlib.metadata import PackageNotFoundError, version
__version__ = version(__name__)
# Use installed distribution version if available; otherwise fall back to a
# sensible default so that importing from a source checkout works without an
# editable install (pip install -e .).
try:
__version__ = version(__name__)
except PackageNotFoundError:
# Package is not installed; provide a fallback version string.
__version__ = "0.0.0+unknown"
# isort: off

View file

@ -23,10 +23,9 @@ __all__ = [
parser = make_config_parser()
# The logger can be accessed from anywhere as manim.logger, or as
# logging.getLogger("manim"). The console must be accessed as manim.console.
# Throughout the codebase, use manim.console.print() instead of print().
# Use error_console to print errors so that it outputs to stderr.
# Logger usage: accessible globally as `manim.logger` or via `logging.getLogger("manim")`.
# For printing, use `manim.console.print()` instead of the built-in `print()`.
# For error output, use `error_console`, which prints to stderr.
logger, console, error_console = make_logger(
parser["logger"],
parser["CLI"]["verbosity"],
@ -45,7 +44,7 @@ frame = ManimFrame(config)
# This has to go here because it needs access to this module's config
@contextmanager
def tempconfig(temp: ManimConfig | dict[str, Any]) -> Generator[None, None, None]:
"""Context manager that temporarily modifies the global ``config`` object.
"""Temporarily modifies the global ``config`` object using a context manager.
Inside the ``with`` statement, the modified config will be used. After
context manager exits, the config will be restored to its original state.

View file

@ -1,3 +1,9 @@
"""Parses CLI context settings from the configuration file and returns a Cloup Context settings dictionary.
This module reads configuration values for help formatting, theme styles, and alignment options
used when rendering command-line interfaces in Manim.
"""
from __future__ import annotations
import configparser
@ -28,6 +34,7 @@ def parse_cli_ctx(parser: configparser.SectionProxy) -> dict[str, Any]:
"col2",
"epilog",
}
# Extract and apply any style-related keys defined in the config section.
for k, v in parser.items():
if k in theme_keys and v:
theme_settings.update({k: Style(v)})

View file

@ -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
@ -33,8 +33,7 @@ from manim.utils.tex import TexTemplate
if TYPE_CHECKING:
from enum import EnumMeta
from typing_extensions import Self
from typing import Self
from manim.typing import StrPath, Vector3D
@ -122,16 +121,20 @@ def make_config_parser(
# read_file() before calling read() for any optional files."
# https://docs.python.org/3/library/configparser.html#configparser.ConfigParser.read
parser = configparser.ConfigParser()
logger.info(f"Reading config file: {library_wide}")
with library_wide.open() as file:
parser.read_file(file) # necessary file
other_files = [user_wide, Path(custom_file) if custom_file else folder_wide]
for path in other_files:
if path.exists():
logger.info(f"Reading config file: {path}")
parser.read(other_files) # optional files
return parser
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
@ -296,6 +299,7 @@ class ManimConfig(MutableMapping):
"save_last_frame",
"save_pngs",
"scene_names",
"seed",
"show_in_file_browser",
"tex_dir",
"tex_template",
@ -334,6 +338,7 @@ class ManimConfig(MutableMapping):
def __contains__(self, key: object) -> bool:
try:
assert isinstance(key, str)
self.__getitem__(key)
return True
except AttributeError:
@ -424,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
@ -591,6 +596,7 @@ class ManimConfig(MutableMapping):
"enable_wireframe",
"force_window",
"no_latex_cleanup",
"dry_run",
]:
setattr(self, key, parser["CLI"].getboolean(key, fallback=False))
@ -602,6 +608,7 @@ class ManimConfig(MutableMapping):
# the next two must be set BEFORE digesting frame_width and frame_height
"pixel_height",
"pixel_width",
"seed",
"window_monitor",
"zero_pad",
]:
@ -625,6 +632,7 @@ class ManimConfig(MutableMapping):
"background_color",
"renderer",
"window_position",
"preview_command",
]:
setattr(self, key, parser["CLI"].get(key, fallback="", raw=True))
@ -648,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)
@ -664,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
@ -767,6 +777,7 @@ class ManimConfig(MutableMapping):
"dry_run",
"no_latex_cleanup",
"preview_command",
"seed",
]:
if hasattr(args, key):
attr = getattr(args, key)
@ -1036,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"]
@ -1068,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"]
@ -1104,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
@ -1129,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
)
@ -1277,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]
)
@ -1287,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
@ -1304,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
@ -1413,12 +1427,12 @@ class ManimConfig(MutableMapping):
self._d.__setitem__("window_position", value)
@property
def window_size(self) -> str:
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:
@ -1447,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"]
@ -1631,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)
@ -1730,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"]
@ -1759,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)
@ -1795,9 +1810,20 @@ 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
def seed(self) -> int | None:
"""Random seed for reproducibility. None means no seed is set."""
return self._d["seed"]
@seed.setter
def seed(self, value: int | None) -> None:
if value is None:
return
self._set_pos_number("seed", value, False)
# TODO: to be used in the future - see PR #620
# https://github.com/ManimCommunity/manim/pull/620
@ -1842,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:
@ -1850,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:
@ -1868,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]

View file

@ -14,12 +14,10 @@ from ..utils.rate_functions import linear, smooth
__all__ = ["Animation", "Wait", "Add", "override_animation"]
from collections.abc import Iterable, Sequence
from collections.abc import Callable, Iterable, Sequence
from copy import deepcopy
from functools import partialmethod
from typing import TYPE_CHECKING, Any, Callable
from typing_extensions import Self
from typing import TYPE_CHECKING, Any, Self
if TYPE_CHECKING:
from manim.scene.scene import Scene
@ -129,7 +127,7 @@ class Animation:
def __init__(
self,
mobject: Mobject | None,
mobject: Mobject | OpenGLMobject | None,
lag_ratio: float = DEFAULT_ANIMATION_LAG_RATIO,
run_time: float = DEFAULT_ANIMATION_RUN_TIME,
rate_func: Callable[[float], float] = smooth,
@ -262,11 +260,11 @@ class Animation:
):
scene.add(self.mobject)
def create_starting_mobject(self) -> Mobject:
def create_starting_mobject(self) -> Mobject | OpenGLMobject:
# Keep track of where the mobject starts
return self.mobject.copy()
def get_all_mobjects(self) -> Sequence[Mobject]:
def get_all_mobjects(self) -> Sequence[Mobject | OpenGLMobject]:
"""Get all mobjects involved in the animation.
Ordering must match the ordering of arguments to interpolate_submobject
@ -280,9 +278,12 @@ class Animation:
def get_all_families_zipped(self) -> Iterable[tuple]:
if config["renderer"] == RendererType.OPENGL:
return zip(*(mob.get_family() for mob in self.get_all_mobjects()))
return zip(
*(mob.get_family() for mob in self.get_all_mobjects()), strict=False
)
return zip(
*(mob.family_members_with_points() for mob in self.get_all_mobjects())
*(mob.family_members_with_points() for mob in self.get_all_mobjects()),
strict=False,
)
def update_mobjects(self, dt: float) -> None:
@ -540,7 +541,7 @@ class Animation:
def prepare_animation(
anim: Animation | mobject._AnimationBuilder,
anim: Animation | mobject._AnimationBuilder | opengl_mobject._AnimationBuilder,
) -> Animation:
r"""Returns either an unchanged animation, or the animation built
from a passed animation factory.

View file

@ -4,10 +4,8 @@ from __future__ import annotations
__all__ = ["AnimatedBoundary", "TracedPath"]
from collections.abc import Sequence
from typing import Callable
from typing_extensions import Any, Self
from collections.abc import Callable, Sequence
from typing import Any, Self
from manim.mobject.mobject import Mobject
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
@ -99,7 +97,7 @@ class AnimatedBoundary(VGroup):
) -> Self:
family1 = mob1.family_members_with_points()
family2 = mob2.family_members_with_points()
for sm1, sm2 in zip(family1, family2):
for sm1, sm2 in zip(family1, family2, strict=False):
sm1.pointwise_become_partial(sm2, a, b)
return self

View file

@ -2,9 +2,8 @@
from __future__ import annotations
import types
from collections.abc import Iterable, Sequence
from typing import TYPE_CHECKING, Callable
from collections.abc import Callable, Iterable, Sequence
from typing import TYPE_CHECKING, Any
import numpy as np
@ -12,7 +11,7 @@ from manim._config import config
from manim.animation.animation import Animation, prepare_animation
from manim.constants import RendererType
from manim.mobject.mobject import Group, Mobject
from manim.mobject.opengl.opengl_mobject import OpenGLGroup
from manim.mobject.opengl.opengl_mobject import OpenGLGroup, OpenGLMobject
from manim.scene.scene import Scene
from manim.utils.iterables import remove_list_redundancies
from manim.utils.parameter_parsing import flatten_iterable_parameters
@ -54,31 +53,34 @@ class AnimationGroup(Animation):
def __init__(
self,
*animations: Animation | Iterable[Animation] | types.GeneratorType[Animation],
group: Group | VGroup | OpenGLGroup | OpenGLVGroup = None,
*animations: Animation | Iterable[Animation],
group: Group | VGroup | OpenGLGroup | OpenGLVGroup | None = None,
run_time: float | None = None,
rate_func: Callable[[float], float] = linear,
lag_ratio: float = 0,
**kwargs,
) -> None:
**kwargs: Any,
):
arg_anim = flatten_iterable_parameters(animations)
self.animations = [prepare_animation(anim) for anim in arg_anim]
self.rate_func = rate_func
self.group = group
if self.group is None:
if group is None:
mobjects = remove_list_redundancies(
[anim.mobject for anim in self.animations if not anim.is_introducer()],
)
if config["renderer"] == RendererType.OPENGL:
self.group = OpenGLGroup(*mobjects)
self.group: Group | VGroup | OpenGLGroup | OpenGLVGroup = OpenGLGroup(
*mobjects
)
else:
self.group = Group(*mobjects)
else:
self.group = group
super().__init__(
self.group, rate_func=self.rate_func, lag_ratio=lag_ratio, **kwargs
)
self.run_time: float = self.init_run_time(run_time)
def get_all_mobjects(self) -> Sequence[Mobject]:
def get_all_mobjects(self) -> Sequence[Mobject | OpenGLMobject]:
return list(self.group)
def begin(self) -> None:
@ -93,7 +95,7 @@ class AnimationGroup(Animation):
for anim in self.animations:
anim.begin()
def _setup_scene(self, scene) -> None:
def _setup_scene(self, scene: Scene) -> None:
for anim in self.animations:
anim._setup_scene(scene)
@ -118,7 +120,7 @@ class AnimationGroup(Animation):
]:
anim.update_mobjects(dt)
def init_run_time(self, run_time) -> float:
def init_run_time(self, run_time: float | None) -> float:
"""Calculates the run time of the animation, if different from ``run_time``.
Parameters
@ -146,9 +148,9 @@ class AnimationGroup(Animation):
run_times = np.array([anim.run_time for anim in self.animations])
num_animations = run_times.shape[0]
dtype = [("anim", "O"), ("start", "f8"), ("end", "f8")]
self.anims_with_timings = np.zeros(num_animations, dtype=dtype)
self.anims_begun = np.zeros(num_animations, dtype=bool)
self.anims_finished = np.zeros(num_animations, dtype=bool)
self.anims_with_timings: np.ndarray = np.zeros(num_animations, dtype=dtype)
self.anims_begun: np.ndarray = np.zeros(num_animations, dtype=bool)
self.anims_finished: np.ndarray = np.zeros(num_animations, dtype=bool)
if num_animations == 0:
return
@ -183,7 +185,9 @@ class AnimationGroup(Animation):
else:
sub_alphas[(sub_alphas > 1) | with_zero_run_time] = 1
for anim_to_update, sub_alpha in zip(to_update["anim"], sub_alphas):
for anim_to_update, sub_alpha in zip(
to_update["anim"], sub_alphas, strict=True
):
anim_to_update.interpolate(sub_alpha)
self.anim_group_time = anim_group_time
@ -228,7 +232,7 @@ class Succession(AnimationGroup):
))
"""
def __init__(self, *animations: Animation, lag_ratio: float = 1, **kwargs) -> None:
def __init__(self, *animations: Animation, lag_ratio: float = 1, **kwargs: Any):
super().__init__(*animations, lag_ratio=lag_ratio, **kwargs)
def begin(self) -> None:
@ -247,7 +251,7 @@ class Succession(AnimationGroup):
if self.active_animation:
self.active_animation.update_mobjects(dt)
def _setup_scene(self, scene) -> None:
def _setup_scene(self, scene: Scene | None) -> None:
if scene is None:
return
if self.is_introducer():
@ -339,7 +343,7 @@ class LaggedStart(AnimationGroup):
self,
*animations: Animation,
lag_ratio: float = DEFAULT_LAGGED_START_LAG_RATIO,
**kwargs,
**kwargs: Any,
):
super().__init__(*animations, lag_ratio=lag_ratio, **kwargs)
@ -349,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,
@ -358,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
--------
@ -384,20 +399,23 @@ class LaggedStartMap(LaggedStart):
def __init__(
self,
AnimationClass: Callable[..., Animation],
animation_class: type[Animation],
mobject: Mobject,
arg_creator: Callable[[Mobject], str] = None,
arg_creator: Callable[[Mobject], Iterable[Any]] | None = None,
run_time: float = 2,
**kwargs,
) -> None:
args_list = []
for submob in mobject:
if arg_creator:
args_list.append(arg_creator(submob))
else:
args_list.append((submob,))
lag_ratio: float = DEFAULT_LAGGED_START_LAG_RATIO,
**kwargs: Any,
):
if arg_creator is None:
def identity(mob: Mobject) -> Mobject:
return mob
arg_creator = identity
args_list = [arg_creator(submob) for submob in mobject]
anim_kwargs = dict(kwargs)
if "lag_ratio" in anim_kwargs:
anim_kwargs.pop("lag_ratio")
animations = [AnimationClass(*args, **anim_kwargs) for args in args_list]
super().__init__(*animations, run_time=run_time, **kwargs)
animations = [animation_class(*args, **anim_kwargs) for args in args_list]
super().__init__(*animations, run_time=run_time, lag_ratio=lag_ratio)

View file

@ -76,8 +76,8 @@ __all__ = [
import itertools as it
from collections.abc import Iterable, Sequence
from typing import TYPE_CHECKING, Callable
from collections.abc import Callable, Iterable, Sequence
from typing import TYPE_CHECKING
import numpy as np
@ -472,7 +472,7 @@ class SpiralIn(Animation):
def interpolate_mobject(self, alpha: float) -> None:
alpha = self.rate_func(alpha)
for original_shape, shape in zip(self.shapes, self.mobject):
for original_shape, shape in zip(self.shapes, self.mobject, strict=True):
shape.restore()
fill_opacity = original_shape.get_fill_opacity()
stroke_opacity = original_shape.get_stroke_opacity()

View file

@ -19,8 +19,9 @@ __all__ = [
"FadeIn",
]
from typing import Any
import numpy as np
from typing_extensions import Any
from manim.mobject.opengl.opengl_mobject import OpenGLMobject

View file

@ -31,16 +31,17 @@ __all__ = [
"SpinInFromNothing",
]
import typing
import numpy as np
from typing import TYPE_CHECKING, Any
from ..animation.transform import Transform
from ..constants import PI
from ..utils.paths import spiral_path
if typing.TYPE_CHECKING:
if TYPE_CHECKING:
from manim.mobject.geometry.line import Arrow
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
from manim.typing import Point3DLike, Vector3DLike
from manim.utils.color import ParsableManimColor
from ..mobject.mobject import Mobject
@ -76,16 +77,20 @@ class GrowFromPoint(Transform):
"""
def __init__(
self, mobject: Mobject, point: np.ndarray, point_color: str = None, **kwargs
) -> None:
self,
mobject: Mobject,
point: Point3DLike,
point_color: ParsableManimColor | None = None,
**kwargs: Any,
):
self.point = point
self.point_color = point_color
super().__init__(mobject, introducer=True, **kwargs)
def create_target(self) -> Mobject:
def create_target(self) -> Mobject | OpenGLMobject:
return self.mobject
def create_starting_mobject(self) -> Mobject:
def create_starting_mobject(self) -> Mobject | OpenGLMobject:
start = super().create_starting_mobject()
start.scale(0)
start.move_to(self.point)
@ -118,7 +123,12 @@ class GrowFromCenter(GrowFromPoint):
"""
def __init__(self, mobject: Mobject, point_color: str = None, **kwargs) -> None:
def __init__(
self,
mobject: Mobject,
point_color: ParsableManimColor | None = None,
**kwargs: Any,
):
point = mobject.get_center()
super().__init__(mobject, point, point_color=point_color, **kwargs)
@ -153,8 +163,12 @@ class GrowFromEdge(GrowFromPoint):
"""
def __init__(
self, mobject: Mobject, edge: np.ndarray, point_color: str = None, **kwargs
) -> None:
self,
mobject: Mobject,
edge: Vector3DLike,
point_color: ParsableManimColor | None = None,
**kwargs: Any,
):
point = mobject.get_critical_point(edge)
super().__init__(mobject, point, point_color=point_color, **kwargs)
@ -183,11 +197,13 @@ class GrowArrow(GrowFromPoint):
"""
def __init__(self, arrow: Arrow, point_color: str = None, **kwargs) -> None:
def __init__(
self, arrow: Arrow, point_color: ParsableManimColor | None = None, **kwargs: Any
):
point = arrow.get_start()
super().__init__(arrow, point, point_color=point_color, **kwargs)
def create_starting_mobject(self) -> Mobject:
def create_starting_mobject(self) -> Mobject | OpenGLMobject:
start_arrow = self.mobject.copy()
start_arrow.scale(0, scale_tips=True, about_point=self.point)
if self.point_color:
@ -224,8 +240,12 @@ class SpinInFromNothing(GrowFromCenter):
"""
def __init__(
self, mobject: Mobject, angle: float = PI / 2, point_color: str = None, **kwargs
) -> None:
self,
mobject: Mobject,
angle: float = PI / 2,
point_color: ParsableManimColor | None = None,
**kwargs: Any,
):
self.angle = angle
super().__init__(
mobject, path_func=spiral_path(angle), point_color=point_color, **kwargs

View file

@ -40,7 +40,7 @@ __all__ = [
]
from collections.abc import Iterable
from typing import Callable
from typing import Any, Self
import numpy as np
@ -48,6 +48,7 @@ from manim.mobject.geometry.arc import Circle, Dot
from manim.mobject.geometry.line import Line
from manim.mobject.geometry.polygram import Rectangle
from manim.mobject.geometry.shape_matchers import SurroundingRectangle
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
from manim.scene.scene import Scene
from .. import config
@ -61,9 +62,10 @@ from ..animation.updaters.update import UpdateFromFunc
from ..constants import *
from ..mobject.mobject import Mobject
from ..mobject.types.vectorized_mobject import VGroup, VMobject
from ..typing import Point3D, Point3DLike, Vector3DLike
from ..utils.bezier import interpolate, inverse_interpolate
from ..utils.color import GREY, YELLOW, ParsableManimColor
from ..utils.rate_functions import smooth, there_and_back, wiggle
from ..utils.color import GREY, PURE_YELLOW, ParsableManimColor
from ..utils.rate_functions import RateFunction, smooth, there_and_back, wiggle
from ..utils.space_ops import normalize
@ -87,7 +89,7 @@ class FocusOn(Transform):
class UsingFocusOn(Scene):
def construct(self):
dot = Dot(color=YELLOW).shift(DOWN)
dot = Dot(color=PURE_YELLOW).shift(DOWN)
self.add(Tex("Focusing on the dot below:"), dot)
self.play(FocusOn(dot))
self.wait()
@ -95,12 +97,12 @@ class FocusOn(Transform):
def __init__(
self,
focus_point: np.ndarray | Mobject,
focus_point: Point3DLike | Mobject,
opacity: float = 0.2,
color: str = GREY,
color: ParsableManimColor = GREY,
run_time: float = 2,
**kwargs,
) -> None:
**kwargs: Any,
):
self.focus_point = focus_point
self.color = color
self.opacity = opacity
@ -151,15 +153,15 @@ class Indicate(Transform):
self,
mobject: Mobject,
scale_factor: float = 1.2,
color: str = YELLOW,
rate_func: Callable[[float, float | None], np.ndarray] = there_and_back,
**kwargs,
) -> None:
color: ParsableManimColor = PURE_YELLOW,
rate_func: RateFunction = there_and_back,
**kwargs: Any,
):
self.color = color
self.scale_factor = scale_factor
super().__init__(mobject, rate_func=rate_func, **kwargs)
def create_target(self) -> Mobject:
def create_target(self) -> Mobject | OpenGLMobject:
target = self.mobject.copy()
target.scale(self.scale_factor)
target.set_color(self.color)
@ -196,7 +198,7 @@ class Flash(AnimationGroup):
class UsingFlash(Scene):
def construct(self):
dot = Dot(color=YELLOW).shift(DOWN)
dot = Dot(color=PURE_YELLOW).shift(DOWN)
self.add(Tex("Flash the dot below:"), dot)
self.play(Flash(dot))
self.wait()
@ -219,20 +221,20 @@ class Flash(AnimationGroup):
def __init__(
self,
point: np.ndarray | Mobject,
point: Point3DLike | Mobject,
line_length: float = 0.2,
num_lines: int = 12,
flash_radius: float = 0.1,
line_stroke_width: int = 3,
color: str = YELLOW,
color: ParsableManimColor = PURE_YELLOW,
time_width: float = 1,
run_time: float = 1.0,
**kwargs,
) -> None:
**kwargs: Any,
):
if isinstance(point, Mobject):
self.point = point.get_center()
self.point: Point3D = point.get_center()
else:
self.point = point
self.point = np.asarray(point)
self.color = color
self.line_length = line_length
self.num_lines = num_lines
@ -303,7 +305,9 @@ class ShowPassingFlash(ShowPartial):
"""
def __init__(self, mobject: VMobject, time_width: float = 0.1, **kwargs) -> None:
def __init__(
self, mobject: VMobject, time_width: float = 0.1, **kwargs: Any
) -> None:
self.time_width = time_width
super().__init__(mobject, remover=True, introducer=True, **kwargs)
@ -322,7 +326,14 @@ class ShowPassingFlash(ShowPartial):
class ShowPassingFlashWithThinningStrokeWidth(AnimationGroup):
def __init__(self, vmobject, n_segments=10, time_width=0.1, remover=True, **kwargs):
def __init__(
self,
vmobject: VMobject,
n_segments: int = 10,
time_width: float = 0.1,
remover: bool = True,
**kwargs: Any,
):
self.n_segments = n_segments
self.time_width = time_width
self.remover = remover
@ -338,6 +349,7 @@ class ShowPassingFlashWithThinningStrokeWidth(AnimationGroup):
for stroke_width, time_width in zip(
np.linspace(0, max_stroke_width, self.n_segments),
np.linspace(max_time_width, 0, self.n_segments),
strict=True,
)
),
)
@ -389,19 +401,19 @@ class ApplyWave(Homotopy):
def __init__(
self,
mobject: Mobject,
direction: np.ndarray = UP,
direction: Vector3DLike = UP,
amplitude: float = 0.2,
wave_func: Callable[[float], float] = smooth,
wave_func: RateFunction = smooth,
time_width: float = 1,
ripples: int = 1,
run_time: float = 2,
**kwargs,
) -> None:
**kwargs: Any,
):
x_min = mobject.get_left()[0]
x_max = mobject.get_right()[0]
vect = amplitude * normalize(direction)
def wave(t):
def wave(t: float) -> float:
# Creates a wave with n ripples from a simple rate_func
# This wave is build up as follows:
# The time is split into 2*ripples phases. In every phase the amplitude
@ -467,7 +479,8 @@ class ApplyWave(Homotopy):
relative_x = inverse_interpolate(x_min, x_max, x)
wave_phase = inverse_interpolate(lower, upper, relative_x)
nudge = wave(wave_phase) * vect
return np.array([x, y, z]) + nudge
return_value: tuple[float, float, float] = np.array([x, y, z]) + nudge
return return_value
super().__init__(homotopy, mobject, run_time=run_time, **kwargs)
@ -511,24 +524,28 @@ class Wiggle(Animation):
scale_value: float = 1.1,
rotation_angle: float = 0.01 * TAU,
n_wiggles: int = 6,
scale_about_point: np.ndarray | None = None,
rotate_about_point: np.ndarray | None = None,
scale_about_point: Point3DLike | None = None,
rotate_about_point: Point3DLike | None = None,
run_time: float = 2,
**kwargs,
) -> None:
**kwargs: Any,
):
self.scale_value = scale_value
self.rotation_angle = rotation_angle
self.n_wiggles = n_wiggles
self.scale_about_point = scale_about_point
if scale_about_point is not None:
self.scale_about_point = np.array(scale_about_point)
self.rotate_about_point = rotate_about_point
if rotate_about_point is not None:
self.rotate_about_point = np.array(rotate_about_point)
super().__init__(mobject, run_time=run_time, **kwargs)
def get_scale_about_point(self) -> np.ndarray:
def get_scale_about_point(self) -> Point3D:
if self.scale_about_point is None:
return self.mobject.get_center()
return self.scale_about_point
def get_rotate_about_point(self) -> np.ndarray:
def get_rotate_about_point(self) -> Point3D:
if self.rotate_about_point is None:
return self.mobject.get_center()
return self.rotate_about_point
@ -538,7 +555,7 @@ class Wiggle(Animation):
submobject: Mobject,
starting_submobject: Mobject,
alpha: float,
) -> None:
) -> Self:
submobject.points[:, :] = starting_submobject.points
submobject.scale(
interpolate(1, self.scale_value, there_and_back(alpha)),
@ -548,6 +565,7 @@ class Wiggle(Animation):
wiggle(alpha, self.n_wiggles) * self.rotation_angle,
about_point=self.get_rotate_about_point(),
)
return self
class Circumscribe(Succession):
@ -595,18 +613,18 @@ class Circumscribe(Succession):
def __init__(
self,
mobject: Mobject,
shape: type = Rectangle,
fade_in=False,
fade_out=False,
time_width=0.3,
shape: type[Rectangle] | type[Circle] = Rectangle,
fade_in: bool = False,
fade_out: bool = False,
time_width: float = 0.3,
buff: float = SMALL_BUFF,
color: ParsableManimColor = YELLOW,
run_time=1,
stroke_width=DEFAULT_STROKE_WIDTH,
**kwargs,
color: ParsableManimColor = PURE_YELLOW,
run_time: float = 1,
stroke_width: float = DEFAULT_STROKE_WIDTH,
**kwargs: Any,
):
if shape is Rectangle:
frame = SurroundingRectangle(
frame: SurroundingRectangle | Circle = SurroundingRectangle(
mobject,
color=color,
buff=buff,
@ -685,7 +703,7 @@ class Blink(Succession):
time_off: float = 0.5,
blinks: int = 1,
hide_at_end: bool = False,
**kwargs,
**kwargs: Any,
):
animations = [
UpdateFromFunc(

View file

@ -10,7 +10,8 @@ __all__ = [
"MoveAlongPath",
]
from typing import TYPE_CHECKING, Any, Callable
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
import numpy as np
@ -18,7 +19,13 @@ from ..animation.animation import Animation
from ..utils.rate_functions import linear
if TYPE_CHECKING:
from ..mobject.mobject import Mobject, VMobject
from typing import Self
from manim.mobject.types.vectorized_mobject import VMobject
from manim.typing import MappingFunction, Point3D
from manim.utils.rate_functions import RateFunction
from ..mobject.mobject import Mobject
class Homotopy(Animation):
@ -72,27 +79,33 @@ class Homotopy(Animation):
mobject: Mobject,
run_time: float = 3,
apply_function_kwargs: dict[str, Any] | None = None,
**kwargs,
) -> None:
**kwargs: Any,
):
self.homotopy = homotopy
self.apply_function_kwargs = (
apply_function_kwargs if apply_function_kwargs is not None else {}
)
super().__init__(mobject, run_time=run_time, **kwargs)
def function_at_time_t(self, t: float) -> tuple[float, float, float]:
return lambda p: self.homotopy(*p, t)
def function_at_time_t(self, t: float) -> MappingFunction:
def mapping_function(p: Point3D) -> Point3D:
x, y, z = p
return np.array(self.homotopy(x, y, z, t))
return mapping_function
def interpolate_submobject(
self,
submobject: Mobject,
starting_submobject: Mobject,
alpha: float,
) -> None:
) -> Self:
submobject.points = starting_submobject.points
submobject.apply_function(
self.function_at_time_t(alpha), **self.apply_function_kwargs
self.function_at_time_t(alpha),
**self.apply_function_kwargs,
)
return self
class SmoothedVectorizedHomotopy(Homotopy):
@ -101,15 +114,20 @@ class SmoothedVectorizedHomotopy(Homotopy):
submobject: Mobject,
starting_submobject: Mobject,
alpha: float,
) -> None:
) -> Self:
assert isinstance(submobject, VMobject)
super().interpolate_submobject(submobject, starting_submobject, alpha)
submobject.make_smooth()
return self
class ComplexHomotopy(Homotopy):
def __init__(
self, complex_homotopy: Callable[[complex], float], mobject: Mobject, **kwargs
) -> None:
self,
complex_homotopy: Callable[[complex, float], float],
mobject: Mobject,
**kwargs: Any,
):
"""Complex Homotopy a function Cx[0, 1] to C"""
def homotopy(
@ -131,9 +149,9 @@ class PhaseFlow(Animation):
mobject: Mobject,
virtual_time: float = 1,
suspend_mobject_updating: bool = False,
rate_func: Callable[[float], float] = linear,
**kwargs,
) -> None:
rate_func: RateFunction = linear,
**kwargs: Any,
):
self.virtual_time = virtual_time
self.function = function
super().__init__(
@ -149,7 +167,7 @@ class PhaseFlow(Animation):
self.rate_func(alpha) - self.rate_func(self.last_alpha)
)
self.mobject.apply_function(lambda p: p + dt * self.function(p))
self.last_alpha = alpha
self.last_alpha: float = alpha
class MoveAlongPath(Animation):
@ -172,8 +190,8 @@ class MoveAlongPath(Animation):
mobject: Mobject,
path: VMobject,
suspend_mobject_updating: bool = False,
**kwargs,
) -> None:
**kwargs: Any,
):
self.path = path
super().__init__(
mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs

View file

@ -5,9 +5,8 @@ from __future__ import annotations
__all__ = ["ChangingDecimal", "ChangeDecimalToValue"]
import typing
from typing_extensions import Any
from collections.abc import Callable
from typing import Any
from manim.mobject.text.numbers import DecimalNumber
@ -16,10 +15,45 @@ from ..utils.bezier import interpolate
class ChangingDecimal(Animation):
"""Animate a :class:`~.DecimalNumber` to values specified by a user-supplied function.
Parameters
----------
decimal_mob
The :class:`~.DecimalNumber` instance to animate.
number_update_func
A function that returns the number to display at each point in the animation.
suspend_mobject_updating
If ``True``, the mobject is not updated outside this animation.
Raises
------
TypeError
If ``decimal_mob`` is not an instance of :class:`~.DecimalNumber`.
Examples
--------
.. manim:: ChangingDecimalExample
class ChangingDecimalExample(Scene):
def construct(self):
number = DecimalNumber(0)
self.add(number)
self.play(
ChangingDecimal(
number,
lambda a: 5 * a,
run_time=3
)
)
self.wait()
"""
def __init__(
self,
decimal_mob: DecimalNumber,
number_update_func: typing.Callable[[float], float],
number_update_func: Callable[[float], float],
suspend_mobject_updating: bool = False,
**kwargs: Any,
) -> None:
@ -38,6 +72,28 @@ class ChangingDecimal(Animation):
class ChangeDecimalToValue(ChangingDecimal):
"""Animate a :class:`~.DecimalNumber` to a target value using linear interpolation.
Parameters
----------
decimal_mob
The :class:`~.DecimalNumber` instance to animate.
target_number
The target value to transition to.
Examples
--------
.. manim:: ChangeDecimalToValueExample
class ChangeDecimalToValueExample(Scene):
def construct(self):
number = DecimalNumber(0)
self.add(number)
self.play(ChangeDecimalToValue(number, 10, run_time=3))
self.wait()
"""
def __init__(
self, decimal_mob: DecimalNumber, target_number: int, **kwargs: Any
) -> None:

View file

@ -4,11 +4,8 @@ from __future__ import annotations
__all__ = ["Rotating", "Rotate"]
from collections.abc import Sequence
from typing import TYPE_CHECKING, Callable
import numpy as np
from typing_extensions import Any
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
from ..animation.animation import Animation
from ..animation.transform import Transform
@ -17,6 +14,8 @@ from ..utils.rate_functions import linear
if TYPE_CHECKING:
from ..mobject.mobject import Mobject
from ..mobject.opengl.opengl_mobject import OpenGLMobject
from ..typing import Point3DLike, Vector3DLike
class Rotating(Animation):
@ -90,9 +89,9 @@ class Rotating(Animation):
self,
mobject: Mobject,
angle: float = TAU,
axis: np.ndarray = OUT,
about_point: np.ndarray | None = None,
about_edge: np.ndarray | None = None,
axis: Vector3DLike = OUT,
about_point: Point3DLike | None = None,
about_edge: Vector3DLike | None = None,
run_time: float = 5,
rate_func: Callable[[float], float] = linear,
**kwargs: Any,
@ -157,9 +156,9 @@ class Rotate(Transform):
self,
mobject: Mobject,
angle: float = PI,
axis: np.ndarray = OUT,
about_point: Sequence[float] | None = None,
about_edge: Sequence[float] | None = None,
axis: Vector3DLike = OUT,
about_point: Point3DLike | None = None,
about_edge: Vector3DLike | None = None,
**kwargs: Any,
) -> None:
if "path_arc" not in kwargs:
@ -174,7 +173,7 @@ class Rotate(Transform):
self.about_point = mobject.get_center()
super().__init__(mobject, path_arc_centers=self.about_point, **kwargs)
def create_target(self) -> Mobject:
def create_target(self) -> Mobject | OpenGLMobject:
target = self.mobject.copy()
target.rotate(
self.angle,

View file

@ -4,7 +4,8 @@ from __future__ import annotations
import inspect
import types
from typing import TYPE_CHECKING, Callable
from collections.abc import Callable
from typing import TYPE_CHECKING
from numpy import piecewise

View file

@ -28,11 +28,12 @@ __all__ = [
import inspect
import types
from collections.abc import Iterable, Sequence
from typing import TYPE_CHECKING, Any, Callable
from collections.abc import Callable, Iterable, Sequence
from typing import TYPE_CHECKING, Any
import numpy as np
from manim.data_structures import MethodWithArgs
from manim.mobject.opengl.opengl_mobject import OpenGLGroup, OpenGLMobject
from .. import config
@ -45,11 +46,13 @@ from ..constants import (
RendererType,
)
from ..mobject.mobject import Group, Mobject
from ..mobject.types.vectorized_mobject import VGroup
from ..utils.paths import path_along_arc, path_along_circles
from ..utils.rate_functions import smooth, squish_rate_func
if TYPE_CHECKING:
from ..scene.scene import Scene
from ..typing import Point3DLike, Point3DLike_Array
class Transform(Animation):
@ -136,12 +139,12 @@ class Transform(Animation):
path_func: Callable | None = None,
path_arc: float = 0,
path_arc_axis: np.ndarray = OUT,
path_arc_centers: np.ndarray = None,
path_arc_centers: Point3DLike | Point3DLike_Array | None = None,
replace_mobject_with_target_in_scene: bool = False,
**kwargs,
) -> None:
self.path_arc_axis: np.ndarray = path_arc_axis
self.path_arc_centers: np.ndarray = path_arc_centers
self.path_arc_centers: Point3DLike | Point3DLike_Array | None = path_arc_centers
self.path_arc: float = path_arc
# path_func is a property a few lines below so it doesn't need to be set in any case
@ -208,7 +211,7 @@ class Transform(Animation):
self.mobject.align_data(self.target_copy)
super().begin()
def create_target(self) -> Mobject:
def create_target(self) -> Mobject | OpenGLMobject:
# Has no meaningful effect here, but may be useful
# in subclasses
return self.target_mobject
@ -233,8 +236,8 @@ class Transform(Animation):
self.target_copy,
]
if config.renderer == RendererType.OPENGL:
return zip(*(mob.get_family() for mob in mobs))
return zip(*(mob.family_members_with_points() for mob in mobs))
return zip(*(mob.get_family() for mob in mobs), strict=True)
return zip(*(mob.family_members_with_points() for mob in mobs), strict=True)
def interpolate_submobject(
self,
@ -302,7 +305,7 @@ class ReplacementTransform(Transform):
class TransformFromCopy(Transform):
"""Performs a reversed Transform"""
"""Preserves a copy of the original VMobject and transforms only it's copy to the target VMobject"""
def __init__(self, mobject: Mobject, target_mobject: Mobject, **kwargs) -> None:
super().__init__(target_mobject, mobject, **kwargs)
@ -438,13 +441,13 @@ class MoveToTarget(Transform):
class _MethodAnimation(MoveToTarget):
def __init__(self, mobject, methods):
def __init__(self, mobject: Mobject, methods: list[MethodWithArgs]) -> None:
self.methods = methods
super().__init__(mobject)
def finish(self) -> None:
for method, method_args, method_kwargs in self.methods:
method.__func__(self.mobject, *method_args, **method_kwargs)
for item in self.methods:
item.method.__func__(self.mobject, *item.args, **item.kwargs)
super().finish()
@ -733,19 +736,36 @@ class CyclicReplace(Transform):
def __init__(
self, *mobjects: Mobject, path_arc: float = 90 * DEGREES, **kwargs
) -> None:
self.group = Group(*mobjects)
if len(mobjects) == 1 and isinstance(mobjects[0], (Group, VGroup)):
self.group = mobjects[0]
else:
self.group = Group(*mobjects)
super().__init__(self.group, path_arc=path_arc, **kwargs)
def create_target(self) -> Group:
def create_target(self) -> Group | VGroup:
target = self.group.copy()
cycled_targets = [target[-1], *target[:-1]]
for m1, m2 in zip(cycled_targets, self.group):
for m1, m2 in zip(cycled_targets, self.group, strict=True):
m1.move_to(m2)
return target
class Swap(CyclicReplace):
pass # Renaming, more understandable for two entries
"""Another name for :class:`~.CyclicReplace`, which is more understandable for two entries.
Examples
--------
.. manim :: SwapExample
class SwapExample(Scene):
def construct(self):
text_a = Text("A").move_to(LEFT)
text_b = Text("B").move_to(RIGHT)
text_group = Group(text_a, text_b)
self.play(FadeIn(text_group))
self.play(Swap(text_group))
self.wait()
"""
# TODO, this may be deprecated...worth reimplementing?
@ -833,7 +853,14 @@ class FadeTransform(Transform):
"""
def __init__(self, mobject, target_mobject, stretch=True, dim_to_match=1, **kwargs):
def __init__(
self,
mobject: Mobject,
target_mobject: Mobject,
stretch: bool = True,
dim_to_match: int = 1,
**kwargs: Any,
):
self.to_add_on_completion = target_mobject
self.stretch = stretch
self.dim_to_match = dim_to_match
@ -927,5 +954,5 @@ class FadeTransformPieces(FadeTransform):
"""Replaces the source submobjects by the target submobjects and sets
the opacity to 0.
"""
for sm0, sm1 in zip(source.get_family(), target.get_family()):
for sm0, sm1 in zip(source.get_family(), target.get_family(), strict=True):
super().ghost_to(sm0, sm1)

View file

@ -4,12 +4,13 @@ from __future__ import annotations
__all__ = ["TransformMatchingShapes", "TransformMatchingTex"]
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
import numpy as np
from manim.mobject.opengl.opengl_mobject import OpenGLGroup, OpenGLMobject
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup, OpenGLVMobject
from manim.mobject.text.tex_mobject import MathTexPart
from .._config import config
from ..constants import RendererType
@ -74,10 +75,10 @@ class TransformMatchingAbstractBase(AnimationGroup):
transform_mismatches: bool = False,
fade_transform_mismatches: bool = False,
key_map: dict | None = None,
**kwargs,
**kwargs: Any,
):
if isinstance(mobject, OpenGLVMobject):
group_type = OpenGLVGroup
group_type: type[OpenGLVGroup | OpenGLGroup | VGroup | Group] = OpenGLVGroup
elif isinstance(mobject, OpenGLMobject):
group_type = OpenGLGroup
elif isinstance(mobject, VMobject):
@ -141,7 +142,7 @@ class TransformMatchingAbstractBase(AnimationGroup):
self.to_add = target_mobject
def get_shape_map(self, mobject: Mobject) -> dict:
shape_map = {}
shape_map: dict[int | str, VGroup | OpenGLVGroup] = {}
for sm in self.get_mobject_parts(mobject):
key = self.get_mobject_key(sm)
if key not in shape_map:
@ -149,23 +150,25 @@ class TransformMatchingAbstractBase(AnimationGroup):
shape_map[key] = OpenGLVGroup()
else:
shape_map[key] = VGroup()
shape_map[key].add(sm)
# error: Argument 1 to "add" of "OpenGLVGroup" has incompatible type "Mobject"; expected "OpenGLVMobject" [arg-type]
shape_map[key].add(sm) # type: ignore[arg-type]
return shape_map
def clean_up_from_scene(self, scene: Scene) -> None:
# Interpolate all animations back to 0 to ensure source mobjects remain unchanged.
for anim in self.animations:
anim.interpolate(0)
scene.remove(self.mobject)
# error: Argument 1 to "remove" of "Scene" has incompatible type "OpenGLMobject"; expected "Mobject" [arg-type]
scene.remove(self.mobject) # type: ignore[arg-type]
scene.remove(*self.to_remove)
scene.add(self.to_add)
@staticmethod
def get_mobject_parts(mobject: Mobject):
def get_mobject_parts(mobject: Mobject) -> list[Mobject]:
raise NotImplementedError("To be implemented in subclass.")
@staticmethod
def get_mobject_key(mobject: Mobject):
def get_mobject_key(mobject: Mobject) -> int | str:
raise NotImplementedError("To be implemented in subclass.")
@ -205,7 +208,7 @@ class TransformMatchingShapes(TransformMatchingAbstractBase):
transform_mismatches: bool = False,
fade_transform_mismatches: bool = False,
key_map: dict | None = None,
**kwargs,
**kwargs: Any,
):
super().__init__(
mobject,
@ -225,7 +228,8 @@ class TransformMatchingShapes(TransformMatchingAbstractBase):
mobject.save_state()
mobject.center()
mobject.set(height=1)
result = hash(np.round(mobject.points, 3).tobytes())
rounded_points = np.round(mobject.points, 3) + 0.0
result = hash(rounded_points.tobytes())
mobject.restore()
return result
@ -268,7 +272,7 @@ class TransformMatchingTex(TransformMatchingAbstractBase):
transform_mismatches: bool = False,
fade_transform_mismatches: bool = False,
key_map: dict | None = None,
**kwargs,
**kwargs: Any,
):
super().__init__(
mobject,
@ -293,4 +297,5 @@ class TransformMatchingTex(TransformMatchingAbstractBase):
@staticmethod
def get_mobject_key(mobject: Mobject) -> str:
assert isinstance(mobject, MathTexPart)
return mobject.tex_string

View file

@ -15,7 +15,8 @@ __all__ = [
import inspect
from typing import TYPE_CHECKING, Callable
from collections.abc import Callable
from typing import TYPE_CHECKING, TypeVar
import numpy as np
@ -28,6 +29,9 @@ if TYPE_CHECKING:
from manim.animation.animation import Animation
M = TypeVar("M", bound=Mobject)
def assert_is_mobject_method(method: Callable) -> None:
assert inspect.ismethod(method)
mobject = method.__self__
@ -42,7 +46,7 @@ def always(method: Callable, *args, **kwargs) -> Mobject:
return mobject
def f_always(method: Callable[[Mobject], None], *arg_generators, **kwargs) -> Mobject:
def f_always(method: Callable[[M], None], *arg_generators, **kwargs) -> M:
"""
More functional version of always, where instead
of taking in args, it takes in functions which output
@ -60,7 +64,7 @@ def f_always(method: Callable[[Mobject], None], *arg_generators, **kwargs) -> Mo
return mobject
def always_redraw(func: Callable[[], Mobject]) -> Mobject:
def always_redraw(func: Callable[[], M]) -> M:
"""Redraw the mobject constructed by a function every frame.
This function returns a mobject with an attached updater that
@ -106,8 +110,8 @@ def always_redraw(func: Callable[[], Mobject]) -> Mobject:
def always_shift(
mobject: Mobject, direction: np.ndarray[np.float64] = RIGHT, rate: float = 0.1
) -> Mobject:
mobject: M, direction: np.ndarray[np.float64] = RIGHT, rate: float = 0.1
) -> M:
"""A mobject which is continuously shifted along some direction
at a certain rate.
@ -144,7 +148,7 @@ def always_shift(
return mobject
def always_rotate(mobject: Mobject, rate: float = 20 * DEGREES, **kwargs) -> Mobject:
def always_rotate(mobject: M, rate: float = 20 * DEGREES, **kwargs) -> M:
"""A mobject which is continuously rotated at a certain rate.
Parameters
@ -213,6 +217,16 @@ def turn_animation_into_updater(
def update(m: Mobject, dt: float):
if animation.total_time >= 0:
run_time = animation.get_run_time()
# handle zero/negative runtime safely
if run_time <= 0:
# instantly snap to final state once, then remove updater
animation.interpolate(1)
animation.update_mobjects(dt)
animation.finish()
m.remove_updater(update)
return
time_ratio = animation.total_time / run_time
if cycle:
alpha = time_ratio % 1

View file

@ -6,14 +6,12 @@ __all__ = ["UpdateFromFunc", "UpdateFromAlphaFunc", "MaintainPositionRelativeTo"
import operator as op
import typing
from typing import Callable
from typing_extensions import Any
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
from manim.animation.animation import Animation
if typing.TYPE_CHECKING:
if TYPE_CHECKING:
from manim.mobject.mobject import Mobject

View file

@ -8,26 +8,39 @@ import copy
import itertools as it
import operator as op
import pathlib
from collections.abc import Iterable
from collections.abc import Callable, Iterable
from functools import reduce
from typing import Any, Callable
from typing import TYPE_CHECKING, Any, Self
import cairo
import numpy as np
from PIL import Image
from scipy.spatial.distance import pdist
from .. import config, logger
from ..constants import *
from ..mobject.mobject import Mobject
from ..mobject.types.image_mobject import AbstractImageMobject
from ..mobject.types.point_cloud_mobject import PMobject
from ..mobject.types.vectorized_mobject import VMobject
from ..utils.color import ManimColor, ParsableManimColor, color_to_int_rgba
from ..utils.family import extract_mobject_family_members
from ..utils.images import get_full_raster_image_path
from ..utils.iterables import list_difference_update
from ..utils.space_ops import angle_of_vector
from manim._config import config, logger
from manim.constants import *
from manim.mobject.mobject import Mobject
from manim.mobject.types.point_cloud_mobject import PMobject
from manim.mobject.types.vectorized_mobject import VMobject
from manim.utils.color import ManimColor, ParsableManimColor, color_to_int_rgba
from manim.utils.family import extract_mobject_family_members
from manim.utils.images import get_full_raster_image_path
from manim.utils.iterables import list_difference_update
from manim.utils.space_ops import cross2d
if TYPE_CHECKING:
import numpy.typing as npt
from manim.mobject.types.image_mobject import AbstractImageMobject
from manim.typing import (
FloatRGBA_Array,
FloatRGBALike_Array,
ManimFloat,
ManimInt,
PixelArray,
Point3D,
Point3D_Array,
)
LINE_JOIN_MAP = {
LineJointType.AUTO: None, # TODO: this could be improved
@ -70,13 +83,13 @@ class Camera:
def __init__(
self,
background_image: str | None = None,
frame_center: np.ndarray = ORIGIN,
frame_center: Point3D = ORIGIN,
image_mode: str = "RGBA",
n_channels: int = 4,
pixel_array_dtype: str = "uint8",
cairo_line_width_multiple: float = 0.01,
use_z_index: bool = True,
background: np.ndarray | None = None,
background: PixelArray | None = None,
pixel_height: int | None = None,
pixel_width: int | None = None,
frame_height: float | None = None,
@ -84,8 +97,8 @@ class Camera:
frame_rate: float | None = None,
background_color: ParsableManimColor | None = None,
background_opacity: float | None = None,
**kwargs,
):
**kwargs: Any,
) -> None:
self.background_image = background_image
self.frame_center = frame_center
self.image_mode = image_mode
@ -94,6 +107,9 @@ class Camera:
self.cairo_line_width_multiple = cairo_line_width_multiple
self.use_z_index = use_z_index
self.background = background
self.background_colored_vmobject_displayer: (
BackgroundColoredVMobjectDisplayer | None
) = None
if pixel_height is None:
pixel_height = config["pixel_height"]
@ -116,11 +132,13 @@ class Camera:
self.frame_rate = frame_rate
if background_color is None:
self._background_color = ManimColor.parse(config["background_color"])
self._background_color: ManimColor = ManimColor.parse(
config["background_color"]
)
else:
self._background_color = ManimColor.parse(background_color)
if background_opacity is None:
self._background_opacity = config["background_opacity"]
self._background_opacity: float = config["background_opacity"]
else:
self._background_opacity = background_opacity
@ -129,7 +147,7 @@ class Camera:
self.max_allowable_norm = config["frame_width"]
self.rgb_max_val = np.iinfo(self.pixel_array_dtype).max
self.pixel_array_to_cairo_context = {}
self.pixel_array_to_cairo_context: dict[int, cairo.Context] = {}
# Contains the correct method to process a list of Mobjects of the
# corresponding class. If a Mobject is not an instance of a class in
@ -140,7 +158,7 @@ class Camera:
self.resize_frame_shape()
self.reset()
def __deepcopy__(self, memo):
def __deepcopy__(self, memo: Any) -> Camera:
# This is to address a strange bug where deepcopying
# will result in a segfault, which is somehow related
# to the aggdraw library
@ -148,24 +166,26 @@ class Camera:
return copy.copy(self)
@property
def background_color(self):
def background_color(self) -> ManimColor:
return self._background_color
@background_color.setter
def background_color(self, color):
def background_color(self, color: ManimColor) -> None:
self._background_color = color
self.init_background()
@property
def background_opacity(self):
def background_opacity(self) -> float:
return self._background_opacity
@background_opacity.setter
def background_opacity(self, alpha):
def background_opacity(self, alpha: float) -> None:
self._background_opacity = alpha
self.init_background()
def type_or_raise(self, mobject: Mobject):
def type_or_raise(
self, mobject: Mobject
) -> type[VMobject] | type[PMobject] | type[AbstractImageMobject] | type[Mobject]:
"""Return the type of mobject, if it is a type that can be rendered.
If `mobject` is an instance of a class that inherits from a class that
@ -192,10 +212,14 @@ class Camera:
:exc:`TypeError`
When mobject is not an instance of a class that can be rendered.
"""
self.display_funcs = {
VMobject: self.display_multiple_vectorized_mobjects,
PMobject: self.display_multiple_point_cloud_mobjects,
AbstractImageMobject: self.display_multiple_image_mobjects,
from ..mobject.types.image_mobject import AbstractImageMobject
self.display_funcs: dict[
type[Mobject], Callable[[list[Mobject], PixelArray], Any]
] = {
VMobject: self.display_multiple_vectorized_mobjects, # type: ignore[dict-item]
PMobject: self.display_multiple_point_cloud_mobjects, # type: ignore[dict-item]
AbstractImageMobject: self.display_multiple_image_mobjects, # type: ignore[dict-item]
Mobject: lambda batch, pa: batch, # Do nothing
}
# We have to check each type in turn because we are dealing with
@ -206,7 +230,7 @@ class Camera:
return _type
raise TypeError(f"Displaying an object of class {_type} is not supported")
def reset_pixel_shape(self, new_height: float, new_width: float):
def reset_pixel_shape(self, new_height: float, new_width: float) -> None:
"""This method resets the height and width
of a single pixel to the passed new_height and new_width.
@ -223,7 +247,7 @@ class Camera:
self.resize_frame_shape()
self.reset()
def resize_frame_shape(self, fixed_dimension: int = 0):
def resize_frame_shape(self, fixed_dimension: int = 0) -> None:
"""
Changes frame_shape to match the aspect ratio
of the pixels, where fixed_dimension determines
@ -248,7 +272,7 @@ class Camera:
self.frame_height = frame_height
self.frame_width = frame_width
def init_background(self):
def init_background(self) -> None:
"""Initialize the background.
If self.background_image is the path of an image
the image is set as background; else, the default
@ -274,7 +298,9 @@ class Camera:
)
self.background[:, :] = background_rgba
def get_image(self, pixel_array: np.ndarray | list | tuple | None = None):
def get_image(
self, pixel_array: PixelArray | list | tuple | None = None
) -> Image.Image:
"""Returns an image from the passed
pixel array, or from the current frame
if the passed pixel array is none.
@ -286,7 +312,7 @@ class Camera:
Returns
-------
PIL.Image
PIL.Image.Image
The PIL image of the array.
"""
if pixel_array is None:
@ -294,8 +320,8 @@ class Camera:
return Image.fromarray(pixel_array, mode=self.image_mode)
def convert_pixel_array(
self, pixel_array: np.ndarray | list | tuple, convert_from_floats: bool = False
):
self, pixel_array: PixelArray | list | tuple, convert_from_floats: bool = False
) -> PixelArray:
"""Converts a pixel array from values that have floats in then
to proper RGB values.
@ -321,8 +347,8 @@ class Camera:
return retval
def set_pixel_array(
self, pixel_array: np.ndarray | list | tuple, convert_from_floats: bool = False
):
self, pixel_array: PixelArray | list | tuple, convert_from_floats: bool = False
) -> None:
"""Sets the pixel array of the camera to the passed pixel array.
Parameters
@ -332,19 +358,21 @@ class Camera:
convert_from_floats
Whether or not to convert float values to proper RGB values, by default False
"""
converted_array = self.convert_pixel_array(pixel_array, convert_from_floats)
converted_array: PixelArray = self.convert_pixel_array(
pixel_array, convert_from_floats
)
if not (
hasattr(self, "pixel_array")
and self.pixel_array.shape == converted_array.shape
):
self.pixel_array = converted_array
self.pixel_array: PixelArray = converted_array
else:
# Set in place
self.pixel_array[:, :, :] = converted_array[:, :, :]
def set_background(
self, pixel_array: np.ndarray | list | tuple, convert_from_floats: bool = False
):
self, pixel_array: PixelArray | list | tuple, convert_from_floats: bool = False
) -> None:
"""Sets the background to the passed pixel_array after converting
to valid RGB values.
@ -360,7 +388,7 @@ class Camera:
# TODO, this should live in utils, not as a method of Camera
def make_background_from_func(
self, coords_to_colors_func: Callable[[np.ndarray], np.ndarray]
):
) -> PixelArray:
"""
Makes a pixel array for the background by using coords_to_colors_func to determine each pixel's color. Each input
pixel's color. Each input to coords_to_colors_func is an (x, y) pair in space (in ordinary space coordinates; not
@ -386,7 +414,7 @@ class Camera:
def set_background_from_func(
self, coords_to_colors_func: Callable[[np.ndarray], np.ndarray]
):
) -> None:
"""
Sets the background to a pixel array using coords_to_colors_func to determine each pixel's color. Each input
pixel's color. Each input to coords_to_colors_func is an (x, y) pair in space (in ordinary space coordinates; not
@ -400,7 +428,7 @@ class Camera:
"""
self.set_background(self.make_background_from_func(coords_to_colors_func))
def reset(self):
def reset(self) -> Self:
"""Resets the camera's pixel array
to that of the background
@ -412,7 +440,7 @@ class Camera:
self.set_pixel_array(self.background)
return self
def set_frame_to_background(self, background):
def set_frame_to_background(self, background: PixelArray) -> None:
self.set_pixel_array(background)
####
@ -422,7 +450,7 @@ class Camera:
mobjects: Iterable[Mobject],
include_submobjects: bool = True,
excluded_mobjects: list | None = None,
):
) -> list[Mobject]:
"""Used to get the list of mobjects to display
with the camera.
@ -454,7 +482,7 @@ class Camera:
mobjects = list_difference_update(mobjects, all_excluded)
return list(mobjects)
def is_in_frame(self, mobject: Mobject):
def is_in_frame(self, mobject: Mobject) -> bool:
"""Checks whether the passed mobject is in
frame or not.
@ -481,7 +509,7 @@ class Camera:
],
)
def capture_mobject(self, mobject: Mobject, **kwargs: Any):
def capture_mobject(self, mobject: Mobject, **kwargs: Any) -> None:
"""Capture mobjects by storing it in :attr:`pixel_array`.
This is a single-mobject version of :meth:`capture_mobjects`.
@ -497,7 +525,7 @@ class Camera:
"""
return self.capture_mobjects([mobject], **kwargs)
def capture_mobjects(self, mobjects: Iterable[Mobject], **kwargs):
def capture_mobjects(self, mobjects: Iterable[Mobject], **kwargs: Any) -> None:
"""Capture mobjects by printing them on :attr:`pixel_array`.
This is the essential function that converts the contents of a Scene
@ -532,7 +560,7 @@ class Camera:
# NOTE: None of the methods below have been mentioned outside of their definitions. Their DocStrings are not as
# detailed as possible.
def get_cached_cairo_context(self, pixel_array: np.ndarray):
def get_cached_cairo_context(self, pixel_array: PixelArray) -> cairo.Context | None:
"""Returns the cached cairo context of the passed
pixel array if it exists, and None if it doesn't.
@ -548,7 +576,7 @@ class Camera:
"""
return self.pixel_array_to_cairo_context.get(id(pixel_array), None)
def cache_cairo_context(self, pixel_array: np.ndarray, ctx: cairo.Context):
def cache_cairo_context(self, pixel_array: PixelArray, ctx: cairo.Context) -> None:
"""Caches the passed Pixel array into a Cairo Context
Parameters
@ -560,7 +588,7 @@ class Camera:
"""
self.pixel_array_to_cairo_context[id(pixel_array)] = ctx
def get_cairo_context(self, pixel_array: np.ndarray):
def get_cairo_context(self, pixel_array: PixelArray) -> cairo.Context:
"""Returns the cairo context for a pixel array after
caching it to self.pixel_array_to_cairo_context
If that array has already been cached, it returns the
@ -585,7 +613,7 @@ class Camera:
fh = self.frame_height
fc = self.frame_center
surface = cairo.ImageSurface.create_for_data(
pixel_array,
pixel_array.data,
cairo.FORMAT_ARGB32,
pw,
ph,
@ -606,8 +634,8 @@ class Camera:
return ctx
def display_multiple_vectorized_mobjects(
self, vmobjects: list, pixel_array: np.ndarray
):
self, vmobjects: list[VMobject], pixel_array: PixelArray
) -> None:
"""Displays multiple VMobjects in the pixel_array
Parameters
@ -630,8 +658,8 @@ class Camera:
)
def display_multiple_non_background_colored_vmobjects(
self, vmobjects: list, pixel_array: np.ndarray
):
self, vmobjects: Iterable[VMobject], pixel_array: PixelArray
) -> None:
"""Displays multiple VMobjects in the cairo context, as long as they don't have
background colors.
@ -646,7 +674,7 @@ class Camera:
for vmobject in vmobjects:
self.display_vectorized(vmobject, ctx)
def display_vectorized(self, vmobject: VMobject, ctx: cairo.Context):
def display_vectorized(self, vmobject: VMobject, ctx: cairo.Context) -> Self:
"""Displays a VMobject in the cairo context
Parameters
@ -667,7 +695,7 @@ class Camera:
self.apply_stroke(ctx, vmobject)
return self
def set_cairo_context_path(self, ctx: cairo.Context, vmobject: VMobject):
def set_cairo_context_path(self, ctx: cairo.Context, vmobject: VMobject) -> Self:
"""Sets a path for the cairo context with the vmobject passed
Parameters
@ -686,7 +714,7 @@ class Camera:
# TODO, shouldn't this be handled in transform_points_pre_display?
# points = points - self.get_frame_center()
if len(points) == 0:
return
return self
ctx.new_path()
subpaths = vmobject.gen_subpaths_from_points_2d(points)
@ -702,8 +730,8 @@ class Camera:
return self
def set_cairo_context_color(
self, ctx: cairo.Context, rgbas: np.ndarray, vmobject: VMobject
):
self, ctx: cairo.Context, rgbas: FloatRGBALike_Array, vmobject: VMobject
) -> Self:
"""Sets the color of the cairo context
Parameters
@ -728,14 +756,13 @@ class Camera:
points = vmobject.get_gradient_start_and_end_points()
points = self.transform_points_pre_display(vmobject, points)
pat = cairo.LinearGradient(*it.chain(*(point[:2] for point in points)))
step = 1.0 / (len(rgbas) - 1)
offsets = np.arange(0, 1 + step, step)
for rgba, offset in zip(rgbas, offsets):
offsets = np.linspace(0, 1, len(rgbas))
for rgba, offset in zip(rgbas, offsets, strict=True):
pat.add_color_stop_rgba(offset, *rgba[2::-1], rgba[3])
ctx.set_source(pat)
return self
def apply_fill(self, ctx: cairo.Context, vmobject: VMobject):
def apply_fill(self, ctx: cairo.Context, vmobject: VMobject) -> Self:
"""Fills the cairo context
Parameters
@ -756,7 +783,7 @@ class Camera:
def apply_stroke(
self, ctx: cairo.Context, vmobject: VMobject, background: bool = False
):
) -> Self:
"""Applies a stroke to the VMobject in the cairo context.
Parameters
@ -795,7 +822,9 @@ class Camera:
ctx.stroke_preserve()
return self
def get_stroke_rgbas(self, vmobject: VMobject, background: bool = False):
def get_stroke_rgbas(
self, vmobject: VMobject, background: bool = False
) -> FloatRGBA_Array:
"""Gets the RGBA array for the stroke of the passed
VMobject.
@ -814,7 +843,7 @@ class Camera:
"""
return vmobject.get_stroke_rgbas(background)
def get_fill_rgbas(self, vmobject: VMobject):
def get_fill_rgbas(self, vmobject: VMobject) -> FloatRGBA_Array:
"""Returns the RGBA array of the fill of the passed VMobject
Parameters
@ -829,25 +858,27 @@ class Camera:
"""
return vmobject.get_fill_rgbas()
def get_background_colored_vmobject_displayer(self):
def get_background_colored_vmobject_displayer(
self,
) -> BackgroundColoredVMobjectDisplayer:
"""Returns the background_colored_vmobject_displayer
if it exists or makes one and returns it if not.
Returns
-------
BackGroundColoredVMobjectDisplayer
BackgroundColoredVMobjectDisplayer
Object that displays VMobjects that have the same color
as the background.
"""
# Quite wordy to type out a bunch
bcvd = "background_colored_vmobject_displayer"
if not hasattr(self, bcvd):
setattr(self, bcvd, BackgroundColoredVMobjectDisplayer(self))
return getattr(self, bcvd)
if self.background_colored_vmobject_displayer is None:
self.background_colored_vmobject_displayer = (
BackgroundColoredVMobjectDisplayer(self)
)
return self.background_colored_vmobject_displayer
def display_multiple_background_colored_vmobjects(
self, cvmobjects: list, pixel_array: np.ndarray
):
self, cvmobjects: Iterable[VMobject], pixel_array: PixelArray
) -> Self:
"""Displays multiple vmobjects that have the same color as the background.
Parameters
@ -873,8 +904,8 @@ class Camera:
# As a result, the other methods do not have as detailed docstrings as would be preferred.
def display_multiple_point_cloud_mobjects(
self, pmobjects: list, pixel_array: np.ndarray
):
self, pmobjects: Iterable[PMobject], pixel_array: PixelArray
) -> None:
"""Displays multiple PMobjects by modifying the passed pixel array.
Parameters
@ -896,11 +927,11 @@ class Camera:
def display_point_cloud(
self,
pmobject: PMobject,
points: list,
rgbas: np.ndarray,
points: Point3D_Array,
rgbas: FloatRGBA_Array,
thickness: float,
pixel_array: np.ndarray,
):
pixel_array: PixelArray,
) -> None:
"""Displays a PMobject by modifying the pixel array suitably.
TODO: Write a description for the rgbas argument.
@ -947,8 +978,10 @@ class Camera:
pixel_array[:, :] = new_pa.reshape((ph, pw, rgba_len))
def display_multiple_image_mobjects(
self, image_mobjects: list, pixel_array: np.ndarray
):
self,
image_mobjects: Iterable[AbstractImageMobject],
pixel_array: PixelArray,
) -> None:
"""Displays multiple image mobjects by modifying the passed pixel_array.
Parameters
@ -963,64 +996,119 @@ class Camera:
def display_image_mobject(
self, image_mobject: AbstractImageMobject, pixel_array: np.ndarray
):
"""Displays an ImageMobject by changing the pixel_array suitably.
) -> None:
"""Display an :class:`~.ImageMobject` by changing the ``pixel_array`` suitably.
Parameters
----------
image_mobject
The imageMobject to display
The :class:`~.ImageMobject` to display.
pixel_array
The Pixel array to put the imagemobject in.
The pixel array to put the :class:`~.ImageMobject` in.
"""
corner_coords = self.points_to_pixel_coords(image_mobject, image_mobject.points)
ul_coords, ur_coords, dl_coords, _ = corner_coords
right_vect = ur_coords - ul_coords
down_vect = dl_coords - ul_coords
center_coords = ul_coords + (right_vect + down_vect) / 2
sub_image = Image.fromarray(image_mobject.get_pixel_array(), mode="RGBA")
original_coords = np.array(
[
[0, 0],
[sub_image.width, 0],
[0, sub_image.height],
[sub_image.width, sub_image.height],
]
)
target_coords = self.points_to_subpixel_coords(
image_mobject, image_mobject.points
)
int_target_coords = target_coords.astype(np.int64)
# Reshape
pixel_width = max(int(pdist([ul_coords, ur_coords]).item()), 1)
pixel_height = max(int(pdist([ul_coords, dl_coords]).item()), 1)
sub_image = sub_image.resize(
(pixel_width, pixel_height),
# Temporarily translate target coords to upper left corner to calculate the
# smallest possible size for the target image.
shift_vector = np.array(
[
min(*[x for x, y in int_target_coords]),
min(*[y for x, y in int_target_coords]),
]
)
target_coords -= shift_vector
int_target_coords -= shift_vector
target_size = (
max(*[x for x, y in int_target_coords]),
max(*[y for x, y in int_target_coords]),
)
# Check that the quadrilateral of the transformed image can actually contain any
# pixels by checking that its height from the longest side is longer than 0.5 pixels.
# If it's not, do not render the image. Otherwise, the perspective transform
# coefficients below might have broken values due to the extreme distortion (for
# example, when the image is perpendicular to the camera).
ordered_vertices = [target_coords[i] for i in (0, 1, 3, 2)]
sides = [ordered_vertices[(i + 1) % 4] - ordered_vertices[i] for i in range(4)]
side_lengths_in_pixels = np.linalg.norm(sides, axis=1)
longest_side_index = np.argmax(side_lengths_in_pixels)
longest_side = sides[longest_side_index]
longest_side_length_in_pixels = side_lengths_in_pixels[longest_side_index]
if longest_side_length_in_pixels == 0:
return
previous_side = sides[(longest_side_index - 1) % 4]
next_side = sides[(longest_side_index - 1) % 4]
# height = area / base
h1 = abs(cross2d(longest_side, previous_side)) / longest_side_length_in_pixels
h2 = abs(cross2d(longest_side, next_side)) / longest_side_length_in_pixels
height_from_longest_side_in_pixels = max(h1, h2)
if height_from_longest_side_in_pixels < 0.5:
return
# Use PIL.Image.Image.transform() to apply a perspective transform to the image.
# The transform coefficients must be calculated. The following is adapted from:
# https://pc-pillow.readthedocs.io/en/latest/Image_class/Image_transform.html#transform-perspective-coefficients
# https://stackoverflow.com/questions/14177744/how-does-perspective-transformation-work-in-pil
# The derivation can be found here:
# https://web.archive.org/web/20150222120106/xenia.media.mit.edu/~cwren/interpolator/
homography_matrix = []
for (x, y), (X, Y) in zip(target_coords, original_coords, strict=True):
homography_matrix.append([x, y, 1, 0, 0, 0, -X * x, -X * y])
homography_matrix.append([0, 0, 0, x, y, 1, -Y * x, -Y * y])
A = np.array(homography_matrix, dtype=np.float64)
b = original_coords.reshape(8).astype(np.float64)
try:
transform_coefficients = np.linalg.solve(A, b)
except np.linalg.LinAlgError:
# The matrix A might be singular if three points are collinear.
# In this case, do nothing and return.
return
sub_image = sub_image.transform(
size=target_size, # Use the smallest possible size for speed.
method=Image.Transform.PERSPECTIVE,
data=transform_coefficients,
resample=image_mobject.resampling_algorithm,
)
# Rotate
angle = angle_of_vector(right_vect)
adjusted_angle = -int(360 * angle / TAU)
if adjusted_angle != 0:
sub_image = sub_image.rotate(
adjusted_angle,
resample=image_mobject.resampling_algorithm,
expand=1,
)
# TODO, there is no accounting for a shear...
# Paste into an image as large as the camera's pixel array
# Paste into an image as large as the camera's pixel array.
full_image = Image.fromarray(
np.zeros((self.pixel_height, self.pixel_width)),
mode="RGBA",
)
new_ul_coords = center_coords - np.array(sub_image.size) / 2
new_ul_coords = new_ul_coords.astype(int)
full_image.paste(
sub_image,
box=(
new_ul_coords[0],
new_ul_coords[1],
new_ul_coords[0] + sub_image.size[0],
new_ul_coords[1] + sub_image.size[1],
shift_vector[0],
shift_vector[1],
shift_vector[0] + target_size[0],
shift_vector[1] + target_size[1],
),
)
# Paint on top of existing pixel array
# Paint on top of existing pixel array.
self.overlay_PIL_image(pixel_array, full_image)
def overlay_rgba_array(self, pixel_array: np.ndarray, new_array: np.ndarray):
def overlay_rgba_array(
self, pixel_array: np.ndarray, new_array: np.ndarray
) -> None:
"""Overlays an RGBA array on top of the given Pixel array.
Parameters
@ -1032,7 +1120,7 @@ class Camera:
"""
self.overlay_PIL_image(pixel_array, self.get_image(new_array))
def overlay_PIL_image(self, pixel_array: np.ndarray, image: Image):
def overlay_PIL_image(self, pixel_array: np.ndarray, image: Image) -> None:
"""Overlays a PIL image on the passed pixel array.
Parameters
@ -1047,7 +1135,7 @@ class Camera:
dtype="uint8",
)
def adjust_out_of_range_points(self, points: np.ndarray):
def adjust_out_of_range_points(self, points: np.ndarray) -> np.ndarray:
"""If any of the points in the passed array are out of
the viable range, they are adjusted suitably.
@ -1078,9 +1166,9 @@ class Camera:
def transform_points_pre_display(
self,
mobject,
points,
): # TODO: Write more detailed docstrings for this method.
mobject: Mobject,
points: Point3D_Array,
) -> Point3D_Array: # TODO: Write more detailed docstrings for this method.
# NOTE: There seems to be an unused argument `mobject`.
# Subclasses (like ThreeDCamera) may want to
@ -1091,11 +1179,13 @@ class Camera:
points = np.zeros((1, 3))
return points
def points_to_pixel_coords(
def points_to_subpixel_coords(
self,
mobject,
points,
): # TODO: Write more detailed docstrings for this method.
mobject: Mobject,
points: Point3D_Array,
) -> npt.NDArray[
ManimFloat
]: # TODO: Write more detailed docstrings for this method.
points = self.transform_points_pre_display(mobject, points)
shifted_points = points - self.frame_center
@ -1113,9 +1203,16 @@ class Camera:
result[:, 0] = shifted_points[:, 0] * width_mult + width_add
result[:, 1] = shifted_points[:, 1] * height_mult + height_add
return result.astype("int")
return result
def on_screen_pixels(self, pixel_coords: np.ndarray):
def points_to_pixel_coords(
self,
mobject: Mobject,
points: Point3D_Array,
) -> npt.NDArray[ManimInt]: # TODO: Write more detailed docstrings for this method.
return self.points_to_subpixel_coords(mobject, points).astype(np.int64)
def on_screen_pixels(self, pixel_coords: np.ndarray) -> PixelArray:
"""Returns array of pixels that are on the screen from a given
array of pixel_coordinates
@ -1154,12 +1251,12 @@ class Camera:
the camera.
"""
# TODO: This seems...unsystematic
big_sum = op.add(config["pixel_height"], config["pixel_width"])
this_sum = op.add(self.pixel_height, self.pixel_width)
big_sum: float = op.add(config["pixel_height"], config["pixel_width"])
this_sum: float = op.add(self.pixel_height, self.pixel_width)
factor = big_sum / this_sum
return 1 + (thickness - 1) * factor
def get_thickening_nudges(self, thickness: float):
def get_thickening_nudges(self, thickness: float) -> PixelArray:
"""Determine a list of vectors used to nudge
two-dimensional pixel coordinates.
@ -1176,7 +1273,9 @@ class Camera:
_range = list(range(-thickness // 2 + 1, thickness // 2 + 1))
return np.array(list(it.product(_range, _range)))
def thickened_coordinates(self, pixel_coords: np.ndarray, thickness: float):
def thickened_coordinates(
self, pixel_coords: np.ndarray, thickness: float
) -> PixelArray:
"""Returns thickened coordinates for a passed array of pixel coords and
a thickness to thicken by.
@ -1198,7 +1297,7 @@ class Camera:
return pixel_coords.reshape((size // 2, 2))
# TODO, reimplement using cairo matrix
def get_coords_of_all_pixels(self):
def get_coords_of_all_pixels(self) -> PixelArray:
"""Returns the cartesian coordinates of each pixel.
Returns
@ -1246,20 +1345,20 @@ class BackgroundColoredVMobjectDisplayer:
def __init__(self, camera: Camera):
self.camera = camera
self.file_name_to_pixel_array_map = {}
self.file_name_to_pixel_array_map: dict[str, PixelArray] = {}
self.pixel_array = np.array(camera.pixel_array)
self.reset_pixel_array()
def reset_pixel_array(self):
def reset_pixel_array(self) -> None:
self.pixel_array[:, :] = 0
def resize_background_array(
self,
background_array: np.ndarray,
background_array: PixelArray,
new_width: float,
new_height: float,
mode: str = "RGBA",
):
) -> PixelArray:
"""Resizes the pixel array representing the background.
Parameters
@ -1284,8 +1383,8 @@ class BackgroundColoredVMobjectDisplayer:
return np.array(resized_image)
def resize_background_array_to_match(
self, background_array: np.ndarray, pixel_array: np.ndarray
):
self, background_array: PixelArray, pixel_array: PixelArray
) -> PixelArray:
"""Resizes the background array to match the passed pixel array.
Parameters
@ -1304,7 +1403,9 @@ class BackgroundColoredVMobjectDisplayer:
mode = "RGBA" if pixel_array.shape[2] == 4 else "RGB"
return self.resize_background_array(background_array, width, height, mode)
def get_background_array(self, image: Image.Image | pathlib.Path | str):
def get_background_array(
self, image: Image.Image | pathlib.Path | str
) -> PixelArray:
"""Gets the background array that has the passed file_name.
Parameters
@ -1333,7 +1434,7 @@ class BackgroundColoredVMobjectDisplayer:
self.file_name_to_pixel_array_map[image_key] = back_array
return back_array
def display(self, *cvmobjects: VMobject):
def display(self, *cvmobjects: VMobject) -> PixelArray | None:
"""Displays the colored VMobjects.
Parameters

View file

@ -1,4 +1,4 @@
"""A camera that allows mapping between objects."""
"""A camera module that supports spatial mapping between objects for distortion effects."""
from __future__ import annotations
@ -17,8 +17,16 @@ from ..utils.config_ops import DictAsObject
class MappingCamera(Camera):
"""Camera object that allows mapping
between objects.
"""Parameters
----------
mapping_func : callable
Function to map 3D points to new 3D points (identity by default).
min_num_curves : int
Minimum number of curves for VMobjects to avoid visual glitches.
allow_object_intrusion : bool
If True, modifies original mobjects; else works on copies.
kwargs : dict
Additional arguments passed to Camera base class.
"""
def __init__(
@ -34,12 +42,18 @@ class MappingCamera(Camera):
super().__init__(**kwargs)
def points_to_pixel_coords(self, mobject, points):
# Map points with custom function before converting to pixels
return super().points_to_pixel_coords(
mobject,
np.apply_along_axis(self.mapping_func, 1, points),
)
def capture_mobjects(self, mobjects, **kwargs):
"""Capture mobjects for rendering after applying the spatial mapping.
Copies mobjects unless intrusion is allowed, and ensures
vector objects have enough curves for smooth distortion.
"""
mobjects = self.get_mobjects_to_display(mobjects, **kwargs)
if self.allow_object_intrusion:
mobject_copies = mobjects
@ -67,6 +81,13 @@ class MappingCamera(Camera):
# TODO, the classes below should likely be deleted
class OldMultiCamera(Camera):
"""Parameters
----------
cameras_with_start_positions : tuple
Tuples of (Camera, (start_y, start_x)) indicating camera and
its pixel offset on the final frame.
"""
def __init__(self, *cameras_with_start_positions, **kwargs):
self.shifted_cameras = [
DictAsObject(
@ -125,6 +146,15 @@ class OldMultiCamera(Camera):
class SplitScreenCamera(OldMultiCamera):
"""Initializes a split screen camera setup with two side-by-side cameras.
Parameters
----------
left_camera : Camera
right_camera : Camera
kwargs : dict
"""
def __init__(self, left_camera, right_camera, **kwargs):
Camera.__init__(self, **kwargs) # to set attributes such as pixel_width
self.left_camera = left_camera

View file

@ -1,45 +1,48 @@
"""A camera able to move through a scene.
"""Defines the MovingCamera class, a camera that can pan and zoom through a scene.
.. SEEALSO::
:mod:`.moving_camera_scene`
"""
from __future__ import annotations
__all__ = ["MovingCamera"]
import numpy as np
from collections.abc import Iterable
from typing import Any, Literal, overload
from cairo import Context
from manim.typing import PixelArray, Point3D, Point3DLike
from .. import config
from ..camera.camera import Camera
from ..constants import DOWN, LEFT, RIGHT, UP
from ..mobject.frame import ScreenRectangle
from ..mobject.mobject import Mobject
from ..utils.color import WHITE
from ..mobject.mobject import Mobject, _AnimationBuilder
from ..utils.color import WHITE, ManimColor
class MovingCamera(Camera):
"""
Stays in line with the height, width and position of it's 'frame', which is a Rectangle
"""A camera that follows and matches the size and position of its 'frame', a Rectangle (or similar Mobject).
The frame defines the region of space the camera displays and can move or resize dynamically.
.. SEEALSO::
:class:`.MovingCameraScene`
"""
def __init__(
self,
frame=None,
fixed_dimension=0, # width
default_frame_stroke_color=WHITE,
default_frame_stroke_width=0,
**kwargs,
frame: Mobject | None = None,
fixed_dimension: int = 0, # width
default_frame_stroke_color: ManimColor = WHITE,
default_frame_stroke_width: int = 0,
**kwargs: Any,
):
"""
Frame is a Mobject, (should almost certainly be a rectangle)
"""Frame is a Mobject, (should almost certainly be a rectangle)
determining which region of space the camera displays
"""
self.fixed_dimension = fixed_dimension
@ -56,7 +59,7 @@ class MovingCamera(Camera):
# TODO, make these work for a rotated frame
@property
def frame_height(self):
def frame_height(self) -> float:
"""Returns the height of the frame.
Returns
@ -66,30 +69,8 @@ class MovingCamera(Camera):
"""
return self.frame.height
@property
def frame_width(self):
"""Returns the width of the frame
Returns
-------
float
The width of the frame.
"""
return self.frame.width
@property
def frame_center(self):
"""Returns the centerpoint of the frame in cartesian coordinates.
Returns
-------
np.array
The cartesian coordinates of the center of the frame.
"""
return self.frame.get_center()
@frame_height.setter
def frame_height(self, frame_height: float):
def frame_height(self, frame_height: float) -> None:
"""Sets the height of the frame in MUnits.
Parameters
@ -99,8 +80,19 @@ class MovingCamera(Camera):
"""
self.frame.stretch_to_fit_height(frame_height)
@property
def frame_width(self) -> float:
"""Returns the width of the frame
Returns
-------
float
The width of the frame.
"""
return self.frame.width
@frame_width.setter
def frame_width(self, frame_width: float):
def frame_width(self, frame_width: float) -> None:
"""Sets the width of the frame in MUnits.
Parameters
@ -110,8 +102,19 @@ class MovingCamera(Camera):
"""
self.frame.stretch_to_fit_width(frame_width)
@property
def frame_center(self) -> Point3D:
"""Returns the centerpoint of the frame in cartesian coordinates.
Returns
-------
np.array
The cartesian coordinates of the center of the frame.
"""
return self.frame.get_center()
@frame_center.setter
def frame_center(self, frame_center: np.ndarray | list | tuple | Mobject):
def frame_center(self, frame_center: Point3DLike | Mobject) -> None:
"""Sets the centerpoint of the frame.
Parameters
@ -123,25 +126,20 @@ class MovingCamera(Camera):
"""
self.frame.move_to(frame_center)
def capture_mobjects(self, mobjects, **kwargs):
def capture_mobjects(self, mobjects: Iterable[Mobject], **kwargs: Any) -> None:
# self.reset_frame_center()
# self.realign_frame_shape()
super().capture_mobjects(mobjects, **kwargs)
# Since the frame can be moving around, the cairo
# context used for updating should be regenerated
# at each frame. So no caching.
def get_cached_cairo_context(self, pixel_array):
"""
Since the frame can be moving around, the cairo
def get_cached_cairo_context(self, pixel_array: PixelArray) -> None:
"""Since the frame can be moving around, the cairo
context used for updating should be regenerated
at each frame. So no caching.
"""
return None
def cache_cairo_context(self, pixel_array, ctx):
"""
Since the frame can be moving around, the cairo
def cache_cairo_context(self, pixel_array: PixelArray, ctx: Context) -> None:
"""Since the frame can be moving around, the cairo
context used for updating should be regenerated
at each frame. So no caching.
"""
@ -158,24 +156,41 @@ class MovingCamera(Camera):
# self.frame_shape = (self.frame.height, width)
# self.resize_frame_shape(fixed_dimension=self.fixed_dimension)
def get_mobjects_indicating_movement(self):
"""
Returns all mobjects whose movement implies that the camera
def get_mobjects_indicating_movement(self) -> list[Mobject]:
"""Returns all mobjects whose movement implies that the camera
should think of all other mobjects on the screen as moving
Returns
-------
list
list[Mobject]
"""
return [self.frame]
@overload
def auto_zoom(
self,
mobjects: list[Mobject],
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,
):
) -> _AnimationBuilder | Mobject:
"""Zooms on to a given array of mobjects (or a singular mobject)
and automatically resizes to frame all the mobjects.
@ -205,37 +220,12 @@ class MovingCamera(Camera):
or ScreenRectangle with position and size updated to zoomed position.
"""
scene_critical_x_left = None
scene_critical_x_right = None
scene_critical_y_up = None
scene_critical_y_down = None
for m in mobjects:
if (m == self.frame) or (
only_mobjects_in_frame and not self.is_in_frame(m)
):
# detected camera frame, should not be used to calculate final position of camera
continue
# initialize scene critical points with first mobjects critical points
if scene_critical_x_left is None:
scene_critical_x_left = m.get_critical_point(LEFT)[0]
scene_critical_x_right = m.get_critical_point(RIGHT)[0]
scene_critical_y_up = m.get_critical_point(UP)[1]
scene_critical_y_down = m.get_critical_point(DOWN)[1]
else:
if m.get_critical_point(LEFT)[0] < scene_critical_x_left:
scene_critical_x_left = m.get_critical_point(LEFT)[0]
if m.get_critical_point(RIGHT)[0] > scene_critical_x_right:
scene_critical_x_right = m.get_critical_point(RIGHT)[0]
if m.get_critical_point(UP)[1] > scene_critical_y_up:
scene_critical_y_up = m.get_critical_point(UP)[1]
if m.get_critical_point(DOWN)[1] < scene_critical_y_down:
scene_critical_y_down = m.get_critical_point(DOWN)[1]
(
scene_critical_x_left,
scene_critical_x_right,
scene_critical_y_up,
scene_critical_y_down,
) = self._get_bounding_box(mobjects, only_mobjects_in_frame)
# calculate center x and y
x = (scene_critical_x_left + scene_critical_x_right) / 2
@ -251,3 +241,52 @@ class MovingCamera(Camera):
return m_target.set_x(x).set_y(y).set(width=new_width + margin)
else:
return m_target.set_x(x).set_y(y).set(height=new_height + margin)
def _get_bounding_box(
self, mobjects: Iterable[Mobject], only_mobjects_in_frame: bool
) -> tuple[float, float, float, float]:
bounding_box_located = False
scene_critical_x_left: float = 0
scene_critical_x_right: float = 1
scene_critical_y_up: float = 1
scene_critical_y_down: float = 0
for m in mobjects:
if (m == self.frame) or (
only_mobjects_in_frame and not self.is_in_frame(m)
):
# detected camera frame, should not be used to calculate final position of camera
continue
# initialize scene critical points with first mobjects critical points
if not bounding_box_located:
scene_critical_x_left = m.get_critical_point(LEFT)[0]
scene_critical_x_right = m.get_critical_point(RIGHT)[0]
scene_critical_y_up = m.get_critical_point(UP)[1]
scene_critical_y_down = m.get_critical_point(DOWN)[1]
bounding_box_located = True
else:
if m.get_critical_point(LEFT)[0] < scene_critical_x_left:
scene_critical_x_left = m.get_critical_point(LEFT)[0]
if m.get_critical_point(RIGHT)[0] > scene_critical_x_right:
scene_critical_x_right = m.get_critical_point(RIGHT)[0]
if m.get_critical_point(UP)[1] > scene_critical_y_up:
scene_critical_y_up = m.get_critical_point(UP)[1]
if m.get_critical_point(DOWN)[1] < scene_critical_y_down:
scene_critical_y_down = m.get_critical_point(DOWN)[1]
if not bounding_box_located:
raise Exception(
"Could not determine bounding box of the mobjects given to 'auto_zoom'."
)
return (
scene_critical_x_left,
scene_critical_x_right,
scene_critical_y_up,
scene_critical_y_down,
)

View file

@ -5,7 +5,11 @@ from __future__ import annotations
__all__ = ["MultiCamera"]
from manim.mobject.types.image_mobject import ImageMobject
from collections.abc import Iterable
from typing import Any, Self
from manim.mobject.mobject import Mobject
from manim.mobject.types.image_mobject import ImageMobjectFromCamera
from ..camera.moving_camera import MovingCamera
from ..utils.iterables import list_difference_update
@ -16,10 +20,10 @@ class MultiCamera(MovingCamera):
def __init__(
self,
image_mobjects_from_cameras: ImageMobject | None = None,
allow_cameras_to_capture_their_own_display=False,
**kwargs,
):
image_mobjects_from_cameras: Iterable[ImageMobjectFromCamera] | None = None,
allow_cameras_to_capture_their_own_display: bool = False,
**kwargs: Any,
) -> None:
"""Initialises the MultiCamera
Parameters
@ -29,7 +33,7 @@ class MultiCamera(MovingCamera):
kwargs
Any valid keyword arguments of MovingCamera.
"""
self.image_mobjects_from_cameras = []
self.image_mobjects_from_cameras: list[ImageMobjectFromCamera] = []
if image_mobjects_from_cameras is not None:
for imfc in image_mobjects_from_cameras:
self.add_image_mobject_from_camera(imfc)
@ -38,7 +42,9 @@ class MultiCamera(MovingCamera):
)
super().__init__(**kwargs)
def add_image_mobject_from_camera(self, image_mobject_from_camera: ImageMobject):
def add_image_mobject_from_camera(
self, image_mobject_from_camera: ImageMobjectFromCamera
) -> None:
"""Adds an ImageMobject that's been obtained from the camera
into the list ``self.image_mobject_from_cameras``
@ -53,20 +59,20 @@ class MultiCamera(MovingCamera):
assert isinstance(imfc.camera, MovingCamera)
self.image_mobjects_from_cameras.append(imfc)
def update_sub_cameras(self):
def update_sub_cameras(self) -> None:
"""Reshape sub_camera pixel_arrays"""
for imfc in self.image_mobjects_from_cameras:
pixel_height, pixel_width = self.pixel_array.shape[:2]
imfc.camera.frame_shape = (
imfc.camera.frame.height,
imfc.camera.frame.width,
)
# imfc.camera.frame_shape = (
# imfc.camera.frame.height,
# imfc.camera.frame.width,
# )
imfc.camera.reset_pixel_shape(
int(pixel_height * imfc.height / self.frame_height),
int(pixel_width * imfc.width / self.frame_width),
)
def reset(self):
def reset(self) -> Self:
"""Resets the MultiCamera.
Returns
@ -79,7 +85,7 @@ class MultiCamera(MovingCamera):
super().reset()
return self
def capture_mobjects(self, mobjects, **kwargs):
def capture_mobjects(self, mobjects: Iterable[Mobject], **kwargs: Any) -> None:
self.update_sub_cameras()
for imfc in self.image_mobjects_from_cameras:
to_add = list(mobjects)
@ -88,7 +94,7 @@ class MultiCamera(MovingCamera):
imfc.camera.capture_mobjects(to_add, **kwargs)
super().capture_mobjects(mobjects, **kwargs)
def get_mobjects_indicating_movement(self):
def get_mobjects_indicating_movement(self) -> list[Mobject]:
"""Returns all mobjects whose movement implies that the camera
should think of all other mobjects on the screen as moving

View file

@ -5,7 +5,8 @@ from __future__ import annotations
__all__ = ["ThreeDCamera"]
from typing import Callable
from collections.abc import Callable, Iterable
from typing import Any
import numpy as np
@ -16,7 +17,15 @@ from manim.mobject.three_d.three_d_utils import (
get_3d_vmob_start_corner,
get_3d_vmob_start_corner_unit_normal,
)
from manim.mobject.types.vectorized_mobject import VMobject
from manim.mobject.value_tracker import ValueTracker
from manim.typing import (
FloatRGBA_Array,
MatrixMN,
Point3D,
Point3D_Array,
Point3DLike,
)
from .. import config
from ..camera.camera import Camera
@ -30,17 +39,17 @@ from ..utils.space_ops import rotation_about_z, rotation_matrix
class ThreeDCamera(Camera):
def __init__(
self,
focal_distance=20.0,
shading_factor=0.2,
default_distance=5.0,
light_source_start_point=9 * DOWN + 7 * LEFT + 10 * OUT,
should_apply_shading=True,
exponential_projection=False,
phi=0,
theta=-90 * DEGREES,
gamma=0,
zoom=1,
**kwargs,
focal_distance: float = 20.0,
shading_factor: float = 0.2,
default_distance: float = 5.0,
light_source_start_point: Point3DLike = 9 * DOWN + 7 * LEFT + 10 * OUT,
should_apply_shading: bool = True,
exponential_projection: bool = False,
phi: float = 0,
theta: float = -90 * DEGREES,
gamma: float = 0,
zoom: float = 1,
**kwargs: Any,
):
"""Initializes the ThreeDCamera
@ -68,23 +77,23 @@ class ThreeDCamera(Camera):
self.focal_distance_tracker = ValueTracker(self.focal_distance)
self.gamma_tracker = ValueTracker(self.gamma)
self.zoom_tracker = ValueTracker(self.zoom)
self.fixed_orientation_mobjects = {}
self.fixed_in_frame_mobjects = set()
self.fixed_orientation_mobjects: dict[Mobject, Callable[[], Point3D]] = {}
self.fixed_in_frame_mobjects: set[Mobject] = set()
self.reset_rotation_matrix()
@property
def frame_center(self):
def frame_center(self) -> Point3D:
return self._frame_center.points[0]
@frame_center.setter
def frame_center(self, point):
def frame_center(self, point: Point3DLike) -> None:
self._frame_center.move_to(point)
def capture_mobjects(self, mobjects, **kwargs):
def capture_mobjects(self, mobjects: Iterable[Mobject], **kwargs: Any) -> None:
self.reset_rotation_matrix()
super().capture_mobjects(mobjects, **kwargs)
def get_value_trackers(self):
def get_value_trackers(self) -> list[ValueTracker]:
"""A list of :class:`ValueTrackers <.ValueTracker>` of phi, theta, focal_distance,
gamma and zoom.
@ -101,7 +110,9 @@ class ThreeDCamera(Camera):
self.zoom_tracker,
]
def modified_rgbas(self, vmobject, rgbas):
def modified_rgbas(
self, vmobject: VMobject, rgbas: FloatRGBA_Array
) -> FloatRGBA_Array:
if not self.should_apply_shading:
return rgbas
if vmobject.shade_in_3d and (vmobject.get_num_points() > 0):
@ -127,28 +138,33 @@ class ThreeDCamera(Camera):
def get_stroke_rgbas(
self,
vmobject,
background=False,
): # NOTE : DocStrings From parent
vmobject: VMobject,
background: bool = False,
) -> FloatRGBA_Array: # NOTE : DocStrings From parent
return self.modified_rgbas(vmobject, vmobject.get_stroke_rgbas(background))
def get_fill_rgbas(self, vmobject): # NOTE : DocStrings From parent
def get_fill_rgbas(
self, vmobject: VMobject
) -> FloatRGBA_Array: # NOTE : DocStrings From parent
return self.modified_rgbas(vmobject, vmobject.get_fill_rgbas())
def get_mobjects_to_display(self, *args, **kwargs): # NOTE : DocStrings From parent
def get_mobjects_to_display(
self, *args: Any, **kwargs: Any
) -> list[Mobject]: # NOTE : DocStrings From parent
mobjects = super().get_mobjects_to_display(*args, **kwargs)
rot_matrix = self.get_rotation_matrix()
def z_key(mob):
def z_key(mob: Mobject) -> float:
if not (hasattr(mob, "shade_in_3d") and mob.shade_in_3d):
return np.inf
return np.inf # type: ignore[no-any-return]
# Assign a number to a three dimensional mobjects
# based on how close it is to the camera
return np.dot(mob.get_z_index_reference_point(), rot_matrix.T)[2]
distance: float = np.dot(mob.get_z_index_reference_point(), rot_matrix.T)[2]
return distance
return sorted(mobjects, key=z_key)
def get_phi(self):
def get_phi(self) -> float:
"""Returns the Polar angle (the angle off Z_AXIS) phi.
Returns
@ -158,7 +174,7 @@ class ThreeDCamera(Camera):
"""
return self.phi_tracker.get_value()
def get_theta(self):
def get_theta(self) -> float:
"""Returns the Azimuthal i.e the angle that spins the camera around the Z_AXIS.
Returns
@ -168,7 +184,7 @@ class ThreeDCamera(Camera):
"""
return self.theta_tracker.get_value()
def get_focal_distance(self):
def get_focal_distance(self) -> float:
"""Returns focal_distance of the Camera.
Returns
@ -178,7 +194,7 @@ class ThreeDCamera(Camera):
"""
return self.focal_distance_tracker.get_value()
def get_gamma(self):
def get_gamma(self) -> float:
"""Returns the rotation of the camera about the vector from the ORIGIN to the Camera.
Returns
@ -189,7 +205,7 @@ class ThreeDCamera(Camera):
"""
return self.gamma_tracker.get_value()
def get_zoom(self):
def get_zoom(self) -> float:
"""Returns the zoom amount of the camera.
Returns
@ -199,7 +215,7 @@ class ThreeDCamera(Camera):
"""
return self.zoom_tracker.get_value()
def set_phi(self, value: float):
def set_phi(self, value: float) -> None:
"""Sets the polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians.
Parameters
@ -209,7 +225,7 @@ class ThreeDCamera(Camera):
"""
self.phi_tracker.set_value(value)
def set_theta(self, value: float):
def set_theta(self, value: float) -> None:
"""Sets the azimuthal angle i.e the angle that spins the camera around Z_AXIS in radians.
Parameters
@ -219,7 +235,7 @@ class ThreeDCamera(Camera):
"""
self.theta_tracker.set_value(value)
def set_focal_distance(self, value: float):
def set_focal_distance(self, value: float) -> None:
"""Sets the focal_distance of the Camera.
Parameters
@ -229,7 +245,7 @@ class ThreeDCamera(Camera):
"""
self.focal_distance_tracker.set_value(value)
def set_gamma(self, value: float):
def set_gamma(self, value: float) -> None:
"""Sets the angle of rotation of the camera about the vector from the ORIGIN to the Camera.
Parameters
@ -239,7 +255,7 @@ class ThreeDCamera(Camera):
"""
self.gamma_tracker.set_value(value)
def set_zoom(self, value: float):
def set_zoom(self, value: float) -> None:
"""Sets the zoom amount of the camera.
Parameters
@ -249,13 +265,13 @@ class ThreeDCamera(Camera):
"""
self.zoom_tracker.set_value(value)
def reset_rotation_matrix(self):
def reset_rotation_matrix(self) -> None:
"""Sets the value of self.rotation_matrix to
the matrix corresponding to the current position of the camera
"""
self.rotation_matrix = self.generate_rotation_matrix()
def get_rotation_matrix(self):
def get_rotation_matrix(self) -> MatrixMN:
"""Returns the matrix corresponding to the current position of the camera.
Returns
@ -265,7 +281,7 @@ class ThreeDCamera(Camera):
"""
return self.rotation_matrix
def generate_rotation_matrix(self):
def generate_rotation_matrix(self) -> MatrixMN:
"""Generates a rotation matrix based off the current position of the camera.
Returns
@ -286,7 +302,7 @@ class ThreeDCamera(Camera):
result = np.dot(matrix, result)
return result
def project_points(self, points: np.ndarray | list):
def project_points(self, points: Point3D_Array) -> Point3D_Array:
"""Applies the current rotation_matrix as a projection
matrix to the passed array of points.
@ -323,7 +339,7 @@ class ThreeDCamera(Camera):
points[:, i] *= factor * zoom
return points
def project_point(self, point: list | np.ndarray):
def project_point(self, point: Point3D) -> Point3D:
"""Applies the current rotation_matrix as a projection
matrix to the passed point.
@ -341,9 +357,9 @@ class ThreeDCamera(Camera):
def transform_points_pre_display(
self,
mobject,
points,
): # TODO: Write Docstrings for this Method.
mobject: Mobject,
points: Point3D_Array,
) -> Point3D_Array: # TODO: Write Docstrings for this Method.
points = super().transform_points_pre_display(mobject, points)
fixed_orientation = mobject in self.fixed_orientation_mobjects
fixed_in_frame = mobject in self.fixed_in_frame_mobjects
@ -362,8 +378,8 @@ class ThreeDCamera(Camera):
self,
*mobjects: Mobject,
use_static_center_func: bool = False,
center_func: Callable[[], np.ndarray] | None = None,
):
center_func: Callable[[], Point3D] | None = None,
) -> None:
"""This method allows the mobject to have a fixed orientation,
even when the camera moves around.
E.G If it was passed through this method, facing the camera, it
@ -384,7 +400,7 @@ class ThreeDCamera(Camera):
# This prevents the computation of mobject.get_center
# every single time a projection happens
def get_static_center_func(mobject):
def get_static_center_func(mobject: Mobject) -> Callable[[], Point3D]:
point = mobject.get_center()
return lambda: point
@ -398,7 +414,7 @@ class ThreeDCamera(Camera):
for submob in mobject.get_family():
self.fixed_orientation_mobjects[submob] = func
def add_fixed_in_frame_mobjects(self, *mobjects: Mobject):
def add_fixed_in_frame_mobjects(self, *mobjects: Mobject) -> None:
"""This method allows the mobject to have a fixed position,
even when the camera moves around.
E.G If it was passed through this method, at the top of the frame, it
@ -414,7 +430,7 @@ class ThreeDCamera(Camera):
for mobject in extract_mobject_family_members(mobjects):
self.fixed_in_frame_mobjects.add(mobject)
def remove_fixed_orientation_mobjects(self, *mobjects: Mobject):
def remove_fixed_orientation_mobjects(self, *mobjects: Mobject) -> None:
"""If a mobject was fixed in its orientation by passing it through
:meth:`.add_fixed_orientation_mobjects`, then this undoes that fixing.
The Mobject will no longer have a fixed orientation.
@ -428,7 +444,7 @@ class ThreeDCamera(Camera):
if mobject in self.fixed_orientation_mobjects:
del self.fixed_orientation_mobjects[mobject]
def remove_fixed_in_frame_mobjects(self, *mobjects: Mobject):
def remove_fixed_in_frame_mobjects(self, *mobjects: Mobject) -> None:
"""If a mobject was fixed in frame by passing it through
:meth:`.add_fixed_in_frame_mobjects`, then this undoes that fixing.
The Mobject will no longer be fixed in frame.

View file

@ -267,6 +267,12 @@ modify write_cfg_subcmd_input to account for it.""",
@cfg.command(context_settings=cli_ctx_settings)
def show() -> None:
console.print("CONFIG FILES READ", style="bold green underline")
for path in config_file_paths():
if path.exists():
console.print(f"{path}")
console.print()
parser = make_config_parser()
rich_non_style_entries = [a.replace(".", "_") for a in RICH_NON_STYLE_ENTRIES]
for category in parser:
@ -302,7 +308,7 @@ Are you sure you want to continue? (y/n)""",
if proceed:
if not directory_path.is_dir():
console.print(f"Creating folder: {directory}.", style="red bold")
directory_path.mkdir(parents=True)
directory_path.mkdir(parents=True, exist_ok=True)
ctx.invoke(write)
from_path = Path.cwd() / "manim.cfg"

View file

@ -6,7 +6,8 @@ from __future__ import annotations
import os
import shutil
from typing import Callable, Protocol, cast
from collections.abc import Callable
from typing import Protocol, cast
__all__ = ["HEALTH_CHECKS"]

View file

@ -13,7 +13,8 @@ In particular, this class is what allows ``manim`` to act as ``manim render``.
from __future__ import annotations
import warnings
from typing import TYPE_CHECKING, Any, Callable
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
import cloup

View file

@ -29,7 +29,7 @@ CFG_DEFAULTS = {
"background_color": "BLACK",
"background_opacity": 1,
"scene_names": "Default",
"resolution": (854, 480),
"resolution": (1920, 1080),
}
__all__ = ["select_resolution", "update_cfg", "project", "scene"]
@ -43,11 +43,10 @@ def select_resolution() -> tuple[int, int]:
tuple[int, int]
Tuple containing height and width.
"""
resolution_options: list[tuple[int, int]] = []
for quality in QUALITIES.items():
resolution_options.append(
(quality[1]["pixel_height"], quality[1]["pixel_width"]),
)
resolution_options: list[tuple[int, int]] = [
(quality[1]["pixel_height"], quality[1]["pixel_width"])
for quality in QUALITIES.items()
]
resolution_options.pop()
choice = click.prompt(
"\nSelect resolution:\n",
@ -76,8 +75,8 @@ def update_cfg(cfg_dict: dict[str, Any], project_cfg_path: Path) -> None:
cli_config = config["CLI"]
for key, value in cfg_dict.items():
if key == "resolution":
cli_config["pixel_width"] = str(value[0])
cli_config["pixel_height"] = str(value[1])
cli_config["pixel_height"] = str(value[0])
cli_config["pixel_width"] = str(value[1])
else:
cli_config[key] = str(value)

View file

@ -125,23 +125,29 @@ global_options = option_group(
"--force_window",
is_flag=True,
help="Force window to open when using the opengl renderer, intended for debugging as it may impact performance",
default=False,
default=None,
),
option(
"--dry_run",
is_flag=True,
help="Renders animations without outputting image or video files and disables the window",
default=False,
default=None,
),
option(
"--no_latex_cleanup",
is_flag=True,
help="Prevents deletion of .aux, .dvi, and .log files produced by Tex and MathTex.",
default=False,
default=None,
),
option(
"--preview_command",
help="The command used to preview the output file (for example vlc for video files)",
default="",
default=None,
),
option(
"--seed",
type=int,
help="Set the random seed to allow reproducibility.",
default=None,
),
)

View file

@ -84,7 +84,7 @@ SCENE_NOT_FOUND_MESSAGE = """
"""
CHOOSE_NUMBER_MESSAGE = """
Choose number corresponding to desired scene/arguments.
(Use comma separated list for multiple entries)
(Use comma separated list for multiple entries or use "*" to select all scenes.)
Choice(s): """
INVALID_NUMBER_MESSAGE = "Invalid scene numbers have been specified. Aborting."
NO_SCENE_MESSAGE = """
@ -111,14 +111,10 @@ ULTRAHEAVY = "ULTRAHEAVY"
RESAMPLING_ALGORITHMS = {
"nearest": Resampling.NEAREST,
"none": Resampling.NEAREST,
"lanczos": Resampling.LANCZOS,
"antialias": Resampling.LANCZOS,
"bilinear": Resampling.BILINEAR,
"linear": Resampling.BILINEAR,
"bicubic": Resampling.BICUBIC,
"cubic": Resampling.BICUBIC,
"box": Resampling.BOX,
"hamming": Resampling.HAMMING,
}
# Geometry: directions

31
manim/data_structures.py Normal file
View file

@ -0,0 +1,31 @@
"""Data classes and other necessary data structures for use in Manim."""
from __future__ import annotations
from collections.abc import Iterable
from dataclasses import dataclass
from types import MethodType
from typing import Any
@dataclass
class MethodWithArgs:
"""Object containing a :attr:`method` which is intended to be called later
with the positional arguments :attr:`args` and the keyword arguments
:attr:`kwargs`.
Attributes
----------
method : MethodType
A callable representing a method of some class.
args : Iterable[Any]
Positional arguments for :attr:`method`.
kwargs : dict[str, Any]
Keyword arguments for :attr:`method`.
"""
__slots__ = ["method", "args", "kwargs"]
method: MethodType
args: Iterable[Any]
kwargs: dict[str, Any]

View file

@ -1,84 +0,0 @@
from __future__ import annotations
from pathlib import Path
try:
import dearpygui.dearpygui as dpg
dearpygui_imported = True
except ImportError:
dearpygui_imported = False
from .. import __version__, config
from ..utils.module_ops import scene_classes_from_file
__all__ = ["configure_pygui"]
if dearpygui_imported:
dpg.create_context()
window = dpg.generate_uuid()
def configure_pygui(renderer, widgets, update=True):
if not dearpygui_imported:
raise RuntimeError("Attempted to use DearPyGUI when it isn't imported.")
if update:
dpg.delete_item(window)
else:
dpg.create_viewport()
dpg.setup_dearpygui()
dpg.show_viewport()
dpg.set_viewport_title(title=f"Manim Community v{__version__}")
dpg.set_viewport_width(1015)
dpg.set_viewport_height(540)
def rerun_callback(sender, data):
renderer.scene.queue.put(("rerun_gui", [], {}))
def continue_callback(sender, data):
renderer.scene.queue.put(("exit_gui", [], {}))
def scene_selection_callback(sender, data):
config["scene_names"] = (dpg.get_value(sender),)
renderer.scene.queue.put(("rerun_gui", [], {}))
scene_classes = scene_classes_from_file(Path(config["input_file"]), full_list=True)
scene_names = [scene_class.__name__ for scene_class in scene_classes]
with dpg.window(
id=window,
label="Manim GUI",
pos=[config["gui_location"][0], config["gui_location"][1]],
width=1000,
height=500,
):
dpg.set_global_font_scale(2)
dpg.add_button(label="Rerun", callback=rerun_callback)
dpg.add_button(label="Continue", callback=continue_callback)
dpg.add_combo(
label="Selected scene",
items=scene_names,
callback=scene_selection_callback,
default_value=config["scene_names"][0],
)
dpg.add_separator()
if len(widgets) != 0:
with dpg.collapsing_header(
label=f"{config['scene_names'][0]} widgets",
default_open=True,
):
for widget_config in widgets:
widget_config_copy = widget_config.copy()
name = widget_config_copy["name"]
widget = widget_config_copy["widget"]
if widget != "separator":
del widget_config_copy["name"]
del widget_config_copy["widget"]
getattr(dpg, f"add_{widget}")(label=name, **widget_config_copy)
else:
dpg.add_separator()
if not update:
dpg.start_dearpygui()

View file

@ -8,7 +8,7 @@ __all__ = [
]
from typing_extensions import Any
from typing import Any
from manim.mobject.geometry.polygram import Rectangle

View file

@ -40,14 +40,14 @@ __all__ = [
"CubicBezier",
"ArcPolygon",
"ArcPolygonFromArcs",
"TangentialArc",
]
import itertools
import warnings
from typing import TYPE_CHECKING, cast
from typing import TYPE_CHECKING, Any, Self, cast
import numpy as np
from typing_extensions import Self
from manim.constants import *
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
@ -55,6 +55,7 @@ from manim.mobject.types.vectorized_mobject import VGroup, VMobject
from manim.utils.color import BLACK, BLUE, RED, WHITE, ParsableManimColor
from manim.utils.iterables import adjacent_pairs
from manim.utils.space_ops import (
angle_between_vectors,
angle_of_vector,
cartesian_to_spherical,
line_intersection,
@ -64,9 +65,9 @@ from manim.utils.space_ops import (
if TYPE_CHECKING:
from collections.abc import Iterable
from typing import Any
import manim.mobject.geometry.tips as tips
from manim.mobject.geometry.line import Line
from manim.mobject.mobject import Mobject
from manim.mobject.text.tex_mobject import SingleStringMathTex, Tex
from manim.mobject.text.text_mobject import Text
@ -74,7 +75,7 @@ if TYPE_CHECKING:
Point3D,
Point3DLike,
QuadraticSpline,
Vector3D,
Vector3DLike,
)
@ -99,13 +100,13 @@ class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
def __init__(
self,
tip_length: float = DEFAULT_ARROW_TIP_LENGTH,
normal_vector: Vector3D = OUT,
tip_style: dict = {},
normal_vector: Vector3DLike = OUT,
tip_style: dict | None = None,
**kwargs: Any,
) -> None:
self.tip_length: float = tip_length
self.normal_vector: Vector3D = normal_vector
self.tip_style: dict = tip_style
self.normal_vector = normal_vector
self.tip_style: dict = tip_style if tip_style is not None else {}
super().__init__(**kwargs)
# Adding, Creating, Modifying tips
@ -127,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
@ -183,7 +184,7 @@ class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
else:
handle = self.get_last_handle()
anchor = self.get_end()
angles = cartesian_to_spherical((handle - anchor).tolist())
angles = cartesian_to_spherical(handle - anchor)
tip.rotate(
angles[1] - PI - tip.tip_angle,
) # Rotates the tip along the azimuthal
@ -200,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
@ -214,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:
@ -240,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:
@ -497,6 +500,98 @@ class ArcBetweenPoints(Arc):
self.radius = np.inf
class TangentialArc(ArcBetweenPoints):
"""
Construct an arc that is tangent to two intersecting lines.
You can choose any of the 4 possible corner arcs via the `corner` tuple.
corner = (s1, s2) where each si is ±1 to control direction along each line.
Examples
--------
.. manim:: TangentialArcExample
:save_last_frame:
class TangentialArcExample(Scene):
def construct(self):
line1 = DashedLine(start=3 * LEFT, end=3 * RIGHT)
line1.rotate(angle=31 * DEGREES, about_point=ORIGIN)
line2 = DashedLine(start=3 * UP, end=3 * DOWN)
line2.rotate(angle=12 * DEGREES, about_point=ORIGIN)
arc = TangentialArc(line1, line2, radius=2.25, corner=(1, 1), color=TEAL)
self.add(arc, line1, line2)
The following example shows all four possible corner configurations:
.. manim:: TangentialArcCorners
:save_last_frame:
class TangentialArcCorners(Scene):
def construct(self):
# Create two intersecting lines
line1 = DashedLine(start=3 * LEFT, end=3 * RIGHT, color=GREY)
line2 = DashedLine(start=3 * UP, end=3 * DOWN, color=GREY)
# All four corner configurations with different colors
arc_pp = TangentialArc(line1, line2, radius=1.5, corner=(1, 1), color=RED)
arc_pn = TangentialArc(line1, line2, radius=1.5, corner=(1, -1), color=GREEN)
arc_np = TangentialArc(line1, line2, radius=1.5, corner=(-1, 1), color=BLUE)
arc_nn = TangentialArc(line1, line2, radius=1.5, corner=(-1, -1), color=YELLOW)
# Labels for each arc
label_pp = Text("(1,1)", font_size=24, color=RED).next_to(arc_pp, UR, buff=0.1)
label_pn = Text("(1,-1)", font_size=24, color=GREEN).next_to(arc_pn, DR, buff=0.1)
label_np = Text("(-1,1)", font_size=24, color=BLUE).next_to(arc_np, UL, buff=0.1)
label_nn = Text("(-1,-1)", font_size=24, color=YELLOW).next_to(arc_nn, DL, buff=0.1)
self.add(line1, line2, arc_pp, arc_pn, arc_np, arc_nn)
self.add(label_pp, label_pn, label_np, label_nn)
"""
def __init__(
self,
line1: Line,
line2: Line,
radius: float,
corner: Any = (1, 1),
**kwargs: Any,
):
self.line1 = line1
self.line2 = line2
intersection_point = line_intersection(
[line1.get_start(), line1.get_end()], [line2.get_start(), line2.get_end()]
)
s1, s2 = corner
# Get unit vector for specified directions
unit_vector1 = s1 * line1.get_unit_vector()
unit_vector2 = s2 * line2.get_unit_vector()
corner_angle = angle_between_vectors(unit_vector1, unit_vector2)
tangent_point_distance = radius / np.tan(corner_angle / 2)
# tangent points
tangent_point1 = intersection_point + tangent_point_distance * unit_vector1
tangent_point2 = intersection_point + tangent_point_distance * unit_vector2
cross_product = (
unit_vector1[0] * unit_vector2[1] - unit_vector1[1] * unit_vector2[0]
)
# Determine start and end points based on orientation
if cross_product < 0:
# Counterclockwise orientation - standard order
start_point = tangent_point1
end_point = tangent_point2
else:
# Clockwise orientation - reverse the points
start_point = tangent_point2
end_point = tangent_point1
super().__init__(start=start_point, end=end_point, radius=radius, **kwargs)
class CurvedArrow(ArcBetweenPoints):
def __init__(
self, start_point: Point3DLike, end_point: Point3DLike, **kwargs: Any
@ -641,8 +736,7 @@ class Circle(Arc):
self.add(circle, s1, s2)
"""
start_angle = angle_of_vector(self.points[0] - self.get_center())
proportion = (angle - start_angle) / TAU
proportion = angle / TAU
proportion -= np.floor(proportion)
return self.point_from_proportion(proportion)
@ -758,8 +852,9 @@ class LabeledDot(Dot):
representing rendered strings like :class:`~.Text` or :class:`~.Tex`
can be passed as well.
radius
The radius of the :class:`Dot`. If ``None`` (the default), the radius
is calculated based on the size of the ``label``.
The radius of the :class:`Dot`. If provided, the ``buff`` is ignored.
If ``None`` (the default), the radius is calculated based on the size
of the ``label`` and the ``buff``.
Examples
--------
@ -785,6 +880,7 @@ class LabeledDot(Dot):
self,
label: str | SingleStringMathTex | Text | Tex,
radius: float | None = None,
buff: float = SMALL_BUFF,
**kwargs: Any,
) -> None:
if isinstance(label, str):
@ -795,7 +891,9 @@ class LabeledDot(Dot):
rendered_label = label
if radius is None:
radius = 0.1 + max(rendered_label.width, rendered_label.height) / 2
radius = buff + float(
np.linalg.norm([rendered_label.width, rendered_label.height]) / 2
)
super().__init__(radius=radius, **kwargs)
rendered_label.move_to(self.get_center())
self.add(rendered_label)
@ -916,7 +1014,8 @@ class AnnularSector(Arc):
self.append_points(outer_arc.points)
self.add_line_to(inner_arc.points[0])
init_points = generate_points
def init_points(self) -> None:
self.generate_points()
class Sector(AnnularSector):
@ -990,7 +1089,8 @@ class Annulus(Circle):
self.append_points(inner_circle.points)
self.shift(self.arc_center)
init_points = generate_points
def init_points(self) -> None:
self.generate_points()
class CubicBezier(VMobject, metaclass=ConvertToOpenGL):
@ -1133,7 +1233,7 @@ class ArcPolygon(VMobject, metaclass=ConvertToOpenGL):
arcs = [
ArcBetweenPoints(*pair, **conf)
for (pair, conf) in zip(point_pairs, all_arc_configs)
for (pair, conf) in zip(point_pairs, all_arc_configs, strict=True)
]
super().__init__(**kwargs)

View file

@ -2,7 +2,7 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
import numpy as np
from pathops import Path as SkiaPath
@ -13,8 +13,6 @@ from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from manim.mobject.types.vectorized_mobject import VMobject
if TYPE_CHECKING:
from typing import Any
from manim.typing import Point2DLike_Array, Point3D_Array, Point3DLike_Array
from ...constants import RendererType
@ -59,7 +57,7 @@ class _BooleanOps(VMobject, metaclass=ConvertToOpenGL):
list_of_points = list(points)
for i, point in enumerate(list_of_points):
if len(point) == 2:
list_of_points[i] = np.array(list(point) + [z_dim])
list_of_points[i] = np.append(point, z_dim)
return np.asarray(list_of_points)
def _convert_vmobject_to_skia_path(self, vmobject: VMobject) -> SkiaPath:
@ -78,10 +76,10 @@ class _BooleanOps(VMobject, metaclass=ConvertToOpenGL):
"""
path = SkiaPath()
if not np.all(np.isfinite(vmobject.points)):
points = np.zeros((1, 3)) # point invalid?
else:
if np.all(np.isfinite(vmobject.points)):
points = vmobject.points
else:
points = np.zeros((1, 3)) # point invalid?
if len(points) == 0: # what? No points so return empty path
return path
@ -184,9 +182,9 @@ class Union(_BooleanOps):
if len(vmobjects) < 2:
raise ValueError("At least 2 mobjects needed for Union.")
super().__init__(**kwargs)
paths = []
for vmobject in vmobjects:
paths.append(self._convert_vmobject_to_skia_path(vmobject))
paths = [
self._convert_vmobject_to_skia_path(vmobject) for vmobject in vmobjects
]
outpen = SkiaPath()
union(paths, outpen.getPen())
self._convert_skia_path_to_vmobject(outpen)

View file

@ -4,7 +4,7 @@ from __future__ import annotations
__all__ = ["Label", "LabeledLine", "LabeledArrow", "LabeledPolygram"]
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
import numpy as np
@ -22,8 +22,6 @@ from manim.utils.color import WHITE
from manim.utils.polylabel import polylabel
if TYPE_CHECKING:
from typing import Any
from manim.typing import Point3DLike_Array

View file

@ -14,14 +14,14 @@ __all__ = [
"RightAngle",
]
from typing import TYPE_CHECKING
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
@ -30,11 +30,9 @@ from manim.utils.color import WHITE
from manim.utils.space_ops import angle_of_vector, line_intersection, normalize
if TYPE_CHECKING:
from typing import Any
from typing import Self, TypeAlias
from typing_extensions import Literal, Self, TypeAlias
from manim.typing import Point2DLike, Point3D, Point3DLike, Vector3D
from manim.typing import Point3D, Point3DLike, Vector2DLike, Vector3D, Vector3DLike
from manim.utils.color import ParsableManimColor
from ..matrix import Matrix # Avoid circular import
@ -147,7 +145,8 @@ class Line(TipableVMobject):
self._account_for_buff(buff)
init_points = generate_points
def init_points(self) -> None:
self.generate_points()
def _account_for_buff(self, buff: float) -> None:
if buff <= 0:
@ -175,7 +174,7 @@ class Line(TipableVMobject):
def _pointify(
self,
mob_or_point: Mobject | Point3DLike,
direction: Vector3D | None = None,
direction: Vector3DLike | None = None,
) -> Point3D:
"""Transforms a mobject into its corresponding point. Does nothing if a point is passed.
@ -649,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:
@ -738,7 +739,7 @@ class Vector(Arrow):
def __init__(
self,
direction: Point2DLike | Point3DLike = RIGHT,
direction: Vector2DLike | Vector3DLike = RIGHT,
buff: float = 0,
**kwargs: Any,
) -> None:

View file

@ -18,7 +18,7 @@ __all__ = [
from math import ceil
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any, Literal
import numpy as np
@ -32,10 +32,9 @@ from manim.utils.qhull import QuickHull
from manim.utils.space_ops import angle_between_vectors, normalize, regular_vertices
if TYPE_CHECKING:
from typing import Any, Literal
from typing import Self
import numpy.typing as npt
from typing_extensions import Self
from manim.typing import (
Point3D,
@ -152,7 +151,9 @@ class Polygram(VMobject, metaclass=ConvertToOpenGL):
# TODO: If any of the original vertex groups contained the starting vertex N
# times, then .get_vertex_groups() splits it into N vertex groups.
group = []
for start, end in zip(self.get_start_anchors(), self.get_end_anchors()):
for start, end in zip(
self.get_start_anchors(), self.get_end_anchors(), strict=True
):
group.append(start)
if self.consider_points_equals(end, group[0]):
@ -239,7 +240,7 @@ class Polygram(VMobject, metaclass=ConvertToOpenGL):
radius_list = radius * ceil(len(vertex_group) / len(radius))
for current_radius, (v1, v2, v3) in zip(
radius_list, adjacent_n_tuples(vertex_group, 3)
radius_list, adjacent_n_tuples(vertex_group, 3), strict=True
):
vect1 = v2 - v1
vect2 = v3 - v2
@ -551,7 +552,7 @@ class Star(Polygon):
)
vertices: list[npt.NDArray] = []
for pair in zip(outer_vertices, inner_vertices):
for pair in zip(outer_vertices, inner_vertices, strict=True):
vertices.extend(pair)
super().__init__(*vertices, **kwargs)

View file

@ -4,9 +4,7 @@ from __future__ import annotations
__all__ = ["SurroundingRectangle", "BackgroundRectangle", "Cross", "Underline"]
from typing import Any
from typing_extensions import Self
from typing import Any, Self
from manim import logger
from manim._config import config
@ -22,7 +20,7 @@ from manim.mobject.geometry.polygram import RoundedRectangle
from manim.mobject.mobject import Mobject
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
from manim.mobject.types.vectorized_mobject import VGroup
from manim.utils.color import BLACK, RED, YELLOW, ManimColor, ParsableManimColor
from manim.utils.color import BLACK, PURE_YELLOW, RED, ParsableManimColor
class SurroundingRectangle(RoundedRectangle):
@ -52,8 +50,8 @@ class SurroundingRectangle(RoundedRectangle):
def __init__(
self,
*mobjects: Mobject,
color: ParsableManimColor = YELLOW,
buff: float = SMALL_BUFF,
color: ParsableManimColor = PURE_YELLOW,
buff: float | tuple[float, float] = SMALL_BUFF,
corner_radius: float = 0.0,
**kwargs: Any,
) -> None:
@ -64,11 +62,17 @@ class SurroundingRectangle(RoundedRectangle):
"Expected all inputs for parameter mobjects to be a Mobjects"
)
if isinstance(buff, tuple):
buff_x = buff[0]
buff_y = buff[1]
else:
buff_x = buff_y = buff
group = Group(*mobjects)
super().__init__(
color=color,
width=group.width + 2 * buff,
height=group.height + 2 * buff,
width=group.width + 2 * buff_x,
height=group.height + 2 * buff_y,
corner_radius=corner_radius,
**kwargs,
)
@ -108,7 +112,7 @@ class BackgroundRectangle(SurroundingRectangle):
stroke_width: float = 0,
stroke_opacity: float = 0,
fill_opacity: float = 0.75,
buff: float = 0,
buff: float | tuple[float, float] = 0,
**kwargs: Any,
) -> None:
if color is None:
@ -145,12 +149,6 @@ class BackgroundRectangle(SurroundingRectangle):
)
return self
def get_fill_color(self) -> ManimColor:
# The type of the color property is set to Any using the property decorator
# vectorized_mobject.py#L571
temp_color: ManimColor = self.color
return temp_color
class Cross(VGroup):
"""Creates a cross.

View file

@ -13,7 +13,7 @@ __all__ = [
"StealthTip",
]
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
import numpy as np
@ -25,8 +25,6 @@ from manim.mobject.types.vectorized_mobject import VMobject
from manim.utils.space_ops import angle_of_vector
if TYPE_CHECKING:
from typing import Any
from manim.typing import Point3D, Vector3D

View file

@ -16,7 +16,7 @@ import networkx as nx
import numpy as np
if TYPE_CHECKING:
from typing_extensions import TypeAlias
from typing import TypeAlias
from manim.scene.scene import Scene
from manim.typing import Point3D, Point3DLike
@ -588,9 +588,7 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
self._labels = labels
elif isinstance(labels, bool):
if labels:
self._labels = {
v: MathTex(v, fill_color=label_fill_color) for v in vertices
}
self._labels = {v: MathTex(v, color=label_fill_color) for v in vertices}
else:
self._labels = {}
@ -697,7 +695,7 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
)
if label is True:
label = MathTex(vertex, fill_color=label_fill_color)
label = MathTex(vertex, color=label_fill_color)
elif vertex in self._labels:
label = self._labels[vertex]
elif not isinstance(label, (Mobject, OpenGLMobject)):
@ -1021,10 +1019,7 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
"""
if edge_config is None:
edge_config = self.default_edge_config.copy()
added_mobjects = []
for v in edge:
if v not in self.vertices:
added_mobjects.append(self._add_vertex(v))
added_mobjects = [self._add_vertex(v) for v in edge if v not in self.vertices]
u, v = edge
self._graph.add_edge(u, v)

View file

@ -13,11 +13,10 @@ __all__ = [
import fractions as fr
import numbers
from collections.abc import Iterable, Sequence
from typing import TYPE_CHECKING, Any, Callable, TypeVar, overload
from collections.abc import Callable, Iterable, Sequence
from typing import TYPE_CHECKING, Any, Self, TypeVar, overload
import numpy as np
from typing_extensions import Self
from manim import config
from manim.constants import *
@ -43,8 +42,8 @@ from manim.utils.color import (
BLUE,
BLUE_D,
GREEN,
PURE_YELLOW,
WHITE,
YELLOW,
ManimColor,
ParsableManimColor,
color_gradient,
@ -64,6 +63,7 @@ if TYPE_CHECKING:
Point3D,
Point3DLike,
Vector3D,
Vector3DLike,
)
LineType = TypeVar("LineType", bound=Line)
@ -126,7 +126,7 @@ class CoordinateSystem:
x_length: float | None = None,
y_length: float | None = None,
dimension: int = 2,
) -> None:
):
self.dimension = dimension
default_step = 1
@ -153,11 +153,14 @@ class CoordinateSystem:
self.x_length = x_length
self.y_length = y_length
self.num_sampled_graph_points_per_tick = 10
self.x_axis: NumberLine
def coords_to_point(self, *coords: ManimFloat):
def coords_to_point(self, *coords: ManimFloat) -> Point3D:
# TODO: I think the method should be able to return more than just a single point.
# E.g. see the implementation of it on line 2065.
raise NotImplementedError()
def point_to_coords(self, point: Point3DLike):
def point_to_coords(self, point: Point3DLike) -> list[ManimFloat]:
raise NotImplementedError()
def polar_to_point(self, radius: float, azimuth: float) -> Point2D:
@ -201,7 +204,7 @@ class CoordinateSystem:
Returns
-------
Tuple[:class:`float`, :class:`float`]
Point2D
The coordinate radius (:math:`r`) and the coordinate azimuth (:math:`\theta`).
"""
x, y = self.point_to_coords(point)
@ -213,7 +216,7 @@ class CoordinateSystem:
"""Abbreviation for :meth:`coords_to_point`"""
return self.coords_to_point(*coords)
def p2c(self, point: Point3DLike):
def p2c(self, point: Point3DLike) -> list[ManimFloat]:
"""Abbreviation for :meth:`point_to_coords`"""
return self.point_to_coords(point)
@ -221,17 +224,18 @@ class CoordinateSystem:
"""Abbreviation for :meth:`polar_to_point`"""
return self.polar_to_point(radius, azimuth)
def pt2pr(self, point: np.ndarray) -> tuple[float, float]:
def pt2pr(self, point: np.ndarray) -> Point2D:
"""Abbreviation for :meth:`point_to_polar`"""
return self.point_to_polar(point)
def get_axes(self):
def get_axes(self) -> VGroup:
raise NotImplementedError()
def get_axis(self, index: int) -> Mobject:
return self.get_axes()[index]
def get_axis(self, index: int) -> NumberLine:
val: NumberLine = self.get_axes()[index]
return val
def get_origin(self) -> np.ndarray:
def get_origin(self) -> Point3D:
"""Gets the origin of :class:`~.Axes`.
Returns
@ -241,13 +245,13 @@ class CoordinateSystem:
"""
return self.coords_to_point(0, 0)
def get_x_axis(self) -> Mobject:
def get_x_axis(self) -> NumberLine:
return self.get_axis(0)
def get_y_axis(self) -> Mobject:
def get_y_axis(self) -> NumberLine:
return self.get_axis(1)
def get_z_axis(self) -> Mobject:
def get_z_axis(self) -> NumberLine:
return self.get_axis(2)
def get_x_unit_size(self) -> float:
@ -258,11 +262,11 @@ class CoordinateSystem:
def get_x_axis_label(
self,
label: float | str | Mobject,
edge: Sequence[float] = UR,
direction: Sequence[float] = UR,
label: float | str | VMobject,
edge: Vector3D = UR,
direction: Vector3D = UR,
buff: float = SMALL_BUFF,
**kwargs,
**kwargs: Any,
) -> Mobject:
"""Generate an x-axis label.
@ -301,11 +305,11 @@ class CoordinateSystem:
def get_y_axis_label(
self,
label: float | str | Mobject,
edge: Sequence[float] = UR,
direction: Sequence[float] = UP * 0.5 + RIGHT,
label: float | str | VMobject,
edge: Vector3D = UR,
direction: Vector3D = UP * 0.5 + RIGHT,
buff: float = SMALL_BUFF,
**kwargs,
**kwargs: Any,
) -> Mobject:
"""Generate a y-axis label.
@ -347,10 +351,10 @@ class CoordinateSystem:
def _get_axis_label(
self,
label: float | str | Mobject,
label: float | str | VMobject,
axis: Mobject,
edge: Sequence[float],
direction: Sequence[float],
edge: Vector3DLike,
direction: Vector3DLike,
buff: float = SMALL_BUFF,
) -> Mobject:
"""Gets the label for an axis.
@ -373,12 +377,14 @@ class CoordinateSystem:
:class:`~.Mobject`
The positioned label along the given axis.
"""
label = self.x_axis._create_label_tex(label)
label.next_to(axis.get_edge_center(edge), direction=direction, buff=buff)
label.shift_onto_screen(buff=MED_SMALL_BUFF)
return label
label_mobject: Mobject = self.x_axis._create_label_tex(label)
label_mobject.next_to(
axis.get_edge_center(edge), direction=direction, buff=buff
)
label_mobject.shift_onto_screen(buff=MED_SMALL_BUFF)
return label_mobject
def get_axis_labels(self):
def get_axis_labels(self) -> VGroup:
raise NotImplementedError()
def add_coordinates(
@ -431,14 +437,20 @@ class CoordinateSystem:
if not axes_numbers:
axes_numbers = [None for _ in range(self.dimension)]
for axis, values in zip(self.axes, axes_numbers):
for axis, values in zip(self.axes, axes_numbers, strict=False):
if isinstance(values, dict):
axis.add_labels(values, **kwargs)
labels = axis.labels
elif values is None and axis.scaling.custom_labels:
tick_range = axis.get_tick_range()
axis.add_labels(
dict(zip(tick_range, axis.scaling.get_custom_labels(tick_range)))
dict(
zip(
tick_range,
axis.scaling.get_custom_labels(tick_range),
strict=True,
)
)
)
labels = axis.labels
else:
@ -453,7 +465,7 @@ class CoordinateSystem:
def get_line_from_axis_to_point(
self,
index: int,
point: Sequence[float],
point: Point3DLike,
line_config: dict | None = ...,
color: ParsableManimColor | None = ...,
stroke_width: float = ...,
@ -463,7 +475,7 @@ class CoordinateSystem:
def get_line_from_axis_to_point(
self,
index: int,
point: Sequence[float],
point: Point3DLike,
line_func: type[LineType],
line_config: dict | None = ...,
color: ParsableManimColor | None = ...,
@ -518,7 +530,7 @@ class CoordinateSystem:
line = line_func(axis.get_projection(point), point, **line_config)
return line
def get_vertical_line(self, point: Sequence[float], **kwargs: Any) -> Line:
def get_vertical_line(self, point: Point3DLike, **kwargs: Any) -> Line:
"""A vertical line from the x-axis to a given point in the scene.
Parameters
@ -552,7 +564,7 @@ class CoordinateSystem:
"""
return self.get_line_from_axis_to_point(0, point, **kwargs)
def get_horizontal_line(self, point: Sequence[float], **kwargs) -> Line:
def get_horizontal_line(self, point: Point3DLike, **kwargs: Any) -> Line:
"""A horizontal line from the y-axis to a given point in the scene.
Parameters
@ -584,7 +596,7 @@ class CoordinateSystem:
"""
return self.get_line_from_axis_to_point(1, point, **kwargs)
def get_lines_to_point(self, point: Sequence[float], **kwargs) -> VGroup:
def get_lines_to_point(self, point: Point3DLike, **kwargs: Any) -> VGroup:
"""Generate both horizontal and vertical lines from the axis to a point.
Parameters
@ -630,7 +642,9 @@ class CoordinateSystem:
function: Callable[[float], float],
x_range: Sequence[float] | None = None,
use_vectorized: bool = False,
colorscale: Union[Iterable[Color], Iterable[Color, float]] | None = None,
colorscale: Iterable[ParsableManimColor]
| Iterable[ParsableManimColor, float]
| None = None,
colorscale_axis: int = 1,
**kwargs: Any,
) -> ParametricFunction:
@ -1093,7 +1107,7 @@ class CoordinateSystem:
def get_graph_label(
self,
graph: ParametricFunction,
label: float | str | Mobject = "f(x)",
label: float | str | VMobject = "f(x)",
x_val: float | None = None,
direction: Sequence[float] = RIGHT,
buff: float = MED_SMALL_BUFF,
@ -1150,7 +1164,7 @@ class CoordinateSystem:
dot_config = {}
if color is None:
color = graph.get_color()
label = self.x_axis._create_label_tex(label).set_color(color)
label_object: Mobject = self.x_axis._create_label_tex(label).set_color(color)
if x_val is None:
# Search from right to left
@ -1161,14 +1175,14 @@ class CoordinateSystem:
else:
point = self.input_to_graph_point(x_val, graph)
label.next_to(point, direction, buff=buff)
label.shift_onto_screen()
label_object.next_to(point, direction, buff=buff)
label_object.shift_onto_screen()
if dot:
dot = Dot(point=point, **dot_config)
label.add(dot)
label.dot = dot
return label
label_object.add(dot)
label_object.dot = dot
return label_object
# calculus
@ -1176,14 +1190,14 @@ class CoordinateSystem:
self,
graph: ParametricFunction,
x_range: Sequence[float] | None = None,
dx: float | None = 0.1,
dx: float = 0.1,
input_sample_type: str = "left",
stroke_width: float = 1,
stroke_color: ParsableManimColor = BLACK,
fill_opacity: float = 1,
color: Iterable[ParsableManimColor] | ParsableManimColor = (BLUE, GREEN),
show_signed_area: bool = True,
bounded_graph: ParametricFunction = None,
bounded_graph: ParametricFunction | None = None,
blend: bool = False,
width_scale_factor: float = 1.001,
) -> VGroup:
@ -1277,16 +1291,16 @@ class CoordinateSystem:
x_range = [*x_range[:2], dx]
rectangles = VGroup()
x_range = np.arange(*x_range)
x_range_array = np.arange(*x_range)
if isinstance(color, (list, tuple)):
color = [ManimColor(c) for c in color]
else:
color = [ManimColor(color)]
colors = color_gradient(color, len(x_range))
colors = color_gradient(color, len(x_range_array))
for x, color in zip(x_range, colors):
for x, color in zip(x_range_array, colors, strict=True):
if input_sample_type == "left":
sample_input = x
elif input_sample_type == "right":
@ -1341,7 +1355,7 @@ class CoordinateSystem:
x_range: tuple[float, float] | None = None,
color: ParsableManimColor | Iterable[ParsableManimColor] = (BLUE, GREEN),
opacity: float = 0.3,
bounded_graph: ParametricFunction = None,
bounded_graph: ParametricFunction | None = None,
**kwargs: Any,
) -> Polygon:
"""Returns a :class:`~.Polygon` representing the area under the graph passed.
@ -1485,10 +1499,14 @@ class CoordinateSystem:
ax.slope_of_tangent(x=-2, graph=curve)
# -3.5000000259052038
"""
return np.tan(self.angle_of_tangent(x, graph, **kwargs))
val: float = np.tan(self.angle_of_tangent(x, graph, **kwargs))
return val
def plot_derivative_graph(
self, graph: ParametricFunction, color: ParsableManimColor = GREEN, **kwargs
self,
graph: ParametricFunction,
color: ParsableManimColor = GREEN,
**kwargs: Any,
) -> ParametricFunction:
"""Returns the curve of the derivative of the passed graph.
@ -1526,7 +1544,7 @@ class CoordinateSystem:
self.add(ax, curves, labels)
"""
def deriv(x):
def deriv(x: float) -> float:
return self.slope_of_tangent(x, graph)
return self.plot(deriv, color=color, **kwargs)
@ -1587,7 +1605,7 @@ class CoordinateSystem:
x_vals = np.linspace(0, x, samples, axis=1 if use_vectorized else 0)
f_vec = np.vectorize(graph.underlying_function)
y_vals = f_vec(x_vals)
return np.trapz(y_vals, x_vals) + y_intercept
return np.trapezoid(y_vals, x_vals) + y_intercept
return self.plot(antideriv, use_vectorized=use_vectorized, **kwargs)
@ -1596,7 +1614,7 @@ class CoordinateSystem:
x: float,
graph: ParametricFunction,
dx: float | None = None,
dx_line_color: ParsableManimColor = YELLOW,
dx_line_color: ParsableManimColor = PURE_YELLOW,
dy_line_color: ParsableManimColor | None = None,
dx_label: float | str | None = None,
dy_label: float | str | None = None,
@ -1778,7 +1796,7 @@ class CoordinateSystem:
triangle_size: float = MED_SMALL_BUFF,
triangle_color: ParsableManimColor | None = WHITE,
line_func: type[Line] = Line,
line_color: ParsableManimColor = YELLOW,
line_color: ParsableManimColor = PURE_YELLOW,
) -> VGroup:
"""Creates a labelled triangle marker with a vertical line from the x-axis
to a curve at a given x-value.
@ -1843,14 +1861,17 @@ class CoordinateSystem:
return T_label_group
def __matmul__(self, coord: Point3DLike | Mobject):
def __matmul__(self, coord: Point3DLike | Mobject) -> Point3DLike:
if isinstance(coord, Mobject):
coord = coord.get_center()
return self.coords_to_point(*coord)
def __rmatmul__(self, point: Point3DLike):
def __rmatmul__(self, point: Point3DLike) -> Point3DLike:
return self.point_to_coords(point)
@staticmethod
def _origin_shift(axis_range: Sequence[float]) -> float: ...
class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
"""Creates a set of axes.
@ -1918,7 +1939,7 @@ class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
y_axis_config: dict | None = None,
tips: bool = True,
**kwargs: Any,
) -> None:
):
VGroup.__init__(self, **kwargs)
CoordinateSystem.__init__(self, x_range, y_range, x_length, y_length)
@ -1926,8 +1947,11 @@ class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
"include_tip": tips,
"numbers_to_exclude": [0],
}
self.x_axis_config = {}
self.y_axis_config = {"rotation": 90 * DEGREES, "label_direction": LEFT}
self.x_axis_config: dict[str, Any] = {}
self.y_axis_config: dict[str, Any] = {
"rotation": 90 * DEGREES,
"label_direction": LEFT,
}
self._update_default_configs(
(self.axis_config, self.x_axis_config, self.y_axis_config),
@ -2012,7 +2036,9 @@ class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
)
)
"""
for default_config, passed_config in zip(default_configs, passed_configs):
for default_config, passed_config in zip(
default_configs, passed_configs, strict=False
):
if passed_config is not None:
update_dict_recursively(default_config, passed_config)
@ -2063,6 +2089,10 @@ class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
``ax.coords_to_point( [[x_0, y_0, z_0], [x_1, y_1, z_1]] )``
A single coordinate can also be passed as a flat list or 1D array:
``ax.coords_to_point( [x, y, z] )``
Returns
-------
np.ndarray
@ -2091,6 +2121,10 @@ class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
array([[0. , 0.86, 0.86],
[0.75, 0.75, 0. ],
[0. , 0. , 0. ]])
>>> np.around(ax.coords_to_point([1, 0, 0]), 2)
array([0.86, 0. , 0. ])
>>> np.around(ax.coords_to_point(np.array([1, 0])), 2)
array([0.86, 0. , 0. ])
.. manim:: CoordsToPointExample
:save_last_frame:
@ -2133,6 +2167,10 @@ class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
else:
coords = coords.T
are_coordinates_transposed = True
# If coords is in the format ([x, y, z]) -- a single flat list/array passed as one argument:
elif coords.ndim == 2 and coords.shape[0] == 1:
# Extract the single list so [x, y, z] is treated like c2p(x, y, z).
coords = coords[0]
# Otherwise, coords already looked like (x, y, z) or ([x1 x2 ...], [y1 y2 ...], [z1 z2 ...]),
# so no further processing is needed.
@ -2142,7 +2180,7 @@ class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
# Although "points" and "nums" are in plural, there might be a single point or number.
points = self.x_axis.number_to_point(coords[0])
other_axes = self.axes.submobjects[1:]
for axis, nums in zip(other_axes, coords[1:]):
for axis, nums in zip(other_axes, coords[1:], strict=False):
points += axis.number_to_point(nums) - origin
# Return points as is, except if coords originally looked like
@ -2267,7 +2305,7 @@ class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
x_values: Iterable[float],
y_values: Iterable[float],
z_values: Iterable[float] | None = None,
line_color: ParsableManimColor = YELLOW,
line_color: ParsableManimColor = PURE_YELLOW,
add_vertex_dots: bool = True,
vertex_dot_radius: float = DEFAULT_DOT_RADIUS,
vertex_dot_style: dict[str, Any] | None = None,
@ -2336,7 +2374,7 @@ class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
vertices = [
self.coords_to_point(x, y, z)
for x, y, z in zip(x_values, y_values, z_values)
for x, y, z in zip(x_values, y_values, z_values, strict=True)
]
graph.set_points_as_corners(vertices)
line_graph["line_graph"] = graph
@ -2414,14 +2452,14 @@ class ThreeDAxes(Axes):
y_length: float | None = config.frame_height + 2.5,
z_length: float | None = config.frame_height - 1.5,
z_axis_config: dict[str, Any] | None = None,
z_normal: Vector3D = DOWN,
z_normal: Vector3DLike = DOWN,
num_axis_pieces: int = 20,
light_source: Sequence[float] = 9 * DOWN + 7 * LEFT + 10 * OUT,
light_source: Point3DLike = 9 * DOWN + 7 * LEFT + 10 * OUT,
# opengl stuff (?)
depth=None,
gloss=0.5,
depth: Any = None,
gloss: float = 0.5,
**kwargs: dict[str, Any],
) -> None:
):
super().__init__(
x_range=x_range,
x_length=x_length,
@ -2433,7 +2471,7 @@ class ThreeDAxes(Axes):
self.z_range = z_range
self.z_length = z_length
self.z_axis_config = {}
self.z_axis_config: dict[str, Any] = {}
self._update_default_configs((self.z_axis_config,), (z_axis_config,))
self.z_axis_config = merge_dicts_recursively(
self.axis_config,
@ -2443,7 +2481,7 @@ class ThreeDAxes(Axes):
self.z_normal = z_normal
self.num_axis_pieces = num_axis_pieces
self.light_source = light_source
self.light_source = np.array(light_source)
self.dimension = 3
@ -2500,13 +2538,13 @@ class ThreeDAxes(Axes):
def get_y_axis_label(
self,
label: float | str | Mobject,
edge: Sequence[float] = UR,
direction: Sequence[float] = UR,
label: float | str | VMobject,
edge: Vector3DLike = UR,
direction: Vector3DLike = UR,
buff: float = SMALL_BUFF,
rotation: float = PI / 2,
rotation_axis: Vector3D = OUT,
**kwargs,
rotation_axis: Vector3DLike = OUT,
**kwargs: dict[str, Any],
) -> Mobject:
"""Generate a y-axis label.
@ -2550,12 +2588,12 @@ class ThreeDAxes(Axes):
def get_z_axis_label(
self,
label: float | str | Mobject,
edge: Vector3D = OUT,
direction: Vector3D = RIGHT,
label: float | str | VMobject,
edge: Vector3DLike = OUT,
direction: Vector3DLike = RIGHT,
buff: float = SMALL_BUFF,
rotation: float = PI / 2,
rotation_axis: Vector3D = RIGHT,
rotation_axis: Vector3DLike = RIGHT,
**kwargs: Any,
) -> Mobject:
"""Generate a z-axis label.
@ -2600,9 +2638,9 @@ class ThreeDAxes(Axes):
def get_axis_labels(
self,
x_label: float | str | Mobject = "x",
y_label: float | str | Mobject = "y",
z_label: float | str | Mobject = "z",
x_label: float | str | VMobject = "x",
y_label: float | str | VMobject = "y",
z_label: float | str | VMobject = "z",
) -> VGroup:
"""Defines labels for the x_axis and y_axis of the graph.
@ -2741,7 +2779,7 @@ class NumberPlane(Axes):
**kwargs: dict[str, Any],
):
# configs
self.axis_config = {
self.axis_config: dict[str, Any] = {
"stroke_width": 2,
"include_ticks": False,
"include_tip": False,
@ -2749,8 +2787,8 @@ class NumberPlane(Axes):
"label_direction": DR,
"font_size": 24,
}
self.y_axis_config = {"label_direction": DR}
self.background_line_style = {
self.y_axis_config: dict[str, Any] = {"label_direction": DR}
self.background_line_style: dict[str, Any] = {
"stroke_color": BLUE_D,
"stroke_width": 2,
"stroke_opacity": 1,
@ -2997,7 +3035,7 @@ class PolarPlane(Axes):
size: float | None = None,
radius_step: float = 1,
azimuth_step: float | None = None,
azimuth_units: str | None = "PI radians",
azimuth_units: str = "PI radians",
azimuth_compact_fraction: bool = True,
azimuth_offset: float = 0,
azimuth_direction: str = "CCW",
@ -3009,7 +3047,7 @@ class PolarPlane(Axes):
faded_line_ratio: int = 1,
make_smooth_after_applying_functions: bool = True,
**kwargs: Any,
) -> None:
):
# error catching
if azimuth_units in ["PI radians", "TAU radians", "degrees", "gradians", None]:
self.azimuth_units = azimuth_units
@ -3130,11 +3168,11 @@ class PolarPlane(Axes):
unit_vector = self.x_axis.get_unit_vector()[0]
for k, x in enumerate(rinput):
new_line = Circle(radius=x * unit_vector)
new_circle = Circle(radius=x * unit_vector)
if k % ratio_faded_lines == 0:
alines1.add(new_line)
alines1.add(new_circle)
else:
alines2.add(new_line)
alines2.add(new_circle)
line = Line(center, self.get_x_axis().get_end())
@ -3219,6 +3257,7 @@ class PolarPlane(Axes):
}
for i in a_values
]
a_tex = []
if self.azimuth_units == "PI radians" or self.azimuth_units == "TAU radians":
a_tex = [
self.get_radian_label(
@ -3292,7 +3331,9 @@ class PolarPlane(Axes):
self.add(self.get_coordinate_labels(r_values, a_values))
return self
def get_radian_label(self, number, font_size: float = 24, **kwargs: Any) -> MathTex:
def get_radian_label(
self, number: float, font_size: float = 24, **kwargs: Any
) -> MathTex:
constant_label = {"PI radians": r"\pi", "TAU radians": r"\tau"}[
self.azimuth_units
]
@ -3361,7 +3402,7 @@ class ComplexPlane(NumberPlane):
"""
def __init__(self, **kwargs: Any) -> None:
def __init__(self, **kwargs: Any):
super().__init__(
**kwargs,
)

View file

@ -5,8 +5,8 @@ from __future__ import annotations
__all__ = ["ParametricFunction", "FunctionGraph", "ImplicitFunction"]
from collections.abc import Iterable, Sequence
from typing import TYPE_CHECKING, Callable
from collections.abc import Callable, Iterable, Sequence
from typing import TYPE_CHECKING
import numpy as np
from isosurfaces import plot_isoline
@ -17,11 +17,12 @@ from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from manim.mobject.types.vectorized_mobject import VMobject
if TYPE_CHECKING:
from typing_extensions import Self
from typing import Any, Self
from manim.typing import Point3D, Point3DLike
from manim.utils.color import ParsableManimColor
from manim.utils.color import YELLOW
from manim.utils.color import PURE_YELLOW
class ParametricFunction(VMobject, metaclass=ConvertToOpenGL):
@ -111,7 +112,7 @@ class ParametricFunction(VMobject, metaclass=ConvertToOpenGL):
discontinuities: Iterable[float] | None = None,
use_smoothing: bool = True,
use_vectorized: bool = False,
**kwargs,
**kwargs: Any,
):
def internal_parametric_function(t: float) -> Point3D:
"""Wrap ``function``'s output inside a NumPy array."""
@ -143,20 +144,20 @@ class ParametricFunction(VMobject, metaclass=ConvertToOpenGL):
lambda t: self.t_min <= t <= self.t_max,
self.discontinuities,
)
discontinuities = np.array(list(discontinuities))
discontinuities_array = np.array(list(discontinuities))
boundary_times = np.array(
[
self.t_min,
self.t_max,
*(discontinuities - self.dt),
*(discontinuities + self.dt),
*(discontinuities_array - self.dt),
*(discontinuities_array + self.dt),
],
)
boundary_times.sort()
else:
boundary_times = [self.t_min, self.t_max]
for t1, t2 in zip(boundary_times[0::2], boundary_times[1::2]):
for t1, t2 in zip(boundary_times[0::2], boundary_times[1::2], strict=True):
t_range = np.array(
[
*self.scaling.function(np.arange(t1, t2, self.t_step)),
@ -179,7 +180,8 @@ class ParametricFunction(VMobject, metaclass=ConvertToOpenGL):
self.make_smooth()
return self
init_points = generate_points
def init_points(self) -> None:
self.generate_points()
class FunctionGraph(ParametricFunction):
@ -211,19 +213,27 @@ class FunctionGraph(ParametricFunction):
self.add(cos_func, sin_func_1, sin_func_2)
"""
def __init__(self, function, x_range=None, color=YELLOW, **kwargs):
def __init__(
self,
function: Callable[[float], Any],
x_range: tuple[float, float] | tuple[float, float, float] | None = None,
color: ParsableManimColor = PURE_YELLOW,
**kwargs: Any,
) -> None:
if x_range is None:
x_range = np.array([-config["frame_x_radius"], config["frame_x_radius"]])
x_range = (-config["frame_x_radius"], config["frame_x_radius"])
self.x_range = x_range
self.parametric_function = lambda t: np.array([t, function(t), 0])
self.function = function
self.parametric_function: Callable[[float], Point3D] = lambda t: np.array(
[t, function(t), 0]
)
self.function = function # type: ignore[assignment]
super().__init__(self.parametric_function, self.x_range, color=color, **kwargs)
def get_function(self):
def get_function(self) -> Callable[[float], Any]:
return self.function
def get_point_from_function(self, x):
def get_point_from_function(self, x: float) -> Point3D:
return self.parametric_function(x)
@ -236,7 +246,7 @@ class ImplicitFunction(VMobject, metaclass=ConvertToOpenGL):
min_depth: int = 5,
max_quads: int = 1500,
use_smoothing: bool = True,
**kwargs,
**kwargs: Any,
):
"""An implicit function.
@ -295,7 +305,7 @@ class ImplicitFunction(VMobject, metaclass=ConvertToOpenGL):
super().__init__(**kwargs)
def generate_points(self):
def generate_points(self) -> Self:
p_min, p_max = (
np.array([self.x_range[0], self.y_range[0]]),
np.array([self.x_range[1], self.y_range[1]]),
@ -317,4 +327,5 @@ class ImplicitFunction(VMobject, metaclass=ConvertToOpenGL):
self.make_smooth()
return self
init_points = generate_points
def init_points(self) -> None:
self.generate_points()

View file

@ -8,12 +8,14 @@ from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
__all__ = ["NumberLine", "UnitInterval"]
from collections.abc import Iterable, Sequence
from typing import TYPE_CHECKING, Callable
from collections.abc import Callable, Iterable, Sequence
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Any, Self
from manim.mobject.geometry.tips import ArrowTip
from manim.typing import Point3DLike
from manim.typing import Point3D, Point3DLike, Vector3D
import numpy as np
@ -21,8 +23,9 @@ from manim import config
from manim.constants import *
from manim.mobject.geometry.line import Line
from manim.mobject.graphing.scale import LinearBase, _ScaleBase
from manim.mobject.text.numbers import DecimalNumber
from manim.mobject.text.numbers import DecimalNumber, Integer
from manim.mobject.text.tex_mobject import MathTex, Tex
from manim.mobject.text.text_mobject import Text
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
from manim.utils.bezier import interpolate
from manim.utils.config_ops import merge_dicts_recursively
@ -157,14 +160,14 @@ class NumberLine(Line):
# numbers/labels
include_numbers: bool = False,
font_size: float = 36,
label_direction: Sequence[float] = DOWN,
label_constructor: VMobject = MathTex,
label_direction: Point3DLike = DOWN,
label_constructor: type[MathTex] = MathTex,
scaling: _ScaleBase = LinearBase(),
line_to_number_buff: float = MED_SMALL_BUFF,
decimal_number_config: dict | None = None,
numbers_to_exclude: Iterable[float] | None = None,
numbers_to_include: Iterable[float] | None = None,
**kwargs,
**kwargs: Any,
):
# avoid mutable arguments in defaults
if numbers_to_exclude is None:
@ -189,6 +192,9 @@ class NumberLine(Line):
# turn into a NumPy array to scale by just applying the function
self.x_range = np.array(x_range, dtype=float)
self.x_min: float
self.x_max: float
self.x_step: float
self.x_min, self.x_max, self.x_step = scaling.function(self.x_range)
self.length = length
self.unit_size = unit_size
@ -246,16 +252,17 @@ class NumberLine(Line):
if self.scaling.custom_labels:
tick_range = self.get_tick_range()
custom_labels = self.scaling.get_custom_labels(
tick_range,
unit_decimal_places=decimal_number_config["num_decimal_places"],
)
self.add_labels(
dict(
zip(
tick_range,
self.scaling.get_custom_labels(
tick_range,
unit_decimal_places=decimal_number_config[
"num_decimal_places"
],
),
custom_labels,
strict=True,
)
),
)
@ -267,21 +274,25 @@ class NumberLine(Line):
font_size=self.font_size,
)
def rotate_about_zero(self, angle: float, axis: Sequence[float] = OUT, **kwargs):
def rotate_about_zero(
self, angle: float, axis: Vector3D = OUT, **kwargs: Any
) -> Self:
return self.rotate_about_number(0, angle, axis, **kwargs)
def rotate_about_number(
self, number: float, angle: float, axis: Sequence[float] = OUT, **kwargs
):
self, number: float, angle: float, axis: Vector3D = OUT, **kwargs: Any
) -> Self:
return self.rotate(angle, axis, about_point=self.n2p(number), **kwargs)
def add_ticks(self):
def add_ticks(self) -> None:
"""Adds ticks to the number line. Ticks can be accessed after creation
via ``self.ticks``.
"""
ticks = VGroup()
elongated_tick_size = self.tick_size * self.longer_tick_multiple
elongated_tick_offsets = self.numbers_with_elongated_ticks - self.x_min
elongated_tick_offsets = (
np.array(self.numbers_with_elongated_ticks) - self.x_min
)
for x in self.get_tick_range():
size = self.tick_size
if np.any(np.isclose(x - self.x_min, elongated_tick_offsets)):
@ -413,31 +424,34 @@ class NumberLine(Line):
point = np.asarray(point)
start, end = self.get_start_and_end()
unit_vect = normalize(end - start)
proportion = np.dot(point - start, unit_vect) / np.dot(end - start, unit_vect)
proportion: float = np.dot(point - start, unit_vect) / np.dot(
end - start, unit_vect
)
return interpolate(self.x_min, self.x_max, proportion)
def n2p(self, number: float | np.ndarray) -> np.ndarray:
def n2p(self, number: float | np.ndarray) -> Point3D:
"""Abbreviation for :meth:`~.NumberLine.number_to_point`."""
return self.number_to_point(number)
def p2n(self, point: Sequence[float]) -> float:
def p2n(self, point: Point3DLike) -> float:
"""Abbreviation for :meth:`~.NumberLine.point_to_number`."""
return self.point_to_number(point)
def get_unit_size(self) -> float:
return self.get_length() / (self.x_range[1] - self.x_range[0])
val: float = self.get_length() / (self.x_range[1] - self.x_range[0])
return val
def get_unit_vector(self) -> np.ndarray:
def get_unit_vector(self) -> Vector3D:
return super().get_unit_vector() * self.unit_size
def get_number_mobject(
self,
x: float,
direction: Sequence[float] | None = None,
direction: Vector3D | None = None,
buff: float | None = None,
font_size: float | None = None,
label_constructor: VMobject | None = None,
**number_config,
label_constructor: type[MathTex] | None = None,
**number_config: dict[str, Any],
) -> VMobject:
"""Generates a positioned :class:`~.DecimalNumber` mobject
generated according to ``label_constructor``.
@ -462,7 +476,7 @@ class NumberLine(Line):
:class:`~.DecimalNumber`
The positioned mobject.
"""
number_config = merge_dicts_recursively(
number_config_merged = merge_dicts_recursively(
self.decimal_number_config,
number_config,
)
@ -476,7 +490,10 @@ class NumberLine(Line):
label_constructor = self.label_constructor
num_mob = DecimalNumber(
x, font_size=font_size, mob_class=label_constructor, **number_config
x,
font_size=font_size,
mob_class=label_constructor,
**number_config_merged,
)
num_mob.next_to(self.number_to_point(x), direction=direction, buff=buff)
@ -485,7 +502,7 @@ class NumberLine(Line):
num_mob.shift(num_mob[0].width * LEFT / 2)
return num_mob
def get_number_mobjects(self, *numbers, **kwargs) -> VGroup:
def get_number_mobjects(self, *numbers: float, **kwargs: Any) -> VGroup:
if len(numbers) == 0:
numbers = self.default_numbers_to_display()
return VGroup([self.get_number_mobject(number, **kwargs) for number in numbers])
@ -498,9 +515,9 @@ class NumberLine(Line):
x_values: Iterable[float] | None = None,
excluding: Iterable[float] | None = None,
font_size: float | None = None,
label_constructor: VMobject | None = None,
**kwargs,
):
label_constructor: type[MathTex] | None = None,
**kwargs: Any,
) -> Self:
"""Adds :class:`~.DecimalNumber` mobjects representing their position
at each tick of the number line. The numbers can be accessed after creation
via ``self.numbers``.
@ -551,11 +568,11 @@ class NumberLine(Line):
def add_labels(
self,
dict_values: dict[float, str | float | VMobject],
direction: Sequence[float] = None,
direction: Point3DLike | None = None,
buff: float | None = None,
font_size: float | None = None,
label_constructor: VMobject | None = None,
):
label_constructor: type[MathTex] | None = None,
) -> Self:
"""Adds specifically positioned labels to the :class:`~.NumberLine` using a ``dict``.
The labels can be accessed after creation via ``self.labels``.
@ -598,6 +615,7 @@ class NumberLine(Line):
label = self._create_label_tex(label, label_constructor)
if hasattr(label, "font_size"):
assert isinstance(label, (MathTex, Tex, Text, Integer)), label
label.font_size = font_size
else:
raise AttributeError(f"{label} is not compatible with add_labels.")
@ -612,7 +630,7 @@ class NumberLine(Line):
self,
label_tex: str | float | VMobject,
label_constructor: Callable | None = None,
**kwargs,
**kwargs: Any,
) -> VMobject:
"""Checks if the label is a :class:`~.VMobject`, otherwise, creates a
label by passing ``label_tex`` to ``label_constructor``.
@ -633,24 +651,25 @@ class NumberLine(Line):
:class:`~.VMobject`
The label.
"""
if label_constructor is None:
label_constructor = self.label_constructor
if isinstance(label_tex, (VMobject, OpenGLVMobject)):
return label_tex
else:
if label_constructor is None:
label_constructor = self.label_constructor
if isinstance(label_tex, str):
return label_constructor(label_tex, **kwargs)
return label_constructor(str(label_tex), **kwargs)
@staticmethod
def _decimal_places_from_step(step) -> int:
step = str(step)
if "." not in step:
def _decimal_places_from_step(step: float) -> int:
step_str = str(step)
if "." not in step_str:
return 0
return len(step.split(".")[-1])
return len(step_str.split(".")[-1])
def __matmul__(self, other: float):
def __matmul__(self, other: float) -> Point3D:
return self.n2p(other)
def __rmatmul__(self, other: Point3DLike | Mobject):
def __rmatmul__(self, other: Point3DLike | Mobject) -> float:
if isinstance(other, Mobject):
other = other.get_center()
return self.p2n(other)
@ -659,10 +678,10 @@ class NumberLine(Line):
class UnitInterval(NumberLine):
def __init__(
self,
unit_size=10,
numbers_with_elongated_ticks=None,
decimal_number_config=None,
**kwargs,
unit_size: float = 10,
numbers_with_elongated_ticks: list[float] | None = None,
decimal_number_config: dict[str, Any] | None = None,
**kwargs: Any,
):
numbers_with_elongated_ticks = (
[0, 1]

View file

@ -6,6 +6,7 @@ __all__ = ["SampleSpace", "BarChart"]
from collections.abc import Iterable, MutableSequence, Sequence
from typing import Any
import numpy as np
@ -13,11 +14,11 @@ from manim import config, logger
from manim.constants import *
from manim.mobject.geometry.polygram import Rectangle
from manim.mobject.graphing.coordinate_systems import Axes
from manim.mobject.mobject import Mobject
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
from manim.mobject.svg.brace import Brace
from manim.mobject.text.tex_mobject import MathTex, Tex
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
from manim.typing import Vector3D
from manim.utils.color import (
BLUE_E,
DARK_GREY,
@ -54,13 +55,13 @@ class SampleSpace(Rectangle):
def __init__(
self,
height=3,
width=3,
fill_color=DARK_GREY,
fill_opacity=1,
stroke_width=0.5,
stroke_color=LIGHT_GREY,
default_label_scale_val=1,
height: float = 3,
width: float = 3,
fill_color: ParsableManimColor = DARK_GREY,
fill_opacity: float = 1,
stroke_width: float = 0.5,
stroke_color: ParsableManimColor = LIGHT_GREY,
default_label_scale_val: float = 1,
):
super().__init__(
height=height,
@ -72,7 +73,9 @@ class SampleSpace(Rectangle):
)
self.default_label_scale_val = default_label_scale_val
def add_title(self, title="Sample space", buff=MED_SMALL_BUFF):
def add_title(
self, title: str = "Sample space", buff: float = MED_SMALL_BUFF
) -> None:
# TODO, should this really exist in SampleSpaceScene
title_mob = Tex(title)
if title_mob.width > self.width:
@ -81,23 +84,30 @@ class SampleSpace(Rectangle):
self.title = title_mob
self.add(title_mob)
def add_label(self, label):
def add_label(self, label: str) -> None:
self.label = label
def complete_p_list(self, p_list):
new_p_list = list(tuplify(p_list))
def complete_p_list(self, p_list: float | Iterable[float]) -> list[float]:
p_list_tuplified: tuple[float] = tuplify(p_list)
new_p_list = list(p_list_tuplified)
remainder = 1.0 - sum(new_p_list)
if abs(remainder) > EPSILON:
new_p_list.append(remainder)
return new_p_list
def get_division_along_dimension(self, p_list, dim, colors, vect):
p_list = self.complete_p_list(p_list)
colors = color_gradient(colors, len(p_list))
def get_division_along_dimension(
self,
p_list: float | Iterable[float],
dim: int,
colors: Sequence[ParsableManimColor],
vect: Vector3D,
) -> VGroup:
p_list_complete = self.complete_p_list(p_list)
colors_in_gradient = color_gradient(colors, len(p_list_complete))
last_point = self.get_edge_center(-vect)
parts = VGroup()
for factor, color in zip(p_list, colors):
for factor, color in zip(p_list_complete, colors_in_gradient, strict=True):
part = SampleSpace()
part.set_fill(color, 1)
part.replace(self, stretch=True)
@ -107,33 +117,43 @@ class SampleSpace(Rectangle):
parts.add(part)
return parts
def get_horizontal_division(self, p_list, colors=[GREEN_E, BLUE_E], vect=DOWN):
def get_horizontal_division(
self,
p_list: float | Iterable[float],
colors: Sequence[ParsableManimColor] = [GREEN_E, BLUE_E],
vect: Vector3D = DOWN,
) -> VGroup:
return self.get_division_along_dimension(p_list, 1, colors, vect)
def get_vertical_division(self, p_list, colors=[MAROON_B, YELLOW], vect=RIGHT):
def get_vertical_division(
self,
p_list: float | Iterable[float],
colors: Sequence[ParsableManimColor] = [MAROON_B, YELLOW],
vect: Vector3D = RIGHT,
) -> VGroup:
return self.get_division_along_dimension(p_list, 0, colors, vect)
def divide_horizontally(self, *args, **kwargs):
def divide_horizontally(self, *args: Any, **kwargs: Any) -> None:
self.horizontal_parts = self.get_horizontal_division(*args, **kwargs)
self.add(self.horizontal_parts)
def divide_vertically(self, *args, **kwargs):
def divide_vertically(self, *args: Any, **kwargs: Any) -> None:
self.vertical_parts = self.get_vertical_division(*args, **kwargs)
self.add(self.vertical_parts)
def get_subdivision_braces_and_labels(
self,
parts,
labels,
direction,
buff=SMALL_BUFF,
min_num_quads=1,
):
parts: VGroup,
labels: list[str | VMobject | OpenGLVMobject],
direction: Vector3D,
buff: float = SMALL_BUFF,
min_num_quads: int = 1,
) -> VGroup:
label_mobs = VGroup()
braces = VGroup()
for label, part in zip(labels, parts):
for label, part in zip(labels, parts, strict=False):
brace = Brace(part, direction, min_num_quads=min_num_quads, buff=buff)
if isinstance(label, (Mobject, OpenGLMobject)):
if isinstance(label, (VMobject, OpenGLVMobject)):
label_mob = label
else:
label_mob = MathTex(label)
@ -141,34 +161,44 @@ class SampleSpace(Rectangle):
label_mob.next_to(brace, direction, buff)
braces.add(brace)
assert isinstance(label_mob, VMobject)
label_mobs.add(label_mob)
parts.braces = braces
parts.labels = label_mobs
parts.label_kwargs = {
parts.braces = braces # type: ignore[attr-defined]
parts.labels = label_mobs # type: ignore[attr-defined]
parts.label_kwargs = { # type: ignore[attr-defined]
"labels": label_mobs.copy(),
"direction": direction,
"buff": buff,
}
return VGroup(parts.braces, parts.labels)
def get_side_braces_and_labels(self, labels, direction=LEFT, **kwargs):
def get_side_braces_and_labels(
self,
labels: list[str | VMobject | OpenGLVMobject],
direction: Vector3D = LEFT,
**kwargs: Any,
) -> VGroup:
assert hasattr(self, "horizontal_parts")
parts = self.horizontal_parts
return self.get_subdivision_braces_and_labels(
parts, labels, direction, **kwargs
)
def get_top_braces_and_labels(self, labels, **kwargs):
def get_top_braces_and_labels(
self, labels: list[str | VMobject | OpenGLVMobject], **kwargs: Any
) -> VGroup:
assert hasattr(self, "vertical_parts")
parts = self.vertical_parts
return self.get_subdivision_braces_and_labels(parts, labels, UP, **kwargs)
def get_bottom_braces_and_labels(self, labels, **kwargs):
def get_bottom_braces_and_labels(
self, labels: list[str | VMobject | OpenGLVMobject], **kwargs: Any
) -> VGroup:
assert hasattr(self, "vertical_parts")
parts = self.vertical_parts
return self.get_subdivision_braces_and_labels(parts, labels, DOWN, **kwargs)
def add_braces_and_labels(self):
def add_braces_and_labels(self) -> None:
for attr in "horizontal_parts", "vertical_parts":
if not hasattr(self, attr):
continue
@ -177,7 +207,7 @@ class SampleSpace(Rectangle):
if hasattr(parts, subattr):
self.add(getattr(parts, subattr))
def __getitem__(self, index):
def __getitem__(self, index: int) -> VMobject:
if hasattr(self, "horizontal_parts"):
return self.horizontal_parts[index]
elif hasattr(self, "vertical_parts"):
@ -253,7 +283,7 @@ class BarChart(Axes):
bar_width: float = 0.6,
bar_fill_opacity: float = 0.7,
bar_stroke_width: float = 3,
**kwargs,
**kwargs: Any,
):
if isinstance(bar_colors, str):
logger.warning(
@ -311,7 +341,7 @@ class BarChart(Axes):
self.y_axis.add_numbers()
def _update_colors(self):
def _update_colors(self) -> None:
"""Initialize the colors of the bars of the chart.
Sets the color of ``self.bars`` via ``self.bar_colors``.
@ -321,24 +351,27 @@ class BarChart(Axes):
"""
self.bars.set_color_by_gradient(*self.bar_colors)
def _add_x_axis_labels(self):
def _add_x_axis_labels(self) -> None:
"""Essentially :meth`:~.NumberLine.add_labels`, but differs in that
the direction of the label with respect to the x_axis changes to UP or DOWN
depending on the value.
UP for negative values and DOWN for positive values.
"""
assert isinstance(self.bar_names, list)
val_range = np.arange(
0.5, len(self.bar_names), 1
) # 0.5 shifted so that labels are centered, not on ticks
labels = VGroup()
for i, (value, bar_name) in enumerate(zip(val_range, self.bar_names)):
for i, (value, bar_name) in enumerate(
zip(val_range, self.bar_names, strict=True)
):
# to accommodate negative bars, the label may need to be
# below or above the x_axis depending on the value of the bar
direction = UP if self.values[i] < 0 else DOWN
bar_name_label = self.x_axis.label_constructor(bar_name)
bar_name_label: MathTex = self.x_axis.label_constructor(bar_name)
bar_name_label.font_size = self.x_axis.font_size
bar_name_label.next_to(
@ -398,8 +431,8 @@ class BarChart(Axes):
color: ParsableManimColor | None = None,
font_size: float = 24,
buff: float = MED_SMALL_BUFF,
label_constructor: type[VMobject] = Tex,
):
label_constructor: type[MathTex] = Tex,
) -> VGroup:
"""Annotates each bar with its corresponding value. Use ``self.bar_labels`` to access the
labels after creation.
@ -430,8 +463,8 @@ class BarChart(Axes):
self.add(chart, c_bar_lbls)
"""
bar_labels = VGroup()
for bar, value in zip(self.bars, self.values):
bar_lbl = label_constructor(str(value))
for bar, value in zip(self.bars, self.values, strict=False):
bar_lbl: MathTex = label_constructor(str(value))
if color is None:
bar_lbl.set_color(bar.get_fill_color())
@ -446,7 +479,9 @@ class BarChart(Axes):
return bar_labels
def change_bar_values(self, values: Iterable[float], update_colors: bool = True):
def change_bar_values(
self, values: Iterable[float], update_colors: bool = True
) -> None:
"""Updates the height of the bars of the chart.
Parameters
@ -476,7 +511,7 @@ class BarChart(Axes):
chart.change_bar_values(list(reversed(values)))
self.add(chart.get_bar_labels(font_size=24))
"""
for i, (bar, value) in enumerate(zip(self.bars, values)):
for i, (bar, value) in enumerate(zip(self.bars, values, strict=False)):
chart_val = self.values[i]
if chart_val > 0:
@ -512,4 +547,4 @@ class BarChart(Axes):
if update_colors:
self._update_colors()
self.values[: len(values)] = values
self.values[: len(list(values))] = values

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import math
from collections.abc import Iterable
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, overload
import numpy as np
@ -11,7 +11,9 @@ __all__ = ["LogBase", "LinearBase"]
from manim.mobject.text.numbers import Integer
if TYPE_CHECKING:
from manim.mobject.mobject import Mobject
from collections.abc import Callable
from manim.mobject.types.vectorized_mobject import VMobject
class _ScaleBase:
@ -26,6 +28,12 @@ class _ScaleBase:
def __init__(self, custom_labels: bool = False):
self.custom_labels = custom_labels
@overload
def function(self, value: float) -> float: ...
@overload
def function(self, value: np.ndarray) -> np.ndarray: ...
def function(self, value: float) -> float:
"""The function that will be used to scale the values.
@ -59,7 +67,8 @@ class _ScaleBase:
def get_custom_labels(
self,
val_range: Iterable[float],
) -> Iterable[Mobject]:
**kw_args: Any,
) -> Iterable[VMobject]:
"""Custom instructions for generating labels along an axis.
Parameters
@ -147,12 +156,14 @@ class LogBase(_ScaleBase):
if isinstance(value, np.ndarray):
condition = value.any() <= 0
func: Callable[[float, float], float]
def func(value: float, base: float) -> float:
return_value: float = np.log(value) / np.log(base)
return return_value
else:
condition = value <= 0
func = math.log # type: ignore[assignment]
func = math.log
if condition:
raise ValueError(
@ -179,7 +190,7 @@ class LogBase(_ScaleBase):
Additional arguments to be passed to :class:`~.Integer`.
"""
# uses `format` syntax to control the number of decimal places.
tex_labels = [
tex_labels: list[Integer] = [
Integer(
self.base,
unit="^{%s}" % (f"{self.inverse_function(i):.{unit_decimal_places}f}"), # noqa: UP031

View file

@ -4,8 +4,9 @@ from __future__ import annotations
__all__ = ["ManimBanner"]
from typing import Any
import svgelements as se
from typing_extensions import Any
from manim.animation.updaters.update import UpdateFromAlphaFunc
from manim.mobject.geometry.arc import Circle

View file

@ -40,14 +40,15 @@ __all__ = [
import itertools as it
from collections.abc import 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
@ -56,7 +57,7 @@ from ..mobject.types.vectorized_mobject import VGroup, VMobject
# Not sure if we should keep it or not.
def matrix_to_tex_string(matrix):
def matrix_to_tex_string(matrix: np.ndarray) -> str:
matrix = np.array(matrix).astype("str")
if matrix.ndim == 1:
matrix = matrix.reshape((matrix.size, 1))
@ -67,7 +68,7 @@ def matrix_to_tex_string(matrix):
return prefix + " \\\\ ".join(rows) + suffix
def matrix_to_mobject(matrix):
def matrix_to_mobject(matrix: np.ndarray) -> MathTex:
return MathTex(matrix_to_tex_string(matrix))
@ -163,21 +164,21 @@ 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[MathTex] = 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,
bracket_config: dict = {},
**kwargs,
**kwargs: Any,
):
self.v_buff = v_buff
self.h_buff = h_buff
@ -205,7 +206,9 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
if self.include_background_rectangle:
self.add_background_rectangle()
def _matrix_to_mob_matrix(self, matrix):
def _matrix_to_mob_matrix(
self, matrix: Iterable[Iterable[Any]]
) -> list[list[VMobject]]:
return [
[
self.element_to_mobject(item, **self.element_to_mobject_config)
@ -214,7 +217,7 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
for row in matrix
]
def _organize_mob_matrix(self, matrix):
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]
@ -224,7 +227,7 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
)
return self
def _add_brackets(self, left: str = "[", right: str = "]", **kwargs):
def _add_brackets(self, left: str = "[", right: str = "]", **kwargs: Any) -> Self:
"""Adds the brackets to the Matrix mobject.
See Latex document for various bracket types.
@ -278,13 +281,13 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
self.add(l_bracket, r_bracket)
return self
def get_columns(self):
def get_columns(self) -> VGroup:
r"""Return columns of the matrix as VGroups.
Returns
--------
List[:class:`~.VGroup`]
Each VGroup contains a column of the matrix.
:class:`~.VGroup`
The VGroup contains a nested VGroup for each column of the matrix.
Examples
--------
@ -305,7 +308,7 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
)
)
def set_column_colors(self, *colors: str):
def set_column_colors(self, *colors: str) -> Self:
r"""Set individual colors for each columns of the matrix.
Parameters
@ -331,17 +334,17 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
self.add(m0)
"""
columns = self.get_columns()
for color, column in zip(colors, columns):
for color, column in zip(colors, columns, strict=False):
column.set_color(color)
return self
def get_rows(self):
def get_rows(self) -> VGroup:
r"""Return rows of the matrix as VGroups.
Returns
--------
List[:class:`~.VGroup`]
Each VGroup contains a row of the matrix.
:class:`~.VGroup`
The VGroup contains a nested VGroup for each row of the matrix.
Examples
--------
@ -357,7 +360,7 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
"""
return VGroup(*(VGroup(*row) for row in self.mob_matrix))
def set_row_colors(self, *colors: str):
def set_row_colors(self, *colors: str) -> Self:
r"""Set individual colors for each row of the matrix.
Parameters
@ -383,11 +386,11 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
self.add(m0)
"""
rows = self.get_rows()
for color, row in zip(colors, rows):
for color, row in zip(colors, rows, strict=False):
row.set_color(color)
return self
def add_background_to_entries(self):
def add_background_to_entries(self) -> Self:
"""Add a black background rectangle to the matrix,
see above for an example.
@ -400,7 +403,7 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
mob.add_background_rectangle()
return self
def get_mob_matrix(self):
def get_mob_matrix(self) -> list[list[VMobject]]:
"""Return the underlying mob matrix mobjects.
Returns
@ -410,7 +413,7 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
"""
return self.mob_matrix
def get_entries(self):
def get_entries(self) -> VGroup:
"""Return the individual entries of the matrix.
Returns
@ -435,13 +438,13 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
"""
return self.elements
def get_brackets(self):
def get_brackets(self) -> VGroup:
r"""Return the bracket mobjects.
Returns
--------
List[:class:`~.VGroup`]
Each VGroup contains a bracket
:class:`~.VGroup`
A VGroup containing the left and right bracket.
Examples
--------
@ -482,10 +485,10 @@ class DecimalMatrix(Matrix):
def __init__(
self,
matrix: Iterable,
element_to_mobject: Mobject = DecimalNumber,
element_to_mobject_config: dict[str, Mobject] = {"num_decimal_places": 1},
**kwargs,
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,
):
"""
Will round/truncate the decimal places as per the provided config.
@ -526,7 +529,10 @@ class IntegerMatrix(Matrix):
"""
def __init__(
self, matrix: Iterable, element_to_mobject: Mobject = Integer, **kwargs
self,
matrix: Iterable[Iterable[Any]],
element_to_mobject: type[VMobject] | Callable[..., VMobject] = Integer,
**kwargs: Any,
):
"""
Will round if there are decimal entries in the matrix.
@ -560,7 +566,12 @@ class MobjectMatrix(Matrix):
self.add(m0)
"""
def __init__(self, matrix, element_to_mobject=lambda m: m, **kwargs):
def __init__(
self,
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)
@ -569,7 +580,7 @@ def get_det_text(
determinant: int | str | None = None,
background_rect: bool = False,
initial_scale_factor: float = 2,
):
) -> VGroup:
r"""Helper function to create determinant.
Parameters

File diff suppressed because it is too large Load diff

View file

@ -2,16 +2,24 @@ from __future__ import annotations
__all__ = ["TrueDot", "DotCloud"]
from typing import Any, Self
import numpy as np
from manim.constants import ORIGIN, RIGHT, UP
from manim.mobject.opengl.opengl_point_cloud_mobject import OpenGLPMobject
from manim.utils.color import YELLOW
from manim.typing import Point3DLike
from manim.utils.color import PURE_YELLOW, ParsableManimColor
class DotCloud(OpenGLPMobject):
def __init__(
self, color=YELLOW, stroke_width=2.0, radius=2.0, density=10, **kwargs
self,
color: ParsableManimColor = PURE_YELLOW,
stroke_width: float = 2.0,
radius: float = 2.0,
density: float = 10,
**kwargs: Any,
):
self.radius = radius
self.epsilon = 1.0 / density
@ -19,7 +27,7 @@ class DotCloud(OpenGLPMobject):
stroke_width=stroke_width, density=density, color=color, **kwargs
)
def init_points(self):
def init_points(self) -> None:
self.points = np.array(
[
r * (np.cos(theta) * RIGHT + np.sin(theta) * UP)
@ -34,7 +42,7 @@ class DotCloud(OpenGLPMobject):
dtype=np.float32,
)
def make_3d(self, gloss=0.5, shadow=0.2):
def make_3d(self, gloss: float = 0.5, shadow: float = 0.2) -> Self:
self.set_gloss(gloss)
self.set_shadow(shadow)
self.apply_depth_test()
@ -42,6 +50,8 @@ class DotCloud(OpenGLPMobject):
class TrueDot(DotCloud):
def __init__(self, center=ORIGIN, stroke_width=2.0, **kwargs):
def __init__(
self, center: Point3DLike = ORIGIN, stroke_width: float = 2.0, **kwargs: Any
):
self.radius = stroke_width
super().__init__(points=[center], stroke_width=stroke_width, **kwargs)

View file

@ -1,6 +1,7 @@
from __future__ import annotations
from abc import ABCMeta
from typing import Any
from manim import config
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
@ -19,13 +20,15 @@ class ConvertToOpenGL(ABCMeta):
on the lowest order inheritance classes such as Mobject and VMobject.
"""
_converted_classes = []
_converted_classes: list[type] = []
def __new__(mcls, name, bases, namespace):
def __new__(
mcls, name: str, bases: tuple[type, ...], namespace: dict[str, Any]
) -> type:
if config.renderer == RendererType.OPENGL:
# Must check class names to prevent
# cyclic importing.
base_names_to_opengl = {
base_names_to_opengl: dict[str, type] = {
"Mobject": OpenGLMobject,
"VMobject": OpenGLVMobject,
"PMobject": OpenGLPMobject,
@ -40,6 +43,6 @@ class ConvertToOpenGL(ABCMeta):
return super().__new__(mcls, name, bases, namespace)
def __init__(cls, name, bases, namespace):
def __init__(cls, name: str, bases: tuple[type, ...], namespace: dict[str, Any]):
super().__init__(name, bases, namespace)
cls._converted_classes.append(cls)

View file

@ -1,14 +1,26 @@
from __future__ import annotations
from typing import Any, Self, cast
import numpy as np
from manim.constants import *
from manim.mobject.mobject import Mobject
from manim.mobject.opengl.opengl_vectorized_mobject import (
OpenGLDashedVMobject,
OpenGLMobject,
OpenGLVGroup,
OpenGLVMobject,
)
from manim.typing import (
Point3D,
Point3D_Array,
Point3DLike,
QuadraticSpline,
Vector2DLike,
Vector3D,
Vector3DLike,
)
from manim.utils.color import *
from manim.utils.iterables import adjacent_n_tuples, adjacent_pairs
from manim.utils.simple_functions import clip
@ -77,17 +89,17 @@ class OpenGLTipableVMobject(OpenGLVMobject):
def __init__(
self,
tip_length=DEFAULT_ARROW_TIP_LENGTH,
normal_vector=OUT,
tip_config={},
**kwargs,
tip_length: float = DEFAULT_ARROW_TIP_LENGTH,
normal_vector: Vector3DLike = OUT,
tip_config: dict[str, Any] = {},
**kwargs: Any,
):
self.tip_length = tip_length
self.normal_vector = normal_vector
self.tip_config = tip_config
super().__init__(**kwargs)
def add_tip(self, at_start=False, **kwargs):
def add_tip(self, at_start: bool = False, **kwargs: Any) -> Self:
"""
Adds a tip to the TipableVMobject instance, recognising
that the endpoints might need to be switched if it's
@ -95,11 +107,11 @@ 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
def create_tip(self, at_start=False, **kwargs):
def create_tip(self, at_start: bool = False, **kwargs: Any) -> OpenGLArrowTip:
"""
Stylises the tip, positions it spacially, and returns
the newly instantiated tip to the caller.
@ -108,7 +120,7 @@ class OpenGLTipableVMobject(OpenGLVMobject):
self.position_tip(tip, at_start)
return tip
def get_unpositioned_tip(self, **kwargs):
def get_unpositioned_tip(self, **kwargs: Any) -> OpenGLArrowTip:
"""
Returns a tip that has been stylistically configured,
but has not yet been given a position in space.
@ -118,7 +130,9 @@ class OpenGLTipableVMobject(OpenGLVMobject):
config.update(kwargs)
return OpenGLArrowTip(**config)
def position_tip(self, tip, at_start=False):
def position_tip(
self, tip: OpenGLArrowTip, at_start: bool = False
) -> OpenGLArrowTip:
# Last two control points, defining both
# the end, and the tangency direction
if at_start:
@ -131,7 +145,7 @@ class OpenGLTipableVMobject(OpenGLVMobject):
tip.shift(anchor - tip.get_tip_point())
return tip
def reset_endpoints_based_on_tip(self, tip, at_start):
def reset_endpoints_based_on_tip(self, tip: OpenGLArrowTip, at_start: bool) -> Self:
if self.get_length() == 0:
# Zero length, put_start_and_end_on wouldn't
# work
@ -146,7 +160,7 @@ class OpenGLTipableVMobject(OpenGLVMobject):
self.put_start_and_end_on(start, end)
return self
def asign_tip_attr(self, tip, at_start):
def assign_tip_attr(self, tip: OpenGLArrowTip, at_start: bool) -> Self:
if at_start:
self.start_tip = tip
else:
@ -154,14 +168,14 @@ class OpenGLTipableVMobject(OpenGLVMobject):
return self
# Checking for tips
def has_tip(self):
def has_tip(self) -> bool:
return hasattr(self, "tip") and self.tip in self
def has_start_tip(self):
def has_start_tip(self) -> bool:
return hasattr(self, "start_tip") and self.start_tip in self
# Getters
def pop_tips(self):
def pop_tips(self) -> OpenGLVGroup:
start, end = self.get_start_and_end()
result = OpenGLVGroup()
if self.has_tip():
@ -173,7 +187,7 @@ class OpenGLTipableVMobject(OpenGLVMobject):
self.put_start_and_end_on(start, end)
return result
def get_tips(self):
def get_tips(self) -> OpenGLVGroup:
"""
Returns a VGroup (collection of VMobjects) containing
the TipableVMObject instance's tips.
@ -185,7 +199,7 @@ class OpenGLTipableVMobject(OpenGLVMobject):
result.add(self.start_tip)
return result
def get_tip(self):
def get_tip(self) -> OpenGLArrowTip:
"""Returns the TipableVMobject instance's (first) tip,
otherwise throws an exception.
"""
@ -193,53 +207,55 @@ class OpenGLTipableVMobject(OpenGLVMobject):
if len(tips) == 0:
raise Exception("tip not found")
else:
return tips[0]
rv = cast(OpenGLArrowTip, tips[0])
return rv
def get_default_tip_length(self):
def get_default_tip_length(self) -> float:
return self.tip_length
def get_first_handle(self):
def get_first_handle(self) -> Point3D:
return self.points[1]
def get_last_handle(self):
def get_last_handle(self) -> Point3D:
return self.points[-2]
def get_end(self):
def get_end(self) -> Point3D:
if self.has_tip():
return self.tip.get_start()
else:
return super().get_end()
def get_start(self):
def get_start(self) -> Point3D:
if self.has_start_tip():
return self.start_tip.get_start()
else:
return super().get_start()
def get_length(self):
def get_length(self) -> float:
start, end = self.get_start_and_end()
return np.linalg.norm(start - end)
rv: float = np.linalg.norm(start - end)
return rv
class OpenGLArc(OpenGLTipableVMobject):
def __init__(
self,
start_angle=0,
angle=TAU / 4,
radius=1.0,
n_components=8,
arc_center=ORIGIN,
**kwargs,
start_angle: float = 0,
angle: float = TAU / 4,
radius: float = 1.0,
n_components: int = 8,
arc_center: Point3DLike = ORIGIN,
**kwargs: Any,
):
self.start_angle = start_angle
self.angle = angle
self.radius = radius
self.n_components = n_components
self.arc_center = arc_center
super().__init__(self, **kwargs)
super().__init__(**kwargs)
self.orientation = -1
def init_points(self):
def init_points(self) -> None:
self.set_points(
OpenGLArc.create_quadratic_bezier_points(
angle=self.angle,
@ -252,7 +268,9 @@ class OpenGLArc(OpenGLTipableVMobject):
self.shift(self.arc_center)
@staticmethod
def create_quadratic_bezier_points(angle, start_angle=0, n_components=8):
def create_quadratic_bezier_points(
angle: float, start_angle: float = 0, n_components: int = 8
) -> QuadraticSpline:
samples = np.array(
[
[np.cos(a), np.sin(a), 0]
@ -272,7 +290,7 @@ class OpenGLArc(OpenGLTipableVMobject):
points[2::3] = samples[2::2]
return points
def get_arc_center(self):
def get_arc_center(self) -> Point3D:
"""
Looks at the normals to the first two
anchors, and finds their intersection points
@ -287,21 +305,29 @@ class OpenGLArc(OpenGLTipableVMobject):
n2 = rotate_vector(t2, TAU / 4)
return find_intersection(a1, n1, a2, n2)
def get_start_angle(self):
def get_start_angle(self) -> float:
angle = angle_of_vector(self.get_start() - self.get_arc_center())
return angle % TAU
rv: float = angle % TAU
return rv
def get_stop_angle(self):
def get_stop_angle(self) -> float:
angle = angle_of_vector(self.get_end() - self.get_arc_center())
return angle % TAU
rv: float = angle % TAU
return rv
def move_arc_center_to(self, point):
def move_arc_center_to(self, point: Point3DLike) -> Self:
self.shift(point - self.get_arc_center())
return self
class OpenGLArcBetweenPoints(OpenGLArc):
def __init__(self, start, end, angle=TAU / 4, **kwargs):
def __init__(
self,
start: Point3DLike,
end: Point3DLike,
angle: float = TAU / 4,
**kwargs: Any,
):
super().__init__(angle=angle, **kwargs)
if angle == 0:
self.set_points_as_corners([LEFT, RIGHT])
@ -309,30 +335,37 @@ class OpenGLArcBetweenPoints(OpenGLArc):
class OpenGLCurvedArrow(OpenGLArcBetweenPoints):
def __init__(self, start_point, end_point, **kwargs):
def __init__(self, start_point: Point3DLike, end_point: Point3DLike, **kwargs: Any):
super().__init__(start_point, end_point, **kwargs)
self.add_tip()
class OpenGLCurvedDoubleArrow(OpenGLCurvedArrow):
def __init__(self, start_point, end_point, **kwargs):
def __init__(self, start_point: Point3DLike, end_point: Point3DLike, **kwargs: Any):
super().__init__(start_point, end_point, **kwargs)
self.add_tip(at_start=True)
class OpenGLCircle(OpenGLArc):
def __init__(self, color=RED, **kwargs):
def __init__(self, color: ParsableManimColor = RED, **kwargs: Any):
super().__init__(0, TAU, color=color, **kwargs)
def surround(self, mobject, dim_to_match=0, stretch=False, buff=MED_SMALL_BUFF):
def surround(
self,
mobject: OpenGLMobject,
dim_to_match: int = 0,
stretch: bool = False,
buff: float = MED_SMALL_BUFF,
) -> Self:
# Ignores dim_to_match and stretch; result will always be a circle
# TODO: Perhaps create an ellipse class to handle singele-dimension stretching
self.replace(mobject, dim_to_match, stretch)
self.stretch((self.get_width() + 2 * buff) / self.get_width(), 0)
self.stretch((self.get_height() + 2 * buff) / self.get_height(), 1)
return self
def point_at_angle(self, angle):
def point_at_angle(self, angle: float) -> Point3D:
start_angle = self.get_start_angle()
return self.point_from_proportion((angle - start_angle) / TAU)
@ -340,12 +373,12 @@ class OpenGLCircle(OpenGLArc):
class OpenGLDot(OpenGLCircle):
def __init__(
self,
point=ORIGIN,
radius=DEFAULT_DOT_RADIUS,
stroke_width=0,
fill_opacity=1.0,
color=WHITE,
**kwargs,
point: Point3DLike = ORIGIN,
radius: float = DEFAULT_DOT_RADIUS,
stroke_width: float = 0,
fill_opacity: float = 1.0,
color: ParsableManimColor = WHITE,
**kwargs: Any,
):
super().__init__(
arc_center=point,
@ -358,7 +391,7 @@ class OpenGLDot(OpenGLCircle):
class OpenGLEllipse(OpenGLCircle):
def __init__(self, width=2, height=1, **kwargs):
def __init__(self, width: float = 2, height: float = 1, **kwargs: Any):
super().__init__(**kwargs)
self.set_width(width, stretch=True)
self.set_height(height, stretch=True)
@ -367,14 +400,14 @@ class OpenGLEllipse(OpenGLCircle):
class OpenGLAnnularSector(OpenGLArc):
def __init__(
self,
inner_radius=1,
outer_radius=2,
angle=TAU / 4,
start_angle=0,
fill_opacity=1,
stroke_width=0,
color=WHITE,
**kwargs,
inner_radius: float = 1,
outer_radius: float = 2,
angle: float = TAU / 4,
start_angle: float = 0,
fill_opacity: float = 1,
stroke_width: float = 0,
color: ParsableManimColor = WHITE,
**kwargs: Any,
):
self.inner_radius = inner_radius
self.outer_radius = outer_radius
@ -387,7 +420,7 @@ class OpenGLAnnularSector(OpenGLArc):
**kwargs,
)
def init_points(self):
def init_points(self) -> None:
inner_arc, outer_arc = (
OpenGLArc(
start_angle=self.start_angle,
@ -405,20 +438,20 @@ class OpenGLAnnularSector(OpenGLArc):
class OpenGLSector(OpenGLAnnularSector):
def __init__(self, outer_radius=1, inner_radius=0, **kwargs):
def __init__(self, outer_radius: float = 1, inner_radius: float = 0, **kwargs: Any):
super().__init__(inner_radius=inner_radius, outer_radius=outer_radius, **kwargs)
class OpenGLAnnulus(OpenGLCircle):
def __init__(
self,
inner_radius=1,
outer_radius=2,
fill_opacity=1,
stroke_width=0,
color=WHITE,
mark_paths_closed=False,
**kwargs,
inner_radius: float = 1,
outer_radius: float = 2,
fill_opacity: float = 1,
stroke_width: float = 0,
color: ParsableManimColor = WHITE,
mark_paths_closed: bool = False,
**kwargs: Any,
):
self.mark_paths_closed = mark_paths_closed # is this even used?
self.inner_radius = inner_radius
@ -427,7 +460,7 @@ class OpenGLAnnulus(OpenGLCircle):
fill_opacity=fill_opacity, stroke_width=stroke_width, color=color, **kwargs
)
def init_points(self):
def init_points(self) -> None:
self.radius = self.outer_radius
outer_circle = OpenGLCircle(radius=self.outer_radius)
inner_circle = OpenGLCircle(radius=self.inner_radius)
@ -438,17 +471,26 @@ class OpenGLAnnulus(OpenGLCircle):
class OpenGLLine(OpenGLTipableVMobject):
def __init__(self, start=LEFT, end=RIGHT, buff=0, path_arc=0, **kwargs):
def __init__(
self,
start: Point3DLike = LEFT,
end: Point3DLike = RIGHT,
buff: float = 0,
path_arc: float = 0,
**kwargs: Any,
):
self.dim = 3
self.buff = buff
self.path_arc = path_arc
self.set_start_and_end_attrs(start, end)
super().__init__(**kwargs)
def init_points(self):
def init_points(self) -> None:
self.set_points_by_ends(self.start, self.end, self.buff, self.path_arc)
def set_points_by_ends(self, start, end, buff=0, path_arc=0):
def set_points_by_ends(
self, start: Point3DLike, end: Point3DLike, buff: float = 0, path_arc: float = 0
) -> None:
if path_arc:
self.set_points(OpenGLArc.create_quadratic_bezier_points(path_arc))
self.put_start_and_end_on(start, end)
@ -456,23 +498,25 @@ class OpenGLLine(OpenGLTipableVMobject):
self.set_points_as_corners([start, end])
self.account_for_buff(self.buff)
def set_path_arc(self, new_value):
def set_path_arc(self, new_value: float) -> None:
self.path_arc = new_value
self.init_points()
def account_for_buff(self, buff):
def account_for_buff(self, buff: float) -> Self:
if buff == 0:
return
return self
#
length = self.get_length() if self.path_arc == 0 else self.get_arc_length()
#
if length < 2 * buff:
return
return self
buff_prop = buff / length
self.pointwise_become_partial(self, buff_prop, 1 - buff_prop)
return self
def set_start_and_end_attrs(self, start, end):
def set_start_and_end_attrs(
self, start: Mobject | Point3DLike, end: Mobject | Point3DLike
) -> None:
# If either start or end are Mobjects, this
# gives their centers
rough_start = self.pointify(start)
@ -484,7 +528,9 @@ class OpenGLLine(OpenGLTipableVMobject):
self.start = self.pointify(start, vect) + self.buff * vect
self.end = self.pointify(end, -vect) - self.buff * vect
def pointify(self, mob_or_point, direction=None):
def pointify(
self, mob_or_point: Mobject | Point3DLike, direction: Vector3DLike = None
) -> Point3D:
"""
Take an argument passed into Line (or subclass) and turn
it into a 3d point.
@ -501,31 +547,32 @@ class OpenGLLine(OpenGLTipableVMobject):
result[: len(point)] = point
return result
def put_start_and_end_on(self, start, end):
def put_start_and_end_on(self, start: Point3DLike, end: Point3DLike) -> Self:
curr_start, curr_end = self.get_start_and_end()
if (curr_start == curr_end).all():
self.set_points_by_ends(start, end, self.path_arc)
return super().put_start_and_end_on(start, end)
def get_vector(self):
def get_vector(self) -> Vector3D:
return self.get_end() - self.get_start()
def get_unit_vector(self):
def get_unit_vector(self) -> Vector3D:
return normalize(self.get_vector())
def get_angle(self):
def get_angle(self) -> float:
return angle_of_vector(self.get_vector())
def get_projection(self, point):
def get_projection(self, point: Point3DLike) -> Point3D:
"""Return projection of a point onto the line"""
unit_vect = self.get_unit_vector()
start = self.get_start()
return start + np.dot(point - start, unit_vect) * unit_vect
def get_slope(self):
return np.tan(self.get_angle())
def get_slope(self) -> float:
rv: float = np.tan(self.get_angle())
return rv
def set_angle(self, angle, about_point=None):
def set_angle(self, angle: float, about_point: Point3DLike | None = None) -> Self:
if about_point is None:
about_point = self.get_start()
self.rotate(
@ -534,13 +581,17 @@ class OpenGLLine(OpenGLTipableVMobject):
)
return self
def set_length(self, length):
def set_length(self, length: float) -> None:
self.scale(length / self.get_length())
class OpenGLDashedLine(OpenGLLine):
def __init__(
self, *args, dash_length=DEFAULT_DASH_LENGTH, dashed_ratio=0.5, **kwargs
self,
*args: Any,
dash_length: float = DEFAULT_DASH_LENGTH,
dashed_ratio: float = 0.5,
**kwargs: Any,
):
self.dashed_ratio = dashed_ratio
self.dash_length = dash_length
@ -555,33 +606,40 @@ class OpenGLDashedLine(OpenGLLine):
self.clear_points()
self.add(*dashes)
def calculate_num_dashes(self, dashed_ratio):
def calculate_num_dashes(self, dashed_ratio: float) -> int:
return max(
2,
int(np.ceil((self.get_length() / self.dash_length) * dashed_ratio)),
)
def get_start(self):
def get_start(self) -> Point3D:
if len(self.submobjects) > 0:
return self.submobjects[0].get_start()
else:
return super().get_start()
def get_end(self):
def get_end(self) -> Point3D:
if len(self.submobjects) > 0:
return self.submobjects[-1].get_end()
else:
return super().get_end()
def get_first_handle(self):
def get_first_handle(self) -> Point3D:
return self.submobjects[0].points[1]
def get_last_handle(self):
def get_last_handle(self) -> Point3D:
return self.submobjects[-1].points[-2]
class OpenGLTangentLine(OpenGLLine):
def __init__(self, vmob, alpha, length=1, d_alpha=1e-6, **kwargs):
def __init__(
self,
vmob: OpenGLVMobject,
alpha: float,
length: float = 1,
d_alpha: float = 1e-6,
**kwargs: Any,
):
self.length = length
self.d_alpha = d_alpha
da = self.d_alpha
@ -592,7 +650,7 @@ class OpenGLTangentLine(OpenGLLine):
class OpenGLElbow(OpenGLVMobject):
def __init__(self, width=0.2, angle=0, **kwargs):
def __init__(self, width: float = 0.2, angle: float = 0, **kwargs: Any):
self.angle = angle
super().__init__(self, **kwargs)
self.set_points_as_corners([UP, UP + RIGHT, RIGHT])
@ -603,19 +661,19 @@ class OpenGLElbow(OpenGLVMobject):
class OpenGLArrow(OpenGLLine):
def __init__(
self,
start=LEFT,
end=RIGHT,
path_arc=0,
fill_color=GREY_A,
fill_opacity=1,
stroke_width=0,
buff=MED_SMALL_BUFF,
thickness=0.05,
tip_width_ratio=5,
tip_angle=PI / 3,
max_tip_length_to_length_ratio=0.5,
max_width_to_length_ratio=0.1,
**kwargs,
start: Point3DLike = LEFT,
end: Point3DLike = RIGHT,
path_arc: float = 0,
fill_color: ParsableManimColor = GREY_A,
fill_opacity: float = 1,
stroke_width: float = 0,
buff: float = MED_SMALL_BUFF,
thickness: float = 0.05,
tip_width_ratio: float = 5,
tip_angle: float = PI / 3,
max_tip_length_to_length_ratio: float = 0.5,
max_width_to_length_ratio: float = 0.1,
**kwargs: Any,
):
self.thickness = thickness
self.tip_width_ratio = tip_width_ratio
@ -633,9 +691,11 @@ class OpenGLArrow(OpenGLLine):
**kwargs,
)
def set_points_by_ends(self, start, end, buff=0, path_arc=0):
def set_points_by_ends(
self, start: Point3DLike, end: Point3DLike, buff: float = 0, path_arc: float = 0
) -> None:
# Find the right tip length and thickness
vect = end - start
vect = np.asarray(end) - np.asarray(start)
length = max(np.linalg.norm(vect), 1e-8)
thickness = self.thickness
w_ratio = self.max_width_to_length_ratio / (thickness / length)
@ -696,7 +756,7 @@ class OpenGLArrow(OpenGLLine):
self.shift(start - self.get_start())
self.refresh_triangulation()
def reset_points_around_ends(self):
def reset_points_around_ends(self) -> Self:
self.set_points_by_ends(
self.get_start(),
self.get_end(),
@ -704,36 +764,41 @@ class OpenGLArrow(OpenGLLine):
)
return self
def get_start(self):
def get_start(self) -> Point3D:
nppc = self.n_points_per_curve
points = self.points
return (points[0] + points[-nppc]) / 2
def get_end(self):
def get_end(self) -> Point3D:
return self.points[self.tip_index]
def put_start_and_end_on(self, start, end):
def put_start_and_end_on(self, start: Point3DLike, end: Point3DLike) -> Self:
self.set_points_by_ends(start, end, buff=0, path_arc=self.path_arc)
return self
def scale(self, *args, **kwargs):
def scale(self, *args: Any, **kwargs: Any) -> Self:
super().scale(*args, **kwargs)
self.reset_points_around_ends()
return self
def set_thickness(self, thickness):
def set_thickness(self, thickness: float) -> Self:
self.thickness = thickness
self.reset_points_around_ends()
return self
def set_path_arc(self, path_arc):
def set_path_arc(self, path_arc: float) -> None:
self.path_arc = path_arc
self.reset_points_around_ends()
return self
# return self
class OpenGLVector(OpenGLArrow):
def __init__(self, direction=RIGHT, buff=0, **kwargs):
def __init__(
self,
direction: Vector2DLike | Vector3DLike = RIGHT,
buff: float = 0,
**kwargs: Any,
):
self.buff = buff
if len(direction) == 2:
direction = np.hstack([direction, 0])
@ -741,30 +806,37 @@ class OpenGLVector(OpenGLArrow):
class OpenGLDoubleArrow(OpenGLArrow):
def __init__(self, *args, **kwargs):
def __init__(self, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)
self.add_tip(at_start=True)
class OpenGLCubicBezier(OpenGLVMobject):
def __init__(self, a0, h0, h1, a1, **kwargs):
def __init__(
self,
a0: Point3DLike,
h0: Point3DLike,
h1: Point3DLike,
a1: Point3DLike,
**kwargs: Any,
):
super().__init__(**kwargs)
self.add_cubic_bezier_curve(a0, h0, h1, a1)
class OpenGLPolygon(OpenGLVMobject):
def __init__(self, *vertices, **kwargs):
self.vertices = vertices
def __init__(self, *vertices: Point3DLike, **kwargs: Any):
self.vertices: Point3D_Array = np.array(vertices)
super().__init__(**kwargs)
def init_points(self):
def init_points(self) -> None:
verts = self.vertices
self.set_points_as_corners([*verts, verts[0]])
def get_vertices(self):
def get_vertices(self) -> Point3D_Array:
return self.get_start_anchors()
def round_corners(self, radius=0.5):
def round_corners(self, radius: float = 0.5) -> Self:
vertices = self.get_vertices()
arcs = []
for v1, v2, v3 in adjacent_n_tuples(vertices, 3):
@ -801,7 +873,7 @@ class OpenGLPolygon(OpenGLVMobject):
class OpenGLRegularPolygon(OpenGLPolygon):
def __init__(self, n=6, start_angle=None, **kwargs):
def __init__(self, n: int = 6, start_angle: float | None = None, **kwargs: Any):
self.start_angle = start_angle
if self.start_angle is None:
if n % 2 == 0:
@ -814,20 +886,20 @@ class OpenGLRegularPolygon(OpenGLPolygon):
class OpenGLTriangle(OpenGLRegularPolygon):
def __init__(self, **kwargs):
def __init__(self, **kwargs: Any):
super().__init__(n=3, **kwargs)
class OpenGLArrowTip(OpenGLTriangle):
def __init__(
self,
fill_opacity=1,
fill_color=WHITE,
stroke_width=0,
width=DEFAULT_ARROW_TIP_WIDTH,
length=DEFAULT_ARROW_TIP_LENGTH,
angle=0,
**kwargs,
fill_opacity: float = 1,
fill_color: ParsableManimColor = WHITE,
stroke_width: float = 0,
width: float = DEFAULT_ARROW_TIP_WIDTH,
length: float = DEFAULT_ARROW_TIP_LENGTH,
angle: float = 0,
**kwargs: Any,
):
super().__init__(
start_angle=0,
@ -839,24 +911,31 @@ class OpenGLArrowTip(OpenGLTriangle):
self.set_width(width, stretch=True)
self.set_height(length, stretch=True)
def get_base(self):
def get_base(self) -> Point3D:
return self.point_from_proportion(0.5)
def get_tip_point(self):
def get_tip_point(self) -> Point3D:
return self.points[0]
def get_vector(self):
def get_vector(self) -> Vector3D:
return self.get_tip_point() - self.get_base()
def get_angle(self):
def get_angle(self) -> float:
return angle_of_vector(self.get_vector())
def get_length(self):
return np.linalg.norm(self.get_vector())
def get_length(self) -> float:
rv: float = np.linalg.norm(self.get_vector())
return rv
class OpenGLRectangle(OpenGLPolygon):
def __init__(self, color=WHITE, width=4.0, height=2.0, **kwargs):
def __init__(
self,
color: ParsableManimColor = WHITE,
width: float = 4.0,
height: float = 2.0,
**kwargs: Any,
):
super().__init__(UR, UL, DL, DR, color=color, **kwargs)
self.set_width(width, stretch=True)
@ -864,14 +943,14 @@ class OpenGLRectangle(OpenGLPolygon):
class OpenGLSquare(OpenGLRectangle):
def __init__(self, side_length=2.0, **kwargs):
def __init__(self, side_length: float = 2.0, **kwargs: Any):
self.side_length = side_length
super().__init__(height=side_length, width=side_length, **kwargs)
class OpenGLRoundedRectangle(OpenGLRectangle):
def __init__(self, corner_radius=0.5, **kwargs):
def __init__(self, corner_radius: float = 0.5, **kwargs: Any):
self.corner_radius = corner_radius
super().__init__(**kwargs)
self.round_corners(self.corner_radius)

View file

@ -5,6 +5,7 @@ __all__ = [
]
from pathlib import Path
from typing import TYPE_CHECKING, Any
import numpy as np
from PIL import Image
@ -13,26 +14,29 @@ from PIL.Image import Resampling
from manim.mobject.opengl.opengl_surface import OpenGLSurface, OpenGLTexturedSurface
from manim.utils.images import get_full_raster_image_path
if TYPE_CHECKING:
import numpy.typing as npt
__all__ = ["OpenGLImageMobject"]
class OpenGLImageMobject(OpenGLTexturedSurface):
def __init__(
self,
filename_or_array: str | Path | np.ndarray,
width: float = None,
height: float = None,
filename_or_array: str | Path | npt.NDArray,
width: float | None = None,
height: float | None = None,
image_mode: str = "RGBA",
resampling_algorithm: int = Resampling.BICUBIC,
resampling_algorithm: Resampling = Resampling.BICUBIC,
opacity: float = 1,
gloss: float = 0,
shadow: float = 0,
**kwargs,
**kwargs: Any,
):
self.image = filename_or_array
self.resampling_algorithm = resampling_algorithm
if isinstance(filename_or_array, np.ndarray):
self.size = self.image.shape[1::-1]
self.size = filename_or_array.shape[1::-1]
elif isinstance(filename_or_array, (str, Path)):
path = get_full_raster_image_path(filename_or_array)
self.size = Image.open(path).size
@ -68,7 +72,7 @@ class OpenGLImageMobject(OpenGLTexturedSurface):
self,
image_file: str | Path | np.ndarray,
image_mode: str,
):
) -> Image.Image:
if isinstance(image_file, (str, Path)):
return super().get_image_from_file(image_file, image_mode)
else:
@ -76,7 +80,7 @@ class OpenGLImageMobject(OpenGLTexturedSurface):
Image.fromarray(image_file.astype("uint8"))
.convert(image_mode)
.resize(
np.array(image_file.shape[:2])
image_file.shape[:2]
* 200, # assumption of 200 ppmu (pixels per manim unit) would suffice
resample=self.resampling_algorithm,
)

File diff suppressed because it is too large Load diff

View file

@ -2,16 +2,35 @@ from __future__ import annotations
__all__ = ["OpenGLPMobject", "OpenGLPGroup", "OpenGLPMPoint"]
from typing import TYPE_CHECKING
import moderngl
import numpy as np
from manim.constants import *
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
from manim.utils.bezier import interpolate
from manim.utils.color import BLACK, WHITE, YELLOW, color_gradient, color_to_rgba
from manim.utils.color import (
BLACK,
PURE_YELLOW,
WHITE,
ParsableManimColor,
color_gradient,
color_to_rgba,
)
from manim.utils.config_ops import _Uniforms
from manim.utils.iterables import resize_with_interpolation
if TYPE_CHECKING:
from typing import Self
from manim.typing import (
FloatRGBA_Array,
FloatRGBALike_Array,
Point3D_Array,
Point3DLike_Array,
)
__all__ = ["OpenGLPMobject", "OpenGLPGroup", "OpenGLPMPoint"]
@ -27,7 +46,11 @@ class OpenGLPMobject(OpenGLMobject):
point_radius = _Uniforms()
def __init__(
self, stroke_width=2.0, color=YELLOW, render_primitive=moderngl.POINTS, **kwargs
self,
stroke_width: float = 2.0,
color: ParsableManimColor = PURE_YELLOW,
render_primitive: int = moderngl.POINTS,
**kwargs,
):
self.stroke_width = stroke_width
super().__init__(color=color, render_primitive=render_primitive, **kwargs)
@ -35,22 +58,28 @@ class OpenGLPMobject(OpenGLMobject):
self.stroke_width * OpenGLPMobject.OPENGL_POINT_RADIUS_SCALE_FACTOR
)
def reset_points(self):
self.rgbas = np.zeros((1, 4))
self.points = np.zeros((0, 3))
def reset_points(self) -> Self:
self.rgbas: FloatRGBA_Array = np.zeros((1, 4))
self.points: Point3D_Array = np.zeros((0, 3))
return self
def get_array_attrs(self):
return ["points", "rgbas"]
def add_points(self, points, rgbas=None, color=None, opacity=None):
def add_points(
self,
points: Point3DLike_Array,
rgbas: FloatRGBALike_Array | None = None,
color: ParsableManimColor | None = None,
opacity: float | None = None,
) -> Self:
"""Add points.
Points must be a Nx3 numpy array.
Rgbas must be a Nx4 numpy array if it is not None.
"""
if rgbas is None and color is None:
color = YELLOW
color = PURE_YELLOW
self.append_points(points)
# rgbas array will have been resized with points
if color is not None:

View file

@ -2,6 +2,7 @@ from __future__ import annotations
from collections.abc import Iterable
from pathlib import Path
from typing import TYPE_CHECKING
import moderngl
import numpy as np
@ -16,6 +17,11 @@ from manim.utils.images import change_to_rgba_array, get_full_raster_image_path
from manim.utils.iterables import listify
from manim.utils.space_ops import normalize_along_axis
if TYPE_CHECKING:
import numpy.typing as npt
from manim.typing import Point3D_Array, Vector3D_Array
__all__ = ["OpenGLSurface", "OpenGLTexturedSurface"]
@ -82,7 +88,7 @@ class OpenGLSurface(OpenGLMobject):
render_primitive=moderngl.TRIANGLES,
depth_test=True,
shader_folder=None,
**kwargs,
**kwargs: Any,
):
self.passed_uv_func = uv_func
self.u_range = u_range if u_range is not None else (0, 1)
@ -160,12 +166,14 @@ class OpenGLSurface(OpenGLMobject):
def get_triangle_indices(self):
return self.triangle_indices
def get_surface_points_and_nudged_points(self):
def get_surface_points_and_nudged_points(
self,
) -> tuple[Point3D_Array, Point3D_Array, Point3D_Array]:
points = self.points
k = len(points) // 3
return points[:k], points[k : 2 * k], points[2 * k :]
def get_unit_normals(self):
def get_unit_normals(self) -> Vector3D_Array:
s_points, du_points, dv_points = self.get_surface_points_and_nudged_points()
normals = np.cross(
(du_points - s_points) / self.epsilon,
@ -371,7 +379,7 @@ class OpenGLTexturedSurface(OpenGLSurface):
def __init__(
self,
uv_surface: OpenGLSurface,
image_file: str | Path,
image_file: str | Path | npt.NDArray,
dark_image_file: str | Path = None,
image_mode: str | Iterable[str] = "RGBA",
shader_folder: str | Path = None,
@ -413,7 +421,7 @@ class OpenGLTexturedSurface(OpenGLSurface):
self,
image_file: str | Path,
image_mode: str,
):
) -> Image.Image:
image_file = get_full_raster_image_path(image_file)
return Image.open(image_file).convert(image_mode)

View file

@ -1,5 +1,7 @@
from __future__ import annotations
from typing import Any
import numpy as np
from manim.mobject.opengl.opengl_surface import OpenGLSurface
@ -11,13 +13,13 @@ __all__ = ["OpenGLSurfaceMesh"]
class OpenGLSurfaceMesh(OpenGLVGroup):
def __init__(
self,
uv_surface,
resolution=None,
stroke_width=1,
normal_nudge=1e-2,
depth_test=True,
flat_stroke=False,
**kwargs,
uv_surface: OpenGLSurface,
resolution: tuple[int, int] | None = None,
stroke_width: float = 1,
normal_nudge: float = 1e-2,
depth_test: bool = True,
flat_stroke: bool = False,
**kwargs: Any,
):
if not isinstance(uv_surface, OpenGLSurface):
raise Exception("uv_surface must be of type OpenGLSurface")
@ -31,7 +33,7 @@ class OpenGLSurfaceMesh(OpenGLVGroup):
**kwargs,
)
def init_points(self):
def init_points(self) -> None:
uv_surface = self.uv_surface
full_nu, full_nv = uv_surface.resolution

View file

@ -2,9 +2,9 @@ from __future__ import annotations
import itertools as it
import operator as op
from collections.abc import Iterable, Sequence
from collections.abc import Callable, Iterable, Sequence
from functools import reduce, wraps
from typing import Callable
from typing import Any, Self
import moderngl
import numpy as np
@ -13,6 +13,7 @@ from manim import config
from manim.constants import *
from manim.mobject.opengl.opengl_mobject import OpenGLMobject, OpenGLPoint
from manim.renderer.shader_wrapper import ShaderWrapper
from manim.typing import Point3D, Point3DLike, Point3DLike_Array
from manim.utils.bezier import (
bezier,
bezier_remap,
@ -83,6 +84,9 @@ class OpenGLVMobject(OpenGLMobject):
stroke_shader_folder = "quadratic_bezier_stroke"
fill_shader_folder = "quadratic_bezier_fill"
# TODO: although these are called "rgba" in singular, they are used as
# FloatRGBA_Arrays and should be called instead "rgbas" in plural for consistency.
# The same should probably apply for "stroke_width" and "unit_normal".
fill_rgba = _Data()
stroke_rgba = _Data()
stroke_width = _Data()
@ -171,6 +175,15 @@ class OpenGLVMobject(OpenGLMobject):
def get_mobject_type_class():
return OpenGLVMobject
@property
def submobjects(self) -> Sequence[OpenGLVMobject]:
return self._submobjects if hasattr(self, "_submobjects") else []
@submobjects.setter
def submobjects(self, submobject_list: Iterable[OpenGLVMobject]) -> None:
self.remove(*self.submobjects)
self.add(*submobject_list)
def init_data(self):
super().init_data()
self.data.pop("rgbas")
@ -269,7 +282,10 @@ class OpenGLVMobject(OpenGLMobject):
if width is not None:
for mob in self.get_family(recurse):
mob.stroke_width = np.array([[width] for width in tuplify(width)])
if isinstance(width, np.ndarray):
mob.stroke_width = width
else:
mob.stroke_width = np.array([[width] for width in tuplify(width)])
if background is not None:
for mob in self.get_family(recurse):
@ -334,7 +350,7 @@ class OpenGLVMobject(OpenGLMobject):
return self
elif len(submobs2) == 0:
submobs2 = [vmobject]
for sm1, sm2 in zip(*make_even(submobs1, submobs2)):
for sm1, sm2 in zip(*make_even(submobs1, submobs2), strict=True):
sm1.match_style(sm2)
return self
@ -462,7 +478,13 @@ class OpenGLVMobject(OpenGLMobject):
self.append_points([point])
return self
def add_cubic_bezier_curve(self, anchor1, handle1, handle2, anchor2):
def add_cubic_bezier_curve(
self,
anchor1: Point3DLike,
handle1: Point3DLike,
handle2: Point3DLike,
anchor2: Point3DLike,
):
new_points = get_quadratic_approximation_of_cubic(
anchor1,
handle1,
@ -558,7 +580,7 @@ class OpenGLVMobject(OpenGLMobject):
new_points.extend(
[
partial_bezier_points(tup, a1, a2)
for a1, a2 in zip(alphas, alphas[1:])
for a1, a2 in zip(alphas[:-1], alphas[1:], strict=True)
],
)
else:
@ -571,7 +593,7 @@ class OpenGLVMobject(OpenGLMobject):
self.add_line_to(point)
return points
def set_points_as_corners(self, points: Iterable[float]) -> OpenGLVMobject:
def set_points_as_corners(self, points: Point3DLike_Array) -> OpenGLVMobject:
"""Given an array of points, set them as corner of the vmobject.
To achieve that, this algorithm sets handles aligned with the anchors such that the resultant bezier curve will be the segment
@ -594,7 +616,9 @@ class OpenGLVMobject(OpenGLMobject):
)
return self
def set_points_smoothly(self, points, true_smooth=False):
def set_points_smoothly(
self, points: Point3DLike_Array, true_smooth: bool = False
) -> Self:
self.set_points_as_corners(points)
self.make_smooth()
return self
@ -745,7 +769,7 @@ class OpenGLVMobject(OpenGLMobject):
split_indices = [0, *split_indices, len(points)]
return [
points[i1:i2]
for i1, i2 in zip(split_indices, split_indices[1:])
for i1, i2 in zip(split_indices[:-1], split_indices[1:], strict=True)
if (i2 - i1) >= nppc
]
@ -922,7 +946,7 @@ class OpenGLVMobject(OpenGLMobject):
for n in range(num_curves):
yield self.get_nth_curve_function_with_length(n, **kwargs)
def point_from_proportion(self, alpha: float) -> np.ndarray:
def point_from_proportion(self, alpha: float) -> Point3D:
"""Gets the point at a proportion along the path of the :class:`OpenGLVMobject`.
Parameters
@ -932,7 +956,7 @@ class OpenGLVMobject(OpenGLMobject):
Returns
-------
:class:`numpy.ndarray`
:class:`Point3D`
The point on the :class:`OpenGLVMobject`.
Raises
@ -969,7 +993,7 @@ class OpenGLVMobject(OpenGLMobject):
def proportion_from_point(
self,
point: Iterable[float | int],
point: Point3DLike,
) -> float:
"""Returns the proportion along the path of the :class:`OpenGLVMobject`
a particular given point is at.
@ -1069,7 +1093,7 @@ class OpenGLVMobject(OpenGLMobject):
s = self.get_start_anchors()
e = self.get_end_anchors()
return list(it.chain.from_iterable(zip(s, e)))
return list(it.chain.from_iterable(zip(s, e, strict=True)))
def get_points_without_null_curves(self, atol=1e-9):
nppc = self.n_points_per_curve
@ -1203,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
@ -1375,7 +1401,7 @@ class OpenGLVMobject(OpenGLMobject):
# Related to triangulation
def refresh_triangulation(self):
def refresh_triangulation(self) -> Self:
for mob in self.get_family():
mob.needs_new_triangulation = True
return self
@ -1654,7 +1680,7 @@ class OpenGLVGroup(OpenGLVMobject):
self.add(circles_group)
"""
def __init__(self, *vmobjects, **kwargs):
def __init__(self, *vmobjects: OpenGLVMobject, **kwargs: Any):
super().__init__(**kwargs)
self.add(*vmobjects)

View file

@ -4,8 +4,7 @@ from __future__ import annotations
__all__ = ["Brace", "BraceLabel", "ArcBrace", "BraceText", "BraceBetweenPoints"]
from collections.abc import Sequence
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any, Self
import numpy as np
import svgelements as se
@ -15,8 +14,10 @@ from manim.mobject.geometry.arc import Arc
from manim.mobject.geometry.line import Line
from manim.mobject.mobject import Mobject
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from manim.mobject.text.tex_mobject import MathTex, Tex
from manim.mobject.text.tex_mobject import MathTex, SingleStringMathTex, Tex
from manim.mobject.text.text_mobject import Text
from ...animation.animation import Animation
from ...animation.composition import AnimationGroup
from ...animation.fading import FadeIn
from ...animation.growing import GrowFromCenter
@ -26,11 +27,9 @@ from ...utils.color import BLACK
from ..svg.svg_mobject import VMobjectFromSVGPath
if TYPE_CHECKING:
from manim.typing import Point3DLike, Vector3D
from manim.typing import Point3D, Point3DLike, Vector3D, Vector3DLike
from manim.utils.color.core import ParsableManimColor
__all__ = ["Brace", "BraceBetweenPoints", "BraceLabel", "ArcBrace"]
class Brace(VMobjectFromSVGPath):
"""Takes a mobject and draws a brace adjacent to it.
@ -70,14 +69,14 @@ class Brace(VMobjectFromSVGPath):
def __init__(
self,
mobject: Mobject,
direction: Vector3D | None = DOWN,
direction: Vector3DLike = DOWN,
buff: float = 0.2,
sharpness: float = 2,
stroke_width: float = 0,
fill_opacity: float = 1.0,
background_stroke_width: float = 0,
background_stroke_color: ParsableManimColor = BLACK,
**kwargs,
**kwargs: Any,
):
path_string_template = (
"m0.01216 0c-0.01152 0-0.01216 6.103e-4 -0.01216 0.01311v0.007762c0.06776 "
@ -130,7 +129,7 @@ class Brace(VMobjectFromSVGPath):
for mob in mobject, self:
mob.rotate(angle, about_point=ORIGIN)
def put_at_tip(self, mob: Mobject, use_next_to: bool = True, **kwargs):
def put_at_tip(self, mob: Mobject, use_next_to: bool = True, **kwargs: Any) -> Self:
"""Puts the given mobject at the brace tip.
Parameters
@ -153,7 +152,7 @@ class Brace(VMobjectFromSVGPath):
mob.shift(self.get_direction() * shift_distance)
return self
def get_text(self, *text, **kwargs):
def get_text(self, *text: str, **kwargs: Any) -> Tex:
"""Places the text at the brace tip.
Parameters
@ -172,7 +171,7 @@ class Brace(VMobjectFromSVGPath):
self.put_at_tip(text_mob, **kwargs)
return text_mob
def get_tex(self, *tex, **kwargs):
def get_tex(self, *tex: str, **kwargs: Any) -> MathTex:
"""Places the tex at the brace tip.
Parameters
@ -191,7 +190,7 @@ class Brace(VMobjectFromSVGPath):
self.put_at_tip(tex_mob, **kwargs)
return tex_mob
def get_tip(self):
def get_tip(self) -> Point3D:
"""Returns the point at the brace tip."""
# Returns the position of the seventh point in the path, which is the tip.
if config["renderer"] == "opengl":
@ -199,7 +198,7 @@ class Brace(VMobjectFromSVGPath):
return self.points[28] # = 7*4
def get_direction(self):
def get_direction(self) -> Vector3D:
"""Returns the direction from the center to the brace tip."""
vect = self.get_tip() - self.get_center()
return vect / np.linalg.norm(vect)
@ -233,12 +232,12 @@ class BraceLabel(VMobject, metaclass=ConvertToOpenGL):
self,
obj: Mobject,
text: str,
brace_direction: np.ndarray = DOWN,
label_constructor: type = MathTex,
brace_direction: Vector3DLike = DOWN,
label_constructor: type[SingleStringMathTex | Text] = MathTex,
font_size: float = DEFAULT_FONT_SIZE,
buff: float = 0.2,
brace_config: dict | None = None,
**kwargs,
brace_config: dict[str, Any] | None = None,
**kwargs: Any,
):
self.label_constructor = label_constructor
super().__init__(**kwargs)
@ -249,37 +248,94 @@ class BraceLabel(VMobject, metaclass=ConvertToOpenGL):
self.brace = Brace(obj, brace_direction, buff, **brace_config)
if isinstance(text, (tuple, list)):
self.label = self.label_constructor(*text, font_size=font_size, **kwargs)
self.label: VMobject = self.label_constructor(
*text, font_size=font_size, **kwargs
)
else:
self.label = self.label_constructor(str(text), font_size=font_size)
self.brace.put_at_tip(self.label)
self.add(self.brace, self.label)
def creation_anim(self, label_anim=FadeIn, brace_anim=GrowFromCenter):
def creation_anim(
self,
label_anim: type[Animation] = FadeIn,
brace_anim: type[Animation] = GrowFromCenter,
) -> AnimationGroup:
return AnimationGroup(brace_anim(self.brace), label_anim(self.label))
def shift_brace(self, obj, **kwargs):
def shift_brace(self, obj: Mobject, **kwargs: Any) -> Self:
if isinstance(obj, list):
obj = self.get_group_class()(*obj)
self.brace = Brace(obj, self.brace_direction, **kwargs)
self.brace.put_at_tip(self.label)
return self
def change_label(self, *text, **kwargs):
self.label = self.label_constructor(*text, **kwargs)
def change_label(self, *text: str, **kwargs: Any) -> Self:
self.remove(self.label)
self.label = self.label_constructor(*text, **kwargs) # type: ignore[arg-type]
self.brace.put_at_tip(self.label)
self.add(self.label)
return self
def change_brace_label(self, obj, *text, **kwargs):
def change_brace_label(self, obj: Mobject, *text: str, **kwargs: Any) -> Self:
self.shift_brace(obj)
self.change_label(*text, **kwargs)
return self
class BraceText(BraceLabel):
def __init__(self, obj, text, label_constructor=Tex, **kwargs):
"""Create a brace with a text label attached.
Parameters
----------
obj
The mobject adjacent to which the brace is placed.
text
The label text.
brace_direction
The direction of the brace. By default ``DOWN``.
label_constructor
A class or function used to construct a mobject representing
the label. By default :class:`~.Text`.
font_size
The font size of the label, passed to the ``label_constructor``.
buff
The buffer between the mobject and the brace.
brace_config
Arguments to be passed to :class:`.Brace`.
kwargs
Additional arguments to be passed to :class:`~.VMobject`.
Examples
--------
.. manim:: BraceTextExample
:save_last_frame:
class BraceTextExample(Scene):
def construct(self):
s1 = Square().move_to(2*LEFT)
self.add(s1)
br1 = BraceText(s1, "Label")
self.add(br1)
s2 = Square().move_to(2*RIGHT)
self.add(s2)
br2 = BraceText(s2, "Label")
br2.change_label("new")
self.add(br2)
self.wait(0.1)
"""
def __init__(
self,
obj: Mobject,
text: str,
label_constructor: type[SingleStringMathTex | Text] = Text,
**kwargs: Any,
):
super().__init__(obj, text, label_constructor=label_constructor, **kwargs)
@ -317,10 +373,10 @@ class BraceBetweenPoints(Brace):
def __init__(
self,
point_1: Point3DLike | None,
point_2: Point3DLike | None,
direction: Vector3D | None = ORIGIN,
**kwargs,
point_1: Point3DLike,
point_2: Point3DLike,
direction: Vector3DLike = ORIGIN,
**kwargs: Any,
):
if all(direction == ORIGIN):
line_vector = np.array(point_2) - np.array(point_1)
@ -386,8 +442,8 @@ class ArcBrace(Brace):
def __init__(
self,
arc: Arc | None = None,
direction: Sequence[float] = RIGHT,
**kwargs,
direction: Vector3DLike = RIGHT,
**kwargs: Any,
):
if arc is None:
arc = Arc(start_angle=-1, angle=2, radius=1)

View file

@ -4,12 +4,14 @@ from __future__ import annotations
import os
from pathlib import Path
from typing import Any
from xml.etree import ElementTree as ET
import numpy as np
import svgelements as se
from manim import config, logger
from manim.utils.color import ManimColor, ParsableManimColor
from ...constants import RIGHT
from ...utils.bezier import get_quadratic_approximation_of_cubic
@ -19,12 +21,12 @@ from ..geometry.arc import Circle
from ..geometry.line import Line
from ..geometry.polygram import Polygon, Rectangle, RoundedRectangle
from ..opengl.opengl_compatibility import ConvertToOpenGL
from ..types.vectorized_mobject import VMobject
from ..types.vectorized_mobject import VGroup, VMobject
__all__ = ["SVGMobject", "VMobjectFromSVGPath"]
SVG_HASH_TO_MOB_MAP: dict[int, VMobject] = {}
SVG_HASH_TO_MOB_MAP: dict[int, SVGMobject] = {}
def _convert_point_to_3d(x: float, y: float) -> np.ndarray:
@ -98,17 +100,17 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
should_center: bool = True,
height: float | None = 2,
width: float | None = None,
color: str | None = None,
color: ParsableManimColor | None = None,
opacity: float | None = None,
fill_color: str | None = None,
fill_color: ParsableManimColor | None = None,
fill_opacity: float | None = None,
stroke_color: str | None = None,
stroke_color: ParsableManimColor | None = None,
stroke_opacity: float | None = None,
stroke_width: float | None = None,
svg_default: dict | None = None,
path_string_config: dict | None = None,
use_svg_cache: bool = True,
**kwargs,
**kwargs: Any,
):
super().__init__(color=None, stroke_color=None, fill_color=None, **kwargs)
@ -118,13 +120,16 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
self.should_center = should_center
self.svg_height = height
self.svg_width = width
self.color = color
self.color = ManimColor(color)
self.opacity = opacity
self.fill_color = fill_color
self.fill_opacity = fill_opacity
self.fill_opacity = fill_opacity # type: ignore[assignment]
self.stroke_color = stroke_color
self.stroke_opacity = stroke_opacity
self.stroke_width = stroke_width
self.stroke_opacity = stroke_opacity # type: ignore[assignment]
self.stroke_width = stroke_width # type: ignore[assignment]
self.id_to_vgroup_dict: dict[str, VGroup] = {}
if self.stroke_width is None:
self.stroke_width = 0
if svg_default is None:
svg_default = {
@ -166,6 +171,7 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
if hash_val in SVG_HASH_TO_MOB_MAP:
mob = SVG_HASH_TO_MOB_MAP[hash_val].copy()
self.add(*mob)
self.id_to_vgroup_dict = mob.id_to_vgroup_dict
return
self.generate_mobject()
@ -191,7 +197,7 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
"""Parse the SVG and translate its elements to submobjects."""
file_path = self.get_file_path()
element_tree = ET.parse(file_path)
new_tree = self.modify_xml_tree(element_tree)
new_tree = self.modify_xml_tree(element_tree) # type: ignore[arg-type]
# Create a temporary svg file to dump modified svg to be parsed
modified_file_path = file_path.with_name(f"{file_path.stem}_{file_path.suffix}")
new_tree.write(modified_file_path)
@ -199,8 +205,9 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
svg = se.SVG.parse(modified_file_path)
modified_file_path.unlink()
mobjects = self.get_mobjects_from(svg)
mobjects, mobject_dict = self.get_mobjects_from(svg)
self.add(*mobjects)
self.id_to_vgroup_dict = mobject_dict
self.flip(RIGHT) # Flip y
def get_file_path(self) -> Path:
@ -228,12 +235,12 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
"style",
)
root = element_tree.getroot()
root_style_dict = {k: v for k, v in root.attrib.items() if k in style_keys}
root_style_dict = {k: v for k, v in root.attrib.items() if k in style_keys} # type: ignore[union-attr]
new_root = ET.Element("svg", {})
config_style_node = ET.SubElement(new_root, "g", config_style_dict)
root_style_node = ET.SubElement(config_style_node, "g", root_style_dict)
root_style_node.extend(root)
root_style_node.extend(root) # type: ignore[arg-type]
return ET.ElementTree(new_root)
def generate_config_style_dict(self) -> dict[str, str]:
@ -254,7 +261,9 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
result[svg_key] = str(svg_default_dict[style_key])
return result
def get_mobjects_from(self, svg: se.SVG) -> list[VMobject]:
def get_mobjects_from(
self, svg: se.SVG
) -> tuple[list[VMobject], dict[str, VGroup]]:
"""Convert the elements of the SVG to a list of mobjects.
Parameters
@ -262,37 +271,78 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
svg
The parsed SVG file.
"""
result = []
for shape in svg.elements():
# can we combine the two continue cases into one?
if isinstance(shape, se.Group): # noqa: SIM114
continue
elif isinstance(shape, se.Path):
mob = self.path_to_mobject(shape)
elif isinstance(shape, se.SimpleLine):
mob = self.line_to_mobject(shape)
elif isinstance(shape, se.Rect):
mob = self.rect_to_mobject(shape)
elif isinstance(shape, (se.Circle, se.Ellipse)):
mob = self.ellipse_to_mobject(shape)
elif isinstance(shape, se.Polygon):
mob = self.polygon_to_mobject(shape)
elif isinstance(shape, se.Polyline):
mob = self.polyline_to_mobject(shape)
elif isinstance(shape, se.Text):
mob = self.text_to_mobject(shape)
elif isinstance(shape, se.Use) or type(shape) is se.SVGElement:
continue
else:
logger.warning(f"Unsupported element type: {type(shape)}")
continue
if mob is None or not mob.has_points():
continue
self.apply_style_to_mobject(mob, shape)
if isinstance(shape, se.Transformable) and shape.apply:
self.handle_transform(mob, shape.transform)
result.append(mob)
return result
result: list[VMobject] = []
stack: list[tuple[se.SVGElement, int]] = []
stack.append((svg, 1))
group_id_number = 0
vgroup_stack: list[str] = ["root"]
vgroup_names: list[str] = ["root"]
vgroups: dict[str, VGroup] = {"root": VGroup()}
while len(stack) > 0:
element, depth = stack.pop()
# Reduce stack heights
vgroup_stack = vgroup_stack[0:(depth)]
try:
group_name = str(element.values["id"])
except Exception:
group_name = f"numbered_group_{group_id_number}"
group_id_number += 1
vg = VGroup()
vgroup_names.append(group_name)
vgroup_stack.append(group_name)
vgroups[group_name] = vg
if isinstance(element, (se.Group, se.Use)):
stack.extend((subelement, depth + 1) for subelement in element[::-1])
# Add element to the parent vgroup
try:
if isinstance(
element,
(
se.Path,
se.SimpleLine,
se.Rect,
se.Circle,
se.Ellipse,
se.Polygon,
se.Polyline,
se.Text,
),
):
mob = self.get_mob_from_shape_element(element)
if mob is not None:
result.append(mob)
for parent_name in vgroup_stack[:-1]:
vgroups[parent_name].add(mob)
except Exception as e:
logger.error(f"Exception occurred in 'get_mobjects_from'. Details: {e}")
return result, vgroups
def get_mob_from_shape_element(self, shape: se.SVGElement) -> VMobject | None:
if isinstance(shape, se.Path):
mob: VMobject | None = self.path_to_mobject(shape)
elif isinstance(shape, se.SimpleLine):
mob = self.line_to_mobject(shape)
elif isinstance(shape, se.Rect):
mob = self.rect_to_mobject(shape)
elif isinstance(shape, (se.Circle, se.Ellipse)):
mob = self.ellipse_to_mobject(shape)
elif isinstance(shape, se.Polygon):
mob = self.polygon_to_mobject(shape)
elif isinstance(shape, se.Polyline):
mob = self.polyline_to_mobject(shape)
elif isinstance(shape, se.Text):
mob = self.text_to_mobject(shape)
else:
logger.warning(f"Unsupported element type: {type(shape)}")
mob = None
if mob is None or not mob.has_points():
return mob
self.apply_style_to_mobject(mob, shape)
if isinstance(shape, se.Transformable) and shape.apply:
self.handle_transform(mob, shape.transform)
return mob
@staticmethod
def handle_transform(mob: VMobject, matrix: se.Matrix) -> VMobject:
@ -422,7 +472,7 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
return vmobject_class().set_points_as_corners(points)
@staticmethod
def text_to_mobject(text: se.Text):
def text_to_mobject(text: se.Text) -> VMobject:
"""Convert a text element to a vectorized mobject.
.. warning::
@ -435,7 +485,7 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
The parsed SVG text.
"""
logger.warning(f"Unsupported element type: {type(text)}")
return
return # type: ignore[return-value]
def move_into_position(self) -> None:
"""Scale and move the generated mobject into position."""
@ -480,7 +530,7 @@ class VMobjectFromSVGPath(VMobject, metaclass=ConvertToOpenGL):
long_lines: bool = False,
should_subdivide_sharp_curves: bool = False,
should_remove_null_curves: bool = False,
**kwargs,
**kwargs: Any,
):
# Get rid of arcs
path_obj.approximate_arcs_with_quads()
@ -492,7 +542,7 @@ class VMobjectFromSVGPath(VMobject, metaclass=ConvertToOpenGL):
super().__init__(**kwargs)
def init_points(self) -> None:
def generate_points(self) -> None:
# TODO: cache mobject in a re-importable way
self.handle_commands()
@ -505,15 +555,16 @@ class VMobjectFromSVGPath(VMobject, metaclass=ConvertToOpenGL):
# Get rid of any null curves
self.set_points(self.get_points_without_null_curves())
generate_points = init_points
def init_points(self) -> None:
self.generate_points()
def handle_commands(self) -> None:
all_points: list[np.ndarray] = []
last_move = None
last_move: np.ndarray = None
curve_start = None
last_true_move = None
def move_pen(pt, *, true_move: bool = False):
def move_pen(pt: np.ndarray, *, true_move: bool = False) -> None:
nonlocal last_move, curve_start, last_true_move
last_move = pt
if curve_start is None:
@ -523,17 +574,19 @@ class VMobjectFromSVGPath(VMobject, metaclass=ConvertToOpenGL):
if self.n_points_per_curve == 4:
def add_cubic(start, cp1, cp2, end):
def add_cubic(
start: np.ndarray, cp1: np.ndarray, cp2: np.ndarray, end: np.ndarray
) -> None:
nonlocal all_points
assert len(all_points) % 4 == 0, len(all_points)
all_points += [start, cp1, cp2, end]
move_pen(end)
def add_quad(start, cp, end):
def add_quad(start: np.ndarray, cp: np.ndarray, end: np.ndarray) -> None:
add_cubic(start, (start + cp + cp) / 3, (cp + cp + end) / 3, end)
move_pen(end)
def add_line(start, end):
def add_line(start: np.ndarray, end: np.ndarray) -> None:
add_cubic(
start, (start + start + end) / 3, (start + end + end) / 3, end
)
@ -541,7 +594,9 @@ class VMobjectFromSVGPath(VMobject, metaclass=ConvertToOpenGL):
else:
def add_cubic(start, cp1, cp2, end):
def add_cubic(
start: np.ndarray, cp1: np.ndarray, cp2: np.ndarray, end: np.ndarray
) -> None:
nonlocal all_points
assert len(all_points) % 3 == 0, len(all_points)
two_quads = get_quadratic_approximation_of_cubic(
@ -554,13 +609,13 @@ class VMobjectFromSVGPath(VMobject, metaclass=ConvertToOpenGL):
all_points += two_quads[3:].tolist()
move_pen(end)
def add_quad(start, cp, end):
def add_quad(start: np.ndarray, cp: np.ndarray, end: np.ndarray) -> None:
nonlocal all_points
assert len(all_points) % 3 == 0, len(all_points)
all_points += [start, cp, end]
move_pen(end)
def add_line(start, end):
def add_line(start: np.ndarray, end: np.ndarray) -> None:
add_quad(start, (start + end) / 2, end)
move_pen(end)

View file

@ -65,8 +65,7 @@ __all__ = [
import itertools as it
from collections.abc import Iterable, Sequence
from typing import Callable
from collections.abc import Callable, Iterable, Sequence
from manim.mobject.geometry.line import Line
from manim.mobject.geometry.polygram import Polygon
@ -80,7 +79,7 @@ from ..animation.composition import AnimationGroup
from ..animation.creation import Create, Write
from ..animation.fading import FadeIn
from ..mobject.types.vectorized_mobject import VGroup, VMobject
from ..utils.color import BLACK, YELLOW, ManimColor, ParsableManimColor
from ..utils.color import BLACK, PURE_YELLOW, ManimColor, ParsableManimColor
from .utils import get_vectorized_mobject_class
@ -105,6 +104,8 @@ class Table(VGroup):
Horizontal buffer passed to :meth:`~.Mobject.arrange_in_grid`, by default 1.3.
include_outer_lines
``True`` if the table should include outer lines, by default False.
include_inner_lines
``True`` if the table should include inner lines, by default True.
add_background_rectangles_to_entries
``True`` if background rectangles should be added to entries, by default ``False``.
entries_background_color
@ -194,6 +195,7 @@ class Table(VGroup):
v_buff: float = 0.8,
h_buff: float = 1.3,
include_outer_lines: bool = False,
include_inner_lines: bool = True,
add_background_rectangles_to_entries: bool = False,
entries_background_color: ParsableManimColor = BLACK,
include_background_rectangle: bool = False,
@ -215,6 +217,7 @@ class Table(VGroup):
self.v_buff = v_buff
self.h_buff = h_buff
self.include_outer_lines = include_outer_lines
self.include_inner_lines = include_inner_lines
self.add_background_rectangles_to_entries = add_background_rectangles_to_entries
self.entries_background_color = ManimColor(entries_background_color)
self.include_background_rectangle = include_background_rectangle
@ -350,15 +353,19 @@ class Table(VGroup):
)
line_group.add(line)
self.add(line)
for k in range(len(self.mob_table) - 1):
anchor = self.get_rows()[k + 1].get_top()[1] + 0.5 * (
self.get_rows()[k].get_bottom()[1] - self.get_rows()[k + 1].get_top()[1]
)
line = Line(
[anchor_left, anchor, 0], [anchor_right, anchor, 0], **self.line_config
)
line_group.add(line)
self.add(line)
if self.include_inner_lines:
for k in range(len(self.mob_table) - 1):
anchor = self.get_rows()[k + 1].get_top()[1] + 0.5 * (
self.get_rows()[k].get_bottom()[1]
- self.get_rows()[k + 1].get_top()[1]
)
line = Line(
[anchor_left, anchor, 0],
[anchor_right, anchor, 0],
**self.line_config,
)
line_group.add(line)
self.add(line)
self.horizontal_lines = line_group
return self
@ -380,16 +387,19 @@ class Table(VGroup):
)
line_group.add(line)
self.add(line)
for k in range(len(self.mob_table[0]) - 1):
anchor = self.get_columns()[k + 1].get_left()[0] + 0.5 * (
self.get_columns()[k].get_right()[0]
- self.get_columns()[k + 1].get_left()[0]
)
line = Line(
[anchor, anchor_bottom, 0], [anchor, anchor_top, 0], **self.line_config
)
line_group.add(line)
self.add(line)
if self.include_inner_lines:
for k in range(len(self.mob_table[0]) - 1):
anchor = self.get_columns()[k + 1].get_left()[0] + 0.5 * (
self.get_columns()[k].get_right()[0]
- self.get_columns()[k + 1].get_left()[0]
)
line = Line(
[anchor, anchor_bottom, 0],
[anchor, anchor_top, 0],
**self.line_config,
)
line_group.add(line)
self.add(line)
self.vertical_lines = line_group
return self
@ -527,7 +537,7 @@ class Table(VGroup):
self.add(table)
"""
columns = self.get_columns()
for color, column in zip(colors, columns):
for color, column in zip(colors, columns, strict=False):
column.set_color(color)
return self
@ -556,7 +566,7 @@ class Table(VGroup):
self.add(table)
"""
rows = self.get_rows()
for color, row in zip(colors, rows):
for color, row in zip(colors, rows, strict=False):
row.set_color(color)
return self
@ -812,7 +822,10 @@ class Table(VGroup):
return rec
def get_highlighted_cell(
self, pos: Sequence[int] = (1, 1), color: ParsableManimColor = YELLOW, **kwargs
self,
pos: Sequence[int] = (1, 1),
color: ParsableManimColor = PURE_YELLOW,
**kwargs,
) -> BackgroundRectangle:
"""Returns a :class:`~.BackgroundRectangle` of the cell at the given position.
@ -848,7 +861,10 @@ class Table(VGroup):
return bg_cell
def add_highlighted_cell(
self, pos: Sequence[int] = (1, 1), color: ParsableManimColor = YELLOW, **kwargs
self,
pos: Sequence[int] = (1, 1),
color: ParsableManimColor = PURE_YELLOW,
**kwargs,
) -> Table:
"""Highlights one cell at a specific position on the table by adding a :class:`~.BackgroundRectangle`.
@ -1079,11 +1095,11 @@ class IntegerTable(Table):
[[0,30,45,60,90],
[90,60,45,30,0]],
col_labels=[
MathTex(r"\frac{\sqrt{0}}{2}"),
MathTex(r"\frac{\sqrt{1}}{2}"),
MathTex(r"\frac{\sqrt{2}}{2}"),
MathTex(r"\frac{\sqrt{3}}{2}"),
MathTex(r"\frac{\sqrt{4}}{2}")],
MathTex(r"\frac{ \sqrt{0} }{2}"),
MathTex(r"\frac{ \sqrt{1} }{2}"),
MathTex(r"\frac{ \sqrt{2} }{2}"),
MathTex(r"\frac{ \sqrt{3} }{2}"),
MathTex(r"\frac{ \sqrt{4} }{2}")],
row_labels=[MathTex(r"\sin"), MathTex(r"\cos")],
h_buff=1,
element_to_mobject_config={"unit": r"^{\circ}"})

View file

@ -6,6 +6,7 @@ __all__ = [
"Code",
]
import re
from pathlib import Path
from typing import Any, Literal
@ -19,7 +20,6 @@ from manim.constants import *
from manim.mobject.geometry.arc import Dot
from manim.mobject.geometry.shape_matchers import SurroundingRectangle
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from manim.mobject.text.text_mobject import Paragraph
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
from manim.typing import StrPath
from manim.utils.color import WHITE, ManimColor
@ -119,6 +119,7 @@ class Code(VMobject, metaclass=ConvertToOpenGL):
"line_spacing": 0.5,
"disable_ligatures": True,
}
code: VMobject
def __init__(
self,
@ -169,10 +170,10 @@ class Code(VMobject, metaclass=ConvertToOpenGL):
if child.name == "span":
try:
child_style = child["style"]
if isinstance(child_style, str):
color = child_style.removeprefix("color: ")
else:
color = None
match_ = re.match(
r"color: (#[A-Fa-f0-9]{6}|#[A-Fa-f0-9]{3})", child_style
)
color = None if match_ is None else match_.group(1)
except KeyError:
color = None
current_line_color_ranges.append(
@ -200,11 +201,13 @@ class Code(VMobject, metaclass=ConvertToOpenGL):
base_paragraph_config = self.default_paragraph_config.copy()
base_paragraph_config.update(paragraph_config)
from manim.mobject.text.text_mobject import Paragraph
self.code_lines = Paragraph(
*code_lines,
**base_paragraph_config,
)
for line, color_range in zip(self.code_lines, color_ranges):
for line, color_range in zip(self.code_lines, color_ranges, strict=False):
for start, end, color in color_range:
line[start:end].set_color(color)
@ -224,6 +227,8 @@ class Code(VMobject, metaclass=ConvertToOpenGL):
)
self.add(self.line_numbers)
for line in self.code_lines:
line.submobjects = [c for c in line if not isinstance(c, Dot)]
self.add(self.code_lines)
if background_config is None:

View file

@ -4,8 +4,7 @@ from __future__ import annotations
__all__ = ["DecimalNumber", "Integer", "Variable"]
from collections.abc import Sequence
from typing import Any
from typing import Any, Self
import numpy as np
@ -16,10 +15,9 @@ from manim.mobject.text.tex_mobject import MathTex, SingleStringMathTex, Tex
from manim.mobject.text.text_mobject import Text
from manim.mobject.types.vectorized_mobject import VMobject
from manim.mobject.value_tracker import ValueTracker
from manim.typing import Vector3DLike
string_to_mob_map = {}
__all__ = ["DecimalNumber", "Integer", "Variable"]
string_to_mob_map: dict[str, SingleStringMathTex] = {}
class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
@ -86,7 +84,7 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
self,
number: float = 0,
num_decimal_places: int = 2,
mob_class: VMobject = MathTex,
mob_class: type[SingleStringMathTex] = MathTex,
include_sign: bool = False,
group_with_commas: bool = True,
digit_buff_per_font_unit: float = 0.001,
@ -94,11 +92,11 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
unit: str | None = None, # Aligned to bottom unless it starts with "^"
unit_buff_per_font_unit: float = 0,
include_background_rectangle: bool = False,
edge_to_fix: Sequence[float] = LEFT,
edge_to_fix: Vector3DLike = LEFT,
font_size: float = DEFAULT_FONT_SIZE,
stroke_width: float = 0,
fill_opacity: float = 1.0,
**kwargs,
**kwargs: Any,
):
super().__init__(**kwargs, fill_opacity=fill_opacity, stroke_width=stroke_width)
self.number = number
@ -137,12 +135,13 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
self.init_colors()
@property
def font_size(self):
def font_size(self) -> float:
"""The font size of the tex mobject."""
return self.height / self.initial_height * self._font_size
return_value: float = self.height / self.initial_height * self._font_size
return return_value
@font_size.setter
def font_size(self, font_val):
def font_size(self, font_val: float) -> None:
if font_val <= 0:
raise ValueError("font_size must be greater than 0.")
elif self.height > 0:
@ -153,7 +152,7 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
# font_size does not depend on current size.
self.scale(font_val / self.font_size)
def _set_submobjects_from_number(self, number):
def _set_submobjects_from_number(self, number: float) -> None:
self.number = number
self.submobjects = []
@ -197,12 +196,12 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
self.unit_sign.align_to(self, UP)
# track the initial height to enable scaling via font_size
self.initial_height = self.height
self.initial_height: float = self.height
if self.include_background_rectangle:
self.add_background_rectangle()
def _get_num_string(self, number):
def _get_num_string(self, number: float | complex) -> str:
if isinstance(number, complex):
formatter = self._get_complex_formatter()
else:
@ -215,7 +214,12 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
return num_string
def _string_to_mob(self, string: str, mob_class: VMobject | None = None, **kwargs):
def _string_to_mob(
self,
string: str,
mob_class: type[SingleStringMathTex] | None = None,
**kwargs: Any,
) -> VMobject:
if mob_class is None:
mob_class = self.mob_class
@ -225,7 +229,7 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
mob.font_size = self._font_size
return mob
def _get_formatter(self, **kwargs):
def _get_formatter(self, **kwargs: Any) -> str:
"""
Configuration is based first off instance attributes,
but overwritten by any kew word argument. Relevant
@ -258,7 +262,7 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
],
)
def _get_complex_formatter(self):
def _get_complex_formatter(self) -> str:
return "".join(
[
self._get_formatter(field_name="0.real"),
@ -267,7 +271,7 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
],
)
def set_value(self, number: float):
def set_value(self, number: float) -> Self:
"""Set the value of the :class:`~.DecimalNumber` to a new number.
Parameters
@ -290,7 +294,7 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
self._set_submobjects_from_number(number)
self.font_size = old_font_size
self.move_to(move_to_point, self.edge_to_fix)
for sm1, sm2 in zip(self.submobjects, old_submobjects):
for sm1, sm2 in zip(self.submobjects, old_submobjects, strict=False):
sm1.match_style(sm2)
if config.renderer == RendererType.CAIRO:
@ -304,10 +308,10 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
self.init_colors()
return self
def get_value(self):
def get_value(self) -> float:
return self.number
def increment_value(self, delta_t=1):
def increment_value(self, delta_t: float = 1) -> None:
self.set_value(self.get_value() + delta_t)
@ -333,7 +337,7 @@ class Integer(DecimalNumber):
) -> None:
super().__init__(number=number, num_decimal_places=num_decimal_places, **kwargs)
def get_value(self):
def get_value(self) -> int:
return int(np.round(super().get_value()))
@ -444,9 +448,9 @@ class Variable(VMobject, metaclass=ConvertToOpenGL):
self,
var: float,
label: str | Tex | MathTex | Text | SingleStringMathTex,
var_type: DecimalNumber | Integer = DecimalNumber,
var_type: type[DecimalNumber | Integer] = DecimalNumber,
num_decimal_places: int = 2,
**kwargs,
**kwargs: Any,
):
self.label = MathTex(label) if isinstance(label, str) else label
equals = MathTex("=").next_to(self.label, RIGHT)

View file

@ -12,7 +12,7 @@ r"""Mobjects representing text rendered using LaTeX.
from __future__ import annotations
from manim.utils.color import BLACK, ManimColor, ParsableManimColor
from manim.utils.color import BLACK, ParsableManimColor
__all__ = [
"SingleStringMathTex",
@ -23,12 +23,12 @@ __all__ = [
]
import itertools as it
import operator as op
import re
from collections.abc import Iterable
from functools import reduce
from textwrap import dedent
from typing import Any, Self
from manim import config, logger
from manim.constants import *
@ -38,7 +38,9 @@ from manim.mobject.types.vectorized_mobject import VGroup, VMobject
from manim.utils.tex import TexTemplate
from manim.utils.tex_file_writing import tex_to_svg_file
tex_string_to_mob_map = {}
from ..opengl.opengl_compatibility import ConvertToOpenGL
MATHTEX_SUBSTRING = "substring"
class SingleStringMathTex(SVGMobject):
@ -59,11 +61,11 @@ class SingleStringMathTex(SVGMobject):
should_center: bool = True,
height: float | None = None,
organize_left_to_right: bool = False,
tex_environment: str = "align*",
tex_environment: str | None = "align*",
tex_template: TexTemplate | None = None,
font_size: float = DEFAULT_FONT_SIZE,
color: ParsableManimColor | None = None,
**kwargs,
**kwargs: Any,
):
if color is None:
color = VMobject().color
@ -73,9 +75,8 @@ class SingleStringMathTex(SVGMobject):
self.tex_environment = tex_environment
if tex_template is None:
tex_template = config["tex_template"]
self.tex_template = tex_template
self.tex_template: TexTemplate = tex_template
assert isinstance(tex_string, str)
self.tex_string = tex_string
file_name = tex_to_svg_file(
self._get_modified_expression(tex_string),
@ -105,16 +106,16 @@ class SingleStringMathTex(SVGMobject):
if self.organize_left_to_right:
self._organize_submobjects_left_to_right()
def __repr__(self):
def __repr__(self) -> str:
return f"{type(self).__name__}({repr(self.tex_string)})"
@property
def font_size(self):
def font_size(self) -> float:
"""The font size of the tex mobject."""
return self.height / self.initial_height / SCALE_FACTOR_PER_FONT_POINT
@font_size.setter
def font_size(self, font_val):
def font_size(self, font_val: float) -> None:
if font_val <= 0:
raise ValueError("font_size must be greater than 0.")
elif self.height > 0:
@ -125,13 +126,13 @@ class SingleStringMathTex(SVGMobject):
# font_size does not depend on current size.
self.scale(font_val / self.font_size)
def _get_modified_expression(self, tex_string):
def _get_modified_expression(self, tex_string: str) -> str:
result = tex_string
result = result.strip()
result = self._modify_special_strings(result)
return result
def _modify_special_strings(self, tex):
def _modify_special_strings(self, tex: str) -> str:
tex = tex.strip()
should_add_filler = reduce(
op.or_,
@ -184,7 +185,7 @@ class SingleStringMathTex(SVGMobject):
tex = ""
return tex
def _remove_stray_braces(self, tex):
def _remove_stray_braces(self, tex: str) -> str:
r"""
Makes :class:`~.MathTex` resilient to unmatched braces.
@ -202,14 +203,14 @@ class SingleStringMathTex(SVGMobject):
num_rights += 1
return tex
def _organize_submobjects_left_to_right(self):
def _organize_submobjects_left_to_right(self) -> Self:
self.sort(lambda p: p[0])
return self
def get_tex_string(self):
def get_tex_string(self) -> str:
return self.tex_string
def init_colors(self, propagate_colors=True):
def init_colors(self, propagate_colors: bool = True) -> Self:
for submobject in self.submobjects:
# needed to preserve original (non-black)
# TeX colors of individual submobjects
@ -220,6 +221,7 @@ class SingleStringMathTex(SVGMobject):
submobject.init_colors()
elif config.renderer == RendererType.CAIRO:
submobject.init_colors(propagate_colors=propagate_colors)
return self
class MathTex(SingleStringMathTex):
@ -235,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::
@ -255,31 +277,40 @@ class MathTex(SingleStringMathTex):
def __init__(
self,
*tex_strings,
*tex_strings: str,
arg_separator: str = " ",
substrings_to_isolate: Iterable[str] | None = None,
tex_to_color_map: dict[str, ManimColor] = None,
tex_environment: str = "align*",
**kwargs,
tex_to_color_map: dict[str, ParsableManimColor] | None = None,
tex_environment: str | None = "align*",
**kwargs: Any,
):
self.tex_template = kwargs.pop("tex_template", config["tex_template"])
self.arg_separator = arg_separator
self.substrings_to_isolate = (
[] if substrings_to_isolate is None else substrings_to_isolate
[] if substrings_to_isolate is None else list(substrings_to_isolate)
)
self.tex_to_color_map = tex_to_color_map
if self.tex_to_color_map is None:
self.tex_to_color_map = {}
if tex_to_color_map is None:
self.tex_to_color_map: dict[str, ParsableManimColor] = {}
else:
self.tex_to_color_map = tex_to_color_map
self.substrings_to_isolate.extend(self.tex_to_color_map.keys())
self.tex_environment = tex_environment
self.brace_notation_split_occurred = False
self.tex_strings = self._break_up_tex_strings(tex_strings)
self.tex_strings = self._prepare_tex_strings(tex_strings)
self.matched_strings_and_ids: list[tuple[str, str]] = []
try:
joined_string = self._join_tex_strings_with_unique_deliminters(
self.tex_strings, self.substrings_to_isolate
)
super().__init__(
self.arg_separator.join(self.tex_strings),
joined_string,
tex_environment=self.tex_environment,
tex_template=self.tex_template,
**kwargs,
)
# Save the original tex_string
self.tex_string = self.arg_separator.join(self.tex_strings)
self._break_up_by_substrings()
except ValueError as compilation_error:
if self.brace_notation_split_occurred:
@ -301,88 +332,231 @@ class MathTex(SingleStringMathTex):
if self.organize_left_to_right:
self._organize_submobjects_left_to_right()
def _break_up_tex_strings(self, tex_strings):
# Separate out anything surrounded in double braces
pre_split_length = len(tex_strings)
tex_strings = [re.split("{{(.*?)}}", str(t)) for t in tex_strings]
tex_strings = sum(tex_strings, [])
if len(tex_strings) > pre_split_length:
def _prepare_tex_strings(self, tex_strings: Iterable[str]) -> list[str]:
# Deal with the case where tex_strings contains integers instead
# of strings.
tex_strings_validated = [
string if isinstance(string, str) else str(string) for string in tex_strings
]
# Locate double curly bracers and split on them.
tex_strings_validated_two = []
for tex_string in tex_strings_validated:
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]
# Separate out any strings specified in the isolate
# or tex_to_color_map lists.
patterns = []
patterns.extend(
[
f"({re.escape(ss)})"
for ss in it.chain(
self.substrings_to_isolate,
self.tex_to_color_map.keys(),
@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:
joined_string = ""
ssIdx = 0
for idx, tex_string in enumerate(tex_strings):
string_part = rf"\special{{dvisvgm:raw <g id='unique{idx:03d}'>}}"
self.matched_strings_and_ids.append((tex_string, f"unique{idx:03d}"))
# Try to match with all substrings_to_isolate and apply the first match
# then match again (on the rest of the string) and continue until no
# characters are left in the string
unprocessed_string = str(tex_string)
processed_string = ""
while len(unprocessed_string) > 0:
first_match = self._locate_first_match(
substrings_to_isolate, unprocessed_string
)
],
)
pattern = "|".join(patterns)
if pattern:
pieces = []
for s in tex_strings:
pieces.extend(re.split(pattern, s))
else:
pieces = tex_strings
return [p for p in pieces if p]
def _break_up_by_substrings(self):
if first_match:
processed, unprocessed_string = self._handle_match(
ssIdx, first_match
)
processed_string = processed_string + processed
ssIdx += 1
else:
processed_string = processed_string + unprocessed_string
unprocessed_string = ""
string_part += processed_string
if idx < len(tex_strings) - 1:
string_part += self.arg_separator
string_part += r"\special{dvisvgm:raw </g>}"
joined_string = joined_string + string_part
return joined_string
def _locate_first_match(
self, substrings_to_isolate: Iterable[str], unprocessed_string: str
) -> re.Match | None:
first_match_start = len(unprocessed_string)
first_match_length = 0
first_match = None
for substring in substrings_to_isolate:
match = re.match(f"(.*?)({re.escape(substring)})(.*)", unprocessed_string)
if match and len(match.group(1)) < first_match_start:
first_match = match
first_match_start = len(match.group(1))
first_match_length = len(match.group(2))
elif match and len(match.group(1)) == first_match_start:
# Break ties by looking at length of matches.
if first_match_length < len(match.group(2)):
first_match = match
first_match_start = len(match.group(1))
first_match_length = len(match.group(2))
return first_match
def _handle_match(self, ssIdx: int, first_match: re.Match) -> tuple[str, str]:
pre_match = first_match.group(1)
matched_string = first_match.group(2)
post_match = first_match.group(3)
pre_string = (
rf"\special{{dvisvgm:raw <g id='unique{ssIdx:03d}{MATHTEX_SUBSTRING}'>}}"
)
post_string = r"\special{dvisvgm:raw </g>}"
self.matched_strings_and_ids.append(
(matched_string, f"unique{ssIdx:03d}{MATHTEX_SUBSTRING}")
)
processed_string = pre_match + pre_string + matched_string + post_string
unprocessed_string = post_match
return processed_string, unprocessed_string
@property
def _substring_matches(self) -> list[tuple[str, str]]:
"""Return only the 'ss' (substring_to_isolate) matches."""
return [
(tex, id_)
for tex, id_ in self.matched_strings_and_ids
if id_.endswith(MATHTEX_SUBSTRING)
]
@property
def _main_matches(self) -> list[tuple[str, str]]:
"""Return only the main tex_string matches."""
return [
(tex, id_)
for tex, id_ in self.matched_strings_and_ids
if not id_.endswith(MATHTEX_SUBSTRING)
]
def _break_up_by_substrings(self) -> Self:
"""
Reorganize existing submobjects one layer
deeper based on the structure of tex_strings (as a list
of tex_strings)
"""
new_submobjects = []
curr_index = 0
for tex_string in self.tex_strings:
sub_tex_mob = SingleStringMathTex(
tex_string,
tex_environment=self.tex_environment,
tex_template=self.tex_template,
new_submobjects: list[VMobject] = []
try:
for tex_string, tex_string_id in self._main_matches:
mtp = MathTexPart()
mtp.tex_string = tex_string
mtp.add(*self.id_to_vgroup_dict[tex_string_id].submobjects)
new_submobjects.append(mtp)
except KeyError:
logger.error(
f"MathTex: Could not find SVG group for tex part '{tex_string}' (id: {tex_string_id}). Using fallback to root group."
)
num_submobs = len(sub_tex_mob.submobjects)
new_index = (
curr_index + num_submobs + len("".join(self.arg_separator.split()))
)
if num_submobs == 0:
last_submob_index = min(curr_index, len(self.submobjects) - 1)
sub_tex_mob.move_to(self.submobjects[last_submob_index], RIGHT)
else:
sub_tex_mob.submobjects = self.submobjects[curr_index:new_index]
new_submobjects.append(sub_tex_mob)
curr_index = new_index
new_submobjects.append(self.id_to_vgroup_dict["root"])
self.submobjects = new_submobjects
return self
def get_parts_by_tex(self, tex, substring=True, case_sensitive=True):
def test(tex1, tex2):
if not case_sensitive:
tex1 = tex1.lower()
tex2 = tex2.lower()
if substring:
return tex1 in tex2
else:
return tex1 == tex2
def get_part_by_tex(self, tex: str, **kwargs: Any) -> VGroup | None:
for tex_str, match_id in self.matched_strings_and_ids:
if tex_str == tex:
return self.id_to_vgroup_dict[match_id]
return None
return VGroup(*(m for m in self.submobjects if test(tex, m.get_tex_string())))
def get_part_by_tex(self, tex, **kwargs):
all_parts = self.get_parts_by_tex(tex, **kwargs)
return all_parts[0] if all_parts else None
def set_color_by_tex(self, tex, color, **kwargs):
parts_to_color = self.get_parts_by_tex(tex, **kwargs)
for part in parts_to_color:
part.set_color(color)
def set_color_by_tex(
self, tex: str, color: ParsableManimColor, **kwargs: Any
) -> Self:
for tex_str, match_id in self.matched_strings_and_ids:
if tex_str == tex:
self.id_to_vgroup_dict[match_id].set_color(color)
return self
def set_opacity_by_tex(
self, tex: str, opacity: float = 0.5, remaining_opacity: float = None, **kwargs
):
self,
tex: str,
opacity: float = 0.5,
remaining_opacity: float | None = None,
**kwargs: Any,
) -> Self:
"""
Sets the opacity of the tex specified. If 'remaining_opacity' is specified,
then the remaining tex will be set to that opacity.
@ -399,36 +573,37 @@ class MathTex(SingleStringMathTex):
"""
if remaining_opacity is not None:
self.set_opacity(opacity=remaining_opacity)
for part in self.get_parts_by_tex(tex):
part.set_opacity(opacity)
for tex_str, match_id in self.matched_strings_and_ids:
if tex_str == tex:
self.id_to_vgroup_dict[match_id].set_opacity(opacity)
return self
def set_color_by_tex_to_color_map(self, texs_to_color_map, **kwargs):
def set_color_by_tex_to_color_map(
self, texs_to_color_map: dict[str, ParsableManimColor], **kwargs: Any
) -> Self:
for texs, color in list(texs_to_color_map.items()):
try:
# If the given key behaves like tex_strings
texs + ""
self.set_color_by_tex(texs, color, **kwargs)
except TypeError:
# If the given key is a tuple
for tex in texs:
self.set_color_by_tex(tex, color, **kwargs)
for match in self.matched_strings_and_ids:
if match[0] == texs:
self.id_to_vgroup_dict[match[1]].set_color(color)
return self
def index_of_part(self, part):
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")
return split_self.index(part)
def index_of_part_by_tex(self, tex, **kwargs):
part = self.get_part_by_tex(tex, **kwargs)
return self.index_of_part(part)
def sort_alphabetically(self):
def sort_alphabetically(self) -> None:
self.submobjects.sort(key=lambda m: m.get_tex_string())
class MathTexPart(VMobject, metaclass=ConvertToOpenGL):
tex_string: str
def __repr__(self) -> str:
return f"{type(self).__name__}({repr(self.tex_string)})"
class Tex(MathTex):
r"""A string compiled with LaTeX in normal mode.
@ -447,7 +622,11 @@ class Tex(MathTex):
"""
def __init__(
self, *tex_strings, arg_separator="", tex_environment="center", **kwargs
self,
*tex_strings: str,
arg_separator: str = "",
tex_environment: str | None = "center",
**kwargs: Any,
):
super().__init__(
*tex_strings,
@ -477,18 +656,20 @@ class BulletedList(Tex):
def __init__(
self,
*items,
buff=MED_LARGE_BUFF,
dot_scale_factor=2,
tex_environment=None,
**kwargs,
*items: str,
buff: float = MED_LARGE_BUFF,
dot_scale_factor: float = 2,
tex_environment: str | None = None,
**kwargs: Any,
):
self.buff = buff
self.dot_scale_factor = dot_scale_factor
self.tex_environment = tex_environment
line_separated_items = [s + "\\\\" for s in items]
super().__init__(
*line_separated_items, tex_environment=tex_environment, **kwargs
*line_separated_items,
tex_environment=tex_environment,
**kwargs,
)
for part in self:
dot = MathTex("\\cdot").scale(self.dot_scale_factor)
@ -496,10 +677,14 @@ class BulletedList(Tex):
part.add_to_back(dot)
self.arrange(DOWN, aligned_edge=LEFT, buff=self.buff)
def fade_all_but(self, index_or_string, opacity=0.5):
def fade_all_but(self, index_or_string: int | str, opacity: float = 0.5) -> None:
arg = index_or_string
if isinstance(arg, str):
part = self.get_part_by_tex(arg)
part: VGroup | VMobject | None = self.get_part_by_tex(arg)
if part is None:
raise Exception(
f"Could not locate part by provided tex string '{arg}'."
)
elif isinstance(arg, int):
part = self.submobjects[arg]
else:
@ -531,11 +716,11 @@ class Title(Tex):
def __init__(
self,
*text_parts,
include_underline=True,
match_underline_width_to_text=False,
underline_buff=MED_SMALL_BUFF,
**kwargs,
*text_parts: str,
include_underline: bool = True,
match_underline_width_to_text: bool = False,
underline_buff: float = MED_SMALL_BUFF,
**kwargs: Any,
):
self.include_underline = include_underline
self.match_underline_width_to_text = match_underline_width_to_text

View file

@ -57,10 +57,11 @@ __all__ = ["Text", "Paragraph", "MarkupText", "register_font"]
import copy
import hashlib
import re
from collections.abc import Iterable, Sequence
from collections.abc import Iterable, Iterator, Sequence
from contextlib import contextmanager
from itertools import chain
from pathlib import Path
from typing import TYPE_CHECKING, Any
import manimpango
import numpy as np
@ -71,8 +72,13 @@ from manim.constants import *
from manim.mobject.geometry.arc import Dot
from manim.mobject.svg.svg_mobject import SVGMobject
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
from manim.typing import Point3D
from manim.utils.color import ManimColor, ParsableManimColor, color_gradient
from manim.utils.deprecation import deprecated
if TYPE_CHECKING:
from typing import Self
from manim.typing import Point3D
TEXT_MOB_SCALE_FACTOR = 0.05
DEFAULT_LINE_SPACING_SCALE = 0.3
@ -81,7 +87,7 @@ TEXT2SVG_ADJUSTMENT_FACTOR = 4.8
__all__ = ["Text", "Paragraph", "MarkupText", "register_font"]
def remove_invisible_chars(mobject: SVGMobject) -> SVGMobject:
def remove_invisible_chars(mobject: VMobject) -> VMobject:
"""Function to remove unwanted invisible characters from some mobjects.
Parameters
@ -94,24 +100,14 @@ def remove_invisible_chars(mobject: SVGMobject) -> SVGMobject:
:class:`~.SVGMobject`
The SVGMobject without unwanted invisible characters.
"""
# TODO: Refactor needed
iscode = False
if mobject.__class__.__name__ == "Text":
mobject = mobject[:]
elif mobject.__class__.__name__ == "Code":
iscode = True
code = mobject
mobject = mobject.code
mobject_without_dots = VGroup()
if mobject[0].__class__ == VGroup:
for i in range(len(mobject)):
mobject_without_dots.add(VGroup())
mobject_without_dots[i].add(*(k for k in mobject[i] if k.__class__ != Dot))
if isinstance(mobject[0], VGroup):
for submob in mobject:
mobject_without_dots.add(
VGroup(k for k in submob if not isinstance(k, Dot))
)
else:
mobject_without_dots.add(*(k for k in mobject if k.__class__ != Dot))
if iscode:
code.code = mobject_without_dots
return code
mobject_without_dots.add(*(k for k in mobject if not isinstance(k, Dot)))
return mobject_without_dots
@ -155,11 +151,11 @@ class Paragraph(VGroup):
def __init__(
self,
*text: Sequence[str],
*text: str,
line_spacing: float = -1,
alignment: str | None = None,
**kwargs,
) -> None:
**kwargs: Any,
):
self.line_spacing = line_spacing
self.alignment = alignment
self.consider_spaces_as_chars = kwargs.get("disable_ligatures", False)
@ -170,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)
@ -225,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
@ -244,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],
)
@ -259,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
@ -273,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(
[
@ -288,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(
[
@ -420,7 +419,8 @@ class Text(SVGMobject):
@staticmethod
@functools.cache
def font_list() -> list[str]:
return manimpango.list_fonts()
value: list[str] = manimpango.list_fonts()
return value
def __init__(
self,
@ -433,22 +433,22 @@ class Text(SVGMobject):
font: str = "",
slant: str = NORMAL,
weight: str = NORMAL,
t2c: dict[str, str] = None,
t2f: dict[str, str] = None,
t2g: dict[str, tuple] = None,
t2s: dict[str, str] = None,
t2w: dict[str, str] = None,
gradient: tuple = None,
t2c: dict[str, str] | None = None,
t2f: dict[str, str] | None = None,
t2g: dict[str, Iterable[ParsableManimColor]] | None = None,
t2s: dict[str, str] | None = None,
t2w: dict[str, str] | None = None,
gradient: Iterable[ParsableManimColor] | None = None,
tab_width: int = 4,
warn_missing_font: bool = True,
# Mobject
height: float = None,
width: float = None,
height: float | None = None,
width: float | None = None,
should_center: bool = True,
disable_ligatures: bool = False,
use_svg_cache: bool = False,
**kwargs,
) -> None:
**kwargs: Any,
):
self.line_spacing = line_spacing
if font and warn_missing_font:
fonts_list = Text.font_list()
@ -489,11 +489,16 @@ class Text(SVGMobject):
t2g = kwargs.pop("text2gradient", t2g)
t2s = kwargs.pop("text2slant", t2s)
t2w = kwargs.pop("text2weight", t2w)
self.t2c = {k: ManimColor(v).to_hex() for k, v in t2c.items()}
self.t2f = t2f
self.t2g = t2g
self.t2s = t2s
self.t2w = t2w
assert t2c is not None
assert t2f is not None
assert t2g is not None
assert t2s is not None
assert t2w is not None
self.t2c: dict[str, str] = {k: ManimColor(v).to_hex() for k, v in t2c.items()}
self.t2f: dict[str, str] = t2f
self.t2g: dict[str, Iterable[ParsableManimColor]] = t2g
self.t2s: dict[str, str] = t2s
self.t2w: dict[str, str] = t2w
self.original_text = text
self.disable_ligatures = disable_ligatures
@ -508,8 +513,8 @@ class Text(SVGMobject):
else:
self.line_spacing = self._font_size + self._font_size * self.line_spacing
color: ManimColor = ManimColor(color) if color else VMobject().color
file_name = self._text2svg(color.to_hex())
parsed_color: ManimColor = ManimColor(color) if color else VMobject().color
file_name = self._text2svg(parsed_color.to_hex())
PangoUtils.remove_last_M(file_name)
super().__init__(
file_name,
@ -541,12 +546,12 @@ class Text(SVGMobject):
# into a numpy array at the end, rather than creating
# new numpy arrays every time a point or fixing line
# is added (which is O(n^2) for numpy arrays).
closed_curve_points = []
closed_curve_points: list[Point3D] = []
# OpenGL has points be part of quadratic Bezier curves;
# Cairo uses cubic Bezier curves.
if nppc == 3: # RendererType.OPENGL
def add_line_to(end):
def add_line_to(end: Point3D) -> None:
nonlocal closed_curve_points
start = closed_curve_points[-1]
closed_curve_points += [
@ -557,7 +562,7 @@ class Text(SVGMobject):
else: # RendererType.CAIRO
def add_line_to(end):
def add_line_to(end: Point3D) -> None:
nonlocal closed_curve_points
start = closed_curve_points[-1]
closed_curve_points += [
@ -588,11 +593,11 @@ class Text(SVGMobject):
self.scale(TEXT_MOB_SCALE_FACTOR)
self.initial_height = self.height
def __repr__(self):
def __repr__(self) -> str:
return f"Text({repr(self.original_text)})"
@property
def font_size(self):
def font_size(self) -> float:
return (
self.height
/ self.initial_height
@ -603,14 +608,14 @@ class Text(SVGMobject):
)
@font_size.setter
def font_size(self, font_val):
def font_size(self, font_val: float) -> None:
# TODO: use pango's font size scaling.
if font_val <= 0:
raise ValueError("font_size must be greater than 0.")
else:
self.scale(font_val / self.font_size)
def _gen_chars(self):
def _gen_chars(self) -> VGroup:
chars = self.get_group_class()()
submobjects_char_index = 0
for char_index in range(len(self.text)):
@ -628,7 +633,7 @@ class Text(SVGMobject):
submobjects_char_index += 1
return chars
def _find_indexes(self, word: str, text: str):
def _find_indexes(self, word: str, text: str) -> list[tuple[int, int]]:
"""Finds the indexes of ``text`` in ``word``."""
temp = re.match(r"\[([0-9\-]{0,}):([0-9\-]{0,})\]", word)
if temp:
@ -636,7 +641,9 @@ class Text(SVGMobject):
end = int(temp.group(2)) if temp.group(2) != "" else len(text)
start = len(text) + start if start < 0 else start
end = len(text) + end if end < 0 else end
return [(start, end)]
return [
(start, end),
]
indexes = []
index = text.find(word)
while index != -1:
@ -644,33 +651,7 @@ class Text(SVGMobject):
index = text.find(word, index + len(word))
return indexes
@deprecated(
since="v0.14.0",
until="v0.15.0",
message="This was internal function, you shouldn't be using it anyway.",
)
def _set_color_by_t2c(self, t2c=None):
"""Sets color for specified strings."""
t2c = t2c if t2c else self.t2c
for word, color in list(t2c.items()):
for start, end in self._find_indexes(word, self.text):
self.chars[start:end].set_color(color)
@deprecated(
since="v0.14.0",
until="v0.15.0",
message="This was internal function, you shouldn't be using it anyway.",
)
def _set_color_by_t2g(self, t2g=None):
"""Sets gradient colors for specified
strings. Behaves similarly to ``set_color_by_t2c``.
"""
t2g = t2g if t2g else self.t2g
for word, gradient in list(t2g.items()):
for start, end in self._find_indexes(word, self.text):
self.chars[start:end].set_color_by_gradient(*gradient)
def _text2hash(self, color: ManimColor):
def _text2hash(self, color: ParsableManimColor) -> str:
"""Generates ``sha256`` hash for file name."""
settings = (
"PANGO" + self.font + self.slant + self.weight + str(color)
@ -678,6 +659,7 @@ class Text(SVGMobject):
settings += str(self.t2f) + str(self.t2s) + str(self.t2w) + str(self.t2c)
settings += str(self.line_spacing) + str(self._font_size)
settings += str(self.disable_ligatures)
settings += str(self.gradient)
id_str = self.text + settings
hasher = hashlib.sha256()
hasher.update(id_str.encode())
@ -713,7 +695,7 @@ class Text(SVGMobject):
self,
t2xs: Sequence[tuple[dict[str, str], str]],
default_args: dict[str, Iterable[str]],
) -> Sequence[TextSetting]:
) -> list[TextSetting]:
settings = []
t2xwords = set(chain(*([*t2x.keys()] for t2x, _ in t2xs)))
for word in t2xwords:
@ -729,34 +711,27 @@ class Text(SVGMobject):
return settings
def _get_settings_from_gradient(
self, default_args: dict[str, Iterable[str]]
) -> Sequence[TextSetting]:
self, default_args: dict[str, Any]
) -> list[TextSetting]:
settings = []
args = copy.copy(default_args)
if self.gradient:
colors = color_gradient(self.gradient, len(self.text))
colors: list[ManimColor] = color_gradient(self.gradient, len(self.text))
for i in range(len(self.text)):
args["color"] = colors[i].to_hex()
settings.append(TextSetting(i, i + 1, **args))
for word, gradient in self.t2g.items():
if isinstance(gradient, str) or len(gradient) == 1:
color = gradient if isinstance(gradient, str) else gradient[0]
gradient = [ManimColor(color)]
colors = (
color_gradient(gradient, len(word))
if len(gradient) != 1
else len(word) * gradient
)
colors = color_gradient(gradient, len(word))
for start, end in self._find_indexes(word, self.text):
for i in range(start, end):
args["color"] = colors[i - start].to_hex()
settings.append(TextSetting(i, i + 1, **args))
return settings
def _text2settings(self, color: str):
def _text2settings(self, color: ParsableManimColor) -> list[TextSetting]:
"""Converts the texts and styles to a setting for parsing."""
t2xs = [
t2xs: list[tuple[dict[str, str], str]] = [
(self.t2f, "font"),
(self.t2s, "slant"),
(self.t2w, "weight"),
@ -764,7 +739,7 @@ class Text(SVGMobject):
]
# setting_args requires values to be strings
default_args = {
default_args: dict[str, Any] = {
arg: getattr(self, arg) if arg != "color" else color for _, arg in t2xs
}
@ -802,15 +777,15 @@ class Text(SVGMobject):
line_num = 0
if re.search(r"\n", self.text):
for start, end in self._find_indexes("\n", self.text):
for for_start, for_end in self._find_indexes("\n", self.text):
for setting in settings:
if setting.line_num == -1:
setting.line_num = line_num
if start < setting.end:
if for_start < setting.end:
line_num += 1
new_setting = copy.copy(setting)
setting.end = end
new_setting.start = end
setting.end = for_end
new_setting.start = for_end
new_setting.line_num = line_num
settings.append(new_setting)
settings.sort(key=lambda setting: setting.start)
@ -821,7 +796,7 @@ class Text(SVGMobject):
return settings
def _text2svg(self, color: ManimColor):
def _text2svg(self, color: ParsableManimColor) -> str:
"""Convert the text to SVG using Pango."""
size = self._font_size
line_spacing = self.line_spacing
@ -829,8 +804,7 @@ class Text(SVGMobject):
line_spacing /= TEXT2SVG_ADJUSTMENT_FACTOR
dir_name = config.get_dir("text_dir")
if not dir_name.is_dir():
dir_name.mkdir(parents=True)
dir_name.mkdir(parents=True, exist_ok=True)
hash_name = self._text2hash(color)
file_name = dir_name / (hash_name + ".svg")
@ -856,11 +830,12 @@ class Text(SVGMobject):
return svg_file
def init_colors(self, propagate_colors=True):
def init_colors(self, propagate_colors: bool = True) -> Self:
if config.renderer == RendererType.OPENGL:
super().init_colors()
elif config.renderer == RendererType.CAIRO:
super().init_colors(propagate_colors=propagate_colors)
return self
class MarkupText(SVGMobject):
@ -1164,7 +1139,8 @@ class MarkupText(SVGMobject):
@staticmethod
@functools.cache
def font_list() -> list[str]:
return manimpango.list_fonts()
value: list[str] = manimpango.list_fonts()
return value
def __init__(
self,
@ -1173,22 +1149,22 @@ class MarkupText(SVGMobject):
stroke_width: float = 0,
color: ParsableManimColor | None = None,
font_size: float = DEFAULT_FONT_SIZE,
line_spacing: int = -1,
line_spacing: float = -1,
font: str = "",
slant: str = NORMAL,
weight: str = NORMAL,
justify: bool = False,
gradient: tuple = None,
gradient: Iterable[ParsableManimColor] | None = None,
tab_width: int = 4,
height: int = None,
width: int = None,
height: int | None = None,
width: int | None = None,
should_center: bool = True,
disable_ligatures: bool = False,
warn_missing_font: bool = True,
**kwargs,
) -> None:
**kwargs: Any,
):
self.text = text
self.line_spacing = line_spacing
self.line_spacing: float = line_spacing
if font and warn_missing_font:
fonts_list = Text.font_list()
# handle special case of sans/sans-serif
@ -1235,8 +1211,8 @@ class MarkupText(SVGMobject):
else:
self.line_spacing = self._font_size + self._font_size * self.line_spacing
color: ManimColor = ManimColor(color) if color else VMobject().color
file_name = self._text2svg(color)
parsed_color: ManimColor = ManimColor(color) if color else VMobject().color
file_name = self._text2svg(parsed_color)
PangoUtils.remove_last_M(file_name)
super().__init__(
@ -1267,12 +1243,12 @@ class MarkupText(SVGMobject):
# into a numpy array at the end, rather than creating
# new numpy arrays every time a point or fixing line
# is added (which is O(n^2) for numpy arrays).
closed_curve_points = []
closed_curve_points: list[Point3D] = []
# OpenGL has points be part of quadratic Bezier curves;
# Cairo uses cubic Bezier curves.
if nppc == 3: # RendererType.OPENGL
def add_line_to(end):
def add_line_to(end: Point3D) -> None:
nonlocal closed_curve_points
start = closed_curve_points[-1]
closed_curve_points += [
@ -1283,7 +1259,7 @@ class MarkupText(SVGMobject):
else: # RendererType.CAIRO
def add_line_to(end):
def add_line_to(end: Point3D) -> None:
nonlocal closed_curve_points
start = closed_curve_points[-1]
closed_curve_points += [
@ -1331,7 +1307,7 @@ class MarkupText(SVGMobject):
self.initial_height = self.height
@property
def font_size(self):
def font_size(self) -> float:
return (
self.height
/ self.initial_height
@ -1342,14 +1318,14 @@ class MarkupText(SVGMobject):
)
@font_size.setter
def font_size(self, font_val):
def font_size(self, font_val: float) -> None:
# TODO: use pango's font size scaling.
if font_val <= 0:
raise ValueError("font_size must be greater than 0.")
else:
self.scale(font_val / self.font_size)
def _text2hash(self, color: ParsableManimColor):
def _text2hash(self, color: ParsableManimColor) -> str:
"""Generates ``sha256`` hash for file name."""
settings = (
"MARKUPPANGO"
@ -1366,22 +1342,21 @@ class MarkupText(SVGMobject):
hasher.update(id_str.encode())
return hasher.hexdigest()[:16]
def _text2svg(self, color: ParsableManimColor | None):
def _text2svg(self, color: ParsableManimColor | None) -> str:
"""Convert the text to SVG using Pango."""
color = ManimColor(color)
size = self._font_size
line_spacing = self.line_spacing
line_spacing: float = self.line_spacing
size /= TEXT2SVG_ADJUSTMENT_FACTOR
line_spacing /= TEXT2SVG_ADJUSTMENT_FACTOR
dir_name = config.get_dir("text_dir")
if not dir_name.is_dir():
dir_name.mkdir(parents=True)
dir_name.mkdir(parents=True, exist_ok=True)
hash_name = self._text2hash(color)
file_name = dir_name / (hash_name + ".svg")
if file_name.exists():
svg_file = str(file_name.resolve())
svg_file: str = str(file_name.resolve())
else:
final_text = (
f'<span foreground="{color.to_hex()}">{self.text}</span>'
@ -1407,7 +1382,7 @@ class MarkupText(SVGMobject):
)
return svg_file
def _count_real_chars(self, s):
def _count_real_chars(self, s: str) -> int:
"""Counts characters that will be displayed.
This is needed for partial coloring or gradients, because space
@ -1426,7 +1401,7 @@ class MarkupText(SVGMobject):
count += 1
return count
def _extract_gradient_tags(self):
def _extract_gradient_tags(self) -> list[dict[str, Any]]:
"""Used to determine which parts (if any) of the string should be formatted
with a gradient.
@ -1437,7 +1412,7 @@ class MarkupText(SVGMobject):
self.original_text,
re.S,
)
gradientmap = []
gradientmap: list[dict[str, Any]] = []
for tag in tags:
start = self._count_real_chars(self.original_text[: tag.start(0)])
end = start + self._count_real_chars(tag.group(5))
@ -1460,14 +1435,14 @@ class MarkupText(SVGMobject):
)
return gradientmap
def _parse_color(self, col):
def _parse_color(self, col: str) -> str:
"""Parse color given in ``<color>`` or ``<gradient>`` tags."""
if re.match("#[0-9a-f]{6}", col):
return col
else:
return ManimColor(col).to_hex()
def _extract_color_tags(self):
def _extract_color_tags(self) -> list[dict[str, Any]]:
"""Used to determine which parts (if any) of the string should be formatted
with a custom color.
@ -1482,7 +1457,7 @@ class MarkupText(SVGMobject):
re.S,
)
colormap = []
colormap: list[dict[str, Any]] = []
for tag in tags:
start = self._count_real_chars(self.original_text[: tag.start(0)])
end = start + self._count_real_chars(tag.group(4))
@ -1504,12 +1479,12 @@ class MarkupText(SVGMobject):
)
return colormap
def __repr__(self):
def __repr__(self) -> str:
return f"MarkupText({repr(self.original_text)})"
@contextmanager
def register_font(font_file: str | Path):
def register_font(font_file: str | Path) -> Iterator[None]:
"""Temporarily add a font file to Pango's search path.
This searches for the font_file at various places. The order it searches it described below.
@ -1561,7 +1536,7 @@ def register_font(font_file: str | Path):
logger.debug("Found file at %s", file_path.absolute())
break
else:
error = f"Can't find {font_file}.Tried these : {possible_paths}"
error = f"Can't find {font_file}. Checked paths: {possible_paths}"
raise FileNotFoundError(error)
try:

View file

@ -2,7 +2,8 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from collections.abc import Hashable
from typing import TYPE_CHECKING, Any
import numpy as np
@ -14,7 +15,7 @@ from manim.utils.qhull import QuickHull
if TYPE_CHECKING:
from manim.mobject.mobject import Mobject
from manim.typing import Point3D
from manim.typing import Point3D, Point3DLike_Array
__all__ = [
"Polyhedron",
@ -96,10 +97,10 @@ class Polyhedron(VGroup):
def __init__(
self,
vertex_coords: list[list[float] | np.ndarray],
vertex_coords: Point3DLike_Array,
faces_list: list[list[int]],
faces_config: dict[str, str | int | float | bool] = {},
graph_config: dict[str, str | int | float | bool] = {},
graph_config: dict[str, Any] = {},
):
super().__init__()
self.faces_config = dict(
@ -116,7 +117,7 @@ class Polyhedron(VGroup):
)
self.vertex_coords = vertex_coords
self.vertex_indices = list(range(len(self.vertex_coords)))
self.layout = dict(enumerate(self.vertex_coords))
self.layout: dict[Hashable, Any] = dict(enumerate(self.vertex_coords))
self.faces_list = faces_list
self.face_coords = [[self.layout[j] for j in i] for i in faces_list]
self.edges = self.get_edges(self.faces_list)
@ -129,14 +130,14 @@ class Polyhedron(VGroup):
def get_edges(self, faces_list: list[list[int]]) -> list[tuple[int, int]]:
"""Creates list of cyclic pairwise tuples."""
edges = []
edges: list[tuple[int, int]] = []
for face in faces_list:
edges += zip(face, face[1:] + face[:1])
edges += zip(face, face[1:] + face[:1], strict=True)
return edges
def create_faces(
self,
face_coords: list[list[list | np.ndarray]],
face_coords: Point3DLike_Array,
) -> VGroup:
"""Creates VGroup of faces from a list of face coordinates."""
face_group = VGroup()
@ -144,18 +145,16 @@ class Polyhedron(VGroup):
face_group.add(Polygon(*face, **self.faces_config))
return face_group
def update_faces(self, m: Mobject):
def update_faces(self, m: Mobject) -> None:
face_coords = self.extract_face_coords()
new_faces = self.create_faces(face_coords)
self.faces.match_points(new_faces)
def extract_face_coords(self) -> list[list[np.ndarray]]:
def extract_face_coords(self) -> Point3DLike_Array:
"""Extracts the coordinates of the vertices in the graph.
Used for updating faces.
"""
new_vertex_coords = []
for v in self.graph.vertices:
new_vertex_coords.append(self.graph[v].get_center())
new_vertex_coords = [self.graph[v].get_center() for v in self.graph.vertices]
layout = dict(enumerate(new_vertex_coords))
return [[layout[j] for j in i] for i in self.faces_list]
@ -181,7 +180,7 @@ class Tetrahedron(Polyhedron):
self.add(obj)
"""
def __init__(self, edge_length: float = 1, **kwargs):
def __init__(self, edge_length: float = 1, **kwargs: Any):
unit = edge_length * np.sqrt(2) / 4
super().__init__(
vertex_coords=[
@ -216,7 +215,7 @@ class Octahedron(Polyhedron):
self.add(obj)
"""
def __init__(self, edge_length: float = 1, **kwargs):
def __init__(self, edge_length: float = 1, **kwargs: Any):
unit = edge_length * np.sqrt(2) / 2
super().__init__(
vertex_coords=[
@ -262,7 +261,7 @@ class Icosahedron(Polyhedron):
self.add(obj)
"""
def __init__(self, edge_length: float = 1, **kwargs):
def __init__(self, edge_length: float = 1, **kwargs: Any):
unit_a = edge_length * ((1 + np.sqrt(5)) / 4)
unit_b = edge_length * (1 / 2)
super().__init__(
@ -327,7 +326,7 @@ class Dodecahedron(Polyhedron):
self.add(obj)
"""
def __init__(self, edge_length: float = 1, **kwargs):
def __init__(self, edge_length: float = 1, **kwargs: Any):
unit_a = edge_length * ((1 + np.sqrt(5)) / 4)
unit_b = edge_length * ((3 + np.sqrt(5)) / 4)
unit_c = edge_length * (1 / 2)
@ -427,7 +426,7 @@ class ConvexHull3D(Polyhedron):
self.add(dots)
"""
def __init__(self, *points: Point3D, tolerance: float = 1e-5, **kwargs):
def __init__(self, *points: Point3D, tolerance: float = 1e-5, **kwargs: Any):
# Build Convex Hull
array = np.array(points)
hull = QuickHull(tolerance)

View file

@ -2,9 +2,6 @@
from __future__ import annotations
from manim.typing import Point3DLike, Vector3D
from manim.utils.color import BLUE, BLUE_D, BLUE_E, LIGHT_GREY, WHITE, interpolate_color
__all__ = [
"ThreeDVMobject",
"Surface",
@ -19,11 +16,10 @@ __all__ = [
"Torus",
]
from collections.abc import Iterable, Sequence
from typing import Any, Callable
from collections.abc import Callable, Iterable, Sequence
from typing import TYPE_CHECKING, Any, Literal, Self
import numpy as np
from typing_extensions import Self
from manim import config, logger
from manim.constants import *
@ -34,15 +30,31 @@ from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
from manim.mobject.types.vectorized_mobject import VectorizedPoint, VGroup, VMobject
from manim.utils.color import (
BLUE,
BLUE_D,
BLUE_E,
LIGHT_GREY,
WHITE,
ManimColor,
ParsableManimColor,
interpolate_color,
)
from manim.utils.iterables import tuplify
from manim.utils.space_ops import normalize, perpendicular_bisector, z_to_vector
if TYPE_CHECKING:
from manim.mobject.graphing.coordinate_systems import ThreeDAxes
from manim.typing import Point3D, Point3DLike, Vector3D, Vector3DLike
class ThreeDVMobject(VMobject, metaclass=ConvertToOpenGL):
def __init__(self, shade_in_3d: bool = True, **kwargs):
u_index: int
v_index: int
u1: float
u2: float
v1: float
v2: float
def __init__(self, shade_in_3d: bool = True, **kwargs: Any):
super().__init__(shade_in_3d=shade_in_3d, **kwargs)
@ -101,13 +113,16 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
def __init__(
self,
func: Callable[[float, float], np.ndarray],
u_range: Sequence[float] = [0, 1],
v_range: Sequence[float] = [0, 1],
resolution: Sequence[int] = 32,
u_range: tuple[float, float] = (0, 1),
v_range: tuple[float, float] = (0, 1),
resolution: int | Sequence[int] = 32,
surface_piece_config: dict = {},
fill_color: ParsableManimColor = BLUE_D,
fill_opacity: float = 1.0,
checkerboard_colors: Sequence[ParsableManimColor] | bool = [BLUE_D, BLUE_E],
checkerboard_colors: Iterable[ParsableManimColor] | Literal[False] = [
BLUE_D,
BLUE_E,
],
stroke_color: ParsableManimColor = LIGHT_GREY,
stroke_width: float = 0.5,
should_make_jagged: bool = False,
@ -116,23 +131,25 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
) -> None:
self.u_range = u_range
self.v_range = v_range
super().__init__(**kwargs)
super().__init__(
fill_color=fill_color,
fill_opacity=fill_opacity,
stroke_color=stroke_color,
stroke_width=stroke_width,
**kwargs,
)
self.resolution = resolution
self.surface_piece_config = surface_piece_config
self.fill_color: ManimColor = ManimColor(fill_color)
self.fill_opacity = fill_opacity
if checkerboard_colors:
self.checkerboard_colors: list[ManimColor] = [
ManimColor(x) for x in checkerboard_colors
]
else:
self.checkerboard_colors: list[ManimColor] | Literal[False]
if checkerboard_colors is False:
self.checkerboard_colors = checkerboard_colors
self.stroke_color: ManimColor = ManimColor(stroke_color)
self.stroke_width = stroke_width
else:
self.checkerboard_colors = [ManimColor(i) for i in checkerboard_colors]
self.should_make_jagged = should_make_jagged
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]))
@ -143,11 +160,10 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
return self._func(u, v)
def _get_u_values_and_v_values(self) -> tuple[np.ndarray, np.ndarray]:
res = tuplify(self.resolution)
if len(res) == 1:
u_res = v_res = res[0]
if isinstance(self.resolution, int):
u_res = v_res = self.resolution
else:
u_res, v_res = res
u_res, v_res = self.resolution
u_values = np.linspace(*self.u_range, u_res + 1)
v_values = np.linspace(*self.v_range, v_res + 1)
@ -157,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]
@ -178,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,
@ -189,7 +207,7 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
self.set_fill_by_checkerboard(*self.checkerboard_colors)
def set_fill_by_checkerboard(
self, *colors: Iterable[ParsableManimColor], opacity: float | None = None
self, *colors: ParsableManimColor, opacity: float | None = None
) -> Self:
"""Sets the fill_color of each face of :class:`Surface` in
an alternating pattern.
@ -208,17 +226,19 @@ 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
def set_fill_by_value(
self,
axes: Mobject,
colorscale: list[ParsableManimColor] | ParsableManimColor | None = None,
axes: ThreeDAxes,
colorscale: Iterable[ParsableManimColor]
| Iterable[tuple[ParsableManimColor, float]]
| None = None,
axis: int = 2,
**kwargs,
**kwargs: Any,
) -> Self:
"""Sets the color of each mobject of a parametric surface to a color
relative to its axis-value.
@ -278,19 +298,23 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
"the surface fill color has not been changed"
)
return self
colorscale_list = list(colorscale)
ranges = [axes.x_range, axes.y_range, axes.z_range]
if type(colorscale[0]) is tuple:
assert isinstance(colorscale_list, list)
new_colors: list[ManimColor]
if type(colorscale_list[0]) is tuple and len(colorscale_list[0]) == 2:
new_colors, pivots = [
[i for i, j in colorscale],
[j for i, j in colorscale],
[ManimColor(i) for i, j in colorscale_list],
[j for i, j in colorscale_list],
]
else:
new_colors = colorscale
new_colors = [ManimColor(i) for i in colorscale_list]
current_range = ranges[axis]
pivot_min = ranges[axis][0]
pivot_max = ranges[axis][1]
assert current_range is not None
pivot_min = current_range[0]
pivot_max = current_range[1]
pivot_frequency = (pivot_max - pivot_min) / (len(new_colors) - 1)
pivots = np.arange(
start=pivot_min,
@ -317,6 +341,7 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
color_index,
)
if config.renderer == RendererType.OPENGL:
assert isinstance(mob, OpenGLMobject)
mob.set_color(mob_color, recurse=False)
elif config.renderer == RendererType.CAIRO:
mob.set_color(mob_color, family=False)
@ -354,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))
@ -369,16 +388,67 @@ 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__(
self,
center: Point3DLike = ORIGIN,
radius: float = 1,
resolution: Sequence[int] | None = None,
u_range: Sequence[float] = (0, TAU),
v_range: Sequence[float] = (0, PI),
**kwargs,
resolution: int | Sequence[int] | None = None,
u_range: tuple[float, float] = (0, TAU),
v_range: tuple[float, float] = (0, PI),
**kwargs: Any,
) -> None:
if config.renderer == RendererType.OPENGL:
res_value = (101, 51)
@ -401,12 +471,12 @@ class Sphere(Surface):
self.shift(center)
def func(self, u: float, v: float) -> np.ndarray:
def func(self, u: float, v: float) -> Point3D:
"""The z values defining the :class:`Sphere` being plotted.
Returns
-------
:class:`numpy.array`
:class:`Point3D`
The z values defining the :class:`Sphere`.
"""
return self.radius * np.array(
@ -448,11 +518,11 @@ class Dot3D(Sphere):
def __init__(
self,
point: list | np.ndarray = ORIGIN,
point: Point3D = ORIGIN,
radius: float = DEFAULT_DOT_RADIUS,
color: ParsableManimColor = WHITE,
resolution: tuple[int, int] = (8, 8),
**kwargs,
resolution: int | tuple[int, int] | None = (8, 8),
**kwargs: Any,
) -> None:
super().__init__(center=point, radius=radius, resolution=resolution, **kwargs)
self.set_color(color)
@ -494,7 +564,7 @@ class Cube(VGroup):
fill_opacity: float = 0.75,
fill_color: ParsableManimColor = BLUE,
stroke_width: float = 0,
**kwargs,
**kwargs: Any,
) -> None:
self.side_length = side_length
super().__init__(
@ -510,6 +580,7 @@ class Cube(VGroup):
face = Square(
side_length=self.side_length,
shade_in_3d=True,
joint_type=LineJointType.BEVEL,
)
face.flip()
face.shift(self.side_length * OUT / 2.0)
@ -517,7 +588,8 @@ class Cube(VGroup):
self.add(face)
init_points = generate_points
def init_points(self) -> None:
self.generate_points()
class Prism(Cube):
@ -544,7 +616,9 @@ class Prism(Cube):
"""
def __init__(
self, dimensions: tuple[float, float, float] | np.ndarray = [3, 2, 1], **kwargs
self,
dimensions: Vector3DLike = [3, 2, 1],
**kwargs: Any,
) -> None:
self.dimensions = dimensions
super().__init__(**kwargs)
@ -598,20 +672,20 @@ class Cone(Surface):
self,
base_radius: float = 1,
height: float = 1,
direction: np.ndarray = Z_AXIS,
direction: Vector3DLike = Z_AXIS,
show_base: bool = False,
v_range: Sequence[float] = [0, TAU],
v_range: tuple[float, float] = (0, TAU),
u_min: float = 0,
checkerboard_colors: bool = False,
checkerboard_colors: Iterable[ParsableManimColor] | Literal[False] = False,
**kwargs: Any,
) -> None:
self.direction = direction
self.direction = np.array(direction)
self.theta = PI - np.arctan(base_radius / height)
super().__init__(
self.func,
v_range=v_range,
u_range=[u_min, np.sqrt(base_radius**2 + height**2)],
u_range=(u_min, np.sqrt(base_radius**2 + height**2)),
checkerboard_colors=checkerboard_colors,
**kwargs,
)
@ -632,7 +706,7 @@ class Cone(Surface):
self._rotate_to_direction()
def func(self, u: float, v: float) -> np.ndarray:
def func(self, u: float, v: float) -> Point3D:
"""Converts from spherical coordinates to cartesian.
Parameters
@ -657,10 +731,10 @@ class Cone(Surface):
],
)
def get_start(self) -> np.ndarray:
def get_start(self) -> Point3D:
return self.start_point.get_center()
def get_end(self) -> np.ndarray:
def get_end(self) -> Point3D:
return self.end_point.get_center()
def _rotate_to_direction(self) -> None:
@ -693,7 +767,7 @@ class Cone(Surface):
self._current_theta = theta
self._current_phi = phi
def set_direction(self, direction: np.ndarray) -> None:
def set_direction(self, direction: Vector3DLike) -> None:
"""Changes the direction of the apex of the :class:`Cone`.
Parameters
@ -701,10 +775,10 @@ class Cone(Surface):
direction
The direction of the apex.
"""
self.direction = direction
self.direction = np.array(direction)
self._rotate_to_direction()
def get_direction(self) -> np.ndarray:
def get_direction(self) -> Vector3D:
"""Returns the current direction of the apex of the :class:`Cone`.
Returns
@ -714,7 +788,7 @@ class Cone(Surface):
"""
return self.direction
def _set_start_and_end_attributes(self, direction):
def _set_start_and_end_attributes(self, direction: Vector3D) -> None:
normalized_direction = direction * np.linalg.norm(direction)
start = self.base_circle.get_center()
@ -760,18 +834,18 @@ class Cylinder(Surface):
self,
radius: float = 1,
height: float = 2,
direction: np.ndarray = Z_AXIS,
v_range: Sequence[float] = [0, TAU],
direction: Vector3DLike = Z_AXIS,
v_range: tuple[float, float] = (0, TAU),
show_ends: bool = True,
resolution: Sequence[int] = (24, 24),
**kwargs,
resolution: int | tuple[int, int] = (24, 24),
**kwargs: Any,
) -> None:
self._height = height
self.radius = radius
super().__init__(
self.func,
resolution=resolution,
u_range=[-self._height / 2, self._height / 2],
u_range=(-self._height / 2, self._height / 2),
v_range=v_range,
**kwargs,
)
@ -803,7 +877,9 @@ class Cylinder(Surface):
def add_bases(self) -> None:
"""Adds the end caps of the cylinder."""
opacity: float
if config.renderer == RendererType.OPENGL:
assert isinstance(self, OpenGLMobject)
color = self.color
opacity = self.opacity
elif config.renderer == RendererType.CAIRO:
@ -858,7 +934,7 @@ class Cylinder(Surface):
self._current_theta = theta
self._current_phi = phi
def set_direction(self, direction: np.ndarray) -> None:
def set_direction(self, direction: Vector3DLike) -> None:
"""Sets the direction of the central axis of the :class:`Cylinder`.
Parameters
@ -917,21 +993,27 @@ class Line3D(Cylinder):
def __init__(
self,
start: np.ndarray = LEFT,
end: np.ndarray = RIGHT,
start: Point3DLike = LEFT,
end: Point3DLike = RIGHT,
thickness: float = 0.02,
color: ParsableManimColor | None = None,
resolution: int | Sequence[int] = 24,
**kwargs,
resolution: int | tuple[int, int] = 24,
**kwargs: Any,
):
self.thickness = thickness
self.resolution = (2, resolution) if isinstance(resolution, int) else resolution
self.resolution: tuple[int, int] = (
(2, resolution) if isinstance(resolution, int) else resolution
)
start = np.array(start, dtype=np.float64)
end = np.array(end, dtype=np.float64)
self.set_start_and_end_attrs(start, end, **kwargs)
if color is not None:
self.set_color(color)
def set_start_and_end_attrs(
self, start: np.ndarray, end: np.ndarray, **kwargs
self, start: Point3DLike, end: Point3DLike, **kwargs: Any
) -> None:
"""Sets the start and end points of the line.
@ -949,7 +1031,7 @@ class Line3D(Cylinder):
rough_end = self.pointify(end)
self.vect = rough_end - rough_start
self.length = np.linalg.norm(self.vect)
self.direction = normalize(self.vect)
self.direction: Vector3D = normalize(self.vect)
# Now that we know the direction between them,
# we can the appropriate boundary point from
# start and end, if they're mobjects
@ -967,8 +1049,8 @@ class Line3D(Cylinder):
def pointify(
self,
mob_or_point: Mobject | Point3DLike,
direction: Vector3D = None,
) -> np.ndarray:
direction: Vector3DLike | None = None,
) -> Point3D:
"""Gets a point representing the center of the :class:`Mobjects <.Mobject>`.
Parameters
@ -991,7 +1073,7 @@ class Line3D(Cylinder):
return mob.get_boundary_point(direction)
return np.array(mob_or_point)
def get_start(self) -> np.ndarray:
def get_start(self) -> Point3D:
"""Returns the starting point of the :class:`Line3D`.
Returns
@ -1001,7 +1083,7 @@ class Line3D(Cylinder):
"""
return self.start
def get_end(self) -> np.ndarray:
def get_end(self) -> Point3D:
"""Returns the ending point of the :class:`Line3D`.
Returns
@ -1015,9 +1097,9 @@ class Line3D(Cylinder):
def parallel_to(
cls,
line: Line3D,
point: Vector3D = ORIGIN,
point: Point3DLike = ORIGIN,
length: float = 5,
**kwargs,
**kwargs: Any,
) -> Line3D:
"""Returns a line parallel to another line going through
a given point.
@ -1051,11 +1133,11 @@ class Line3D(Cylinder):
line2 = Line3D.parallel_to(line1, color=YELLOW)
self.add(ax, line1, line2)
"""
point = np.array(point)
np_point = np.asarray(point)
vect = normalize(line.vect)
return cls(
point + vect * length / 2,
point - vect * length / 2,
np_point + vect * length / 2,
np_point - vect * length / 2,
**kwargs,
)
@ -1063,9 +1145,9 @@ class Line3D(Cylinder):
def perpendicular_to(
cls,
line: Line3D,
point: Vector3D = ORIGIN,
point: Point3DLike = ORIGIN,
length: float = 5,
**kwargs,
**kwargs: Any,
) -> Line3D:
"""Returns a line perpendicular to another line going through
a given point.
@ -1099,17 +1181,17 @@ class Line3D(Cylinder):
line2 = Line3D.perpendicular_to(line1, color=BLUE)
self.add(ax, line1, line2)
"""
point = np.array(point)
np_point = np.asarray(point)
norm = np.cross(line.vect, point - line.start)
norm = np.cross(line.vect, np_point - line.start)
if all(np.linalg.norm(norm) == np.zeros(3)):
raise ValueError("Could not find the perpendicular.")
start, end = perpendicular_bisector([line.start, line.end], norm)
vect = normalize(end - start)
return cls(
point + vect * length / 2,
point - vect * length / 2,
np_point + vect * length / 2,
np_point - vect * length / 2,
**kwargs,
)
@ -1153,14 +1235,14 @@ class Arrow3D(Line3D):
def __init__(
self,
start: np.ndarray = LEFT,
end: np.ndarray = RIGHT,
start: Point3DLike = LEFT,
end: Point3DLike = RIGHT,
thickness: float = 0.02,
height: float = 0.3,
base_radius: float = 0.08,
color: ParsableManimColor = WHITE,
resolution: int | Sequence[int] = 24,
**kwargs,
resolution: int | tuple[int, int] = 24,
**kwargs: Any,
) -> None:
super().__init__(
start=start,
@ -1183,8 +1265,9 @@ class Arrow3D(Line3D):
height=height,
**kwargs,
)
self.cone.shift(end)
self.end_point = VectorizedPoint(end)
np_end = np.asarray(end, dtype=np.float64)
self.cone.shift(np_end)
self.end_point = VectorizedPoint(np_end)
self.add(self.end_point, self.cone)
self.set_color(color)
@ -1226,10 +1309,10 @@ class Torus(Surface):
self,
major_radius: float = 3,
minor_radius: float = 1,
u_range: Sequence[float] = (0, TAU),
v_range: Sequence[float] = (0, TAU),
resolution: tuple[int, int] | None = None,
**kwargs,
u_range: tuple[float, float] = (0, TAU),
v_range: tuple[float, float] = (0, TAU),
resolution: int | tuple[int, int] | None = None,
**kwargs: Any,
) -> None:
if config.renderer == RendererType.OPENGL:
res_value = (101, 101)
@ -1248,7 +1331,7 @@ class Torus(Surface):
**kwargs,
)
def func(self, u: float, v: float) -> np.ndarray:
def func(self, u: float, v: float) -> Point3D:
"""The z values defining the :class:`Torus` being plotted.
Returns

Some files were not shown because too many files have changed in this diff Show more