mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-22 10:01:47 +00:00
Compare commits
245 commits
adventure-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e83f4b09a |
||
|
|
f7fd708276 |
||
|
|
4c4622df54 |
||
|
|
66d5a4937a |
||
|
|
c07137dbed |
||
|
|
71ab85f960 |
||
|
|
16f0a3de3e |
||
|
|
537a134360 |
||
|
|
b7bf2ea90f |
||
|
|
bdaf4497b7 |
||
|
|
037b376ec2 |
||
|
|
e9639c2697 |
||
|
|
1b2d5ce72b |
||
|
|
c94a7ea9fc |
||
|
|
bb1be6ef8a |
||
|
|
516c8c8ba7 |
||
|
|
ccee37a614 |
||
|
|
31db147222 |
||
|
|
34124c3f60 |
||
|
|
d999d422c9 |
||
|
|
561de9d72a |
||
|
|
05b3042ab0 |
||
|
|
2ece488b2c |
||
|
|
852ebd1c60 |
||
|
|
82522795f1 |
||
|
|
12c5640a32 |
||
|
|
33424fe43d |
||
|
|
1b3390073c |
||
|
|
56f7eb2a1f |
||
|
|
cfb5c684b7 |
||
|
|
429f25328d |
||
|
|
c45724989d |
||
|
|
af70b6fef2 |
||
|
|
4b32312dd1 |
||
|
|
90141df105 |
||
|
|
82f93b6c3c |
||
|
|
752b46a003 |
||
|
|
21cf9998cc |
||
|
|
ebb230f6f1 |
||
|
|
46177d247e |
||
|
|
468929889b |
||
|
|
98c458b6b2 |
||
|
|
d4af5b2baa |
||
|
|
1157b746c3 |
||
|
|
6f825e8513 |
||
|
|
a0414dccec |
||
|
|
33a0e56d73 |
||
|
|
80fd11efbc |
||
|
|
498f0b9c89 |
||
|
|
87cd63549c |
||
|
|
cd370610c5 |
||
|
|
a6af7f3d76 |
||
|
|
e34e707858 |
||
|
|
7c1c9258d0 |
||
|
|
000e7792bd |
||
|
|
704830ff5f | ||
|
|
d1eea48aa6 |
||
|
|
9504757918 |
||
|
|
7eab4c5450 |
||
|
|
fda336978b |
||
|
|
ab7cfc54d8 |
||
|
|
601a007192 |
||
|
|
ab17eb58a3 |
||
|
|
a5d4ab134c |
||
|
|
f9b12e5d12 |
||
|
|
ae501baf19 |
||
|
|
761bc46cc8 |
||
|
|
357bb3fbba |
||
|
|
6e9ec60b6f |
||
|
|
c8db165825 |
||
|
|
b4049bd6c7 |
||
|
|
8a5267a9ee |
||
|
|
6adc6e4cf6 |
||
|
|
21e7e0d9c1 |
||
|
|
7efa45492f |
||
|
|
c261c61dfd |
||
|
|
9d98f00ee9 |
||
|
|
587e8d6546 |
||
|
|
d49115316e |
||
|
|
2f38426ceb |
||
|
|
bbdcda1ff5 |
||
|
|
2c0b49be9f |
||
|
|
eb8194a640 |
||
|
|
eca7f17853 |
||
|
|
2317b0ee8b |
||
|
|
d938533742 |
||
|
|
0b3f28a5af |
||
|
|
6a56cc5ce6 |
||
|
|
cd7cd1b219 |
||
|
|
9a6550d356 |
||
|
|
06f2fe231a |
||
|
|
d64af99f89 |
||
|
|
2e0aa8f529 |
||
|
|
88718ce43f |
||
|
|
73eeacb880 |
||
|
|
739c2c3f14 |
||
|
|
d75f84a985 |
||
|
|
b59f311331 |
||
|
|
dc4a8bb741 |
||
|
|
4bc77b3a00 |
||
|
|
c424f83cb4 |
||
|
|
0d2533231b |
||
|
|
b24b61776b |
||
|
|
d308ae3372 |
||
|
|
ff86ee6333 | ||
|
|
ef0cf2a34d | ||
|
|
75285d1f01 | ||
|
|
d60154d024 |
||
|
|
976d634656 |
||
|
|
765f02f3ed |
||
|
|
a50d8363e7 |
||
|
|
de090c1bd0 |
||
|
|
e85bfca015 |
||
|
|
0541f9e6f1 | ||
|
|
c78de89e64 | ||
|
|
ab045f0ae1 | ||
|
|
52820c6b65 |
||
|
|
51aa01acea |
||
|
|
fcb9f6fee3 |
||
|
|
adb0915605 |
||
|
|
b6a464ed64 |
||
|
|
3bf384ab1e |
||
|
|
a1bca6c3b9 |
||
|
|
51377b530c |
||
|
|
3c6ea0e2d6 |
||
|
|
00d0b297b4 |
||
|
|
83bcafb0e7 |
||
|
|
7e950b38fa |
||
|
|
9528337ad4 |
||
|
|
afa4692f7b |
||
|
|
038d16b531 |
||
|
|
3e8f41c9be |
||
|
|
4dd2937f1e |
||
|
|
a810f5535c |
||
|
|
dd04fce52a |
||
|
|
e7d69834b3 |
||
|
|
7ffdf04dcb |
||
|
|
e489ebf220 |
||
|
|
ce2c3c43e4 |
||
|
|
5ccc5bac19 |
||
|
|
e6777d6334 |
||
|
|
855ea863b6 |
||
|
|
8535d0675b |
||
|
|
3ea23e9cd0 |
||
|
|
7e2ea86e98 |
||
|
|
6b668784f2 |
||
|
|
dd91a8a2ee |
||
|
|
ec306075a9 |
||
|
|
2d3aa0d2f4 |
||
|
|
a56c06cd51 |
||
|
|
03a94141d2 |
||
|
|
5a2b3384f1 |
||
|
|
c4dc0ea86d |
||
|
|
e822de0315 |
||
|
|
f69f2545d9 |
||
|
|
48d5a36aad |
||
|
|
05cc414014 |
||
|
|
b3df1cf79e |
||
|
|
cb8af6f2d0 |
||
|
|
d8a7e55ee1 |
||
|
|
2d8d81c209 |
||
|
|
fc68c10433 |
||
|
|
d0521fa06d |
||
|
|
e339a68e54 |
||
|
|
c887b51d63 |
||
|
|
ded54e41ae |
||
|
|
04503adb70 |
||
|
|
9c9ebf428e |
||
|
|
d18dc8f89b |
||
|
|
7eb88562c2 |
||
|
|
aa0cd4fe82 |
||
|
|
a9b65eed5f |
||
|
|
dba6fa8f99 |
||
|
|
df36f4f196 |
||
|
|
7ea765a759 |
||
|
|
21fe73b5c2 |
||
|
|
088affd876 |
||
|
|
c7ff795c0a |
||
|
|
dea245ad9a |
||
|
|
a8c16fbfb1 |
||
|
|
83d4301184 |
||
|
|
b65b0f2470 |
||
|
|
4ddf77739d |
||
|
|
e73b6590dd |
||
|
|
fd8ab62613 |
||
|
|
047db453ff |
||
|
|
2f02eb9328 |
||
|
|
c1663f1a1e |
||
|
|
2790a70bc5 |
||
|
|
fdb5cb9f04 |
||
|
|
ec501b960c |
||
|
|
e54113e263 |
||
|
|
b83410aee0 |
||
|
|
f124235a9b |
||
|
|
9e74ee7edf |
||
|
|
3721fb1612 |
||
|
|
3d029c1280 |
||
|
|
f63ba71f04 |
||
|
|
a234ff31e8 |
||
|
|
9c43ad0d21 |
||
|
|
ea16d22735 |
||
|
|
c4b7a80258 |
||
|
|
0cec80216b |
||
|
|
a1bb04947a |
||
|
|
325fa08a72 |
||
|
|
a8458cb329 |
||
|
|
64862994ac |
||
|
|
c9707f281d |
||
|
|
23a2df1e5a |
||
|
|
3377f6c1f6 |
||
|
|
e4b1d105a3 |
||
|
|
204e44a106 |
||
|
|
d517b50567 |
||
|
|
5301ab874f |
||
|
|
be6a9dfaa7 |
||
|
|
e569fd47ad |
||
|
|
bf72127152 |
||
|
|
ffa6ffa548 |
||
|
|
bcab73a3d2 |
||
|
|
fdc496c211 |
||
|
|
f6cdb547d2 |
||
|
|
5a3dcf77fa |
||
|
|
c53ac5544a |
||
|
|
7f6743e97c |
||
|
|
a2876dc77c |
||
|
|
de210bad8b |
||
|
|
ab4855728f |
||
|
|
f304bd93ea |
||
|
|
384350cf3b |
||
|
|
593010b87b |
||
|
|
3c6a1ee80b |
||
|
|
44da20d71d |
||
|
|
e1925e46dc |
||
|
|
2ab99b53d3 |
||
|
|
2bdd5ac230 |
||
|
|
9479febef2 |
||
|
|
d85ddb0ca5 |
||
|
|
cd2a3b92f6 |
||
|
|
01fc1ef71f |
||
|
|
d521bc9104 |
||
|
|
95fba7da10 |
||
|
|
a6990bd83c |
||
|
|
8f919b1726 |
||
|
|
6ca08fd187 |
||
|
|
106b849a6d |
238 changed files with 16204 additions and 11299 deletions
|
|
@ -1,4 +1,4 @@
|
|||
[codespell]
|
||||
check-hidden = True
|
||||
skip = .git,*.js,*.js.map,*.css,*.css.map,*.html,*.po,*.pot,poetry.lock,*.log,*.svg
|
||||
skip = .git,*.js,*.js.map,*.css,*.css.map,*.html,*.po,*.pot,uv.lock,*.log,*.svg
|
||||
ignore-words = .codespell_ignorewords
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -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
6
.github/codeql.yml
vendored
|
|
@ -9,6 +9,12 @@ query-filters:
|
|||
id: py/multiple-calls-to-init
|
||||
- exclude:
|
||||
id: py/missing-call-to-init
|
||||
- exclude:
|
||||
id: py/method-first-arg-is-not-self
|
||||
- exclude:
|
||||
id: py/cyclic-import
|
||||
- exclude:
|
||||
id: py/unsafe-cyclic-import
|
||||
paths:
|
||||
- manim
|
||||
paths-ignore:
|
||||
|
|
|
|||
67
.github/release.yml
vendored
Normal file
67
.github/release.yml
vendored
Normal 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:
|
||||
- "*"
|
||||
40
.github/scripts/ci_build_cairo.py
vendored
40
.github/scripts/ci_build_cairo.py
vendored
|
|
@ -14,8 +14,8 @@ import subprocess
|
|||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import typing
|
||||
import urllib.request
|
||||
from collections.abc import Generator
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from sys import stdout
|
||||
|
|
@ -67,7 +67,7 @@ def run_command(command, cwd=None, env=None):
|
|||
|
||||
|
||||
@contextmanager
|
||||
def gha_group(title: str) -> typing.Generator:
|
||||
def gha_group(title: str) -> Generator:
|
||||
if not is_ci():
|
||||
yield
|
||||
return
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
2
.github/workflows/cffconvert.yml
vendored
2
.github/workflows/cffconvert.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out a copy of the repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check whether the citation metadata from CITATION.cff is valid
|
||||
uses: citation-file-format/cffconvert-github-action@2.0.0
|
||||
|
|
|
|||
62
.github/workflows/ci.yml
vendored
62
.github/workflows/ci.yml
vendored
|
|
@ -22,28 +22,25 @@ 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
|
||||
|
||||
- name: Install Poetry
|
||||
run: |
|
||||
pipx install "poetry==1.8.*"
|
||||
poetry config virtualenvs.prefer-active-python true
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Python ${{ matrix.python }}
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
cache: "poetry"
|
||||
|
||||
- name: Setup macOS PATH
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Setup cache variables
|
||||
shell: bash
|
||||
|
|
@ -60,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'
|
||||
|
|
@ -72,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'
|
||||
|
|
@ -88,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:
|
||||
|
|
@ -104,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
|
||||
|
|
@ -119,18 +119,17 @@ jobs:
|
|||
shell: bash
|
||||
run: |
|
||||
echo "/Library/TeX/texbin" >> $GITHUB_PATH
|
||||
echo "$HOME/.poetry/bin" >> $GITHUB_PATH
|
||||
echo "$PWD/macos-cache/TinyTeX/bin/universal-darwin" >> $GITHUB_PATH
|
||||
|
||||
- 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'
|
||||
|
|
@ -138,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
|
||||
|
|
@ -150,21 +149,20 @@ jobs:
|
|||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
$env:Path += ";" + "$($PWD)\ManimCache\LatexWindows\TinyTeX\bin\windows"
|
||||
$env:Path = "$env:USERPROFILE\.poetry\bin;$($env:PATH)"
|
||||
echo "$env:Path" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
|
||||
- name: Install dependencies and manim
|
||||
run: |
|
||||
poetry install --all-extras
|
||||
uv sync --all-extras --locked
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
poetry run python -m pytest
|
||||
uv run python -m pytest
|
||||
|
||||
- name: Run module doctests
|
||||
run: |
|
||||
poetry run python -m pytest -v --cov-append --ignore-glob="*opengl*" --doctest-modules manim
|
||||
uv run python -m pytest -v --cov-append --ignore-glob="*opengl*" --doctest-modules manim
|
||||
|
||||
- name: Run doctests in rst files
|
||||
run: |
|
||||
cd docs && poetry run make doctest O=-tskip-manim
|
||||
cd docs && uv run make doctest O=-tskip-manim
|
||||
|
|
|
|||
8
.github/workflows/codeql.yml
vendored
8
.github/workflows/codeql.yml
vendored
|
|
@ -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 }}"
|
||||
|
|
|
|||
16
.github/workflows/publish-docker.yml
vendored
16
.github/workflows/publish-docker.yml
vendored
|
|
@ -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
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- 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
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- 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
|
||||
|
|
|
|||
69
.github/workflows/python-publish.yml
vendored
69
.github/workflows/python-publish.yml
vendored
|
|
@ -6,65 +6,42 @@ on:
|
|||
|
||||
jobs:
|
||||
release:
|
||||
name: "Publish release"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
environment: release
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.11
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install dependencies
|
||||
run: python -m pip install --upgrade poetry
|
||||
run: sudo apt-get update && sudo apt-get install -y build-essential python3-dev libcairo2-dev libpango1.0-dev
|
||||
|
||||
# TODO: Set PYPI_API_TOKEN to api token from pip in secrets
|
||||
- name: Configure pypi credentials
|
||||
env:
|
||||
PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
||||
run: poetry config http-basic.pypi __token__ "$PYPI_API_TOKEN"
|
||||
- name: Set up Python 3.13
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.13
|
||||
|
||||
- name: Publish release to pypi
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Build and push release to PyPI
|
||||
run: |
|
||||
poetry publish --build
|
||||
poetry build
|
||||
uv build
|
||||
uv publish
|
||||
|
||||
- name: Store artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -9,12 +9,15 @@ jobs:
|
|||
build-and-publish-htmldocs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.11
|
||||
python-version: 3.13
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
|
|
@ -30,18 +33,17 @@ jobs:
|
|||
babel-english ctex doublestroke dvisvgm frcursive fundus-calligra jknapltx \
|
||||
mathastext microtype physics preview ragged2e relsize rsfs setspace standalone \
|
||||
wasy wasysym
|
||||
python -m pip install --upgrade poetry
|
||||
poetry install
|
||||
uv sync --extra typst
|
||||
|
||||
- name: Build and package documentation
|
||||
run: |
|
||||
cd docs/
|
||||
poetry run make html
|
||||
uv run make html
|
||||
cd build/html/
|
||||
tar -czvf ../html-docs.tar.gz *
|
||||
|
||||
- name: Store artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: ${{ github.workspace }}/docs/build/html-docs.tar.gz
|
||||
name: html-docs.tar.gz
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -11,9 +11,17 @@ repos:
|
|||
- id: mixed-line-ending
|
||||
- id: end-of-file-fixer
|
||||
- id: check-toml
|
||||
name: Validate Poetry
|
||||
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.9.1
|
||||
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.14.1
|
||||
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.3.0
|
||||
hooks:
|
||||
- id: codespell
|
||||
files: ^.*\.(py|md|rst)$
|
||||
args: ["-L", "medias,nam"]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
...
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@ Examples of conflicts of interest include:
|
|||
* The reporter or reported person is a maintainer who regularly reviews your contributions
|
||||
* The reporter or reported person is your metamour.
|
||||
* The reporter or reported person is your family member
|
||||
|
||||
Committee members do not need to state why they have a conflict of interest, only that one exists. Other team members should not ask why the person has a conflict of interest.
|
||||
|
||||
Anyone who has a conflict of interest will remove themselves from the discussion of the incident, and recluse themselves from voting on a response to the report.
|
||||
|
|
|
|||
36
README.md
36
README.md
|
|
@ -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 />
|
||||
|
|
@ -22,17 +20,17 @@
|
|||
Manim is an animation engine for explanatory math videos. It's used to create precise animations programmatically, as demonstrated in the videos of [3Blue1Brown](https://www.3blue1brown.com/).
|
||||
|
||||
> [!NOTE]
|
||||
> The community edition of Manim has been forked from 3b1b/manim, a tool originally created and open-sourced by Grant Sanderson, also creator of the 3Blue1Brown educational math videos. While Grant Sanderson’s repository continues to be maintained separately by him, he is not among the maintainers of the community edition. We recommend this version for its continued development, improved features, enhanced documentation, and more active community-driven maintenance. If you would like to study how Grant makes his videos, head over to his repository ([3b1b/manim](https://github.com/3b1b/manim)).
|
||||
> The community edition of Manim (ManimCE) is a version maintained and developed by the community. It was forked from 3b1b/manim, a tool originally created and open-sourced by Grant Sanderson, also creator of the 3Blue1Brown educational math videos. While Grant Sanderson continues to maintain his own repository, we recommend this version for its continued development, improved features, enhanced documentation, and more active community-driven maintenance. If you would like to study how Grant makes his videos, head over to his repository ([3b1b/manim](https://github.com/3b1b/manim)).
|
||||
|
||||
## Table of Contents:
|
||||
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Documentation](#documentation)
|
||||
- [Docker](#docker)
|
||||
- [Help with Manim](#help-with-manim)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Documentation](#documentation)
|
||||
- [Docker](#docker)
|
||||
- [Help with Manim](#help-with-manim)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -90,9 +88,9 @@ The `-p` flag in the command above is for previewing, meaning the video file wil
|
|||
|
||||
Some other useful flags include:
|
||||
|
||||
- `-s` to skip to the end and just show the final frame.
|
||||
- `-n <number>` to skip ahead to the `n`'th animation of a scene.
|
||||
- `-f` show the file in the file browser.
|
||||
- `-s` to skip to the end and just show the final frame.
|
||||
- `-n <number>` to skip ahead to the `n`'th animation of a scene.
|
||||
- `-f` show the file in the file browser.
|
||||
|
||||
For a thorough list of command line arguments, visit the [documentation](https://docs.manim.community/en/stable/guides/configuration.html).
|
||||
|
||||
|
|
@ -120,8 +118,8 @@ The contribution guide may become outdated quickly; we highly recommend joining
|
|||
[Discord server](https://www.manim.community/discord/) to discuss any potential
|
||||
contributions and keep up to date with the latest developments.
|
||||
|
||||
Most developers on the project use `poetry` for management. You'll want to have poetry installed and available in your environment.
|
||||
Learn more about `poetry` at its [documentation](https://python-poetry.org/docs/) and find out how to install manim with poetry at the [manim dev-installation guide](https://docs.manim.community/en/stable/contributing/development.html) in the manim documentation.
|
||||
Most developers on the project use `uv` for management. You'll want to have uv installed and available in your environment.
|
||||
Learn more about `uv` at its [documentation](https://docs.astral.sh/uv/) and find out how to install manim with uv at the [manim dev-installation guide](https://docs.manim.community/en/latest/contributing/development.html) in the manim documentation.
|
||||
|
||||
## How to Cite Manim
|
||||
|
||||
|
|
|
|||
382
agents/typst_selector.md
Normal file
382
agents/typst_selector.md
Normal file
|
|
@ -0,0 +1,382 @@
|
|||
# Design: Sub-Expression Selection for `Typst` / `TypstMath`
|
||||
|
||||
## Problem Statement
|
||||
|
||||
Users need to interact with individual parts of a Typst-rendered expression:
|
||||
color a variable, animate the numerator of a fraction, morph one sub-expression
|
||||
into another, etc. The `MathTex` class solves this with:
|
||||
|
||||
1. **`{{ ... }}` double-brace notation** — splits the TeX string into named
|
||||
submobject groups at compile time.
|
||||
2. **`substrings_to_isolate` / `get_part_by_tex`** — identifies submobjects
|
||||
whose TeX source matches a given string.
|
||||
|
||||
Both mechanisms ultimately rely on injecting `\special{dvisvgm:raw <g id='...'>}`
|
||||
markers into the LaTeX source so that the resulting SVG contains `<g>` elements
|
||||
with known `id` attributes, which SVGMobject's parser maps to `VGroup`
|
||||
sub-trees via `id_to_vgroup_dict`.
|
||||
|
||||
We need an analogous mechanism for Typst.
|
||||
|
||||
## Key Discovery: `data-typst-label` in SVG Output
|
||||
|
||||
Typst's SVG renderer (`typst-svg` crate) already emits a `data-typst-label`
|
||||
attribute on `<g>` elements whenever a `GroupItem` (hard frame) carries a
|
||||
label. The relevant code path:
|
||||
|
||||
```rust
|
||||
// typst-svg/src/lib.rs — render_group()
|
||||
if let Some(label) = group.label {
|
||||
svg.init().attr("data-typst-label", label.resolve());
|
||||
}
|
||||
```
|
||||
|
||||
A **hard frame** is created by the `box` element (and `block`, etc.). Crucially,
|
||||
`box` can be used inline inside math mode, and labels can be attached to it.
|
||||
|
||||
### Proof of Concept
|
||||
|
||||
The following Typst helper wraps content in a labeled `box`:
|
||||
|
||||
```typst
|
||||
#let grp(lbl, body) = [#box(body) #label(lbl)]
|
||||
```
|
||||
|
||||
When used in math:
|
||||
|
||||
```typst
|
||||
$ #grp("numerator", $a + b$) / #grp("denom", $c - d$) = #grp("result", $x$) $
|
||||
```
|
||||
|
||||
The compiled SVG contains:
|
||||
|
||||
```xml
|
||||
<g class="typst-group" ... data-typst-label="numerator">
|
||||
<!-- glyphs for a + b -->
|
||||
</g>
|
||||
<g class="typst-group" ... data-typst-label="denom">
|
||||
<!-- glyphs for c - d -->
|
||||
</g>
|
||||
<g class="typst-group" ... data-typst-label="result">
|
||||
<!-- glyph for x -->
|
||||
</g>
|
||||
```
|
||||
|
||||
**Nesting works.** A `grp` wrapping a fraction that itself contains `grp`-ed
|
||||
sub-parts produces nested `data-typst-label` groups:
|
||||
|
||||
```typst
|
||||
$ #grp("whole-frac", $frac(#grp("numerator", $a + b$), #grp("denom", $c - d$))$) $
|
||||
```
|
||||
|
||||
SVG output:
|
||||
|
||||
```xml
|
||||
<g ... data-typst-label="whole-frac">
|
||||
<g ... data-typst-label="numerator"> ... </g>
|
||||
<g ... data-typst-label="denom"> ... </g>
|
||||
<path class="typst-shape" ... /> <!-- fraction bar -->
|
||||
</g>
|
||||
```
|
||||
|
||||
### SVG Parser Compatibility
|
||||
|
||||
Manim uses `svgelements` to parse SVGs. The library preserves
|
||||
`data-typst-label` in the `values` dictionary of `Group` objects, and it
|
||||
propagates to child elements. Manim's `SVGMobject.get_mobjects_from()` already
|
||||
iterates over groups and builds `id_to_vgroup_dict` keyed by the `id` attribute.
|
||||
Extending this to also key by `data-typst-label` is straightforward.
|
||||
|
||||
## Proposed Interface
|
||||
|
||||
### 1. Explicit Groups via `{{ ... }}` Notation (Compile-Time)
|
||||
|
||||
Mirror the `MathTex` double-brace convention. Users write:
|
||||
|
||||
```python
|
||||
eq = TypstMath("{{ a + b }} / {{ c - d }} = {{ x }}")
|
||||
```
|
||||
|
||||
The pre-processor splits on `{{ ... }}` (reusing the same whitespace-guard
|
||||
rules as `MathTex._split_double_braces`) and wraps each group in a labeled
|
||||
`box` call:
|
||||
|
||||
```typst
|
||||
$ #box[$a + b$] <_grp-0> / #box[$c - d$] <_grp-1> = #box[$x$] <_grp-2> $
|
||||
```
|
||||
|
||||
Each group gets an auto-generated label (`_grp-0`, `_grp-1`, ...).
|
||||
The `data-typst-label` attributes then appear in the SVG, and
|
||||
`SVGMobject.get_mobjects_from()` can map them to `VGroup` entries in
|
||||
`label_to_vgroup_dict` (or reuse `id_to_vgroup_dict`).
|
||||
|
||||
These groups become sub-mobjects of the `TypstMath` instance, accessible by
|
||||
index:
|
||||
|
||||
```python
|
||||
eq[0] # VGroup for "a + b"
|
||||
eq[1] # VGroup for "c - d"
|
||||
eq[2] # VGroup for "x"
|
||||
```
|
||||
|
||||
(Non-group content between groups — like `/` and `=` — also becomes
|
||||
its own submobject, mirroring `MathTex` behavior.)
|
||||
|
||||
**For `Typst` (text mode):** the same `{{ ... }}` notation applies, but the
|
||||
wrapper is `#box[...]` without math delimiters.
|
||||
|
||||
### 2. Named Groups via Labels
|
||||
|
||||
Users can also assign explicit label names for retrieval by name:
|
||||
|
||||
```python
|
||||
eq = Typst(
|
||||
r"$ #box[$a + b$] <numerator> / #box[$c - d$] <denom> $"
|
||||
)
|
||||
eq.select("numerator").set_color(RED)
|
||||
eq.select("denom").set_color(BLUE)
|
||||
```
|
||||
|
||||
Alternatively, an even more ergonomic approach that hides the `box` boilerplate
|
||||
and uses the `{{ ... : label }}` notation:
|
||||
|
||||
```python
|
||||
eq = TypstMath("{{ a + b : numerator }} / {{ c - d : denom }}")
|
||||
eq.select("numerator").set_color(RED)
|
||||
```
|
||||
|
||||
Here the pre-processor recognizes `{{ content : label }}` and emits
|
||||
`#box[$content$] <label>` in the Typst source.
|
||||
|
||||
### 3. The `.select()` Method
|
||||
|
||||
```python
|
||||
def select(self, key: str | int) -> VGroup:
|
||||
"""Select a labeled sub-expression.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
key
|
||||
Either a label name (string) matching a ``data-typst-label``
|
||||
in the SVG, or an integer index into the auto-numbered
|
||||
``{{ ... }}`` groups.
|
||||
|
||||
Returns
|
||||
-------
|
||||
VGroup
|
||||
The sub-mobjects corresponding to the selected group.
|
||||
|
||||
Raises
|
||||
------
|
||||
KeyError
|
||||
If no group with the given label/index exists.
|
||||
"""
|
||||
```
|
||||
|
||||
This returns a `VGroup` containing exactly the submobjects (paths) that
|
||||
were rendered inside the corresponding `<g data-typst-label="...">` in the SVG.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Step 1: Extend `SVGMobject.get_mobjects_from()` to Track Labels
|
||||
|
||||
In `manim/mobject/svg/svg_mobject.py`, the group-walking loop already checks
|
||||
for `id` attributes. Add a parallel check for `data-typst-label`:
|
||||
|
||||
```python
|
||||
try:
|
||||
group_name = str(element.values["id"])
|
||||
except Exception:
|
||||
# Fall back to data-typst-label if available
|
||||
label = element.values.get("data-typst-label")
|
||||
if label:
|
||||
group_name = f"typst-label:{label}"
|
||||
else:
|
||||
group_name = f"numbered_group_{group_id_number}"
|
||||
group_id_number += 1
|
||||
```
|
||||
|
||||
This automatically populates `id_to_vgroup_dict` with label-keyed entries.
|
||||
|
||||
### Step 2: Pre-Processing `{{ ... }}` in Typst Source
|
||||
|
||||
Add a `_split_and_label_groups()` method that:
|
||||
|
||||
1. Scans the input for `{{ ... }}` or `{{ ... : label }}` patterns
|
||||
(using the same whitespace-guard rules as `MathTex._split_double_braces`).
|
||||
2. Replaces each group with `#box[$content$] <label>` (math mode) or
|
||||
`#box[content] <label>` (text mode).
|
||||
3. Records the mapping from label → original source string for later lookup.
|
||||
|
||||
### Step 3: `Typst.select()` / Index Access
|
||||
|
||||
- Store the ordered list of group labels and their source strings.
|
||||
- `select(label_or_index)` looks up the corresponding `VGroup` from
|
||||
`id_to_vgroup_dict` (using the `typst-label:...` key).
|
||||
- `__getitem__(int)` returns the *n*-th group's `VGroup`.
|
||||
|
||||
### Step 4: Compatibility with `TransformMatchingTex` (future)
|
||||
|
||||
`TransformMatchingTex` (and its successor `TransformMatchingShapes`) works by
|
||||
matching submobjects between two `MathTex` instances by their TeX string keys.
|
||||
The same approach extends to `Typst` if each `{{ ... }}` group carries its
|
||||
original source string as metadata. A `TransformMatchingTypst` animation could
|
||||
match groups by label name or by source string equality.
|
||||
|
||||
## Open Design Questions
|
||||
|
||||
### Q1: Context-Aware Wrapping — Math vs. Text Mode
|
||||
|
||||
The `box` + `label` mechanism works identically in math and text mode, but the
|
||||
**wrapping** of group content must match the surrounding context:
|
||||
|
||||
- **In text mode:** `{{ Hello : greeting }}` → `#box[Hello] <greeting>`
|
||||
- **In math mode:** `{{ y^2 : second }}` → `#box[$y^2$] <second>`
|
||||
|
||||
Getting this wrong is not a silent error — it produces visually broken output.
|
||||
Wrapping math content with `#box[y^2]` (no `$...$`) renders `y^2` as literal
|
||||
text in the body font instead of as a math superscript.
|
||||
|
||||
This is a real problem for `Typst()`, where a single source string can mix text
|
||||
and math freely:
|
||||
|
||||
```python
|
||||
Typst("hello world, here is a formula: $x^2 + {{ y^2 : second }} = z^2$")
|
||||
```
|
||||
|
||||
Here `{{ y^2 : second }}` is inside a `$ ... $` block, so it needs the
|
||||
math-mode wrapper, but the pre-processor has no way to know this unless it
|
||||
tracks `$` delimiters.
|
||||
|
||||
### The `#` prefix problem and math calls
|
||||
|
||||
A natural idea is to translate `{{ content }}` into a Typst function call like
|
||||
`grp("lbl", content)`. However, this has a subtle but critical context
|
||||
sensitivity: Typst has two different call conventions depending on context:
|
||||
|
||||
- **Math call** (no `#` prefix): `$ grp("lbl", a^2 + b) $` — arguments are
|
||||
parsed **in math mode**. The content `a^2 + b` is math. ✓
|
||||
- **Code call** (`#` prefix): `$ #grp("lbl", a^2 + b) $` — arguments are
|
||||
parsed **in code mode**. `a^2` is a syntax error in code! ✗
|
||||
|
||||
So in math mode, the function MUST be called without `#` for args to stay in
|
||||
math mode. In text/markup mode, the function MUST be called WITH `#` (that's
|
||||
how you invoke code from markup), and content arguments need `[...]` wrapping:
|
||||
|
||||
```typst
|
||||
// Text context: #grp("lbl", [Hello world])
|
||||
// Math context: grp("lbl", a^2 + b)
|
||||
```
|
||||
|
||||
The function definition is the same either way:
|
||||
```typst
|
||||
#let grp(lbl, body) = [#box(body) #label(lbl)]
|
||||
```
|
||||
|
||||
This means the function call approach has **exactly the same context problem**
|
||||
as the raw `#box` approach: the pre-processor must know whether it's in math or
|
||||
text to emit the right calling convention.
|
||||
|
||||
### Further complication: string literals and content blocks
|
||||
|
||||
Even inside `TypstMath` (where everything is math), the scanner must avoid
|
||||
`{{ }}` matches inside string literals or content blocks:
|
||||
|
||||
```python
|
||||
TypstMath('x^2 + y^2 =_("Hello {{ world }}") z^2')
|
||||
```
|
||||
|
||||
Here `{{ world }}` is inside a `"..."` string literal — it should NOT be
|
||||
processed. Similarly, content blocks `[...]` inside math switch back to text
|
||||
mode.
|
||||
|
||||
### Options
|
||||
|
||||
**A. `TypstMath`: math calls with simple string-aware scanning.**
|
||||
For `TypstMath`, the entire body is math, so `{{ content }}` always becomes
|
||||
`grp("_grp-N", content)` (no `#`, no `$...$`). The scanner just needs to
|
||||
skip `"..."` string literals and `[...]` content blocks — no `$` tracking
|
||||
needed. This is clean and robust.
|
||||
|
||||
**B. `Typst`: context-aware scanning (full parser).**
|
||||
For the general `Typst` class, the scanner must additionally track `$...$`
|
||||
math blocks (toggling a mode flag on unescaped `$`) to choose between
|
||||
`grp(...)` (in math) and `#grp("lbl", [...])` (in text). It must also handle
|
||||
string literals and content blocks inside math that switch context back. This
|
||||
is doable but non-trivial — essentially a mini Typst lexer.
|
||||
|
||||
**C. `Typst`: no `{{ }}`, manual groups only.**
|
||||
For the general `Typst` class, don't support `{{ }}` at all. Users write
|
||||
`grp(...)` / `#grp(...)` themselves (with the helper injected into the
|
||||
preamble). `{{ }}` is only available on `TypstMath`. This is simpler and
|
||||
avoids the parsing complexity, at the cost of ergonomics for mixed-mode
|
||||
documents.
|
||||
|
||||
**Recommendation:** Start with A (TypstMath only) and C (manual for Typst).
|
||||
Upgrade to B later if demand warrants it — the function call infrastructure
|
||||
is already in place, it's only the scanner that needs upgrading.
|
||||
|
||||
### Q2: What about "unlabeled" content between groups?
|
||||
|
||||
Like `MathTex`, the pieces of content *between* `{{ ... }}` groups should also
|
||||
become their own submobjects (auto-labeled with sequential indices). For
|
||||
example:
|
||||
|
||||
```python
|
||||
TypstMath("{{ a }} + {{ b }} = {{ c }}")
|
||||
# group-0: "a"
|
||||
# group-1: "+" (auto-group for inter-group content)
|
||||
# group-2: "b"
|
||||
# group-3: "=" (auto-group for inter-group content)
|
||||
# group-4: "c"
|
||||
```
|
||||
|
||||
Each segment (group or inter-group) gets wrapped in its own labeled `box`.
|
||||
|
||||
### Q3: What happens with `box` and baseline alignment?
|
||||
|
||||
`box` is an inline element in Typst, and when used inside math mode it
|
||||
participates in math layout. Testing confirms that fractions, superscripts, and
|
||||
other constructs render correctly when their children are `box`-wrapped.
|
||||
However, `box` creates a "hard frame" boundary which may subtly affect spacing
|
||||
in edge cases (e.g., math operator spacing around a boxed expression). This
|
||||
needs further testing; if issues arise, we could explore `block(breakable: false)`
|
||||
or invisible `rect` wrappers as alternatives.
|
||||
|
||||
### Q4: Can we avoid the `#grp(...)` / `#box[...] <label>` verbosity?
|
||||
|
||||
Yes — the `{{ ... }}` double-brace notation is purely syntactic sugar that gets
|
||||
pre-processed by Manim before the source reaches the Typst compiler. Users never
|
||||
need to write raw `#box` or `#label()` calls unless they want finer control.
|
||||
|
||||
### Q5: String-based selection without explicit groups?
|
||||
|
||||
A future enhancement could support:
|
||||
|
||||
```python
|
||||
eq = TypstMath(r"a + b = c")
|
||||
eq.select("a") # finds submobjects corresponding to the glyph "a"
|
||||
```
|
||||
|
||||
This is hard to do reliably because:
|
||||
- Typst SVGs embed glyphs as `<use xlink:href="#gXXX">` references; there's no
|
||||
text content in the SVG itself.
|
||||
- A single variable in Typst may span multiple glyphs (e.g., `"alpha"` → one
|
||||
glyph) or identical glyphs may appear multiple times.
|
||||
|
||||
A possible approach: at pre-processing time, wrap every "token" in the Typst
|
||||
math source in its own labeled `box`. This would require a Typst math tokenizer
|
||||
and is better suited for a v2 implementation.
|
||||
|
||||
## Summary: What Typst Gives Us
|
||||
|
||||
| Mechanism | How it works | SVG output |
|
||||
|---|---|---|
|
||||
| `#box(body) <label>` | Creates a hard-frame `GroupItem` with a `Label` | `<g data-typst-label="label">...</g>` |
|
||||
| `#metadata(val) <label>` | Invisible; queryable via `typst query` CLI | No visual output (useful for CLI queries, not SVG) |
|
||||
| Show rules on labels | `#show <label>: ...` | Transforms visual output but no automatic SVG grouping |
|
||||
| `context query(<label>)` | Document introspection (positions, counters) | In-document only; not available from Python |
|
||||
|
||||
The `box` + `label` mechanism is the **only** one that produces identifiable
|
||||
groups in the SVG output, making it the correct tool for sub-expression
|
||||
selection in Manim.
|
||||
|
|
@ -1,41 +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 \
|
||||
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
|
||||
|
|
@ -48,11 +81,8 @@ RUN adduser --disabled-password \
|
|||
--uid ${NB_UID} \
|
||||
${NB_USER}
|
||||
|
||||
# create working directory for user to mount local directory into
|
||||
WORKDIR ${HOME}
|
||||
USER root
|
||||
RUN chown -R ${NB_USER}:${NB_USER} ${HOME}
|
||||
RUN chmod 777 ${HOME}
|
||||
RUN chown -R ${NB_USER}:${NB_USER} ${HOME} && chmod 777 ${HOME}
|
||||
USER ${NB_USER}
|
||||
|
||||
CMD [ "/bin/bash" ]
|
||||
CMD ["/bin/bash"]
|
||||
|
|
|
|||
|
|
@ -13,3 +13,11 @@ Multi-platform builds are possible by running
|
|||
docker buildx build --push --platform linux/arm64/v8,linux/amd64 --tag manimcommunity/manim:TAG -f docker/Dockerfile .
|
||||
```
|
||||
from the root directory of the repository.
|
||||
|
||||
# Runtime notes
|
||||
- The image is built via a multi-stage Dockerfile (build dependencies are not
|
||||
carried into the runtime stage).
|
||||
- The image does not include the `ffmpeg` CLI binary.
|
||||
- The default TeX installation is minimal and does not include `ctex`.
|
||||
- Headless OpenGL rendering relies on EGL/GL runtime libraries available in the
|
||||
image.
|
||||
|
|
|
|||
|
|
@ -4,5 +4,5 @@ sphinx>=7.3
|
|||
sphinx-copybutton
|
||||
sphinxext-opengraph
|
||||
sphinx-design
|
||||
sphinx-togglebutton
|
||||
sphinx-reredirects
|
||||
typst>=0.14
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
jupyterlab
|
||||
sphinxcontrib-programoutput
|
||||
sphinx-design
|
||||
typst>=0.14
|
||||
|
|
|
|||
0
docs/skip-manim
Normal file
0
docs/skip-manim
Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 160 KiB |
|
|
@ -1,696 +0,0 @@
|
|||
******************************************************
|
||||
An Adventure through Manim's Features and Capabilities
|
||||
******************************************************
|
||||
|
||||
.. image:: ../_static/AdventureManim.png
|
||||
:align: center
|
||||
|
||||
|
||||
**Authors:** `Tristan Schulz <https://github.com/MrDiver>`__ and `Aarush Deshpande <https://github.com/JasonGrace2282>`__
|
||||
|
||||
.. note:: This is a work in progress guide and might not be complete at this point
|
||||
|
||||
##############
|
||||
What to expect
|
||||
##############
|
||||
This guide will take you on a Tour through the features and capabilities of Manim. The goal is to give you a good overview of what Manim can do and how to use it. It is not meant to be a complete reference, but rather a starting point for your own explorations.
|
||||
|
||||
The goal of this guide is to give you a clear path from the basics of Manim to a finished animation. It will not go into detail about the inner workings of Manim, but rather focus on the practical aspects of using it.
|
||||
At the end of this guide you should be able to create your own animations and have a good understanding of how to use Manim.
|
||||
|
||||
.. warning::
|
||||
Please note that this guide is only for Manim and expects basic knowledge about programming with Python. If you are new to Python you should first learn the basics of Python before you start with Manim.
|
||||
You can find a full introduction to Python here: https://docs.python.org/3/tutorial/
|
||||
|
||||
You can still follow this guide with basic knowledge of Python, but you will have to learn some Python basics along the way in order to understand the code examples.
|
||||
|
||||
#################
|
||||
What are Mobjects
|
||||
#################
|
||||
|
||||
Mobjects are the basic building blocks of Manim. They are the objects that are animated and displayed on the screen.
|
||||
Mobjects can be anything from simple shapes to complex 3D objects. They can be animated, moved, rotated, scaled and much more.
|
||||
In this guide we will focus on the 2D Mobjects, but the same principles apply to 3D Mobjects as well.
|
||||
|
||||
.. manim:: MobjectsFloating
|
||||
:hide_source:
|
||||
|
||||
class MobjectsFloating(Scene):
|
||||
def construct(self):
|
||||
c = Circle()
|
||||
s = Square()
|
||||
t = Triangle()
|
||||
c.shift(UP)
|
||||
t.shift(LEFT*3+DOWN)
|
||||
s.shift(RIGHT*3+DOWN)
|
||||
self.add(c, s, t)
|
||||
timer = ValueTracker(0)
|
||||
c.add_updater(lambda m: m.move_to(UP+0.2*DOWN*np.sin(timer.get_value()+1)))
|
||||
s.add_updater(lambda m: m.move_to(RIGHT*3+DOWN+0.3*DOWN*np.sin(timer.get_value()+2)))
|
||||
t.add_updater(lambda m: m.move_to(LEFT*3+DOWN+0.3*DOWN*np.sin(timer.get_value()+4)))
|
||||
self.add(timer)
|
||||
self.play(timer.animate.set_value(2*np.pi), run_time=5, rate_func=linear)
|
||||
|
||||
|
||||
|
||||
For a list of all Mobjects you can look at the :doc:`/reference_index/mobjects` Documentation Page. There are many more to explore and you can even create your own Mobjects, which we will cover later.
|
||||
|
||||
|
||||
.. manim:: PredefinedMobjects
|
||||
:save_last_frame:
|
||||
:hide_source:
|
||||
|
||||
class PredefinedMobjects(Scene):
|
||||
def construct(self):
|
||||
c = Circle()
|
||||
s = Square().set_color(GREEN)
|
||||
t = Triangle()
|
||||
graph = FunctionGraph(lambda x: np.sin(x))
|
||||
axis = Axes()
|
||||
par = ParametricFunction(lambda t: np.array([np.cos(3*t), np.sin(2*t), 0]), [0, 2*np.pi])
|
||||
mat = Matrix([["\\pi", 0], [0, 1]])
|
||||
chart = BarChart(
|
||||
values=[-5, 40, -10, 20, -3],
|
||||
bar_names=["one", "two", "three", "four", "five"],
|
||||
y_range=[-20, 50, 10],
|
||||
y_length=6,
|
||||
x_length=10,
|
||||
x_axis_config={"font_size": 36},
|
||||
).shift(RIGHT/2)
|
||||
func = lambda pos: ((pos[0] * UR + pos[1] * LEFT) - pos) / 3
|
||||
vecfield = ArrowVectorField(func,x_range=[-3,3],y_range=[-3,3])
|
||||
cross = VGroup(
|
||||
Line(UP + LEFT, DOWN + RIGHT),
|
||||
Line(UP + RIGHT, DOWN + LEFT))
|
||||
a = Circle().set_color(RED).scale(0.5)
|
||||
b = cross.set_color(BLUE).scale(0.5)
|
||||
t3 = MobjectTable(
|
||||
[[a.copy(),b.copy(),a.copy()],
|
||||
[b.copy(),a.copy(),a.copy()],
|
||||
[a.copy(),b.copy(),b.copy()]])
|
||||
t3.add(Line(
|
||||
t3.get_corner(DL), t3.get_corner(UR)
|
||||
).set_color(RED))
|
||||
|
||||
group = [c, s, t, graph, axis, par, mat, chart, vecfield, t3]
|
||||
names = ["Circle", "Square", "Triangle", "FunctionGraph", "Axes", "ParametricFunction", "Matrix", "BarChart" ,"ArrowVectorField", "MobjectTable"]
|
||||
zipped = zip(group, names)
|
||||
combined = []
|
||||
for mob, name in zipped:
|
||||
square = Square()
|
||||
name = Text(name).scale(0.5)
|
||||
mob.scale_to_fit_width(square.get_width())
|
||||
square.scale(1.2)
|
||||
name.next_to(square, DOWN)
|
||||
group = VGroup(mob, name, square)
|
||||
combined.append(group)
|
||||
|
||||
all = VGroup(*combined).arrange_in_grid(buff=1,rows=2).scale(0.8).to_edge(UP)
|
||||
dots = MathTex("\\dots").next_to(all, DOWN, buff=1)
|
||||
self.add(all, dots)
|
||||
|
||||
.. note::
|
||||
The type of Mobject that is used most of the time is the `VMobject`. This is a Mobject that is made up of `VectorizedPoints`. These are points that are defined by their coordinates and can be connected by lines or curves.
|
||||
Every time we talk about Mobjects in this guide we mean VMobjects, unless we state otherwise.
|
||||
|
||||
=============================
|
||||
Mobjects and their Attributes
|
||||
=============================
|
||||
|
||||
In order to display Mobjects in your animations you need to add them to the scene. You can do this by calling ``self.add(mobject)`` in the ``construct`` method of your scene.
|
||||
This tells Manim that you want to display the Mobject in your scene.
|
||||
|
||||
.. manim:: CreatingMobjects
|
||||
:save_last_frame:
|
||||
|
||||
class CreatingMobjects(Scene):
|
||||
def construct(self):
|
||||
c = Circle()
|
||||
self.add(c)
|
||||
|
||||
This will be the basic structure of all your animations. You will create Mobjects and add them to the scene. Then you can animate them and change their properties.
|
||||
Try the "Make Interactive" Button and see if you can create a `Square` instead of a `Circle`.
|
||||
|
||||
The first line is the name of your scene, in this case it is ``CreatingMobjects``. It inherits from ``Scene``: as we explore later, you'll find examples where we inherit from
|
||||
class other than ``Scene`` to gain access to more specialized methods. Your animation must take place in the ``construct`` method of your scene, otherwise it will not render.
|
||||
|
||||
You can run this scene on your local machine by saving it in a file called ``my_first_scene.py`` and running ``manim -pqm my_first_scene.py`` in the terminal.
|
||||
|
||||
------------------
|
||||
Mobject Attributes
|
||||
------------------
|
||||
|
||||
Mobjects also posses many attributes that you can change. For example you can change the color of a Mobject by calling ``mobject.set_color(color)`` or scale it by calling ``mobject.scale(factor)``.
|
||||
|
||||
The basic attributes are the ``points``, ``fill_color``, ``fill_opacity``, ``stroke_color``, ``stroke_opacity``, ``stroke_width``.
|
||||
The ``points`` define the outline of the Mobject, whereas the color attributes define how this outline is displayed.
|
||||
|
||||
A full list of the attributes of :class:`VMobject` can be found in the :doc:`../reference/manim.mobject.types.vectorized_mobject.VMobject` Documentation Page. Please note that depending
|
||||
on the type of Mobject you are using, there might be additional attributes, which are listed on the corresponding documentation page.
|
||||
|
||||
-------------------
|
||||
Changing the Points
|
||||
-------------------
|
||||
|
||||
Most of the function that you will use in Manim will be functions that change the points of a Mobject. For example ``mobject.shift(direction)`` will move the Mobject in the given direction.
|
||||
On the other hand, ``mobject.rotate(angle)`` will rotate the Mobject by the given angle.
|
||||
|
||||
.. manim:: MobjectPoints
|
||||
:save_last_frame:
|
||||
|
||||
class MobjectPoints(Scene):
|
||||
def construct(self):
|
||||
c = Circle()
|
||||
s = Square()
|
||||
t = Triangle()
|
||||
|
||||
c.shift(3*LEFT)
|
||||
s.rotate(PI/4)
|
||||
t.shift(3*RIGHT)
|
||||
|
||||
self.add(c, s, t)
|
||||
|
||||
------------------
|
||||
Changing the Color
|
||||
------------------
|
||||
|
||||
Changing the color works in the same way but instead of modifying it you can set it to a new value. For example ``mobject.set_fill(color=color)`` will set the fill color of the Mobject to the given color.
|
||||
|
||||
You can also pass in attributes through the constructor of the Mobject. For example ``Circle(fill_color=RED)`` will create a circle with a red fill color.
|
||||
For a list of parameters that you can pass you can always visit the corresponding Documentation Page in the Reference Manual.
|
||||
|
||||
.. manim:: MobjectColor
|
||||
:save_last_frame:
|
||||
|
||||
class MobjectColor(Scene):
|
||||
def construct(self):
|
||||
c = Circle(fill_color=YELLOW).shift(3*LEFT)
|
||||
s = Square()
|
||||
t = Triangle().shift(3*RIGHT)
|
||||
|
||||
c.set_fill(color=RED).set_opacity(1)
|
||||
s.set_stroke(color=GREEN)
|
||||
t.set_color(color=BLUE).set_opacity(0.5)
|
||||
|
||||
self.add(c, s, t)
|
||||
|
||||
|
||||
-------------------
|
||||
Test your Knowledge
|
||||
-------------------
|
||||
|
||||
Now that you saw the basic ways to change Mobjects, try to reproduce the following Image. You can use the "Make Interactive" Button of the above Scene to get started.
|
||||
|
||||
.. manim:: TestYourKnowledge1
|
||||
:save_last_frame:
|
||||
:hide_source:
|
||||
|
||||
class TestYourKnowledge1(Scene):
|
||||
def construct(self):
|
||||
c = Circle(fill_color=RED,stroke_color=GREEN).shift(3*LEFT)
|
||||
s = Square(fill_color=GREEN,stroke_color=BLUE).set_opacity(0.2)
|
||||
t = Triangle(fill_color=RED,stroke_opacity=0).shift(RIGHT)
|
||||
|
||||
c.set_fill(color=RED).set_opacity(1)
|
||||
s.set_stroke(color=GREEN)
|
||||
t.set_color(color=BLUE).set_opacity(0.5)
|
||||
|
||||
self.add(c, s, t)
|
||||
|
||||
|
||||
###################
|
||||
Animations in Manim
|
||||
###################
|
||||
|
||||
Now that we looked long enough at static Images, let's get to the fun part of Manim. Animations!
|
||||
Animations are at the core of Manim and are what makes it so powerful. You can animate almost anything in Manim and you can do it in many different ways.
|
||||
In this section we will look at the different ways to animate Mobjects and how to control the animations.
|
||||
|
||||
.. manim:: Manimations1
|
||||
:hide_source:
|
||||
|
||||
class Manimations1(Scene):
|
||||
def construct(self):
|
||||
c = Circle().shift(UP).set_color(RED)
|
||||
s = Square().shift(LEFT*3)
|
||||
t = Triangle().shift(RIGHT*3)
|
||||
l = MathTex(r"\mathbf{M}").shift(DOWN).set_fill(opacity=0).set_stroke(color=WHITE, opacity=1, width=5).scale(4)
|
||||
self.play(AnimationGroup(Create(c), GrowFromCenter(s), Write(l), FadeIn(t), lag_ratio=0.2))
|
||||
group = VGroup(l,c, s, t)
|
||||
self.play(group.animate.arrange(RIGHT))
|
||||
self.play(group.animate.arrange(DOWN))
|
||||
self.play(group.animate.arrange_in_grid(buff=1,rows=2))
|
||||
self.play(Unwrite(group))
|
||||
|
||||
|
||||
|
||||
================================
|
||||
Introduction to Basic Animations
|
||||
================================
|
||||
|
||||
There are multiple ways to animate the addition and removal of mobjects from the scene. The most common ways to introduce mobjects is with ``FadeIn`` or ``Create``,
|
||||
and the most common ways to remove objects from the scene are their counterparts: ``FadeOut`` and ``Uncreate``.
|
||||
|
||||
.. manim:: BasicAnimations
|
||||
|
||||
class BasicAnimations(Scene):
|
||||
def construct(self):
|
||||
c1 = Circle().shift(2*LEFT)
|
||||
c2 = Circle().shift(2*RIGHT)
|
||||
self.play(FadeIn(c1), Create(c2))
|
||||
self.play(FadeOut(c1), Uncreate(c2))
|
||||
|
||||
--------
|
||||
Runtimes
|
||||
--------
|
||||
|
||||
You can adjust the duration of each animation individually, or you can set a duration for all in animations in a ``Scene.play`` call.
|
||||
|
||||
.. manim:: AnimationRuntimes
|
||||
|
||||
class AnimationRuntimes(Scene):
|
||||
def construct(self):
|
||||
c = Circle().shift(2*LEFT)
|
||||
s = Square().shift(2*RIGHT)
|
||||
# set animation runtimes individually
|
||||
self.play(Create(c, run_time=2), Create(s, run_time=1))
|
||||
# in this call, the individual runtimes of each animation
|
||||
# are overridden by the runtime in the self.play call
|
||||
self.play(FadeOut(c, run_time=2), FadeOut(s, run_time=1), run_time=1.5)
|
||||
|
||||
--------------
|
||||
Rate Functions
|
||||
--------------
|
||||
A rate function allows you to adjust the speed at an animation proceeds.
|
||||
|
||||
.. manim:: RateFunctionsExample
|
||||
|
||||
class RateFunctionsExample(Scene):
|
||||
def construct(self):
|
||||
c1 = Circle().shift(2*LEFT)
|
||||
c2 = Circle().shift(2*RIGHT)
|
||||
self.play(
|
||||
Create(c1, rate_func=rate_functions.linear),
|
||||
Create(c2, rate_func=rate_functions.ease_in_sine),
|
||||
run_time=5
|
||||
)
|
||||
|
||||
You can see all of the current ones below:
|
||||
|
||||
.. manim:: AllRateFunctions
|
||||
:hide_source:
|
||||
|
||||
class AllRateFunctions(Scene):
|
||||
def construct(self):
|
||||
time_progress = ValueTracker(0)
|
||||
func_grid = VGroup()
|
||||
exclude = ["wraps", "bezier", "sigmoid", "unit_interval", "zero", "not_quite_there", "squish_rate_func"]
|
||||
rate_funcs = list(filter(
|
||||
lambda t: str(t[1])[:10] == "<function " and all(t[0] != s for s in exclude),
|
||||
rate_functions.__dict__.items(),
|
||||
))
|
||||
for name, rate_func in rate_funcs:
|
||||
plot_bg = Rectangle(height=1.5, width=2.0)
|
||||
y_zero = DashedLine(stroke_width=1.5, stroke_color=YELLOW)
|
||||
y_one = DashedLine(stroke_width=0.5, stroke_color=BLUE).shift(0.5*UP)
|
||||
y_minus_one = y_one.copy().shift(DOWN)
|
||||
plot_title = (
|
||||
Text(name, weight=SEMIBOLD, font="Open Sans")
|
||||
.scale(0.4)
|
||||
.next_to(plot_bg, UP, buff=0.1)
|
||||
)
|
||||
func_grid.add(VGroup(plot_bg, y_zero, y_one, y_minus_one, plot_title))
|
||||
|
||||
func_grid.arrange_in_grid(cols=8)
|
||||
func_grid.stretch_to_fit_height(0.9 * config.frame_height)
|
||||
func_grid.stretch_to_fit_width(0.9 * config.frame_width)
|
||||
func_grid.move_to(ORIGIN)
|
||||
|
||||
y_zero, y_one = func_grid.submobjects[0].submobjects[1:3]
|
||||
origin = y_zero.get_start()
|
||||
height = (y_one.get_start() - origin)[1]
|
||||
width = (y_zero.get_end() - origin)[0]
|
||||
|
||||
funcs = []
|
||||
dots = VGroup()
|
||||
for plot_group, (_, rate_func) in zip(func_grid.submobjects, rate_funcs):
|
||||
origin = plot_group.submobjects[1].get_start()
|
||||
func = lambda t, o=origin, rf=rate_func: o + np.array([width*t, height*rf(t), 0])
|
||||
funcs.append(func)
|
||||
plot = (
|
||||
ParametricFunction(
|
||||
func,
|
||||
t_range=[0, 1, 0.01],
|
||||
use_smoothing=False,
|
||||
color=YELLOW,
|
||||
)
|
||||
)
|
||||
plot_group.add(plot)
|
||||
|
||||
dot = Dot().scale(0.5).move_to(func(0))
|
||||
dots.add(dot)
|
||||
|
||||
def dot_updater(dots):
|
||||
t = time_progress.get_value()
|
||||
for dot, func in zip(dots.submobjects, funcs):
|
||||
dot.move_to(func(t))
|
||||
|
||||
self.add(func_grid, dots)
|
||||
dots.add_updater(dot_updater)
|
||||
# there is some wacky rate function giving out-of-bounds results...
|
||||
self.play(
|
||||
time_progress.animate.set_value(1),
|
||||
run_time=3,
|
||||
)
|
||||
|
||||
Alternatively, you can create your own. A rate function takes in a value between 0 and 1 representing the "progress" of the animation. You can think of this as the
|
||||
ratio of the time passed since the animation started, to the runtime of the animation. It should return how much of the animation should have been completed by that time.
|
||||
|
||||
As an example, check out the rate function below.
|
||||
|
||||
.. manim:: CustomRateFunctions
|
||||
|
||||
class CustomRateFunctions(Scene):
|
||||
def construct(self):
|
||||
def there_and_back_three(alpha: float):
|
||||
if alpha <= 1/3:
|
||||
return 3*alpha
|
||||
elif alpha <= 2/3:
|
||||
return 1-3*(alpha-1/3)
|
||||
else:
|
||||
return 3*(alpha-2/3)
|
||||
|
||||
self.play(Create(Circle(), rate_func=there_and_back_three), run_time=4)
|
||||
|
||||
This rate function will spend 1/3rd of the time finishing the animate, the next 1/3rd of the time "undoing" the animation,
|
||||
and the final 1/3rd of the animation replaying the first 1/3rd. As you can see, it returns the amount of the animation
|
||||
completed as a value between 0 to 1.
|
||||
|
||||
----------------------
|
||||
The ``Wait`` Animation
|
||||
----------------------
|
||||
|
||||
Now all these animations seem a bit rushed. Luckily, Manim allows us to create periods of time where nothing is happening.
|
||||
Let's look at an example:
|
||||
|
||||
.. manim:: BasicAnimationWithWait
|
||||
|
||||
class BasicAnimationWithWait(Scene):
|
||||
def construct(self):
|
||||
c = Circle()
|
||||
self.play(Create(c))
|
||||
self.wait() # wait for one second by default
|
||||
self.play(FadeOut(c))
|
||||
self.wait(0.5) # wait half a second
|
||||
|
||||
A little bit later on, we will learn how to leverage the ``stop_condition`` parameter to stop after a certain event happens.
|
||||
|
||||
=====================
|
||||
Transforming Mobjects
|
||||
=====================
|
||||
|
||||
Manim allows us to smoothly transform one :class:`.Mobject` into another using :class:`.Transform` and :class:`.ReplacementTransform`.
|
||||
``Transform(mob1, mob2)`` turns the attributes of ``mob1`` into the attributes of ``mob2``.
|
||||
|
||||
.. manim:: TransformAnimation
|
||||
|
||||
class TransformAnimation(Scene):
|
||||
def construct(self):
|
||||
c = Circle()
|
||||
self.add(c)
|
||||
self.play(Transform(c, Square()))
|
||||
self.play(FadeOut(c)) # fadeout c
|
||||
|
||||
-----------------------------------------
|
||||
``Transform`` vs ``ReplacementTransform``
|
||||
-----------------------------------------
|
||||
|
||||
While :class:`Transform(mob1, mob2)` changes the attributes of ``mob1`` into the attributes of ``mob2`` by copying the values, :class:`ReplacementTransform(mob1, mob2)` instead replaces ``mob1`` in the
|
||||
scene with ``mob2``.
|
||||
|
||||
Here is the same scene in the last section, but using :class:`.ReplacementTransform`:
|
||||
|
||||
.. manim:: ReplacementTransformAnimation
|
||||
|
||||
class ReplacementTransformAnimation(Scene):
|
||||
def construct(self):
|
||||
c = Circle()
|
||||
s = Square()
|
||||
self.add(c)
|
||||
self.play(ReplacementTransform(c, s))
|
||||
self.play(FadeOut(s)) # fadeout s
|
||||
|
||||
Ultimately, the choice of which to use is up to the programmer. However, some examples like the one below make the code simpler when using one over the other.
|
||||
|
||||
.. manim:: CyclingShapesAnimation
|
||||
|
||||
class CyclingShapesAnimation(Scene):
|
||||
def construct(self):
|
||||
mob = Circle()
|
||||
shapes = (Square(), Triangle(), Circle().set_fill(color=RED, opacity=0.5))
|
||||
self.add(mob)
|
||||
for shape in shapes:
|
||||
# if we use transform, we avoid having to
|
||||
# keep track of the previously transformed
|
||||
# shape
|
||||
self.play(Transform(mob, shape))
|
||||
self.wait(0.3)
|
||||
|
||||
|
||||
-------------------
|
||||
``.animate`` Syntax
|
||||
-------------------
|
||||
|
||||
One of the most powerful features of Manim is it's :attr:`~.Mobject.animate` syntax.
|
||||
It allows you to animate the changing of an attribute of a mobject. You can see an example below:
|
||||
|
||||
.. manim:: AnimateSyntaxExample
|
||||
|
||||
class AnimateSyntaxExample(Scene):
|
||||
def construct(self):
|
||||
c = Circle()
|
||||
self.add(c)
|
||||
self.play(c.animate.shift(RIGHT))
|
||||
self.play(c.animate.to_corner(DL).set_fill(color=RED, opacity=0.4))
|
||||
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
``.animate`` works by interpolating between the initial and the final mobject. As such, beware when using ``.animate.rotate`` with angles greater than pi radians
|
||||
as it may not produce the intended animation.
|
||||
|
||||
|
||||
-------------------
|
||||
Test Your Knowledge
|
||||
-------------------
|
||||
|
||||
Try to create the following animation!
|
||||
|
||||
.. manim:: TestBasicAnimationKnowledge
|
||||
:hide_source:
|
||||
|
||||
class TestBasicAnimationKnowledge(Scene):
|
||||
def construct(self):
|
||||
c = Circle().set_fill(color=RED, opacity=0.5)
|
||||
s = Star().set_stroke(color=YELLOW).set_fill(color=YELLOW, opacity=0.3)
|
||||
t = Triangle().set_fill(color=BLUE, opacity=0.1)
|
||||
VGroup(c, s, t).arrange(RIGHT).move_to(ORIGIN) # users will arrange manually
|
||||
self.play(
|
||||
DrawBorderThenFill(c),
|
||||
GrowFromPoint(s, ORIGIN),
|
||||
SpinInFromNothing(t),
|
||||
run_time=2
|
||||
)
|
||||
self.wait()
|
||||
for mob in (s, t):
|
||||
self.play(Transform(c, mob))
|
||||
self.remove(mob)
|
||||
self.wait(0.2)
|
||||
self.play(c.animate.move_to(ORIGIN))
|
||||
|
||||
Hint: you might need to look at different :doc:`/reference_index/animations`!
|
||||
|
||||
=================
|
||||
Grouping Mobjects
|
||||
=================
|
||||
|
||||
Oftentimes it is convenient to animate the movement of several mobjects at once. To help accomplish this goal, manim provides two classes: ``Group`` and ``VGroup``.
|
||||
99% of the time, ``VGroup``'s are used, but if you're dealing with some form of an ``ImageMobject`` you will have to use ``Group``. Here's an example of how groups can be useful:
|
||||
|
||||
.. manim:: GroupingExample
|
||||
|
||||
class GroupingExample(Scene):
|
||||
def construct(self):
|
||||
tri = Triangle()
|
||||
sq = Square()
|
||||
circ = Circle()
|
||||
grp1 = VGroup(tri,sq,circ).arrange(RIGHT)
|
||||
grp2 = VGroup(tri,circ)
|
||||
self.add(tri,sq,circ)
|
||||
self.play(grp1.animate.shift(UP))
|
||||
self.play(grp2.animate.shift(2*DOWN))
|
||||
self.play(tri.animate.next_to(circ,RIGHT))
|
||||
self.play(grp1.animate.shift(UP))
|
||||
self.wait()
|
||||
|
||||
.. note::
|
||||
From now onwards, if we refer to a group we are referring to a ``VGroup``, unless specifically stated otherwise.
|
||||
|
||||
Groups also have a bunch of methods to make your life easier. Take a look at some in the example below:
|
||||
|
||||
.. manim:: GroupingMethodsExample
|
||||
|
||||
class GroupingMethodsExample(Scene):
|
||||
def construct(self):
|
||||
group = VGroup(
|
||||
Square(),
|
||||
Star(color=YELLOW).set_fill(color=YELLOW, opacity=0.5),
|
||||
Triangle(),
|
||||
Circle().set_fill(color=RED, opacity=0.5)
|
||||
)
|
||||
self.play(group.animate.arrange(DOWN), run_time=2)
|
||||
self.play(group.animate.arrange_in_grid(cols=2), run_time=2)
|
||||
for mob in group:
|
||||
self.play(Uncreate(mob))
|
||||
self.wait(0.2)
|
||||
|
||||
##################
|
||||
Syncing Animations
|
||||
##################
|
||||
|
||||
In many animations it makes sense to have things moving together at the same rate.
|
||||
However, Manim gives you better ways to accomplish this task then by copying the same parameters
|
||||
everywhere.
|
||||
|
||||
=========
|
||||
Updaters
|
||||
=========
|
||||
Manim allows you to "update" the attributes of a mobject every frame of an animation
|
||||
via something called updaters. There are two types: normal updaters, and time-based updaters.
|
||||
|
||||
.. note::
|
||||
The way manim works with time based updaters is going to be reworked at some point. Stay
|
||||
up to date with the changelogs to make sure your code will work.
|
||||
|
||||
---------------
|
||||
Normal Updaters
|
||||
---------------
|
||||
You can attach an updater to a mobject via the `.add_updater` method. It takes a function whose
|
||||
first parameter is the mobject itself, and you can modify the mobject however you want.
|
||||
|
||||
For example, here we used ``lambda m: m.next_to(d, RIGHT)``. In this case, ``m`` is the Mobject ``Text("Hi!")``.
|
||||
|
||||
.. manim:: UpdaterExample
|
||||
:ref_classes: MoveAlongPath TracedPath
|
||||
|
||||
class UpdaterExample(Scene):
|
||||
def construct(self):
|
||||
t = Text("Hi!")
|
||||
d = Dot(color=ORANGE)
|
||||
trace = TracedPath(d.get_center, dissipating_time=1, stroke_color=RED)
|
||||
t.add_updater(lambda m: m.next_to(d, RIGHT))
|
||||
self.add(t, trace)
|
||||
self.play(MoveAlongPath(d, Square(), rate_func=linear, run_time=3))
|
||||
self.wait()
|
||||
|
||||
-------------------
|
||||
Time Based Updaters
|
||||
-------------------
|
||||
Time based updaters are just like normal updaters, but take an extra parameter ``dt``.
|
||||
This represents how much time has passed between the last call of your updater.
|
||||
|
||||
.. manim:: TimeBasedUpdater
|
||||
|
||||
class TimeBasedUpdater(Scene):
|
||||
def construct(self):
|
||||
time = 0
|
||||
d = DecimalNumber(0)
|
||||
def updater(m: VMobject, dt: float):
|
||||
# access the time defined outside this function
|
||||
nonlocal time
|
||||
time+=dt
|
||||
m.set_value(time)
|
||||
d.add_updater(updater)
|
||||
self.add(d)
|
||||
self.wait(1.1)
|
||||
|
||||
|
||||
|
||||
=============
|
||||
ValueTrackers
|
||||
=============
|
||||
|
||||
ValueTrackers are the real things that allow you to synchronize multiple animations at once.
|
||||
They are basically just stored values, but you can animate their ``.set_value`` to produce animations.
|
||||
|
||||
.. manim:: ValueTrackerShowcase
|
||||
|
||||
class ValueTrackerShowcase(Scene):
|
||||
def construct(self):
|
||||
line = Rectangle(height=1, width=4).set_stroke(color=WHITE, opacity=1).move_to(ORIGIN)
|
||||
vt = ValueTracker(1e-2) # setting to zero creates bugs with stretch_to_fit_width
|
||||
progress = Rectangle(height=1, width=vt.get_value()).set_stroke(color=RED,opacity=1)
|
||||
progress.add_updater(lambda p: p.stretch_to_fit_width(vt.get_value()).align_to(line, LEFT))
|
||||
d = DecimalNumber(0).to_edge(UP)
|
||||
d.add_updater(lambda d: d.set_value(vt.get_value()))
|
||||
self.add(d,line,progress)
|
||||
self.play(vt.animate.set_value(4), rate_func=linear, run_time=1.5)
|
||||
self.wait(0.1)
|
||||
|
||||
-------------
|
||||
always_redraw
|
||||
-------------
|
||||
``always_redraw`` is a simple function that allows you to recreate a mobject at
|
||||
every frame of the animation. As an example, check out this animation:
|
||||
|
||||
.. manim:: AlwaysRedrawTangentAnimation
|
||||
|
||||
class AlwaysRedrawTangentAnimation(Scene):
|
||||
def construct(self):
|
||||
ax = Axes()
|
||||
sine = ax.plot(np.sin, color=RED)
|
||||
alpha = ValueTracker(0)
|
||||
point = always_redraw(
|
||||
lambda: Dot(
|
||||
sine.point_from_proportion(alpha.get_value()),
|
||||
color=BLUE
|
||||
)
|
||||
)
|
||||
tangent = always_redraw(
|
||||
lambda: TangentLine(
|
||||
sine,
|
||||
alpha=alpha.get_value(),
|
||||
color=YELLOW,
|
||||
length=4
|
||||
)
|
||||
)
|
||||
self.add(ax, sine, point, tangent)
|
||||
self.play(alpha.animate.set_value(1), rate_func=linear, run_time=2)
|
||||
|
||||
-------------------
|
||||
Test Your Knowledge
|
||||
-------------------
|
||||
Try to recreate the following animation!
|
||||
|
||||
.. manim:: KnowledgeCheckUpdaters
|
||||
:hide_source:
|
||||
|
||||
class KnowledgeCheckUpdaters(Scene):
|
||||
def construct(self):
|
||||
l1 = Line(6*LEFT,6*RIGHT)
|
||||
l2 = Line(4*DL,3*UR)
|
||||
vt = ValueTracker(0)
|
||||
d1, d2 = Dot(color=RED), Dot(color=ORANGE)
|
||||
txt = MathTex(r"\Delta", color=RED).add_updater(lambda t: t.next_to(d2, LEFT)).scale(2)
|
||||
bt = TracedPath(d1.get_center, stroke_color=RED)
|
||||
tt = TracedPath(d2.get_center, stroke_color=ORANGE)
|
||||
d1.add_updater(lambda d: d.move_to(l1.point_from_proportion(vt.get_value())))
|
||||
d2.add_updater(lambda d: d.move_to(l2.point_from_proportion(vt.get_value())))
|
||||
self.add(d1, d2, bt, tt, txt)
|
||||
self.play(vt.animate.set_value(1), run_time=1.5)
|
||||
self.play(vt.animate.set_value(0.8))
|
||||
self.play(Create(Line(d1.get_center(), d2.get_center(), color=YELLOW)))
|
||||
vmob = VMobject(color=ORANGE).set_points_as_corners([ORIGIN, d1.get_center(), d2.get_center(), ORIGIN]).set_fill(color=[RED,ORANGE,YELLOW], opacity=1).set_z_index(-50)
|
||||
txt.clear_updaters()
|
||||
self.play(Create(vmob), txt.animate.move_to(vmob.get_center()).set_z_index(50).set_color(BLUE))
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
##############################
|
||||
Exploring Manim's Capabilities
|
||||
##############################
|
||||
|
||||
=============
|
||||
What is this?
|
||||
=============
|
||||
|
||||
One of the hardest parts of learning how to use Manim is to deviate from the quickstart and basic examples given in the documentation,
|
||||
and coding your own animation. By the end of this, you should have had some experience learning how to go about with this!
|
||||
|
||||
--------------------
|
||||
Part One: The Basics
|
||||
--------------------
|
||||
|
||||
Here we talk about the basics: what are Mobjects, Animations, Groups, Updaters, and many other basic aspects of Manim. It gives many examples
|
||||
about each concept, and encourages you to test your understanding by recreating certain sequences of animations.
|
||||
Feel free to return to this section anytime you need a refresher on something during part two.
|
||||
|
||||
---------------------
|
||||
Part Two: The Project
|
||||
---------------------
|
||||
|
||||
.. note:: As of right now, this section is still under construction.
|
||||
|
||||
|
||||
Here is the fun part: We go through and make our own creation from start to finish, explaining how to effectively search the documentation,
|
||||
and come up with the logic to make your ideas come to life. By the end of this section, you should be fully prepared to tackle your own project.
|
||||
|
||||
=================
|
||||
Table of Contents
|
||||
=================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
basics
|
||||
project
|
||||
|
|
@ -1,519 +0,0 @@
|
|||
***************
|
||||
A Basic Project
|
||||
***************
|
||||
|
||||
.. image:: ../_static/AdventureManim.png
|
||||
:align: center
|
||||
|
||||
|
||||
**Authors:** `Tristan Schulz <https://github.com/MrDiver>`__ and `Aarush Deshpande <https://github.com/JasonGrace2282>`__
|
||||
|
||||
.. note:: This is a work in progress guide and might not be complete at this point
|
||||
|
||||
############
|
||||
Introduction
|
||||
############
|
||||
Throughout this guide, we'll walk you through how to create a simple 30 second video about vector addition. If you don't
|
||||
already know what that is, it's recommended you watch `this <https://youtu.be/fNk_zzaMoSs?si=fQDML214IeNl0OZ1>`_ video
|
||||
by the original creator of manim, 3Blue1Brown.
|
||||
|
||||
The next step is figuring out how the project should look: what content should it cover, in what order, etc. In this
|
||||
tutorial, we'll focus on two parts of vector addition: the algebraic way, and the geometric way. For the algebraic way,
|
||||
we'll show two vectors (as matrices) being added, and give a short explanation. After that we'll show the typical tip-to-tail
|
||||
method for adding vectors graphically. Of course, choosing good examples is very important to help the viewer understand.
|
||||
In our case, we'll use the two vectors :math:`v_1\equiv\langle 2, 1\rangle` and :math:`v_2\equiv\langle 0,-3 \rangle`.
|
||||
|
||||
################
|
||||
Vector Addition
|
||||
################
|
||||
|
||||
We'll start with the basic setup needed for every manim video.
|
||||
To do this, we can use the manim cli to speed stuff up. In the terminal,
|
||||
run::
|
||||
|
||||
manim init project VectorAddition
|
||||
|
||||
This should create a folder called ``VectorAddition`` with the basic setup.
|
||||
|
||||
.. hint::
|
||||
|
||||
You may want to open this folder in your IDE (like VS Code, or PyCharm).
|
||||
|
||||
You will have a ``manim.cfg`` file, where you configuration will be stored, and a ``main.py`` script.
|
||||
The ``main.py`` script is where you will write your scenes.
|
||||
|
||||
If you did it correctly, running the python file with ``manim -p main.py`` should render a scene
|
||||
with a circle being created:
|
||||
|
||||
.. manim:: CreateCircle
|
||||
:hide_source:
|
||||
|
||||
class CreateCircle(Scene):
|
||||
def construct(self):
|
||||
circle = Circle()
|
||||
circle.set_fill(PINK, opacity=0.5)
|
||||
|
||||
square = Square()
|
||||
square.flip(RIGHT)
|
||||
square.rotate(-3 * TAU / 8)
|
||||
|
||||
self.play(Create(square))
|
||||
self.play(Transform(square, circle))
|
||||
self.play(FadeOut(square))
|
||||
|
||||
============
|
||||
Introduction
|
||||
============
|
||||
First we need to introduce the viewer to what we're going to talk about. Ideally,
|
||||
it would be an interesting hook, but for the sake of learning the library we will
|
||||
stick with a simple text-based intro. Try to recreate the following:
|
||||
|
||||
.. manim:: AdventureIntro
|
||||
:hide_source:
|
||||
:ref_classes: Tex Text Write Unwrite Create
|
||||
|
||||
class AdventureIntro(Scene):
|
||||
def construct(self):
|
||||
intro = Text("Let's try to add two vectors!")
|
||||
# put an r"" instead of a normal string so we don't have any special characters like \n
|
||||
vec_txts = Tex(r"We'll use $\boldsymbol{\vec{v}_1}=(2, 2)$ and $\boldsymbol{\vec{v}_2}=(0, -3)$")
|
||||
self.play(Create(intro))
|
||||
self.wait(1)
|
||||
# "grey out" the intro and shift it upwards as we write the second line
|
||||
self.play(intro.animate.shift(2*UP).set_opacity(0.5), Write(vec_txts))
|
||||
self.wait(1)
|
||||
self.play(Unwrite(intro), Unwrite(vec_txts), run_time=.5)
|
||||
self.wait(0.2)
|
||||
|
||||
|
||||
.. admonition:: Authors solution
|
||||
:class: dropdown
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class AdventureIntro(Scene):
|
||||
def construct(self):
|
||||
intro = Text("Let's try to add two vectors!")
|
||||
# put an r"" instead of a normal string so we don't have any special characters like \n
|
||||
vec_txts = Tex(
|
||||
r"We'll use $\boldsymbol{\vec{v}_1}=(2, 2)$ and $\boldsymbol{\vec{v}_2}=(0, -3)$"
|
||||
)
|
||||
self.play(Create(intro))
|
||||
self.wait(1)
|
||||
# "grey out" the intro and shift it upwards as we write the second line
|
||||
self.play(intro.animate.shift(2 * UP).set_opacity(0.5), Write(vec_txts))
|
||||
self.wait(1)
|
||||
self.play(Unwrite(intro), Unwrite(vec_txts), run_time=0.5)
|
||||
self.wait(0.2)
|
||||
|
||||
============================
|
||||
Algebraic vector addition
|
||||
============================
|
||||
|
||||
Then, let's show the viewer how vector addition between two vectors is done algebraically.
|
||||
Once again, try to recreate the following:
|
||||
|
||||
|
||||
.. manim:: AlgebraicAddition
|
||||
:hide_source:
|
||||
:ref_classes: Title MathTex Paragraph Tex Text Write Unwrite Create FadeIn
|
||||
|
||||
class AlgebraicAddition(Scene):
|
||||
|
||||
def construct(self):
|
||||
title = Title("Vector Addition Algebraically")
|
||||
|
||||
v1x, v1y = (2, 2)
|
||||
v2x, v2y = (0, -3)
|
||||
math = MathTex(r"""
|
||||
\begin{bmatrix} %(v1x)d \\ %(v1y)d \end{bmatrix}
|
||||
+\begin{bmatrix} %(v2x)d \\ %(v2y)d \end{bmatrix}
|
||||
""" % {
|
||||
'v1x': v1x,
|
||||
'v2x': v2x,
|
||||
'v1y': v1y,
|
||||
'v2y': v2y
|
||||
}).shift(DOWN)
|
||||
|
||||
resultant_vector = r"=\begin{bmatrix} %(x)d \\ %(y)d \end{bmatrix}" % {
|
||||
'x': v1x+v2x,
|
||||
'y': v1y+v2y
|
||||
}
|
||||
math_with_answer = MathTex(
|
||||
math.get_tex_string()+resultant_vector
|
||||
).move_to(math.get_center())
|
||||
|
||||
self.play(Write(math), FadeIn(title))
|
||||
self.wait(2)
|
||||
self.play(
|
||||
math.animate.shift(2*UP).set_opacity(0.5),
|
||||
Write(math_with_answer)
|
||||
)
|
||||
conclusion = Paragraph("As you can see,\nYou add each component individually").to_edge(DOWN)
|
||||
self.play(Write(conclusion))
|
||||
self.wait(2)
|
||||
self.play(Unwrite(math), Unwrite(math_with_answer), Unwrite(conclusion), Unwrite(title))
|
||||
|
||||
Hints
|
||||
-----
|
||||
|
||||
Use :class:`.Title` to display the title at the top of the Scene.
|
||||
|
||||
Use :class:`.MathTex` to represent the matrices, and try not to hardcode the values into the LaTeX string. Instead, you can use python string formatting and numpy vector addition, which will make it easier to change the vectors later if we need to.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
v1x, v1y = (2, 2)
|
||||
v2x, v2y = ...
|
||||
math = MathTex(r"""
|
||||
... %(v1x)d \\ %(v1y)d ...
|
||||
+ ... %(v2x)d \\ %(v2y)d ...
|
||||
""" % {
|
||||
'v1x': v1x,
|
||||
'v2x': v2x,
|
||||
'v1y': v1y,
|
||||
'v2y': v2y
|
||||
})
|
||||
|
||||
.. admonition:: Authors solution
|
||||
:class: dropdown
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class AlgebraicAddition(Scene):
|
||||
|
||||
def construct(self):
|
||||
title = Title("Vector Addition Algebraically")
|
||||
|
||||
v1x, v1y = (2, 2)
|
||||
v2x, v2y = (0, -3)
|
||||
math = MathTex(r"""
|
||||
\begin{bmatrix} %(v1x)d \\ %(v1y)d \end{bmatrix}
|
||||
+\begin{bmatrix} %(v2x)d \\ %(v2y)d \end{bmatrix}
|
||||
""" % {
|
||||
'v1x': v1x,
|
||||
'v2x': v2x,
|
||||
'v1y': v1y,
|
||||
'v2y': v2y
|
||||
}).shift(DOWN)
|
||||
|
||||
resultant_vector = r"=\begin{bmatrix} %(x)d \\ %(y)d \end{bmatrix}" % {
|
||||
'x': v1x+v2x,
|
||||
'y': v1y+v2y
|
||||
}
|
||||
math_with_answer = MathTex(
|
||||
math.get_tex_string()+resultant_vector
|
||||
).move_to(math.get_center())
|
||||
|
||||
self.play(Write(math), FadeIn(title))
|
||||
self.wait(2)
|
||||
self.play(
|
||||
math.animate.shift(2*UP).set_opacity(0.5),
|
||||
Write(math_with_answer)
|
||||
)
|
||||
conclusion = Paragraph("As you can see,\nYou add each component individually").to_edge(DOWN)
|
||||
self.play(Write(conclusion))
|
||||
self.wait(2)
|
||||
self.play(Unwrite(math), Unwrite(math_with_answer), Unwrite(conclusion), Unwrite(title))
|
||||
|
||||
============================
|
||||
Geometric vector addition
|
||||
============================
|
||||
|
||||
Lastly, let's show the vector addition geometrically. Try your best to reconstruct the following:
|
||||
|
||||
.. manim:: GeometricAddition
|
||||
:hide_source:
|
||||
:ref_classes: Title MathTex Paragraph Tex Text Write Unwrite Create FadeIn NumberPlane Arrow AnimationGroup ReplacementTransform VGroup
|
||||
|
||||
class VectorGroup(VGroup):
|
||||
def __init__(
|
||||
self, start, end, labelname: str,
|
||||
vector_color: ParsableManimColor, direction = RIGHT,
|
||||
plane: NumberPlane | None = None, **kwargs
|
||||
) -> None:
|
||||
if plane is not None:
|
||||
# if using a plane convert from plane units
|
||||
# to Munits
|
||||
start = plane.c2p(*start)
|
||||
end = plane.c2p(*end)
|
||||
|
||||
self.vector = Arrow(
|
||||
start,
|
||||
end,
|
||||
color=vector_color,
|
||||
buff=0
|
||||
)
|
||||
self.label = MathTex(labelname, color=vector_color)
|
||||
|
||||
def label_updater(m: MathTex, d=direction):
|
||||
m.next_to(self.vector, direction=d, **kwargs)
|
||||
|
||||
self.label.add_updater(label_updater, call_updater=True)
|
||||
super().__init__(self.vector, self.label, **kwargs)
|
||||
|
||||
@override_animation(Create)
|
||||
def _create_vec_write_label(self) -> AnimationGroup:
|
||||
return AnimationGroup(
|
||||
Create(self.vector),
|
||||
Write(self.label),
|
||||
lag_ratio=0
|
||||
)
|
||||
|
||||
@override_animation(Uncreate)
|
||||
def _uncreate_vec_unwrite_label(self) -> AnimationGroup:
|
||||
return AnimationGroup(
|
||||
Uncreate(self.vector),
|
||||
Unwrite(self.label),
|
||||
lag_ratio=0
|
||||
)
|
||||
|
||||
class GeometricAddition(Scene):
|
||||
def construct(self):
|
||||
title = Text("Now let's take a look at it geometrically")
|
||||
self.play(Write(title))
|
||||
self.wait(2)
|
||||
self.play(Unwrite(title))
|
||||
|
||||
plane = NumberPlane()
|
||||
|
||||
sum_point = (2, -1, 0)
|
||||
|
||||
v1 = VectorGroup(
|
||||
ORIGIN,
|
||||
(2, 2, 0),
|
||||
r"\boldsymbol{\vec{v}_1}",
|
||||
RED,
|
||||
direction=UP,
|
||||
plane=plane
|
||||
)
|
||||
|
||||
v2 = VectorGroup(
|
||||
ORIGIN,
|
||||
(0, -3, 0),
|
||||
r"\boldsymbol{\vec{v}_2}",
|
||||
YELLOW,
|
||||
direction=LEFT,
|
||||
plane=plane
|
||||
)
|
||||
|
||||
v1moved = VectorGroup(
|
||||
(0, -3, 0),
|
||||
sum_point,
|
||||
r"\boldsymbol{\vec{v}_1}",
|
||||
v1.vector.get_color(),
|
||||
plane=plane
|
||||
)
|
||||
|
||||
v2moved = VectorGroup(
|
||||
(2, 2, 0),
|
||||
sum_point,
|
||||
r"\boldsymbol{\vec{v}_2}",
|
||||
v2.vector.get_color(),
|
||||
plane=plane
|
||||
)
|
||||
|
||||
sum_vec = VectorGroup(
|
||||
ORIGIN,
|
||||
sum_point,
|
||||
r"\boldsymbol{\vec{v}_1}+\boldsymbol{\vec{v}_2}",
|
||||
ORANGE,
|
||||
direction=DOWN,
|
||||
plane=plane
|
||||
)
|
||||
|
||||
self.play(Create(plane), Create(v1))
|
||||
self.wait(0.5)
|
||||
self.play(Create(v2))
|
||||
self.wait()
|
||||
|
||||
# animate movement of vectors
|
||||
self.play(
|
||||
Succession(
|
||||
ReplacementTransform(v1.copy(), v1moved),
|
||||
ReplacementTransform(v2.copy(), v2moved)
|
||||
)
|
||||
)
|
||||
self.wait()
|
||||
# draw sum vector
|
||||
self.play(Create(sum_vec))
|
||||
self.wait()
|
||||
self.play(*[
|
||||
Uncreate(x)
|
||||
for x in (
|
||||
plane,
|
||||
v1,
|
||||
v2,
|
||||
v1moved,
|
||||
v2moved,
|
||||
sum_vec
|
||||
)
|
||||
])
|
||||
|
||||
Hints
|
||||
-----
|
||||
|
||||
Use :class:`.NumberPlane` to define the cartesian plane.
|
||||
|
||||
Use :class:`.Arrow` for the vectors.:
|
||||
|
||||
To make sure the label of the vector and the vector shift together, you can define a custom :class:`.VGroup` subclass.
|
||||
Take a look at the decorator :func:`.override_animation` to override the :class:`.Create` and :class:`.Uncreate` animations, it will come in handy when animating the subclass.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class VectorGroup(VGroup):
|
||||
def __init__(
|
||||
...
|
||||
) -> None:
|
||||
...
|
||||
|
||||
@override_animation(Create)
|
||||
def _create_vec_write_label(self) -> AnimationGroup:
|
||||
return AnimationGroup(
|
||||
...
|
||||
)
|
||||
|
||||
@override_animation(Uncreate)
|
||||
def _uncreate_vec_unwrite_label(self) -> AnimationGroup:
|
||||
return AnimationGroup(
|
||||
...
|
||||
)
|
||||
|
||||
.. admonition:: Authors solution
|
||||
:class: dropdown
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class VectorGroup(VGroup):
|
||||
def __init__(
|
||||
self, start, end, labelname: str,
|
||||
vector_color: ParsableManimColor, direction = RIGHT,
|
||||
plane: NumberPlane | None = None, **kwargs
|
||||
) -> None:
|
||||
if plane is not None:
|
||||
# if using a plane convert from plane units
|
||||
# to Munits
|
||||
start = plane.c2p(*start)
|
||||
end = plane.c2p(*end)
|
||||
|
||||
self.vector = Arrow(
|
||||
start,
|
||||
end,
|
||||
color=vector_color,
|
||||
buff=0
|
||||
)
|
||||
self.label = MathTex(labelname, color=vector_color)
|
||||
|
||||
def label_updater(m: MathTex, d=direction):
|
||||
m.next_to(self.vector, direction=d, **kwargs)
|
||||
|
||||
self.label.add_updater(label_updater, call_updater=True)
|
||||
super().__init__(self.vector, self.label, **kwargs)
|
||||
|
||||
@override_animation(Create)
|
||||
def _create_vec_write_label(self) -> AnimationGroup:
|
||||
return AnimationGroup(
|
||||
Create(self.vector),
|
||||
Write(self.label),
|
||||
lag_ratio=0
|
||||
)
|
||||
|
||||
@override_animation(Uncreate)
|
||||
def _uncreate_vec_unwrite_label(self) -> AnimationGroup:
|
||||
return AnimationGroup(
|
||||
Uncreate(self.vector),
|
||||
Unwrite(self.label),
|
||||
lag_ratio=0
|
||||
)
|
||||
|
||||
class GeometricAddition(Scene):
|
||||
def construct(self):
|
||||
title = Text("Now let's take a look at it geometrically")
|
||||
self.play(Write(title))
|
||||
self.wait(2)
|
||||
self.play(Unwrite(title))
|
||||
|
||||
plane = NumberPlane()
|
||||
|
||||
sum_point = (2, -1, 0)
|
||||
|
||||
v1 = VectorGroup(
|
||||
ORIGIN,
|
||||
(2, 2, 0),
|
||||
r"\boldsymbol{\vec{v}_1}",
|
||||
RED,
|
||||
direction=UP,
|
||||
plane=plane
|
||||
)
|
||||
|
||||
v2 = VectorGroup(
|
||||
ORIGIN,
|
||||
(0, -3, 0),
|
||||
r"\boldsymbol{\vec{v}_2}",
|
||||
YELLOW,
|
||||
direction=LEFT,
|
||||
plane=plane
|
||||
)
|
||||
|
||||
v1moved = VectorGroup(
|
||||
(0, -3, 0),
|
||||
sum_point,
|
||||
r"\boldsymbol{\vec{v}_1}",
|
||||
v1.vector.get_color(),
|
||||
plane=plane
|
||||
)
|
||||
|
||||
v2moved = VectorGroup(
|
||||
(2, 2, 0),
|
||||
sum_point,
|
||||
r"\boldsymbol{\vec{v}_2}",
|
||||
v2.vector.get_color(),
|
||||
plane=plane
|
||||
)
|
||||
|
||||
sum_vec = VectorGroup(
|
||||
ORIGIN,
|
||||
sum_point,
|
||||
r"\boldsymbol{\vec{v}_1}+\boldsymbol{\vec{v}_2}",
|
||||
ORANGE,
|
||||
direction=DOWN,
|
||||
plane=plane
|
||||
)
|
||||
|
||||
self.play(Create(plane), Create(v1))
|
||||
self.wait(0.5)
|
||||
self.play(Create(v2))
|
||||
self.wait()
|
||||
|
||||
# animate movement of vectors
|
||||
self.play(
|
||||
Succession(
|
||||
ReplacementTransform(v1.copy(), v1moved),
|
||||
ReplacementTransform(v2.copy(), v2moved)
|
||||
)
|
||||
)
|
||||
self.wait()
|
||||
# draw sum vector
|
||||
self.play(Create(sum_vec))
|
||||
self.wait()
|
||||
self.play(*[
|
||||
Uncreate(x)
|
||||
for x in (
|
||||
plane,
|
||||
v1,
|
||||
v2,
|
||||
v1moved,
|
||||
v2moved,
|
||||
sum_vec
|
||||
)
|
||||
])
|
||||
|
||||
|
||||
|
||||
################
|
||||
The Final Result
|
||||
################
|
||||
Putting it all together, we can render the final result.
|
||||
|
||||
.. include:: vector_addition.rst
|
||||
|
|
@ -1,179 +0,0 @@
|
|||
.. manim:: Adventure
|
||||
:hide_source:
|
||||
|
||||
class VectorGroup(VGroup):
|
||||
def __init__(
|
||||
self, start, end, labelname: str,
|
||||
vector_color: ParsableManimColor, direction = RIGHT,
|
||||
plane: NumberPlane | None = None, **kwargs
|
||||
) -> None:
|
||||
if plane is not None:
|
||||
# if using a plane convert from plane units
|
||||
# to Munits
|
||||
start = plane.c2p(*start)
|
||||
end = plane.c2p(*end)
|
||||
|
||||
self.vector = Arrow(
|
||||
start,
|
||||
end,
|
||||
color=vector_color,
|
||||
buff=0
|
||||
)
|
||||
self.label = MathTex(labelname, color=vector_color)
|
||||
|
||||
def label_updater(m: MathTex, d=direction):
|
||||
m.next_to(self.vector, direction=d, **kwargs)
|
||||
|
||||
self.label.add_updater(label_updater, call_updater=True)
|
||||
super().__init__(self.vector, self.label, **kwargs)
|
||||
|
||||
@override_animation(Create)
|
||||
def _create_vec_write_label(self) -> AnimationGroup:
|
||||
return AnimationGroup(
|
||||
Create(self.vector),
|
||||
Write(self.label),
|
||||
lag_ratio=0
|
||||
)
|
||||
|
||||
@override_animation(Uncreate)
|
||||
def _uncreate_vec_unwrite_label(self) -> AnimationGroup:
|
||||
return AnimationGroup(
|
||||
Uncreate(self.vector),
|
||||
Unwrite(self.label),
|
||||
lag_ratio=0
|
||||
)
|
||||
class Adventure(Scene):
|
||||
"""Goal: Make an example showcasing manim's features"""
|
||||
|
||||
def construct(self) -> None:
|
||||
intro = Text("Let's try to add two vectors!")
|
||||
vec_txts = Tex(r"We'll use $\boldsymbol{\vec{v}_1}=(2, 2)$ and $\boldsymbol{\vec{v}_2}=(0, -3)$")
|
||||
self.play(Create(intro))
|
||||
self.wait(1)
|
||||
self.play(intro.animate.shift(2*UP).set_opacity(0.5), Write(vec_txts))
|
||||
self.wait(1)
|
||||
self.play(Unwrite(intro), Unwrite(vec_txts), run_time=.5)
|
||||
self.wait(0.2)
|
||||
|
||||
self.show_addition_math()
|
||||
self.wait(0.2)
|
||||
self.show_vector_addition()
|
||||
|
||||
outro = Text("Thanks for watching!")
|
||||
self.play(Create(outro))
|
||||
self.wait()
|
||||
|
||||
def show_addition_math(self) -> None:
|
||||
title = Title("Vector Addition Algebraically")
|
||||
|
||||
v1x, v1y = (2, 2)
|
||||
v2x, v2y = (0, -3)
|
||||
math = MathTex(r"""
|
||||
\begin{bmatrix} %(v1x)d \\ %(v1y)d \end{bmatrix}
|
||||
+\begin{bmatrix} %(v2x)d \\ %(v2y)d \end{bmatrix}
|
||||
""" % {
|
||||
'v1x': v1x,
|
||||
'v2x': v2x,
|
||||
'v1y': v1y,
|
||||
'v2y': v2y
|
||||
}).shift(DOWN)
|
||||
|
||||
resultant_vector = r"=\begin{bmatrix} %(x)d \\ %(y)d \end{bmatrix}" % {
|
||||
'x': v1x+v2x,
|
||||
'y': v1y+v2y
|
||||
}
|
||||
math_with_answer = MathTex(
|
||||
math.get_tex_string()+resultant_vector
|
||||
).move_to(math.get_center())
|
||||
|
||||
self.play(Write(math), FadeIn(title))
|
||||
self.wait(2)
|
||||
self.play(
|
||||
math.animate.shift(2*UP).set_opacity(0.5),
|
||||
Write(math_with_answer)
|
||||
)
|
||||
conclusion = Paragraph("As you can see,\nYou add each component individually").to_edge(DOWN)
|
||||
self.play(Write(conclusion))
|
||||
self.wait(2)
|
||||
self.play(Unwrite(math), Unwrite(math_with_answer), Unwrite(conclusion), Unwrite(title))
|
||||
|
||||
def show_vector_addition(self) -> None:
|
||||
title = Text("Now let's take a look at it geometrically")
|
||||
self.play(Write(title))
|
||||
self.wait(2)
|
||||
self.play(Unwrite(title))
|
||||
|
||||
plane = NumberPlane()
|
||||
|
||||
sum_point = (2, -1, 0)
|
||||
|
||||
v1 = VectorGroup(
|
||||
ORIGIN,
|
||||
(2, 2, 0),
|
||||
r"\boldsymbol{\vec{v}_1}",
|
||||
RED,
|
||||
direction=UP,
|
||||
plane=plane
|
||||
)
|
||||
|
||||
v2 = VectorGroup(
|
||||
ORIGIN,
|
||||
(0, -3, 0),
|
||||
r"\boldsymbol{\vec{v}_2}",
|
||||
YELLOW,
|
||||
direction=LEFT,
|
||||
plane=plane
|
||||
)
|
||||
|
||||
v1moved = VectorGroup(
|
||||
(0, -3, 0),
|
||||
sum_point,
|
||||
r"\boldsymbol{\vec{v}_1}",
|
||||
v1.vector.get_color(),
|
||||
plane=plane
|
||||
)
|
||||
|
||||
v2moved = VectorGroup(
|
||||
(2, 2, 0),
|
||||
sum_point,
|
||||
r"\boldsymbol{\vec{v}_2}",
|
||||
v2.vector.get_color(),
|
||||
plane=plane
|
||||
)
|
||||
|
||||
sum_vec = VectorGroup(
|
||||
ORIGIN,
|
||||
sum_point,
|
||||
r"\boldsymbol{\vec{v}_1}+\boldsymbol{\vec{v}_2}",
|
||||
ORANGE,
|
||||
direction=DOWN,
|
||||
plane=plane
|
||||
)
|
||||
|
||||
self.play(Create(plane), Create(v1))
|
||||
self.wait(0.5)
|
||||
self.play(Create(v2))
|
||||
self.wait()
|
||||
|
||||
# animate movement of vectors
|
||||
self.play(
|
||||
Succession(
|
||||
ReplacementTransform(v1.copy(), v1moved),
|
||||
ReplacementTransform(v2.copy(), v2moved)
|
||||
)
|
||||
)
|
||||
self.wait()
|
||||
# draw sum vector
|
||||
self.play(Create(sum_vec))
|
||||
self.wait()
|
||||
self.play(*[
|
||||
Uncreate(x)
|
||||
for x in (
|
||||
plane,
|
||||
v1,
|
||||
v2,
|
||||
v1moved,
|
||||
v2moved,
|
||||
sum_vec
|
||||
)
|
||||
])
|
||||
|
|
@ -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
|
||||
|
|
|
|||
9
docs/source/changelog/0.18.0.post0-changelog.rst
Normal file
9
docs/source/changelog/0.18.0.post0-changelog.rst
Normal 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.
|
||||
160
docs/source/changelog/0.18.1-changelog.md
Normal file
160
docs/source/changelog/0.18.1-changelog.md
Normal 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
|
||||
197
docs/source/changelog/0.19.1-changelog.md
Normal file
197
docs/source/changelog/0.19.1-changelog.md
Normal 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 Manim’s 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
|
||||
41
docs/source/changelog/0.19.2-changelog.md
Normal file
41
docs/source/changelog/0.19.2-changelog.md
Normal 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
|
||||
86
docs/source/changelog/0.20.0-changelog.md
Normal file
86
docs/source/changelog/0.20.0-changelog.md
Normal 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)
|
||||
41
docs/source/changelog/0.20.1-changelog.md
Normal file
41
docs/source/changelog/0.20.1-changelog.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
short-title: v0.20.1
|
||||
description: Changelog for v0.20.1
|
||||
---
|
||||
|
||||
# v0.20.1
|
||||
|
||||
Date
|
||||
: February 27, 2026
|
||||
|
||||
|
||||
## What's Changed
|
||||
### Enhancements 🚀
|
||||
* Cleanup `TipableVMobject`: avoid mutable default and fix `assign_tip_attr` typo by {user}`josiest` in {pr}`4503`
|
||||
* enhancement: optimize Docker image build and runtime footprint by {user}`behackl` in {pr}`4604`
|
||||
|
||||
### Bug Fixes 🐛
|
||||
* fix: MathTex double-brace splitting no longer fires on natural LaTeX `}}` by {user}`behackl` in {pr}`4602`
|
||||
* Fix creation or animation of a zero-length `DashedLine` by {user}`SORVER` in {pr}`4606`
|
||||
* Fix moving-object detection for nested AnimationGroups with z-indexed mobjects by {user}`Merzlikin-Matvey` in {pr}`4389`
|
||||
* Fix unintended propagation of `kwargs` in `LaggedStartMap` by {user}`irvanalhaq9` in {pr}`4613`
|
||||
|
||||
### Documentation 📚
|
||||
* Documentation: manual installation of manim as a local package by {user}`u7920349` in {pr}`4456`
|
||||
* Add alt text to all images in `README.md` by {user}`VerisimilitudeX` in {pr}`4064`
|
||||
|
||||
### Code Quality & Refactoring 🧹
|
||||
* Fix publish release workflow by {user}`behackl` in {pr}`4600`
|
||||
* Silence pydub ffmpeg/avconv import warning when ffmpeg CLI is absent by {user}`behackl` in {pr}`4603`
|
||||
|
||||
### Type Hints 📝
|
||||
* Add type annotations to `manim/_config/utils.py` by {user}`henrikmidtiby` in {pr}`4230`
|
||||
|
||||
## New Contributors
|
||||
* {user}`SORVER` made their first contribution in {pr}`4606`
|
||||
* {user}`josiest` made their first contribution in {pr}`4503`
|
||||
* {user}`u7920349` made their first contribution in {pr}`4456`
|
||||
* {user}`Merzlikin-Matvey` made their first contribution in {pr}`4389`
|
||||
* {user}`VerisimilitudeX` made their first contribution in {pr}`4064`
|
||||
|
||||
**Full Changelog**: [Compare view](https://github.com/ManimCommunity/manim/compare/v0.20.0...v0.20.1)
|
||||
|
|
@ -51,7 +51,6 @@ extensions = [
|
|||
"sphinx.ext.inheritance_diagram",
|
||||
"sphinxcontrib.programoutput",
|
||||
"myst_parser",
|
||||
"sphinx_togglebutton",
|
||||
"sphinx_design",
|
||||
"sphinx_reredirects",
|
||||
]
|
||||
|
|
@ -59,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 = {
|
||||
|
|
@ -125,7 +124,6 @@ html_theme_options = {
|
|||
"source_repository": "https://github.com/ManimCommunity/manim/",
|
||||
"source_branch": "main",
|
||||
"source_directory": "docs/source/",
|
||||
"top_of_page_button": None,
|
||||
"light_logo": "manim-logo-sidebar.svg",
|
||||
"dark_logo": "manim-logo-sidebar-dark.svg",
|
||||
"light_css_variables": {
|
||||
|
|
@ -158,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
|
||||
|
|
|
|||
249
docs/source/contributing/development.md
Normal file
249
docs/source/contributing/development.md
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
# Manim Development Process
|
||||
|
||||
## For first-time contributors
|
||||
|
||||
1. Install git:
|
||||
|
||||
For instructions see <https://git-scm.com/>.
|
||||
|
||||
2. Fork the project:
|
||||
|
||||
Go to <https://github.com/ManimCommunity/manim> and click the "fork" button
|
||||
to create a copy of the project for you to work on. You will need a
|
||||
GitHub account. This will allow you to make a "Pull Request" (PR)
|
||||
to the ManimCommunity repo later on.
|
||||
|
||||
3. Clone your fork to your local computer:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/<your-username>/manim.git
|
||||
```
|
||||
|
||||
GitHub will provide both a SSH (`git@github.com:<your-username>/manim.git`) and
|
||||
HTTPS (`https://github.com/<your-username>/manim.git`) URL for cloning.
|
||||
You can use SSH if you have SSH keys setup.
|
||||
|
||||
:::{WARNING}
|
||||
Do not clone the ManimCommunity repository. You must clone your own
|
||||
fork.
|
||||
:::
|
||||
|
||||
4. Change the directory to enter the project folder:
|
||||
|
||||
```shell
|
||||
cd manim
|
||||
```
|
||||
|
||||
5. Add the upstream repository, ManimCommunity:
|
||||
|
||||
```shell
|
||||
git remote add upstream https://github.com/ManimCommunity/manim.git
|
||||
```
|
||||
|
||||
6. Now, `git remote -v` should show two remote repositories named:
|
||||
|
||||
- `origin`, your forked repository
|
||||
- `upstream` the ManimCommunity repository
|
||||
|
||||
7. Install the Python project management tool `uv`, as recommended
|
||||
in our {doc}`installation guide for users </installation/uv>`.
|
||||
|
||||
8. Let `uv` create a virtual environment for your development
|
||||
installation by running
|
||||
|
||||
```shell
|
||||
uv sync
|
||||
```
|
||||
|
||||
In case you need (or want) to install some of the optional dependency
|
||||
groups defined in our [`pyproject.toml`](https://github.com/ManimCommunity/manim/blob/main/pyproject.toml),
|
||||
run `uv sync --all-extras`, or pass the `--extra` flag with the
|
||||
name of a group, for example `uv sync --extra jupyterhub`.
|
||||
|
||||
9. Install Pre-Commit:
|
||||
|
||||
```shell
|
||||
uv run pre-commit install
|
||||
```
|
||||
|
||||
This will ensure during development that each of your commits is properly
|
||||
formatted against our linter and formatters.
|
||||
|
||||
You are now ready to work on Manim!
|
||||
|
||||
## Develop your contribution
|
||||
|
||||
1. Checkout your local repository's main branch and pull the latest
|
||||
changes from ManimCommunity, `upstream`, into your local repository:
|
||||
|
||||
```shell
|
||||
git switch main
|
||||
git pull --rebase upstream main
|
||||
```
|
||||
|
||||
2. Create a branch for the changes you want to work on rather than working
|
||||
off of your local main branch:
|
||||
|
||||
```shell
|
||||
git switch -c <new branch name> upstream/main
|
||||
```
|
||||
|
||||
This ensures you can easily update your local repository's main with the
|
||||
first step and switch branches to work on multiple features.
|
||||
|
||||
3. Write some awesome code!
|
||||
|
||||
You're ready to make changes in your local repository's branch.
|
||||
You can add local files you've changed within the current directory with
|
||||
`git add .`, or add specific files with
|
||||
|
||||
```shell
|
||||
git add <file/directory>
|
||||
```
|
||||
|
||||
and commit these changes to your local history with `git commit`. If you
|
||||
have installed pre-commit, your commit will succeed only if none of the
|
||||
hooks fail.
|
||||
|
||||
:::{tip}
|
||||
When crafting commit messages, it is highly recommended that
|
||||
you adhere to [these guidelines](https://www.conventionalcommits.org/en/v1.0.0/).
|
||||
:::
|
||||
|
||||
4. Add new or update existing tests.
|
||||
|
||||
Depending on your changes, you may need to update or add new tests. For new
|
||||
features, it is required that you include tests with your PR. Details of
|
||||
our testing system are explained in the {doc}`testing guide <testing>`.
|
||||
|
||||
5. Update docstrings and documentation:
|
||||
|
||||
Update the docstrings (the text in triple quotation marks) of any functions
|
||||
or classes you change and include them with any new functions you add.
|
||||
See the {doc}`documentation guide <docs/docstrings>` for more information about how we
|
||||
prefer our code to be documented. The content of the docstrings will be
|
||||
rendered in the {doc}`reference manual <../reference>`.
|
||||
|
||||
:::{tip}
|
||||
Use the {mod}`manim directive for Sphinx <manim.utils.docbuild.manim_directive>` to add examples
|
||||
to the documentation!
|
||||
:::
|
||||
|
||||
As far as development on your local machine goes, these are the main steps you
|
||||
should follow.
|
||||
|
||||
(polishing-changes-and-submitting-a-pull-request)=
|
||||
|
||||
## Polishing Changes and Submitting a Pull Request
|
||||
|
||||
As soon as you are ready to share your local changes with the community
|
||||
so that they can be discussed, go through the following steps to open a
|
||||
pull request. A pull request signifies to the ManimCommunity organization,
|
||||
"Here are some changes I wrote; I think it's worthwhile for you to maintain
|
||||
them."
|
||||
|
||||
:::{note}
|
||||
You do not need to have everything (code/documentation/tests) complete
|
||||
to open a pull request (PR). If the PR is still under development, please
|
||||
mark it as a draft. Community developers will still be able to review the
|
||||
changes, discuss yet-to-be-implemented changes, and offer advice; however,
|
||||
the more complete your PR, the quicker it will be merged.
|
||||
:::
|
||||
|
||||
1. Update your fork on GitHub to reflect your local changes:
|
||||
|
||||
```shell
|
||||
git push -u origin <branch name>
|
||||
```
|
||||
|
||||
Doing so creates a new branch on your remote fork, `origin`, with the
|
||||
contents of your local repository on GitHub. In subsequent pushes, this
|
||||
local branch will track the branch `origin` and `git push` is enough.
|
||||
|
||||
2. Make a pull request (PR) on GitHub.
|
||||
|
||||
In order to make the ManimCommunity development team aware of your changes,
|
||||
you can make a PR to the ManimCommunity repository from your fork.
|
||||
|
||||
:::{WARNING}
|
||||
Make sure to select `ManimCommunity/manim` instead of `3b1b/manim`
|
||||
as the base repository!
|
||||
:::
|
||||
|
||||
Choose the branch from your fork as the head repository - see the
|
||||
screenshot below.
|
||||
|
||||
```{image} /_static/pull-requests.png
|
||||
:align: center
|
||||
```
|
||||
|
||||
Please make sure you follow the template (this is the default
|
||||
text you are shown when first opening the 'New Pull Request' page).
|
||||
|
||||
Your changes are eligible to be merged if:
|
||||
|
||||
1. there are no merge conflicts
|
||||
2. the tests in our pipeline pass
|
||||
3. at least one (two for more complex changes) Community Developer approves the changes
|
||||
|
||||
You can check for merge conflicts between the current upstream/main and
|
||||
your branch by executing `git pull upstream main` locally. If this
|
||||
generates any merge conflicts, you need to resolve them and push an
|
||||
updated version of the branch to your fork of the repository.
|
||||
|
||||
Our pipeline consists of a series of different tests that ensure
|
||||
that Manim still works as intended and that the code you added
|
||||
sticks to our coding conventions.
|
||||
|
||||
- **Code style**: We use the code style imposed
|
||||
by [Black](https://black.readthedocs.io/en/stable/), [isort](https://pycqa.github.io/isort/)
|
||||
and [flake8](https://flake8.pycqa.org/en/latest/). The GitHub pipeline
|
||||
makes sure that the (Python) files changed in your pull request
|
||||
also adhere to this code style. If this step of the pipeline fails,
|
||||
fix your code formatting automatically by running `black <file or directory>` and `isort <file or directory>`.
|
||||
To fix code style problems, run `flake8 <file or directory>` for a style report, and then fix the problems
|
||||
manually that were detected by `flake8`.
|
||||
- **Tests**: The pipeline runs Manim's test suite on different operating systems
|
||||
(the latest versions of Ubuntu, macOS, and Windows) for different versions of Python.
|
||||
The test suite consists of two different kinds of tests: integration tests
|
||||
and doctests. You can run them locally by executing `uv run pytest`
|
||||
and `uv run pytest --doctest-modules manim`, respectively, from the
|
||||
root directory of your cloned fork.
|
||||
- **Documentation**: We also build a version of the documentation corresponding
|
||||
to your pull request. Make sure not to introduce any Sphinx errors, and have
|
||||
a look at the built HTML files to see whether the formatting of the documentation
|
||||
you added looks as you intended. You can build the documentation locally
|
||||
by running `make html` from the `docs` directory. Make sure you have [Graphviz](https://graphviz.org/)
|
||||
installed locally in order to build the inheritance diagrams. See {doc}`docs` for
|
||||
more information.
|
||||
|
||||
Finally, if the pipeline passes and you are satisfied with your changes: wait for
|
||||
feedback and iterate over any requested changes. You will likely be asked to
|
||||
edit or modify your PR in one way or another during this process. This is not
|
||||
an indictment of your work, but rather a strong signal that the community
|
||||
wants to merge your changes! Once approved, your changes may be merged!
|
||||
|
||||
### Further useful guidelines
|
||||
|
||||
1. When submitting a PR, please mention explicitly if it includes breaking changes.
|
||||
2. When submitting a PR, make sure that your proposed changes are as general as
|
||||
possible, and ready to be taken advantage of by all of Manim's users. In
|
||||
particular, leave out any machine-specific configurations, or any personal
|
||||
information it may contain.
|
||||
3. If you are a maintainer, please label issues and PRs appropriately and
|
||||
frequently.
|
||||
4. When opening a new issue, if there are old issues that are related, add a link
|
||||
to them in your new issue (even if the old ones are closed).
|
||||
5. When submitting a code review, it is highly recommended that you adhere to
|
||||
[these general guidelines](https://conventionalcomments.org/).
|
||||
6. If you find stale or inactive issues that seem to be irrelevant, please post
|
||||
a comment saying 'This issue should be closed', and a community developer
|
||||
will take a look.
|
||||
7. Please do as much as possible to keep issues, PRs, and development in
|
||||
general as tidy as possible.
|
||||
|
||||
You can find examples for the `docs` in several places:
|
||||
the {doc}`Example Gallery <../examples>`, {doc}`Tutorials <../tutorials/index>`,
|
||||
and {doc}`Reference Classes <../reference>`.
|
||||
|
||||
**Thank you for contributing!**
|
||||
|
|
@ -1,288 +0,0 @@
|
|||
=========================
|
||||
Manim Development Process
|
||||
=========================
|
||||
|
||||
For first-time contributors
|
||||
---------------------------
|
||||
#. Install git:
|
||||
|
||||
For instructions see https://git-scm.com/.
|
||||
|
||||
|
||||
#. Fork the project:
|
||||
|
||||
Go to https://github.com/ManimCommunity/manim and click the "fork" button
|
||||
to create a copy of the project for you to work on. You will need a
|
||||
GitHub account. This will allow you to make a "Pull Request" (PR)
|
||||
to the ManimCommunity repo later on.
|
||||
|
||||
#. Clone your fork to your local computer:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
git clone https://github.com/<your-username>/manim.git
|
||||
|
||||
GitHub will provide both a SSH (``git@github.com:<your-username>/manim.git``) and
|
||||
HTTPS (``https://github.com/<your-username>/manim.git``) URL for cloning.
|
||||
You can use SSH if you have SSH keys setup.
|
||||
|
||||
.. WARNING::
|
||||
|
||||
Do not clone the ManimCommunity repository. You must clone your own
|
||||
fork.
|
||||
|
||||
#. Change the directory to enter the project folder:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
cd manim
|
||||
|
||||
#. Add the upstream repository, ManimCommunity:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
git remote add upstream https://github.com/ManimCommunity/manim.git
|
||||
|
||||
#. Now, ``git remote -v`` should show two remote repositories named:
|
||||
|
||||
- ``origin``, your forked repository
|
||||
- ``upstream`` the ManimCommunity repository
|
||||
|
||||
#. Install Manim:
|
||||
|
||||
- Follow the steps in our :doc:`installation instructions
|
||||
<../installation>` to install **Manim's system dependencies**.
|
||||
We also recommend installing a LaTeX distribution.
|
||||
|
||||
- We recommend using `Poetry <https://python-poetry.org>`__ to manage your
|
||||
developer installation of Manim. Poetry is a tool for dependency
|
||||
management and packaging in Python. It allows you to declare the libraries
|
||||
your project depends on, and it will manage (install / update) them
|
||||
for you. In addition, Poetry provides a simple interface for
|
||||
managing virtual environments.
|
||||
|
||||
If you choose to use Poetry as well, follow `Poetry's installation
|
||||
guidelines <https://python-poetry.org/docs/#installing-with-pipx>`__
|
||||
to install it on your system, then run ``poetry install`` from
|
||||
your cloned repository. Poetry will then install Manim, as well
|
||||
as create and enter a virtual environment. You can always re-enter
|
||||
that environment by running ``poetry shell``.
|
||||
|
||||
- In case you want to install extra dependencies that are defined in
|
||||
the ``[tool.poetry.extras]`` section of ``pyproject.toml``, this can be done by passing
|
||||
the ``-E`` flag, for example ``poetry install -E jupyterlab -E gui``.
|
||||
|
||||
- In case you decided against Poetry, you can install Manim via pip
|
||||
by running ``python3 -m pip install .``. Note that due to our
|
||||
development infrastructure being based on Poetry, we currently
|
||||
do not support editable installs via ``pip``, so you will have
|
||||
to re-run this command every time you make changes to the source
|
||||
code.
|
||||
|
||||
.. note::
|
||||
|
||||
The following steps assume that you chose to install and work with
|
||||
Poetry.
|
||||
|
||||
#. Install Pre-Commit:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
poetry run pre-commit install
|
||||
|
||||
This will ensure during development that each of your commits is properly
|
||||
formatted against our linter and formatters, ``black``, ``flake8``,
|
||||
``isort`` and ``codespell``.
|
||||
|
||||
You are now ready to work on Manim!
|
||||
|
||||
Develop your contribution
|
||||
-------------------------
|
||||
|
||||
#. Checkout your local repository's main branch and pull the latest
|
||||
changes from ManimCommunity, ``upstream``, into your local repository:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
git checkout main
|
||||
git pull --rebase upstream main
|
||||
|
||||
#. Create a branch for the changes you want to work on rather than working
|
||||
off of your local main branch:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
git checkout -b <new branch name> upstream/main
|
||||
|
||||
This ensures you can easily update your local repository's main with the
|
||||
first step and switch branches to work on multiple features.
|
||||
|
||||
#. Write some awesome code!
|
||||
|
||||
You're ready to make changes in your local repository's branch.
|
||||
You can add local files you've changed within the current directory with
|
||||
``git add .``, or add specific files with
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
git add <file/directory>
|
||||
|
||||
and commit these changes to your local history with ``git commit``. If you
|
||||
have installed pre-commit, your commit will succeed only if none of the
|
||||
hooks fail.
|
||||
|
||||
.. tip::
|
||||
|
||||
When crafting commit messages, it is highly recommended that
|
||||
you adhere to `these guidelines <https://www.conventionalcommits.org/en/v1.0.0/>`_.
|
||||
|
||||
#. Add new or update existing tests.
|
||||
|
||||
Depending on your changes, you may need to update or add new tests. For new
|
||||
features, it is required that you include tests with your PR. Details of
|
||||
our testing system are explained in the :doc:`testing guide <testing>`.
|
||||
|
||||
|
||||
#. Update docstrings and documentation:
|
||||
|
||||
Update the docstrings (the text in triple quotation marks) of any functions
|
||||
or classes you change and include them with any new functions you add.
|
||||
See the :doc:`documentation guide <docs/docstrings>` for more information about how we
|
||||
prefer our code to be documented. The content of the docstrings will be
|
||||
rendered in the :doc:`reference manual <../reference>`.
|
||||
|
||||
.. tip::
|
||||
|
||||
Use the :mod:`manim directive for Sphinx <manim.utils.docbuild.manim_directive>` to add examples
|
||||
to the documentation!
|
||||
|
||||
As far as development on your local machine goes, these are the main steps you
|
||||
should follow.
|
||||
|
||||
.. _polishing-changes-and-submitting-a-pull-request:
|
||||
|
||||
Polishing Changes and Submitting a Pull Request
|
||||
-----------------------------------------------
|
||||
|
||||
As soon as you are ready to share your local changes with the community
|
||||
so that they can be discussed, go through the following steps to open a
|
||||
pull request. A pull request signifies to the ManimCommunity organization,
|
||||
"Here are some changes I wrote; I think it's worthwhile for you to maintain
|
||||
them."
|
||||
|
||||
.. note::
|
||||
|
||||
You do not need to have everything (code/documentation/tests) complete
|
||||
to open a pull request (PR). If the PR is still under development, please
|
||||
mark it as a draft. Community developers will still be able to review the
|
||||
changes, discuss yet-to-be-implemented changes, and offer advice; however,
|
||||
the more complete your PR, the quicker it will be merged.
|
||||
|
||||
#. Update your fork on GitHub to reflect your local changes:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
git push -u origin <branch name>
|
||||
|
||||
Doing so creates a new branch on your remote fork, ``origin``, with the
|
||||
contents of your local repository on GitHub. In subsequent pushes, this
|
||||
local branch will track the branch ``origin`` and ``git push`` is enough.
|
||||
|
||||
|
||||
#. Make a pull request (PR) on GitHub.
|
||||
|
||||
In order to make the ManimCommunity development team aware of your changes,
|
||||
you can make a PR to the ManimCommunity repository from your fork.
|
||||
|
||||
.. WARNING::
|
||||
|
||||
Make sure to select ``ManimCommunity/manim`` instead of ``3b1b/manim``
|
||||
as the base repository!
|
||||
|
||||
Choose the branch from your fork as the head repository - see the
|
||||
screenshot below.
|
||||
|
||||
.. image:: /_static/pull-requests.png
|
||||
:align: center
|
||||
|
||||
Please make sure you follow the template (this is the default
|
||||
text you are shown when first opening the 'New Pull Request' page).
|
||||
|
||||
|
||||
Your changes are eligible to be merged if:
|
||||
|
||||
#. there are no merge conflicts
|
||||
#. the tests in our pipeline pass
|
||||
#. at least one (two for more complex changes) Community Developer approves the changes
|
||||
|
||||
You can check for merge conflicts between the current upstream/main and
|
||||
your branch by executing ``git pull upstream main`` locally. If this
|
||||
generates any merge conflicts, you need to resolve them and push an
|
||||
updated version of the branch to your fork of the repository.
|
||||
|
||||
Our pipeline consists of a series of different tests that ensure
|
||||
that Manim still works as intended and that the code you added
|
||||
sticks to our coding conventions.
|
||||
|
||||
- **Code style**: We use the code style imposed
|
||||
by `Black <https://black.readthedocs.io/en/stable/>`_, `isort <https://pycqa.github.io/isort/>`_
|
||||
and `flake8 <https://flake8.pycqa.org/en/latest/>`_. The GitHub pipeline
|
||||
makes sure that the (Python) files changed in your pull request
|
||||
also adhere to this code style. If this step of the pipeline fails,
|
||||
fix your code formatting automatically by running ``black <file or directory>`` and ``isort <file or directory>``.
|
||||
To fix code style problems, run ``flake8 <file or directory>`` for a style report, and then fix the problems
|
||||
manually that were detected by ``flake8``.
|
||||
|
||||
- **Tests**: The pipeline runs Manim's test suite on different operating systems
|
||||
(the latest versions of Ubuntu, macOS, and Windows) for different versions of Python.
|
||||
The test suite consists of two different kinds of tests: integration tests
|
||||
and doctests. You can run them locally by executing ``poetry run pytest``
|
||||
and ``poetry run pytest --doctest-modules manim``, respectively, from the
|
||||
root directory of your cloned fork.
|
||||
|
||||
- **Documentation**: We also build a version of the documentation corresponding
|
||||
to your pull request. Make sure not to introduce any Sphinx errors, and have
|
||||
a look at the built HTML files to see whether the formatting of the documentation
|
||||
you added looks as you intended. You can build the documentation locally
|
||||
by running ``make html`` from the ``docs`` directory. Make sure you have `Graphviz <https://graphviz.org/>`_
|
||||
installed locally in order to build the inheritance diagrams. See :doc:`docs` for
|
||||
more information.
|
||||
|
||||
Finally, if the pipeline passes and you are satisfied with your changes: wait for
|
||||
feedback and iterate over any requested changes. You will likely be asked to
|
||||
edit or modify your PR in one way or another during this process. This is not
|
||||
an indictment of your work, but rather a strong signal that the community
|
||||
wants to merge your changes! Once approved, your changes may be merged!
|
||||
|
||||
Further useful guidelines
|
||||
=========================
|
||||
|
||||
#. When submitting a PR, please mention explicitly if it includes breaking changes.
|
||||
|
||||
#. When submitting a PR, make sure that your proposed changes are as general as
|
||||
possible, and ready to be taken advantage of by all of Manim's users. In
|
||||
particular, leave out any machine-specific configurations, or any personal
|
||||
information it may contain.
|
||||
|
||||
#. If you are a maintainer, please label issues and PRs appropriately and
|
||||
frequently.
|
||||
|
||||
#. When opening a new issue, if there are old issues that are related, add a link
|
||||
to them in your new issue (even if the old ones are closed).
|
||||
|
||||
#. When submitting a code review, it is highly recommended that you adhere to
|
||||
`these general guidelines <https://conventionalcomments.org/>`_.
|
||||
|
||||
#. If you find stale or inactive issues that seem to be irrelevant, please post
|
||||
a comment saying 'This issue should be closed', and a community developer
|
||||
will take a look.
|
||||
|
||||
#. Please do as much as possible to keep issues, PRs, and development in
|
||||
general as tidy as possible.
|
||||
|
||||
|
||||
You can find examples for the ``docs`` in several places:
|
||||
the :doc:`Example Gallery <../examples>`, :doc:`Tutorials <../tutorials/index>`,
|
||||
and :doc:`Reference Classes <../reference>`.
|
||||
|
||||
**Thank you for contributing!**
|
||||
|
|
@ -85,22 +85,4 @@ Attention
|
|||
.. attention::
|
||||
A attention
|
||||
|
||||
Dropdown
|
||||
~~~~~~~~~
|
||||
|
||||
.. code-block:: rest
|
||||
|
||||
.. admonition:: A dropdown
|
||||
:class: dropdown
|
||||
|
||||
A dropdown
|
||||
|
||||
Make sure you leave a line between ":class:" and the text below, otherwise the dropdown won't render.
|
||||
|
||||
.. admonition:: A dropdown
|
||||
:class: dropdown
|
||||
|
||||
A dropdown
|
||||
|
||||
|
||||
You can find further information about Admonitions here: https://pradyunsg.me/furo/reference/admonitions/
|
||||
|
|
|
|||
|
|
@ -85,14 +85,8 @@ typed as a :class:`~.Point3D`, because it represents a direction along
|
|||
which to shift a :class:`~.Mobject`, not a position in space.
|
||||
|
||||
As a general rule, if a parameter is called ``direction`` or ``axis``,
|
||||
it should be type hinted as some form of :class:`~.VectorND`.
|
||||
|
||||
.. warning::
|
||||
|
||||
This is not always true. For example, as of Manim 0.18.0, the direction
|
||||
parameter of the :class:`.Vector` Mobject should be
|
||||
``Point2DLike | Point3DLike``, as it can also accept ``tuple[float, float]``
|
||||
and ``tuple[float, float, float]``.
|
||||
it should be type hinted as some form of :class:`~.VectorND` or
|
||||
:class:`~.VectorNDLike`.
|
||||
|
||||
Colors
|
||||
------
|
||||
|
|
|
|||
|
|
@ -299,7 +299,7 @@ Animations
|
|||
path.become(previous_path)
|
||||
path.add_updater(update_path)
|
||||
self.add(path, dot)
|
||||
self.play(Rotating(dot, radians=PI, about_point=RIGHT, run_time=2))
|
||||
self.play(Rotating(dot, angle=PI, about_point=RIGHT, run_time=2))
|
||||
self.wait()
|
||||
self.play(dot.animate.shift(UP))
|
||||
self.play(dot.animate.shift(LEFT))
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -2,17 +2,24 @@
|
|||
Rendering Text and Formulas
|
||||
###########################
|
||||
|
||||
There are two different ways by which you can render **Text** in videos:
|
||||
There are three different ways by which you can render **Text** in videos:
|
||||
|
||||
1. Using Pango (:mod:`~.text_mobject`)
|
||||
2. Using LaTeX (:mod:`~.tex_mobject`)
|
||||
3. Using Typst (:mod:`~.typst_mobject`)
|
||||
|
||||
If you want to render simple text, you should use either :class:`~.Text` or
|
||||
:class:`~.MarkupText`, or one of its derivatives like :class:`~.Paragraph`.
|
||||
Manim's Pango-based text classes include :class:`~.Text`,
|
||||
:class:`~.MarkupText`, and derivatives such as :class:`~.Paragraph`.
|
||||
See :ref:`using-text-objects` for more information.
|
||||
|
||||
LaTeX should be used when you need mathematical typesetting. See
|
||||
:ref:`rendering-with-latex` for more information.
|
||||
LaTeX rendering is available via :class:`~.Tex` and
|
||||
:class:`~.MathTex`. See :ref:`rendering-with-latex` for more
|
||||
information.
|
||||
|
||||
Typst support is available via :class:`~.Typst` and
|
||||
:class:`~.TypstMath`. It offers both general markup and mathematical
|
||||
typesetting through the Typst compiler without requiring a TeX
|
||||
distribution. See :ref:`typst-mobjects` for more information.
|
||||
|
||||
.. _using-text-objects:
|
||||
|
||||
|
|
@ -50,7 +57,7 @@ For example:
|
|||
)
|
||||
self.add(text)
|
||||
|
||||
.. _Pango library: https://pango.gnome.org
|
||||
.. _Pango library: https://pango.org
|
||||
|
||||
Working with :class:`~.Text`
|
||||
============================
|
||||
|
|
@ -291,6 +298,54 @@ and further references about PangoMarkup.
|
|||
)
|
||||
self.add(text)
|
||||
|
||||
.. _rendering-with-typst:
|
||||
|
||||
Text With Typst
|
||||
***************
|
||||
|
||||
Manim also supports rendering text and formulas with Typst via
|
||||
:class:`~.Typst` and :class:`~.TypstMath`.
|
||||
|
||||
.. important::
|
||||
|
||||
Typst support requires the optional ``typst`` dependency. Install it with
|
||||
``pip install manim[typst]``.
|
||||
|
||||
Typst mobjects compile Typst markup directly to SVG and import the result as
|
||||
vector graphics. This works both for general markup and for mathematical
|
||||
expressions.
|
||||
|
||||
.. manim:: HelloTypst
|
||||
:save_last_frame:
|
||||
:ref_classes: Typst
|
||||
|
||||
class HelloTypst(Scene):
|
||||
def construct(self):
|
||||
text = Typst(r"*Hello* from _Typst!_", font_size=96)
|
||||
self.add(text)
|
||||
|
||||
For mathematical expressions, use :class:`~.TypstMath`:
|
||||
|
||||
.. manim:: HelloTypstMath
|
||||
:save_last_frame:
|
||||
:ref_classes: TypstMath
|
||||
|
||||
class HelloTypstMath(Scene):
|
||||
def construct(self):
|
||||
equation = TypstMath(r"sum_(k=1)^n k = (n(n + 1)) / 2", font_size=72)
|
||||
self.add(equation)
|
||||
|
||||
Typst also supports selecting subexpressions via labels in the Typst source,
|
||||
or via Manim's ``{{ ... }}`` shorthand in :class:`~.TypstMath`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
eq = TypstMath("{{ a + b : lhs }} = {{ c }}")
|
||||
eq.select("lhs").set_color(BLUE)
|
||||
eq.select(0).set_color(YELLOW)
|
||||
|
||||
See :ref:`typst-mobjects` for more details and additional examples.
|
||||
|
||||
.. _rendering-with-latex:
|
||||
|
||||
Text With LaTeX
|
||||
|
|
@ -389,8 +444,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 +454,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 +468,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
|
||||
=======================================================
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ in the right place!
|
|||
.. note::
|
||||
|
||||
Please be aware that there are different, incompatible versions of Manim available.
|
||||
ManimCE was forked from 3b1b/manim, originally created and open-sourced by Grant Sanderson, creator of [3Blue1Brown](https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw) educational math videos.
|
||||
This version, the Community Edition of Manim (`ManimCE <https://github.com/ManimCommunity/manim>`_),
|
||||
is a separate project maintained by the community, but it was forked from `3b1b/manim <https://github.com/3b1b/manim>`_,
|
||||
the original Manim created and open-sourced by Grant Sanderson, creator of `3Blue1Brown <https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw>`_ educational math videos.
|
||||
Check our :ref:`installation FAQ <different-versions>`
|
||||
to learn more!
|
||||
|
||||
|
|
@ -92,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
|
||||
-----
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
-----------------------------------
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ the instructions below.
|
|||
|
||||
::::::{tab-set}
|
||||
|
||||
:::::{tab-item} MacOS & Windows
|
||||
:::::{tab-item} Windows
|
||||
The following commands will
|
||||
|
||||
- create a new directory for a Python project,
|
||||
|
|
@ -163,8 +163,55 @@ Manim is now installed in your local project environment!
|
|||
|
||||
:::::
|
||||
|
||||
:::::{tab-item} MacOS
|
||||
Before we can install Manim, we need to make sure that the system utilities
|
||||
`cairo` and `pkg-config` are present. They are needed for the [`pycairo` Python
|
||||
package](https://pycairo.readthedocs.io/en/latest/), a dependency of Manim.
|
||||
|
||||
The easiest way of installing these utilities is by using [Homebrew](https://brew.sh/),
|
||||
a fairly popular 3rd party package manager for MacOS. Check whether Homebrew is
|
||||
already installed by running
|
||||
|
||||
```bash
|
||||
brew --version
|
||||
```
|
||||
|
||||
which will report something along the lines of `Homebrew 4.4.15-54-...`
|
||||
if it is installed, and a message `command not found: brew` otherwise. In this
|
||||
case, use the shell installer [as instructed on Homebrew's website](https://brew.sh/),
|
||||
or get a `.pkg`-installer from
|
||||
[their GitHub release page](https://github.com/Homebrew/brew/releases). Make sure to
|
||||
follow the instructions of the installer carefully, especially when prompted to
|
||||
modify your `.zprofile` to add Homebrew to your system's PATH.
|
||||
|
||||
With Homebrew available, the required utilities can be installed by running
|
||||
|
||||
```bash
|
||||
brew install cairo pkg-config
|
||||
```
|
||||
|
||||
With all of this preparation out of the way, now it is time to actually install
|
||||
Manim itself! The following commands will
|
||||
|
||||
- create a new directory for a Python project,
|
||||
- and add Manim as a dependency, which installs it into the corresponding
|
||||
local Python environment.
|
||||
|
||||
The name for the Python project is *manimations*, which you can change
|
||||
to anything you like.
|
||||
|
||||
```bash
|
||||
uv init manimations
|
||||
cd manimations
|
||||
uv add manim
|
||||
```
|
||||
|
||||
Manim is now installed in your local project environment!
|
||||
|
||||
:::::
|
||||
|
||||
:::::{tab-item} Linux
|
||||
Practically, the instructions given in the *MacOS & Windows* tab
|
||||
Practically, the instructions given in the *Windows* tab
|
||||
also apply for Linux -- however, some additional dependencies are
|
||||
required as Linux users need to build
|
||||
[ManimPango](https://github.com/ManimCommunity/ManimPango)
|
||||
|
|
@ -282,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
|
||||
```
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -96,14 +96,15 @@ The only requirement of manim plugins is that they specify an entry point
|
|||
with the group, ``"manim.plugins"``. This allows Manim to discover plugins
|
||||
available in the user's environment. Everything regarding the plugin's
|
||||
directory structure, build system, and naming are completely up to your
|
||||
discretion as an author. The aforementioned template plugin is only a model
|
||||
using Poetry since this is the build system Manim uses. The plugin's `entry
|
||||
point <https://packaging.python.org/specifications/entry-points/>`_ can be
|
||||
specified in Poetry as:
|
||||
discretion as an author.
|
||||
|
||||
The standard way to specify an entry point (see
|
||||
`the Python packaging guide <https://packaging.python.org/specifications/entry-points/>`__
|
||||
for details) is to include the following in your ``pyproject.toml``:
|
||||
|
||||
.. code-block:: toml
|
||||
|
||||
[tool.poetry.plugins."manim.plugins"]
|
||||
[project.entry-points."manim.plugins"]
|
||||
"name" = "object_reference"
|
||||
|
||||
.. versionremoved:: 0.18.1
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ Module Index
|
|||
~utils.commands
|
||||
~utils.config_ops
|
||||
constants
|
||||
data_structures
|
||||
~utils.debug
|
||||
~utils.deprecation
|
||||
~utils.docbuild
|
||||
|
|
|
|||
|
|
@ -327,6 +327,13 @@ Generally, you start with the starting number and add only some part of the valu
|
|||
So, the logic of calculating the number to display at each step will be ``50 + alpha * (100 - 50)``.
|
||||
Once you set the calculated value for the :class:`~.DecimalNumber`, you are done.
|
||||
|
||||
.. note::
|
||||
|
||||
If you're creating a custom animation and want to use a ``rate_func``, you must explicitly apply
|
||||
``self.rate_func(alpha)`` to the parameter you're animating. For example, try switching the rate
|
||||
function to ``rate_functions.there_and_back`` to observe how it affects the counting behavior.
|
||||
|
||||
|
||||
Once you have defined your ``Count`` animation, you can play it in your :class:`~.Scene` for any duration you want for any :class:`~.DecimalNumber` with any rate function.
|
||||
|
||||
.. manim:: CountingScene
|
||||
|
|
@ -343,7 +350,7 @@ Once you have defined your ``Count`` animation, you can play it in your :class:`
|
|||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
# Set value of DecimalNumber according to alpha
|
||||
value = self.start + (alpha * (self.end - self.start))
|
||||
value = self.start + (self.rate_func(alpha) * (self.end - self.start))
|
||||
self.mobject.set_value(value)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -7,5 +7,4 @@ Tutorials & Guides
|
|||
|
||||
tutorials/index
|
||||
guides/index
|
||||
adventure/index
|
||||
faq/index
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class TexFontTemplateLibrary(Scene):
|
|||
Many of the in the TexFontTemplates collection require that specific fonts
|
||||
are installed on your local machine.
|
||||
For example, choosing the template TexFontTemplates.comic_sans will
|
||||
not compile if the Comic Sans Micrososft font is not installed.
|
||||
not compile if the Comic Sans Microsoft font is not installed.
|
||||
|
||||
This scene will only render those Templates that do not cause a TeX
|
||||
compilation error on your system. Furthermore, some of the ones that do render,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
#!/usr/bin/env python
|
||||
from __future__ import annotations
|
||||
|
||||
from importlib.metadata import version
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
|
||||
__version__ = version(__name__)
|
||||
# Use installed distribution version if available; otherwise fall back to a
|
||||
# sensible default so that importing from a source checkout works without an
|
||||
# editable install (pip install -e .).
|
||||
try:
|
||||
__version__ = version(__name__)
|
||||
except PackageNotFoundError:
|
||||
# Package is not installed; provide a fallback version string.
|
||||
__version__ = "0.0.0+unknown"
|
||||
|
||||
|
||||
# isort: off
|
||||
|
|
@ -66,6 +73,7 @@ from .mobject.text.code_mobject import *
|
|||
from .mobject.text.numbers import *
|
||||
from .mobject.text.tex_mobject import *
|
||||
from .mobject.text.text_mobject import *
|
||||
from .mobject.text.typst_mobject import *
|
||||
from .mobject.three_d.polyhedra import *
|
||||
from .mobject.three_d.three_d_utils import *
|
||||
from .mobject.three_d.three_dimensions import *
|
||||
|
|
|
|||
|
|
@ -23,10 +23,9 @@ __all__ = [
|
|||
|
||||
parser = make_config_parser()
|
||||
|
||||
# The logger can be accessed from anywhere as manim.logger, or as
|
||||
# logging.getLogger("manim"). The console must be accessed as manim.console.
|
||||
# Throughout the codebase, use manim.console.print() instead of print().
|
||||
# Use error_console to print errors so that it outputs to stderr.
|
||||
# Logger usage: accessible globally as `manim.logger` or via `logging.getLogger("manim")`.
|
||||
# For printing, use `manim.console.print()` instead of the built-in `print()`.
|
||||
# For error output, use `error_console`, which prints to stderr.
|
||||
logger, console, error_console = make_logger(
|
||||
parser["logger"],
|
||||
parser["CLI"]["verbosity"],
|
||||
|
|
@ -45,7 +44,7 @@ frame = ManimFrame(config)
|
|||
# This has to go here because it needs access to this module's config
|
||||
@contextmanager
|
||||
def tempconfig(temp: ManimConfig | dict[str, Any]) -> Generator[None, None, None]:
|
||||
"""Context manager that temporarily modifies the global ``config`` object.
|
||||
"""Temporarily modifies the global ``config`` object using a context manager.
|
||||
|
||||
Inside the ``with`` statement, the modified config will be used. After
|
||||
context manager exits, the config will be restored to its original state.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
"""Parses CLI context settings from the configuration file and returns a Cloup Context settings dictionary.
|
||||
|
||||
This module reads configuration values for help formatting, theme styles, and alignment options
|
||||
used when rendering command-line interfaces in Manim.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
|
|
@ -9,7 +15,7 @@ __all__ = ["parse_cli_ctx"]
|
|||
|
||||
|
||||
def parse_cli_ctx(parser: configparser.SectionProxy) -> dict[str, Any]:
|
||||
formatter_settings: dict[str, str | int] = {
|
||||
formatter_settings: dict[str, str | int | None] = {
|
||||
"indent_increment": int(parser["indent_increment"]),
|
||||
"width": int(parser["width"]),
|
||||
"col1_max_width": int(parser["col1_max_width"]),
|
||||
|
|
@ -28,6 +34,7 @@ def parse_cli_ctx(parser: configparser.SectionProxy) -> dict[str, Any]:
|
|||
"col2",
|
||||
"epilog",
|
||||
}
|
||||
# Extract and apply any style-related keys defined in the config section.
|
||||
for k, v in parser.items():
|
||||
if k in theme_keys and v:
|
||||
theme_settings.update({k: Style(v)})
|
||||
|
|
@ -37,22 +44,24 @@ def parse_cli_ctx(parser: configparser.SectionProxy) -> dict[str, Any]:
|
|||
if theme is None:
|
||||
formatter = HelpFormatter.settings(
|
||||
theme=HelpTheme(**theme_settings),
|
||||
**formatter_settings, # type: ignore[arg-type]
|
||||
**formatter_settings,
|
||||
)
|
||||
elif theme.lower() == "dark":
|
||||
formatter = HelpFormatter.settings(
|
||||
theme=HelpTheme.dark().with_(**theme_settings),
|
||||
**formatter_settings, # type: ignore[arg-type]
|
||||
**formatter_settings,
|
||||
)
|
||||
elif theme.lower() == "light":
|
||||
formatter = HelpFormatter.settings(
|
||||
theme=HelpTheme.light().with_(**theme_settings),
|
||||
**formatter_settings, # type: ignore[arg-type]
|
||||
**formatter_settings,
|
||||
)
|
||||
|
||||
return Context.settings(
|
||||
return_val: dict[str, Any] = Context.settings(
|
||||
align_option_groups=parser["align_option_groups"].lower() == "true",
|
||||
align_sections=parser["align_sections"].lower() == "true",
|
||||
show_constraints=True,
|
||||
formatter_settings=formatter,
|
||||
)
|
||||
|
||||
return return_val
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import configparser
|
|||
import copy
|
||||
import json
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from rich import color, errors
|
||||
from rich import print as printf
|
||||
|
|
@ -91,7 +91,7 @@ def make_logger(
|
|||
# set the rich handler
|
||||
rich_handler = RichHandler(
|
||||
console=console,
|
||||
show_time=parser.getboolean("log_timestamps"),
|
||||
show_time=parser.getboolean("log_timestamps", fallback=False),
|
||||
keywords=HIGHLIGHTED_KEYWORDS,
|
||||
)
|
||||
|
||||
|
|
@ -108,7 +108,7 @@ def make_logger(
|
|||
return logger, console, error_console
|
||||
|
||||
|
||||
def parse_theme(parser: configparser.SectionProxy) -> Theme:
|
||||
def parse_theme(parser: configparser.SectionProxy) -> Theme | None:
|
||||
"""Configure the rich style of logger and console output.
|
||||
|
||||
Parameters
|
||||
|
|
@ -126,7 +126,7 @@ def parse_theme(parser: configparser.SectionProxy) -> Theme:
|
|||
:func:`make_logger`.
|
||||
|
||||
"""
|
||||
theme = {key.replace("_", "."): parser[key] for key in parser}
|
||||
theme: dict[str, Any] = {key.replace("_", "."): parser[key] for key in parser}
|
||||
|
||||
theme["log.width"] = None if theme["log.width"] == "-1" else int(theme["log.width"])
|
||||
theme["log.height"] = (
|
||||
|
|
@ -188,8 +188,11 @@ class JSONFormatter(logging.Formatter):
|
|||
"""Format the record in a custom JSON format."""
|
||||
record_c = copy.deepcopy(record)
|
||||
if record_c.args:
|
||||
for arg in record_c.args:
|
||||
record_c.args[arg] = "<>"
|
||||
if isinstance(record_c.args, dict):
|
||||
for arg in record_c.args:
|
||||
record_c.args[arg] = "<>"
|
||||
else:
|
||||
record_c.args = ("<>",) * len(record_c.args)
|
||||
return json.dumps(
|
||||
{
|
||||
"levelname": record_c.levelname,
|
||||
|
|
|
|||
|
|
@ -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,8 +1461,8 @@ class ManimConfig(MutableMapping):
|
|||
self._set_boolean("enable_gui", value)
|
||||
|
||||
@property
|
||||
def gui_location(self) -> tuple[Any]:
|
||||
"""Enable GUI interaction."""
|
||||
def gui_location(self) -> tuple[int, ...]:
|
||||
"""Location parameters for the GUI window (e.g., screen coordinates or layout settings)."""
|
||||
return self._d["gui_location"]
|
||||
|
||||
@gui_location.setter
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -54,7 +52,6 @@ class Animation:
|
|||
For example ``rate_func(0.5)`` is the proportion of the animation that is done
|
||||
after half of the animations run time.
|
||||
|
||||
|
||||
reverse_rate_function
|
||||
Reverses the rate function of the animation. Setting ``reverse_rate_function``
|
||||
does not have any effect on ``remover`` or ``introducer``. These need to be
|
||||
|
|
@ -121,7 +118,7 @@ class Animation:
|
|||
if func is not None:
|
||||
anim = func(mobject, *args, **kwargs)
|
||||
logger.debug(
|
||||
f"The {cls.__name__} animation has been is overridden for "
|
||||
f"The {cls.__name__} animation has been overridden for "
|
||||
f"{type(mobject).__name__} mobjects. use_override = False can "
|
||||
f" be used as keyword argument to prevent animation overriding.",
|
||||
)
|
||||
|
|
@ -130,7 +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,
|
||||
|
|
@ -140,8 +137,8 @@ class Animation:
|
|||
suspend_mobject_updating: bool = True,
|
||||
introducer: bool = False,
|
||||
*,
|
||||
_on_finish: Callable[[], None] = lambda _: None,
|
||||
**kwargs,
|
||||
_on_finish: Callable[[Scene], None] = lambda _: None,
|
||||
use_override: bool = True, # included here to avoid TypeError if passed from a subclass' constructor
|
||||
) -> None:
|
||||
self._typecheck_input(mobject)
|
||||
self.run_time: float = run_time
|
||||
|
|
@ -161,8 +158,6 @@ class Animation:
|
|||
else:
|
||||
self.starting_mobject: Mobject = Mobject()
|
||||
self.mobject: Mobject = mobject if mobject is not None else Mobject()
|
||||
if kwargs:
|
||||
logger.debug("Animation received extra kwargs: %s", kwargs)
|
||||
|
||||
if hasattr(self, "CONFIG"):
|
||||
logger.error(
|
||||
|
|
@ -265,11 +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
|
||||
|
|
@ -283,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:
|
||||
|
|
@ -499,6 +497,8 @@ class Animation:
|
|||
|
||||
cls._original__init__ = cls.__init__
|
||||
|
||||
_original__init__ = __init__ # needed if set_default() is called with no kwargs directly from Animation
|
||||
|
||||
@classmethod
|
||||
def set_default(cls, **kwargs) -> None:
|
||||
"""Sets the default values of keyword arguments.
|
||||
|
|
@ -541,7 +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.
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["AnimatedBoundary", "TracedPath"]
|
||||
|
||||
from typing import Callable
|
||||
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
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.utils.color import (
|
||||
|
|
@ -16,7 +18,7 @@ from manim.utils.color import (
|
|||
WHITE,
|
||||
ParsableManimColor,
|
||||
)
|
||||
from manim.utils.rate_functions import smooth
|
||||
from manim.utils.rate_functions import RateFunction, smooth
|
||||
|
||||
|
||||
class AnimatedBoundary(VGroup):
|
||||
|
|
@ -38,14 +40,14 @@ class AnimatedBoundary(VGroup):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
vmobject,
|
||||
colors=[BLUE_D, BLUE_B, BLUE_E, GREY_BROWN],
|
||||
max_stroke_width=3,
|
||||
cycle_rate=0.5,
|
||||
back_and_forth=True,
|
||||
draw_rate_func=smooth,
|
||||
fade_rate_func=smooth,
|
||||
**kwargs,
|
||||
vmobject: VMobject,
|
||||
colors: Sequence[ParsableManimColor] = [BLUE_D, BLUE_B, BLUE_E, GREY_BROWN],
|
||||
max_stroke_width: float = 3,
|
||||
cycle_rate: float = 0.5,
|
||||
back_and_forth: bool = True,
|
||||
draw_rate_func: RateFunction = smooth,
|
||||
fade_rate_func: RateFunction = smooth,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.colors = colors
|
||||
|
|
@ -59,10 +61,10 @@ class AnimatedBoundary(VGroup):
|
|||
vmobject.copy().set_style(stroke_width=0, fill_opacity=0) for x in range(2)
|
||||
]
|
||||
self.add(*self.boundary_copies)
|
||||
self.total_time = 0
|
||||
self.total_time = 0.0
|
||||
self.add_updater(lambda m, dt: self.update_boundary_copies(dt))
|
||||
|
||||
def update_boundary_copies(self, dt):
|
||||
def update_boundary_copies(self, dt: float) -> None:
|
||||
# Not actual time, but something which passes at
|
||||
# an altered rate to make the implementation below
|
||||
# cleaner
|
||||
|
|
@ -78,9 +80,9 @@ class AnimatedBoundary(VGroup):
|
|||
fade_alpha = self.fade_rate_func(alpha)
|
||||
|
||||
if self.back_and_forth and int(time) % 2 == 1:
|
||||
bounds = (1 - draw_alpha, 1)
|
||||
bounds = (1.0 - draw_alpha, 1.0)
|
||||
else:
|
||||
bounds = (0, draw_alpha)
|
||||
bounds = (0.0, draw_alpha)
|
||||
self.full_family_become_partial(growing, vmobject, *bounds)
|
||||
growing.set_stroke(colors[index], width=msw)
|
||||
|
||||
|
|
@ -90,10 +92,12 @@ class AnimatedBoundary(VGroup):
|
|||
|
||||
self.total_time += dt
|
||||
|
||||
def full_family_become_partial(self, mob1, mob2, a, b):
|
||||
def full_family_become_partial(
|
||||
self, mob1: VMobject, mob2: VMobject, a: float, b: float
|
||||
) -> Self:
|
||||
family1 = mob1.family_members_with_points()
|
||||
family2 = mob2.family_members_with_points()
|
||||
for sm1, sm2 in zip(family1, family2):
|
||||
for sm1, sm2 in zip(family1, family2, strict=False):
|
||||
sm1.pointwise_become_partial(sm2, a, b)
|
||||
return self
|
||||
|
||||
|
|
@ -146,20 +150,21 @@ class TracedPath(VMobject, metaclass=ConvertToOpenGL):
|
|||
stroke_width: float = 2,
|
||||
stroke_color: ParsableManimColor | None = WHITE,
|
||||
dissipating_time: float | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(stroke_color=stroke_color, stroke_width=stroke_width, **kwargs)
|
||||
self.traced_point_func = traced_point_func
|
||||
self.dissipating_time = dissipating_time
|
||||
self.time = 1 if self.dissipating_time else None
|
||||
self.time = 1.0 if self.dissipating_time else None
|
||||
self.add_updater(self.update_path)
|
||||
|
||||
def update_path(self, mob, dt):
|
||||
def update_path(self, mob: Mobject, dt: float) -> None:
|
||||
new_point = self.traced_point_func()
|
||||
if not self.has_points():
|
||||
self.start_new_path(new_point)
|
||||
self.add_line_to(new_point)
|
||||
if self.dissipating_time:
|
||||
assert self.time is not None
|
||||
self.time += dt
|
||||
if self.time - 1 > self.dissipating_time:
|
||||
nppcc = self.n_points_per_curve
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import types
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -12,7 +11,7 @@ from manim._config import config
|
|||
from manim.animation.animation import Animation, prepare_animation
|
||||
from manim.constants import RendererType
|
||||
from manim.mobject.mobject import Group, Mobject
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLGroup
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLGroup, OpenGLMobject
|
||||
from manim.scene.scene import Scene
|
||||
from manim.utils.iterables import remove_list_redundancies
|
||||
from manim.utils.parameter_parsing import flatten_iterable_parameters
|
||||
|
|
@ -54,31 +53,34 @@ class AnimationGroup(Animation):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
*animations: Animation | Iterable[Animation] | types.GeneratorType[Animation],
|
||||
group: Group | VGroup | OpenGLGroup | OpenGLVGroup = None,
|
||||
*animations: Animation | Iterable[Animation],
|
||||
group: Group | VGroup | OpenGLGroup | OpenGLVGroup | None = None,
|
||||
run_time: float | None = None,
|
||||
rate_func: Callable[[float], float] = linear,
|
||||
lag_ratio: float = 0,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
**kwargs: Any,
|
||||
):
|
||||
arg_anim = flatten_iterable_parameters(animations)
|
||||
self.animations = [prepare_animation(anim) for anim in arg_anim]
|
||||
self.rate_func = rate_func
|
||||
self.group = group
|
||||
if self.group is None:
|
||||
if group is None:
|
||||
mobjects = remove_list_redundancies(
|
||||
[anim.mobject for anim in self.animations if not anim.is_introducer()],
|
||||
)
|
||||
if config["renderer"] == RendererType.OPENGL:
|
||||
self.group = OpenGLGroup(*mobjects)
|
||||
self.group: Group | VGroup | OpenGLGroup | OpenGLVGroup = OpenGLGroup(
|
||||
*mobjects
|
||||
)
|
||||
else:
|
||||
self.group = Group(*mobjects)
|
||||
else:
|
||||
self.group = group
|
||||
super().__init__(
|
||||
self.group, rate_func=self.rate_func, lag_ratio=lag_ratio, **kwargs
|
||||
)
|
||||
self.run_time: float = self.init_run_time(run_time)
|
||||
|
||||
def get_all_mobjects(self) -> Sequence[Mobject]:
|
||||
def get_all_mobjects(self) -> Sequence[Mobject | OpenGLMobject]:
|
||||
return list(self.group)
|
||||
|
||||
def begin(self) -> None:
|
||||
|
|
@ -93,7 +95,7 @@ class AnimationGroup(Animation):
|
|||
for anim in self.animations:
|
||||
anim.begin()
|
||||
|
||||
def _setup_scene(self, scene) -> None:
|
||||
def _setup_scene(self, scene: Scene) -> None:
|
||||
for anim in self.animations:
|
||||
anim._setup_scene(scene)
|
||||
|
||||
|
|
@ -118,7 +120,7 @@ class AnimationGroup(Animation):
|
|||
]:
|
||||
anim.update_mobjects(dt)
|
||||
|
||||
def init_run_time(self, run_time) -> float:
|
||||
def init_run_time(self, run_time: float | None) -> float:
|
||||
"""Calculates the run time of the animation, if different from ``run_time``.
|
||||
|
||||
Parameters
|
||||
|
|
@ -146,9 +148,9 @@ class AnimationGroup(Animation):
|
|||
run_times = np.array([anim.run_time for anim in self.animations])
|
||||
num_animations = run_times.shape[0]
|
||||
dtype = [("anim", "O"), ("start", "f8"), ("end", "f8")]
|
||||
self.anims_with_timings = np.zeros(num_animations, dtype=dtype)
|
||||
self.anims_begun = np.zeros(num_animations, dtype=bool)
|
||||
self.anims_finished = np.zeros(num_animations, dtype=bool)
|
||||
self.anims_with_timings: np.ndarray = np.zeros(num_animations, dtype=dtype)
|
||||
self.anims_begun: np.ndarray = np.zeros(num_animations, dtype=bool)
|
||||
self.anims_finished: np.ndarray = np.zeros(num_animations, dtype=bool)
|
||||
if num_animations == 0:
|
||||
return
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -76,8 +76,8 @@ __all__ = [
|
|||
|
||||
|
||||
import itertools as it
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -120,7 +120,7 @@ class ShowPartial(Animation):
|
|||
):
|
||||
pointwise = getattr(mobject, "pointwise_become_partial", None)
|
||||
if not callable(pointwise):
|
||||
raise NotImplementedError("This animation is not defined for this Mobject.")
|
||||
raise TypeError(f"{self.__class__.__name__} only works for VMobjects.")
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def interpolate_submobject(
|
||||
|
|
@ -133,7 +133,7 @@ class ShowPartial(Animation):
|
|||
starting_submobject, *self._get_bounds(alpha)
|
||||
)
|
||||
|
||||
def _get_bounds(self, alpha: float) -> None:
|
||||
def _get_bounds(self, alpha: float) -> tuple[float, float]:
|
||||
raise NotImplementedError("Please use Create or ShowPassingFlash")
|
||||
|
||||
|
||||
|
|
@ -173,7 +173,7 @@ class Create(ShowPartial):
|
|||
) -> None:
|
||||
super().__init__(mobject, lag_ratio=lag_ratio, introducer=introducer, **kwargs)
|
||||
|
||||
def _get_bounds(self, alpha: float) -> tuple[int, float]:
|
||||
def _get_bounds(self, alpha: float) -> tuple[float, float]:
|
||||
return (0, alpha)
|
||||
|
||||
|
||||
|
|
@ -229,8 +229,6 @@ class DrawBorderThenFill(Animation):
|
|||
rate_func: Callable[[float], float] = double_smooth,
|
||||
stroke_width: float = 2,
|
||||
stroke_color: str = None,
|
||||
draw_border_animation_config: dict = {}, # what does this dict accept?
|
||||
fill_animation_config: dict = {},
|
||||
introducer: bool = True,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
|
|
@ -244,8 +242,6 @@ class DrawBorderThenFill(Animation):
|
|||
)
|
||||
self.stroke_width = stroke_width
|
||||
self.stroke_color = stroke_color
|
||||
self.draw_border_animation_config = draw_border_animation_config
|
||||
self.fill_animation_config = fill_animation_config
|
||||
self.outline = self.get_outline()
|
||||
|
||||
def _typecheck_input(self, vmobject: VMobject | OpenGLVMobject) -> None:
|
||||
|
|
@ -476,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()
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ __all__ = [
|
|||
"FadeIn",
|
||||
]
|
||||
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
|
|
@ -53,7 +55,7 @@ class _Fade(Transform):
|
|||
shift: np.ndarray | None = None,
|
||||
target_position: np.ndarray | Mobject | None = None,
|
||||
scale: float = 1,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
if not mobjects:
|
||||
raise ValueError("At least one mobject must be passed.")
|
||||
|
|
@ -85,7 +87,7 @@ class _Fade(Transform):
|
|||
Mobject
|
||||
The faded, shifted and scaled copy of the mobject.
|
||||
"""
|
||||
faded_mobject = self.mobject.copy()
|
||||
faded_mobject: Mobject = self.mobject.copy() # type: ignore[assignment]
|
||||
faded_mobject.fade(1)
|
||||
direction_modifier = -1 if fadeIn and not self.point_target else 1
|
||||
faded_mobject.shift(self.shift_vector * direction_modifier)
|
||||
|
|
@ -131,13 +133,13 @@ class FadeIn(_Fade):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, *mobjects: Mobject, **kwargs) -> None:
|
||||
def __init__(self, *mobjects: Mobject, **kwargs: Any) -> None:
|
||||
super().__init__(*mobjects, introducer=True, **kwargs)
|
||||
|
||||
def create_target(self):
|
||||
return self.mobject
|
||||
def create_target(self) -> Mobject:
|
||||
return self.mobject # type: ignore[return-value]
|
||||
|
||||
def create_starting_mobject(self):
|
||||
def create_starting_mobject(self) -> Mobject:
|
||||
return self._create_faded_mobject(fadeIn=True)
|
||||
|
||||
|
||||
|
|
@ -179,12 +181,12 @@ class FadeOut(_Fade):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, *mobjects: Mobject, **kwargs) -> None:
|
||||
def __init__(self, *mobjects: Mobject, **kwargs: Any) -> None:
|
||||
super().__init__(*mobjects, remover=True, **kwargs)
|
||||
|
||||
def create_target(self):
|
||||
def create_target(self) -> Mobject:
|
||||
return self._create_faded_mobject(fadeIn=False)
|
||||
|
||||
def clean_up_from_scene(self, scene: Scene = None) -> None:
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
super().clean_up_from_scene(scene)
|
||||
self.interpolate(0)
|
||||
|
|
|
|||
|
|
@ -31,16 +31,17 @@ __all__ = [
|
|||
"SpinInFromNothing",
|
||||
]
|
||||
|
||||
import typing
|
||||
|
||||
import numpy as np
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ..animation.transform import Transform
|
||||
from ..constants import PI
|
||||
from ..utils.paths import spiral_path
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.geometry.line import Arrow
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.typing import Point3DLike, Vector3DLike
|
||||
from manim.utils.color import ParsableManimColor
|
||||
|
||||
from ..mobject.mobject import Mobject
|
||||
|
||||
|
|
@ -76,16 +77,20 @@ class GrowFromPoint(Transform):
|
|||
"""
|
||||
|
||||
def __init__(
|
||||
self, mobject: Mobject, point: np.ndarray, point_color: str = None, **kwargs
|
||||
) -> None:
|
||||
self,
|
||||
mobject: Mobject,
|
||||
point: Point3DLike,
|
||||
point_color: ParsableManimColor | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.point = point
|
||||
self.point_color = point_color
|
||||
super().__init__(mobject, introducer=True, **kwargs)
|
||||
|
||||
def create_target(self) -> Mobject:
|
||||
def create_target(self) -> Mobject | OpenGLMobject:
|
||||
return self.mobject
|
||||
|
||||
def create_starting_mobject(self) -> Mobject:
|
||||
def create_starting_mobject(self) -> Mobject | OpenGLMobject:
|
||||
start = super().create_starting_mobject()
|
||||
start.scale(0)
|
||||
start.move_to(self.point)
|
||||
|
|
@ -118,7 +123,12 @@ class GrowFromCenter(GrowFromPoint):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, mobject: Mobject, point_color: str = None, **kwargs) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
point_color: ParsableManimColor | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
point = mobject.get_center()
|
||||
super().__init__(mobject, point, point_color=point_color, **kwargs)
|
||||
|
||||
|
|
@ -153,8 +163,12 @@ class GrowFromEdge(GrowFromPoint):
|
|||
"""
|
||||
|
||||
def __init__(
|
||||
self, mobject: Mobject, edge: np.ndarray, point_color: str = None, **kwargs
|
||||
) -> None:
|
||||
self,
|
||||
mobject: Mobject,
|
||||
edge: Vector3DLike,
|
||||
point_color: ParsableManimColor | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
point = mobject.get_critical_point(edge)
|
||||
super().__init__(mobject, point, point_color=point_color, **kwargs)
|
||||
|
||||
|
|
@ -183,11 +197,13 @@ class GrowArrow(GrowFromPoint):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, arrow: Arrow, point_color: str = None, **kwargs) -> None:
|
||||
def __init__(
|
||||
self, arrow: Arrow, point_color: ParsableManimColor | None = None, **kwargs: Any
|
||||
):
|
||||
point = arrow.get_start()
|
||||
super().__init__(arrow, point, point_color=point_color, **kwargs)
|
||||
|
||||
def create_starting_mobject(self) -> Mobject:
|
||||
def create_starting_mobject(self) -> Mobject | OpenGLMobject:
|
||||
start_arrow = self.mobject.copy()
|
||||
start_arrow.scale(0, scale_tips=True, about_point=self.point)
|
||||
if self.point_color:
|
||||
|
|
@ -224,8 +240,12 @@ class SpinInFromNothing(GrowFromCenter):
|
|||
"""
|
||||
|
||||
def __init__(
|
||||
self, mobject: Mobject, angle: float = PI / 2, point_color: str = None, **kwargs
|
||||
) -> None:
|
||||
self,
|
||||
mobject: Mobject,
|
||||
angle: float = PI / 2,
|
||||
point_color: ParsableManimColor | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.angle = angle
|
||||
super().__init__(
|
||||
mobject, path_func=spiral_path(angle), point_color=point_color, **kwargs
|
||||
|
|
|
|||
|
|
@ -40,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,11 +305,13 @@ class ShowPassingFlash(ShowPartial):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, mobject: VMobject, time_width: float = 0.1, **kwargs) -> None:
|
||||
def __init__(
|
||||
self, mobject: VMobject, time_width: float = 0.1, **kwargs: Any
|
||||
) -> None:
|
||||
self.time_width = time_width
|
||||
super().__init__(mobject, remover=True, introducer=True, **kwargs)
|
||||
|
||||
def _get_bounds(self, alpha: float) -> tuple[float]:
|
||||
def _get_bounds(self, alpha: float) -> tuple[float, float]:
|
||||
tw = self.time_width
|
||||
upper = interpolate(0, 1 + tw, alpha)
|
||||
lower = upper - tw
|
||||
|
|
@ -322,7 +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(
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
@ -171,9 +189,9 @@ class MoveAlongPath(Animation):
|
|||
self,
|
||||
mobject: Mobject,
|
||||
path: VMobject,
|
||||
suspend_mobject_updating: bool | None = False,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
suspend_mobject_updating: bool = False,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.path = path
|
||||
super().__init__(
|
||||
mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ from __future__ import annotations
|
|||
__all__ = ["ChangingDecimal", "ChangeDecimalToValue"]
|
||||
|
||||
|
||||
import typing
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from manim.mobject.text.numbers import DecimalNumber
|
||||
|
||||
|
|
@ -14,12 +15,47 @@ from ..utils.bezier import interpolate
|
|||
|
||||
|
||||
class ChangingDecimal(Animation):
|
||||
"""Animate a :class:`~.DecimalNumber` to values specified by a user-supplied function.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
decimal_mob
|
||||
The :class:`~.DecimalNumber` instance to animate.
|
||||
number_update_func
|
||||
A function that returns the number to display at each point in the animation.
|
||||
suspend_mobject_updating
|
||||
If ``True``, the mobject is not updated outside this animation.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
If ``decimal_mob`` is not an instance of :class:`~.DecimalNumber`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim:: ChangingDecimalExample
|
||||
|
||||
class ChangingDecimalExample(Scene):
|
||||
def construct(self):
|
||||
number = DecimalNumber(0)
|
||||
self.add(number)
|
||||
self.play(
|
||||
ChangingDecimal(
|
||||
number,
|
||||
lambda a: 5 * a,
|
||||
run_time=3
|
||||
)
|
||||
)
|
||||
self.wait()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
decimal_mob: DecimalNumber,
|
||||
number_update_func: typing.Callable[[float], float],
|
||||
suspend_mobject_updating: bool | None = False,
|
||||
**kwargs,
|
||||
number_update_func: Callable[[float], float],
|
||||
suspend_mobject_updating: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.check_validity_of_input(decimal_mob)
|
||||
self.number_update_func = number_update_func
|
||||
|
|
@ -32,12 +68,34 @@ class ChangingDecimal(Animation):
|
|||
raise TypeError("ChangingDecimal can only take in a DecimalNumber")
|
||||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
self.mobject.set_value(self.number_update_func(self.rate_func(alpha)))
|
||||
self.mobject.set_value(self.number_update_func(self.rate_func(alpha))) # type: ignore[attr-defined]
|
||||
|
||||
|
||||
class ChangeDecimalToValue(ChangingDecimal):
|
||||
"""Animate a :class:`~.DecimalNumber` to a target value using linear interpolation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
decimal_mob
|
||||
The :class:`~.DecimalNumber` instance to animate.
|
||||
target_number
|
||||
The target value to transition to.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim:: ChangeDecimalToValueExample
|
||||
|
||||
class ChangeDecimalToValueExample(Scene):
|
||||
def construct(self):
|
||||
number = DecimalNumber(0)
|
||||
self.add(number)
|
||||
self.play(ChangeDecimalToValue(number, 10, run_time=3))
|
||||
self.wait()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, decimal_mob: DecimalNumber, target_number: int, **kwargs
|
||||
self, decimal_mob: DecimalNumber, target_number: int, **kwargs: Any
|
||||
) -> None:
|
||||
start_number = decimal_mob.number
|
||||
super().__init__(
|
||||
|
|
|
|||
|
|
@ -4,10 +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 collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ..animation.animation import Animation
|
||||
from ..animation.transform import Transform
|
||||
|
|
@ -16,22 +14,90 @@ 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):
|
||||
"""Animation that rotates a Mobject.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mobject
|
||||
The mobject to be rotated.
|
||||
angle
|
||||
The rotation angle in radians. Predefined constants such as ``DEGREES``
|
||||
can also be used to specify the angle in degrees.
|
||||
axis
|
||||
The rotation axis as a numpy vector.
|
||||
about_point
|
||||
The rotation center.
|
||||
about_edge
|
||||
If ``about_point`` is ``None``, this argument specifies
|
||||
the direction of the bounding box point to be taken as
|
||||
the rotation center.
|
||||
run_time
|
||||
The duration of the animation in seconds.
|
||||
rate_func
|
||||
The function defining the animation progress based on the relative
|
||||
runtime (see :mod:`~.rate_functions`) .
|
||||
**kwargs
|
||||
Additional keyword arguments passed to :class:`~.Animation`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. manim:: RotatingDemo
|
||||
|
||||
class RotatingDemo(Scene):
|
||||
def construct(self):
|
||||
circle = Circle(radius=1, color=BLUE)
|
||||
line = Line(start=ORIGIN, end=RIGHT)
|
||||
arrow = Arrow(start=ORIGIN, end=RIGHT, buff=0, color=GOLD)
|
||||
vg = VGroup(circle,line,arrow)
|
||||
self.add(vg)
|
||||
anim_kw = {"about_point": arrow.get_start(), "run_time": 1}
|
||||
self.play(Rotating(arrow, 180*DEGREES, **anim_kw))
|
||||
self.play(Rotating(arrow, PI, **anim_kw))
|
||||
self.play(Rotating(vg, PI, about_point=RIGHT))
|
||||
self.play(Rotating(vg, PI, axis=UP, about_point=ORIGIN))
|
||||
self.play(Rotating(vg, PI, axis=RIGHT, about_edge=UP))
|
||||
self.play(vg.animate.move_to(ORIGIN))
|
||||
|
||||
.. manim:: RotatingDifferentAxis
|
||||
|
||||
class RotatingDifferentAxis(ThreeDScene):
|
||||
def construct(self):
|
||||
axes = ThreeDAxes()
|
||||
cube = Cube()
|
||||
arrow2d = Arrow(start=[0, -1.2, 1], end=[0, 1.2, 1], color=YELLOW_E)
|
||||
cube_group = VGroup(cube,arrow2d)
|
||||
self.set_camera_orientation(gamma=0, phi=40*DEGREES, theta=40*DEGREES)
|
||||
self.add(axes, cube_group)
|
||||
play_kw = {"run_time": 1.5}
|
||||
self.play(Rotating(cube_group, PI), **play_kw)
|
||||
self.play(Rotating(cube_group, PI, axis=UP), **play_kw)
|
||||
self.play(Rotating(cube_group, 180*DEGREES, axis=RIGHT), **play_kw)
|
||||
self.wait(0.5)
|
||||
|
||||
See also
|
||||
--------
|
||||
:class:`~.Rotate`, :meth:`~.Mobject.rotate`
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
axis: np.ndarray = OUT,
|
||||
radians: np.ndarray = TAU,
|
||||
about_point: np.ndarray | None = None,
|
||||
about_edge: np.ndarray | None = None,
|
||||
angle: float = TAU,
|
||||
axis: Vector3DLike = OUT,
|
||||
about_point: Point3DLike | None = None,
|
||||
about_edge: Vector3DLike | None = None,
|
||||
run_time: float = 5,
|
||||
rate_func: Callable[[float], float] = linear,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.angle = angle
|
||||
self.axis = axis
|
||||
self.radians = radians
|
||||
self.about_point = about_point
|
||||
self.about_edge = about_edge
|
||||
super().__init__(mobject, run_time=run_time, rate_func=rate_func, **kwargs)
|
||||
|
|
@ -39,7 +105,7 @@ class Rotating(Animation):
|
|||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
self.mobject.become(self.starting_mobject)
|
||||
self.mobject.rotate(
|
||||
self.rate_func(alpha) * self.radians,
|
||||
self.rate_func(alpha) * self.angle,
|
||||
axis=self.axis,
|
||||
about_point=self.about_point,
|
||||
about_edge=self.about_edge,
|
||||
|
|
@ -80,16 +146,20 @@ class Rotate(Transform):
|
|||
Rotate(Square(side_length=0.5), angle=2*PI, rate_func=linear),
|
||||
)
|
||||
|
||||
See also
|
||||
--------
|
||||
:class:`~.Rotating`, :meth:`~.Mobject.rotate`
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
angle: float = PI,
|
||||
axis: np.ndarray = OUT,
|
||||
about_point: Sequence[float] | None = None,
|
||||
about_edge: Sequence[float] | None = None,
|
||||
**kwargs,
|
||||
axis: Vector3DLike = OUT,
|
||||
about_point: Point3DLike | None = None,
|
||||
about_edge: Vector3DLike | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
if "path_arc" not in kwargs:
|
||||
kwargs["path_arc"] = angle
|
||||
|
|
@ -103,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,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from collections.abc import Sequence
|
|||
from typing import Any
|
||||
|
||||
from manim.animation.transform import Restore
|
||||
from manim.mobject.mobject import Mobject
|
||||
|
||||
from ..constants import *
|
||||
from .composition import LaggedStart
|
||||
|
|
@ -50,7 +51,7 @@ class Broadcast(LaggedStart):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
mobject,
|
||||
mobject: Mobject,
|
||||
focal_point: Sequence[float] = ORIGIN,
|
||||
n_mobs: int = 5,
|
||||
initial_opacity: float = 1,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ from __future__ import annotations
|
|||
|
||||
import inspect
|
||||
import types
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from numpy import piecewise
|
||||
|
||||
|
|
|
|||
|
|
@ -28,11 +28,12 @@ __all__ = [
|
|||
|
||||
import inspect
|
||||
import types
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.data_structures import MethodWithArgs
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLGroup, OpenGLMobject
|
||||
|
||||
from .. import config
|
||||
|
|
@ -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):
|
||||
|
|
@ -123,6 +126,10 @@ class Transform(Animation):
|
|||
|
||||
self.play(*anims, run_time=2)
|
||||
self.wait()
|
||||
|
||||
See also
|
||||
--------
|
||||
:class:`~.ReplacementTransform`, :meth:`~.Mobject.interpolate`, :meth:`~.Mobject.align_data`
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -132,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
|
||||
|
|
@ -204,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
|
||||
|
|
@ -229,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,
|
||||
|
|
@ -298,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)
|
||||
|
|
@ -434,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()
|
||||
|
||||
|
||||
|
|
@ -729,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?
|
||||
|
|
@ -829,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
|
||||
|
|
@ -923,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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
@ -96,7 +97,6 @@ class TransformMatchingAbstractBase(AnimationGroup):
|
|||
# target_map
|
||||
transform_source = group_type()
|
||||
transform_target = group_type()
|
||||
kwargs["final_alpha_value"] = 0
|
||||
for key in set(source_map).intersection(target_map):
|
||||
transform_source.add(source_map[key])
|
||||
transform_target.add(target_map[key])
|
||||
|
|
@ -142,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:
|
||||
|
|
@ -150,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.")
|
||||
|
||||
|
||||
|
|
@ -206,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,
|
||||
|
|
@ -226,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
|
||||
|
||||
|
|
@ -269,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,
|
||||
|
|
@ -294,4 +297,5 @@ class TransformMatchingTex(TransformMatchingAbstractBase):
|
|||
|
||||
@staticmethod
|
||||
def get_mobject_key(mobject: Mobject) -> str:
|
||||
assert isinstance(mobject, MathTexPart)
|
||||
return mobject.tex_string
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ __all__ = ["UpdateFromFunc", "UpdateFromAlphaFunc", "MaintainPositionRelativeTo"
|
|||
|
||||
|
||||
import operator as op
|
||||
import typing
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from manim.animation.animation import Animation
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.mobject import Mobject
|
||||
|
||||
|
||||
|
|
@ -24,9 +25,9 @@ class UpdateFromFunc(Animation):
|
|||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
update_function: typing.Callable[[Mobject], typing.Any],
|
||||
update_function: Callable[[Mobject], Any],
|
||||
suspend_mobject_updating: bool = False,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.update_function = update_function
|
||||
super().__init__(
|
||||
|
|
@ -34,16 +35,18 @@ class UpdateFromFunc(Animation):
|
|||
)
|
||||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
self.update_function(self.mobject)
|
||||
self.update_function(self.mobject) # type: ignore[arg-type]
|
||||
|
||||
|
||||
class UpdateFromAlphaFunc(UpdateFromFunc):
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
self.update_function(self.mobject, self.rate_func(alpha))
|
||||
self.update_function(self.mobject, self.rate_func(alpha)) # type: ignore[call-arg, arg-type]
|
||||
|
||||
|
||||
class MaintainPositionRelativeTo(Animation):
|
||||
def __init__(self, mobject: Mobject, tracked_mobject: Mobject, **kwargs) -> None:
|
||||
def __init__(
|
||||
self, mobject: Mobject, tracked_mobject: Mobject, **kwargs: Any
|
||||
) -> None:
|
||||
self.tracked_mobject = tracked_mobject
|
||||
self.diff = op.sub(
|
||||
mobject.get_center(),
|
||||
|
|
|
|||
|
|
@ -8,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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""A camera that allows mapping between objects."""
|
||||
"""A camera module that supports spatial mapping between objects for distortion effects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -17,8 +17,16 @@ from ..utils.config_ops import DictAsObject
|
|||
|
||||
|
||||
class MappingCamera(Camera):
|
||||
"""Camera object that allows mapping
|
||||
between objects.
|
||||
"""Parameters
|
||||
----------
|
||||
mapping_func : callable
|
||||
Function to map 3D points to new 3D points (identity by default).
|
||||
min_num_curves : int
|
||||
Minimum number of curves for VMobjects to avoid visual glitches.
|
||||
allow_object_intrusion : bool
|
||||
If True, modifies original mobjects; else works on copies.
|
||||
kwargs : dict
|
||||
Additional arguments passed to Camera base class.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -34,12 +42,18 @@ class MappingCamera(Camera):
|
|||
super().__init__(**kwargs)
|
||||
|
||||
def points_to_pixel_coords(self, mobject, points):
|
||||
# Map points with custom function before converting to pixels
|
||||
return super().points_to_pixel_coords(
|
||||
mobject,
|
||||
np.apply_along_axis(self.mapping_func, 1, points),
|
||||
)
|
||||
|
||||
def capture_mobjects(self, mobjects, **kwargs):
|
||||
"""Capture mobjects for rendering after applying the spatial mapping.
|
||||
|
||||
Copies mobjects unless intrusion is allowed, and ensures
|
||||
vector objects have enough curves for smooth distortion.
|
||||
"""
|
||||
mobjects = self.get_mobjects_to_display(mobjects, **kwargs)
|
||||
if self.allow_object_intrusion:
|
||||
mobject_copies = mobjects
|
||||
|
|
@ -67,6 +81,13 @@ class MappingCamera(Camera):
|
|||
|
||||
# TODO, the classes below should likely be deleted
|
||||
class OldMultiCamera(Camera):
|
||||
"""Parameters
|
||||
----------
|
||||
cameras_with_start_positions : tuple
|
||||
Tuples of (Camera, (start_y, start_x)) indicating camera and
|
||||
its pixel offset on the final frame.
|
||||
"""
|
||||
|
||||
def __init__(self, *cameras_with_start_positions, **kwargs):
|
||||
self.shifted_cameras = [
|
||||
DictAsObject(
|
||||
|
|
@ -125,6 +146,15 @@ class OldMultiCamera(Camera):
|
|||
|
||||
|
||||
class SplitScreenCamera(OldMultiCamera):
|
||||
"""Initializes a split screen camera setup with two side-by-side cameras.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
left_camera : Camera
|
||||
right_camera : Camera
|
||||
kwargs : dict
|
||||
"""
|
||||
|
||||
def __init__(self, left_camera, right_camera, **kwargs):
|
||||
Camera.__init__(self, **kwargs) # to set attributes such as pixel_width
|
||||
self.left_camera = left_camera
|
||||
|
|
|
|||
|
|
@ -1,45 +1,48 @@
|
|||
"""A camera able to move through a scene.
|
||||
"""Defines the MovingCamera class, a camera that can pan and zoom through a scene.
|
||||
|
||||
.. SEEALSO::
|
||||
|
||||
:mod:`.moving_camera_scene`
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["MovingCamera"]
|
||||
|
||||
import numpy as np
|
||||
from collections.abc import Iterable
|
||||
from typing import Any, 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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ from __future__ import annotations
|
|||
__all__ = ["ThreeDCamera"]
|
||||
|
||||
|
||||
from typing import Callable
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -16,7 +17,15 @@ from manim.mobject.three_d.three_d_utils import (
|
|||
get_3d_vmob_start_corner,
|
||||
get_3d_vmob_start_corner_unit_normal,
|
||||
)
|
||||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
from manim.mobject.value_tracker import ValueTracker
|
||||
from manim.typing import (
|
||||
FloatRGBA_Array,
|
||||
MatrixMN,
|
||||
Point3D,
|
||||
Point3D_Array,
|
||||
Point3DLike,
|
||||
)
|
||||
|
||||
from .. import config
|
||||
from ..camera.camera import Camera
|
||||
|
|
@ -30,17 +39,17 @@ from ..utils.space_ops import rotation_about_z, rotation_matrix
|
|||
class ThreeDCamera(Camera):
|
||||
def __init__(
|
||||
self,
|
||||
focal_distance=20.0,
|
||||
shading_factor=0.2,
|
||||
default_distance=5.0,
|
||||
light_source_start_point=9 * DOWN + 7 * LEFT + 10 * OUT,
|
||||
should_apply_shading=True,
|
||||
exponential_projection=False,
|
||||
phi=0,
|
||||
theta=-90 * DEGREES,
|
||||
gamma=0,
|
||||
zoom=1,
|
||||
**kwargs,
|
||||
focal_distance: float = 20.0,
|
||||
shading_factor: float = 0.2,
|
||||
default_distance: float = 5.0,
|
||||
light_source_start_point: Point3DLike = 9 * DOWN + 7 * LEFT + 10 * OUT,
|
||||
should_apply_shading: bool = True,
|
||||
exponential_projection: bool = False,
|
||||
phi: float = 0,
|
||||
theta: float = -90 * DEGREES,
|
||||
gamma: float = 0,
|
||||
zoom: float = 1,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Initializes the ThreeDCamera
|
||||
|
||||
|
|
@ -68,23 +77,23 @@ class ThreeDCamera(Camera):
|
|||
self.focal_distance_tracker = ValueTracker(self.focal_distance)
|
||||
self.gamma_tracker = ValueTracker(self.gamma)
|
||||
self.zoom_tracker = ValueTracker(self.zoom)
|
||||
self.fixed_orientation_mobjects = {}
|
||||
self.fixed_in_frame_mobjects = set()
|
||||
self.fixed_orientation_mobjects: dict[Mobject, Callable[[], Point3D]] = {}
|
||||
self.fixed_in_frame_mobjects: set[Mobject] = set()
|
||||
self.reset_rotation_matrix()
|
||||
|
||||
@property
|
||||
def frame_center(self):
|
||||
def frame_center(self) -> Point3D:
|
||||
return self._frame_center.points[0]
|
||||
|
||||
@frame_center.setter
|
||||
def frame_center(self, point):
|
||||
def frame_center(self, point: Point3DLike) -> None:
|
||||
self._frame_center.move_to(point)
|
||||
|
||||
def capture_mobjects(self, mobjects, **kwargs):
|
||||
def capture_mobjects(self, mobjects: Iterable[Mobject], **kwargs: Any) -> None:
|
||||
self.reset_rotation_matrix()
|
||||
super().capture_mobjects(mobjects, **kwargs)
|
||||
|
||||
def get_value_trackers(self):
|
||||
def get_value_trackers(self) -> list[ValueTracker]:
|
||||
"""A list of :class:`ValueTrackers <.ValueTracker>` of phi, theta, focal_distance,
|
||||
gamma and zoom.
|
||||
|
||||
|
|
@ -101,7 +110,9 @@ class ThreeDCamera(Camera):
|
|||
self.zoom_tracker,
|
||||
]
|
||||
|
||||
def modified_rgbas(self, vmobject, rgbas):
|
||||
def modified_rgbas(
|
||||
self, vmobject: VMobject, rgbas: FloatRGBA_Array
|
||||
) -> FloatRGBA_Array:
|
||||
if not self.should_apply_shading:
|
||||
return rgbas
|
||||
if vmobject.shade_in_3d and (vmobject.get_num_points() > 0):
|
||||
|
|
@ -127,28 +138,33 @@ class ThreeDCamera(Camera):
|
|||
|
||||
def get_stroke_rgbas(
|
||||
self,
|
||||
vmobject,
|
||||
background=False,
|
||||
): # NOTE : DocStrings From parent
|
||||
vmobject: VMobject,
|
||||
background: bool = False,
|
||||
) -> FloatRGBA_Array: # NOTE : DocStrings From parent
|
||||
return self.modified_rgbas(vmobject, vmobject.get_stroke_rgbas(background))
|
||||
|
||||
def get_fill_rgbas(self, vmobject): # NOTE : DocStrings From parent
|
||||
def get_fill_rgbas(
|
||||
self, vmobject: VMobject
|
||||
) -> FloatRGBA_Array: # NOTE : DocStrings From parent
|
||||
return self.modified_rgbas(vmobject, vmobject.get_fill_rgbas())
|
||||
|
||||
def get_mobjects_to_display(self, *args, **kwargs): # NOTE : DocStrings From parent
|
||||
def get_mobjects_to_display(
|
||||
self, *args: Any, **kwargs: Any
|
||||
) -> list[Mobject]: # NOTE : DocStrings From parent
|
||||
mobjects = super().get_mobjects_to_display(*args, **kwargs)
|
||||
rot_matrix = self.get_rotation_matrix()
|
||||
|
||||
def z_key(mob):
|
||||
def z_key(mob: Mobject) -> float:
|
||||
if not (hasattr(mob, "shade_in_3d") and mob.shade_in_3d):
|
||||
return np.inf
|
||||
return np.inf # type: ignore[no-any-return]
|
||||
# Assign a number to a three dimensional mobjects
|
||||
# based on how close it is to the camera
|
||||
return np.dot(mob.get_z_index_reference_point(), rot_matrix.T)[2]
|
||||
distance: float = np.dot(mob.get_z_index_reference_point(), rot_matrix.T)[2]
|
||||
return distance
|
||||
|
||||
return sorted(mobjects, key=z_key)
|
||||
|
||||
def get_phi(self):
|
||||
def get_phi(self) -> float:
|
||||
"""Returns the Polar angle (the angle off Z_AXIS) phi.
|
||||
|
||||
Returns
|
||||
|
|
@ -158,7 +174,7 @@ class ThreeDCamera(Camera):
|
|||
"""
|
||||
return self.phi_tracker.get_value()
|
||||
|
||||
def get_theta(self):
|
||||
def get_theta(self) -> float:
|
||||
"""Returns the Azimuthal i.e the angle that spins the camera around the Z_AXIS.
|
||||
|
||||
Returns
|
||||
|
|
@ -168,7 +184,7 @@ class ThreeDCamera(Camera):
|
|||
"""
|
||||
return self.theta_tracker.get_value()
|
||||
|
||||
def get_focal_distance(self):
|
||||
def get_focal_distance(self) -> float:
|
||||
"""Returns focal_distance of the Camera.
|
||||
|
||||
Returns
|
||||
|
|
@ -178,7 +194,7 @@ class ThreeDCamera(Camera):
|
|||
"""
|
||||
return self.focal_distance_tracker.get_value()
|
||||
|
||||
def get_gamma(self):
|
||||
def get_gamma(self) -> float:
|
||||
"""Returns the rotation of the camera about the vector from the ORIGIN to the Camera.
|
||||
|
||||
Returns
|
||||
|
|
@ -189,7 +205,7 @@ class ThreeDCamera(Camera):
|
|||
"""
|
||||
return self.gamma_tracker.get_value()
|
||||
|
||||
def get_zoom(self):
|
||||
def get_zoom(self) -> float:
|
||||
"""Returns the zoom amount of the camera.
|
||||
|
||||
Returns
|
||||
|
|
@ -199,7 +215,7 @@ class ThreeDCamera(Camera):
|
|||
"""
|
||||
return self.zoom_tracker.get_value()
|
||||
|
||||
def set_phi(self, value: float):
|
||||
def set_phi(self, value: float) -> None:
|
||||
"""Sets the polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians.
|
||||
|
||||
Parameters
|
||||
|
|
@ -209,7 +225,7 @@ class ThreeDCamera(Camera):
|
|||
"""
|
||||
self.phi_tracker.set_value(value)
|
||||
|
||||
def set_theta(self, value: float):
|
||||
def set_theta(self, value: float) -> None:
|
||||
"""Sets the azimuthal angle i.e the angle that spins the camera around Z_AXIS in radians.
|
||||
|
||||
Parameters
|
||||
|
|
@ -219,7 +235,7 @@ class ThreeDCamera(Camera):
|
|||
"""
|
||||
self.theta_tracker.set_value(value)
|
||||
|
||||
def set_focal_distance(self, value: float):
|
||||
def set_focal_distance(self, value: float) -> None:
|
||||
"""Sets the focal_distance of the Camera.
|
||||
|
||||
Parameters
|
||||
|
|
@ -229,7 +245,7 @@ class ThreeDCamera(Camera):
|
|||
"""
|
||||
self.focal_distance_tracker.set_value(value)
|
||||
|
||||
def set_gamma(self, value: float):
|
||||
def set_gamma(self, value: float) -> None:
|
||||
"""Sets the angle of rotation of the camera about the vector from the ORIGIN to the Camera.
|
||||
|
||||
Parameters
|
||||
|
|
@ -239,7 +255,7 @@ class ThreeDCamera(Camera):
|
|||
"""
|
||||
self.gamma_tracker.set_value(value)
|
||||
|
||||
def set_zoom(self, value: float):
|
||||
def set_zoom(self, value: float) -> None:
|
||||
"""Sets the zoom amount of the camera.
|
||||
|
||||
Parameters
|
||||
|
|
@ -249,13 +265,13 @@ class ThreeDCamera(Camera):
|
|||
"""
|
||||
self.zoom_tracker.set_value(value)
|
||||
|
||||
def reset_rotation_matrix(self):
|
||||
def reset_rotation_matrix(self) -> None:
|
||||
"""Sets the value of self.rotation_matrix to
|
||||
the matrix corresponding to the current position of the camera
|
||||
"""
|
||||
self.rotation_matrix = self.generate_rotation_matrix()
|
||||
|
||||
def get_rotation_matrix(self):
|
||||
def get_rotation_matrix(self) -> MatrixMN:
|
||||
"""Returns the matrix corresponding to the current position of the camera.
|
||||
|
||||
Returns
|
||||
|
|
@ -265,7 +281,7 @@ class ThreeDCamera(Camera):
|
|||
"""
|
||||
return self.rotation_matrix
|
||||
|
||||
def generate_rotation_matrix(self):
|
||||
def generate_rotation_matrix(self) -> MatrixMN:
|
||||
"""Generates a rotation matrix based off the current position of the camera.
|
||||
|
||||
Returns
|
||||
|
|
@ -286,7 +302,7 @@ class ThreeDCamera(Camera):
|
|||
result = np.dot(matrix, result)
|
||||
return result
|
||||
|
||||
def project_points(self, points: np.ndarray | list):
|
||||
def project_points(self, points: Point3D_Array) -> Point3D_Array:
|
||||
"""Applies the current rotation_matrix as a projection
|
||||
matrix to the passed array of points.
|
||||
|
||||
|
|
@ -323,7 +339,7 @@ class ThreeDCamera(Camera):
|
|||
points[:, i] *= factor * zoom
|
||||
return points
|
||||
|
||||
def project_point(self, point: list | np.ndarray):
|
||||
def project_point(self, point: Point3D) -> Point3D:
|
||||
"""Applies the current rotation_matrix as a projection
|
||||
matrix to the passed point.
|
||||
|
||||
|
|
@ -341,9 +357,9 @@ class ThreeDCamera(Camera):
|
|||
|
||||
def transform_points_pre_display(
|
||||
self,
|
||||
mobject,
|
||||
points,
|
||||
): # TODO: Write Docstrings for this Method.
|
||||
mobject: Mobject,
|
||||
points: Point3D_Array,
|
||||
) -> Point3D_Array: # TODO: Write Docstrings for this Method.
|
||||
points = super().transform_points_pre_display(mobject, points)
|
||||
fixed_orientation = mobject in self.fixed_orientation_mobjects
|
||||
fixed_in_frame = mobject in self.fixed_in_frame_mobjects
|
||||
|
|
@ -362,8 +378,8 @@ class ThreeDCamera(Camera):
|
|||
self,
|
||||
*mobjects: Mobject,
|
||||
use_static_center_func: bool = False,
|
||||
center_func: Callable[[], np.ndarray] | None = None,
|
||||
):
|
||||
center_func: Callable[[], Point3D] | None = None,
|
||||
) -> None:
|
||||
"""This method allows the mobject to have a fixed orientation,
|
||||
even when the camera moves around.
|
||||
E.G If it was passed through this method, facing the camera, it
|
||||
|
|
@ -384,7 +400,7 @@ class ThreeDCamera(Camera):
|
|||
|
||||
# This prevents the computation of mobject.get_center
|
||||
# every single time a projection happens
|
||||
def get_static_center_func(mobject):
|
||||
def get_static_center_func(mobject: Mobject) -> Callable[[], Point3D]:
|
||||
point = mobject.get_center()
|
||||
return lambda: point
|
||||
|
||||
|
|
@ -398,7 +414,7 @@ class ThreeDCamera(Camera):
|
|||
for submob in mobject.get_family():
|
||||
self.fixed_orientation_mobjects[submob] = func
|
||||
|
||||
def add_fixed_in_frame_mobjects(self, *mobjects: Mobject):
|
||||
def add_fixed_in_frame_mobjects(self, *mobjects: Mobject) -> None:
|
||||
"""This method allows the mobject to have a fixed position,
|
||||
even when the camera moves around.
|
||||
E.G If it was passed through this method, at the top of the frame, it
|
||||
|
|
@ -414,7 +430,7 @@ class ThreeDCamera(Camera):
|
|||
for mobject in extract_mobject_family_members(mobjects):
|
||||
self.fixed_in_frame_mobjects.add(mobject)
|
||||
|
||||
def remove_fixed_orientation_mobjects(self, *mobjects: Mobject):
|
||||
def remove_fixed_orientation_mobjects(self, *mobjects: Mobject) -> None:
|
||||
"""If a mobject was fixed in its orientation by passing it through
|
||||
:meth:`.add_fixed_orientation_mobjects`, then this undoes that fixing.
|
||||
The Mobject will no longer have a fixed orientation.
|
||||
|
|
@ -428,7 +444,7 @@ class ThreeDCamera(Camera):
|
|||
if mobject in self.fixed_orientation_mobjects:
|
||||
del self.fixed_orientation_mobjects[mobject]
|
||||
|
||||
def remove_fixed_in_frame_mobjects(self, *mobjects: Mobject):
|
||||
def remove_fixed_in_frame_mobjects(self, *mobjects: Mobject) -> None:
|
||||
"""If a mobject was fixed in frame by passing it through
|
||||
:meth:`.add_fixed_in_frame_mobjects`, then this undoes that fixing.
|
||||
The Mobject will no longer be fixed in frame.
|
||||
|
|
|
|||
|
|
@ -267,6 +267,12 @@ modify write_cfg_subcmd_input to account for it.""",
|
|||
|
||||
@cfg.command(context_settings=cli_ctx_settings)
|
||||
def show() -> None:
|
||||
console.print("CONFIG FILES READ", style="bold green underline")
|
||||
for path in config_file_paths():
|
||||
if path.exists():
|
||||
console.print(f"{path}")
|
||||
console.print()
|
||||
|
||||
parser = make_config_parser()
|
||||
rich_non_style_entries = [a.replace(".", "_") for a in RICH_NON_STYLE_ENTRIES]
|
||||
for category in parser:
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ from __future__ import annotations
|
|||
|
||||
import os
|
||||
import shutil
|
||||
from typing import Callable, Protocol, cast
|
||||
from collections.abc import Callable
|
||||
from typing import Protocol, cast
|
||||
|
||||
__all__ = ["HEALTH_CHECKS"]
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ In particular, this class is what allows ``manim`` to act as ``manim render``.
|
|||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import cloup
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ CFG_DEFAULTS = {
|
|||
"background_color": "BLACK",
|
||||
"background_opacity": 1,
|
||||
"scene_names": "Default",
|
||||
"resolution": (854, 480),
|
||||
"resolution": (1920, 1080),
|
||||
}
|
||||
|
||||
__all__ = ["select_resolution", "update_cfg", "project", "scene"]
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
31
manim/data_structures.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
"""Data classes and other necessary data structures for use in Manim."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from types import MethodType
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class MethodWithArgs:
|
||||
"""Object containing a :attr:`method` which is intended to be called later
|
||||
with the positional arguments :attr:`args` and the keyword arguments
|
||||
:attr:`kwargs`.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
method : MethodType
|
||||
A callable representing a method of some class.
|
||||
args : Iterable[Any]
|
||||
Positional arguments for :attr:`method`.
|
||||
kwargs : dict[str, Any]
|
||||
Keyword arguments for :attr:`method`.
|
||||
"""
|
||||
|
||||
__slots__ = ["method", "args", "kwargs"]
|
||||
|
||||
method: MethodType
|
||||
args: Iterable[Any]
|
||||
kwargs: dict[str, Any]
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import dearpygui.dearpygui as dpg
|
||||
|
||||
dearpygui_imported = True
|
||||
except ImportError:
|
||||
dearpygui_imported = False
|
||||
|
||||
|
||||
from .. import __version__, config
|
||||
from ..utils.module_ops import scene_classes_from_file
|
||||
|
||||
__all__ = ["configure_pygui"]
|
||||
|
||||
if dearpygui_imported:
|
||||
dpg.create_context()
|
||||
window = dpg.generate_uuid()
|
||||
|
||||
|
||||
def configure_pygui(renderer, widgets, update=True):
|
||||
if not dearpygui_imported:
|
||||
raise RuntimeError("Attempted to use DearPyGUI when it isn't imported.")
|
||||
if update:
|
||||
dpg.delete_item(window)
|
||||
else:
|
||||
dpg.create_viewport()
|
||||
dpg.setup_dearpygui()
|
||||
dpg.show_viewport()
|
||||
|
||||
dpg.set_viewport_title(title=f"Manim Community v{__version__}")
|
||||
dpg.set_viewport_width(1015)
|
||||
dpg.set_viewport_height(540)
|
||||
|
||||
def rerun_callback(sender, data):
|
||||
renderer.scene.queue.put(("rerun_gui", [], {}))
|
||||
|
||||
def continue_callback(sender, data):
|
||||
renderer.scene.queue.put(("exit_gui", [], {}))
|
||||
|
||||
def scene_selection_callback(sender, data):
|
||||
config["scene_names"] = (dpg.get_value(sender),)
|
||||
renderer.scene.queue.put(("rerun_gui", [], {}))
|
||||
|
||||
scene_classes = scene_classes_from_file(Path(config["input_file"]), full_list=True)
|
||||
scene_names = [scene_class.__name__ for scene_class in scene_classes]
|
||||
|
||||
with dpg.window(
|
||||
id=window,
|
||||
label="Manim GUI",
|
||||
pos=[config["gui_location"][0], config["gui_location"][1]],
|
||||
width=1000,
|
||||
height=500,
|
||||
):
|
||||
dpg.set_global_font_scale(2)
|
||||
dpg.add_button(label="Rerun", callback=rerun_callback)
|
||||
dpg.add_button(label="Continue", callback=continue_callback)
|
||||
dpg.add_combo(
|
||||
label="Selected scene",
|
||||
items=scene_names,
|
||||
callback=scene_selection_callback,
|
||||
default_value=config["scene_names"][0],
|
||||
)
|
||||
dpg.add_separator()
|
||||
if len(widgets) != 0:
|
||||
with dpg.collapsing_header(
|
||||
label=f"{config['scene_names'][0]} widgets",
|
||||
default_open=True,
|
||||
):
|
||||
for widget_config in widgets:
|
||||
widget_config_copy = widget_config.copy()
|
||||
name = widget_config_copy["name"]
|
||||
widget = widget_config_copy["widget"]
|
||||
if widget != "separator":
|
||||
del widget_config_copy["name"]
|
||||
del widget_config_copy["widget"]
|
||||
getattr(dpg, f"add_{widget}")(label=name, **widget_config_copy)
|
||||
else:
|
||||
dpg.add_separator()
|
||||
|
||||
if not update:
|
||||
dpg.start_dearpygui()
|
||||
|
|
@ -8,17 +8,21 @@ __all__ = [
|
|||
]
|
||||
|
||||
|
||||
from typing import Any
|
||||
|
||||
from manim.mobject.geometry.polygram import Rectangle
|
||||
|
||||
from .. import config
|
||||
|
||||
|
||||
class ScreenRectangle(Rectangle):
|
||||
def __init__(self, aspect_ratio=16.0 / 9.0, height=4, **kwargs):
|
||||
def __init__(
|
||||
self, aspect_ratio: float = 16.0 / 9.0, height: float = 4, **kwargs: Any
|
||||
) -> None:
|
||||
super().__init__(width=aspect_ratio * height, height=height, **kwargs)
|
||||
|
||||
@property
|
||||
def aspect_ratio(self):
|
||||
def aspect_ratio(self) -> float:
|
||||
"""The aspect ratio.
|
||||
|
||||
When set, the width is stretched to accommodate
|
||||
|
|
@ -27,11 +31,11 @@ class ScreenRectangle(Rectangle):
|
|||
return self.width / self.height
|
||||
|
||||
@aspect_ratio.setter
|
||||
def aspect_ratio(self, value):
|
||||
def aspect_ratio(self, value: float) -> None:
|
||||
self.stretch_to_fit_width(value * self.height)
|
||||
|
||||
|
||||
class FullScreenRectangle(ScreenRectangle):
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.height = config["frame_height"]
|
||||
|
|
|
|||
|
|
@ -40,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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -15,16 +15,15 @@ from manim.mobject.geometry.shape_matchers import (
|
|||
BackgroundRectangle,
|
||||
SurroundingRectangle,
|
||||
)
|
||||
from manim.mobject.text.tex_mobject import MathTex, Tex
|
||||
from manim.mobject.text.tex_mobject import MathTex
|
||||
from manim.mobject.text.text_mobject import Text
|
||||
from manim.mobject.text.typst_mobject import Typst
|
||||
from manim.mobject.types.vectorized_mobject import VGroup
|
||||
from manim.utils.color import WHITE
|
||||
from manim.utils.polylabel import polylabel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from manim.typing import Point3DLike_Array
|
||||
from manim.typing import ManimTextLabel, Point3DLike_Array
|
||||
|
||||
|
||||
class Label(VGroup):
|
||||
|
|
@ -63,7 +62,7 @@ class Label(VGroup):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
label: str | Tex | MathTex | Text,
|
||||
label: str | ManimTextLabel,
|
||||
label_config: dict[str, Any] | None = None,
|
||||
box_config: dict[str, Any] | None = None,
|
||||
frame_config: dict[str, Any] | None = None,
|
||||
|
|
@ -96,13 +95,15 @@ class Label(VGroup):
|
|||
frame_config = default_frame_config | (frame_config or {})
|
||||
|
||||
# Determine the type of label and instantiate the appropriate object
|
||||
self.rendered_label: MathTex | Tex | Text
|
||||
self.rendered_label: ManimTextLabel
|
||||
if isinstance(label, str):
|
||||
self.rendered_label = MathTex(label, **label_config)
|
||||
elif isinstance(label, (MathTex, Tex, Text)):
|
||||
elif isinstance(label, (MathTex, Text, Typst)):
|
||||
self.rendered_label = label
|
||||
else:
|
||||
raise TypeError("Unsupported label type. Must be MathTex, Tex, or Text.")
|
||||
raise TypeError(
|
||||
"Unsupported label type. Must be MathTex, Tex, Text, Typst, or TypstMath."
|
||||
)
|
||||
|
||||
# Add a background box
|
||||
self.background_rect = BackgroundRectangle(self.rendered_label, **box_config)
|
||||
|
|
@ -157,7 +158,7 @@ class LabeledLine(Line):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
label: str | Tex | MathTex | Text,
|
||||
label: str | ManimTextLabel,
|
||||
label_position: float = 0.5,
|
||||
label_config: dict[str, Any] | None = None,
|
||||
box_config: dict[str, Any] | None = None,
|
||||
|
|
@ -345,7 +346,7 @@ class LabeledPolygram(Polygram):
|
|||
def __init__(
|
||||
self,
|
||||
*vertex_groups: Point3DLike_Array,
|
||||
label: str | Tex | MathTex | Text,
|
||||
label: str | ManimTextLabel,
|
||||
precision: float = 0.01,
|
||||
label_config: dict[str, Any] | None = None,
|
||||
box_config: dict[str, Any] | None = None,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -65,12 +63,41 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class Line(TipableVMobject):
|
||||
"""A straight or curved line segment between two points or mobjects.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
start
|
||||
The starting point or Mobject of the line.
|
||||
end
|
||||
The ending point or Mobject of the line.
|
||||
buff
|
||||
The distance to shorten the line from both ends.
|
||||
path_arc
|
||||
If nonzero, the line will be curved into an arc with this angle (in radians).
|
||||
kwargs
|
||||
Additional arguments to be passed to :class:`TipableVMobject`
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. manim:: LineExample
|
||||
:save_last_frame:
|
||||
|
||||
class LineExample(Scene):
|
||||
def construct(self):
|
||||
line1 = Line(LEFT*2, RIGHT*2)
|
||||
line2 = Line(LEFT*2, RIGHT*2, buff=0.5)
|
||||
line3 = Line(LEFT*2, RIGHT*2, path_arc=PI/2)
|
||||
grp = VGroup(line1,line2,line3).arrange(DOWN, buff=2)
|
||||
self.add(grp)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
start: Point3DLike | Mobject = LEFT,
|
||||
end: Point3DLike | Mobject = RIGHT,
|
||||
buff: float = 0,
|
||||
path_arc: float | None = None,
|
||||
path_arc: float = 0,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.dim = 3
|
||||
|
|
@ -78,14 +105,13 @@ class Line(TipableVMobject):
|
|||
self.path_arc = path_arc
|
||||
self._set_start_and_end_attrs(start, end)
|
||||
super().__init__(**kwargs)
|
||||
# TODO: Deal with the situation where path_arc is None
|
||||
|
||||
def generate_points(self) -> None:
|
||||
self.set_points_by_ends(
|
||||
start=self.start,
|
||||
end=self.end,
|
||||
buff=self.buff,
|
||||
path_arc=self.path_arc, # type: ignore[arg-type]
|
||||
path_arc=self.path_arc,
|
||||
)
|
||||
|
||||
def set_points_by_ends(
|
||||
|
|
@ -112,9 +138,6 @@ class Line(TipableVMobject):
|
|||
"""
|
||||
self._set_start_and_end_attrs(start, end)
|
||||
if path_arc:
|
||||
# self.path_arc could potentially be None, which is not accepted
|
||||
# as parameter.
|
||||
assert self.path_arc is not None
|
||||
arc = ArcBetweenPoints(self.start, self.end, angle=self.path_arc)
|
||||
self.set_points(arc.points)
|
||||
else:
|
||||
|
|
@ -122,19 +145,17 @@ 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:
|
||||
if buff <= 0:
|
||||
return
|
||||
#
|
||||
length = self.get_length() if self.path_arc == 0 else self.get_arc_length()
|
||||
#
|
||||
if length < 2 * buff:
|
||||
return
|
||||
buff_proportion = buff / length
|
||||
self.pointwise_become_partial(self, buff_proportion, 1 - buff_proportion)
|
||||
return
|
||||
|
||||
def _set_start_and_end_attrs(
|
||||
self, start: Point3DLike | Mobject, end: Point3DLike | Mobject
|
||||
|
|
@ -153,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.
|
||||
|
||||
|
|
@ -627,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:
|
||||
|
|
@ -716,7 +739,7 @@ class Vector(Arrow):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
direction: Point2DLike | Point3DLike = RIGHT,
|
||||
direction: Vector2DLike | Vector3DLike = RIGHT,
|
||||
buff: float = 0,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ __all__ = [
|
|||
|
||||
|
||||
from math import ceil
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -32,13 +32,11 @@ 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 (
|
||||
ManimFloat,
|
||||
Point3D,
|
||||
Point3D_Array,
|
||||
Point3DLike,
|
||||
|
|
@ -122,39 +120,47 @@ class Polygram(VMobject, metaclass=ConvertToOpenGL):
|
|||
"""
|
||||
return self.get_start_anchors()
|
||||
|
||||
def get_vertex_groups(self) -> npt.NDArray[ManimFloat]:
|
||||
def get_vertex_groups(self) -> list[Point3D_Array]:
|
||||
"""Gets the vertex groups of the :class:`Polygram`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`numpy.ndarray`
|
||||
The vertex groups of the :class:`Polygram`.
|
||||
list[Point3D_Array]
|
||||
The list of vertex groups of the :class:`Polygram`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
::
|
||||
|
||||
>>> poly = Polygram([ORIGIN, RIGHT, UP], [LEFT, LEFT + UP, 2 * LEFT])
|
||||
>>> poly.get_vertex_groups()
|
||||
array([[[ 0., 0., 0.],
|
||||
[ 1., 0., 0.],
|
||||
[ 0., 1., 0.]],
|
||||
<BLANKLINE>
|
||||
[[-1., 0., 0.],
|
||||
[-1., 1., 0.],
|
||||
[-2., 0., 0.]]])
|
||||
>>> poly = Polygram([ORIGIN, RIGHT, UP, LEFT + UP], [LEFT, LEFT + UP, 2 * LEFT])
|
||||
>>> groups = poly.get_vertex_groups()
|
||||
>>> len(groups)
|
||||
2
|
||||
>>> groups[0]
|
||||
array([[ 0., 0., 0.],
|
||||
[ 1., 0., 0.],
|
||||
[ 0., 1., 0.],
|
||||
[-1., 1., 0.]])
|
||||
>>> groups[1]
|
||||
array([[-1., 0., 0.],
|
||||
[-1., 1., 0.],
|
||||
[-2., 0., 0.]])
|
||||
"""
|
||||
vertex_groups = []
|
||||
|
||||
# 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]):
|
||||
vertex_groups.append(group)
|
||||
vertex_groups.append(np.array(group))
|
||||
group = []
|
||||
|
||||
return np.array(vertex_groups)
|
||||
return vertex_groups
|
||||
|
||||
def round_corners(
|
||||
self,
|
||||
|
|
@ -223,18 +229,18 @@ class Polygram(VMobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
new_points: list[Point3D] = []
|
||||
|
||||
for vertices in self.get_vertex_groups():
|
||||
for vertex_group in self.get_vertex_groups():
|
||||
arcs = []
|
||||
|
||||
# Repeat the radius list as necessary in order to provide a radius
|
||||
# for each vertex.
|
||||
if isinstance(radius, (int, float)):
|
||||
radius_list = [radius] * len(vertices)
|
||||
radius_list = [radius] * len(vertex_group)
|
||||
else:
|
||||
radius_list = radius * ceil(len(vertices) / len(radius))
|
||||
radius_list = radius * ceil(len(vertex_group) / len(radius))
|
||||
|
||||
for currentRadius, (v1, v2, v3) in zip(
|
||||
radius_list, adjacent_n_tuples(vertices, 3)
|
||||
for current_radius, (v1, v2, v3) in zip(
|
||||
radius_list, adjacent_n_tuples(vertex_group, 3), strict=True
|
||||
):
|
||||
vect1 = v2 - v1
|
||||
vect2 = v3 - v2
|
||||
|
|
@ -243,10 +249,10 @@ class Polygram(VMobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
angle = angle_between_vectors(vect1, vect2)
|
||||
# Negative radius gives concave curves
|
||||
angle *= np.sign(currentRadius)
|
||||
angle *= np.sign(current_radius)
|
||||
|
||||
# Distance between vertex and start of the arc
|
||||
cut_off_length = currentRadius * np.tan(angle / 2)
|
||||
cut_off_length = current_radius * np.tan(angle / 2)
|
||||
|
||||
# Determines counterclockwise vs. clockwise
|
||||
sign = np.sign(np.cross(vect1, vect2)[2])
|
||||
|
|
@ -261,17 +267,17 @@ class Polygram(VMobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
if evenly_distribute_anchors:
|
||||
# Determine the average length of each curve
|
||||
nonZeroLengthArcs = [arc for arc in arcs if len(arc.points) > 4]
|
||||
if len(nonZeroLengthArcs):
|
||||
totalArcLength = sum(
|
||||
[arc.get_arc_length() for arc in nonZeroLengthArcs]
|
||||
nonzero_length_arcs = [arc for arc in arcs if len(arc.points) > 4]
|
||||
if len(nonzero_length_arcs) > 0:
|
||||
total_arc_length = sum(
|
||||
[arc.get_arc_length() for arc in nonzero_length_arcs]
|
||||
)
|
||||
totalCurveCount = (
|
||||
sum([len(arc.points) for arc in nonZeroLengthArcs]) / 4
|
||||
num_curves = (
|
||||
sum([len(arc.points) for arc in nonzero_length_arcs]) / 4
|
||||
)
|
||||
averageLengthPerCurve = totalArcLength / totalCurveCount
|
||||
average_arc_length = total_arc_length / num_curves
|
||||
else:
|
||||
averageLengthPerCurve = 1
|
||||
average_arc_length = 1.0
|
||||
|
||||
# To ensure that we loop through starting with last
|
||||
arcs = [arcs[-1], *arcs[:-1]]
|
||||
|
|
@ -284,9 +290,7 @@ class Polygram(VMobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
# Make sure anchors are evenly distributed, if necessary
|
||||
if evenly_distribute_anchors:
|
||||
line.insert_n_curves(
|
||||
ceil(line.get_length() / averageLengthPerCurve)
|
||||
)
|
||||
line.insert_n_curves(ceil(line.get_length() / average_arc_length))
|
||||
|
||||
new_points.extend(line.points)
|
||||
|
||||
|
|
@ -548,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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -20,8 +18,9 @@ from manim.constants import (
|
|||
from manim.mobject.geometry.line import Line
|
||||
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):
|
||||
|
|
@ -51,23 +50,29 @@ 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:
|
||||
from manim.mobject.mobject import Group
|
||||
|
||||
if not all(isinstance(mob, Mobject) for mob in mobjects):
|
||||
if not all(isinstance(mob, (Mobject, OpenGLMobject)) for mob in mobjects):
|
||||
raise TypeError(
|
||||
"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,
|
||||
)
|
||||
|
|
@ -107,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:
|
||||
|
|
@ -144,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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -335,7 +335,7 @@ def _tree_layout(
|
|||
# Always make a copy of the children because they get eaten
|
||||
stack = [list(children[root_vertex]).copy()]
|
||||
stick = [root_vertex]
|
||||
parent = {u: root_vertex for u in children[root_vertex]}
|
||||
parent = dict.fromkeys(children[root_vertex], root_vertex)
|
||||
pos = {}
|
||||
obstruction = [0.0] * len(T)
|
||||
o = -1 if orientation == "down" else 1
|
||||
|
|
@ -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 = {}
|
||||
|
||||
|
|
@ -671,8 +669,30 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
"""Helper method for populating the edges of the graph."""
|
||||
raise NotImplementedError("To be implemented in concrete subclasses")
|
||||
|
||||
def __getitem__(self: Graph, v: Hashable) -> Mobject:
|
||||
return self.vertices[v]
|
||||
def __getitem__(self: Graph, k: Hashable | tuple[Hashable, Hashable]) -> Mobject:
|
||||
"""Get a vertex or edge by its name/identifier.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
k
|
||||
A vertex name (hashable) or an edge tuple ``(u, v)``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Mobject
|
||||
The :class:`~.Mobject` corresponding to the given vertex or edge.
|
||||
|
||||
Raises
|
||||
------
|
||||
KeyError
|
||||
If ``k`` is not a valid vertex or edge.
|
||||
"""
|
||||
if k in self.vertices:
|
||||
return self.vertices[k]
|
||||
elif k in self.edges:
|
||||
return self.edges[k]
|
||||
else:
|
||||
raise ValueError(f"Could not find {k} in vertices or edges")
|
||||
|
||||
def _create_vertex(
|
||||
self,
|
||||
|
|
@ -697,7 +717,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)):
|
||||
|
|
@ -810,12 +830,12 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
vertex_mobjects = {}
|
||||
|
||||
graph_center = self.get_center()
|
||||
base_positions = {v: graph_center for v in vertices}
|
||||
base_positions = dict.fromkeys(vertices, graph_center)
|
||||
base_positions.update(positions)
|
||||
positions = base_positions
|
||||
|
||||
if isinstance(labels, bool):
|
||||
labels = {v: labels for v in vertices}
|
||||
labels = dict.fromkeys(vertices, labels)
|
||||
else:
|
||||
assert isinstance(labels, dict)
|
||||
base_labels = dict.fromkeys(vertices, False)
|
||||
|
|
@ -1021,10 +1041,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)
|
||||
|
|
@ -1035,7 +1052,10 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
self._edge_config[(u, v)] = edge_config
|
||||
|
||||
edge_mobject = edge_type(
|
||||
self[u].get_center(), self[v].get_center(), z_index=-1, **edge_config
|
||||
start=self[u].get_center(),
|
||||
end=self[v].get_center(),
|
||||
z_index=-1,
|
||||
**edge_config,
|
||||
)
|
||||
self.edges[(u, v)] = edge_mobject
|
||||
|
||||
|
|
@ -1344,6 +1364,11 @@ class Graph(GenericGraph):
|
|||
g[2].animate.move_to([-1, 1, 0]),
|
||||
g[3].animate.move_to([1, -1, 0]),
|
||||
g[4].animate.move_to([-1, -1, 0]))
|
||||
self.play(LaggedStart(Wiggle(g[(1, 2)]),
|
||||
Wiggle(g[(2, 3)]),
|
||||
Wiggle(g[(3, 4)]),
|
||||
Wiggle(g[(1, 3)]),
|
||||
Wiggle(g[(1, 4)])))
|
||||
self.wait()
|
||||
|
||||
There are several automatic positioning algorithms to choose from:
|
||||
|
|
@ -1543,8 +1568,8 @@ class Graph(GenericGraph):
|
|||
):
|
||||
self.edges = {
|
||||
(u, v): edge_type(
|
||||
self[u].get_center(),
|
||||
self[v].get_center(),
|
||||
start=self[u].get_center(),
|
||||
end=self[v].get_center(),
|
||||
z_index=-1,
|
||||
**self._edge_config[(u, v)],
|
||||
)
|
||||
|
|
@ -1750,8 +1775,8 @@ class DiGraph(GenericGraph):
|
|||
):
|
||||
self.edges = {
|
||||
(u, v): edge_type(
|
||||
self[u],
|
||||
self[v],
|
||||
start=self[u],
|
||||
end=self[v],
|
||||
z_index=-1,
|
||||
**self._edge_config[(u, v)],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ __all__ = ["NumberLine", "UnitInterval"]
|
|||
|
||||
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Self
|
||||
|
||||
from manim.mobject.geometry.tips import ArrowTip
|
||||
from manim.typing import Point3DLike
|
||||
from manim.typing import ManimTextLabel, Point3D, Point3DLike, Vector3D
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -22,7 +24,8 @@ 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.tex_mobject import MathTex, Tex
|
||||
from manim.mobject.text.tex_mobject import MathTex, SingleStringMathTex, Tex
|
||||
from manim.mobject.text.typst_mobject import Typst, TypstMath
|
||||
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[ManimTextLabel] = 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[SingleStringMathTex] | 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,
|
||||
)
|
||||
|
|
@ -473,10 +487,13 @@ class NumberLine(Line):
|
|||
if font_size is None:
|
||||
font_size = self.font_size
|
||||
if label_constructor is None:
|
||||
label_constructor = self.label_constructor
|
||||
label_constructor = cast(type[SingleStringMathTex], 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[SingleStringMathTex] | 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``.
|
||||
|
|
@ -530,7 +547,7 @@ class NumberLine(Line):
|
|||
font_size = self.font_size
|
||||
|
||||
if label_constructor is None:
|
||||
label_constructor = self.label_constructor
|
||||
label_constructor = cast(type[SingleStringMathTex], self.label_constructor)
|
||||
|
||||
numbers = VGroup()
|
||||
for x in x_values:
|
||||
|
|
@ -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[ManimTextLabel] | None = None,
|
||||
) -> Self:
|
||||
"""Adds specifically positioned labels to the :class:`~.NumberLine` using a ``dict``.
|
||||
The labels can be accessed after creation via ``self.labels``.
|
||||
|
||||
|
|
@ -592,13 +609,18 @@ class NumberLine(Line):
|
|||
# TODO: remove this check and ability to call
|
||||
# this method via CoordinateSystem.add_coordinates()
|
||||
# must be explicitly called
|
||||
if isinstance(label, str) and label_constructor is MathTex:
|
||||
label = Tex(label)
|
||||
if isinstance(label, str):
|
||||
if label_constructor is MathTex:
|
||||
label = Tex(label)
|
||||
elif label_constructor is TypstMath:
|
||||
label = Typst(label)
|
||||
else:
|
||||
label = self._create_label_tex(label, label_constructor)
|
||||
else:
|
||||
label = self._create_label_tex(label, label_constructor)
|
||||
|
||||
if hasattr(label, "font_size"):
|
||||
label.font_size = font_size
|
||||
cast(Any, label).font_size = font_size
|
||||
else:
|
||||
raise AttributeError(f"{label} is not compatible with add_labels.")
|
||||
label.next_to(self.number_to_point(x), direction=direction, buff=buff)
|
||||
|
|
@ -611,8 +633,8 @@ class NumberLine(Line):
|
|||
def _create_label_tex(
|
||||
self,
|
||||
label_tex: str | float | VMobject,
|
||||
label_constructor: Callable | None = None,
|
||||
**kwargs,
|
||||
label_constructor: type[ManimTextLabel] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> VMobject:
|
||||
"""Checks if the label is a :class:`~.VMobject`, otherwise, creates a
|
||||
label by passing ``label_tex`` to ``label_constructor``.
|
||||
|
|
@ -633,24 +655,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 +682,10 @@ class NumberLine(Line):
|
|||
class UnitInterval(NumberLine):
|
||||
def __init__(
|
||||
self,
|
||||
unit_size=10,
|
||||
numbers_with_elongated_ticks=None,
|
||||
decimal_number_config=None,
|
||||
**kwargs,
|
||||
unit_size: float = 10,
|
||||
numbers_with_elongated_ticks: list[float] | None = None,
|
||||
decimal_number_config: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
numbers_with_elongated_ticks = (
|
||||
[0, 1]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ __all__ = ["SampleSpace", "BarChart"]
|
|||
|
||||
|
||||
from collections.abc import Iterable, MutableSequence, Sequence
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -13,11 +14,11 @@ from manim import config, logger
|
|||
from manim.constants import *
|
||||
from manim.mobject.geometry.polygram import Rectangle
|
||||
from manim.mobject.graphing.coordinate_systems import Axes
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
|
||||
from manim.mobject.svg.brace import Brace
|
||||
from manim.mobject.text.tex_mobject import MathTex, Tex
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.typing import Vector3D
|
||||
from manim.utils.color import (
|
||||
BLUE_E,
|
||||
DARK_GREY,
|
||||
|
|
@ -54,13 +55,13 @@ class SampleSpace(Rectangle):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
height=3,
|
||||
width=3,
|
||||
fill_color=DARK_GREY,
|
||||
fill_opacity=1,
|
||||
stroke_width=0.5,
|
||||
stroke_color=LIGHT_GREY,
|
||||
default_label_scale_val=1,
|
||||
height: float = 3,
|
||||
width: float = 3,
|
||||
fill_color: ParsableManimColor = DARK_GREY,
|
||||
fill_opacity: float = 1,
|
||||
stroke_width: float = 0.5,
|
||||
stroke_color: ParsableManimColor = LIGHT_GREY,
|
||||
default_label_scale_val: float = 1,
|
||||
):
|
||||
super().__init__(
|
||||
height=height,
|
||||
|
|
@ -72,7 +73,9 @@ class SampleSpace(Rectangle):
|
|||
)
|
||||
self.default_label_scale_val = default_label_scale_val
|
||||
|
||||
def add_title(self, title="Sample space", buff=MED_SMALL_BUFF):
|
||||
def add_title(
|
||||
self, title: str = "Sample space", buff: float = MED_SMALL_BUFF
|
||||
) -> None:
|
||||
# TODO, should this really exist in SampleSpaceScene
|
||||
title_mob = Tex(title)
|
||||
if title_mob.width > self.width:
|
||||
|
|
@ -81,23 +84,30 @@ class SampleSpace(Rectangle):
|
|||
self.title = title_mob
|
||||
self.add(title_mob)
|
||||
|
||||
def add_label(self, label):
|
||||
def add_label(self, label: str) -> None:
|
||||
self.label = label
|
||||
|
||||
def complete_p_list(self, p_list):
|
||||
new_p_list = list(tuplify(p_list))
|
||||
def complete_p_list(self, p_list: float | Iterable[float]) -> list[float]:
|
||||
p_list_tuplified: tuple[float] = tuplify(p_list)
|
||||
new_p_list = list(p_list_tuplified)
|
||||
remainder = 1.0 - sum(new_p_list)
|
||||
if abs(remainder) > EPSILON:
|
||||
new_p_list.append(remainder)
|
||||
return new_p_list
|
||||
|
||||
def get_division_along_dimension(self, p_list, dim, colors, vect):
|
||||
p_list = self.complete_p_list(p_list)
|
||||
colors = color_gradient(colors, len(p_list))
|
||||
def get_division_along_dimension(
|
||||
self,
|
||||
p_list: float | Iterable[float],
|
||||
dim: int,
|
||||
colors: Sequence[ParsableManimColor],
|
||||
vect: Vector3D,
|
||||
) -> VGroup:
|
||||
p_list_complete = self.complete_p_list(p_list)
|
||||
colors_in_gradient = color_gradient(colors, len(p_list_complete))
|
||||
|
||||
last_point = self.get_edge_center(-vect)
|
||||
parts = VGroup()
|
||||
for factor, color in zip(p_list, colors):
|
||||
for factor, color in zip(p_list_complete, colors_in_gradient, 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,20 +351,23 @@ 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -139,15 +148,19 @@ class LogBase(_ScaleBase):
|
|||
|
||||
def function(self, value: float) -> float:
|
||||
"""Scales the value to fit it to a logarithmic scale.``self.function(5)==10**5``"""
|
||||
return self.base**value
|
||||
return_value: float = self.base**value
|
||||
return return_value
|
||||
|
||||
def inverse_function(self, value: float) -> float:
|
||||
"""Inverse of ``function``. The value must be greater than 0"""
|
||||
if isinstance(value, np.ndarray):
|
||||
condition = value.any() <= 0
|
||||
|
||||
def func(value, base):
|
||||
return np.log(value) / np.log(base)
|
||||
func: Callable[[float, float], float]
|
||||
|
||||
def func(value: float, base: float) -> float:
|
||||
return_value: float = np.log(value) / np.log(base)
|
||||
return return_value
|
||||
else:
|
||||
condition = value <= 0
|
||||
func = math.log
|
||||
|
|
@ -163,8 +176,8 @@ class LogBase(_ScaleBase):
|
|||
self,
|
||||
val_range: Iterable[float],
|
||||
unit_decimal_places: int = 0,
|
||||
**base_config: dict[str, Any],
|
||||
) -> list[Mobject]:
|
||||
**base_config: Any,
|
||||
) -> list[Integer]:
|
||||
"""Produces custom :class:`~.Integer` labels in the form of ``10^2``.
|
||||
|
||||
Parameters
|
||||
|
|
@ -177,7 +190,7 @@ class LogBase(_ScaleBase):
|
|||
Additional arguments to be passed to :class:`~.Integer`.
|
||||
"""
|
||||
# uses `format` syntax to control the number of decimal places.
|
||||
tex_labels = [
|
||||
tex_labels: list[Integer] = [
|
||||
Integer(
|
||||
self.base,
|
||||
unit="^{%s}" % (f"{self.inverse_function(i):.{unit_decimal_places}f}"), # noqa: UP031
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue