This commit is contained in:
JasonGrace2282 2024-07-09 08:38:11 -04:00
commit 79e5b8022d
No known key found for this signature in database
GPG key ID: 8D61FE3F93FB15FA
191 changed files with 7030 additions and 3179 deletions

View file

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

@ -0,0 +1,5 @@
nam
sherif
falsy
medias
strager

View file

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

View file

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

@ -0,0 +1,2 @@
# Switched to ruff format:
24025b60d57301b0a59754c38d77bccd8ed69feb

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ fail_fast: false
exclude: ^(manim/grpc/gen/|docs/i18n/)
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: 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)$

View file

@ -7,7 +7,6 @@ build:
apt_packages:
- libpango1.0-dev
- ffmpeg
- graphviz
python:

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
furo
myst-parser
sphinx<5.1
sphinx>=7.3
sphinx-copybutton
sphinxext-opengraph

View file

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

View file

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

View file

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

View file

@ -81,3 +81,4 @@ Index
docs/examples
docs/references
docs/typings
docs/types

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -66,7 +66,7 @@ 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,5 @@
from __future__ import annotations
import sys
import click
import cloup

View file

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

View file

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

View file

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

View file

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

View file

@ -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)."""
@ -1059,15 +1069,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 +1202,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

View file

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

View file

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

View file

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

View file

@ -12,7 +12,6 @@
"""
from __future__ import annotations
__all__ = [

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,6 +5,7 @@ init``. Here you can specify options, subcommands, and subgroups for the init
group.
"""
from __future__ import annotations
import configparser

View file

@ -5,6 +5,7 @@ plugin``. Here you can specify options, subcommands, and subgroups for the plugi
group.
"""
from __future__ import annotations
import cloup

View file

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

View file

@ -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="",
),
)

View file

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

View file

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

View file

@ -472,7 +472,6 @@ class Star(Polygon):
Examples
--------
.. manim:: StarExample
:save_as_gif:
class StarExample(Scene):
def construct(self):

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -40,7 +40,7 @@ __all__ = [
import itertools as it
from typing import Iterable, Sequence
from collections.abc import Iterable, Sequence
import numpy as np

View file

@ -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,14 +39,9 @@ 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,
@ -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
@ -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

View file

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

View file

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

View file

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

View file

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

View file

@ -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}")

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -24,6 +24,9 @@ in the standard:
.. automanimcolormodule:: manim.utils.color.BS381
"""
from __future__ import annotations
from .core import ManimColor
BS381_101 = ManimColor("#94BFAC")

View file

@ -22,6 +22,9 @@ List of Color Constants
.. automanimcolormodule:: manim.utils.color.X11
"""
from __future__ import annotations
from .core import ManimColor
ALICEBLUE = ManimColor("#F0F8FF")

View file

@ -23,6 +23,9 @@ taken from https://www.w3schools.com/colors/colors_xkcd.asp.
.. automanimcolormodule:: manim.utils.color.XKCD
"""
from __future__ import annotations
from .core import ManimColor
ACIDGREEN = ManimColor("#8FFE09")

View file

@ -47,12 +47,12 @@ The following modules contain the predefined color constants:
"""
from typing import Dict, List
from __future__ import annotations
from . import AS2700, BS381, X11, XKCD
from .core import *
from .manim_colors import *
_all_color_dict: Dict[str, ManimColor] = {
_all_color_dict: dict[str, ManimColor] = {
k: v for k, v in globals().items() if isinstance(v, ManimColor)
}

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