mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-22 10:01:47 +00:00
Merge branch 'main' into patch-1
This commit is contained in:
commit
2607f7b040
210 changed files with 7442 additions and 3589 deletions
|
|
@ -1 +0,0 @@
|
|||
<path id="nd" d="m 464.7,68.6 -1.1,2.8 .8,1.4 -.3,5.1 -.5,1.1 2.7,9.1 1.3,2.5 .7,14 1,2.7 -.4,5.8 2.9,7.4 .3,5.8 -.1,2.1 -29.5,-.4 -46,-2.1 -39.2,-2.9 5.2,-66.7 44.5,3.4 55.3,1.6 z">
|
||||
5
.codespell_ignorewords
Normal file
5
.codespell_ignorewords
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
nam
|
||||
sherif
|
||||
falsy
|
||||
medias
|
||||
strager
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
[codespell]
|
||||
exclude-file=.codespell_ignorelines
|
||||
check-hidden=True
|
||||
ignore-words-list = nam,sherif,falsy
|
||||
check-hidden = True
|
||||
skip = .git,*.js,*.js.map,*.css,*.css.map,*.html,*.po,*.pot,poetry.lock,*.log,*.svg
|
||||
ignore-words = .codespell_ignorewords
|
||||
|
|
|
|||
8
.flake8
8
.flake8
|
|
@ -2,7 +2,7 @@
|
|||
# Exclude the grpc generated code
|
||||
exclude = ./manim/grpc/gen/*, __pycache__,.git,
|
||||
per-file-ignores = __init__.py:F401
|
||||
max-complexity = 15
|
||||
max-complexity = 29
|
||||
max-line-length = 88
|
||||
statistics = True
|
||||
# Prevents some flake8-rst-docstrings errors
|
||||
|
|
@ -18,6 +18,9 @@ extend-ignore = E203, W503, D202, D212, D213, D404
|
|||
# Misc
|
||||
F401, F403, F405, F841, E501, E731, E402, F811, F821,
|
||||
|
||||
# multiple statements on one line (overload)
|
||||
E704,
|
||||
|
||||
# Plug-in: flake8-builtins
|
||||
A001, A002, A003,
|
||||
|
||||
|
|
@ -27,9 +30,6 @@ extend-ignore = E203, W503, D202, D212, D213, D404
|
|||
# Plug-in: flake8-simplify
|
||||
SIM105, SIM106, SIM119,
|
||||
|
||||
# Plug-in: flake8-comprehensions
|
||||
C901
|
||||
|
||||
# Plug-in: flake8-pytest-style
|
||||
PT001, PT004, PT006, PT011, PT018, PT022, PT023,
|
||||
|
||||
|
|
|
|||
2
.git-blame-ignore-revs
Normal file
2
.git-blame-ignore-revs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Switched to ruff format:
|
||||
24025b60d57301b0a59754c38d77bccd8ed69feb
|
||||
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -70,14 +70,5 @@ PASTE HERE
|
|||
<!-- output of `tlmgr list --only-installed` for TeX Live or a screenshot of the Packages page for MikTeX -->
|
||||
</details>
|
||||
|
||||
<details><summary>FFMPEG</summary>
|
||||
|
||||
Output of `ffmpeg -version`:
|
||||
|
||||
```
|
||||
PASTE HERE
|
||||
```
|
||||
</details>
|
||||
|
||||
## Additional comments
|
||||
<!-- Add further context that you think might be relevant for this issue here. -->
|
||||
|
|
|
|||
9
.github/ISSUE_TEMPLATE/installation_issue.md
vendored
9
.github/ISSUE_TEMPLATE/installation_issue.md
vendored
|
|
@ -53,14 +53,5 @@ PASTE HERE
|
|||
<!-- output of `tlmgr list --only-installed` for TeX Live or a screenshot of the Packages page for MikTeX -->
|
||||
</details>
|
||||
|
||||
<details><summary>FFMPEG</summary>
|
||||
|
||||
Output of `ffmpeg -version`:
|
||||
|
||||
```
|
||||
PASTE HERE
|
||||
```
|
||||
</details>
|
||||
|
||||
## Additional comments
|
||||
<!-- Add further context that you think might be relevant for this issue here. -->
|
||||
|
|
|
|||
10
.github/manimdependency.json
vendored
10
.github/manimdependency.json
vendored
|
|
@ -4,7 +4,10 @@
|
|||
"standalone",
|
||||
"preview",
|
||||
"doublestroke",
|
||||
"ms",
|
||||
"count1to",
|
||||
"multitoc",
|
||||
"prelim2e",
|
||||
"ragged2e",
|
||||
"everysel",
|
||||
"setspace",
|
||||
"rsfs",
|
||||
|
|
@ -29,7 +32,10 @@
|
|||
"standalone",
|
||||
"preview",
|
||||
"doublestroke",
|
||||
"ms",
|
||||
"count1to",
|
||||
"multitoc",
|
||||
"prelim2e",
|
||||
"ragged2e",
|
||||
"everysel",
|
||||
"setspace",
|
||||
"rsfs",
|
||||
|
|
|
|||
17
.github/workflows/ci.yml
vendored
17
.github/workflows/ci.yml
vendored
|
|
@ -18,10 +18,11 @@ jobs:
|
|||
env:
|
||||
DISPLAY: :0
|
||||
PYTEST_ADDOPTS: "--color=yes" # colors in pytest
|
||||
PYTHONIOENCODING: "utf8"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-22.04, macos-latest, windows-latest]
|
||||
os: [ubuntu-22.04, macos-13, windows-latest]
|
||||
python: ["3.9", "3.10", "3.11", "3.12"]
|
||||
|
||||
steps:
|
||||
|
|
@ -50,12 +51,6 @@ jobs:
|
|||
run: |
|
||||
echo "date=$(/bin/date -u "+%m%w%Y")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install and cache ffmpeg (all OS)
|
||||
uses: FedericoCarboni/setup-ffmpeg@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
id: setup-ffmpeg
|
||||
|
||||
- name: Install system dependencies (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
uses: awalsh128/cache-apt-pkgs-action@latest
|
||||
|
|
@ -77,7 +72,7 @@ jobs:
|
|||
sudo /usr/bin/Xvfb $DISPLAY -screen 0 1280x1024x24 &
|
||||
|
||||
- name: Setup Cairo Cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
id: cache-cairo
|
||||
if: runner.os == 'Linux' || runner.os == 'macOS'
|
||||
with:
|
||||
|
|
@ -93,7 +88,7 @@ jobs:
|
|||
run: python .github/scripts/ci_build_cairo.py --set-env-vars
|
||||
|
||||
- name: Setup macOS cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
id: cache-macos
|
||||
if: runner.os == 'macOS'
|
||||
with:
|
||||
|
|
@ -130,12 +125,12 @@ jobs:
|
|||
- name: Setup Windows cache
|
||||
id: cache-windows
|
||||
if: runner.os == 'Windows'
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
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@v1
|
||||
- uses: ssciwr/setup-mesa-dist-win@v2
|
||||
|
||||
- name: Install system dependencies (Windows)
|
||||
if: runner.os == 'Windows' && steps.cache-windows.outputs.cache-hit != 'true'
|
||||
|
|
|
|||
4
.github/workflows/publish-docker.yml
vendored
4
.github/workflows/publish-docker.yml
vendored
|
|
@ -25,7 +25,7 @@ jobs:
|
|||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/arm64,linux/amd64
|
||||
push: true
|
||||
|
|
@ -61,7 +61,7 @@ jobs:
|
|||
print(f"tag_name={ref_tag}", file=f)
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/arm64,linux/amd64
|
||||
push: true
|
||||
|
|
|
|||
|
|
@ -19,12 +19,13 @@ jobs:
|
|||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt update && sudo apt install -y \
|
||||
pkg-config libcairo-dev libpango1.0-dev ffmpeg wget fonts-roboto
|
||||
pkg-config libcairo-dev libpango1.0-dev wget fonts-roboto
|
||||
wget -qO- "https://yihui.org/tinytex/install-bin-unix.sh" | sh
|
||||
echo ${HOME}/.TinyTeX/bin/x86_64-linux >> $GITHUB_PATH
|
||||
|
||||
- name: Install LaTeX and Python dependencies
|
||||
run: |
|
||||
tlmgr update --self
|
||||
tlmgr install \
|
||||
babel-english ctex doublestroke dvisvgm frcursive fundus-calligra jknapltx \
|
||||
mathastext microtype physics preview ragged2e relsize rsfs setspace standalone \
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ fail_fast: false
|
|||
exclude: ^(manim/grpc/gen/|docs/i18n/)
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: check-ast
|
||||
name: Validate Python
|
||||
|
|
@ -12,39 +12,22 @@ repos:
|
|||
- id: end-of-file-fixer
|
||||
- id: check-toml
|
||||
name: Validate Poetry
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
- id: isort
|
||||
name: isort (cython)
|
||||
types: [cython]
|
||||
- id: isort
|
||||
name: isort (pyi)
|
||||
types: [pyi]
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.10.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
name: Update code to new python versions
|
||||
args: [--py37-plus]
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: v1.10.0
|
||||
hooks:
|
||||
- id: python-check-blanket-noqa
|
||||
name: Precision flake ignores
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.7.0
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.5.1
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/asottile/blacken-docs
|
||||
rev: 1.15.0
|
||||
hooks:
|
||||
- id: blacken-docs
|
||||
additional_dependencies: [black==22.3.0]
|
||||
- id: ruff
|
||||
name: ruff lint
|
||||
types: [python]
|
||||
args: [--exit-non-zero-on-fix]
|
||||
- id: ruff-format
|
||||
types: [python]
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.1.0
|
||||
rev: 7.1.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies:
|
||||
|
|
@ -58,7 +41,7 @@ repos:
|
|||
flake8-simplify==0.14.1,
|
||||
]
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.5.1
|
||||
rev: v1.10.1
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies:
|
||||
|
|
@ -72,7 +55,7 @@ repos:
|
|||
files: ^manim/
|
||||
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.2.5
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
- id: codespell
|
||||
files: ^.*\.(py|md|rst)$
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ build:
|
|||
|
||||
apt_packages:
|
||||
- libpango1.0-dev
|
||||
- ffmpeg
|
||||
- graphviz
|
||||
|
||||
python:
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ authors:
|
|||
-
|
||||
name: "The Manim Community Developers"
|
||||
cff-version: "1.2.0"
|
||||
date-released: 2023-11-11
|
||||
date-released: 2024-04-28
|
||||
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.18.0"
|
||||
version: "v0.18.1"
|
||||
...
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021, the Manim Community Developers
|
||||
Copyright (c) 2024, the Manim Community Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
|||
|
|
@ -5,12 +5,6 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
try:
|
||||
# https://github.com/moderngl/moderngl/issues/517
|
||||
import readline # required to prevent a segfault on Python 3.10
|
||||
except ModuleNotFoundError: # windows
|
||||
pass
|
||||
|
||||
import cairo
|
||||
import moderngl
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ FROM python:3.11-slim
|
|||
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install --no-install-recommends -y \
|
||||
ffmpeg \
|
||||
build-essential \
|
||||
gcc \
|
||||
cmake \
|
||||
|
|
@ -23,9 +22,9 @@ RUN wget -O /tmp/install-tl-unx.tar.gz http://mirror.ctan.org/systems/texlive/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 ctex doublestroke dvisvgm everysel \
|
||||
amsmath babel-english cbfonts-fd cm-super count1to ctex doublestroke dvisvgm everysel \
|
||||
fontspec frcursive fundus-calligra gnu-freefont jknapltx latex-bin \
|
||||
mathastext microtype ms physics preview ragged2e relsize rsfs \
|
||||
mathastext microtype multitoc physics prelim2e preview ragged2e relsize rsfs \
|
||||
setspace standalone tipa wasy wasysym xcolor xetex xkeyval
|
||||
|
||||
# clone and build manim
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
furo
|
||||
myst-parser
|
||||
sphinx<5.1
|
||||
sphinx>=7.3
|
||||
sphinx-copybutton
|
||||
sphinxext-opengraph
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
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/>`__.
|
||||
|
||||
.. toctree::
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from __future__ import annotations
|
|||
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import manim
|
||||
|
|
@ -25,7 +26,7 @@ sys.path.insert(0, os.path.abspath("."))
|
|||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = "Manim"
|
||||
copyright = "2020-2022, The Manim Community Dev Team"
|
||||
copyright = f"2020-{datetime.now().year}, The Manim Community Dev Team"
|
||||
author = "The Manim Community Dev Team"
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -51,8 +51,8 @@ For first-time contributors
|
|||
#. Install Manim:
|
||||
|
||||
- Follow the steps in our :doc:`installation instructions
|
||||
<../installation>` to install **Manim's dependencies**,
|
||||
primarily ``ffmpeg`` and ``LaTeX``.
|
||||
<../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
|
||||
|
|
@ -62,7 +62,7 @@ For first-time contributors
|
|||
managing virtual environments.
|
||||
|
||||
If you choose to use Poetry as well, follow `Poetry's installation
|
||||
guidelines <https://python-poetry.org/docs/master/#installation>`__
|
||||
guidelines <https://python-poetry.org/docs/master/#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
|
||||
|
|
|
|||
|
|
@ -81,3 +81,4 @@ Index
|
|||
docs/examples
|
||||
docs/references
|
||||
docs/typings
|
||||
docs/types
|
||||
|
|
|
|||
|
|
@ -77,8 +77,7 @@ Example:
|
|||
The mobject linked to this instance.
|
||||
"""
|
||||
|
||||
def __init__(name: str, id: int, singleton: MyClass, mobj: Mobject = None):
|
||||
...
|
||||
def __init__(name: str, id: int, singleton: MyClass, mobj: Mobject = None): ...
|
||||
|
||||
2. The usage of ``Parameters`` on functions to specify how
|
||||
every parameter works and what it does. This should be excluded if
|
||||
|
|
|
|||
134
docs/source/contributing/docs/types.rst
Normal file
134
docs/source/contributing/docs/types.rst
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
===================
|
||||
Choosing Type Hints
|
||||
===================
|
||||
In order to provide the best user experience,
|
||||
it's important that type hints are chosen correctly.
|
||||
With the large variety of types provided by Manim, choosing
|
||||
which one to use can be difficult. This guide aims to
|
||||
aid you in the process of choosing the right type for the scenario.
|
||||
|
||||
|
||||
The first step is figuring out which category your type hint fits into.
|
||||
|
||||
Coordinates
|
||||
-----------
|
||||
Coordinates encompass two main categories: points, and vectors.
|
||||
|
||||
|
||||
Points
|
||||
~~~~~~
|
||||
The purpose of points is pretty straightforward: they represent a point
|
||||
in space. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def status2D(coord: Point2D) -> None:
|
||||
x, y = coord
|
||||
print(f"Point at {x=},{y=}")
|
||||
|
||||
|
||||
def status3D(coord: Point3D) -> None:
|
||||
x, y, z = coord
|
||||
print(f"Point at {x=},{y=},{z=}")
|
||||
|
||||
|
||||
def get_statuses(coords: Point2D_Array | Point3D_Array) -> None:
|
||||
for coord in coords:
|
||||
if len(coord) == 2:
|
||||
# it's a Point2D
|
||||
status2D(coord)
|
||||
else:
|
||||
# it's a point3D
|
||||
status3D(coord)
|
||||
|
||||
It's important to realize that the status functions accepted both
|
||||
tuples/lists of the correct length, and ``NDArray``'s of the correct shape.
|
||||
If they only accepted ``NDArray``'s, we would use their ``Internal`` counterparts:
|
||||
:class:`~.typing.InternalPoint2D`, :class:`~.typing.InternalPoint3D`, :class:`~.typing.InternalPoint2D_Array` and :class:`~.typing.InternalPoint3D_Array`.
|
||||
|
||||
In general, the type aliases prefixed with ``Internal`` should never be used on
|
||||
user-facing classes and functions, but should be reserved for internal behavior.
|
||||
|
||||
Vectors
|
||||
~~~~~~~
|
||||
Vectors share many similarities to points. However, they have a different
|
||||
connotation. Vectors should be used to represent direction. For example,
|
||||
consider this slightly contrived function:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def shift_mobject(mob: Mobject, direction: Vector3D, scale_factor: float = 1) -> mob:
|
||||
return mob.shift(direction * scale_factor)
|
||||
|
||||
Here we see an important example of the difference. ``direction`` can not, and
|
||||
should not, be typed as a :class:`~.typing.Point3D` because the function does not accept tuples/lists,
|
||||
like ``direction=(0, 1, 0)``. You could type it as :class:`~.typing.InternalPoint3D` and
|
||||
the type checker and linter would be happy; however, this makes the code harder
|
||||
to understand.
|
||||
|
||||
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 ``Point2D | Point3D``,
|
||||
as it can also accept ``tuple[float, float]`` and ``tuple[float, float, float]``.
|
||||
|
||||
Colors
|
||||
------
|
||||
The interface Manim provides for working with colors is :class:`.ManimColor`.
|
||||
The main color types Manim supports are RGB, RGBA, and HSV. You will want
|
||||
to add type hints to a function depending on which type it uses. If any color will work,
|
||||
you will need something like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.utils.color import ParsableManimColor
|
||||
|
||||
# type hint stuff with ParsableManimColor
|
||||
|
||||
|
||||
|
||||
Béziers
|
||||
-------
|
||||
Manim internally represents a :class:`.Mobject` by a collection of points. In the case of :class:`.VMobject`,
|
||||
the most commonly used subclass of :class:`.Mobject`, these points represent Bézier curves,
|
||||
which are a way of representing a curve using a sequence of points.
|
||||
|
||||
.. note::
|
||||
|
||||
To learn more about Béziers, take a look at https://pomax.github.io/bezierinfo/
|
||||
|
||||
|
||||
Manim supports two different renderers, which each have different representations of
|
||||
Béziers: Cairo uses cubic Bézier curves, while OpenGL uses quadratic Bézier curves.
|
||||
|
||||
Type hints like :class:`~.typing.BezierPoints` represent a single bezier curve, and :class:`~.typing.BezierPath`
|
||||
represents multiple Bézier curves. A :class:`~.typing.Spline` is when the Bézier curves in a :class:`~.typing.BezierPath`
|
||||
forms a single connected curve. Manim also provides more specific type aliases when working with
|
||||
quadratic or cubic curves, and they are prefixed with their respective type (e.g. :class:`~.typing.CubicBezierPoints`,
|
||||
is a :class:`~.typing.BezierPoints` consisting of exactly 4 points representing a cubic Bézier curve).
|
||||
|
||||
|
||||
Functions
|
||||
---------
|
||||
Throughout the codebase, many different types of functions are used. The most obvious example
|
||||
is a rate function, which takes in a float and outputs a float (``Callable[[float], float]``).
|
||||
Another example is for overriding animations. One will often need to map a :class:`.Mobject`
|
||||
to an overridden :class:`.Animation`, and for that we have the :class:`~.typing.FunctionOverride` type hint.
|
||||
|
||||
:class:`~.typing.PathFuncType` and :class:`~.typing.MappingFunction` are more niche, but are related to moving objects
|
||||
along a path, or applying functions. If you need to use it, you'll know.
|
||||
|
||||
|
||||
Images
|
||||
------
|
||||
There are several representations of images in Manim. The most common is
|
||||
the representation as a NumPy array of floats representing the pixels of an image.
|
||||
This is especially common when it comes to the OpenGL renderer.
|
||||
|
||||
This is the use case of the :class:`~.typing.Image` type hint. Sometimes, Manim may use ``PIL.Image``,
|
||||
in which case one should use that type hint instead.
|
||||
Of course, if a more specific type of image is needed, it can be annotated as such.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
==============
|
||||
Adding Typings
|
||||
==============
|
||||
==================
|
||||
Typing Conventions
|
||||
==================
|
||||
|
||||
.. warning::
|
||||
This section is still a work in progress.
|
||||
|
|
@ -18,9 +18,7 @@ https://realpython.com/python-type-checking/#hello-types.
|
|||
Typing standards
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Manim uses `mypy`_ to type check its codebase. You will find a list of
|
||||
configuration values in the ``mypy.ini`` configuration file.
|
||||
|
||||
Manim uses `mypy`_ to type check its codebase. You will find a list of configuration values in the ``mypy.ini`` configuration file.
|
||||
To be able to use the newest typing features not available in the lowest
|
||||
supported Python version, make use of `typing_extensions`_.
|
||||
|
||||
|
|
@ -86,11 +84,36 @@ Typing guidelines
|
|||
T = TypeVar("T")
|
||||
|
||||
|
||||
def copy(self: T) -> T:
|
||||
...
|
||||
def copy(self: T) -> T: ...
|
||||
|
||||
* Use ``typing.Iterable`` whenever the function works with *any* iterable, not a specific type.
|
||||
|
||||
* Prefer ``numpy.typing.NDArray`` over ``numpy.ndarray``
|
||||
|
||||
.. code:: py
|
||||
|
||||
import numpy as np
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy.typing as npt
|
||||
|
||||
|
||||
def foo() -> npt.NDArray[float]:
|
||||
return np.array([1, 0, 1])
|
||||
|
||||
* If a method returns ``self``, use ``typing_extensions.Self``.
|
||||
|
||||
.. code:: py
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
|
||||
class CustomMobject:
|
||||
def set_color(self, color: ManimColor) -> Self:
|
||||
...
|
||||
return self
|
||||
|
||||
* If the function returns a container of a specific length each time, consider using ``tuple`` instead of ``list``.
|
||||
|
||||
.. code:: py
|
||||
|
|
@ -115,8 +138,8 @@ Typing guidelines
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.typing import Vector3
|
||||
# type stuff with Vector3
|
||||
from manim.typing import Vector3D
|
||||
# type stuff with Vector3D
|
||||
|
||||
Missing Sections for typehints are:
|
||||
-----------------------------------
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ Contributing
|
|||
|
||||
That being said, improving the documentation and making it more accessible is still highly encouraged.
|
||||
And even if your work gets outdated and requires change, you or someone else can simply adjust the translation.
|
||||
Your efforts are not in vail!
|
||||
Your efforts are not in vain!
|
||||
|
||||
|
||||
Voting
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ and then record it during rendering:
|
|||
from manim_voiceover import VoiceoverScene
|
||||
from manim_voiceover.services.recorder import RecorderService
|
||||
|
||||
|
||||
# Simply inherit from VoiceoverScene instead of Scene to get all the
|
||||
# voiceover functionality.
|
||||
class RecorderExample(VoiceoverScene):
|
||||
|
|
|
|||
|
|
@ -266,7 +266,7 @@ The scene then asks its renderer to initialize the scene by calling
|
|||
|
||||
Inspecting both the default Cairo renderer and the OpenGL renderer shows that the ``init_scene``
|
||||
method effectively makes the renderer instantiate a :class:`.SceneFileWriter` object, which
|
||||
basically is Manim's interface to ``ffmpeg`` and actually writes the movie file. The Cairo
|
||||
basically is Manim's interface to ``libav`` (FFMPEG) and actually writes the movie file. The Cairo
|
||||
renderer (see the implementation `here <https://github.com/ManimCommunity/manim/blob/main/manim/renderer/cairo_renderer.py>`__) does not require any further initialization. The OpenGL renderer
|
||||
does some additional setup to enable the realtime rendering preview window, which we do not go
|
||||
into detail further here.
|
||||
|
|
@ -310,8 +310,8 @@ the order they are called, these customizable methods are:
|
|||
After these three methods are run, the animations have been fully rendered,
|
||||
and Manim calls :meth:`.CairoRenderer.scene_finished` to gracefully
|
||||
complete the rendering process. This checks whether any animations have been
|
||||
played -- and if so, it tells the :class:`.SceneFileWriter` to close the pipe
|
||||
to ``ffmpeg``. If not, Manim assumes that a static image should be output
|
||||
played -- and if so, it tells the :class:`.SceneFileWriter` to close the output
|
||||
file. If not, Manim assumes that a static image should be output
|
||||
which it then renders using the same strategy by calling the render loop
|
||||
(see below) once.
|
||||
|
||||
|
|
@ -762,10 +762,10 @@ to learn more, the :func:`.get_hash_from_play_call` function in the
|
|||
mechanism.
|
||||
|
||||
In the event that the animation has to be rendered, the renderer asks
|
||||
its :class:`.SceneFileWriter` to start a writing process. The process
|
||||
is started by a call to ``ffmpeg`` and opens a pipe to which rendered
|
||||
raw frames can be written. As long as the pipe is open, the process
|
||||
can be accessed via the ``writing_process`` attribute of the file writer.
|
||||
its :class:`.SceneFileWriter` to open an output container. The process
|
||||
is started by a call to ``libav`` and opens a container to which rendered
|
||||
raw frames can be written. As long as the output is open, the container
|
||||
can be accessed via the ``output_container`` attribute of the file writer.
|
||||
With the writing process in place, the renderer then asks the scene
|
||||
to "begin" the animations.
|
||||
|
||||
|
|
@ -815,7 +815,7 @@ time is extracted (3 seconds long) and stored in
|
|||
skip (it should not), then whether the animation is already
|
||||
cached (it is not). The corresponding animation hash value is
|
||||
determined and passed to the file writer, which then also calls
|
||||
``ffmpeg`` to start the writing process which waits for rendered
|
||||
``libav`` to start the writing process which waits for rendered
|
||||
frames from the library.
|
||||
|
||||
The scene then ``begin``\ s the animation: for the
|
||||
|
|
@ -1001,7 +1001,7 @@ and :meth:`.Animation.clean_up_from_scene` methods are called.
|
|||
In the end, the time progression is closed (which completes the displayed progress bar)
|
||||
in the terminal. With the closing of the time progression, the
|
||||
:meth:`.Scene.play_internal` call is completed, and we return to the renderer,
|
||||
which now orders the :class:`.SceneFileWriter` to close the movie pipe that has
|
||||
which now orders the :class:`.SceneFileWriter` to close the output container that has
|
||||
been opened for this animation: a partial movie file is written.
|
||||
|
||||
This pretty much concludes the walkthrough of a :class:`.Scene.play` call,
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ in the right place!
|
|||
You can also find information on Manim's docker images and (online)
|
||||
notebook environments there.
|
||||
- Want to try the library before installing it? Take a look at our
|
||||
interactive online playground at https://try.manim.community in form
|
||||
interactive online playground at https://try.manim.community in the form
|
||||
of a Jupyter notebook.
|
||||
- In our :doc:`Tutorials <tutorials/index>` section you will find a
|
||||
collection of resources that will teach you how to use Manim. In particular,
|
||||
|
|
@ -71,7 +71,7 @@ Here are some short summaries for all of the sections in this documentation:
|
|||
can be found in the :doc:`FAQ </faq/index>` section.
|
||||
- The :doc:`Reference Manual </reference>` contains a comprehensive list of all of Manim's
|
||||
(documented) modules, classes, and functions. If you are somewhat familiar with Manim's
|
||||
module structure feel free to browse the manual directly. If you are searching for
|
||||
module structure, feel free to browse the manual directly. If you are searching for
|
||||
something specific, feel free to use the documentation's search feature in the sidebar.
|
||||
Many classes and methods come with their own illustrated examples too!
|
||||
- The :doc:`Plugins </plugins>` page documents how to install, write, and distribute
|
||||
|
|
|
|||
|
|
@ -28,17 +28,25 @@ your system's Python, or via Docker).
|
|||
|
||||
.. _conda-installation:
|
||||
|
||||
Installing Manim in conda
|
||||
*************************
|
||||
Installing Manim via Conda and related environment managers
|
||||
***********************************************************
|
||||
|
||||
Conda is a package manager for Python that allows creating environments
|
||||
where all your dependencies are stored. Like this, you don't clutter up your PC with
|
||||
unwanted libraries and you can just delete the environment when you don't need it anymore.
|
||||
It is a good way to install manim since all dependencies like
|
||||
``ffmpeg``, ``pycairo``, etc. come with it.
|
||||
It is a good way to install manim since all dependencies like ``pycairo``, etc. come with it.
|
||||
Also, the installation steps are the same, no matter if you are
|
||||
on Windows, Linux, Intel Macs or on Apple Silicon.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
There are various popular alternatives to Conda like
|
||||
`mamba <https://mamba.readthedocs.io/en/latest/>`__ /
|
||||
`micromamba <https://mamba.readthedocs.io/en/latest/user_guide/micromamba.html>`__,
|
||||
or `pixi <https://pixi.sh>`__.
|
||||
They all can be used to setup a suitable, isolated environment
|
||||
for your Manim projects.
|
||||
|
||||
The following pages show how to install Manim in a conda environment:
|
||||
|
||||
.. toctree::
|
||||
|
|
@ -54,12 +62,13 @@ Installing Manim locally
|
|||
************************
|
||||
|
||||
Manim is a Python library, and it can be
|
||||
`installed via pip <https://pypi.org/project/manim/>`__. However,
|
||||
installed via `pip <https://pypi.org/project/manim/>`__
|
||||
or `conda <https://anaconda.org/conda-forge/manim/>`__. However,
|
||||
in order for Manim to work properly, some additional system
|
||||
dependencies need to be installed first. The following pages have
|
||||
operating system specific instructions for you to follow.
|
||||
|
||||
Manim requires Python version ``3.8`` or above to run.
|
||||
Manim requires Python version ``3.9`` or above to run.
|
||||
|
||||
.. hint::
|
||||
|
||||
|
|
|
|||
|
|
@ -4,18 +4,21 @@ Conda
|
|||
Required Dependencies
|
||||
---------------------
|
||||
|
||||
To create a conda environment, you must first install
|
||||
`conda <https://docs.conda.io/projects/conda/en/latest/user-guide/install/download.html>`__
|
||||
or `mamba <https://mamba.readthedocs.io/en/latest/installation.html>`__,
|
||||
the two most popular conda clients.
|
||||
There are several package managers that work with conda packages,
|
||||
namely `conda <https://docs.conda.io/projects/conda/en/latest/user-guide/install/download.html>`__,
|
||||
`mamba <https://mamba.readthedocs.io>`__ and `pixi <https://pixi.sh>`__.
|
||||
|
||||
After installing conda, you can create a new environment and install ``manim`` inside by running
|
||||
After installing your package manager, you can create a new environment and install ``manim`` inside by running
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# using conda or mamba
|
||||
conda create -n my-manim-environment
|
||||
conda activate my-manim-environment
|
||||
conda install -c conda-forge manim
|
||||
# using pixi
|
||||
pixi init
|
||||
pixi add manim
|
||||
|
||||
Since all dependencies (except LaTeX) are handled by conda, you don't need to worry
|
||||
about needing to install additional dependencies.
|
||||
|
|
|
|||
|
|
@ -66,12 +66,12 @@ then execute it.
|
|||
.. code-block::
|
||||
|
||||
!sudo apt update
|
||||
!sudo apt install libcairo2-dev ffmpeg \
|
||||
!sudo apt install libcairo2-dev \
|
||||
texlive texlive-latex-extra texlive-fonts-extra \
|
||||
texlive-latex-recommended texlive-science \
|
||||
tipa libpango1.0-dev
|
||||
!pip install manim
|
||||
!pip install IPython --upgrade
|
||||
!pip install IPython==8.21.0
|
||||
|
||||
You should start to see Colab installing all the dependencies specified
|
||||
in these commands. After the execution has completed, you will be prompted
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@ The installation instructions depend on your particular operating
|
|||
system and package manager. If you happen to know exactly what you are doing,
|
||||
you can also simply ensure that your system has:
|
||||
|
||||
- a reasonably recent version of Python 3 (3.8 or above),
|
||||
- a reasonably recent version of Python 3 (3.9 or above),
|
||||
- with working Cairo bindings in the form of
|
||||
`pycairo <https://cairographics.org/pycairo/>`__,
|
||||
- FFmpeg accessible from the command line as ``ffmpeg``,
|
||||
- and `Pango <https://pango.gnome.org>`__ headers.
|
||||
|
||||
Then, installing Manim is just a matter of running:
|
||||
|
|
@ -33,13 +32,13 @@ Required Dependencies
|
|||
apt – Ubuntu / Mint / Debian
|
||||
****************************
|
||||
|
||||
To first update your sources, and then install Cairo, Pango, and FFmpeg
|
||||
To first update your sources, and then install Cairo and Pango
|
||||
simply run:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt update
|
||||
sudo apt install build-essential python3-dev libcairo2-dev libpango1.0-dev ffmpeg
|
||||
sudo apt install build-essential python3-dev libcairo2-dev libpango1.0-dev
|
||||
|
||||
If you don't have python3-pip installed, install it via:
|
||||
|
||||
|
|
@ -72,14 +71,6 @@ need the Python development headers:
|
|||
|
||||
sudo dnf install python3-devel
|
||||
|
||||
FFmpeg is only available via the RPMfusion repository which you have to
|
||||
configure first – please consult https://rpmfusion.org/Configuration/ for
|
||||
instructions. Then, install FFmpeg:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo dnf install ffmpeg
|
||||
|
||||
At this point you have all required dependencies and can install
|
||||
Manim by running:
|
||||
|
||||
|
|
@ -100,12 +91,12 @@ pacman – Arch / Manjaro
|
|||
|
||||
If you don't want to use the packaged version from AUR, here is what
|
||||
you need to do manually: Update your package sources, then install
|
||||
Cairo, Pango, and FFmpeg:
|
||||
Cairo and Pango:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo pacman -Syu
|
||||
sudo pacman -S cairo pango ffmpeg
|
||||
sudo pacman -S cairo pango
|
||||
|
||||
If you don't have ``python-pip`` installed, get it by running:
|
||||
|
||||
|
|
|
|||
|
|
@ -20,12 +20,12 @@ follow `Homebrew's installation instructions <https://docs.brew.sh/Installation>
|
|||
Required Dependencies
|
||||
---------------------
|
||||
|
||||
To install all required dependencies for installing Manim (namely: ffmpeg, Python,
|
||||
To install all required dependencies for installing Manim (namely: Python,
|
||||
and some required Python packages), run:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew install py3cairo ffmpeg
|
||||
brew install py3cairo
|
||||
|
||||
On *Apple Silicon* based machines (i.e., devices with the M1 chip or similar; if
|
||||
you are unsure which processor you have check by opening the Apple menu, select
|
||||
|
|
|
|||
|
|
@ -3,11 +3,8 @@ Windows
|
|||
|
||||
The easiest way of installing Manim and its dependencies is by using a
|
||||
package manager like `Chocolatey <https://chocolatey.org/>`__
|
||||
or `Scoop <https://scoop.sh>`__. If you are not afraid of editing
|
||||
your System's ``PATH``, a manual installation is also possible.
|
||||
In fact, if you already have an existing Python
|
||||
installation (3.8 or above), it might be the easiest way to get
|
||||
everything up and running.
|
||||
or `Scoop <https://scoop.sh>`__, especially if you need optional dependencies
|
||||
like LaTeX support.
|
||||
|
||||
If you choose to use one of the package managers, please follow
|
||||
their installation instructions
|
||||
|
|
@ -19,7 +16,7 @@ to make one of them available on your system.
|
|||
Required Dependencies
|
||||
---------------------
|
||||
|
||||
Manim requires a recent version of Python (3.8 or above) and ``ffmpeg``
|
||||
Manim requires a recent version of Python (3.9 or above)
|
||||
in order to work.
|
||||
|
||||
Chocolatey
|
||||
|
|
@ -34,53 +31,11 @@ Manim can be installed via Chocolatey simply by running:
|
|||
That's it, no further steps required. You can continue with installing
|
||||
the :ref:`optional dependencies <win-optional-dependencies>` below.
|
||||
|
||||
Scoop
|
||||
*****
|
||||
|
||||
While there is no recipe for installing Manim with Scoop directly,
|
||||
you can install all requirements by running:
|
||||
|
||||
.. code-block:: powershell
|
||||
|
||||
scoop install python ffmpeg
|
||||
|
||||
and then Manim can be installed by running:
|
||||
|
||||
.. code-block:: powershell
|
||||
|
||||
python -m pip install manim
|
||||
|
||||
Manim should now be installed on your system. Continue reading
|
||||
the :ref:`optional dependencies <win-optional-dependencies>` section
|
||||
below.
|
||||
|
||||
Winget
|
||||
******
|
||||
|
||||
While there is no recipe for installing Manim with Winget directly,
|
||||
you can install all requirements by running:
|
||||
|
||||
.. code-block:: powershell
|
||||
|
||||
winget install python
|
||||
winget install ffmpeg
|
||||
|
||||
and then Manim can be installed by running:
|
||||
|
||||
.. code-block:: powershell
|
||||
|
||||
python -m pip install manim
|
||||
|
||||
Manim should now be installed on your system. Continue reading
|
||||
the :ref:`optional dependencies <win-optional-dependencies>` section
|
||||
below.
|
||||
|
||||
|
||||
Manual Installation
|
||||
*******************
|
||||
Pip
|
||||
***
|
||||
|
||||
As mentioned above, Manim needs a reasonably recent version of
|
||||
Python 3 (3.8 or above) and FFmpeg.
|
||||
Python 3 (3.9 or above).
|
||||
|
||||
**Python:** Head over to https://www.python.org, download an installer
|
||||
for a recent version of Python, and follow its instructions to get Python
|
||||
|
|
@ -94,35 +49,16 @@ installed on your system.
|
|||
install Python directly from the
|
||||
`official website <https://www.python.org>`__.
|
||||
|
||||
**FFmpeg:** In order to install FFmpeg, you can get a
|
||||
pre-compiled and ready-to-use version from one of the resources
|
||||
linked at https://ffmpeg.org/download.html#build-windows, such as
|
||||
`the version available here
|
||||
<https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.7z>`__
|
||||
(recommended), or if you know exactly what you are doing
|
||||
you can alternatively get the source code
|
||||
from https://ffmpeg.org/download.html and compile it yourself.
|
||||
|
||||
|
||||
After downloading the pre-compiled archive,
|
||||
`unzip it <https://www.7-zip.org>`__ and, if you like, move the
|
||||
extracted directory to some more permanent place (e.g.,
|
||||
``C:\Program Files\``). Next, edit the ``PATH`` environment variable:
|
||||
first, visit ``Control Panel`` > ``System`` > ``System settings`` >
|
||||
``Environment Variables``, then add the full path to the ``bin``
|
||||
directory inside of the (moved) ffmpeg directory to the
|
||||
``PATH`` variable. Finally, save your changes and exit.
|
||||
|
||||
If you now open a new command line prompt (or PowerShell) and
|
||||
run ``ffmpeg``, the command should be recognized.
|
||||
|
||||
At this point, you have all the required dependencies and can now
|
||||
install Manim via
|
||||
Then, Manim can be installed via Pip simply by running:
|
||||
|
||||
.. code-block:: powershell
|
||||
|
||||
python -m pip install manim
|
||||
|
||||
Manim should now be installed on your system. Continue reading
|
||||
the :ref:`optional dependencies <win-optional-dependencies>` section
|
||||
below.
|
||||
|
||||
|
||||
.. _win-optional-dependencies:
|
||||
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ specified in Poetry as:
|
|||
[tool.poetry.plugins."manim.plugins"]
|
||||
"name" = "object_reference"
|
||||
|
||||
.. versionremoved:: 0.19.0
|
||||
.. versionremoved:: 0.18.1
|
||||
|
||||
Plugins should be imported explicitly to be usable in user code. The plugin
|
||||
system will probably be refactored in the future to provide a more structured
|
||||
|
|
|
|||
|
|
@ -276,25 +276,28 @@ When executing the command
|
|||
|
||||
manim -pql scene.py SquareToCircle
|
||||
|
||||
it was necessary to specify which ``Scene`` class to render. This is because a
|
||||
single file can contain more than one ``Scene`` class. If your file contains
|
||||
multiple ``Scene`` classes, and you want to render them all, you can use the
|
||||
``-a`` flag.
|
||||
it specifies the scene to render. This is not necessary now. When a single
|
||||
file contains only one ``Scene`` class, it will just render the ``Scene``
|
||||
class. When a single file contains more than one ``Scene`` class, manim will
|
||||
let you choose a ``Scene`` class. If your file contains multiple ``Scene``
|
||||
classes, and you want to render them all, you can use the ``-a`` flag.
|
||||
|
||||
As discussed previously, the ``-ql`` specifies low render quality. This does
|
||||
not look very good, but is very useful for rapid prototyping and testing. The
|
||||
other options that specify render quality are ``-qm``, ``-qh``, and ``-qk`` for
|
||||
medium, high, and 4k quality, respectively.
|
||||
As discussed previously, the ``-ql`` specifies low render quality (854x480
|
||||
15FPS). This does not look very good, but is very useful for rapid
|
||||
prototyping and testing. The other options that specify render quality are
|
||||
``-qm``, ``-qh``, ``-qp`` and ``-qk`` for medium (1280x720 30FPS), high
|
||||
(1920x1080 60FPS), 2k (2560x1440 60FPS) and 4k quality (3840x2160 60FPS),
|
||||
respectively.
|
||||
|
||||
The ``-p`` flag plays the animation once it is rendered. If you want to open
|
||||
the file browser at the location of the animation instead of playing it, you
|
||||
can use the ``-f`` flag. You can also omit these two flags.
|
||||
|
||||
Finally, by default manim will output .mp4 files. If you want your animations
|
||||
in .gif format instead, use the ``-i`` flag. The output files will be in the
|
||||
same folder as the .mp4 files, and with the same name, but a different file
|
||||
extension.
|
||||
in .gif format instead, use the ``--format gif`` flag. The output files will
|
||||
be in the same folder as the .mp4 files, and with the same name, but a
|
||||
different file extension.
|
||||
|
||||
This was a quick review of some of the most frequent command-line flags. For a
|
||||
thorough review of all flags available, see the
|
||||
:doc:`thematic guide on Manim's configuration system </guides/configuration>`.
|
||||
This was a quick review of some of the most frequent command-line flags.
|
||||
For a thorough review of all flags available, see the :doc:`thematic guide on
|
||||
Manim's configuration system </guides/configuration>`.
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ Now let's look at the next two lines:
|
|||
|
||||
class CreateCircle(Scene):
|
||||
def construct(self):
|
||||
...
|
||||
[...]
|
||||
|
||||
Most of the time, the code for scripting an animation is entirely contained within
|
||||
the :meth:`~.Scene.construct` method of a :class:`.Scene` class.
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ class SpiralInExample(Scene):
|
|||
],
|
||||
color=PURPLE_B,
|
||||
fill_opacity=1,
|
||||
stroke_width=0
|
||||
stroke_width=0,
|
||||
).shift(UP + 2 * RIGHT)
|
||||
shapes = VGroup(triangle, square, circle, pentagon, pi)
|
||||
self.play(SpiralIn(shapes, fade_in_fraction=0.9))
|
||||
|
|
|
|||
|
|
@ -406,7 +406,7 @@ class InteractiveDevelopment(Scene):
|
|||
self.play(Create(square))
|
||||
self.wait()
|
||||
|
||||
# This opens an iPython termnial where you can keep writing
|
||||
# This opens an iPython terminal where you can keep writing
|
||||
# lines as if they were part of this construct method.
|
||||
# In particular, 'square', 'circle' and 'self' will all be
|
||||
# part of the local namespace in that terminal.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
import click
|
||||
import cloup
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import Generator
|
||||
from contextlib import contextmanager
|
||||
from typing import Any, Generator
|
||||
from typing import Any
|
||||
|
||||
from .cli_colors import parse_cli_ctx
|
||||
from .logger_utils import make_logger
|
||||
|
|
@ -67,7 +68,6 @@ def tempconfig(temp: ManimConfig | dict[str, Any]) -> Generator[None, None, None
|
|||
8.0
|
||||
>>> with tempconfig({"frame_height": 100.0}):
|
||||
... print(config["frame_height"])
|
||||
...
|
||||
100.0
|
||||
>>> config["frame_height"]
|
||||
8.0
|
||||
|
|
|
|||
|
|
@ -35,15 +35,18 @@ def parse_cli_ctx(parser: configparser.SectionProxy) -> Context:
|
|||
theme = parser["theme"] if parser["theme"] else None
|
||||
if theme is None:
|
||||
formatter = HelpFormatter.settings(
|
||||
theme=HelpTheme(**theme_settings), **formatter_settings # type: ignore[arg-type]
|
||||
theme=HelpTheme(**theme_settings),
|
||||
**formatter_settings, # type: ignore[arg-type]
|
||||
)
|
||||
elif theme.lower() == "dark":
|
||||
formatter = HelpFormatter.settings(
|
||||
theme=HelpTheme.dark().with_(**theme_settings), **formatter_settings # type: ignore[arg-type]
|
||||
theme=HelpTheme.dark().with_(**theme_settings),
|
||||
**formatter_settings, # type: ignore[arg-type]
|
||||
)
|
||||
elif theme.lower() == "light":
|
||||
formatter = HelpFormatter.settings(
|
||||
theme=HelpTheme.light().with_(**theme_settings), **formatter_settings # type: ignore[arg-type]
|
||||
theme=HelpTheme.light().with_(**theme_settings),
|
||||
**formatter_settings, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
return Context.settings(
|
||||
|
|
|
|||
|
|
@ -221,8 +221,6 @@ repr_number = green
|
|||
# Uncomment the following line to manually set the loglevel for ffmpeg. See
|
||||
# ffmpeg manpage for accepted values
|
||||
loglevel = ERROR
|
||||
# defaults to the one present in path
|
||||
ffmpeg_executable = ffmpeg
|
||||
|
||||
[jupyter]
|
||||
media_embed = False
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ Both ``logger`` and ``console`` use the ``rich`` library to produce rich text
|
|||
format.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
|
|
@ -99,6 +100,10 @@ def make_logger(
|
|||
logger.addHandler(rich_handler)
|
||||
logger.setLevel(verbosity)
|
||||
|
||||
if not (libav_logger := logging.getLogger()).hasHandlers():
|
||||
libav_logger.addHandler(rich_handler)
|
||||
libav_logger.setLevel(verbosity)
|
||||
|
||||
return logger, console, error_console
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ movie vs writing a single frame).
|
|||
See :doc:`/guides/configuration` for an introduction to Manim's configuration system.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
|
|
@ -19,9 +20,9 @@ import logging
|
|||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import Mapping, MutableMapping
|
||||
from collections.abc import Iterable, Iterator, Mapping, MutableMapping
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Iterable, Iterator, NoReturn
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, NoReturn
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -245,8 +246,7 @@ class ManimConfig(MutableMapping):
|
|||
config.background_color = RED
|
||||
|
||||
|
||||
class MyScene(Scene):
|
||||
...
|
||||
class MyScene(Scene): ...
|
||||
|
||||
the background color will be set to RED, regardless of the contents of
|
||||
``manim.cfg`` or the CLI arguments used when invoking manim.
|
||||
|
|
@ -263,7 +263,6 @@ class ManimConfig(MutableMapping):
|
|||
"dry_run",
|
||||
"enable_wireframe",
|
||||
"ffmpeg_loglevel",
|
||||
"ffmpeg_executable",
|
||||
"format",
|
||||
"flush_cache",
|
||||
"frame_height",
|
||||
|
|
@ -318,6 +317,7 @@ class ManimConfig(MutableMapping):
|
|||
"zero_pad",
|
||||
"force_window",
|
||||
"no_latex_cleanup",
|
||||
"preview_command",
|
||||
}
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
|
@ -651,7 +651,12 @@ class ManimConfig(MutableMapping):
|
|||
setattr(self, "window_size", window_size)
|
||||
|
||||
# plugins
|
||||
self.plugins = parser["CLI"].get("plugins", fallback="", raw=True).split(",")
|
||||
plugins = parser["CLI"].get("plugins", fallback="", raw=True)
|
||||
if plugins == "":
|
||||
plugins = []
|
||||
else:
|
||||
plugins = plugins.split(",")
|
||||
self.plugins = plugins
|
||||
# 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)
|
||||
|
|
@ -673,10 +678,6 @@ class ManimConfig(MutableMapping):
|
|||
if val:
|
||||
self.ffmpeg_loglevel = val
|
||||
|
||||
# TODO: Fix the mess above and below
|
||||
val = parser["ffmpeg"].get("ffmpeg_executable")
|
||||
setattr(self, "ffmpeg_executable", val)
|
||||
|
||||
try:
|
||||
val = parser["jupyter"].getboolean("media_embed")
|
||||
except ValueError:
|
||||
|
|
@ -767,6 +768,7 @@ class ManimConfig(MutableMapping):
|
|||
"force_window",
|
||||
"dry_run",
|
||||
"no_latex_cleanup",
|
||||
"preview_command",
|
||||
]:
|
||||
if hasattr(args, key):
|
||||
attr = getattr(args, key)
|
||||
|
|
@ -1016,6 +1018,14 @@ class ManimConfig(MutableMapping):
|
|||
def no_latex_cleanup(self, value: bool) -> None:
|
||||
self._set_boolean("no_latex_cleanup", value)
|
||||
|
||||
@property
|
||||
def preview_command(self) -> str:
|
||||
return self._d["preview_command"]
|
||||
|
||||
@preview_command.setter
|
||||
def preview_command(self, value: str) -> None:
|
||||
self._set_str("preview_command", value)
|
||||
|
||||
@property
|
||||
def verbosity(self) -> str:
|
||||
"""Logger verbosity; "DEBUG", "INFO", "WARNING", "ERROR", or "CRITICAL" (-v)."""
|
||||
|
|
@ -1042,6 +1052,7 @@ class ManimConfig(MutableMapping):
|
|||
val,
|
||||
[None, "png", "gif", "mp4", "mov", "webm"],
|
||||
)
|
||||
self.resolve_movie_file_extension(self.transparent)
|
||||
if self.format == "webm":
|
||||
logging.getLogger("manim").warning(
|
||||
"Output format set as webm, this can be slower than other formats",
|
||||
|
|
@ -1059,15 +1070,7 @@ class ManimConfig(MutableMapping):
|
|||
val,
|
||||
["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
||||
)
|
||||
|
||||
@property
|
||||
def ffmpeg_executable(self) -> str:
|
||||
"""Custom path to the ffmpeg executable."""
|
||||
return self._d["ffmpeg_executable"]
|
||||
|
||||
@ffmpeg_executable.setter
|
||||
def ffmpeg_executable(self, value: str) -> None:
|
||||
self._set_str("ffmpeg_executable", value)
|
||||
logging.getLogger("libav").setLevel(self.ffmpeg_loglevel)
|
||||
|
||||
@property
|
||||
def media_embed(self) -> bool:
|
||||
|
|
@ -1200,7 +1203,7 @@ class ManimConfig(MutableMapping):
|
|||
|
||||
@property
|
||||
def upto_animation_number(self) -> int:
|
||||
"""Stop rendering animations at this nmber. Use -1 to avoid skipping (-n)."""
|
||||
"""Stop rendering animations at this number. Use -1 to avoid skipping (-n)."""
|
||||
return self._d["upto_animation_number"]
|
||||
|
||||
@upto_animation_number.setter
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
"""Animate mobjects."""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
|
|
@ -15,8 +14,9 @@ from ..utils.rate_functions import linear, smooth
|
|||
__all__ = ["Animation", "Wait", "override_animation"]
|
||||
|
||||
|
||||
from collections.abc import Iterable, Sequence
|
||||
from copy import deepcopy
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
|
|
@ -193,11 +193,6 @@ class Animation:
|
|||
method.
|
||||
|
||||
"""
|
||||
if self.run_time <= 0:
|
||||
raise ValueError(
|
||||
f"{self} has a run_time of <= 0 seconds, this cannot be rendered correctly. "
|
||||
"Please set the run_time to be positive"
|
||||
)
|
||||
self.starting_mobject = self.create_starting_mobject()
|
||||
if self.suspend_mobject_updating:
|
||||
# All calls to self.mobject's internal updaters
|
||||
|
|
@ -405,6 +400,7 @@ class Animation:
|
|||
self.run_time = run_time
|
||||
return self
|
||||
|
||||
# TODO: is this getter even necessary?
|
||||
def get_run_time(self) -> float:
|
||||
"""Get the run time of the animation.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +1,26 @@
|
|||
"""Tools for displaying multiple animations at once."""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import types
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Sequence
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
|
||||
import numpy as np
|
||||
|
||||
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.scene.scene import Scene
|
||||
from manim.utils.iterables import remove_list_redundancies
|
||||
from manim.utils.parameter_parsing import flatten_iterable_parameters
|
||||
|
||||
from .._config import config
|
||||
from ..animation.animation import Animation, prepare_animation
|
||||
from ..constants import RendererType
|
||||
from ..mobject.mobject import Group, Mobject
|
||||
from ..scene.scene import Scene
|
||||
from ..utils.iterables import remove_list_redundancies
|
||||
from ..utils.rate_functions import linear
|
||||
from manim.utils.rate_functions import linear
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup
|
||||
|
||||
from ..mobject.types.vectorized_mobject import VGroup
|
||||
from manim.mobject.types.vectorized_mobject import VGroup
|
||||
|
||||
__all__ = ["AnimationGroup", "Succession", "LaggedStart", "LaggedStartMap"]
|
||||
|
||||
|
|
@ -84,16 +82,13 @@ class AnimationGroup(Animation):
|
|||
return list(self.group)
|
||||
|
||||
def begin(self) -> None:
|
||||
if self.run_time <= 0:
|
||||
tmp = (
|
||||
"Please set the run_time to be positive"
|
||||
if len(self.animations) != 0
|
||||
else "Please add at least one Animation with positive run_time"
|
||||
)
|
||||
if not self.animations:
|
||||
raise ValueError(
|
||||
f"{self} has a run_time of 0 seconds, this cannot be "
|
||||
f"rendered correctly. {tmp}."
|
||||
f"Trying to play {self} without animations, this is not supported. "
|
||||
"Please add at least one subanimation."
|
||||
)
|
||||
|
||||
self.anim_group_time = 0.0
|
||||
if self.suspend_mobject_updating:
|
||||
self.group.suspend_updating()
|
||||
for anim in self.animations:
|
||||
|
|
@ -104,8 +99,9 @@ class AnimationGroup(Animation):
|
|||
anim._setup_scene(scene)
|
||||
|
||||
def finish(self) -> None:
|
||||
for anim in self.animations:
|
||||
anim.finish()
|
||||
self.interpolate(1)
|
||||
self.anims_begun[:] = True
|
||||
self.anims_finished[:] = True
|
||||
if self.suspend_mobject_updating:
|
||||
self.group.resume_updating()
|
||||
|
||||
|
|
@ -117,7 +113,9 @@ class AnimationGroup(Animation):
|
|||
anim.clean_up_from_scene(scene)
|
||||
|
||||
def update_mobjects(self, dt: float) -> None:
|
||||
for anim in self.animations:
|
||||
for anim in self.anims_with_timings["anim"][
|
||||
self.anims_begun & ~self.anims_finished
|
||||
]:
|
||||
anim.update_mobjects(dt)
|
||||
|
||||
def init_run_time(self, run_time) -> float:
|
||||
|
|
@ -134,22 +132,30 @@ class AnimationGroup(Animation):
|
|||
The duration of the animation in seconds.
|
||||
"""
|
||||
self.build_animations_with_timings()
|
||||
if self.anims_with_timings:
|
||||
self.max_end_time = np.max([awt[2] for awt in self.anims_with_timings])
|
||||
else:
|
||||
self.max_end_time = 0
|
||||
# Note: if lag_ratio < 1, then not necessarily the final animation's
|
||||
# end time will be the max end time! Therefore we must calculate the
|
||||
# maximum over all the end times, and not just take the last one.
|
||||
# Example: if you want to play 2 animations of 10s and 1s with a
|
||||
# lag_ratio of 0.1, the 1st one will end at t=10 and the 2nd one will
|
||||
# end at t=2, so the AnimationGroup will end at t=10.
|
||||
self.max_end_time = max(self.anims_with_timings["end"], default=0)
|
||||
return self.max_end_time if run_time is None else run_time
|
||||
|
||||
def build_animations_with_timings(self) -> None:
|
||||
"""Creates a list of triplets of the form (anim, start_time, end_time)."""
|
||||
self.anims_with_timings = []
|
||||
curr_time: float = 0
|
||||
for anim in self.animations:
|
||||
start_time: float = curr_time
|
||||
end_time: float = start_time + anim.get_run_time()
|
||||
self.anims_with_timings.append((anim, start_time, end_time))
|
||||
# Start time of next animation is based on the lag_ratio
|
||||
curr_time = (1 - self.lag_ratio) * start_time + self.lag_ratio * end_time
|
||||
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)
|
||||
if num_animations == 0:
|
||||
return
|
||||
|
||||
lags = run_times[:-1] * self.lag_ratio
|
||||
self.anims_with_timings["anim"] = self.animations
|
||||
self.anims_with_timings["start"][1:] = np.add.accumulate(lags)
|
||||
self.anims_with_timings["end"] = self.anims_with_timings["start"] + run_times
|
||||
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
# Note, if the run_time of AnimationGroup has been
|
||||
|
|
@ -157,14 +163,30 @@ class AnimationGroup(Animation):
|
|||
# times might not correspond to actual times,
|
||||
# e.g. of the surrounding scene. Instead they'd
|
||||
# be a rescaled version. But that's okay!
|
||||
time = self.rate_func(alpha) * self.max_end_time
|
||||
for anim, start_time, end_time in self.anims_with_timings:
|
||||
anim_time = end_time - start_time
|
||||
if anim_time == 0:
|
||||
sub_alpha = 0
|
||||
else:
|
||||
sub_alpha = np.clip((time - start_time) / anim_time, 0, 1)
|
||||
anim.interpolate(sub_alpha)
|
||||
anim_group_time = self.rate_func(alpha) * self.max_end_time
|
||||
time_goes_back = anim_group_time < self.anim_group_time
|
||||
|
||||
# Only update ongoing animations
|
||||
awt = self.anims_with_timings
|
||||
new_begun = anim_group_time >= awt["start"]
|
||||
new_finished = anim_group_time > awt["end"]
|
||||
to_update = awt[
|
||||
(self.anims_begun | new_begun) & (~self.anims_finished | ~new_finished)
|
||||
]
|
||||
|
||||
run_times = to_update["end"] - to_update["start"]
|
||||
sub_alphas = (anim_group_time - to_update["start"]) / run_times
|
||||
if time_goes_back:
|
||||
sub_alphas[sub_alphas < 0] = 0
|
||||
else:
|
||||
sub_alphas[sub_alphas > 1] = 1
|
||||
|
||||
for anim_to_update, sub_alpha in zip(to_update["anim"], sub_alphas):
|
||||
anim_to_update.interpolate(sub_alpha)
|
||||
|
||||
self.anim_group_time = anim_group_time
|
||||
self.anims_begun = new_begun
|
||||
self.anims_finished = new_finished
|
||||
|
||||
|
||||
class Succession(AnimationGroup):
|
||||
|
|
@ -208,7 +230,11 @@ class Succession(AnimationGroup):
|
|||
super().__init__(*animations, lag_ratio=lag_ratio, **kwargs)
|
||||
|
||||
def begin(self) -> None:
|
||||
assert len(self.animations) > 0
|
||||
if not self.animations:
|
||||
raise ValueError(
|
||||
f"Trying to play {self} without animations, this is not supported. "
|
||||
"Please add at least one subanimation."
|
||||
)
|
||||
self.update_active_animation(0)
|
||||
|
||||
def finish(self) -> None:
|
||||
|
|
@ -239,8 +265,8 @@ class Succession(AnimationGroup):
|
|||
self.active_animation = self.animations[index]
|
||||
self.active_animation._setup_scene(self.scene)
|
||||
self.active_animation.begin()
|
||||
self.active_start_time = self.anims_with_timings[index][1]
|
||||
self.active_end_time = self.anims_with_timings[index][2]
|
||||
self.active_start_time = self.anims_with_timings[index]["start"]
|
||||
self.active_end_time = self.anims_with_timings[index]["end"]
|
||||
|
||||
def next_animation(self) -> None:
|
||||
"""Proceeds to the next animation.
|
||||
|
|
@ -257,7 +283,7 @@ class Succession(AnimationGroup):
|
|||
self.next_animation()
|
||||
if self.active_animation is not None and self.active_start_time is not None:
|
||||
elapsed = current_time - self.active_start_time
|
||||
active_run_time = self.active_animation.get_run_time()
|
||||
active_run_time = self.active_animation.run_time
|
||||
subalpha = elapsed / active_run_time if active_run_time != 0.0 else 1.0
|
||||
self.active_animation.interpolate(subalpha)
|
||||
|
||||
|
|
|
|||
|
|
@ -70,17 +70,22 @@ __all__ = [
|
|||
"RemoveTextLetterByLetter",
|
||||
"ShowSubmobjectsOneByOne",
|
||||
"AddTextWordByWord",
|
||||
"TypeWithCursor",
|
||||
"UntypeWithCursor",
|
||||
]
|
||||
|
||||
|
||||
import itertools as it
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Sequence
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
|
||||
import numpy as np
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.text.text_mobject import Text
|
||||
from manim.scene.scene import Scene
|
||||
|
||||
from manim.constants import RIGHT, TAU
|
||||
from manim.mobject.opengl.opengl_surface import OpenGLSurface
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
|
||||
from manim.utils.color import ManimColor
|
||||
|
|
@ -88,7 +93,6 @@ from manim.utils.color import ManimColor
|
|||
from .. import config
|
||||
from ..animation.animation import Animation
|
||||
from ..animation.composition import Succession
|
||||
from ..constants import TAU
|
||||
from ..mobject.mobject import Group, Mobject
|
||||
from ..mobject.types.vectorized_mobject import VMobject
|
||||
from ..utils.bezier import integer_interpolate
|
||||
|
|
@ -456,7 +460,7 @@ class SpiralIn(Animation):
|
|||
fade_in_fraction=0.3,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self.shapes = shapes
|
||||
self.shapes = shapes.copy()
|
||||
self.scale_factor = scale_factor
|
||||
self.shape_center = shapes.get_center()
|
||||
self.fade_in_fraction = fade_in_fraction
|
||||
|
|
@ -473,15 +477,21 @@ class SpiralIn(Animation):
|
|||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
alpha = self.rate_func(alpha)
|
||||
for shape in self.shapes:
|
||||
for original_shape, shape in zip(self.shapes, self.mobject):
|
||||
shape.restore()
|
||||
shape.save_state()
|
||||
opacity = shape.get_fill_opacity()
|
||||
new_opacity = min(opacity, alpha * opacity / self.fade_in_fraction)
|
||||
fill_opacity = original_shape.get_fill_opacity()
|
||||
stroke_opacity = original_shape.get_stroke_opacity()
|
||||
new_fill_opacity = min(
|
||||
fill_opacity, alpha * fill_opacity / self.fade_in_fraction
|
||||
)
|
||||
new_stroke_opacity = min(
|
||||
stroke_opacity, alpha * stroke_opacity / self.fade_in_fraction
|
||||
)
|
||||
shape.shift((shape.final_position - shape.initial_position) * alpha)
|
||||
shape.rotate(TAU * alpha, about_point=self.shape_center)
|
||||
shape.rotate(-TAU * alpha, about_point=shape.get_center_of_mass())
|
||||
shape.set_opacity(new_opacity)
|
||||
shape.set_fill(opacity=new_fill_opacity)
|
||||
shape.set_stroke(opacity=new_stroke_opacity)
|
||||
|
||||
|
||||
class ShowIncreasingSubsets(Animation):
|
||||
|
|
@ -668,3 +678,176 @@ class AddTextWordByWord(Succession):
|
|||
)
|
||||
)
|
||||
super().__init__(*anims, **kwargs)
|
||||
|
||||
|
||||
class TypeWithCursor(AddTextLetterByLetter):
|
||||
"""Similar to :class:`~.AddTextLetterByLetter` , but with an additional cursor mobject at the end.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
time_per_char
|
||||
Frequency of appearance of the letters.
|
||||
cursor
|
||||
:class:`~.Mobject` shown after the last added letter.
|
||||
buff
|
||||
Controls how far away the cursor is to the right of the last added letter.
|
||||
keep_cursor_y
|
||||
If ``True``, the cursor's y-coordinate is set to the center of the ``Text`` and remains the same throughout the animation. Otherwise, it is set to the center of the last added letter.
|
||||
leave_cursor_on
|
||||
Whether to show the cursor after the animation.
|
||||
|
||||
.. tip::
|
||||
This is currently only possible for class:`~.Text` and not for class:`~.MathTex`.
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim:: InsertingTextExample
|
||||
:ref_classes: Blink
|
||||
|
||||
class InsertingTextExample(Scene):
|
||||
def construct(self):
|
||||
text = Text("Inserting", color=PURPLE).scale(1.5).to_edge(LEFT)
|
||||
cursor = Rectangle(
|
||||
color = GREY_A,
|
||||
fill_color = GREY_A,
|
||||
fill_opacity = 1.0,
|
||||
height = 1.1,
|
||||
width = 0.5,
|
||||
).move_to(text[0]) # Position the cursor
|
||||
|
||||
self.play(TypeWithCursor(text, cursor))
|
||||
self.play(Blink(cursor, blinks=2))
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
text: Text,
|
||||
cursor: Mobject,
|
||||
buff: float = 0.1,
|
||||
keep_cursor_y: bool = True,
|
||||
leave_cursor_on: bool = True,
|
||||
time_per_char: float = 0.1,
|
||||
reverse_rate_function=False,
|
||||
introducer=True,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self.cursor = cursor
|
||||
self.buff = buff
|
||||
self.keep_cursor_y = keep_cursor_y
|
||||
self.leave_cursor_on = leave_cursor_on
|
||||
super().__init__(
|
||||
text,
|
||||
time_per_char=time_per_char,
|
||||
reverse_rate_function=reverse_rate_function,
|
||||
introducer=introducer,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def begin(self) -> None:
|
||||
self.y_cursor = self.cursor.get_y()
|
||||
self.cursor.initial_position = self.mobject.get_center()
|
||||
if self.keep_cursor_y:
|
||||
self.cursor.set_y(self.y_cursor)
|
||||
|
||||
self.cursor.set_opacity(0)
|
||||
self.mobject.add(self.cursor)
|
||||
super().begin()
|
||||
|
||||
def finish(self) -> None:
|
||||
if self.leave_cursor_on:
|
||||
self.cursor.set_opacity(1)
|
||||
else:
|
||||
self.cursor.set_opacity(0)
|
||||
self.mobject.remove(self.cursor)
|
||||
super().finish()
|
||||
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
if not self.leave_cursor_on:
|
||||
scene.remove(self.cursor)
|
||||
super().clean_up_from_scene(scene)
|
||||
|
||||
def update_submobject_list(self, index: int) -> None:
|
||||
for mobj in self.all_submobs[:index]:
|
||||
mobj.set_opacity(1)
|
||||
|
||||
for mobj in self.all_submobs[index:]:
|
||||
mobj.set_opacity(0)
|
||||
|
||||
if index != 0:
|
||||
self.cursor.next_to(
|
||||
self.all_submobs[index - 1], RIGHT, buff=self.buff
|
||||
).set_y(self.cursor.initial_position[1])
|
||||
else:
|
||||
self.cursor.move_to(self.all_submobs[0]).set_y(
|
||||
self.cursor.initial_position[1]
|
||||
)
|
||||
|
||||
if self.keep_cursor_y:
|
||||
self.cursor.set_y(self.y_cursor)
|
||||
self.cursor.set_opacity(1)
|
||||
|
||||
|
||||
class UntypeWithCursor(TypeWithCursor):
|
||||
"""Similar to :class:`~.RemoveTextLetterByLetter` , but with an additional cursor mobject at the end.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
time_per_char
|
||||
Frequency of appearance of the letters.
|
||||
cursor
|
||||
:class:`~.Mobject` shown after the last added letter.
|
||||
buff
|
||||
Controls how far away the cursor is to the right of the last added letter.
|
||||
keep_cursor_y
|
||||
If ``True``, the cursor's y-coordinate is set to the center of the ``Text`` and remains the same throughout the animation. Otherwise, it is set to the center of the last added letter.
|
||||
leave_cursor_on
|
||||
Whether to show the cursor after the animation.
|
||||
|
||||
.. tip::
|
||||
This is currently only possible for class:`~.Text` and not for class:`~.MathTex`.
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim:: DeletingTextExample
|
||||
:ref_classes: Blink
|
||||
|
||||
class DeletingTextExample(Scene):
|
||||
def construct(self):
|
||||
text = Text("Deleting", color=PURPLE).scale(1.5).to_edge(LEFT)
|
||||
cursor = Rectangle(
|
||||
color = GREY_A,
|
||||
fill_color = GREY_A,
|
||||
fill_opacity = 1.0,
|
||||
height = 1.1,
|
||||
width = 0.5,
|
||||
).move_to(text[0]) # Position the cursor
|
||||
|
||||
self.play(UntypeWithCursor(text, cursor))
|
||||
self.play(Blink(cursor, blinks=2))
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
text: Text,
|
||||
cursor: VMobject | None = None,
|
||||
time_per_char: float = 0.1,
|
||||
reverse_rate_function=True,
|
||||
introducer=False,
|
||||
remover=True,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
text,
|
||||
cursor=cursor,
|
||||
time_per_char=time_per_char,
|
||||
reverse_rate_function=reverse_rate_function,
|
||||
introducer=introducer,
|
||||
remover=remover,
|
||||
**kwargs,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@
|
|||
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = [
|
||||
|
|
|
|||
|
|
@ -25,19 +25,22 @@ Examples
|
|||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = [
|
||||
"FocusOn",
|
||||
"Indicate",
|
||||
"Flash",
|
||||
"ShowPassingFlash",
|
||||
"ShowPassingFlashWithThinningStrokeWidth",
|
||||
"ShowCreationThenFadeOut",
|
||||
"ApplyWave",
|
||||
"Circumscribe",
|
||||
"Wiggle",
|
||||
"Blink",
|
||||
]
|
||||
|
||||
from typing import Callable, Iterable, Optional, Tuple, Type, Union
|
||||
from collections.abc import Iterable
|
||||
from typing import Callable
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -54,12 +57,12 @@ from ..animation.creation import Create, ShowPartial, Uncreate
|
|||
from ..animation.fading import FadeIn, FadeOut
|
||||
from ..animation.movement import Homotopy
|
||||
from ..animation.transform import Transform
|
||||
from ..animation.updaters.update import UpdateFromFunc
|
||||
from ..constants import *
|
||||
from ..mobject.mobject import Mobject
|
||||
from ..mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from ..utils.bezier import interpolate, inverse_interpolate
|
||||
from ..utils.color import GREY, YELLOW, ParsableManimColor
|
||||
from ..utils.deprecation import deprecated
|
||||
from ..utils.rate_functions import smooth, there_and_back, wiggle
|
||||
from ..utils.space_ops import normalize
|
||||
|
||||
|
|
@ -77,8 +80,6 @@ class FocusOn(Transform):
|
|||
The color of the spotlight.
|
||||
run_time
|
||||
The duration of the animation.
|
||||
kwargs
|
||||
Additional arguments to be passed to the :class:`~.Succession` constructor
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
|
@ -94,11 +95,11 @@ class FocusOn(Transform):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
focus_point: Union[np.ndarray, Mobject],
|
||||
focus_point: np.ndarray | Mobject,
|
||||
opacity: float = 0.2,
|
||||
color: str = GREY,
|
||||
run_time: float = 2,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self.focus_point = focus_point
|
||||
self.color = color
|
||||
|
|
@ -151,8 +152,8 @@ class Indicate(Transform):
|
|||
mobject: Mobject,
|
||||
scale_factor: float = 1.2,
|
||||
color: str = YELLOW,
|
||||
rate_func: Callable[[float, Optional[float]], np.ndarray] = there_and_back,
|
||||
**kwargs
|
||||
rate_func: Callable[[float, float | None], np.ndarray] = there_and_back,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self.color = color
|
||||
self.scale_factor = scale_factor
|
||||
|
|
@ -218,7 +219,7 @@ class Flash(AnimationGroup):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
point: Union[np.ndarray, Mobject],
|
||||
point: np.ndarray | Mobject,
|
||||
line_length: float = 0.2,
|
||||
num_lines: int = 12,
|
||||
flash_radius: float = 0.1,
|
||||
|
|
@ -226,7 +227,7 @@ class Flash(AnimationGroup):
|
|||
color: str = YELLOW,
|
||||
time_width: float = 1,
|
||||
run_time: float = 1.0,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
) -> None:
|
||||
if isinstance(point, Mobject):
|
||||
self.point = point.get_center()
|
||||
|
|
@ -256,7 +257,7 @@ class Flash(AnimationGroup):
|
|||
lines.set_stroke(width=self.line_stroke_width)
|
||||
return lines
|
||||
|
||||
def create_line_anims(self) -> Iterable["ShowPassingFlash"]:
|
||||
def create_line_anims(self) -> Iterable[ShowPassingFlash]:
|
||||
return [
|
||||
ShowPassingFlash(
|
||||
line,
|
||||
|
|
@ -302,11 +303,11 @@ 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) -> 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]:
|
||||
tw = self.time_width
|
||||
upper = interpolate(0, 1 + tw, alpha)
|
||||
lower = upper - tw
|
||||
|
|
@ -342,16 +343,6 @@ class ShowPassingFlashWithThinningStrokeWidth(AnimationGroup):
|
|||
)
|
||||
|
||||
|
||||
@deprecated(
|
||||
since="v0.15.0",
|
||||
until="v0.16.0",
|
||||
message="Use Create then FadeOut to achieve this effect.",
|
||||
)
|
||||
class ShowCreationThenFadeOut(Succession):
|
||||
def __init__(self, mobject: Mobject, remover: bool = True, **kwargs) -> None:
|
||||
super().__init__(Create(mobject), FadeOut(mobject), remover=remover, **kwargs)
|
||||
|
||||
|
||||
class ApplyWave(Homotopy):
|
||||
"""Send a wave through the Mobject distorting it temporarily.
|
||||
|
||||
|
|
@ -404,7 +395,7 @@ class ApplyWave(Homotopy):
|
|||
time_width: float = 1,
|
||||
ripples: int = 1,
|
||||
run_time: float = 2,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
) -> None:
|
||||
x_min = mobject.get_left()[0]
|
||||
x_max = mobject.get_right()[0]
|
||||
|
|
@ -470,7 +461,7 @@ class ApplyWave(Homotopy):
|
|||
y: float,
|
||||
z: float,
|
||||
t: float,
|
||||
) -> Tuple[float, float, float]:
|
||||
) -> tuple[float, float, float]:
|
||||
upper = interpolate(0, 1 + time_width, t)
|
||||
lower = upper - time_width
|
||||
relative_x = inverse_interpolate(x_min, x_max, x)
|
||||
|
|
@ -520,10 +511,10 @@ class Wiggle(Animation):
|
|||
scale_value: float = 1.1,
|
||||
rotation_angle: float = 0.01 * TAU,
|
||||
n_wiggles: int = 6,
|
||||
scale_about_point: Optional[np.ndarray] = None,
|
||||
rotate_about_point: Optional[np.ndarray] = None,
|
||||
scale_about_point: np.ndarray | None = None,
|
||||
rotate_about_point: np.ndarray | None = None,
|
||||
run_time: float = 2,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self.scale_value = scale_value
|
||||
self.rotation_angle = rotation_angle
|
||||
|
|
@ -567,7 +558,7 @@ class Circumscribe(Succession):
|
|||
mobject
|
||||
The mobject to be circumscribed.
|
||||
shape
|
||||
The shape with which to surrond the given mobject. Should be either
|
||||
The shape with which to surround the given mobject. Should be either
|
||||
:class:`~.Rectangle` or :class:`~.Circle`
|
||||
fade_in
|
||||
Whether to make the surrounding shape to fade in. It will be drawn otherwise.
|
||||
|
|
@ -604,7 +595,7 @@ class Circumscribe(Succession):
|
|||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
shape: Type = Rectangle,
|
||||
shape: type = Rectangle,
|
||||
fade_in=False,
|
||||
fade_out=False,
|
||||
time_width=0.3,
|
||||
|
|
@ -612,7 +603,7 @@ class Circumscribe(Succession):
|
|||
color: ParsableManimColor = YELLOW,
|
||||
run_time=1,
|
||||
stroke_width=DEFAULT_STROKE_WIDTH,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
):
|
||||
if shape is Rectangle:
|
||||
frame = SurroundingRectangle(
|
||||
|
|
@ -654,3 +645,68 @@ class Circumscribe(Succession):
|
|||
super().__init__(
|
||||
ShowPassingFlash(frame, time_width, run_time=run_time), **kwargs
|
||||
)
|
||||
|
||||
|
||||
class Blink(Succession):
|
||||
"""Blink the mobject.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mobject
|
||||
The mobject to be blinked.
|
||||
time_on
|
||||
The duration that the mobject is shown for one blink.
|
||||
time_off
|
||||
The duration that the mobject is hidden for one blink.
|
||||
blinks
|
||||
The number of blinks
|
||||
hide_at_end
|
||||
Whether to hide the mobject at the end of the animation.
|
||||
kwargs
|
||||
Additional arguments to be passed to the :class:`~.Succession` constructor.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim:: BlinkingExample
|
||||
|
||||
class BlinkingExample(Scene):
|
||||
def construct(self):
|
||||
text = Text("Blinking").scale(1.5)
|
||||
self.add(text)
|
||||
self.play(Blink(text, blinks=3))
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
time_on: float = 0.5,
|
||||
time_off: float = 0.5,
|
||||
blinks: int = 1,
|
||||
hide_at_end: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
animations = [
|
||||
UpdateFromFunc(
|
||||
mobject,
|
||||
update_function=lambda mob: mob.set_opacity(1.0),
|
||||
run_time=time_on,
|
||||
),
|
||||
UpdateFromFunc(
|
||||
mobject,
|
||||
update_function=lambda mob: mob.set_opacity(0.0),
|
||||
run_time=time_off,
|
||||
),
|
||||
] * blinks
|
||||
|
||||
if not hide_at_end:
|
||||
animations.append(
|
||||
UpdateFromFunc(
|
||||
mobject,
|
||||
update_function=lambda mob: mob.set_opacity(1.0),
|
||||
run_time=time_on,
|
||||
),
|
||||
)
|
||||
|
||||
super().__init__(*animations, **kwargs)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["Rotating", "Rotate"]
|
||||
|
||||
from typing import TYPE_CHECKING, Callable, Sequence
|
||||
from collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -59,7 +60,7 @@ class Rotate(Transform):
|
|||
about_point
|
||||
The rotation center.
|
||||
about_edge
|
||||
If ``about_point``is ``None``, this argument specifies
|
||||
If ``about_point`` is ``None``, this argument specifies
|
||||
the direction of the bounding box point to be taken as
|
||||
the rotation center.
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["Broadcast"]
|
||||
|
||||
from typing import Any, Sequence
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from manim.animation.transform import Restore
|
||||
|
||||
|
|
|
|||
|
|
@ -4,15 +4,18 @@ from __future__ import annotations
|
|||
|
||||
import inspect
|
||||
import types
|
||||
from typing import Callable
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
|
||||
from numpy import piecewise
|
||||
|
||||
from ..animation.animation import Animation, Wait, prepare_animation
|
||||
from ..animation.composition import AnimationGroup
|
||||
from ..mobject.mobject import Mobject, Updater, _AnimationBuilder
|
||||
from ..mobject.mobject import Mobject, _AnimationBuilder
|
||||
from ..scene.scene import Scene
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..mobject.mobject import Updater
|
||||
|
||||
__all__ = ["ChangeSpeed"]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ __all__ = [
|
|||
|
||||
import inspect
|
||||
import types
|
||||
from typing import TYPE_CHECKING, Any, Callable, Iterable, Sequence
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
"""A camera converts the mobjects contained in a Scene into an array of pixels."""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["Camera", "BackgroundColoredVMobjectDisplayer"]
|
||||
|
|
@ -9,8 +8,9 @@ import copy
|
|||
import itertools as it
|
||||
import operator as op
|
||||
import pathlib
|
||||
from collections.abc import Iterable
|
||||
from functools import reduce
|
||||
from typing import Any, Callable, Iterable
|
||||
from typing import Any, Callable
|
||||
|
||||
import cairo
|
||||
import numpy as np
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ cfg``. Here you can specify options, subcommands, and subgroups for the cfg
|
|||
group.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ast import literal_eval
|
||||
|
|
|
|||
|
|
@ -5,11 +5,8 @@ from __future__ import annotations
|
|||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import Callable
|
||||
|
||||
from ..._config import config
|
||||
|
||||
__all__ = ["HEALTH_CHECKS"]
|
||||
|
||||
HEALTH_CHECKS = []
|
||||
|
|
@ -104,45 +101,6 @@ def is_manim_executable_associated_to_this_library():
|
|||
return b"manim.__main__" in manim_exec or b'"%~dp0\\manim"' in manim_exec
|
||||
|
||||
|
||||
@healthcheck(
|
||||
description="Checking whether ffmpeg is available",
|
||||
recommendation=(
|
||||
"Manim does not work without ffmpeg. Please follow our "
|
||||
"installation instructions "
|
||||
"at https://docs.manim.community/en/stable/installation.html "
|
||||
"to download ffmpeg. Then, either ...\n\n"
|
||||
"(a) ... make the ffmpeg executable available to your system's PATH,\n"
|
||||
"(b) or, alternatively, use <manim cfg write --open> to create a "
|
||||
"custom configuration and set the ffmpeg_executable variable to the "
|
||||
"full absolute path to the ffmpeg executable."
|
||||
),
|
||||
)
|
||||
def is_ffmpeg_available():
|
||||
path_to_ffmpeg = shutil.which(config.ffmpeg_executable)
|
||||
return path_to_ffmpeg is not None and os.access(path_to_ffmpeg, os.X_OK)
|
||||
|
||||
|
||||
@healthcheck(
|
||||
description="Checking whether ffmpeg is working",
|
||||
recommendation=(
|
||||
"Your installed version of ffmpeg does not support x264 encoding, "
|
||||
"which manim requires. Please follow our installation instructions "
|
||||
"at https://docs.manim.community/en/stable/installation.html "
|
||||
"to download and install a newer version of ffmpeg."
|
||||
),
|
||||
skip_on_failed=[is_ffmpeg_available],
|
||||
)
|
||||
def is_ffmpeg_working():
|
||||
ffmpeg_version = subprocess.run(
|
||||
[config.ffmpeg_executable, "-version"],
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout.decode()
|
||||
return (
|
||||
ffmpeg_version.startswith("ffmpeg version")
|
||||
and "--enable-libx264" in ffmpeg_version
|
||||
)
|
||||
|
||||
|
||||
@healthcheck(
|
||||
description="Checking whether latex is available",
|
||||
recommendation=(
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ In particular, this class is what allows ``manim`` to act as ``manim render``.
|
|||
This library isn't used as a dependency as we need to inherit from ``cloup.Group`` instead
|
||||
of ``click.Group``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
|
||||
import cloup
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ init``. Here you can specify options, subcommands, and subgroups for the init
|
|||
group.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ plugin``. Here you can specify options, subcommands, and subgroups for the plugi
|
|||
group.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import cloup
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ Manim's render subcommand is accessed in the command-line interface via
|
|||
can specify options, and arguments for the render command.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import http.client
|
||||
|
|
@ -142,14 +143,14 @@ def render(
|
|||
)
|
||||
except Exception:
|
||||
logger.debug("Something went wrong: %s", warn_prompt)
|
||||
|
||||
stable = json_data["info"]["version"]
|
||||
if stable != __version__:
|
||||
console.print(
|
||||
f"You are using manim version [red]v{__version__}[/red], but version [green]v{stable}[/green] is available.",
|
||||
)
|
||||
console.print(
|
||||
"You should consider upgrading via [yellow]pip install -U manim[/yellow]",
|
||||
)
|
||||
else:
|
||||
stable = json_data["info"]["version"]
|
||||
if stable != __version__:
|
||||
console.print(
|
||||
f"You are using manim version [red]v{__version__}[/red], but version [green]v{stable}[/green] is available.",
|
||||
)
|
||||
console.print(
|
||||
"You should consider upgrading via [yellow]pip install -U manim[/yellow]",
|
||||
)
|
||||
|
||||
return args
|
||||
|
|
|
|||
|
|
@ -105,4 +105,9 @@ global_options = option_group(
|
|||
help="Prevents deletion of .aux, .dvi, and .log files produced by Tex and MathTex.",
|
||||
default=False,
|
||||
),
|
||||
option(
|
||||
"--preview_command",
|
||||
help="The command used to preview the output file (for example vlc for video files)",
|
||||
default="",
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ class _BooleanOps(VMobject, metaclass=ConvertToOpenGL):
|
|||
n1, n2 = self._convert_2d_to_3d_array(points)
|
||||
vmobject.add_quadratic_bezier_curve_to(n1, n2)
|
||||
else:
|
||||
raise Exception("Unsupported: %s" % path_verb)
|
||||
raise Exception(f"Unsupported: {path_verb}")
|
||||
return vmobject
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ __all__ = [
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim import config
|
||||
from manim.constants import *
|
||||
|
|
@ -31,6 +30,8 @@ from manim.utils.color import WHITE
|
|||
from manim.utils.space_ops import angle_of_vector, line_intersection, normalize
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim.typing import Point2D, Point3D, Vector3D
|
||||
from manim.utils.color import ParsableManimColor
|
||||
|
||||
|
|
@ -40,8 +41,8 @@ if TYPE_CHECKING:
|
|||
class Line(TipableVMobject):
|
||||
def __init__(
|
||||
self,
|
||||
start: Point3D = LEFT,
|
||||
end: Point3D = RIGHT,
|
||||
start: Point3D | Mobject = LEFT,
|
||||
end: Point3D | Mobject = RIGHT,
|
||||
buff: float = 0,
|
||||
path_arc: float | None = None,
|
||||
**kwargs,
|
||||
|
|
@ -62,16 +63,32 @@ class Line(TipableVMobject):
|
|||
|
||||
def set_points_by_ends(
|
||||
self,
|
||||
start: Point3D,
|
||||
end: Point3D,
|
||||
start: Point3D | Mobject,
|
||||
end: Point3D | Mobject,
|
||||
buff: float = 0,
|
||||
path_arc: float = 0,
|
||||
) -> None:
|
||||
"""Sets the points of the line based on its start and end points.
|
||||
Unlike :meth:`put_start_and_end_on`, this method respects `self.buff` and
|
||||
Mobject bounding boxes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
start
|
||||
The start point or Mobject of the line.
|
||||
end
|
||||
The end point or Mobject of the line.
|
||||
buff
|
||||
The empty space between the start and end of the line, by default 0.
|
||||
path_arc
|
||||
The angle of a circle spanned by this arc, by default 0 which is a straight line.
|
||||
"""
|
||||
self._set_start_and_end_attrs(start, end)
|
||||
if path_arc:
|
||||
arc = ArcBetweenPoints(self.start, self.end, angle=self.path_arc)
|
||||
self.set_points(arc.points)
|
||||
else:
|
||||
self.set_points_as_corners([start, end])
|
||||
self.set_points_as_corners([self.start, self.end])
|
||||
|
||||
self._account_for_buff(buff)
|
||||
|
||||
|
|
@ -92,7 +109,9 @@ class Line(TipableVMobject):
|
|||
self.pointwise_become_partial(self, buff_proportion, 1 - buff_proportion)
|
||||
return self
|
||||
|
||||
def _set_start_and_end_attrs(self, start: Point3D, end: Point3D) -> None:
|
||||
def _set_start_and_end_attrs(
|
||||
self, start: Point3D | Mobject, end: Point3D | Mobject
|
||||
) -> None:
|
||||
# If either start or end are Mobjects, this
|
||||
# gives their centers
|
||||
rough_start = self._pointify(start)
|
||||
|
|
@ -659,7 +678,9 @@ class Vector(Arrow):
|
|||
self.add(plane, vector_1, vector_2)
|
||||
"""
|
||||
|
||||
def __init__(self, direction: Vector3D = RIGHT, buff: float = 0, **kwargs) -> None:
|
||||
def __init__(
|
||||
self, direction: Point2D | Point3D = RIGHT, buff: float = 0, **kwargs
|
||||
) -> None:
|
||||
self.buff = buff
|
||||
if len(direction) == 2:
|
||||
direction = np.hstack([direction, 0])
|
||||
|
|
|
|||
|
|
@ -472,7 +472,6 @@ class Star(Polygon):
|
|||
Examples
|
||||
--------
|
||||
.. manim:: StarExample
|
||||
:save_as_gif:
|
||||
|
||||
class StarExample(Scene):
|
||||
def construct(self):
|
||||
|
|
|
|||
|
|
@ -60,8 +60,9 @@ class ArrowTip(VMobject, metaclass=ConvertToOpenGL):
|
|||
... RegularPolygon.__init__(self, n=5, **kwargs)
|
||||
... self.width = length
|
||||
... self.stretch_to_fit_height(length)
|
||||
>>> arr = Arrow(np.array([-2, -2, 0]), np.array([2, 2, 0]),
|
||||
... tip_shape=MyCustomArrowTip)
|
||||
>>> arr = Arrow(
|
||||
... np.array([-2, -2, 0]), np.array([2, 2, 0]), tip_shape=MyCustomArrowTip
|
||||
... )
|
||||
>>> isinstance(arr.tip, RegularPolygon)
|
||||
True
|
||||
>>> from manim import Scene, Create
|
||||
|
|
|
|||
|
|
@ -8,12 +8,21 @@ __all__ = [
|
|||
]
|
||||
|
||||
import itertools as it
|
||||
from collections.abc import Hashable, Iterable
|
||||
from copy import copy
|
||||
from typing import Hashable, Iterable
|
||||
from typing import TYPE_CHECKING, Any, Literal, Protocol, cast
|
||||
|
||||
import networkx as nx
|
||||
import numpy as np
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from manim.scene.scene import Scene
|
||||
from manim.typing import Point3D
|
||||
|
||||
NxGraph: TypeAlias = nx.classes.graph.Graph | nx.classes.digraph.DiGraph
|
||||
|
||||
from manim.animation.composition import AnimationGroup
|
||||
from manim.animation.creation import Create, Uncreate
|
||||
from manim.mobject.geometry.arc import Dot, LabeledDot
|
||||
|
|
@ -26,88 +35,290 @@ from manim.mobject.types.vectorized_mobject import VMobject
|
|||
from manim.utils.color import BLACK
|
||||
|
||||
|
||||
def _determine_graph_layout(
|
||||
nx_graph: nx.classes.graph.Graph | nx.classes.digraph.DiGraph,
|
||||
layout: str | dict = "spring",
|
||||
layout_scale: float = 2,
|
||||
layout_config: dict | None = None,
|
||||
class LayoutFunction(Protocol):
|
||||
"""A protocol for automatic layout functions that compute a layout for a graph to be used in :meth:`~.Graph.change_layout`.
|
||||
|
||||
.. note:: The layout function must be a pure function, i.e., it must not modify the graph passed to it.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Here is an example that arranges nodes in an n x m grid in sorted order.
|
||||
|
||||
.. manim:: CustomLayoutExample
|
||||
:save_last_frame:
|
||||
|
||||
class CustomLayoutExample(Scene):
|
||||
def construct(self):
|
||||
import numpy as np
|
||||
import networkx as nx
|
||||
|
||||
# create custom layout
|
||||
def custom_layout(
|
||||
graph: nx.Graph,
|
||||
scale: float | tuple[float, float, float] = 2,
|
||||
n: int | None = None,
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
):
|
||||
nodes = sorted(list(graph))
|
||||
height = len(nodes) // n
|
||||
return {
|
||||
node: (scale * np.array([
|
||||
(i % n) - (n-1)/2,
|
||||
-(i // n) + height/2,
|
||||
0
|
||||
])) for i, node in enumerate(graph)
|
||||
}
|
||||
|
||||
# draw graph
|
||||
n = 4
|
||||
graph = Graph(
|
||||
[i for i in range(4 * 2 - 1)],
|
||||
[(0, 1), (0, 4), (1, 2), (1, 5), (2, 3), (2, 6), (4, 5), (5, 6)],
|
||||
labels=True,
|
||||
layout=custom_layout,
|
||||
layout_config={'n': n}
|
||||
)
|
||||
self.add(graph)
|
||||
|
||||
Several automatic layouts are provided by manim, and can be used by passing their name as the ``layout`` parameter to :meth:`~.Graph.change_layout`.
|
||||
Alternatively, a custom layout function can be passed to :meth:`~.Graph.change_layout` as the ``layout`` parameter. Such a function must adhere to the :class:`~.LayoutFunction` protocol.
|
||||
|
||||
The :class:`~.LayoutFunction` s provided by manim are illustrated below:
|
||||
|
||||
- Circular Layout: places the vertices on a circle
|
||||
|
||||
.. manim:: CircularLayout
|
||||
:save_last_frame:
|
||||
|
||||
class CircularLayout(Scene):
|
||||
def construct(self):
|
||||
graph = Graph(
|
||||
[1, 2, 3, 4, 5, 6],
|
||||
[(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 1), (5, 1), (1, 3), (3, 5)],
|
||||
layout="circular",
|
||||
labels=True
|
||||
)
|
||||
self.add(graph)
|
||||
|
||||
- Kamada Kawai Layout: tries to place the vertices such that the given distances between them are respected
|
||||
|
||||
.. manim:: KamadaKawaiLayout
|
||||
:save_last_frame:
|
||||
|
||||
class KamadaKawaiLayout(Scene):
|
||||
def construct(self):
|
||||
from collections import defaultdict
|
||||
distances: dict[int, dict[int, float]] = defaultdict(dict)
|
||||
|
||||
# set desired distances
|
||||
distances[1][2] = 1 # distance between vertices 1 and 2 is 1
|
||||
distances[2][3] = 1 # distance between vertices 2 and 3 is 1
|
||||
distances[3][4] = 2 # etc
|
||||
distances[4][5] = 3
|
||||
distances[5][6] = 5
|
||||
distances[6][1] = 8
|
||||
|
||||
graph = Graph(
|
||||
[1, 2, 3, 4, 5, 6],
|
||||
[(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 1)],
|
||||
layout="kamada_kawai",
|
||||
layout_config={"dist": distances},
|
||||
layout_scale=4,
|
||||
labels=True
|
||||
)
|
||||
self.add(graph)
|
||||
|
||||
- Partite Layout: places vertices into distinct partitions
|
||||
|
||||
.. manim:: PartiteLayout
|
||||
:save_last_frame:
|
||||
|
||||
class PartiteLayout(Scene):
|
||||
def construct(self):
|
||||
graph = Graph(
|
||||
[1, 2, 3, 4, 5, 6],
|
||||
[(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 1), (5, 1), (1, 3), (3, 5)],
|
||||
layout="partite",
|
||||
layout_config={"partitions": [[1,2],[3,4],[5,6]]},
|
||||
labels=True
|
||||
)
|
||||
self.add(graph)
|
||||
|
||||
- Planar Layout: places vertices such that edges do not cross
|
||||
|
||||
.. manim:: PlanarLayout
|
||||
:save_last_frame:
|
||||
|
||||
class PlanarLayout(Scene):
|
||||
def construct(self):
|
||||
graph = Graph(
|
||||
[1, 2, 3, 4, 5, 6],
|
||||
[(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 1), (5, 1), (1, 3), (3, 5)],
|
||||
layout="planar",
|
||||
layout_scale=4,
|
||||
labels=True
|
||||
)
|
||||
self.add(graph)
|
||||
|
||||
- Random Layout: randomly places vertices
|
||||
|
||||
.. manim:: RandomLayout
|
||||
:save_last_frame:
|
||||
|
||||
class RandomLayout(Scene):
|
||||
def construct(self):
|
||||
graph = Graph(
|
||||
[1, 2, 3, 4, 5, 6],
|
||||
[(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 1), (5, 1), (1, 3), (3, 5)],
|
||||
layout="random",
|
||||
labels=True
|
||||
)
|
||||
self.add(graph)
|
||||
|
||||
- Shell Layout: places vertices in concentric circles
|
||||
|
||||
.. manim:: ShellLayout
|
||||
:save_last_frame:
|
||||
|
||||
class ShellLayout(Scene):
|
||||
def construct(self):
|
||||
nlist = [[1, 2, 3], [4, 5, 6, 7, 8, 9]]
|
||||
graph = Graph(
|
||||
[1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
[(1, 2), (2, 3), (3, 1), (4, 1), (4, 2), (5, 2), (6, 2), (6, 3), (7, 3), (8, 3), (8, 1), (9, 1)],
|
||||
layout="shell",
|
||||
layout_config={"nlist": nlist},
|
||||
labels=True
|
||||
)
|
||||
self.add(graph)
|
||||
|
||||
- Spectral Layout: places vertices using the eigenvectors of the graph Laplacian (clusters nodes which are an approximation of the ratio cut)
|
||||
|
||||
.. manim:: SpectralLayout
|
||||
:save_last_frame:
|
||||
|
||||
class SpectralLayout(Scene):
|
||||
def construct(self):
|
||||
graph = Graph(
|
||||
[1, 2, 3, 4, 5, 6],
|
||||
[(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 1), (5, 1), (1, 3), (3, 5)],
|
||||
layout="spectral",
|
||||
labels=True
|
||||
)
|
||||
self.add(graph)
|
||||
|
||||
- Sprial Layout: places vertices in a spiraling pattern
|
||||
|
||||
.. manim:: SpiralLayout
|
||||
:save_last_frame:
|
||||
|
||||
class SpiralLayout(Scene):
|
||||
def construct(self):
|
||||
graph = Graph(
|
||||
[1, 2, 3, 4, 5, 6],
|
||||
[(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 1), (5, 1), (1, 3), (3, 5)],
|
||||
layout="spiral",
|
||||
labels=True
|
||||
)
|
||||
self.add(graph)
|
||||
|
||||
- Spring Layout: places nodes according to the Fruchterman-Reingold force-directed algorithm (attempts to minimize edge length while maximizing node separation)
|
||||
|
||||
.. manim:: SpringLayout
|
||||
:save_last_frame:
|
||||
|
||||
class SpringLayout(Scene):
|
||||
def construct(self):
|
||||
graph = Graph(
|
||||
[1, 2, 3, 4, 5, 6],
|
||||
[(1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 1), (5, 1), (1, 3), (3, 5)],
|
||||
layout="spring",
|
||||
labels=True
|
||||
)
|
||||
self.add(graph)
|
||||
|
||||
- Tree Layout: places vertices into a tree with a root node and branches (can only be used with legal trees)
|
||||
|
||||
.. manim:: TreeLayout
|
||||
:save_last_frame:
|
||||
|
||||
class TreeLayout(Scene):
|
||||
def construct(self):
|
||||
graph = Graph(
|
||||
[1, 2, 3, 4, 5, 6, 7],
|
||||
[(1, 2), (1, 3), (2, 4), (2, 5), (3, 6), (3, 7)],
|
||||
layout="tree",
|
||||
layout_config={"root_vertex": 1},
|
||||
labels=True
|
||||
)
|
||||
self.add(graph)
|
||||
|
||||
"""
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
graph: NxGraph,
|
||||
scale: float | tuple[float, float, float] = 2,
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> dict[Hashable, Point3D]:
|
||||
"""Given a graph and a scale, return a dictionary of coordinates.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
graph : NxGraph
|
||||
The underlying NetworkX graph to be laid out. DO NOT MODIFY.
|
||||
scale : float | tuple[float, float, float], optional
|
||||
Either a single float value, or a tuple of three float values specifying the scale along each axis.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict[Hashable, Point3D]
|
||||
A dictionary mapping vertices to their positions.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
def _partite_layout(
|
||||
nx_graph: NxGraph,
|
||||
scale: float = 2,
|
||||
partitions: list[list[Hashable]] | None = None,
|
||||
root_vertex: Hashable | None = None,
|
||||
) -> dict:
|
||||
automatic_layouts = {
|
||||
"circular": nx.layout.circular_layout,
|
||||
"kamada_kawai": nx.layout.kamada_kawai_layout,
|
||||
"planar": nx.layout.planar_layout,
|
||||
"random": nx.layout.random_layout,
|
||||
"shell": nx.layout.shell_layout,
|
||||
"spectral": nx.layout.spectral_layout,
|
||||
"partite": nx.layout.multipartite_layout,
|
||||
"tree": _tree_layout,
|
||||
"spiral": nx.layout.spiral_layout,
|
||||
"spring": nx.layout.spring_layout,
|
||||
}
|
||||
|
||||
custom_layouts = ["random", "partite", "tree"]
|
||||
|
||||
if layout_config is None:
|
||||
layout_config = {}
|
||||
|
||||
if isinstance(layout, dict):
|
||||
return layout
|
||||
elif layout in automatic_layouts and layout not in custom_layouts:
|
||||
auto_layout = automatic_layouts[layout](
|
||||
nx_graph, scale=layout_scale, **layout_config
|
||||
)
|
||||
# NetworkX returns a dictionary of 3D points if the dimension
|
||||
# is specified to be 3. Otherwise, it returns a dictionary of
|
||||
# 2D points, so adjusting is required.
|
||||
if layout_config.get("dim") == 3:
|
||||
return auto_layout
|
||||
else:
|
||||
return {k: np.append(v, [0]) for k, v in auto_layout.items()}
|
||||
elif layout == "tree":
|
||||
return _tree_layout(
|
||||
nx_graph, root_vertex=root_vertex, scale=layout_scale, **layout_config
|
||||
)
|
||||
elif layout == "partite":
|
||||
if partitions is None or len(partitions) == 0:
|
||||
raise ValueError(
|
||||
"The partite layout requires the 'partitions' parameter to contain the partition of the vertices",
|
||||
)
|
||||
partition_count = len(partitions)
|
||||
for i in range(partition_count):
|
||||
for v in partitions[i]:
|
||||
if nx_graph.nodes[v] is None:
|
||||
raise ValueError(
|
||||
"The partition must contain arrays of vertices in the graph",
|
||||
)
|
||||
nx_graph.nodes[v]["subset"] = i
|
||||
# Add missing vertices to their own side
|
||||
for v in nx_graph.nodes:
|
||||
if "subset" not in nx_graph.nodes[v]:
|
||||
nx_graph.nodes[v]["subset"] = partition_count
|
||||
|
||||
auto_layout = automatic_layouts["partite"](
|
||||
nx_graph, scale=layout_scale, **layout_config
|
||||
)
|
||||
return {k: np.append(v, [0]) for k, v in auto_layout.items()}
|
||||
elif layout == "random":
|
||||
# the random layout places coordinates in [0, 1)
|
||||
# we need to rescale manually afterwards...
|
||||
auto_layout = automatic_layouts["random"](nx_graph, **layout_config)
|
||||
for k, v in auto_layout.items():
|
||||
auto_layout[k] = 2 * layout_scale * (v - np.array([0.5, 0.5]))
|
||||
return {k: np.append(v, [0]) for k, v in auto_layout.items()}
|
||||
else:
|
||||
**kwargs: Any,
|
||||
) -> dict[Hashable, Point3D]:
|
||||
if partitions is None or len(partitions) == 0:
|
||||
raise ValueError(
|
||||
f"The layout '{layout}' is neither a recognized automatic layout, "
|
||||
"nor a vertex placement dictionary.",
|
||||
"The partite layout requires partitions parameter to contain the partition of the vertices",
|
||||
)
|
||||
partition_count = len(partitions)
|
||||
for i in range(partition_count):
|
||||
for v in partitions[i]:
|
||||
if nx_graph.nodes[v] is None:
|
||||
raise ValueError(
|
||||
"The partition must contain arrays of vertices in the graph",
|
||||
)
|
||||
nx_graph.nodes[v]["subset"] = i
|
||||
# Add missing vertices to their own side
|
||||
for v in nx_graph.nodes:
|
||||
if "subset" not in nx_graph.nodes[v]:
|
||||
nx_graph.nodes[v]["subset"] = partition_count
|
||||
|
||||
return nx.layout.multipartite_layout(nx_graph, scale=scale, **kwargs)
|
||||
|
||||
|
||||
def _random_layout(nx_graph: NxGraph, scale: float = 2, **kwargs: Any):
|
||||
# the random layout places coordinates in [0, 1)
|
||||
# we need to rescale manually afterwards...
|
||||
auto_layout = nx.layout.random_layout(nx_graph, **kwargs)
|
||||
for k, v in auto_layout.items():
|
||||
auto_layout[k] = 2 * scale * (v - np.array([0.5, 0.5]))
|
||||
return {k: np.append(v, [0]) for k, v in auto_layout.items()}
|
||||
|
||||
|
||||
def _tree_layout(
|
||||
T: nx.classes.graph.Graph | nx.classes.digraph.DiGraph,
|
||||
root_vertex: Hashable | None,
|
||||
T: NxGraph,
|
||||
root_vertex: Hashable | None = None,
|
||||
scale: float | tuple | None = 2,
|
||||
vertex_spacing: tuple | None = None,
|
||||
orientation: str = "down",
|
||||
|
|
@ -212,6 +423,68 @@ def _tree_layout(
|
|||
return {v: (np.array([x, y, 0]) - center) * sf for v, (x, y) in pos.items()}
|
||||
|
||||
|
||||
LayoutName = Literal[
|
||||
"circular",
|
||||
"kamada_kawai",
|
||||
"partite",
|
||||
"planar",
|
||||
"random",
|
||||
"shell",
|
||||
"spectral",
|
||||
"spiral",
|
||||
"spring",
|
||||
"tree",
|
||||
]
|
||||
|
||||
_layouts: dict[LayoutName, LayoutFunction] = {
|
||||
"circular": cast(LayoutFunction, nx.layout.circular_layout),
|
||||
"kamada_kawai": cast(LayoutFunction, nx.layout.kamada_kawai_layout),
|
||||
"partite": cast(LayoutFunction, _partite_layout),
|
||||
"planar": cast(LayoutFunction, nx.layout.planar_layout),
|
||||
"random": cast(LayoutFunction, _random_layout),
|
||||
"shell": cast(LayoutFunction, nx.layout.shell_layout),
|
||||
"spectral": cast(LayoutFunction, nx.layout.spectral_layout),
|
||||
"spiral": cast(LayoutFunction, nx.layout.spiral_layout),
|
||||
"spring": cast(LayoutFunction, nx.layout.spring_layout),
|
||||
"tree": cast(LayoutFunction, _tree_layout),
|
||||
}
|
||||
|
||||
|
||||
def _determine_graph_layout(
|
||||
nx_graph: nx.classes.graph.Graph | nx.classes.digraph.DiGraph,
|
||||
layout: LayoutName | dict[Hashable, Point3D] | LayoutFunction = "spring",
|
||||
layout_scale: float | tuple[float, float, float] = 2,
|
||||
layout_config: dict[str, Any] | None = None,
|
||||
) -> dict[Hashable, Point3D]:
|
||||
if layout_config is None:
|
||||
layout_config = {}
|
||||
|
||||
if isinstance(layout, dict):
|
||||
return layout
|
||||
elif layout in _layouts:
|
||||
auto_layout = _layouts[layout](nx_graph, scale=layout_scale, **layout_config)
|
||||
# NetworkX returns a dictionary of 3D points if the dimension
|
||||
# is specified to be 3. Otherwise, it returns a dictionary of
|
||||
# 2D points, so adjusting is required.
|
||||
if (
|
||||
layout_config.get("dim") == 3
|
||||
or auto_layout[next(auto_layout.__iter__())].shape[0] == 3
|
||||
):
|
||||
return auto_layout
|
||||
else:
|
||||
return {k: np.append(v, [0]) for k, v in auto_layout.items()}
|
||||
else:
|
||||
try:
|
||||
return cast(LayoutFunction, layout)(
|
||||
nx_graph, scale=layout_scale, **layout_config
|
||||
)
|
||||
except TypeError:
|
||||
raise ValueError(
|
||||
f"The layout '{layout}' is neither a recognized layout, a layout function,"
|
||||
"nor a vertex placement dictionary.",
|
||||
)
|
||||
|
||||
|
||||
class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
||||
"""Abstract base class for graphs (that is, a collection of vertices
|
||||
connected with edges).
|
||||
|
|
@ -254,14 +527,14 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
layout
|
||||
Either one of ``"spring"`` (the default), ``"circular"``, ``"kamada_kawai"``,
|
||||
``"planar"``, ``"random"``, ``"shell"``, ``"spectral"``, ``"spiral"``, ``"tree"``, and ``"partite"``
|
||||
for automatic vertex positioning using ``networkx``
|
||||
for automatic vertex positioning primarily using ``networkx``
|
||||
(see `their documentation <https://networkx.org/documentation/stable/reference/drawing.html#module-networkx.drawing.layout>`_
|
||||
for more details), or a dictionary specifying a coordinate (value)
|
||||
for each vertex (key) for manual positioning.
|
||||
for more details), a dictionary specifying a coordinate (value)
|
||||
for each vertex (key) for manual positioning, or a .:class:`~.LayoutFunction` with a user-defined automatic layout.
|
||||
layout_config
|
||||
Only for automatically generated layouts. A dictionary whose entries
|
||||
are passed as keyword arguments to the automatic layout algorithm
|
||||
specified via ``layout`` of``networkx``.
|
||||
Only for automatic layouts. A dictionary whose entries
|
||||
are passed as keyword arguments to the named layout or automatic layout function
|
||||
specified via ``layout``.
|
||||
The ``tree`` layout also accepts a special parameter ``vertex_spacing``
|
||||
passed as a keyword argument inside the ``layout_config`` dictionary.
|
||||
Passing a tuple ``(space_x, space_y)`` as this argument overrides
|
||||
|
|
@ -288,6 +561,7 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
all other configuration options for a vertex.
|
||||
edge_type
|
||||
The mobject class used for displaying edges in the scene.
|
||||
Must be a subclass of :class:`~.Line` for default updaters to work.
|
||||
edge_config
|
||||
Either a dictionary containing keyword arguments to be passed
|
||||
to the class specified via ``edge_type``, or a dictionary whose
|
||||
|
|
@ -301,8 +575,8 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
edges: list[tuple[Hashable, Hashable]],
|
||||
labels: bool | dict = False,
|
||||
label_fill_color: str = BLACK,
|
||||
layout: str | dict = "spring",
|
||||
layout_scale: float | tuple = 2,
|
||||
layout: LayoutName | dict[Hashable, Point3D] | LayoutFunction = "spring",
|
||||
layout_scale: float | tuple[float, float, float] = 2,
|
||||
layout_config: dict | None = None,
|
||||
vertex_type: type[Mobject] = Dot,
|
||||
vertex_config: dict | None = None,
|
||||
|
|
@ -319,15 +593,6 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
nx_graph.add_edges_from(edges)
|
||||
self._graph = nx_graph
|
||||
|
||||
self._layout = _determine_graph_layout(
|
||||
nx_graph,
|
||||
layout=layout,
|
||||
layout_scale=layout_scale,
|
||||
layout_config=layout_config,
|
||||
partitions=partitions,
|
||||
root_vertex=root_vertex,
|
||||
)
|
||||
|
||||
if isinstance(labels, dict):
|
||||
self._labels = labels
|
||||
elif isinstance(labels, bool):
|
||||
|
|
@ -361,8 +626,14 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
self.vertices = {v: vertex_type(**self._vertex_config[v]) for v in vertices}
|
||||
self.vertices.update(vertex_mobjects)
|
||||
for v in self.vertices:
|
||||
self[v].move_to(self._layout[v])
|
||||
|
||||
self.change_layout(
|
||||
layout=layout,
|
||||
layout_scale=layout_scale,
|
||||
layout_config=layout_config,
|
||||
partitions=partitions,
|
||||
root_vertex=root_vertex,
|
||||
)
|
||||
|
||||
# build edge_config
|
||||
if edge_config is None:
|
||||
|
|
@ -399,7 +670,7 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
self.add_updater(self.update_edges)
|
||||
|
||||
@staticmethod
|
||||
def _empty_networkx_graph():
|
||||
def _empty_networkx_graph() -> nx.classes.graph.Graph:
|
||||
"""Return an empty networkx graph for the given graph type."""
|
||||
raise NotImplementedError("To be implemented in concrete subclasses")
|
||||
|
||||
|
|
@ -415,13 +686,13 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
def _create_vertex(
|
||||
self,
|
||||
vertex: Hashable,
|
||||
position: np.ndarray | None = None,
|
||||
position: Point3D | None = None,
|
||||
label: bool = False,
|
||||
label_fill_color: str = BLACK,
|
||||
vertex_type: type[Mobject] = Dot,
|
||||
vertex_config: dict | None = None,
|
||||
vertex_mobject: dict | None = None,
|
||||
) -> tuple[Hashable, np.ndarray, dict, Mobject]:
|
||||
) -> tuple[Hashable, Point3D, dict, Mobject]:
|
||||
if position is None:
|
||||
position = self.get_center()
|
||||
|
||||
|
|
@ -459,7 +730,7 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
def _add_created_vertex(
|
||||
self,
|
||||
vertex: Hashable,
|
||||
position: np.ndarray,
|
||||
position: Point3D,
|
||||
vertex_config: dict,
|
||||
vertex_mobject: Mobject,
|
||||
) -> Mobject:
|
||||
|
|
@ -485,7 +756,7 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
def _add_vertex(
|
||||
self,
|
||||
vertex: Hashable,
|
||||
position: np.ndarray | None = None,
|
||||
position: Point3D | None = None,
|
||||
label: bool = False,
|
||||
label_fill_color: str = BLACK,
|
||||
vertex_type: type[Mobject] = Dot,
|
||||
|
|
@ -540,7 +811,7 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
vertex_type: type[Mobject] = Dot,
|
||||
vertex_config: dict | None = None,
|
||||
vertex_mobjects: dict | None = None,
|
||||
) -> Iterable[tuple[Hashable, np.ndarray, dict, Mobject]]:
|
||||
) -> Iterable[tuple[Hashable, Point3D, dict, Mobject]]:
|
||||
if positions is None:
|
||||
positions = {}
|
||||
if vertex_mobjects is None:
|
||||
|
|
@ -944,9 +1215,9 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
def change_layout(
|
||||
self,
|
||||
layout: str | dict = "spring",
|
||||
layout_scale: float = 2,
|
||||
layout_config: dict | None = None,
|
||||
layout: LayoutName | dict[Hashable, Point3D] | LayoutFunction = "spring",
|
||||
layout_scale: float | tuple[float, float, float] = 2,
|
||||
layout_config: dict[str, Any] | None = None,
|
||||
partitions: list[list[Hashable]] | None = None,
|
||||
root_vertex: Hashable | None = None,
|
||||
) -> Graph:
|
||||
|
|
@ -970,14 +1241,19 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
self.play(G.animate.change_layout("circular"))
|
||||
self.wait()
|
||||
"""
|
||||
layout_config = {} if layout_config is None else layout_config
|
||||
if partitions is not None and "partitions" not in layout_config:
|
||||
layout_config["partitions"] = partitions
|
||||
if root_vertex is not None and "root_vertex" not in layout_config:
|
||||
layout_config["root_vertex"] = root_vertex
|
||||
|
||||
self._layout = _determine_graph_layout(
|
||||
self._graph,
|
||||
layout=layout,
|
||||
layout_scale=layout_scale,
|
||||
layout_config=layout_config,
|
||||
partitions=partitions,
|
||||
root_vertex=root_vertex,
|
||||
)
|
||||
|
||||
for v in self.vertices:
|
||||
self[v].move_to(self._layout[v])
|
||||
return self
|
||||
|
|
@ -1239,7 +1515,8 @@ class Graph(GenericGraph):
|
|||
*new_edges,
|
||||
vertex_config=self.VERTEX_CONF,
|
||||
positions={
|
||||
k: g.vertices[vertex_id].get_center() + 0.1 * DOWN for k in new_vertices
|
||||
k: g.vertices[vertex_id].get_center() + 0.1 * DOWN
|
||||
for k in new_vertices
|
||||
},
|
||||
)
|
||||
if depth < self.DEPTH:
|
||||
|
|
@ -1283,7 +1560,12 @@ class Graph(GenericGraph):
|
|||
def update_edges(self, graph):
|
||||
for (u, v), edge in graph.edges.items():
|
||||
# Undirected graph has a Line edge
|
||||
edge.put_start_and_end_on(graph[u].get_center(), graph[v].get_center())
|
||||
edge.set_points_by_ends(
|
||||
graph[u].get_center(),
|
||||
graph[v].get_center(),
|
||||
buff=self._edge_config.get("buff", 0),
|
||||
path_arc=self._edge_config.get("path_arc", 0),
|
||||
)
|
||||
|
||||
def __repr__(self: Graph) -> str:
|
||||
return f"Undirected graph on {len(self.vertices)} vertices and {len(self.edges)} edges"
|
||||
|
|
@ -1492,10 +1774,15 @@ class DiGraph(GenericGraph):
|
|||
deformed.
|
||||
"""
|
||||
for (u, v), edge in graph.edges.items():
|
||||
edge_type = type(edge)
|
||||
tip = edge.pop_tips()[0]
|
||||
new_edge = edge_type(self[u], self[v], **self._edge_config[(u, v)])
|
||||
edge.become(new_edge)
|
||||
# Passing the Mobject instead of the vertex makes the tip
|
||||
# stop on the bounding box of the vertex.
|
||||
edge.set_points_by_ends(
|
||||
graph[u],
|
||||
graph[v],
|
||||
buff=self._edge_config.get("buff", 0),
|
||||
path_arc=self._edge_config.get("path_arc", 0),
|
||||
)
|
||||
edge.add_tip(tip)
|
||||
|
||||
def __repr__(self: DiGraph) -> str:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
"""Mobjects that represent coordinate systems."""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = [
|
||||
|
|
@ -14,7 +13,8 @@ __all__ = [
|
|||
|
||||
import fractions as fr
|
||||
import numbers
|
||||
from typing import TYPE_CHECKING, Any, Callable, Iterable, Sequence, TypeVar, overload
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Any, Callable, TypeVar, overload
|
||||
|
||||
import numpy as np
|
||||
from typing_extensions import Self
|
||||
|
|
@ -27,6 +27,7 @@ from manim.mobject.geometry.polygram import Polygon, Rectangle, RegularPolygon
|
|||
from manim.mobject.graphing.functions import ImplicitFunction, ParametricFunction
|
||||
from manim.mobject.graphing.number_line import NumberLine
|
||||
from manim.mobject.graphing.scale import LinearBase
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.opengl.opengl_surface import OpenGLSurface
|
||||
from manim.mobject.text.tex_mobject import MathTex
|
||||
|
|
@ -96,10 +97,10 @@ class CoordinateSystem:
|
|||
)
|
||||
|
||||
# Extra lines and labels for point (1,1)
|
||||
graphs += grid.get_horizontal_line(grid.c2p(1, 1, 0), color=BLUE)
|
||||
graphs += grid.get_vertical_line(grid.c2p(1, 1, 0), color=BLUE)
|
||||
graphs += Dot(point=grid.c2p(1, 1, 0), color=YELLOW)
|
||||
graphs += Tex("(1,1)").scale(0.75).next_to(grid.c2p(1, 1, 0))
|
||||
graphs += grid.get_horizontal_line(grid @ (1, 1, 0), color=BLUE)
|
||||
graphs += grid.get_vertical_line(grid @ (1, 1, 0), color=BLUE)
|
||||
graphs += Dot(point=grid @ (1, 1, 0), color=YELLOW)
|
||||
graphs += Tex("(1,1)").scale(0.75).next_to(grid @ (1, 1, 0))
|
||||
title = Title(
|
||||
# spaces between braces to prevent SyntaxError
|
||||
r"Graphs of $y=x^{ {1}\over{n} }$ and $y=x^n (n=1,2,3,...,20)$",
|
||||
|
|
@ -145,7 +146,7 @@ class CoordinateSystem:
|
|||
self.y_length = y_length
|
||||
self.num_sampled_graph_points_per_tick = 10
|
||||
|
||||
def coords_to_point(self, *coords: Sequence[ManimFloat]):
|
||||
def coords_to_point(self, *coords: ManimFloat):
|
||||
raise NotImplementedError()
|
||||
|
||||
def point_to_coords(self, point: Point3D):
|
||||
|
|
@ -394,7 +395,9 @@ class CoordinateSystem:
|
|||
ax = ThreeDAxes()
|
||||
x_labels = range(-4, 5)
|
||||
z_labels = range(-4, 4, 2)
|
||||
ax.add_coordinates(x_labels, None, z_labels) # default y labels, custom x & z labels
|
||||
ax.add_coordinates(
|
||||
x_labels, None, z_labels
|
||||
) # default y labels, custom x & z labels
|
||||
ax.add_coordinates(x_labels) # only x labels
|
||||
|
||||
You can also specifically control the position and value of the labels using a dict.
|
||||
|
|
@ -405,7 +408,15 @@ class CoordinateSystem:
|
|||
x_pos = [x for x in range(1, 8)]
|
||||
|
||||
# strings are automatically converted into a Tex mobject.
|
||||
x_vals = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
||||
x_vals = [
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
"Sunday",
|
||||
]
|
||||
x_dict = dict(zip(x_pos, x_vals))
|
||||
ax.add_coordinates(x_dict)
|
||||
"""
|
||||
|
|
@ -441,8 +452,7 @@ class CoordinateSystem:
|
|||
line_config: dict | None = ...,
|
||||
color: ParsableManimColor | None = ...,
|
||||
stroke_width: float = ...,
|
||||
) -> DashedLine:
|
||||
...
|
||||
) -> DashedLine: ...
|
||||
|
||||
@overload
|
||||
def get_line_from_axis_to_point(
|
||||
|
|
@ -453,8 +463,7 @@ class CoordinateSystem:
|
|||
line_config: dict | None = ...,
|
||||
color: ParsableManimColor | None = ...,
|
||||
stroke_width: float = ...,
|
||||
) -> LineType:
|
||||
...
|
||||
) -> LineType: ...
|
||||
|
||||
def get_line_from_axis_to_point( # type: ignore[no-untyped-def]
|
||||
self,
|
||||
|
|
@ -562,7 +571,7 @@ class CoordinateSystem:
|
|||
class GetHorizontalLineExample(Scene):
|
||||
def construct(self):
|
||||
ax = Axes().add_coordinates()
|
||||
point = ax.c2p(-4, 1.5)
|
||||
point = ax @ (-4, 1.5)
|
||||
|
||||
dot = Dot(point)
|
||||
line = ax.get_horizontal_line(point, line_func=Line)
|
||||
|
|
@ -855,9 +864,11 @@ class CoordinateSystem:
|
|||
function: Callable[[float], float],
|
||||
u_range: Sequence[float] | None = None,
|
||||
v_range: Sequence[float] | None = None,
|
||||
colorscale: Sequence[ParsableManimColor]
|
||||
| Sequence[tuple[ParsableManimColor, float]]
|
||||
| None = None,
|
||||
colorscale: (
|
||||
Sequence[ParsableManimColor]
|
||||
| Sequence[tuple[ParsableManimColor, float]]
|
||||
| None
|
||||
) = None,
|
||||
colorscale_axis: int = 2,
|
||||
**kwargs: Any,
|
||||
) -> Surface | OpenGLSurface:
|
||||
|
|
@ -1780,6 +1791,14 @@ class CoordinateSystem:
|
|||
|
||||
return T_label_group
|
||||
|
||||
def __matmul__(self, coord: Point3D | Mobject):
|
||||
if isinstance(coord, Mobject):
|
||||
coord = coord.get_center()
|
||||
return self.coords_to_point(*coord)
|
||||
|
||||
def __rmatmul__(self, point: Point3D):
|
||||
return self.point_to_coords(point)
|
||||
|
||||
|
||||
class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
|
||||
"""Creates a set of axes.
|
||||
|
|
@ -1980,6 +1999,7 @@ class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
|
|||
self, *coords: float | Sequence[float] | Sequence[Sequence[float]] | np.ndarray
|
||||
) -> np.ndarray:
|
||||
"""Accepts coordinates from the axes and returns a point with respect to the scene.
|
||||
Equivalent to `ax @ (coord1)`
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
|
@ -2008,6 +2028,8 @@ class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
|
|||
>>> ax = Axes()
|
||||
>>> np.around(ax.coords_to_point(1, 0, 0), 2)
|
||||
array([0.86, 0. , 0. ])
|
||||
>>> np.around(ax @ (1, 0, 0), 2)
|
||||
array([0.86, 0. , 0. ])
|
||||
>>> np.around(ax.coords_to_point([[0, 1], [1, 1], [1, 0]]), 2)
|
||||
array([[0. , 0.75, 0. ],
|
||||
[0.86, 0.75, 0. ],
|
||||
|
|
@ -2571,7 +2593,7 @@ class ThreeDAxes(Axes):
|
|||
self.set_camera_orientation(phi=2*PI/5, theta=PI/5)
|
||||
axes = ThreeDAxes()
|
||||
labels = axes.get_axis_labels(
|
||||
Tex("x-axis").scale(0.7), Text("y-axis").scale(0.45), Text("z-axis").scale(0.45)
|
||||
Text("x-axis").scale(0.7), Text("y-axis").scale(0.45), Text("z-axis").scale(0.45)
|
||||
)
|
||||
self.add(axes, labels)
|
||||
"""
|
||||
|
|
@ -2653,14 +2675,12 @@ class NumberPlane(Axes):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
x_range: Sequence[float]
|
||||
| None = (
|
||||
x_range: Sequence[float] | None = (
|
||||
-config["frame_x_radius"],
|
||||
config["frame_x_radius"],
|
||||
1,
|
||||
),
|
||||
y_range: Sequence[float]
|
||||
| None = (
|
||||
y_range: Sequence[float] | None = (
|
||||
-config["frame_y_radius"],
|
||||
config["frame_y_radius"],
|
||||
1,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ from __future__ import annotations
|
|||
__all__ = ["ParametricFunction", "FunctionGraph", "ImplicitFunction"]
|
||||
|
||||
|
||||
from typing import Callable, Iterable, Sequence
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
|
||||
import numpy as np
|
||||
from isosurfaces import plot_isoline
|
||||
|
|
@ -14,6 +15,10 @@ from manim import config
|
|||
from manim.mobject.graphing.scale import LinearBase, _ScaleBase
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.typing import Point2D, Point3D
|
||||
|
||||
from manim.utils.color import YELLOW
|
||||
|
||||
|
||||
|
|
@ -23,9 +28,9 @@ class ParametricFunction(VMobject, metaclass=ConvertToOpenGL):
|
|||
Parameters
|
||||
----------
|
||||
function
|
||||
The function to be plotted in the form of ``(lambda x: x**2)``
|
||||
The function to be plotted in the form of ``(lambda t: (x(t), y(t), z(t)))``
|
||||
t_range
|
||||
Determines the length that the function spans. By default ``[0, 1]``
|
||||
Determines the length that the function spans in the form of (t_min, t_max, step=0.01). By default ``[0, 1]``
|
||||
scaling
|
||||
Scaling class applied to the points of the function. Default of :class:`~.LinearBase`.
|
||||
use_smoothing
|
||||
|
|
@ -49,10 +54,10 @@ class ParametricFunction(VMobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
class PlotParametricFunction(Scene):
|
||||
def func(self, t):
|
||||
return np.array((np.sin(2 * t), np.sin(3 * t), 0))
|
||||
return (np.sin(2 * t), np.sin(3 * t), 0)
|
||||
|
||||
def construct(self):
|
||||
func = ParametricFunction(self.func, t_range = np.array([0, TAU]), fill_opacity=0).set_color(RED)
|
||||
func = ParametricFunction(self.func, t_range = (0, TAU), fill_opacity=0).set_color(RED)
|
||||
self.add(func.scale(3))
|
||||
|
||||
.. manim:: ThreeDParametricSpring
|
||||
|
|
@ -61,11 +66,11 @@ class ParametricFunction(VMobject, metaclass=ConvertToOpenGL):
|
|||
class ThreeDParametricSpring(ThreeDScene):
|
||||
def construct(self):
|
||||
curve1 = ParametricFunction(
|
||||
lambda u: np.array([
|
||||
lambda u: (
|
||||
1.2 * np.cos(u),
|
||||
1.2 * np.sin(u),
|
||||
u * 0.05
|
||||
]), color=RED, t_range = np.array([-3*TAU, 5*TAU, 0.01])
|
||||
), color=RED, t_range = (-3*TAU, 5*TAU, 0.01)
|
||||
).set_shade_in_3d(True)
|
||||
axes = ThreeDAxes()
|
||||
self.add(axes, curve1)
|
||||
|
|
@ -97,8 +102,8 @@ class ParametricFunction(VMobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
function: Callable[[float, float], float],
|
||||
t_range: Sequence[float] | None = None,
|
||||
function: Callable[[float], Point3D],
|
||||
t_range: Point2D | Point3D = (0, 1),
|
||||
scaling: _ScaleBase = LinearBase(),
|
||||
dt: float = 1e-8,
|
||||
discontinuities: Iterable[float] | None = None,
|
||||
|
|
@ -107,7 +112,7 @@ class ParametricFunction(VMobject, metaclass=ConvertToOpenGL):
|
|||
**kwargs,
|
||||
):
|
||||
self.function = function
|
||||
t_range = [0, 1, 0.01] if t_range is None else t_range
|
||||
t_range = (0, 1, 0.01) if t_range is None else t_range
|
||||
if len(t_range) == 2:
|
||||
t_range = np.array([*t_range, 0.01])
|
||||
|
||||
|
|
|
|||
|
|
@ -2,15 +2,18 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
|
||||
|
||||
__all__ = ["NumberLine", "UnitInterval"]
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Sequence
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.geometry.tips import ArrowTip
|
||||
from manim.typing import Point3D
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -343,6 +346,7 @@ class NumberLine(Line):
|
|||
def number_to_point(self, number: float | np.ndarray) -> np.ndarray:
|
||||
"""Accepts a value along the number line and returns a point with
|
||||
respect to the scene.
|
||||
Equivalent to `NumberLine @ number`
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
|
@ -363,7 +367,9 @@ class NumberLine(Line):
|
|||
array([0., 0., 0.])
|
||||
>>> number_line.number_to_point(1)
|
||||
array([1., 0., 0.])
|
||||
>>> number_line.number_to_point([1,2,3])
|
||||
>>> number_line @ 1
|
||||
array([1., 0., 0.])
|
||||
>>> number_line.number_to_point([1, 2, 3])
|
||||
array([[1., 0., 0.],
|
||||
[2., 0., 0.],
|
||||
[3., 0., 0.]])
|
||||
|
|
@ -395,11 +401,11 @@ class NumberLine(Line):
|
|||
|
||||
>>> from manim import NumberLine
|
||||
>>> number_line = NumberLine()
|
||||
>>> number_line.point_to_number((0,0,0))
|
||||
>>> number_line.point_to_number((0, 0, 0))
|
||||
0.0
|
||||
>>> number_line.point_to_number((1,0,0))
|
||||
>>> number_line.point_to_number((1, 0, 0))
|
||||
1.0
|
||||
>>> number_line.point_to_number([[0.5,0,0],[1,0,0],[1.5,0,0]])
|
||||
>>> number_line.point_to_number([[0.5, 0, 0], [1, 0, 0], [1.5, 0, 0]])
|
||||
array([0.5, 1. , 1.5])
|
||||
|
||||
"""
|
||||
|
|
@ -641,6 +647,14 @@ class NumberLine(Line):
|
|||
return 0
|
||||
return len(step.split(".")[-1])
|
||||
|
||||
def __matmul__(self, other: float):
|
||||
return self.n2p(other)
|
||||
|
||||
def __rmatmul__(self, other: Point3D | Mobject):
|
||||
if isinstance(other, Mobject):
|
||||
other = other.get_center()
|
||||
return self.p2n(other)
|
||||
|
||||
|
||||
class UnitInterval(NumberLine):
|
||||
def __init__(
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
|||
__all__ = ["SampleSpace", "BarChart"]
|
||||
|
||||
|
||||
from typing import Iterable, MutableSequence, Sequence
|
||||
from collections.abc import Iterable, MutableSequence, Sequence
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import TYPE_CHECKING, Any, Iterable
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -145,7 +146,9 @@ class LogBase(_ScaleBase):
|
|||
"""Inverse of ``function``. The value must be greater than 0"""
|
||||
if isinstance(value, np.ndarray):
|
||||
condition = value.any() <= 0
|
||||
func = lambda value, base: np.log(value) / np.log(base)
|
||||
|
||||
def func(value, base):
|
||||
return np.log(value) / np.log(base)
|
||||
else:
|
||||
condition = value <= 0
|
||||
func = math.log
|
||||
|
|
@ -179,7 +182,7 @@ class LogBase(_ScaleBase):
|
|||
tex_labels = [
|
||||
Integer(
|
||||
self.base,
|
||||
unit="^{%s}" % (f"{self.inverse_function(i):.{unit_decimal_places}f}"),
|
||||
unit="^{%s}" % (f"{self.inverse_function(i):.{unit_decimal_places}f}"), # noqa: UP031
|
||||
**base_config,
|
||||
)
|
||||
for i in val_range
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ __all__ = [
|
|||
|
||||
|
||||
import itertools as it
|
||||
from typing import Iterable, Sequence
|
||||
from collections.abc import Iterable, Sequence
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@ import random
|
|||
import sys
|
||||
import types
|
||||
import warnings
|
||||
from collections.abc import Iterable
|
||||
from functools import partialmethod, reduce
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Literal, TypeVar, Union
|
||||
from typing import TYPE_CHECKING, Callable, Literal
|
||||
|
||||
import numpy as np
|
||||
from typing_extensions import Self, TypeAlias
|
||||
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
|
||||
|
|
@ -39,21 +39,16 @@ from ..utils.iterables import list_update, remove_list_redundancies
|
|||
from ..utils.paths import straight_path
|
||||
from ..utils.space_ops import angle_between_vectors, normalize, rotation_matrix
|
||||
|
||||
# TODO: Explain array_attrs
|
||||
|
||||
TimeBasedUpdater: TypeAlias = Callable[["Mobject", float], None]
|
||||
NonTimeBasedUpdater: TypeAlias = Callable[["Mobject"], None]
|
||||
Updater: TypeAlias = Union[NonTimeBasedUpdater, TimeBasedUpdater]
|
||||
T = TypeVar("T", bound="Mobject")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self, TypeAlias
|
||||
|
||||
from manim.typing import (
|
||||
FunctionOverride,
|
||||
Image,
|
||||
ManimFloat,
|
||||
ManimInt,
|
||||
MappingFunction,
|
||||
PathFuncType,
|
||||
PixelArray,
|
||||
Point3D,
|
||||
Point3D_Array,
|
||||
Vector3D,
|
||||
|
|
@ -61,6 +56,10 @@ if TYPE_CHECKING:
|
|||
|
||||
from ..animation.animation import Animation
|
||||
|
||||
TimeBasedUpdater: TypeAlias = Callable[["Mobject", float], object]
|
||||
NonTimeBasedUpdater: TypeAlias = Callable[["Mobject"], object]
|
||||
Updater: TypeAlias = NonTimeBasedUpdater | TimeBasedUpdater
|
||||
|
||||
|
||||
class Mobject:
|
||||
"""Mathematical Object: base class for objects that can be displayed on screen.
|
||||
|
|
@ -117,6 +116,63 @@ class Mobject:
|
|||
self.generate_points()
|
||||
self.init_colors()
|
||||
|
||||
def _assert_valid_submobjects(self, submobjects: Iterable[Mobject]) -> Self:
|
||||
"""Check that all submobjects are actually instances of
|
||||
:class:`Mobject`, and that none of them is ``self`` (a
|
||||
:class:`Mobject` cannot contain itself).
|
||||
|
||||
This is an auxiliary function called when adding Mobjects to the
|
||||
:attr:`submobjects` list.
|
||||
|
||||
This function is intended to be overridden by subclasses such as
|
||||
:class:`VMobject`, which should assert that only other VMobjects
|
||||
may be added into it.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
submobjects
|
||||
The list containing values to validate.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`Mobject`
|
||||
The Mobject itself.
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
If any of the values in `submobjects` is not a :class:`Mobject`.
|
||||
ValueError
|
||||
If there was an attempt to add a :class:`Mobject` as its own
|
||||
submobject.
|
||||
"""
|
||||
return self._assert_valid_submobjects_internal(submobjects, Mobject)
|
||||
|
||||
def _assert_valid_submobjects_internal(
|
||||
self, submobjects: list[Mobject], mob_class: type[Mobject]
|
||||
) -> Self:
|
||||
for i, submob in enumerate(submobjects):
|
||||
if not isinstance(submob, mob_class):
|
||||
error_message = (
|
||||
f"Only values of type {mob_class.__name__} can be added "
|
||||
f"as submobjects of {type(self).__name__}, but the value "
|
||||
f"{submob} (at index {i}) is of type "
|
||||
f"{type(submob).__name__}."
|
||||
)
|
||||
# Intended for subclasses such as VMobject, which
|
||||
# cannot have regular Mobjects as submobjects
|
||||
if isinstance(submob, Mobject):
|
||||
error_message += (
|
||||
" You can try adding this value into a Group instead."
|
||||
)
|
||||
raise TypeError(error_message)
|
||||
if submob is self:
|
||||
raise ValueError(
|
||||
f"Cannot add {type(self).__name__} as a submobject of "
|
||||
f"itself (at index {i})."
|
||||
)
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def animation_override_for(
|
||||
cls,
|
||||
|
|
@ -237,7 +293,7 @@ class Mobject:
|
|||
cls.__init__ = cls._original__init__
|
||||
|
||||
@property
|
||||
def animate(self: T) -> _AnimationBuilder | T:
|
||||
def animate(self) -> _AnimationBuilder | Self:
|
||||
"""Used to animate the application of any method of :code:`self`.
|
||||
|
||||
Any method called on :code:`animate` is converted to an animation of applying
|
||||
|
|
@ -415,12 +471,19 @@ class Mobject:
|
|||
>>> len(outer.submobjects)
|
||||
1
|
||||
|
||||
Only Mobjects can be added::
|
||||
|
||||
>>> outer.add(3)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError: Only values of type Mobject can be added as submobjects of Mobject, but the value 3 (at index 0) is of type int.
|
||||
|
||||
Adding an object to itself raises an error::
|
||||
|
||||
>>> outer.add(outer)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: Mobject cannot contain self
|
||||
ValueError: Cannot add Mobject as a submobject of itself (at index 0).
|
||||
|
||||
A given mobject cannot be added as a submobject
|
||||
twice to some parent::
|
||||
|
|
@ -434,12 +497,7 @@ class Mobject:
|
|||
[child]
|
||||
|
||||
"""
|
||||
for m in mobjects:
|
||||
if not isinstance(m, Mobject):
|
||||
raise TypeError("All submobjects must be of type Mobject")
|
||||
if m is self:
|
||||
raise ValueError("Mobject cannot contain self")
|
||||
|
||||
self._assert_valid_submobjects(mobjects)
|
||||
unique_mobjects = remove_list_redundancies(mobjects)
|
||||
if len(mobjects) != len(unique_mobjects):
|
||||
logger.warning(
|
||||
|
|
@ -465,10 +523,7 @@ class Mobject:
|
|||
mobject
|
||||
The mobject to be inserted.
|
||||
"""
|
||||
if not isinstance(mobject, Mobject):
|
||||
raise TypeError("All submobjects must be of type Mobject")
|
||||
if mobject is self:
|
||||
raise ValueError("Mobject cannot contain self")
|
||||
self._assert_valid_submobjects([mobject])
|
||||
self.submobjects.insert(index, mobject)
|
||||
|
||||
def __add__(self, mobject: Mobject):
|
||||
|
|
@ -521,13 +576,7 @@ class Mobject:
|
|||
:meth:`add`
|
||||
|
||||
"""
|
||||
if self in mobjects:
|
||||
raise ValueError("A mobject shouldn't contain itself")
|
||||
|
||||
for mobject in mobjects:
|
||||
if not isinstance(mobject, Mobject):
|
||||
raise TypeError("All submobjects must be of type Mobject")
|
||||
|
||||
self._assert_valid_submobjects(mobjects)
|
||||
self.remove(*mobjects)
|
||||
# dict.fromkeys() removes duplicates while maintaining order
|
||||
self.submobjects = list(dict.fromkeys(mobjects)) + self.submobjects
|
||||
|
|
@ -776,7 +825,7 @@ class Mobject:
|
|||
|
||||
# Displaying
|
||||
|
||||
def get_image(self, camera=None) -> Image:
|
||||
def get_image(self, camera=None) -> PixelArray:
|
||||
if camera is None:
|
||||
from ..camera.camera import Camera
|
||||
|
||||
|
|
@ -882,7 +931,7 @@ class Mobject:
|
|||
|
||||
Returns
|
||||
-------
|
||||
class:`bool`
|
||||
:class:`bool`
|
||||
``True`` if at least one updater uses the ``dt`` parameter, ``False``
|
||||
otherwise.
|
||||
|
||||
|
|
@ -1736,7 +1785,8 @@ class Mobject:
|
|||
curr_start, curr_end = self.get_start_and_end()
|
||||
curr_vect = curr_end - curr_start
|
||||
if np.all(curr_vect == 0):
|
||||
raise Exception("Cannot position endpoints of closed loop")
|
||||
self.points = start
|
||||
return self
|
||||
target_vect = np.array(end) - np.array(start)
|
||||
axis = (
|
||||
normalize(np.cross(curr_vect, target_vect))
|
||||
|
|
@ -1905,7 +1955,17 @@ class Mobject:
|
|||
return self
|
||||
|
||||
def get_color(self) -> ManimColor:
|
||||
"""Returns the color of the :class:`~.Mobject`"""
|
||||
"""Returns the color of the :class:`~.Mobject`
|
||||
|
||||
Examples
|
||||
--------
|
||||
::
|
||||
|
||||
>>> from manim import Square, RED
|
||||
>>> Square(color=RED).get_color() == RED
|
||||
True
|
||||
|
||||
"""
|
||||
return self.color
|
||||
|
||||
##
|
||||
|
|
@ -2004,7 +2064,7 @@ class Mobject:
|
|||
|
||||
::
|
||||
|
||||
sample = Arc(start_angle=PI/7, angle = PI/5)
|
||||
sample = Arc(start_angle=PI / 7, angle=PI / 5)
|
||||
|
||||
# These are all equivalent
|
||||
max_y_1 = sample.get_top()[1]
|
||||
|
|
@ -2700,13 +2760,13 @@ class Mobject:
|
|||
|
||||
def add_n_more_submobjects(self, n: int) -> Self | None:
|
||||
if n == 0:
|
||||
return
|
||||
return None
|
||||
|
||||
curr = len(self.submobjects)
|
||||
if curr == 0:
|
||||
# If empty, simply add n point mobjects
|
||||
self.submobjects = [self.get_point_mobject() for k in range(n)]
|
||||
return
|
||||
return None
|
||||
|
||||
target = curr + n
|
||||
# TODO, factor this out to utils so as to reuse
|
||||
|
|
@ -2761,7 +2821,6 @@ class Mobject:
|
|||
def become(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
copy_submobjects: bool = True,
|
||||
match_height: bool = False,
|
||||
match_width: bool = False,
|
||||
match_depth: bool = False,
|
||||
|
|
@ -2774,20 +2833,25 @@ class Mobject:
|
|||
.. note::
|
||||
|
||||
If both match_height and match_width are ``True`` then the transformed :class:`~.Mobject`
|
||||
will match the height first and then the width
|
||||
will match the height first and then the width.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
match_height
|
||||
If ``True``, then the transformed :class:`~.Mobject` will match the height of the original
|
||||
Whether or not to preserve the height of the original
|
||||
:class:`~.Mobject`.
|
||||
match_width
|
||||
If ``True``, then the transformed :class:`~.Mobject` will match the width of the original
|
||||
Whether or not to preserve the width of the original
|
||||
:class:`~.Mobject`.
|
||||
match_depth
|
||||
If ``True``, then the transformed :class:`~.Mobject` will match the depth of the original
|
||||
Whether or not to preserve the depth of the original
|
||||
:class:`~.Mobject`.
|
||||
match_center
|
||||
If ``True``, then the transformed :class:`~.Mobject` will match the center of the original
|
||||
Whether or not to preserve the center of the original
|
||||
:class:`~.Mobject`.
|
||||
stretch
|
||||
If ``True``, then the transformed :class:`~.Mobject` will stretch to fit the proportions of the original
|
||||
Whether or not to stretch the target mobject to match the
|
||||
the proportions of the original :class:`~.Mobject`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
|
@ -2801,8 +2865,65 @@ class Mobject:
|
|||
self.wait(0.5)
|
||||
circ.become(square)
|
||||
self.wait(0.5)
|
||||
"""
|
||||
|
||||
|
||||
The following examples illustrate how mobject measurements
|
||||
change when using the ``match_...`` and ``stretch`` arguments.
|
||||
We start with a rectangle that is 2 units high and 4 units wide,
|
||||
which we want to turn into a circle of radius 3::
|
||||
|
||||
>>> from manim import Rectangle, Circle
|
||||
>>> import numpy as np
|
||||
>>> rect = Rectangle(height=2, width=4)
|
||||
>>> circ = Circle(radius=3)
|
||||
|
||||
With ``stretch=True``, the target circle is deformed to match
|
||||
the proportions of the rectangle, which results in the target
|
||||
mobject being an ellipse with height 2 and width 4. We can
|
||||
check that the resulting points satisfy the ellipse equation
|
||||
:math:`x^2/a^2 + y^2/b^2 = 1` with :math:`a = 4/2` and :math:`b = 2/2`
|
||||
being the semi-axes::
|
||||
|
||||
>>> result = rect.copy().become(circ, stretch=True)
|
||||
>>> result.height, result.width
|
||||
(2.0, 4.0)
|
||||
>>> ellipse_points = np.array(result.get_anchors())
|
||||
>>> ellipse_eq = np.sum(ellipse_points**2 * [1/4, 1, 0], axis=1)
|
||||
>>> np.allclose(ellipse_eq, 1)
|
||||
True
|
||||
|
||||
With ``match_height=True`` and ``match_width=True`` the circle is
|
||||
scaled such that the height or the width of the rectangle will
|
||||
be preserved, respectively.
|
||||
The points of the resulting mobject satisfy the circle equation
|
||||
:math:`x^2 + y^2 = r^2` for the corresponding radius :math:`r`::
|
||||
|
||||
>>> result = rect.copy().become(circ, match_height=True)
|
||||
>>> result.height, result.width
|
||||
(2.0, 2.0)
|
||||
>>> circle_points = np.array(result.get_anchors())
|
||||
>>> circle_eq = np.sum(circle_points**2, axis=1)
|
||||
>>> np.allclose(circle_eq, 1)
|
||||
True
|
||||
>>> result = rect.copy().become(circ, match_width=True)
|
||||
>>> result.height, result.width
|
||||
(4.0, 4.0)
|
||||
>>> circle_points = np.array(result.get_anchors())
|
||||
>>> circle_eq = np.sum(circle_points**2, axis=1)
|
||||
>>> np.allclose(circle_eq, 2**2)
|
||||
True
|
||||
|
||||
With ``match_center=True``, the resulting mobject is moved such that
|
||||
its center is the same as the center of the original mobject::
|
||||
|
||||
>>> rect = rect.shift(np.array([0, 1, 0]))
|
||||
>>> np.allclose(rect.get_center(), circ.get_center())
|
||||
False
|
||||
>>> result = rect.copy().become(circ, match_center=True)
|
||||
>>> np.allclose(rect.get_center(), result.get_center())
|
||||
True
|
||||
"""
|
||||
mobject = mobject.copy()
|
||||
if stretch:
|
||||
mobject.stretch_to_fit_height(self.height)
|
||||
mobject.stretch_to_fit_width(self.width)
|
||||
|
|
@ -2858,7 +2979,7 @@ class Mobject:
|
|||
self,
|
||||
z_index_value: float,
|
||||
family: bool = True,
|
||||
) -> T:
|
||||
) -> Self:
|
||||
"""Sets the :class:`~.Mobject`'s :attr:`z_index` to the value specified in `z_index_value`.
|
||||
|
||||
Parameters
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ from manim.utils.space_ops import (
|
|||
)
|
||||
|
||||
DEFAULT_DOT_RADIUS = 0.08
|
||||
DEFAULT_SMALL_DOT_RADIUS = 0.04
|
||||
DEFAULT_DASH_LENGTH = 0.05
|
||||
DEFAULT_ARROW_TIP_LENGTH = 0.35
|
||||
DEFAULT_ARROW_TIP_WIDTH = 0.35
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
import moderngl
|
||||
import numpy as np
|
||||
|
|
@ -12,7 +12,6 @@ from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
|||
from manim.utils.bezier import integer_interpolate, interpolate
|
||||
from manim.utils.color import *
|
||||
from manim.utils.config_ops import _Data, _Uniforms
|
||||
from manim.utils.deprecation import deprecated
|
||||
from manim.utils.images import change_to_rgba_array, get_full_raster_image_path
|
||||
from manim.utils.iterables import listify
|
||||
from manim.utils.space_ops import normalize_along_axis
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ from __future__ import annotations
|
|||
|
||||
import itertools as it
|
||||
import operator as op
|
||||
from collections.abc import Iterable, Sequence
|
||||
from functools import reduce, wraps
|
||||
from typing import Callable, Iterable, Sequence
|
||||
from typing import Callable
|
||||
|
||||
import moderngl
|
||||
import numpy as np
|
||||
|
|
@ -14,17 +15,17 @@ from manim.mobject.opengl.opengl_mobject import OpenGLMobject, OpenGLPoint
|
|||
from manim.renderer.shader_wrapper import ShaderWrapper
|
||||
from manim.utils.bezier import (
|
||||
bezier,
|
||||
bezier_remap,
|
||||
get_quadratic_approximation_of_cubic,
|
||||
get_smooth_cubic_bezier_handle_points,
|
||||
integer_interpolate,
|
||||
interpolate,
|
||||
partial_quadratic_bezier_points,
|
||||
partial_bezier_points,
|
||||
proportions_along_bezier_curve_for_point,
|
||||
quadratic_bezier_remap,
|
||||
)
|
||||
from manim.utils.color import BLACK, WHITE, ManimColor, ParsableManimColor
|
||||
from manim.utils.config_ops import _Data
|
||||
from manim.utils.iterables import listify, make_even, resize_with_interpolation
|
||||
from manim.utils.iterables import make_even, resize_with_interpolation, tuplify
|
||||
from manim.utils.space_ops import (
|
||||
angle_between_vectors,
|
||||
cross2d,
|
||||
|
|
@ -160,6 +161,9 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
if stroke_color is not None:
|
||||
self.stroke_color = ManimColor.parse(stroke_color)
|
||||
|
||||
def _assert_valid_submobjects(self, submobjects: Iterable[OpenGLVMobject]) -> Self:
|
||||
return self._assert_valid_submobjects_internal(submobjects, OpenGLVMobject)
|
||||
|
||||
def get_group_class(self):
|
||||
return OpenGLVGroup
|
||||
|
||||
|
|
@ -265,7 +269,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
|
||||
if width is not None:
|
||||
for mob in self.get_family(recurse):
|
||||
mob.stroke_width = np.array([[width] for width in listify(width)])
|
||||
mob.stroke_width = np.array([[width] for width in tuplify(width)])
|
||||
|
||||
if background is not None:
|
||||
for mob in self.get_family(recurse):
|
||||
|
|
@ -320,6 +324,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
vmobject_style = vmobject.get_style()
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
vmobject_style["stroke_width"] = vmobject_style["stroke_width"][0][0]
|
||||
vmobject_style["fill_opacity"] = self.get_fill_opacity()
|
||||
self.set_style(**vmobject_style, recurse=False)
|
||||
if recurse:
|
||||
# Does its best to match up submobject lists, and
|
||||
|
|
@ -401,7 +406,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
return self.get_stroke_opacities()[0]
|
||||
|
||||
def get_color(self):
|
||||
if self.has_stroke():
|
||||
if not self.has_fill():
|
||||
return self.get_stroke_color()
|
||||
return self.get_fill_color()
|
||||
|
||||
|
|
@ -554,7 +559,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
alphas = np.linspace(0, 1, n + 1)
|
||||
new_points.extend(
|
||||
[
|
||||
partial_quadratic_bezier_points(tup, a1, a2)
|
||||
partial_bezier_points(tup, a1, a2)
|
||||
for a1, a2 in zip(alphas, alphas[1:])
|
||||
],
|
||||
)
|
||||
|
|
@ -1025,7 +1030,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
|
||||
return alpha
|
||||
|
||||
def get_anchors_and_handles(self):
|
||||
def get_anchors_and_handles(self) -> Iterable[np.ndarray]:
|
||||
"""
|
||||
Returns anchors1, handles, anchors2,
|
||||
where (anchors1[i], handles[i], anchors2[i])
|
||||
|
|
@ -1057,27 +1062,21 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
nppc = self.n_points_per_curve
|
||||
return self.points[nppc - 1 :: nppc]
|
||||
|
||||
def get_anchors(self) -> np.ndarray:
|
||||
def get_anchors(self) -> Iterable[np.ndarray]:
|
||||
"""Returns the anchors of the curves forming the OpenGLVMobject.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.ndarray
|
||||
Iterable[np.ndarray]
|
||||
The anchors.
|
||||
"""
|
||||
points = self.points
|
||||
if len(points) == 1:
|
||||
return points
|
||||
return np.array(
|
||||
list(
|
||||
it.chain(
|
||||
*zip(
|
||||
self.get_start_anchors(),
|
||||
self.get_end_anchors(),
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
s = self.get_start_anchors()
|
||||
e = self.get_end_anchors()
|
||||
return list(it.chain.from_iterable(zip(s, e)))
|
||||
|
||||
def get_points_without_null_curves(self, atol=1e-9):
|
||||
nppc = self.n_points_per_curve
|
||||
|
|
@ -1225,8 +1224,8 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
return path
|
||||
|
||||
for n in range(n_subpaths):
|
||||
sp1 = get_nth_subpath(subpaths1, n)
|
||||
sp2 = get_nth_subpath(subpaths2, n)
|
||||
sp1 = np.asarray(get_nth_subpath(subpaths1, n))
|
||||
sp2 = np.asarray(get_nth_subpath(subpaths2, n))
|
||||
diff1 = max(0, (len(sp2) - len(sp1)) // nppc)
|
||||
diff2 = max(0, (len(sp1) - len(sp2)) // nppc)
|
||||
sp1 = self.insert_n_curves_to_point_list(diff1, sp1)
|
||||
|
|
@ -1280,33 +1279,12 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
if len(points) == 1:
|
||||
return np.repeat(points, nppc * n, 0)
|
||||
|
||||
bezier_groups = self.get_bezier_tuples_from_points(points)
|
||||
norms = np.array([np.linalg.norm(bg[nppc - 1] - bg[0]) for bg in bezier_groups])
|
||||
total_norm = sum(norms)
|
||||
# Calculate insertions per curve (ipc)
|
||||
if total_norm < 1e-6:
|
||||
ipc = [n] + [0] * (len(bezier_groups) - 1)
|
||||
else:
|
||||
ipc = np.round(n * norms / sum(norms)).astype(int)
|
||||
|
||||
diff = n - sum(ipc)
|
||||
for _ in range(diff):
|
||||
ipc[np.argmin(ipc)] += 1
|
||||
for _ in range(-diff):
|
||||
ipc[np.argmax(ipc)] -= 1
|
||||
|
||||
new_length = sum(x + 1 for x in ipc)
|
||||
new_points = np.empty((new_length, nppc, 3))
|
||||
i = 0
|
||||
for group, n_inserts in zip(bezier_groups, ipc):
|
||||
# What was once a single quadratic curve defined
|
||||
# by "group" will now be broken into n_inserts + 1
|
||||
# smaller quadratic curves
|
||||
alphas = np.linspace(0, 1, n_inserts + 2)
|
||||
for a1, a2 in zip(alphas, alphas[1:]):
|
||||
new_points[i] = partial_quadratic_bezier_points(group, a1, a2)
|
||||
i = i + 1
|
||||
return np.vstack(new_points)
|
||||
bezier_tuples = self.get_bezier_tuples_from_points(points)
|
||||
current_number_of_curves = len(bezier_tuples)
|
||||
new_number_of_curves = current_number_of_curves + n
|
||||
new_bezier_tuples = bezier_remap(bezier_tuples, new_number_of_curves)
|
||||
new_points = new_bezier_tuples.reshape(-1, 3)
|
||||
return new_points
|
||||
|
||||
def interpolate(self, mobject1, mobject2, alpha, *args, **kwargs):
|
||||
super().interpolate(mobject1, mobject2, alpha, *args, **kwargs)
|
||||
|
|
@ -1359,7 +1337,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
return self
|
||||
if lower_index == upper_index:
|
||||
self.append_points(
|
||||
partial_quadratic_bezier_points(
|
||||
partial_bezier_points(
|
||||
bezier_triplets[lower_index],
|
||||
lower_residue,
|
||||
upper_residue,
|
||||
|
|
@ -1367,24 +1345,18 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
)
|
||||
else:
|
||||
self.append_points(
|
||||
partial_quadratic_bezier_points(
|
||||
bezier_triplets[lower_index], lower_residue, 1
|
||||
),
|
||||
partial_bezier_points(bezier_triplets[lower_index], lower_residue, 1),
|
||||
)
|
||||
inner_points = bezier_triplets[lower_index + 1 : upper_index]
|
||||
if len(inner_points) > 0:
|
||||
if remap:
|
||||
new_triplets = quadratic_bezier_remap(
|
||||
inner_points, num_quadratics - 2
|
||||
)
|
||||
new_triplets = bezier_remap(inner_points, num_quadratics - 2)
|
||||
else:
|
||||
new_triplets = bezier_triplets
|
||||
|
||||
self.append_points(np.asarray(new_triplets).reshape(-1, 3))
|
||||
self.append_points(
|
||||
partial_quadratic_bezier_points(
|
||||
bezier_triplets[upper_index], 0, upper_residue
|
||||
),
|
||||
partial_bezier_points(bezier_triplets[upper_index], 0, upper_residue),
|
||||
)
|
||||
return self
|
||||
|
||||
|
|
@ -1691,8 +1663,6 @@ class OpenGLVGroup(OpenGLVMobject):
|
|||
"""
|
||||
|
||||
def __init__(self, *vmobjects, **kwargs):
|
||||
if not all(isinstance(m, OpenGLVMobject) for m in vmobjects):
|
||||
raise Exception("All submobjects must be of type OpenGLVMobject")
|
||||
super().__init__(**kwargs)
|
||||
self.add(*vmobjects)
|
||||
|
||||
|
|
@ -1758,8 +1728,6 @@ class OpenGLVGroup(OpenGLVMobject):
|
|||
(gr-circle_red).animate.shift(RIGHT)
|
||||
)
|
||||
"""
|
||||
if not all(isinstance(m, OpenGLVMobject) for m in vmobjects):
|
||||
raise TypeError("All submobjects must be of type OpenGLVMobject")
|
||||
return super().add(*vmobjects)
|
||||
|
||||
def __add__(self, vmobject):
|
||||
|
|
@ -1805,8 +1773,7 @@ class OpenGLVGroup(OpenGLVMobject):
|
|||
|
||||
>>> config.renderer = original_renderer
|
||||
"""
|
||||
if not all(isinstance(m, OpenGLVMobject) for m in value):
|
||||
raise TypeError("All submobjects must be of type OpenGLVMobject")
|
||||
self._assert_valid_submobjects(tuplify(value))
|
||||
self.submobjects[key] = value
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["Brace", "BraceLabel", "ArcBrace", "BraceText", "BraceBetweenPoints"]
|
||||
|
||||
from typing import Sequence
|
||||
from collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
import svgelements as se
|
||||
|
|
@ -24,6 +25,10 @@ from ...mobject.types.vectorized_mobject import VMobject
|
|||
from ...utils.color import BLACK
|
||||
from ..svg.svg_mobject import VMobjectFromSVGPath
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.typing import Point3D, Vector3D
|
||||
from manim.utils.color.core import ParsableManimColor
|
||||
|
||||
__all__ = ["Brace", "BraceBetweenPoints", "BraceLabel", "ArcBrace"]
|
||||
|
||||
|
||||
|
|
@ -65,13 +70,13 @@ class Brace(VMobjectFromSVGPath):
|
|||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
direction: Sequence[float] | None = DOWN,
|
||||
buff=0.2,
|
||||
sharpness=2,
|
||||
stroke_width=0,
|
||||
fill_opacity=1.0,
|
||||
background_stroke_width=0,
|
||||
background_stroke_color=BLACK,
|
||||
direction: Vector3D | None = DOWN,
|
||||
buff: float = 0.2,
|
||||
sharpness: float = 2,
|
||||
stroke_width: float = 0,
|
||||
fill_opacity: float = 1.0,
|
||||
background_stroke_width: float = 0,
|
||||
background_stroke_color: ParsableManimColor = BLACK,
|
||||
**kwargs,
|
||||
):
|
||||
path_string_template = (
|
||||
|
|
@ -125,7 +130,20 @@ class Brace(VMobjectFromSVGPath):
|
|||
for mob in mobject, self:
|
||||
mob.rotate(angle, about_point=ORIGIN)
|
||||
|
||||
def put_at_tip(self, mob, use_next_to=True, **kwargs):
|
||||
def put_at_tip(self, mob: Mobject, use_next_to: bool = True, **kwargs):
|
||||
"""Puts the given mobject at the brace tip.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mob
|
||||
The mobject to be placed at the tip.
|
||||
use_next_to
|
||||
If true, then :meth:`next_to` is used to place the mobject at the
|
||||
tip.
|
||||
kwargs
|
||||
Any additional keyword arguments are passed to :meth:`next_to` which
|
||||
is used to put the mobject next to the brace tip.
|
||||
"""
|
||||
if use_next_to:
|
||||
mob.next_to(self.get_tip(), np.round(self.get_direction()), **kwargs)
|
||||
else:
|
||||
|
|
@ -136,16 +154,45 @@ class Brace(VMobjectFromSVGPath):
|
|||
return self
|
||||
|
||||
def get_text(self, *text, **kwargs):
|
||||
"""Places the text at the brace tip.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
text
|
||||
The text to be placed at the brace tip.
|
||||
kwargs
|
||||
Any additional keyword arguments are passed to :meth:`.put_at_tip` which
|
||||
is used to position the text at the brace tip.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`~.Tex`
|
||||
"""
|
||||
text_mob = Tex(*text)
|
||||
self.put_at_tip(text_mob, **kwargs)
|
||||
return text_mob
|
||||
|
||||
def get_tex(self, *tex, **kwargs):
|
||||
"""Places the tex at the brace tip.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tex
|
||||
The tex to be placed at the brace tip.
|
||||
kwargs
|
||||
Any further keyword arguments are passed to :meth:`.put_at_tip` which
|
||||
is used to position the tex at the brace tip.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`~.MathTex`
|
||||
"""
|
||||
tex_mob = MathTex(*tex)
|
||||
self.put_at_tip(tex_mob, **kwargs)
|
||||
return tex_mob
|
||||
|
||||
def get_tip(self):
|
||||
"""Returns the point at the brace tip."""
|
||||
# Returns the position of the seventh point in the path, which is the tip.
|
||||
if config["renderer"] == "opengl":
|
||||
return self.points[34]
|
||||
|
|
@ -153,6 +200,7 @@ class Brace(VMobjectFromSVGPath):
|
|||
return self.points[28] # = 7*4
|
||||
|
||||
def get_direction(self):
|
||||
"""Returns the direction from the center to the brace tip."""
|
||||
vect = self.get_tip() - self.get_center()
|
||||
return vect / np.linalg.norm(vect)
|
||||
|
||||
|
|
@ -269,9 +317,9 @@ class BraceBetweenPoints(Brace):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
point_1: Sequence[float] | None,
|
||||
point_2: Sequence[float] | None,
|
||||
direction: Sequence[float] | None = ORIGIN,
|
||||
point_1: Point3D | None,
|
||||
point_2: Point3D | None,
|
||||
direction: Vector3D | None = ORIGIN,
|
||||
**kwargs,
|
||||
):
|
||||
if all(direction == ORIGIN):
|
||||
|
|
|
|||
|
|
@ -510,17 +510,15 @@ class VMobjectFromSVGPath(VMobject, metaclass=ConvertToOpenGL):
|
|||
all_points: list[np.ndarray] = []
|
||||
last_move = None
|
||||
curve_start = None
|
||||
last_true_move = None
|
||||
|
||||
# These lambdas behave the same as similar functions in
|
||||
# vectorized_mobject, except they add to a list of points instead
|
||||
# of updating this Mobject's numpy array of points. This way,
|
||||
# we don't observe O(n^2) behavior for complex paths due to
|
||||
# numpy's need to re-allocate memory on every append.
|
||||
def move_pen(pt):
|
||||
nonlocal last_move, curve_start
|
||||
def move_pen(pt, *, true_move: bool = False):
|
||||
nonlocal last_move, curve_start, last_true_move
|
||||
last_move = pt
|
||||
if curve_start is None:
|
||||
curve_start = last_move
|
||||
if true_move:
|
||||
last_true_move = last_move
|
||||
|
||||
if self.n_points_per_curve == 4:
|
||||
|
||||
|
|
@ -568,7 +566,7 @@ class VMobjectFromSVGPath(VMobject, metaclass=ConvertToOpenGL):
|
|||
for segment in self.path_obj:
|
||||
segment_class = segment.__class__
|
||||
if segment_class == se.Move:
|
||||
move_pen(_convert_point_to_3d(*segment.end))
|
||||
move_pen(_convert_point_to_3d(*segment.end), true_move=True)
|
||||
elif segment_class == se.Line:
|
||||
add_line(last_move, _convert_point_to_3d(*segment.end))
|
||||
elif segment_class == se.QuadraticBezier:
|
||||
|
|
@ -588,8 +586,8 @@ class VMobjectFromSVGPath(VMobject, metaclass=ConvertToOpenGL):
|
|||
# If the SVG path naturally ends at the beginning of the curve,
|
||||
# we do *not* need to draw a closing line. To account for floating
|
||||
# point precision, we use a small value to compare the two points.
|
||||
if abs(np.linalg.norm(last_move - curve_start)) > 0.0001:
|
||||
add_line(last_move, curve_start)
|
||||
if abs(np.linalg.norm(last_move - last_true_move)) > 0.0001:
|
||||
add_line(last_move, last_true_move)
|
||||
curve_start = None
|
||||
else:
|
||||
raise AssertionError(f"Not implemented: {segment_class}")
|
||||
|
|
|
|||
|
|
@ -65,7 +65,8 @@ __all__ = [
|
|||
|
||||
|
||||
import itertools as it
|
||||
from typing import Callable, Iterable, Sequence
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import Callable
|
||||
|
||||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.geometry.polygram import Polygon
|
||||
|
|
@ -74,7 +75,6 @@ from manim.mobject.text.numbers import DecimalNumber, Integer
|
|||
from manim.mobject.text.tex_mobject import MathTex
|
||||
from manim.mobject.text.text_mobject import Paragraph
|
||||
|
||||
from .. import config
|
||||
from ..animation.animation import Animation
|
||||
from ..animation.composition import AnimationGroup
|
||||
from ..animation.creation import Create, Write
|
||||
|
|
|
|||
|
|
@ -12,12 +12,11 @@ import re
|
|||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from pygments import highlight
|
||||
from pygments import highlight, styles
|
||||
from pygments.formatters.html import HtmlFormatter
|
||||
from pygments.lexers import get_lexer_by_name, guess_lexer_for_filename
|
||||
from pygments.styles import get_all_styles
|
||||
|
||||
from manim import logger
|
||||
# from pygments.styles import get_all_styles
|
||||
from manim.constants import *
|
||||
from manim.mobject.geometry.arc import Dot
|
||||
from manim.mobject.geometry.polygram import RoundedRectangle
|
||||
|
|
@ -26,8 +25,6 @@ from manim.mobject.text.text_mobject import Paragraph
|
|||
from manim.mobject.types.vectorized_mobject import VGroup
|
||||
from manim.utils.color import WHITE
|
||||
|
||||
__all__ = ["Code"]
|
||||
|
||||
|
||||
class Code(VGroup):
|
||||
"""A highlighted source code listing.
|
||||
|
|
@ -64,7 +61,7 @@ class Code(VGroup):
|
|||
background_stroke_width=1,
|
||||
background_stroke_color=WHITE,
|
||||
insert_line_no=True,
|
||||
style=Code.styles_list[15],
|
||||
style="emacs",
|
||||
background="window",
|
||||
language="cpp",
|
||||
)
|
||||
|
|
@ -128,7 +125,9 @@ class Code(VGroup):
|
|||
line_no_buff
|
||||
Defines the spacing between line numbers and displayed code. Defaults to 0.4.
|
||||
style
|
||||
Defines the style type of displayed code. You can see possible names of styles in with :attr:`styles_list`. Defaults to ``"vim"``.
|
||||
Defines the style type of displayed code. To see a list possible
|
||||
names of styles call :meth:`get_styles_list`.
|
||||
Defaults to ``"vim"``.
|
||||
language
|
||||
Specifies the programming language the given code was written in. If ``None``
|
||||
(the default), the language will be automatically detected. For the list of
|
||||
|
|
@ -157,7 +156,7 @@ class Code(VGroup):
|
|||
# For more information about pygments.lexers visit https://pygments.org/docs/lexers/
|
||||
# from pygments.lexers import get_all_lexers
|
||||
# all_lexers = get_all_lexers()
|
||||
styles_list = list(get_all_styles())
|
||||
_styles_list_cache: list[str] | None = None
|
||||
# For more information about pygments.styles visit https://pygments.org/docs/styles/
|
||||
|
||||
def __init__(
|
||||
|
|
@ -289,6 +288,20 @@ class Code(VGroup):
|
|||
)
|
||||
self.move_to(np.array([0, 0, 0]))
|
||||
|
||||
@classmethod
|
||||
def get_styles_list(cls):
|
||||
"""Get list of available code styles.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[str]
|
||||
The list of available code styles to use for the ``styles``
|
||||
argument.
|
||||
"""
|
||||
if cls._styles_list_cache is None:
|
||||
cls._styles_list_cache = list(styles.get_all_styles())
|
||||
return cls._styles_list_cache
|
||||
|
||||
def _ensure_valid_file(self):
|
||||
"""Function to validate file."""
|
||||
if self.file_name is None:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["DecimalNumber", "Integer", "Variable"]
|
||||
|
||||
from typing import Sequence
|
||||
from collections.abc import Sequence
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
|
|||
|
|
@ -26,15 +26,15 @@ __all__ = [
|
|||
import itertools as it
|
||||
import operator as op
|
||||
import re
|
||||
from collections.abc import Iterable
|
||||
from functools import reduce
|
||||
from textwrap import dedent
|
||||
from typing import Iterable
|
||||
|
||||
from manim import config, logger
|
||||
from manim.constants import *
|
||||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.svg.svg_mobject import SVGMobject
|
||||
from manim.mobject.types.vectorized_mobject import VectorizedPoint, VGroup, VMobject
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.utils.tex import TexTemplate
|
||||
from manim.utils.tex_file_writing import tex_to_svg_file
|
||||
|
||||
|
|
@ -175,8 +175,8 @@ class SingleStringMathTex(SVGMobject):
|
|||
tex = self._remove_stray_braces(tex)
|
||||
|
||||
for context in ["array"]:
|
||||
begin_in = ("\\begin{%s}" % context) in tex
|
||||
end_in = ("\\end{%s}" % context) in tex
|
||||
begin_in = ("\\begin{%s}" % context) in tex # noqa: UP031
|
||||
end_in = ("\\end{%s}" % context) in tex # noqa: UP031
|
||||
if begin_in ^ end_in:
|
||||
# Just turn this into a blank string,
|
||||
# which means caller should leave a
|
||||
|
|
|
|||
|
|
@ -56,12 +56,11 @@ __all__ = ["Text", "Paragraph", "MarkupText", "register_font"]
|
|||
|
||||
import copy
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
from collections.abc import Iterable, Sequence
|
||||
from contextlib import contextmanager
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Sequence
|
||||
|
||||
import manimpango
|
||||
import numpy as np
|
||||
|
|
@ -135,10 +134,17 @@ class Paragraph(VGroup):
|
|||
--------
|
||||
Normal usage::
|
||||
|
||||
paragraph = Paragraph('this is a awesome', 'paragraph',
|
||||
'With \nNewlines', '\tWith Tabs',
|
||||
' With Spaces', 'With Alignments',
|
||||
'center', 'left', 'right')
|
||||
paragraph = Paragraph(
|
||||
"this is a awesome",
|
||||
"paragraph",
|
||||
"With \nNewlines",
|
||||
"\tWith Tabs",
|
||||
" With Spaces",
|
||||
"With Alignments",
|
||||
"center",
|
||||
"left",
|
||||
"right",
|
||||
)
|
||||
|
||||
Remove unwanted invisible characters::
|
||||
|
||||
|
|
@ -412,7 +418,7 @@ class Text(SVGMobject):
|
|||
"""
|
||||
|
||||
@staticmethod
|
||||
@functools.lru_cache(maxsize=None)
|
||||
@functools.cache
|
||||
def font_list() -> list[str]:
|
||||
return manimpango.list_fonts()
|
||||
|
||||
|
|
@ -907,7 +913,7 @@ class MarkupText(SVGMobject):
|
|||
Here is a list of supported tags:
|
||||
|
||||
- ``<b>bold</b>``, ``<i>italic</i>`` and ``<b><i>bold+italic</i></b>``
|
||||
- ``<ul>underline</ul>`` and ``<s>strike through</s>``
|
||||
- ``<u>underline</u>`` and ``<s>strike through</s>``
|
||||
- ``<tt>typewriter font</tt>``
|
||||
- ``<big>bigger font</big>`` and ``<small>smaller font</small>``
|
||||
- ``<sup>superscript</sup>`` and ``<sub>subscript</sub>``
|
||||
|
|
@ -1155,7 +1161,7 @@ class MarkupText(SVGMobject):
|
|||
"""
|
||||
|
||||
@staticmethod
|
||||
@functools.lru_cache(maxsize=None)
|
||||
@functools.cache
|
||||
def font_list() -> list[str]:
|
||||
return manimpango.list_fonts()
|
||||
|
||||
|
|
@ -1305,15 +1311,13 @@ class MarkupText(SVGMobject):
|
|||
self.set_color_by_gradient(*self.gradient)
|
||||
for col in colormap:
|
||||
self.chars[
|
||||
col["start"]
|
||||
- col["start_offset"] : col["end"]
|
||||
col["start"] - col["start_offset"] : col["end"]
|
||||
- col["start_offset"]
|
||||
- col["end_offset"]
|
||||
].set_color(self._parse_color(col["color"]))
|
||||
for grad in gradientmap:
|
||||
self.chars[
|
||||
grad["start"]
|
||||
- grad["start_offset"] : grad["end"]
|
||||
grad["start"] - grad["start_offset"] : grad["end"]
|
||||
- grad["start_offset"]
|
||||
- grad["end_offset"]
|
||||
].set_color_by_gradient(
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ __all__ = [
|
|||
"Torus",
|
||||
]
|
||||
|
||||
from typing import Any, Callable, Iterable, Sequence
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import Any, Callable
|
||||
|
||||
import numpy as np
|
||||
from typing_extensions import Self
|
||||
|
|
@ -31,16 +32,10 @@ from manim.mobject.geometry.polygram import Square
|
|||
from manim.mobject.mobject import *
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.mobject.types.vectorized_mobject import VectorizedPoint, VGroup, VMobject
|
||||
from manim.utils.color import (
|
||||
BLUE,
|
||||
BLUE_D,
|
||||
BLUE_E,
|
||||
LIGHT_GREY,
|
||||
WHITE,
|
||||
ManimColor,
|
||||
ParsableManimColor,
|
||||
interpolate_color,
|
||||
)
|
||||
from manim.utils.iterables import tuplify
|
||||
from manim.utils.space_ops import normalize, perpendicular_bisector, z_to_vector
|
||||
|
|
@ -621,17 +616,18 @@ class Cone(Surface):
|
|||
**kwargs,
|
||||
)
|
||||
# used for rotations
|
||||
self.new_height = height
|
||||
self._current_theta = 0
|
||||
self._current_phi = 0
|
||||
|
||||
self.base_circle = Circle(
|
||||
radius=base_radius,
|
||||
color=self.fill_color,
|
||||
fill_opacity=self.fill_opacity,
|
||||
stroke_width=0,
|
||||
)
|
||||
self.base_circle.shift(height * IN)
|
||||
self._set_start_and_end_attributes(direction)
|
||||
if show_base:
|
||||
self.base_circle = Circle(
|
||||
radius=base_radius,
|
||||
color=self.fill_color,
|
||||
fill_opacity=self.fill_opacity,
|
||||
stroke_width=0,
|
||||
)
|
||||
self.base_circle.shift(height * IN)
|
||||
self.add(self.base_circle)
|
||||
|
||||
self._rotate_to_direction()
|
||||
|
|
@ -661,6 +657,12 @@ class Cone(Surface):
|
|||
],
|
||||
)
|
||||
|
||||
def get_start(self) -> np.ndarray:
|
||||
return self.start_point.get_center()
|
||||
|
||||
def get_end(self) -> np.ndarray:
|
||||
return self.end_point.get_center()
|
||||
|
||||
def _rotate_to_direction(self) -> None:
|
||||
x, y, z = self.direction
|
||||
|
||||
|
|
@ -715,6 +717,15 @@ class Cone(Surface):
|
|||
"""
|
||||
return self.direction
|
||||
|
||||
def _set_start_and_end_attributes(self, direction):
|
||||
normalized_direction = direction * np.linalg.norm(direction)
|
||||
|
||||
start = self.base_circle.get_center()
|
||||
end = start + normalized_direction * self.new_height
|
||||
self.start_point = VectorizedPoint(start)
|
||||
self.end_point = VectorizedPoint(end)
|
||||
self.add(self.start_point, self.end_point)
|
||||
|
||||
|
||||
class Cylinder(Surface):
|
||||
"""A cylinder, defined by its height, radius and direction,
|
||||
|
|
@ -1155,14 +1166,20 @@ class Arrow3D(Line3D):
|
|||
self.end - height * self.direction,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.cone = Cone(
|
||||
direction=self.direction, base_radius=base_radius, height=height, **kwargs
|
||||
direction=self.direction,
|
||||
base_radius=base_radius,
|
||||
height=height,
|
||||
**kwargs,
|
||||
)
|
||||
self.cone.shift(end)
|
||||
self.add(self.cone)
|
||||
self.end_point = VectorizedPoint(end)
|
||||
self.add(self.end_point, self.cone)
|
||||
self.set_color(color)
|
||||
|
||||
def get_end(self) -> np.ndarray:
|
||||
return self.end_point.get_center()
|
||||
|
||||
|
||||
class Torus(Surface):
|
||||
"""A torus.
|
||||
|
|
|
|||
|
|
@ -14,44 +14,42 @@ __all__ = [
|
|||
|
||||
import itertools as it
|
||||
import sys
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Callable,
|
||||
Generator,
|
||||
Hashable,
|
||||
Iterable,
|
||||
Literal,
|
||||
Mapping,
|
||||
Sequence,
|
||||
)
|
||||
from collections.abc import Generator, Hashable, Iterable, Mapping, Sequence
|
||||
from typing import TYPE_CHECKING, Callable, Literal
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
from PIL.Image import Image
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim import config
|
||||
from manim.constants import *
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
|
||||
from manim.mobject.three_d.three_d_utils import (
|
||||
get_3d_vmob_gradient_start_and_end_points,
|
||||
)
|
||||
|
||||
from ... import config
|
||||
from ...constants import *
|
||||
from ...mobject.mobject import Mobject
|
||||
from ...utils.bezier import (
|
||||
from manim.utils.bezier import (
|
||||
bezier,
|
||||
get_smooth_handle_points,
|
||||
bezier_remap,
|
||||
get_smooth_cubic_bezier_handle_points,
|
||||
integer_interpolate,
|
||||
interpolate,
|
||||
partial_bezier_points,
|
||||
proportions_along_bezier_curve_for_point,
|
||||
)
|
||||
from ...utils.color import BLACK, WHITE, ManimColor, ParsableManimColor
|
||||
from ...utils.iterables import make_even, resize_array, stretch_array_to_length, tuplify
|
||||
from ...utils.space_ops import rotate_vector, shoelace_direction
|
||||
from manim.utils.color import BLACK, WHITE, ManimColor, ParsableManimColor
|
||||
from manim.utils.iterables import (
|
||||
make_even,
|
||||
resize_array,
|
||||
stretch_array_to_length,
|
||||
tuplify,
|
||||
)
|
||||
from manim.utils.space_ops import rotate_vector, shoelace_direction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy.typing as npt
|
||||
from typing_extensions import Self
|
||||
|
||||
from manim.typing import (
|
||||
BezierPoints,
|
||||
CubicBezierPoints,
|
||||
|
|
@ -161,6 +159,9 @@ class VMobject(Mobject):
|
|||
self.shade_in_3d: bool = shade_in_3d
|
||||
self.tolerance_for_point_equality: float = tolerance_for_point_equality
|
||||
self.n_points_per_cubic_curve: int = n_points_per_cubic_curve
|
||||
self._bezier_t_values: npt.NDArray[float] = np.linspace(
|
||||
0, 1, n_points_per_cubic_curve
|
||||
)
|
||||
self.cap_style: CapStyleType = cap_style
|
||||
super().__init__(**kwargs)
|
||||
self.submobjects: list[VMobject]
|
||||
|
|
@ -174,6 +175,9 @@ class VMobject(Mobject):
|
|||
if stroke_color is not None:
|
||||
self.stroke_color = ManimColor.parse(stroke_color)
|
||||
|
||||
def _assert_valid_submobjects(self, submobjects: Iterable[VMobject]) -> Self:
|
||||
return self._assert_valid_submobjects_internal(submobjects, VMobject)
|
||||
|
||||
# OpenGL compatibility
|
||||
@property
|
||||
def n_points_per_curve(self) -> int:
|
||||
|
|
@ -347,7 +351,7 @@ class VMobject(Mobject):
|
|||
setattr(self, opacity_name, opacity)
|
||||
if color is not None and background:
|
||||
if isinstance(color, (list, tuple)):
|
||||
self.background_stroke_color = color
|
||||
self.background_stroke_color = ManimColor.parse(color)
|
||||
else:
|
||||
self.background_stroke_color = ManimColor(color)
|
||||
return self
|
||||
|
|
@ -752,7 +756,7 @@ class VMobject(Mobject):
|
|||
assert len(anchors1) == len(handles1) == len(handles2) == len(anchors2)
|
||||
nppcc = self.n_points_per_cubic_curve # 4
|
||||
total_len = nppcc * len(anchors1)
|
||||
self.points = np.zeros((total_len, self.dim))
|
||||
self.points = np.empty((total_len, self.dim))
|
||||
# the following will, from the four sets, dispatch them in points such that
|
||||
# self.points = [
|
||||
# anchors1[0], handles1[0], handles2[0], anchors1[0], anchors1[1],
|
||||
|
|
@ -764,23 +768,61 @@ class VMobject(Mobject):
|
|||
return self
|
||||
|
||||
def clear_points(self) -> None:
|
||||
# TODO: shouldn't this return self instead of None?
|
||||
self.points = np.zeros((0, self.dim))
|
||||
|
||||
def append_points(self, new_points: Point3D_Array) -> Self:
|
||||
"""Append the given ``new_points`` to the end of
|
||||
:attr:`VMobject.points`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
new_points
|
||||
An array of 3D points to append.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`VMobject`
|
||||
The VMobject itself, after appending ``new_points``.
|
||||
"""
|
||||
# TODO, check that number new points is a multiple of 4?
|
||||
# or else that if len(self.points) % 4 == 1, then
|
||||
# len(new_points) % 4 == 3?
|
||||
self.points = np.append(self.points, new_points, axis=0)
|
||||
n = len(self.points)
|
||||
points = np.empty((n + len(new_points), self.dim))
|
||||
points[:n] = self.points
|
||||
points[n:] = new_points
|
||||
self.points = points
|
||||
return self
|
||||
|
||||
def start_new_path(self, point: Point3D) -> Self:
|
||||
if len(self.points) % 4 != 0:
|
||||
"""Append a ``point`` to the :attr:`VMobject.points`, which will be the
|
||||
beginning of a new Bézier curve in the path given by the points. If
|
||||
there's an unfinished curve at the end of :attr:`VMobject.points`,
|
||||
complete it by appending the last Bézier curve's start anchor as many
|
||||
times as needed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
point
|
||||
A 3D point to append to :attr:`VMobject.points`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`VMobject`
|
||||
The VMobject itself, after appending ``point`` and starting a new
|
||||
curve.
|
||||
"""
|
||||
n_points = len(self.points)
|
||||
nppc = self.n_points_per_curve
|
||||
if n_points % nppc != 0:
|
||||
# close the open path by appending the last
|
||||
# start anchor sufficiently often
|
||||
last_anchor = self.get_start_anchors()[-1]
|
||||
for _ in range(4 - (len(self.points) % 4)):
|
||||
self.append_points([last_anchor])
|
||||
self.append_points([point])
|
||||
closure = [last_anchor] * (nppc - (n_points % nppc))
|
||||
self.append_points(closure + [point])
|
||||
else:
|
||||
self.append_points([point])
|
||||
return self
|
||||
|
||||
def add_cubic_bezier_curve(
|
||||
|
|
@ -862,18 +904,17 @@ class VMobject(Mobject):
|
|||
----------
|
||||
|
||||
point
|
||||
end of the straight line.
|
||||
The end of the straight line.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`VMobject`
|
||||
``self``
|
||||
"""
|
||||
nppcc = self.n_points_per_cubic_curve
|
||||
self.add_cubic_bezier_curve_to(
|
||||
*(
|
||||
interpolate(self.get_last_point(), point, a)
|
||||
for a in np.linspace(0, 1, nppcc)[1:]
|
||||
interpolate(self.get_last_point(), point, t)
|
||||
for t in self._bezier_t_values[1:]
|
||||
)
|
||||
)
|
||||
return self
|
||||
|
|
@ -938,15 +979,54 @@ class VMobject(Mobject):
|
|||
self.add_line_to(self.get_subpaths()[-1][0])
|
||||
|
||||
def add_points_as_corners(self, points: Iterable[Point3D]) -> Iterable[Point3D]:
|
||||
for point in points:
|
||||
self.add_line_to(point)
|
||||
"""Append multiple straight lines at the end of
|
||||
:attr:`VMobject.points`, which connect the given ``points`` in order
|
||||
starting from the end of the current path. These ``points`` would be
|
||||
therefore the corners of the new polyline appended to the path.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
points
|
||||
An array of 3D points representing the corners of the polyline to
|
||||
append to :attr:`VMobject.points`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`VMobject`
|
||||
The VMobject itself, after appending the straight lines to its
|
||||
path.
|
||||
"""
|
||||
points = np.asarray(points).reshape(-1, self.dim)
|
||||
if self.has_new_path_started():
|
||||
# Pop the last point from self.points and
|
||||
# add it to start_corners
|
||||
start_corners = np.empty((len(points), self.dim))
|
||||
start_corners[0] = self.points[-1]
|
||||
start_corners[1:] = points[:-1]
|
||||
end_corners = points
|
||||
self.points = self.points[:-1]
|
||||
else:
|
||||
start_corners = points[:-1]
|
||||
end_corners = points[1:]
|
||||
|
||||
nppcc = self.n_points_per_cubic_curve
|
||||
new_points = np.empty((nppcc * start_corners.shape[0], self.dim))
|
||||
new_points[::nppcc] = start_corners
|
||||
new_points[nppcc - 1 :: nppcc] = end_corners
|
||||
for i, t in enumerate(self._bezier_t_values):
|
||||
new_points[i::nppcc] = interpolate(start_corners, end_corners, t)
|
||||
|
||||
self.append_points(new_points)
|
||||
# TODO: shouldn't this method return self instead of points?
|
||||
return points
|
||||
|
||||
def set_points_as_corners(self, points: Point3D_Array) -> Self:
|
||||
"""Given an array of points, set them as corner of the vmobject.
|
||||
"""Given an array of points, set them as corners of the
|
||||
:class:`VMobject`.
|
||||
|
||||
To achieve that, this algorithm sets handles aligned with the anchors such that the resultant bezier curve will be the segment
|
||||
between the two anchors.
|
||||
To achieve that, this algorithm sets handles aligned with the anchors
|
||||
such that the resultant Bézier curve will be the segment between the
|
||||
two anchors.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
|
@ -956,14 +1036,34 @@ class VMobject(Mobject):
|
|||
Returns
|
||||
-------
|
||||
:class:`VMobject`
|
||||
``self``
|
||||
The VMobject itself, after setting the new points as corners.
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. manim:: PointsAsCornersExample
|
||||
:save_last_frame:
|
||||
|
||||
class PointsAsCornersExample(Scene):
|
||||
def construct(self):
|
||||
corners = (
|
||||
# create square
|
||||
UR, UL,
|
||||
DL, DR,
|
||||
UR,
|
||||
# create crosses
|
||||
DL, UL,
|
||||
DR
|
||||
)
|
||||
vmob = VMobject(stroke_color=RED)
|
||||
vmob.set_points_as_corners(corners).scale(2)
|
||||
self.add(vmob)
|
||||
"""
|
||||
nppcc = self.n_points_per_cubic_curve
|
||||
points = np.array(points)
|
||||
# This will set the handles aligned with the anchors.
|
||||
# Id est, a bezier curve will be the segment from the two anchors such that the handles belongs to this segment.
|
||||
self.set_anchors_and_handles(
|
||||
*(interpolate(points[:-1], points[1:], a) for a in np.linspace(0, 1, nppcc))
|
||||
*(interpolate(points[:-1], points[1:], t) for t in self._bezier_t_values)
|
||||
)
|
||||
return self
|
||||
|
||||
|
|
@ -993,7 +1093,7 @@ class VMobject(Mobject):
|
|||
# The append is needed as the last element is not reached when slicing with numpy.
|
||||
anchors = np.append(subpath[::nppcc], subpath[-1:], 0)
|
||||
if mode == "smooth":
|
||||
h1, h2 = get_smooth_handle_points(anchors)
|
||||
h1, h2 = get_smooth_cubic_bezier_handle_points(anchors)
|
||||
else: # mode == "jagged"
|
||||
# The following will make the handles aligned with the anchors, thus making the bezier curve a segment
|
||||
a1 = anchors[:-1]
|
||||
|
|
@ -1014,17 +1114,15 @@ class VMobject(Mobject):
|
|||
|
||||
def add_subpath(self, points: Point3D_Array) -> Self:
|
||||
assert len(points) % 4 == 0
|
||||
self.points: Point3D_Array = np.append(self.points, points, axis=0)
|
||||
self.append_points(points)
|
||||
return self
|
||||
|
||||
def append_vectorized_mobject(self, vectorized_mobject: VMobject) -> None:
|
||||
new_points = list(vectorized_mobject.points)
|
||||
|
||||
if self.has_new_path_started():
|
||||
# Remove last point, which is starting
|
||||
# a new path
|
||||
self.points = self.points[:-1]
|
||||
self.append_points(new_points)
|
||||
self.append_points(vectorized_mobject.points)
|
||||
|
||||
def apply_function(self, function: MappingFunction) -> Self:
|
||||
factor = self.pre_function_handle_to_anchor_scale_factor
|
||||
|
|
@ -1382,6 +1480,22 @@ class VMobject(Mobject):
|
|||
If ``alpha`` is not between 0 and 1.
|
||||
:exc:`Exception`
|
||||
If the :class:`VMobject` has no points.
|
||||
|
||||
Example
|
||||
-------
|
||||
.. manim:: PointFromProportion
|
||||
:save_last_frame:
|
||||
|
||||
class PointFromProportion(Scene):
|
||||
def construct(self):
|
||||
line = Line(2*DL, 2*UR)
|
||||
self.add(line)
|
||||
colors = (RED, BLUE, YELLOW)
|
||||
proportions = (1/4, 1/2, 3/4)
|
||||
for color, proportion in zip(colors, proportions):
|
||||
self.add(Dot(color=color).move_to(
|
||||
line.point_from_proportion(proportion)
|
||||
))
|
||||
"""
|
||||
|
||||
if alpha < 0 or alpha > 1:
|
||||
|
|
@ -1406,6 +1520,9 @@ class VMobject(Mobject):
|
|||
return curve(residue)
|
||||
|
||||
current_length += length
|
||||
raise Exception(
|
||||
"Not sure how you reached here, please file a bug report at https://github.com/ManimCommunity/manim/issues/new/choose"
|
||||
)
|
||||
|
||||
def proportion_from_point(
|
||||
self,
|
||||
|
|
@ -1508,9 +1625,10 @@ class VMobject(Mobject):
|
|||
"""
|
||||
if self.points.shape[0] == 1:
|
||||
return self.points
|
||||
return np.array(
|
||||
tuple(it.chain(*zip(self.get_start_anchors(), self.get_end_anchors()))),
|
||||
)
|
||||
|
||||
s = self.get_start_anchors()
|
||||
e = self.get_end_anchors()
|
||||
return list(it.chain.from_iterable(zip(s, e)))
|
||||
|
||||
def get_points_defining_boundary(self) -> Point3D_Array:
|
||||
# Probably returns all anchors, but this is weird regarding the name of the method.
|
||||
|
|
@ -1655,40 +1773,11 @@ class VMobject(Mobject):
|
|||
if len(points) == 1:
|
||||
nppcc = self.n_points_per_cubic_curve
|
||||
return np.repeat(points, nppcc * n, 0)
|
||||
bezier_quads = self.get_cubic_bezier_tuples_from_points(points)
|
||||
curr_num = len(bezier_quads)
|
||||
target_num = curr_num + n
|
||||
# This is an array with values ranging from 0
|
||||
# up to curr_num, with repeats such that
|
||||
# it's total length is target_num. For example,
|
||||
# with curr_num = 10, target_num = 15, this would
|
||||
# be [0, 0, 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 8, 9]
|
||||
repeat_indices = (np.arange(target_num, dtype="i") * curr_num) // target_num
|
||||
|
||||
# If the nth term of this list is k, it means
|
||||
# that the nth curve of our path should be split
|
||||
# into k pieces.
|
||||
# In the above example our array had the following elements
|
||||
# [0, 0, 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 8, 9]
|
||||
# We have two 0s, one 1, two 2s and so on.
|
||||
# The split factors array would hence be:
|
||||
# [2, 1, 2, 1, 2, 1, 2, 1, 2, 1]
|
||||
split_factors = np.zeros(curr_num, dtype="i")
|
||||
for val in repeat_indices:
|
||||
split_factors[val] += 1
|
||||
|
||||
new_points = np.zeros((0, self.dim))
|
||||
for quad, sf in zip(bezier_quads, split_factors):
|
||||
# What was once a single cubic curve defined
|
||||
# by "quad" will now be broken into sf
|
||||
# smaller cubic curves
|
||||
alphas = np.linspace(0, 1, sf + 1)
|
||||
for a1, a2 in zip(alphas, alphas[1:]):
|
||||
new_points = np.append(
|
||||
new_points,
|
||||
partial_bezier_points(quad, a1, a2),
|
||||
axis=0,
|
||||
)
|
||||
bezier_tuples = self.get_cubic_bezier_tuples_from_points(points)
|
||||
current_number_of_curves = len(bezier_tuples)
|
||||
new_number_of_curves = current_number_of_curves + n
|
||||
new_bezier_tuples = bezier_remap(bezier_tuples, new_number_of_curves)
|
||||
new_points = new_bezier_tuples.reshape(-1, 3)
|
||||
return new_points
|
||||
|
||||
def align_rgbas(self, vmobject: VMobject) -> Self:
|
||||
|
|
@ -1730,7 +1819,10 @@ class VMobject(Mobject):
|
|||
interpolate(getattr(mobject1, attr), getattr(mobject2, attr), alpha),
|
||||
)
|
||||
if alpha == 1.0:
|
||||
setattr(self, attr, getattr(mobject2, attr))
|
||||
val = getattr(mobject2, attr)
|
||||
if isinstance(val, np.ndarray):
|
||||
val = val.copy()
|
||||
setattr(self, attr, val)
|
||||
|
||||
def pointwise_become_partial(
|
||||
self,
|
||||
|
|
@ -1910,19 +2002,21 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
|
|||
>>> triangle, square = Triangle(), Square()
|
||||
>>> vg.add(triangle)
|
||||
VGroup(Triangle)
|
||||
>>> vg + square # a new VGroup is constructed
|
||||
>>> vg + square # a new VGroup is constructed
|
||||
VGroup(Triangle, Square)
|
||||
>>> vg # not modified
|
||||
>>> vg # not modified
|
||||
VGroup(Triangle)
|
||||
>>> vg += square; vg # modifies vg
|
||||
>>> vg += square
|
||||
>>> vg # modifies vg
|
||||
VGroup(Triangle, Square)
|
||||
>>> vg.remove(triangle)
|
||||
VGroup(Square)
|
||||
>>> vg - square; # a new VGroup is constructed
|
||||
>>> vg - square # a new VGroup is constructed
|
||||
VGroup()
|
||||
>>> vg # not modified
|
||||
>>> vg # not modified
|
||||
VGroup(Square)
|
||||
>>> vg -= square; vg # modifies vg
|
||||
>>> vg -= square
|
||||
>>> vg # modifies vg
|
||||
VGroup()
|
||||
|
||||
.. manim:: ArcShapeIris
|
||||
|
|
@ -2003,14 +2097,6 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
|
|||
(gr-circle_red).animate.shift(RIGHT)
|
||||
)
|
||||
"""
|
||||
for m in vmobjects:
|
||||
if not isinstance(m, (VMobject, OpenGLVMobject)):
|
||||
raise TypeError(
|
||||
f"All submobjects of {self.__class__.__name__} must be of type VMobject. "
|
||||
f"Got {repr(m)} ({type(m).__name__}) instead. "
|
||||
"You can try using `Group` instead."
|
||||
)
|
||||
|
||||
return super().add(*vmobjects)
|
||||
|
||||
def __add__(self, vmobject: VMobject) -> Self:
|
||||
|
|
@ -2048,8 +2134,7 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
|
|||
>>> new_obj = VMobject()
|
||||
>>> vgroup[0] = new_obj
|
||||
"""
|
||||
if not all(isinstance(m, (VMobject, OpenGLVMobject)) for m in value):
|
||||
raise TypeError("All submobjects must be of type VMobject")
|
||||
self._assert_valid_submobjects(tuplify(value))
|
||||
self.submobjects[key] = value
|
||||
|
||||
|
||||
|
|
@ -2193,7 +2278,7 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
|
|||
Normal usage::
|
||||
|
||||
square_obj = Square()
|
||||
my_dict.add([('s', square_obj)])
|
||||
my_dict.add([("s", square_obj)])
|
||||
"""
|
||||
for key, value in dict(mapping_or_iterable).items():
|
||||
self.add_key_value_pair(key, value)
|
||||
|
|
@ -2220,10 +2305,10 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
|
|||
--------
|
||||
Normal usage::
|
||||
|
||||
my_dict.remove('square')
|
||||
my_dict.remove("square")
|
||||
"""
|
||||
if key not in self.submob_dict:
|
||||
raise KeyError("The given key '%s' is not present in the VDict" % str(key))
|
||||
raise KeyError(f"The given key '{key!s}' is not present in the VDict")
|
||||
super().remove(self.submob_dict[key])
|
||||
del self.submob_dict[key]
|
||||
return self
|
||||
|
|
@ -2245,7 +2330,7 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
|
|||
--------
|
||||
Normal usage::
|
||||
|
||||
self.play(Create(my_dict['s']))
|
||||
self.play(Create(my_dict["s"]))
|
||||
"""
|
||||
submob = self.submob_dict[key]
|
||||
return submob
|
||||
|
|
@ -2269,7 +2354,7 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
|
|||
Normal usage::
|
||||
|
||||
square_obj = Square()
|
||||
my_dict['sq'] = square_obj
|
||||
my_dict["sq"] = square_obj
|
||||
"""
|
||||
if key in self.submob_dict:
|
||||
self.remove(key)
|
||||
|
|
@ -2374,11 +2459,10 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
|
|||
Normal usage::
|
||||
|
||||
square_obj = Square()
|
||||
self.add_key_value_pair('s', square_obj)
|
||||
self.add_key_value_pair("s", square_obj)
|
||||
|
||||
"""
|
||||
if not isinstance(value, (VMobject, OpenGLVMobject)):
|
||||
raise TypeError("All submobjects must be of type VMobject")
|
||||
self._assert_valid_submobjects([value])
|
||||
mob = value
|
||||
if self.show_keys:
|
||||
# This import is here and not at the top to avoid circular import
|
||||
|
|
|
|||
|
|
@ -10,8 +10,9 @@ __all__ = [
|
|||
|
||||
import itertools as it
|
||||
import random
|
||||
from collections.abc import Iterable, Sequence
|
||||
from math import ceil, floor
|
||||
from typing import Callable, Iterable, Sequence
|
||||
from typing import Callable
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ from ..utils.iterables import list_update
|
|||
|
||||
if typing.TYPE_CHECKING:
|
||||
import types
|
||||
from typing import Any, Iterable
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
from manim.animation.animation import Animation
|
||||
from manim.scene.scene import Scene
|
||||
|
|
@ -186,8 +187,7 @@ class CairoRenderer:
|
|||
if self.skip_animations:
|
||||
return
|
||||
self.time += num_frames * dt
|
||||
for _ in range(num_frames):
|
||||
self.file_writer.write_frame(frame)
|
||||
self.file_writer.write_frame(frame, num_frames=num_frames)
|
||||
|
||||
def freeze_current_frame(self, duration: float):
|
||||
"""Adds a static frame to the movie for a given duration. The static frame is the current frame.
|
||||
|
|
|
|||
|
|
@ -1,15 +1,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import itertools as it
|
||||
import sys
|
||||
import time
|
||||
from functools import cached_property
|
||||
from typing import Any
|
||||
|
||||
if sys.version_info < (3, 8):
|
||||
from backports.cached_property import cached_property
|
||||
else:
|
||||
from functools import cached_property
|
||||
|
||||
import moderngl
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
|
@ -219,9 +214,6 @@ class OpenGLCamera(OpenGLMobject):
|
|||
self.refresh_rotation_matrix()
|
||||
|
||||
|
||||
points_per_curve = 3
|
||||
|
||||
|
||||
class OpenGLRenderer:
|
||||
def __init__(self, file_writer_class=SceneFileWriter, skip_animations=False):
|
||||
# Measured in pixel widths, used for vector graphics
|
||||
|
|
@ -428,8 +420,9 @@ class OpenGLRenderer:
|
|||
self.update_frame(scene)
|
||||
|
||||
if not self.skip_animations:
|
||||
for _ in range(int(config.frame_rate * scene.duration)):
|
||||
self.file_writer.write_frame(self)
|
||||
self.file_writer.write_frame(
|
||||
self, num_frames=int(config.frame_rate * scene.duration)
|
||||
)
|
||||
|
||||
if self.window is not None:
|
||||
self.window.swap_buffers()
|
||||
|
|
@ -575,7 +568,7 @@ class OpenGLRenderer:
|
|||
if pixel_shape is None:
|
||||
return np.array([0, 0, 0])
|
||||
pw, ph = pixel_shape
|
||||
fw, fh = config["frame_width"], config["frame_height"]
|
||||
fh = config["frame_height"]
|
||||
fc = self.camera.get_center()
|
||||
if relative:
|
||||
return 2 * np.array([px / pw, py / ph, 0])
|
||||
|
|
|
|||
|
|
@ -52,7 +52,8 @@ from ..utils.file_ops import open_media_file
|
|||
from ..utils.iterables import list_difference_update, list_update
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Iterable
|
||||
from collections.abc import Iterable
|
||||
from typing import Callable
|
||||
|
||||
|
||||
class RerunSceneHandler(FileSystemEventHandler):
|
||||
|
|
@ -229,7 +230,7 @@ class Scene:
|
|||
self.construct()
|
||||
except EndSceneEarlyException:
|
||||
pass
|
||||
except RerunSceneException as e:
|
||||
except RerunSceneException:
|
||||
self.remove(*self.mobjects)
|
||||
self.renderer.clear_screen()
|
||||
self.renderer.num_plays = 0
|
||||
|
|
@ -287,7 +288,7 @@ class Scene:
|
|||
Examples
|
||||
--------
|
||||
A typical manim script includes a class derived from :class:`Scene` with an
|
||||
overridden :meth:`Scene.contruct` method:
|
||||
overridden :meth:`Scene.construct` method:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
|
@ -1030,12 +1031,28 @@ class Scene:
|
|||
float
|
||||
The total ``run_time`` of all of the animations in the list.
|
||||
"""
|
||||
max_run_time = 0
|
||||
frame_rate = (
|
||||
1 / config.frame_rate
|
||||
) # config.frame_rate holds the number of frames per second
|
||||
for animation in animations:
|
||||
if animation.run_time <= 0:
|
||||
raise ValueError(
|
||||
f"{animation} has a run_time of <= 0 seconds which Manim cannot render. "
|
||||
"Please set the run_time to be positive."
|
||||
)
|
||||
elif animation.run_time < frame_rate:
|
||||
logger.warning(
|
||||
f"Original run time of {animation} is shorter than current frame "
|
||||
f"rate (1 frame every {frame_rate:.2f} sec.) which cannot be rendered. "
|
||||
"Rendering with the shortest possible duration instead."
|
||||
)
|
||||
animation.run_time = frame_rate
|
||||
|
||||
if len(animations) == 1 and isinstance(animations[0], Wait):
|
||||
return animations[0].duration
|
||||
if animation.run_time > max_run_time:
|
||||
max_run_time = animation.run_time
|
||||
|
||||
else:
|
||||
return np.max([animation.run_time for animation in animations])
|
||||
return max_run_time
|
||||
|
||||
def play(
|
||||
self,
|
||||
|
|
@ -1205,16 +1222,16 @@ class Scene:
|
|||
self.moving_mobjects = []
|
||||
self.static_mobjects = []
|
||||
|
||||
self.duration = self.get_run_time(self.animations)
|
||||
if len(self.animations) == 1 and isinstance(self.animations[0], Wait):
|
||||
if self.should_update_mobjects():
|
||||
self.update_mobjects(dt=0) # Any problems with this?
|
||||
self.stop_condition = self.animations[0].stop_condition
|
||||
else:
|
||||
self.duration = self.animations[0].duration
|
||||
# Static image logic when the wait is static is done by the renderer, not here.
|
||||
self.animations[0].is_static_wait = True
|
||||
return None
|
||||
self.duration = self.get_run_time(self.animations)
|
||||
|
||||
return self
|
||||
|
||||
def begin_animations(self) -> None:
|
||||
|
|
@ -1538,8 +1555,7 @@ class Scene:
|
|||
|
||||
# second option: within the call to Scene.play
|
||||
self.play(
|
||||
Transform(square, circle),
|
||||
subcaption="The square transforms."
|
||||
Transform(square, circle), subcaption="The square transforms."
|
||||
)
|
||||
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -5,12 +5,11 @@ from __future__ import annotations
|
|||
__all__ = ["SceneFileWriter"]
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import av
|
||||
import numpy as np
|
||||
import srt
|
||||
from PIL import Image
|
||||
|
|
@ -24,11 +23,9 @@ from ..constants import RendererType
|
|||
from ..utils.file_ops import (
|
||||
add_extension_if_not_present,
|
||||
add_version_before_extension,
|
||||
ensure_executable,
|
||||
guarantee_existence,
|
||||
is_gif_format,
|
||||
is_png_format,
|
||||
is_webm_format,
|
||||
modify_atime,
|
||||
write_to_movie,
|
||||
)
|
||||
|
|
@ -83,14 +80,6 @@ class SceneFileWriter:
|
|||
self.next_section(
|
||||
name="autocreated", type=DefaultSectionType.NORMAL, skip_animations=False
|
||||
)
|
||||
# fail fast if ffmpeg is not found
|
||||
if not ensure_executable(Path(config.ffmpeg_executable)):
|
||||
raise RuntimeError(
|
||||
"Manim could not find ffmpeg, which is required for generating video output.\n"
|
||||
"For installing ffmpeg please consult https://docs.manim.community/en/stable/installation.html\n"
|
||||
"Make sure to either add ffmpeg to the PATH environment variable\n"
|
||||
"or set path to the ffmpeg executable under the ffmpeg header in Manim's configuration."
|
||||
)
|
||||
|
||||
def init_output_directories(self, scene_name):
|
||||
"""Initialise output directories.
|
||||
|
|
@ -137,7 +126,7 @@ class SceneFileWriter:
|
|||
self.output_name, config["movie_file_extension"]
|
||||
)
|
||||
|
||||
# TODO: /dev/null would be good in case sections_output_dir is used without bein set (doesn't work on Windows), everyone likes defensive programming, right?
|
||||
# TODO: /dev/null would be good in case sections_output_dir is used without being set (doesn't work on Windows), everyone likes defensive programming, right?
|
||||
self.sections_output_dir = Path("")
|
||||
if config.save_sections:
|
||||
self.sections_output_dir = guarantee_existence(
|
||||
|
|
@ -358,7 +347,7 @@ class SceneFileWriter:
|
|||
Whether or not to write to a video file.
|
||||
"""
|
||||
if write_to_movie() and allow_write:
|
||||
self.open_movie_pipe(file_path=file_path)
|
||||
self.open_partial_movie_stream(file_path=file_path)
|
||||
|
||||
def end_animation(self, allow_write: bool = False):
|
||||
"""
|
||||
|
|
@ -371,9 +360,11 @@ class SceneFileWriter:
|
|||
Whether or not to write to a video file.
|
||||
"""
|
||||
if write_to_movie() and allow_write:
|
||||
self.close_movie_pipe()
|
||||
self.close_partial_movie_stream()
|
||||
|
||||
def write_frame(self, frame_or_renderer: np.ndarray | OpenGLRenderer):
|
||||
def write_frame(
|
||||
self, frame_or_renderer: np.ndarray | OpenGLRenderer, num_frames: int = 1
|
||||
):
|
||||
"""
|
||||
Used internally by Manim to write a frame to
|
||||
the FFMPEG input buffer.
|
||||
|
|
@ -382,41 +373,41 @@ class SceneFileWriter:
|
|||
----------
|
||||
frame_or_renderer
|
||||
Pixel array of the frame.
|
||||
num_frames
|
||||
The number of times to write frame.
|
||||
"""
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
self.write_opengl_frame(frame_or_renderer)
|
||||
elif config.renderer == RendererType.CAIRO:
|
||||
frame = frame_or_renderer
|
||||
if write_to_movie():
|
||||
self.writing_process.stdin.write(frame.tobytes())
|
||||
if is_png_format() and not config["dry_run"]:
|
||||
self.output_image_from_array(frame)
|
||||
|
||||
def write_opengl_frame(self, renderer: OpenGLRenderer):
|
||||
if write_to_movie():
|
||||
self.writing_process.stdin.write(
|
||||
renderer.get_raw_frame_buffer_object_data(),
|
||||
frame: np.ndarray = (
|
||||
frame_or_renderer.get_frame()
|
||||
if config.renderer == RendererType.OPENGL
|
||||
else frame_or_renderer
|
||||
)
|
||||
for _ in range(num_frames):
|
||||
# Notes: precomputing reusing packets does not work!
|
||||
# I.e., you cannot do `packets = encode(...)`
|
||||
# and reuse it, as it seems that `mux(...)`
|
||||
# consumes the packet.
|
||||
# The same issue applies for `av_frame`,
|
||||
# reusing it renders weird-looking frames.
|
||||
av_frame = av.VideoFrame.from_ndarray(frame, format="rgba")
|
||||
for packet in self.video_stream.encode(av_frame):
|
||||
self.video_container.mux(packet)
|
||||
|
||||
if is_png_format() and not config["dry_run"]:
|
||||
image: Image = (
|
||||
frame_or_renderer.get_image()
|
||||
if config.renderer == RendererType.OPENGL
|
||||
else Image.fromarray(frame_or_renderer)
|
||||
)
|
||||
elif is_png_format() and not config["dry_run"]:
|
||||
target_dir = self.image_file_path.parent / self.image_file_path.stem
|
||||
extension = self.image_file_path.suffix
|
||||
self.output_image(
|
||||
renderer.get_image(),
|
||||
image,
|
||||
target_dir,
|
||||
extension,
|
||||
config["zero_pad"],
|
||||
)
|
||||
|
||||
def output_image_from_array(self, frame_data):
|
||||
target_dir = self.image_file_path.parent / self.image_file_path.stem
|
||||
extension = self.image_file_path.suffix
|
||||
self.output_image(
|
||||
Image.fromarray(frame_data),
|
||||
target_dir,
|
||||
extension,
|
||||
config["zero_pad"],
|
||||
)
|
||||
|
||||
def output_image(self, image: Image.Image, target_dir, ext, zero_pad: bool):
|
||||
if zero_pad:
|
||||
image.save(f"{target_dir}{str(self.frame_count).zfill(zero_pad)}{ext}")
|
||||
|
|
@ -467,11 +458,11 @@ class SceneFileWriter:
|
|||
if self.subcaptions:
|
||||
self.write_subcaption_file()
|
||||
|
||||
def open_movie_pipe(self, file_path=None):
|
||||
"""
|
||||
Used internally by Manim to initialise
|
||||
FFMPEG and begin writing to FFMPEG's input
|
||||
buffer.
|
||||
def open_partial_movie_stream(self, file_path=None):
|
||||
"""Open a container holding a video stream.
|
||||
|
||||
This is used internally by Manim initialize the container holding
|
||||
the video stream of a partial movie file.
|
||||
"""
|
||||
if file_path is None:
|
||||
file_path = self.partial_movie_files[self.renderer.num_plays]
|
||||
|
|
@ -480,49 +471,48 @@ class SceneFileWriter:
|
|||
fps = config["frame_rate"]
|
||||
if fps == int(fps): # fps is integer
|
||||
fps = int(fps)
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
width, height = self.renderer.get_pixel_shape()
|
||||
else:
|
||||
height = config["pixel_height"]
|
||||
width = config["pixel_width"]
|
||||
|
||||
command = [
|
||||
config.ffmpeg_executable,
|
||||
"-y", # overwrite output file if it exists
|
||||
"-f",
|
||||
"rawvideo",
|
||||
"-s",
|
||||
"%dx%d" % (width, height), # size of one frame
|
||||
"-pix_fmt",
|
||||
"rgba",
|
||||
"-r",
|
||||
str(fps), # frames per second
|
||||
"-i",
|
||||
"-", # The input comes from a pipe
|
||||
"-an", # Tells FFMPEG not to expect any audio
|
||||
"-loglevel",
|
||||
config["ffmpeg_loglevel"].lower(),
|
||||
"-metadata",
|
||||
f"comment=Rendered with Manim Community v{__version__}",
|
||||
]
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
command += ["-vf", "vflip"]
|
||||
if is_webm_format():
|
||||
command += ["-vcodec", "libvpx-vp9", "-auto-alt-ref", "0"]
|
||||
# .mov format
|
||||
elif config["transparent"]:
|
||||
command += ["-vcodec", "qtrle"]
|
||||
else:
|
||||
command += ["-vcodec", "libx264", "-pix_fmt", "yuv420p"]
|
||||
command += [file_path]
|
||||
self.writing_process = subprocess.Popen(command, stdin=subprocess.PIPE)
|
||||
partial_movie_file_codec = "libx264"
|
||||
partial_movie_file_pix_fmt = "yuv420p"
|
||||
av_options = {
|
||||
"an": "1", # ffmpeg: -an, no audio
|
||||
"crf": "23", # ffmpeg: -crf, constant rate factor (improved bitrate)
|
||||
}
|
||||
|
||||
def close_movie_pipe(self):
|
||||
if config.format == "webm":
|
||||
partial_movie_file_codec = "libvpx-vp9"
|
||||
av_options["-auto-alt-ref"] = "1"
|
||||
if config.transparent:
|
||||
partial_movie_file_pix_fmt = "yuva420p"
|
||||
|
||||
elif config.transparent:
|
||||
partial_movie_file_codec = "qtrle"
|
||||
partial_movie_file_pix_fmt = "argb"
|
||||
|
||||
with av.open(file_path, mode="w") as video_container:
|
||||
stream = video_container.add_stream(
|
||||
partial_movie_file_codec,
|
||||
rate=config.frame_rate,
|
||||
options=av_options,
|
||||
)
|
||||
stream.pix_fmt = partial_movie_file_pix_fmt
|
||||
stream.width = config.pixel_width
|
||||
stream.height = config.pixel_height
|
||||
|
||||
self.video_container = video_container
|
||||
self.video_stream = stream
|
||||
|
||||
def close_partial_movie_stream(self):
|
||||
"""Close the currently opened video container.
|
||||
|
||||
Used internally by Manim to first flush the remaining packages
|
||||
in the video stream holding a partial file, and then close
|
||||
the corresponding container.
|
||||
"""
|
||||
Used internally by Manim to gracefully stop writing to FFMPEG's input buffer
|
||||
"""
|
||||
self.writing_process.stdin.close()
|
||||
self.writing_process.wait()
|
||||
for packet in self.video_stream.encode():
|
||||
self.video_container.mux(packet)
|
||||
|
||||
self.video_container.close()
|
||||
|
||||
logger.info(
|
||||
f"Animation {self.renderer.num_plays} : Partial movie file written in %(path)s",
|
||||
|
|
@ -567,37 +557,92 @@ class SceneFileWriter:
|
|||
for pf_path in input_files:
|
||||
pf_path = Path(pf_path).as_posix()
|
||||
fp.write(f"file 'file:{pf_path}'\n")
|
||||
commands = [
|
||||
config.ffmpeg_executable,
|
||||
"-y", # overwrite output file if it exists
|
||||
"-f",
|
||||
"concat",
|
||||
"-safe",
|
||||
"0",
|
||||
"-i",
|
||||
str(file_list),
|
||||
"-loglevel",
|
||||
config.ffmpeg_loglevel.lower(),
|
||||
"-metadata",
|
||||
f"comment=Rendered with Manim Community v{__version__}",
|
||||
"-nostdin",
|
||||
]
|
||||
|
||||
if create_gif:
|
||||
commands += [
|
||||
"-vf",
|
||||
f"fps={np.clip(config['frame_rate'], 1, 50)},split[s0][s1];[s0]palettegen=stats_mode=diff[p];[s1][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle",
|
||||
]
|
||||
else:
|
||||
commands += ["-c", "copy"]
|
||||
av_options = {
|
||||
"safe": "0", # needed to read files
|
||||
}
|
||||
|
||||
if not includes_sound:
|
||||
commands += ["-an"]
|
||||
av_options["an"] = "1"
|
||||
|
||||
commands += [str(output_file)]
|
||||
partial_movies_input = av.open(
|
||||
str(file_list), options=av_options, format="concat"
|
||||
)
|
||||
partial_movies_stream = partial_movies_input.streams.video[0]
|
||||
output_container = av.open(str(output_file), mode="w")
|
||||
output_container.metadata["comment"] = (
|
||||
f"Rendered with Manim Community v{__version__}"
|
||||
)
|
||||
output_stream = output_container.add_stream(
|
||||
codec_name="gif" if create_gif else None,
|
||||
template=partial_movies_stream if not create_gif else None,
|
||||
)
|
||||
if config.transparent and config.format == "webm":
|
||||
output_stream.pix_fmt = "yuva420p"
|
||||
if create_gif:
|
||||
"""
|
||||
The following solution was largely inspired from this comment
|
||||
https://github.com/imageio/imageio/issues/995#issuecomment-1580533018,
|
||||
and the following code
|
||||
https://github.com/imageio/imageio/blob/65d79140018bb7c64c0692ea72cb4093e8d632a0/imageio/plugins/pyav.py#L927-L996.
|
||||
"""
|
||||
output_stream.pix_fmt = "rgb8"
|
||||
if config.transparent:
|
||||
output_stream.pix_fmt = "pal8"
|
||||
output_stream.width = config.pixel_width
|
||||
output_stream.height = config.pixel_height
|
||||
output_stream.rate = config.frame_rate
|
||||
graph = av.filter.Graph()
|
||||
input_buffer = graph.add_buffer(template=partial_movies_stream)
|
||||
split = graph.add("split")
|
||||
palettegen = graph.add("palettegen", "stats_mode=diff")
|
||||
paletteuse = graph.add(
|
||||
"paletteuse", "dither=bayer:bayer_scale=5:diff_mode=rectangle"
|
||||
)
|
||||
output_sink = graph.add("buffersink")
|
||||
|
||||
combine_process = subprocess.Popen(commands)
|
||||
combine_process.wait()
|
||||
input_buffer.link_to(split)
|
||||
split.link_to(palettegen, 0, 0) # 1st input of split -> input of palettegen
|
||||
split.link_to(paletteuse, 1, 0) # 2nd output of split -> 1st input
|
||||
palettegen.link_to(paletteuse, 0, 1) # output of palettegen -> 2nd input
|
||||
paletteuse.link_to(output_sink)
|
||||
|
||||
graph.configure()
|
||||
|
||||
for frame in partial_movies_input.decode(video=0):
|
||||
graph.push(frame)
|
||||
|
||||
graph.push(None) # EOF: https://github.com/PyAV-Org/PyAV/issues/886.
|
||||
|
||||
frames_written = 0
|
||||
while True:
|
||||
try:
|
||||
frame = graph.pull()
|
||||
frame.time_base = output_stream.codec_context.time_base
|
||||
frame.pts = frames_written
|
||||
frames_written += 1
|
||||
output_container.mux(output_stream.encode(frame))
|
||||
except av.error.EOFError:
|
||||
break
|
||||
|
||||
for packet in output_stream.encode():
|
||||
output_container.mux(packet)
|
||||
|
||||
else:
|
||||
for packet in partial_movies_input.demux(partial_movies_stream):
|
||||
# We need to skip the "flushing" packets that `demux` generates.
|
||||
if packet.dts is None:
|
||||
continue
|
||||
|
||||
packet.dts = None # This seems to be needed, as dts from consecutive
|
||||
# files may not be monotically increasing, so we let libav compute it.
|
||||
|
||||
# We need to assign the packet to the new stream.
|
||||
packet.stream = output_stream
|
||||
output_container.mux(packet)
|
||||
|
||||
partial_movies_input.close()
|
||||
output_container.close()
|
||||
|
||||
def combine_to_movie(self):
|
||||
"""Used internally by Manim to combine the separate
|
||||
|
|
@ -614,6 +659,10 @@ class SceneFileWriter:
|
|||
movie_file_path = self.movie_file_path
|
||||
if is_gif_format():
|
||||
movie_file_path = self.gif_file_path
|
||||
if len(partial_movie_files) == 0: # Prevent calling concat on empty list
|
||||
logger.info("No animations are contained in this scene.")
|
||||
return
|
||||
|
||||
logger.info("Combining to Movie file.")
|
||||
self.combine_files(
|
||||
partial_movie_files,
|
||||
|
|
@ -623,44 +672,77 @@ class SceneFileWriter:
|
|||
)
|
||||
|
||||
# handle sound
|
||||
if self.includes_sound:
|
||||
if self.includes_sound and config.format != "gif":
|
||||
sound_file_path = movie_file_path.with_suffix(".wav")
|
||||
# Makes sure sound file length will match video file
|
||||
self.add_audio_segment(AudioSegment.silent(0))
|
||||
self.audio_segment.export(
|
||||
sound_file_path,
|
||||
format="wav",
|
||||
bitrate="312k",
|
||||
)
|
||||
# Audio added to a VP9 encoded (webm) video file needs
|
||||
# to be encoded as vorbis or opus. Directly exporting
|
||||
# self.audio_segment with such a codec works in principle,
|
||||
# but tries to call ffmpeg via its CLI -- which we want
|
||||
# to avoid. This is why we need to do the conversion
|
||||
# manually.
|
||||
if config.format == "webm":
|
||||
with (
|
||||
av.open(sound_file_path) as wav_audio,
|
||||
av.open(sound_file_path.with_suffix(".ogg"), "w") as opus_audio,
|
||||
):
|
||||
wav_audio_stream = wav_audio.streams.audio[0]
|
||||
opus_audio_stream = opus_audio.add_stream("libvorbis")
|
||||
for frame in wav_audio.decode(wav_audio_stream):
|
||||
for packet in opus_audio_stream.encode(frame):
|
||||
opus_audio.mux(packet)
|
||||
|
||||
for packet in opus_audio_stream.encode():
|
||||
opus_audio.mux(packet)
|
||||
|
||||
sound_file_path = sound_file_path.with_suffix(".ogg")
|
||||
|
||||
temp_file_path = movie_file_path.with_name(
|
||||
f"{movie_file_path.stem}_temp{movie_file_path.suffix}"
|
||||
)
|
||||
commands = [
|
||||
config.ffmpeg_executable,
|
||||
"-i",
|
||||
str(movie_file_path),
|
||||
"-i",
|
||||
str(sound_file_path),
|
||||
"-y", # overwrite output file if it exists
|
||||
"-c:v",
|
||||
"copy",
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-b:a",
|
||||
"320k",
|
||||
# select video stream from first file
|
||||
"-map",
|
||||
"0:v:0",
|
||||
# select audio stream from second file
|
||||
"-map",
|
||||
"1:a:0",
|
||||
"-loglevel",
|
||||
config.ffmpeg_loglevel.lower(),
|
||||
"-metadata",
|
||||
f"comment=Rendered with Manim Community v{__version__}",
|
||||
# "-shortest",
|
||||
str(temp_file_path),
|
||||
]
|
||||
subprocess.call(commands)
|
||||
av_options = {
|
||||
"shortest": "1",
|
||||
"metadata": f"comment=Rendered with Manim Community v{__version__}",
|
||||
}
|
||||
|
||||
with (
|
||||
av.open(movie_file_path) as video_input,
|
||||
av.open(sound_file_path) as audio_input,
|
||||
):
|
||||
video_stream = video_input.streams.video[0]
|
||||
audio_stream = audio_input.streams.audio[0]
|
||||
output_container = av.open(
|
||||
str(temp_file_path), mode="w", options=av_options
|
||||
)
|
||||
output_video_stream = output_container.add_stream(template=video_stream)
|
||||
output_audio_stream = output_container.add_stream(template=audio_stream)
|
||||
|
||||
for packet in video_input.demux(video_stream):
|
||||
# We need to skip the "flushing" packets that `demux` generates.
|
||||
if packet.dts is None:
|
||||
continue
|
||||
|
||||
# We need to assign the packet to the new stream.
|
||||
packet.stream = output_video_stream
|
||||
output_container.mux(packet)
|
||||
|
||||
for packet in audio_input.demux(audio_stream):
|
||||
# We need to skip the "flushing" packets that `demux` generates.
|
||||
if packet.dts is None:
|
||||
continue
|
||||
|
||||
# We need to assign the packet to the new stream.
|
||||
packet.stream = output_audio_stream
|
||||
output_container.mux(packet)
|
||||
|
||||
output_container.close()
|
||||
|
||||
shutil.move(str(temp_file_path), str(movie_file_path))
|
||||
sound_file_path.unlink()
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ __all__ = ["ThreeDScene", "SpecialThreeDScene"]
|
|||
|
||||
|
||||
import warnings
|
||||
from typing import Iterable, Sequence
|
||||
from collections.abc import Iterable, Sequence
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -283,16 +283,15 @@ class ThreeDScene(Scene):
|
|||
frame_center = frame_center.get_center()
|
||||
frame_center = list(frame_center)
|
||||
|
||||
zoom_value = None
|
||||
if zoom is not None:
|
||||
zoom_value = config.frame_height / (zoom * cam.height)
|
||||
|
||||
for value, method in [
|
||||
[theta, "theta"],
|
||||
[phi, "phi"],
|
||||
[gamma, "gamma"],
|
||||
[
|
||||
config.frame_height / (zoom * cam.height)
|
||||
if zoom is not None
|
||||
else None,
|
||||
"zoom",
|
||||
],
|
||||
[zoom_value, "zoom"],
|
||||
[frame_center, "frame_center"],
|
||||
]:
|
||||
if value is not None:
|
||||
|
|
|
|||
|
|
@ -297,7 +297,7 @@ class VectorScene(Scene):
|
|||
"""
|
||||
if not isinstance(label, MathTex):
|
||||
if len(label) == 1:
|
||||
label = "\\vec{\\textbf{%s}}" % label
|
||||
label = "\\vec{\\textbf{%s}}" % label # noqa: UP031
|
||||
label = MathTex(label)
|
||||
if color is None:
|
||||
color = vector.get_color()
|
||||
|
|
@ -904,9 +904,8 @@ class LinearTransformationScene(VectorScene):
|
|||
if new_label:
|
||||
label_mob.target_text = new_label
|
||||
else:
|
||||
label_mob.target_text = "{}({})".format(
|
||||
transformation_name,
|
||||
label_mob.get_tex_string(),
|
||||
label_mob.target_text = (
|
||||
f"{transformation_name}({label_mob.get_tex_string()})"
|
||||
)
|
||||
label_mob.vector = vector
|
||||
label_mob.kwargs = kwargs
|
||||
|
|
@ -1003,8 +1002,11 @@ class LinearTransformationScene(VectorScene):
|
|||
Animation
|
||||
The animation of the movement.
|
||||
"""
|
||||
start = VGroup(*pieces)
|
||||
target = VGroup(*(mob.target for mob in pieces))
|
||||
|
||||
v_pieces = [piece for piece in pieces if isinstance(piece, VMobject)]
|
||||
start = VGroup(*v_pieces)
|
||||
target = VGroup(*(mob.target for mob in v_pieces))
|
||||
|
||||
# don't add empty VGroups
|
||||
if self.leave_ghost_vectors and start.submobjects:
|
||||
# start.copy() gives a VGroup of Vectors
|
||||
|
|
@ -1091,6 +1093,7 @@ class LinearTransformationScene(VectorScene):
|
|||
**kwargs
|
||||
Any valid keyword argument of self.apply_transposed_matrix()
|
||||
"""
|
||||
|
||||
self.apply_transposed_matrix(np.array(matrix).T, **kwargs)
|
||||
|
||||
def apply_inverse(self, matrix: np.ndarray | list | tuple, **kwargs):
|
||||
|
|
|
|||
|
|
@ -77,10 +77,10 @@ __all__ = [
|
|||
"FunctionOverride",
|
||||
"PathFuncType",
|
||||
"MappingFunction",
|
||||
"Image",
|
||||
"GrayscaleImage",
|
||||
"RGBImage",
|
||||
"RGBAImage",
|
||||
"PixelArray",
|
||||
"GrayscalePixelArray",
|
||||
"RGBPixelArray",
|
||||
"RGBAPixelArray",
|
||||
"StrPath",
|
||||
"StrOrBytesPath",
|
||||
]
|
||||
|
|
@ -582,7 +582,7 @@ MappingFunction: TypeAlias = Callable[[Point3D], Point3D]
|
|||
Image types
|
||||
"""
|
||||
|
||||
Image: TypeAlias = npt.NDArray[ManimInt]
|
||||
PixelArray: TypeAlias = npt.NDArray[ManimInt]
|
||||
"""``shape: (height, width) | (height, width, 3) | (height, width, 4)``
|
||||
|
||||
A rasterized image with a height of ``height`` pixels and a width of
|
||||
|
|
@ -595,24 +595,24 @@ lightness (for greyscale images), an `RGB_Array_Int` or an
|
|||
`RGBA_Array_Int`.
|
||||
"""
|
||||
|
||||
GrayscaleImage: TypeAlias = Image
|
||||
GrayscalePixelArray: TypeAlias = PixelArray
|
||||
"""``shape: (height, width)``
|
||||
|
||||
A 100% opaque grayscale `Image`, where every pixel value is a
|
||||
A 100% opaque grayscale `PixelArray`, where every pixel value is a
|
||||
`ManimInt` indicating its lightness (black -> gray -> white).
|
||||
"""
|
||||
|
||||
RGBImage: TypeAlias = Image
|
||||
RGBPixelArray: TypeAlias = PixelArray
|
||||
"""``shape: (height, width, 3)``
|
||||
|
||||
A 100% opaque `Image` in color, where every pixel value is an
|
||||
A 100% opaque `PixelArray` in color, where every pixel value is an
|
||||
`RGB_Array_Int` object.
|
||||
"""
|
||||
|
||||
RGBAImage: TypeAlias = Image
|
||||
RGBAPixelArray: TypeAlias = PixelArray
|
||||
"""``shape: (height, width, 4)``
|
||||
|
||||
An `Image` in color where pixels can be transparent. Every pixel
|
||||
A `PixelArray` in color where pixels can be transparent. Every pixel
|
||||
value is an `RGBA_Array_Int` object.
|
||||
"""
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -24,6 +24,8 @@ are non official approximate values intended to simulate AS 2700 colors:
|
|||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .core import ManimColor
|
||||
|
||||
B11_RICH_BLUE = ManimColor("#2B3770")
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ in the standard:
|
|||
.. automanimcolormodule:: manim.utils.color.BS381
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .core import ManimColor
|
||||
|
||||
BS381_101 = ManimColor("#94BFAC")
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ List of Color Constants
|
|||
|
||||
.. automanimcolormodule:: manim.utils.color.X11
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .core import ManimColor
|
||||
|
||||
ALICEBLUE = ManimColor("#F0F8FF")
|
||||
|
|
|
|||
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