Bring in main's FileWriter into experimental (#3821)

* pyproject.toml: update manimpango version (#3405)

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Added docs for functions in `mobject_update_utils` (#3325)

* Added docs for functions in mobject_update_utils

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Updated docstring of always_shift

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Added period to sentence.

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Updated parameter description in always_redraw

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Update always_rotate description

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Finished parameters in always_redraw

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Changed comment in always_shift

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* update always_shift description

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* used normalize from manim.utils.space_ops

* fixed indentation in always_redraw

* added type-hints

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Fix tests to run on Cairo 1.18.0 (#3416)

* Add a script to build and install cairo

* Update gui tests for cairo 1.18.0

* update script to set env vars

* Make the script run with plain python

* Prefer the recently built one in pkg-config

* Skip the built if it's windows

* CI: build and install latest cairo

* CI: only run when cache is missed

* Disable compiling tests while building cairo

* update poetry lock file

* Display the cairo version when running pytest

* fixup

* tests: skip graphical test when cairo is old

* fix the path to find the pkgconfig files on linux

* set the LD_LIBRARY_PATH too

only then it'll work on linux

* fixup

* small fixup

* Move the script inside `.github/scripts` folder

* Make the minimum cairo version a constant

* Seperate setting env vars to a sperate step

this seem to have broken when cache is hit

* Fix: Fixed a bug in regards to empty inputs in AddTextLetterByLetter class.  (#3404)

* Misc: Just a class to test out some functions

* Fix: Fixed a bug in AddTextLetterByLetter class

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix: Adjusted changes according to Ben's comments

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix: Removed imports

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Feat: Adjusted changes to AddTextLetterByLetter

* Feat: Added test_creation

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Introduce new workflow creating a downloadable version of the documentation (#3417)

* Revert "rtd: enable htmlzip build (#3355)"

This reverts commit 571f79be2c.

* use python3.11 to build docs

* upgrade python version used in release publish workflow

* new workflow for building downloadable docs

* change event trigger for testing

* sudo apt

* rename release job; build html in poetry env

* set GITHUB_PATH instead of PATH

* introduce additional step

* use correct binary path

* forgot microtype

* fonts-roboto + actually compress files correctly

* fix asset path

* Update .github/workflows/release-publish-documentation.yml

Co-authored-by: Naveen M K <naveen521kk@gmail.com>

* pull_request -> workflow_dispatch

* Update .github/workflows/release-publish-documentation.yml

---------

Co-authored-by: Naveen M K <naveen521kk@gmail.com>

* Fix incorrect submobject count of multi-part Tex/MathTex mobjects by stopping them from adding empty submobjects (#3423)

* do not add a VectorizedPoint as a submobject if SingleStringMathTex renders to empty SVG

* test new behavior

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update tests/module/mobject/text/test_texmobject.py

* Update tests/module/mobject/text/test_texmobject.py

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* CI: fix caching of cairo (#3419)

I forgot to change the path after moving around the file.

* Fix CSV reader adding empty lists in rendering summary (#3430)

* Fix CSV reader adding empty files

Fixes issue #3311

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Fix None check order in _tree_layout (#3421)

* Fix None check order in _tree_layout

* add tests to test_graph.py

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Bump teatimeguest/setup-texlive-action from 2 to 3 (#3431)

Bumps [teatimeguest/setup-texlive-action](https://github.com/teatimeguest/setup-texlive-action) from 2 to 3.
- [Release notes](https://github.com/teatimeguest/setup-texlive-action/releases)
- [Commits](https://github.com/teatimeguest/setup-texlive-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: teatimeguest/setup-texlive-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* bump dependencies -- see #3241 (#3433)

* Fix Typing (#3086)

* first draft of color class + starting library conversion

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* changed everything to Manim color todo: figure out circular dependency in utils

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* first working draft of new color version

* resolving conflicts

* resolving conflicts

* resolving conflicts

* resolving conflicts

* resolving conflicts

* changed default internal value of ManimColor to np.ndarray[float]

* starting to fix tests

* fixed more tests and changed precision of manim color

* removed premature color conversion

* fixed some more tests

* final test changes

* fix doctests

* fix for 3.8

* fixing ManimColor string representation

* removing some unneccesary conversions

* moved community constants to manim_colors.py and added more color standards

* Added typing.py and typed bezier.py, core.py, constants.py  fully

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fixed codeql complaints

* add type ignore for np.allclose

* fixed import in three_dimensions

* added ignore for F401 back again in flake

* added typings to coordinate_systems.py

* Few improvements to `graphing/coordinate_systems.py`

* added some typings to mobject/geometry/line.py

* updated typings for mobject/geometry/line.py

* Add missing imports to `line.py`

* added typings to three_dimensions.py

* Use `FunctionOverride` for animation overrides

Fix type signature of `set_color_by_gradient`

* Remove `TYPE_CHECKING` check

Doc is failing

* Revert "Remove `TYPE_CHECKING` check"

Fails due to circular import

* Use `Self` in `coordinate_systems.py`

* Typehinted mobject.py and updated manim.typing.py

* Typed VMobject

* Type-hinted manim.mobject.geometry

* math.cos->np.cos, etc & fixed incorrect typehints

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix missing annotations import

* TypeAlias fix in typing.py

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add ignore errors again to mypy because commits are not possible like this

* Fix last typing issues

* Update docs

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Only type check manim

* Try fixing pre-commit

* fix merge

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix compat

* Fix compat again

* Fix imports compat

* Use union syntax

* Use union syntax

* Fix reduce_across_dimension

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Various test and merge fixes

* Doc fixes

* Last doc fix

* Revert usage of np over math

* Bump numpy version

* Remove obsolete duplicate example

* Fixed Incorrect Typehint in manim.constants

* Fix docstring typo

* More fixes

Use mypy.ini instead of .mypy.ini
Fix more docstrings
Improve types in utils and constants

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* docs fixes

* Add internal aliases

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix compat

* line lengths in .rst file, formatting, typos

* add docstring for space_ops:cross2d

* add some more arrow tip typings (in a non-circular import causing way)

* yes, this can be deleted

* fix formatting of example

* added docstring to bezier::inverse_interpolation

* added docstring + test for bezier::match_interpolate

* some improvements in coordinate_systems

* Vector -> Vector3

* replaced np.ndarray with more appropriate type hints

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Apply feedback

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* revert to previous (new) version

* fix doctest

* fix ReST errors

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Alex Lembcke <alex.lembcke@gmail.com>
Co-authored-by: Viicos <65306057+Viicos@users.noreply.github.com>
Co-authored-by: JasonGrace2282 <aarush.deshpande@gmail.com>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* fix: issue with ImageMobject bounding box (#3340)

* fix: fix an issue with ImageMobject bounding box

A missing point resulted in smaller bounding box causing issues it to be
smaller when the object is rotated. Added the missing fourth point to
ImageMobject points and altered call from camera. Filled in docstring
that used to propagate from superclass, saying that ImageMobject has no
points.

* add a test to check that rotating an image to and from doesn't change it

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: Václav Blažej <vaclav.blazej@warwick.ac.uk>
Co-authored-by: Naveen M K <naveen521kk@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* chore(deps): add Python 3.12 support (#3395)

* chore(deps): add Python 3.11 and 3.12 support


chore(deps): update lock file


chore(deps): remove colour


fix(deps): force NumPy version


fix(deps): relax constraints


chore(deps): update lock file

* fix(deps): make poetry happy

* fix(ci): skia pathops on 3.12

* fix(test): doctest skip

* disable python 3.8 pipeline

* removed get_parameters, replaced by direct call to inspect

* black

---------

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Added ability to remove non-svg LaTeX files (#3322)

* Added ability to remove latex junk (default True)

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fixed tests (hopefully), and whitelisted .tex

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* reverted weird changes from merge

* See previous commit message

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fixed logs-too-long test

* Fixed log output

* Fixed typo ;)

* deleted unused variable

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* moved latex deletion to tex_file_writing.py

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* removed changes in scene files

* Added caching based on LaTeX expression .svg

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Deleted unused function in delete_old_tex

* make if condition more readable

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* cleaned up svg file check

* changed blacklist -> whitelist for file endings

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Reverted docstring change

* Updated delete_non_svg files docstring

* Changed list to a set

* Update manim/_config/utils.py

* Update manim/cli/render/global_options.py

* added one test for the no_latex_cleanup config option

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
Co-authored-by: Tristan Schulz <mrdiverlp@gmail.com>

* feat: DecimalNumber() - added spacing between values and unit (#3366)

* feat: DecimalNumber() - added spacing between values and unit

* Update manim/mobject/text/numbers.py

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Update manim/mobject/text/numbers.py

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Update manim/mobject/text/numbers.py

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Update manim/mobject/text/numbers.py

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Update manim/mobject/text/numbers.py

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Update manim/mobject/text/numbers.py

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Update manim/mobject/text/numbers.py

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

---------

Co-authored-by: Tristan Schulz <mrdiverlp@gmail.com>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Add option to run examples directly with binder (#3427)

* Add option to run examples directly with binder

The minified JS is from
https://github.com/naveen521kk/manim-binder

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* slight style changes

* update the js file to fix on chrome

Signed-off-by: Naveen M K <naveen521kk@gmail.com>

* show the run button as an cursor

* make the video to be 100% of the width

* Update manim/utils/docbuild/manim_directive.py

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Add a "Make interactive" button instead of "Run" button

Clicking on the "Make interactive" button show the code-editor and "run" button

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* update margin for run interactive button

---------

Signed-off-by: Naveen M K <naveen521kk@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Prepare v0.18.0 (#3439)

* generated changelog and bumped version

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* changed some PR descriptions in the changelog

* fix some docbuild warnings

* fixed a reference that became ambiguous

* copyedit pass of changelog

* some more changelog polishing

* bump release date

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* updated release date

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Fixed wrong path in action building downloadable docs (#3450)

* fixed wrong path in action building downloadable docs

* fix second occurrence of wrong path

* Allow accessing ghost vectors in :class:`.LinearTransformationScene` (#3435)

* Fix CSV reader adding empty files

Fixes issue #3311

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Added LinearTransformationScene.ghost_vectors

* Added test and prevented empty VGroups as ghost vectors

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fixed typo in example

* Added ability to join together multiple renders

* Revert "Added ability to join together multiple renders" (wrong branch)

This reverts commit dee29c390f.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Add type hints to `_config` (#3440)

* Add type hints to `_config`

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix call issues

* Fix wrong value being used

* Fix test

* Fix wrong value being set

* lint

* Few type fixes

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Fix Idicate docs typo (#3461)

* Update indication.py (#3477)

reading docs, im sure oppising isnt a word

* Optimized `get_unit_normal()` and replaced `np.cross()` with custom `cross()` in `manim.utils.space_ops` (#3494)

* Added cross and optimized get_unit_normal in manim.utils.space_ops

* Added missing border case to new get_unit_normal where one vector is nonzero

* Updated test_threed.py::test_Sphere test data

* Update dependency constraints, fix deprecation warnings (#3376)

* WIP: Update metadata

* Finish removing upper bounds

Drop requests dependency, use urllib instead
order depencencies

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix issues on 3.12

* Order dev dependencies

* Update most dev deps, update lint config

* Add missing import

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* trigger CI

* More deprecation fixes

* Missing argument

* Deprecation fixes, again

* Use older xdist to fix test flakyness

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* fix 360° to 180° in quickstart tutorial (#3498)

* Update Docker base image to python3.12-slim (#3458) (#3459)

* Update Docker base image to python3.12-slim (#3458)

* Update docker/Dockerfile

---------

Co-authored-by: Melody Griesen <jvgriese@ncsu.edu>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* fix line_join to joint_type in example_scenes/basic.py (#3510)

* fix typo in docstring for DtUpdater example: line -> square (#3509)

* Implement caching of fonts list to improve runtime performance (#3316)

* Implement caching of fonts list to improve runtime performance

* Fix small use_svg_cache kwargs error

* replaced font list with LRU cache

* Removed deprecated new command (#3512)

Co-authored-by: Naveen M K <naveen521kk@gmail.com>

* Added `cap_style` feature to `VMobject` (#3516)

* Added cap_style feature to VMobject

* Added an example to `set_cap_style` method

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Unsplitted line 2501

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Added graphical test for cap_style

* Added vmobject_cap_styles.npz for testing cap_styles

* Removed # noqa comments from vectorized_mobject.py

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* feat(cli): optionally hide version splash (#3329)

* feat(cli): optionally hide version splash

As discussed in #3326, this PR proposes a new optional flag to hide the version splash when manim command in launched. Additionally, the splash print is now inly executed when the CLI is executed, not on module import.

After looking at the current documentation, it does not seem to change anything. I only saw that you documented a version splash for when the CLI is used, but not when the module is imported. So removing it should not break the api docs.

In the future, users can still have version information with `import manim; print(manim.__version__)`.

Closes #3326

* chore(tests): make tests pass

---------

Co-authored-by: Tristan Schulz <mrdiverlp@gmail.com>

* Reformatting the `--save_sections` output to have the format `<Scene>_<SecNum>_<SecName><extension>` (#3499)

* Worked on issue 3471, fixing rendered file names to inherit section name

* Modified file name to include section number and name

* Modified tests for file names to include number and name, in order to pass

---------

Co-authored-by: Tristan Schulz <mrdiverlp@gmail.com>

* Explain ``.Transform`` vs ``.ReplacementTransform`` in quickstart examples (#3500)

* Explained ReplacementTransform vs Transform

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Added section explaining Transform vs ReplacementTransform

* Added a->b->c example

* Clarified explanation

* Fixed Typo

* Fixed missing colon

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Tristan Schulz <mrdiverlp@gmail.com>

* Fix formatting building blocks (#3515)

* Fix formatting building blocks

* Fix formatting building blocks

---------

Co-authored-by: Jason Grace <110117391+JasonGrace2282@users.noreply.github.com>

* Bump jupyter-server from 2.9.1 to 2.11.2 (#3497)

Bumps [jupyter-server](https://github.com/jupyter-server/jupyter_server) from 2.9.1 to 2.11.2.
- [Release notes](https://github.com/jupyter-server/jupyter_server/releases)
- [Changelog](https://github.com/jupyter-server/jupyter_server/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jupyter-server/jupyter_server/compare/v2.9.1...v2.11.2)

---
updated-dependencies:
- dependency-name: jupyter-server
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Account for dtype in the pixel array so the maximum value stays correct in the invert function (#3493)

* fix(lib): fix

This fixes an issue where the `invert` argument would only work for `uint8` dtypes. Now the `max` value is updated according to the pixel array dtype.

Maybe we should add unit tests for that, but haven't found an obvious place to put unit tests.

* chore(ci): add basic test

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(ci): wrong attr name

* Update tests/module/mobject/test_image.py

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Added `grid_lines` attribute to `Rectangle` to add individual styling to the grid lines (#3428)

* Added 'grid_line_stroke_width' parameter in Rectangle

* Added 'grid_lines' (VGroup) attribute to 'Rectangle' class

---------

Co-authored-by: Tristan Schulz <mrdiverlp@gmail.com>

* Fix rectangle grid properties (#3082) (#3513)

* Import  for both vertical and horizontal gridlines in

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: Tristan Schulz <mrdiverlp@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Fix animations with zero runtime length to give a useful error instead of a broken pipe (#3491)

* Fix animation group not erroring when instantiated with an empty list

* Move error messages into Animation.begin()

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Added tests

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update manim/animation/animation.py

* Update manim/animation/composition.py

* Update manim/animation/animation.py

Co-authored-by: Jason Grace <110117391+JasonGrace2282@users.noreply.github.com>

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jason Grace <110117391+JasonGrace2282@users.noreply.github.com>

* fixed the stroke width issue with single color in streamlines (#3436)

* fixed the stroke width issue with single color in streamlines

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Added test for streamlines

* Added test for streamlines

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: MrDiver <mrdiverlp@gmail.com>

* Add Documentation to `.to_edge` and `to_corner` (#3408)

* Added docstrings and example renders to Mobject.to_corner() and Mobject.to_edge

* Added docstrings and example renders to Mobject.to_corner() and Mobject.to_edge

* Update manim/mobject/mobject.py

* Update manim/mobject/mobject.py

* Update manim/mobject/mobject.py

* Update manim/mobject/mobject.py

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update manim/mobject/mobject.py

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: Tristan Schulz <mrdiverlp@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Adding the ability to pass lists and generators to .play() (#3365)

* adding the ability to pass lists and generators to .play()

* fix for _AnimationBuilder

* Changed handling of generators to accept lists of generators and normal arguments at the same time

* Animation group handles generators

* Refactored into own function for reusability

* Fix typing

* Fix typing

---------

Co-authored-by: Jason Grace <110117391+JasonGrace2282@users.noreply.github.com>

* follow-up to #3491, made errors more consistent. fixes #3527

* chore(docs): add some words about Cairo 1.18 (#3530)

* chore(docs): add some words about Cairo 1.18

Closes #3521

* fix(docs): typo

* Update testing.rst

* Update testing.rst

* Fix formatting of ``MoveAlongPath`` docs (#3541)

* Remove wag method from Mobject

* Fixed MoveAlongPath

* Revert remove wag

Created a new branch with the wrong base, sorry ;)

* Fixed Animate Type-hint (#3543)

* Remove wag method from Mobject (#3539)

Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>

* Fix typo of `get_y_axis_label` docstring (#3547)

* Finish TODO's in ``contributing/typings.rst`` (#3545)

* Updated typing docs

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Added link for protocols

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Added object vs Any

* Fix Typo

* Rephrase TypeVar

Co-authored-by: Viicos <65306057+Viicos@users.noreply.github.com>

* Compare between tuple vs list

Co-authored-by: Viicos <65306057+Viicos@users.noreply.github.com>

* typing -> collections.abc

Co-authored-by: Viicos <65306057+Viicos@users.noreply.github.com>

* typing -> collections.abc

Co-authored-by: Viicos <65306057+Viicos@users.noreply.github.com>

* change method to attr

Co-authored-by: Viicos <65306057+Viicos@users.noreply.github.com>

* clarify object typehint

Co-authored-by: Viicos <65306057+Viicos@users.noreply.github.com>

* Fix code typo

Co-authored-by: Viicos <65306057+Viicos@users.noreply.github.com>

* Added if TYPE_CHECKING section

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix reST for inline code

* Elaborate on if TYPE_CHECKING

Co-authored-by: Viicos <65306057+Viicos@users.noreply.github.com>

* functions -> collections

Co-authored-by: Viicos <65306057+Viicos@users.noreply.github.com>

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Viicos <65306057+Viicos@users.noreply.github.com>

* Fix use of `Mobject`'s deprecated `get_*()` and `set_*()` methods in Cairo tests (#3549)

* Fix Deprecation warnings in cairo tests

* Fix animation/specialized.py

* add note in docstring of ManimColor about class constructors (#3554)

* Added support for Manim type aliases in Sphinx docs + Added new TypeAliases (#3484)

* Updated manim.typing and included TypeAliases in docs.source.conf

* Added Vector2 and reorganized manim_type_aliases

* Fixed __all__ exports for __all__ of manim

* Update manim/cli/render/global_options.py

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Draft of new typing docs and new autotyping directive

* Changed vertical bars to Unions

* Updated poetry.lock

* Created custom file parser for manim.typing

* Got reST parser going

* Updated autotyping and parsing

* Update parsing

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Added code_block toggle

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Added typings to directives

* Renamed Tuple to tuple in manim.typings

* Added missing docs for type aliases

* Fixed exponent typo in ManimInt

* Hyperlinks to types work - removed Module Attributes section

* Removed Unused Import

Remove ``import re``

* Added freeglut-devel to workflows for Linux

Hopefully (?) fix the GLU import error

* Fix package name

* Add support for Type Aliases section in every module - Renaming of Vector types

* Add/fix docs for directive, parser and others

* Fixed alias typo in module_parsing

* Fix decode/import bugs, fix minor details in docs

* Added missing docs for utils.docbuild and utils.testing

* Sort alphabetically entries in utilities_misc.rst

* Address review comments, add notes about Vector and hyperlinks inside definition blocks

---------

Co-authored-by: MrDiver <mrdiverlp@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: JasonGrace2282 <aarush.deshpande@gmail.com>

* Improve documentation section about contributing to docs (#3555)

* Improve section in docs about contributing to docs

* Add note about doc build command depending on the OS

* Improve section in docs about contributing to docs

* Add note about doc build command depending on the OS

* Fix wrong toctree path in docs/source/contributing/docs.rst

* Add helpful hints to `VGroup.add()` error message (#3561)

* Improve VGroup creation error message

* Use .__name__ for the type

Co-authored-by: Jason Grace <110117391+JasonGrace2282@users.noreply.github.com>

---------

Co-authored-by: Jason Grace <110117391+JasonGrace2282@users.noreply.github.com>

* exception add if new_rings is none (#3574)

* exception add if new_rings is none

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Fix typing of `Animation` (#3568)

* Add 'to be used in the future' TODOs to ManimFrame (#3553)

* Refactor `TexTemplate` (#3520)

* Refactor `TexTemplate`

* Add tests, refactor some things

* Fixed Some tests

* Move typing imports

* Fix remaining tests

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: JasonGrace2282 <aarush.deshpande@gmail.com>
Co-authored-by: Jason Grace <110117391+JasonGrace2282@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Bump github/codeql-action from 2 to 3 (#3567)

Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump actions/upload-artifact from 3 to 4 (#3566)

Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump actions/setup-python from 4 to 5 (#3565)

Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* updated several packages (pillow, jupyterlab, notebook, jupyterlab-lsp, jinja2, gitpython) (#3593)

* Removed -s / --save_last_frame flag from CLI arguments (#3528)

* Remove -s flag

* Make help text more verbose

* fix write_subcaption_file error when using opengl renderer (#3546)

* fix write_subcaption_file error when using opengl renderer

* Update manim/scene/scene_file_writer.py

---------

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Update docker.rst to use bash from the PATH (#3582)

* fix typo in value_tracker.py (#3594)

* fix `get_arc_center()` returning reference of point (#3599)

* Add ref_class (#3598)

* Fix typehint (#3592)

* Update ci.yml (#3611)

* fix type hint of indication.py (#3613)

* Revert vector type aliases to NumPy ndarrays (#3595)

* Improve handling of specified font name (#3429)

Co-authored-by: Jason Grace <110117391+JasonGrace2282@users.noreply.github.com>
Co-authored-by: JasonGrace2282 <aarush.deshpande@gmail.com>

The proposed fix does two things :

* If the specified font is 'sans-serif' : change it to 'sans' as this is the name used in the list of fonts
* if the font name is not in the list of fonts, automatically check if the capitalized version of the font exists in the list of fonts. If not, print a warning to the user.

* Remove support for dynamic plugin imports (#3524)

* Remove call to deprecated `pkg_resources`

* Remove support for dynamic plugin imports, update plugin utilities

* fix affected tests

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* more fixes

* Last fix

* Fix import

* Update docs

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jason Villanueva <a@jsonvillanueva.com>

* Run poetry lock --no-update (#3621)

* Update jupyter.rst (#3630)

Pinpoint IPython==8.21.0 for Google Colab, because more recent versions are incompatible with their runtime.

* Fix Vector3 -> Vector3D in contributing docs (#3639)

* Bump black from 23.12.1 to 24.3.0 (#3649)

Bumps [black](https://github.com/psf/black) from 23.12.1 to 24.3.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/23.12.1...24.3.0)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump cryptography from 42.0.0 to 42.0.4 (#3629)

Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.0 to 42.0.4.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/42.0.0...42.0.4)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Code Cleanup: removing unused imports and global variables (#3620)

* Remove unused import

* More security fixes

* Remove unused global variable

* More fixes

* Revert change (actual fix would require some rewrite)

* Add exception for edge case to satisfy warning

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Stuff

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Fixing the behavior of `.become` to not modify target mobject via side effects fix color linking (#3508)

* Copied ndarray for rgbas when interpolating

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* changing .become to copy the target mobject

* change tests and test data to reflect .become new behavior

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update tests/test_graphical_units/test_mobjects.py

* removed unused copy_submobject kwarg

* added doctests and improved documentation

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Added some examples for `Mobject`/`VMobject` methods (#3641)

* Add examples to mobject+vmobject methods

* Add missing import

* Separate whitespace to point_from_proportion

* Fixes!

* Changed example of Mobject.get_color

* Remove unneccessary import

* Add in import

* Fix typehint of `Vector` direction parameter (#3640)

* Fix typehint of Vector

* Change from Vector to Point in typehint

In `TipableVMobject._pointify` it converts a 3D
list of the form [x, y, z] to a Vector3D. Therefore
the direction parameter can take lists, not just numpy arrays.

* Fix bug in :class:`.VMobjectFromSVGPath` (#3677)

* Fixes #3676

* Update manim/mobject/svg/svg_mobject.py

Co-authored-by: adeshpande <110117391+JasonGrace2282@users.noreply.github.com>

* Fixed problem and added test

---------

Co-authored-by: adeshpande <110117391+JasonGrace2282@users.noreply.github.com>

* Flake8 rule C901 is about McCabe code complexity (#3673)

* Flake8 rule C901 is about McCabe code complexity

It is not about flake8-comprehensions.

* max-complexity = 29

* Fix broken link to Poetry's installation guide in the first time contributors page (#3692)

* Fix minor grammatical errors found in the index page of the documentation (#3690)

* Fix some minor grammatical errors in the index page of the docs

* Fix grammar

* Undo uneccessary change in phrasing

* fix(LICENSE): update year (#3689)

* Remove deprecated parameters and animations (#3688)

* Remove deprecated parameters/animations

* Remove test

* Remove test data

* Attempted fix for windows cp1252 encoding failure (#3687)

* Attempt to fix windows test

* Revert "Attempt to fix windows test"

This reverts commit e31c2077cd.

* try a different fix

* maybe both fixes together?

* try adding in CI

* Update ci.yml

* Update logger_utils.py

* maybe needs a dash?

* try utf8 again

* Remove legacy_windows

* try changing test

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Try decoding after capturing bytes output

* Nicer fix

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix typo (#3696)

* Docs: fix out-dated CLI option in Manim's Output Settings (#3674)

* Docs: fix out-dated CLI option in Manim's Output Settings

* Docs: more fluent English

Co-authored-by: adeshpande <110117391+JasonGrace2282@users.noreply.github.com>

* Docs: break lines

* Docs: more fluent English

* Docs: remove a space

Co-authored-by: adeshpande <110117391+JasonGrace2282@users.noreply.github.com>

---------

Co-authored-by: adeshpande <110117391+JasonGrace2282@users.noreply.github.com>

* only do actions if try succeeded (#3694)

* Mention pixi in installation guide (#3678)

* Mention pixi in installation guide

* Update docs/source/installation/conda.rst

Co-authored-by: adeshpande <110117391+JasonGrace2282@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Add note

---------

Co-authored-by: adeshpande <110117391+JasonGrace2282@users.noreply.github.com>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Fix successive calls of :meth:`.LinearTransformationScene.apply_matrix` (#3675)

* docs: improve installation FAQ's

* I have potentially resolved the issue when in LinearTransformationScene between two animations of transforming space we invoke the self.wait()

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* added another solutions in comments, added tests and removed wrong files from git

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* yeah , i forgot to save the file xd

* fixed the test, removed the comments my in changed file

* fix test and speed up test time for test_apply_matrix

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fixed the test, removed the comments my in changed file

* fixed the test

* Revert "docs: improve installation FAQ's"

This reverts commit e53a1c8d6f.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: JasonGrace2282 <aarush.deshpande@gmail.com>
Co-authored-by: adeshpande <110117391+JasonGrace2282@users.noreply.github.com>

* Bump actions/cache from 3 to 4 (#3607)

Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: adeshpande <110117391+JasonGrace2282@users.noreply.github.com>

* Bump FedericoCarboni/setup-ffmpeg from 2 to 3 (#3608)

Bumps [FedericoCarboni/setup-ffmpeg](https://github.com/federicocarboni/setup-ffmpeg) from 2 to 3.
- [Release notes](https://github.com/federicocarboni/setup-ffmpeg/releases)
- [Commits](https://github.com/federicocarboni/setup-ffmpeg/compare/v2...v3)

---
updated-dependencies:
- dependency-name: FedericoCarboni/setup-ffmpeg
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump ssciwr/setup-mesa-dist-win from 1 to 2 (#3609)

Bumps [ssciwr/setup-mesa-dist-win](https://github.com/ssciwr/setup-mesa-dist-win) from 1 to 2.
- [Release notes](https://github.com/ssciwr/setup-mesa-dist-win/releases)
- [Commits](https://github.com/ssciwr/setup-mesa-dist-win/compare/v1...v2)

---
updated-dependencies:
- dependency-name: ssciwr/setup-mesa-dist-win
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* docs: update typing guidelines (#3704)

* Update typing guidelines

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix formatting

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update documentation and typings for `ParametricFunction` (#3703)

* Update documentation and typings for ParametricFunction

* Use manim tyings

Co-authored-by: adeshpande <110117391+JasonGrace2282@users.noreply.github.com>

* fix typings

* a few doc fixes

* Update manim/mobject/graphing/functions.py

Co-authored-by: adeshpande <110117391+JasonGrace2282@users.noreply.github.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* update typings

* remove extraneous line

* update example code

* add line back for comptibility

* import TYPE_CHECKING

---------

Co-authored-by: adeshpande <110117391+JasonGrace2282@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* fix(copyright): automate copyright updating for docs (#3708)

* Fix some typehints in mobject.py (#3668)

* refactor(mobject): fix some typehints

* Move typing_extensions import under `if TYPE_CHECKING`
* Change from using `def animate(self: T ,...) -> T` to `def
  animate(self, ...) -> Self` as stated in PEP 673
* Fix incorrect usage of `T` in a method

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* move updaters type alias into TYPE_CHECKING

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Bump idna from 3.6 to 3.7 (#3693)

Bumps [idna](https://github.com/kjd/idna) from 3.6 to 3.7.
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.rst)
- [Commits](https://github.com/kjd/idna/compare/v3.6...v3.7)

---
updated-dependencies:
- dependency-name: idna
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump pillow from 10.2.0 to 10.3.0 (#3672)

Bumps [pillow](https://github.com/python-pillow/Pillow) from 10.2.0 to 10.3.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/10.2.0...10.3.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Fix typo (#3721)

* Fixed `Mobject.put_start_and_end_on` with same start and end point (#3718)

* fix put_start_and_end_on() at the same point

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* [pre-commit.ci] pre-commit autoupdate (#3332)

* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/pre-commit/pre-commit-hooks: v4.4.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.4.0...v4.6.0)
- [github.com/pycqa/isort: 5.12.0 → 5.13.2](https://github.com/pycqa/isort/compare/5.12.0...5.13.2)
- [github.com/asottile/pyupgrade: v3.10.1 → v3.15.2](https://github.com/asottile/pyupgrade/compare/v3.10.1...v3.15.2)
- [github.com/psf/black: 23.7.0 → 24.4.0](https://github.com/psf/black/compare/23.7.0...24.4.0)
- [github.com/asottile/blacken-docs: 1.15.0 → 1.16.0](https://github.com/asottile/blacken-docs/compare/1.15.0...1.16.0)
- [github.com/PyCQA/flake8: 6.1.0 → 7.0.0](https://github.com/PyCQA/flake8/compare/6.1.0...7.0.0)
- [github.com/pre-commit/mirrors-mypy: v1.5.1 → v1.9.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.5.1...v1.9.0)
- [github.com/codespell-project/codespell: v2.2.5 → v2.2.6](https://github.com/codespell-project/codespell/compare/v2.2.5...v2.2.6)

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* make smoothererstep readable again, avoid overlong line

* zoom_value more readable

* fix blacken-docs touching .github

* fix codespell setup, remove unnecessary file, fix some typos

* flake8: ignore E704, triggered by overload

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update docs/source/tutorials/quickstart.rst

* more flake fixes

* try to make blacken-docs happy

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* fix(autoaliasattr): search for type aliases under if TYPE_CHECKING (#3671)

* build(deps): read-the-docs sphinx (#3720)

* Fix issue where SpiralIn doesn't show elements. (#3589)

* Set SpiralIn to use fill_opacity 1 if not set

* Create SpiralIn control data

* Create test for SpiralIn

* Fix spiralin to separate fill and stroke opacity

* resolve opacity issue

* fix test data

---------

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Clean Graph layouts and increase flexibility (#3434)

* allow user-defined layout functions for Graph
+ fixup type annotations

* only pass relevant args

* write tests

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* change_layout forward root_vertex and partitions
- deduplicated layout code in __init__ and change_layout
- fixed change_layout backwards compatibility

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* add test for change_layout

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix copy/paste error

* fix

* fixup types for CodeQL

* static type the Layout Names

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix dynamic union type for Python 3.9

* add example scenes to LayoutFunction protocol documentation

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Replace references to np.ndarray with standard Manim types

* Label NxGraph as a TypeAlias

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Tristan Schulz <mrdiverlp@gmail.com>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Follow-up to graph layout cleanup: improvements for tests and typing (#3728)

* suggestions from review on #3434

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Update coordinate_systems.py (#3730)

small change

* build(ci): change from macos-latest to macos-13 (#3729)

* Add ``--preview_command`` cli flag (#3615)

* Add preview_command cli flag

* Edit help for --preview_command

* Change back from subprocess.run

* Remove old comment

* Bug with timg stopped happening with sp.run

* Fix docstring

* Revert "Fix docstring"

This reverts commit d2c00fc24dc46586f994237f1d2758528b78d6a3.

* Actually fix docstring

* Change help for option

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

---------

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* AnimationGroup: optimized interpolate() and fixed alpha bug on finish() (#3542)

* Optimized AnimationGroup computation of start-end times with lag ratio

* Added extra comment for init_run_time

* Added full path to imports in composition.py

* Optimized AnimationGroup.interpolate

* Fixed final bugs

* Removed accidental print

* Final fix to AnimationGroup.interpolate

* Fixed animations being skipped unintentionally

* Addressed requested changes

---------

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Fixed ```get_anchors()``` Return Type Inconsistency (#3214)

* changed return type of get_anchors()

* Ensured consistency with OpenGLVMobject

* Fixed CodeQl, updated docstring

* Update manim/mobject/types/vectorized_mobject.py

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Update manim/mobject/opengl/opengl_vectorized_mobject.py

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* fixed typo, t -> e

* fixed doctest

---------

Co-authored-by: Tristan Schulz <mrdiverlp@gmail.com>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>

* fixed [""] being set as loaded plugins (#3734)

* Prepare new release: v0.18.1 (#3719)

* add note about changelog in changelog.rst

* bump version

* Update CITATION.cff

* feat: Add three animations that together simulate a typing animation (#3612)

* feat: Add animations that together simulate typing

AddTextLetterByLetterWithCursor

RemoveTextLetterByLetterWithCursor

Blink

* Revert "feat: Add animations that together simulate typing"

This reverts commit 5fe256880d.

* Revert "Revert "feat: Add animations that together simulate typing""

This reverts commit 6a8244a157.

* Add new animations to __all__

* Temporarily remove docs example

* Modify "Blink" and add docstring examples back in

To avoid 0-second animations, which fail docstring test

* Address requested changes

Fix imports
Remove redundant constructor arguments
Improve names

* Shorten names

* Fix release documentation building (#3737)

* [pre-commit.ci] pre-commit autoupdate (#3739)

updates:
- [github.com/psf/black: 24.4.0 → 24.4.2](https://github.com/psf/black/compare/24.4.0...24.4.2)
- [github.com/pre-commit/mirrors-mypy: v1.9.0 → v1.10.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.9.0...v1.10.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Fixes #3744 (#3745)

Co-authored-by: Andrzej Nagórko <>

* Bump tqdm from 4.66.1 to 4.66.3 (#3746)

Bumps [tqdm](https://github.com/tqdm/tqdm) from 4.66.1 to 4.66.3.
- [Release notes](https://github.com/tqdm/tqdm/releases)
- [Commits](https://github.com/tqdm/tqdm/compare/v4.66.1...v4.66.3)

---
updated-dependencies:
- dependency-name: tqdm
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump jinja2 from 3.1.3 to 3.1.4 (#3750)

Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.3 to 3.1.4.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.3...3.1.4)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Add typehints to `manim.utils.iterables` (#3751)

* typehint iterables

* organize typing hints

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* remove any

* Add overloads for tuplify

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Remove example

* feedback

* Make TypeVars accessible at runtime

* Add hints for zip

Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com>

* typing -> collections.abc

Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com>

* try to make mypy happy

* zip[tuple[T, ...]] instead of zip[T]

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com>

* Let `SceneFileWriter` access `ffmpeg` via `av` instead of via external process (#3501)

* added av as a dependency

* make partial movie files use av instead of piping to external ffmpeg

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* opengl rendering: use av for movie files

* no need to check for ffmpeg executable

* refactor: *_movie_pipe -> *_partial_movie_stream

* improve (oneline) documentation

* pass more options to partial movie file rendering

* move ffmpeg verbosity settings to config; renamed option dict

* replaced call to ffmpeg in combine_files by using av

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* there was one examples saved as a gif?

* chore(deps): re-order av

* chore(lib): simplify `write_frame` method

Reduces the overall code complexity

* chore(lib): add audio

* fix(lib): same issue for conversion

* fix(lib): webm export

* fix(lib): transparent export

Though the output video is weird

* try(lib): fix gif + TODOs

* chore(deps): lower dep crit

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* feat(lib): add support for GIF

* fix(ci): rewrite tests

* fix

* chore(ci): prevent calling concat on empty list

* add missing dot

* fix(ci): update frame comparison ?

* fix(log): add handler to libav logger

* chore: add TODO

* fix(lib): concat issue

* Revert "fix(ci): update frame comparison ?"

This reverts commit 904cfb46ae.

* fix(ci): make it pass tests

* chore(lib/docs/ci): remove FFMPEG entirely

This removes any reference to FFMPEG, except in translation files

* added av as a dependency

* make partial movie files use av instead of piping to external ffmpeg

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* opengl rendering: use av for movie files

* no need to check for ffmpeg executable

* refactor: *_movie_pipe -> *_partial_movie_stream

* improve (oneline) documentation

* pass more options to partial movie file rendering

* move ffmpeg verbosity settings to config; renamed option dict

* replaced call to ffmpeg in combine_files by using av

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* there was one examples saved as a gif?

* chore(deps): re-order av

* chore(lib): simplify `write_frame` method

Reduces the overall code complexity

* chore(lib): add audio

* fix(lib): same issue for conversion

* fix(lib): webm export

* fix(lib): transparent export

Though the output video is weird

* try(lib): fix gif + TODOs

* chore(deps): lower dep crit

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* feat(lib): add support for GIF

* fix(ci): rewrite tests

* fix

* chore(ci): prevent calling concat on empty list

* add missing dot

* fix(ci): update frame comparison ?

* fix(log): add handler to libav logger

* chore: add TODO

* fix(lib): concat issue

* Revert "fix(ci): update frame comparison ?"

This reverts commit 904cfb46ae.

* fix(ci): make it pass tests

* chore(lib/docs/ci): remove FFMPEG entirely

This removes any reference to FFMPEG, except in translation files

* chore(deps): update lockfile

* chore(lib): rewrite ffprobe

* fix typo

* slightly more aggressive removal of ffmpeg in docs; minor language changes

* fix gif output stream dimensions

* minor style change

* fix encoding of (transparent) mov files

* fixed metadata / comment

* set frame rate for --format=gif in output_stream

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* more video tests for different render settings, also test pix_fmt

* improve default bitrate setting via crf

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* parametrized format/transparency rendering test

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* context managers for (some) av.open

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update manim/utils/commands.py

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>

* fixed segfault

* update test data involving implicit functions (output improved!)

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* explicity set pix_fmt for transparent webms

* special-special case extracting frame from vp9-encoded file with transparency

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix transparent gifs, more special casing in parametrized video format test

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* run tests on macos-latest again

* removed old control data

* Revert "run tests on macos-latest again"

This reverts commit f50efa4b88.

* added sound to codec test; fixed issue with sound track in gif (disabled) and webm (now via opus)

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* manual wav -> ogg transcoding

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fixed f-string

* refactored codec test, split out gif

* check for non-zero audio samples

* more cleanup

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* remove ffmpeg from readthedocs apt_packages

* round up run_time if positive and shorter than current frame rate

* added more run_time tests

* black

* improve implementation of test

* removed some unused imports

* improve wording of logged warning

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>

* move run_time checks from Animation.begin to Scene.get_run_time

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* remove unused import

* flake: PT012

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>

* Use --py39-plus in pre-commit (#3761)

* Use --py39-plus in pre-commit

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix indication.py

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Optimized `manim.utils.bezier.is_closed()` (#3768)

* Optimized manim.utils.bezier.is_closed()

* oops, that shouldn't have been there

* Slightly optimized is_closed() even more

* Added doctest for is_closed()

* Created and optimized Bézier splitting functions such as `partial_bezier_points()` in `manim.utils.bezier` (#3766)

* Optimized manim.utils.partial_bezier_points()

* Added split_bezier, subdivide_bezier and bezier_remap, and tests

* Use bezier_remap() in VMobject and OpenGLVMobject()

* Note that partial_bezier_points is similar to calling split_bezier twice

* Bump requests to 2.32.0 (#3776)

updated-dependencies:
- dependency-name: requests
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Fix assertions and improve error messages when adding submobjects (#3756)

* Optimized AnimationGroup computation of start-end times with lag ratio

* Added extra comment for init_run_time

* Added full path to imports in composition.py

* Optimized AnimationGroup.interpolate

* Fixed final bugs

* Removed accidental print

* Final fix to AnimationGroup.interpolate

* Fixed animations being skipped unintentionally

* Fix and improve Mobject assertions when adding submobjects

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update examples in Mobject.add() and OpenGLMobject.add() docstrings

* overriden -> overridden

* Joined string in OpenGLMobject error message

* Address requested changes

* OpenGLVMObjects -> OpenGLVMobjects

* Use tuplify in VGroup.__setitem__()

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Add pyproject for ruff formatting (#3777)

* Add pyproject for ruff

* add black config back

* Make only formatting

* rearrange isort to undo diff

* poetry lock

* Feedback

* style

Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com>

---------

Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com>

* pre-commit change to ruff (#3779)

* pre-commit change to ruff

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fixes

* astral-sh ruff bump

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Ignore Ruff format in git blame (#3781)

* Fixed `there_and_back_with_pause()` rate function behaviour with different `pause_ratio` values (#3778)

Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>

* Optimize VMobject methods which append to points (#3765)

* Add `@` shorthand for `CoordinateSystem` methods `coords_to_point` (`c2p`) and `point_to_coords` (`p2c`) (#3754)

* Add shorthand for axes

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add spacing

Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com>

* Convert CoordinateSystem example, and add to NumberLine

* Add doctest for NumberLine

* Add test

* Fix typehint for c2p

Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com>

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>

* [pre-commit.ci] pre-commit autoupdate (#3784)

* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.4 → v0.4.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.4...v0.4.5)
- [github.com/codespell-project/codespell: v2.2.6 → v2.3.0](https://github.com/codespell-project/codespell/compare/v2.2.6...v2.3.0)

* Fix typo

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: JasonGrace2282 <aarush.deshpande@gmail.com>

* [pre-commit.ci] pre-commit autoupdate (#3794)

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.5 → v0.4.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.5...v0.4.7)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Add Ruff Lint (#3780)

Adds Ruff Linting to CI, and replaces isort in the pre-commit config with Ruff's isort rules.

Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com>

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com>

* Replace Pyupgrade with Ruff rule (#3795)

* Add config for pyupgrade

* Fix pyupgrade errors

* Unsafe-fixes

* Nicer way of formatting

Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Revert "Nicer way of formatting"

This reverts commit 48013f4a30.

---------

Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Bump tornado from 6.4 to 6.4.1 (#3796)

Bumps [tornado](https://github.com/tornadoweb/tornado) from 6.4 to 6.4.1.
- [Changelog](https://github.com/tornadoweb/tornado/blob/master/docs/releases.rst)
- [Commits](https://github.com/tornadoweb/tornado/compare/v6.4.0...v6.4.1)

---
updated-dependencies:
- dependency-name: tornado
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Update opengl_vectorized_mobject.py (#3790)

The "insert_n_curves_to_point_list" function requires the "points" argument to be a numpy array, since it calls the "get_bezier_tuples_from_points" function, which requires "points" to be a numpy array because it has the "return points.reshape((-1, nppc, 3))" statement. Ordinary lists do not have a "reshape" method.

So we need to convert "sp1" and "sp2" to numpy arrays before calling the "insert_n_curves_to_point_list" function.

Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>

* [pre-commit.ci] pre-commit autoupdate (#3801)

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.7 → v0.4.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.7...v0.4.8)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Add typings to `OpenGLMobject` (#3803)

* Add typings to OpenGLMobject

* Import typing_extensions

* Add explicit returns to inner functions in .arrange_in_grid()

* Add quotes to parameters in ValueError

* Add some more typings

* Address requested changes

* Type apply_over_attr_arrays with TypeVar

* Fix use of TypeVar

* Add Vector3D typing in set_x, set_y and set_z

* fix: importing manim should not trigger pygments.styles.get_all_styles (#3797)

* fix: importing manim should not trigger pygments.styles.get_all_styles

Removed the Code.styles_list attribute.

Rewrote the documentation to say that a list of all styles can be generated by calling list(pygments.styles.get_all_styles()).

The example in the docstring of Code was rewritten to use an explicit code style name.

* fix: small change to documentation

* Added potential class method to get available code styles.

* Adding typehints to newly-added attributes.

Co-authored-by: adeshpande <110117391+JasonGrace2282@users.noreply.github.com>

* Removing unnecessary lines.

Co-authored-by: adeshpande <110117391+JasonGrace2282@users.noreply.github.com>

---------

Co-authored-by: adeshpande <110117391+JasonGrace2282@users.noreply.github.com>

* [pre-commit.ci] pre-commit autoupdate (#3809)

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.8 → v0.4.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.8...v0.4.9)
- [github.com/PyCQA/flake8: 7.0.0 → 7.1.0](https://github.com/PyCQA/flake8/compare/7.0.0...7.1.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Bump urllib3 from 2.2.1 to 2.2.2 (#3810)

Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.2.1 to 2.2.2.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.2.1...2.2.2)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Update macos packages (#3812)

* Fixed infinite loop in OpenGL `BackgroundRectangle.get_color()` (#3732)

Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>

* docs(contributing): add manim.typing guide (#3669)

* docs: add manim.typing guide

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add colors

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add another example for when to typehint as Vector

* Add docs for images+functions

* write Beziers

* Improve based on feedback

* type -> Type

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>

* Implement partial movie files

* Sort of combining working

* Fix `DiGraph` edges not fading correctly on `FadeIn` and `FadeOut` (#3786)

* Make `Line::set_points_by_ends` behavior consistent with constructor

* Use `Line::set_points_by_ends` in edge updaters

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Undo unnecessary change to Graph

* Update manim/mobject/geometry/line.py

Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>

* fix segfault

* Fix upside down video

* fix some bugs with wait()

* Fix CLI flags

* Fix FileWriter not rendering for the correct amount of time in video

* Progress on removing cairo

* Avoid printing caching message if write-to-movie is false

* Fix Manim directive

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] pre-commit autoupdate (#3823)

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.9 → v0.4.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.9...v0.4.10)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Implement progressbar

* hji

* feat(autoaliasattr): Implement documentation of TypeVar's (#3818)

* feat(autoaliasattr): Implement Documentation of TypeVar's

* Feedback

---------

Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>

* Fixed `Arrow3D.put_start_and_end_on()` to use the actual end of the arrow (#3706)

* my test is not passing, i need to add a little bit of docs. except that everything is fine. Issue is solved!

* fixed the issue #3655

* removed comments

* fix: 3706 original issue, without adding unnecessary dot
added: i added self.height parameter in Cone class
my tests passes

* Changes that way how end point of Arrow3D is calculated.

* I've improved the methods get_start and get_end for the Cone class, and get_end for the Arrow3D class to ensure they return accurate geometrical points after transformations. Additionally, I've included unit tests to verify the correctness of these methods for the Cone class.

* Finished! Replaced VMobject by VectorizedPoint as Ben suggested while ago

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>

* Optimized `manim.utils.bezier.get_smooth_cubic_bezier_handle_points()` (#3767)

* Optimized manim.utils.get_smooth_cubic_bezier_handle_points()

* Fixed typo in docstring regarding vector u

* Add tests for get_smooth_cubic_bezier_handle_points

* Fix backreference in test docstrings

Co-authored-by: adeshpande <110117391+JasonGrace2282@users.noreply.github.com>

---------

Co-authored-by: adeshpande <110117391+JasonGrace2282@users.noreply.github.com>

* Implement no progressbar if write_to_movie=False

* Fix docker profile (#3827)

* Doc: add docstrings to Brace (#3715)

* Add docstrings to `Brace` methods

* Add full NumPy format docstring for the `Brace` methods

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* feedback

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: JasonGrace2282 <aarush.deshpande@gmail.com>
Co-authored-by: adeshpande <110117391+JasonGrace2282@users.noreply.github.com>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* Some docs

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] pre-commit autoupdate (#3834)

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.10 → v0.5.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.10...v0.5.0)
- [github.com/pre-commit/mirrors-mypy: v1.10.0 → v1.10.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.10.0...v1.10.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Bump docker/build-push-action from 5 to 6 (#3835)

Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* bugfixes + speed + linting

* Deprecate opengl fixture

* Implement basic plugins

* Add docstring with doctest

* Add pydantic to deps

* remove all pycairo references

* Cleanup other stuff

Adjust default values for config, clean up Scene

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] pre-commit autoupdate (#3844)

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.0 → v0.5.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.0...v0.5.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Bump certifi from 2024.2.2 to 2024.7.4 (#3841)

Bumps [certifi](https://github.com/certifi/python-certifi) from 2024.2.2 to 2024.7.4.
- [Commits](https://github.com/certifi/python-certifi/compare/2024.02.02...2024.07.04)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: adeshpande <110117391+JasonGrace2282@users.noreply.github.com>

* more plugin stuff

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* kinda working Rotate?

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Working Rotation?

* Better test scene

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* change interpolate_mobject -> interpolate

* README.md

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* undo i18n changes

* Remove deprecated quadratic Bézier functions

* Add docstring back to Rotate

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add missing type annotations

* Add manim prefix to imports in rotation.py

* Fix imports of AnimationProtocol and OpenGLMobject in __init__.py

* Modify Scene.replace() docstring

* Remove Point(PMobject) and only use Point(Mobject)

* Remove Point from point_cloud_mobject.__all__

* Fix Ruff errors and comment out submob.refresh_triangulation() in OpenGLVMobject.change_anchor_mode()

* Update OpenGLVMobject.insert_n_curves_to_point_list()

* Fix incorrect version number in plugin section in docs (#3849)

* Bump zipp from 3.18.2 to 3.19.1 (#3847)

Bumps [zipp](https://github.com/jaraco/zipp) from 3.18.2 to 3.19.1.
- [Release notes](https://github.com/jaraco/zipp/releases)
- [Changelog](https://github.com/jaraco/zipp/blob/main/NEWS.rst)
- [Commits](https://github.com/jaraco/zipp/compare/v3.18.2...v3.19.1)

---
updated-dependencies:
- dependency-name: zipp
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Fix commands.py to use Manager(SceneClass) instead of SceneClass()

* Reimplement buffers, add typehints, and start caching

Also started cleaning up Scene and Manager

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix ci

* Rename assemble_family -> note_changed_family to match 3b1b

* patch typehints due to lack of Proxy[T] in python typing

* Rename Image aliases to PixelArray, refactor imports to avoid circular imports, and refactor TypeVar R definition to allow defining class OpenGLMobject(Generic[R])

* Rename typing.Image type aliases to PixelArray to avoid conflict with PIL.Image (#3851)

* Revert some import refactor to pass tests

* Use get_bezier_tuples_from_points(points) in OpenGLVMobject.insert_n_curves_to_point_list()

* Progress on rewriting frame stuff

* Change from tempconfig to a config fixture in tests (#3853)

* Implement changes to fixtures

* Change tests to use the config fixture

* fix merge conflicts

* Fix :attr:`.ManimConfig.format` not updating movie file extension (#3839)

* Fix config.format not updating config.movie_file_extension

* Add test

* Rewrite `manim.utils.bezier.get_quadratic_approximation_of_cubic()` to produce curves which can be animated smoothly (#3829)

* Rewrite get_quadratic_approximation_of_cubic() and add test

* Move test_get... to end of file

* Complete docstring for get_quadratic...()

* Progress towards a working png file-writer

* Log execution time of scene rendering in the Manim Checkhealth command (#3855)

* Log execution time of scene rendering in the Manim Checkhealth command

* Use timeit.timeit instead of time.time for more reliable profiling

* Optimize `VMobject.pointwise_become_partial()` (#3760)

* Optimize VMobject.pointwise_become_partial()

* selftransformation -> self

* Small factorization of nppc

* Update macos.rst (#3857)

* Update macos.rst

As of July/2024, brew installs Manim and its dependencies.
Guideline for installing dependencies with brew, and attempt to install using pip3 will no longer work either venv or not.
Now homebrew team manages python resources as "System-wide" only with Brew installed resources. 
Hence, to give first time installer just leave a single line install command would be the simplest option we have.

* Update docs/source/installation/macos.rst

---------

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>

* VGroup -> OpenGLVGroup + typehint changes

* Remove file that appeared somehow

* Delete opengl tests

* Make manager generic in its scene for autocomplete

* Undo deletion of manim/mobject/opengl

* progress on tests

* Convert scene.render() -> manager.render()

* fix opengl context not found in tests

* use keyword argument when creating scene

This makes it less changes for subclasses to "just work"

* Change Scene() -> Manager(Scene)

* Change to use OpenGL* for some classes

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Naveen M K <naveen521kk@gmail.com>
Co-authored-by: Naveen M K <naveen521kk@gmail.com>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Immanuel-Alvaro-Bhirawa <127812163+Immanuel-Alvaro-Bhirawa@users.noreply.github.com>
Co-authored-by: Nikhil Iyer <iyer.h.nikhil@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Harald Schilly <harald.schilly@gmail.com>
Co-authored-by: Tristan Schulz <mrdiverlp@gmail.com>
Co-authored-by: Alex Lembcke <alex.lembcke@gmail.com>
Co-authored-by: Viicos <65306057+Viicos@users.noreply.github.com>
Co-authored-by: Václav Blažej <6208643+vaclavblazej@users.noreply.github.com>
Co-authored-by: Václav Blažej <vaclav.blazej@warwick.ac.uk>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
Co-authored-by: Uwe Zimmermann <uwe.zimmermann@sciencetronics.com>
Co-authored-by: Lawrence Qupty <80665382+Lawqup@users.noreply.github.com>
Co-authored-by: JosephD <46393716+jcep@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
Co-authored-by: szchixy <szchixy@outlook.com>
Co-authored-by: Melody Griesen <pikablue107@gmail.com>
Co-authored-by: Melody Griesen <jvgriese@ncsu.edu>
Co-authored-by: yuan <yuan_xin_yu@hotmail.com>
Co-authored-by: Benjamín Ubilla <118409119+MathItYT@users.noreply.github.com>
Co-authored-by: Doaa Muhammad <126016494+doaamuham@users.noreply.github.com>
Co-authored-by: Robin Dimasin <robindimasin@gmail.com>
Co-authored-by: Paul Uhlenbruck <48606747+pauluhlenbruck@users.noreply.github.com>
Co-authored-by: Yash Mundada <F20210001@pilani.bits-pilani.ac.in>
Co-authored-by: TheMathematicFanatic <63360493+TheMathematicFanatic@users.noreply.github.com>
Co-authored-by: Václav Volhejn <8401624+vvolhejn@users.noreply.github.com>
Co-authored-by: Hydromel Victor Doledji <victorvaddely@gmail.com>
Co-authored-by: Dan Davison <dandavison7@gmail.com>
Co-authored-by: Greg Rupp <gmrupp@gmail.com>
Co-authored-by: NotWearingPants <26556598+NotWearingPants@users.noreply.github.com>
Co-authored-by: Sparsh Goenka <43041139+sparshg@users.noreply.github.com>
Co-authored-by: Said Taghadouini <84044788+staghado@users.noreply.github.com>
Co-authored-by: Jason Villanueva <a@jsonvillanueva.com>
Co-authored-by: Abulafia <44573666+abul4fia@users.noreply.github.com>
Co-authored-by: Christian Clauss <cclauss@me.com>
Co-authored-by: Chin Zhe Ning <108804868+biinnnggggg@users.noreply.github.com>
Co-authored-by: HairlessVillager <64526732+HairlessVillager@users.noreply.github.com>
Co-authored-by: Pavel Zwerschke <pavelzw@gmail.com>
Co-authored-by: Sir James Clark Maxwell <71722499+SirJamesClarkMaxwell@users.noreply.github.com>
Co-authored-by: Daniel Zhu <danielfangzhu@gmail.com>
Co-authored-by: Stefano Ottolenghi <stejey@gmail.com>
Co-authored-by: MontroyJosh <122334909+MontroyJosh@users.noreply.github.com>
Co-authored-by: Greg Rupp <greg.rupp@66degrees.com>
Co-authored-by: Amirreza A <45117218+amrear@users.noreply.github.com>
Co-authored-by: Jinchu Li <63861808+JinchuLi2002@users.noreply.github.com>
Co-authored-by: VPC <111203113+VinhPhmCng@users.noreply.github.com>
Co-authored-by: anagorko <3418166+anagorko@users.noreply.github.com>
Co-authored-by: jkjkil4 <52841865+jkjkil4@users.noreply.github.com>
Co-authored-by: yang-tsao <caoyang2005@outlook.com>
Co-authored-by: Eddie Ruiz <eduardo.j.ruiz@gmail.com>
Co-authored-by: Cameron Burdgick <156892808+camburd2@users.noreply.github.com>
Co-authored-by: Francisco Manríquez <francisco.manriquezn@usm.cl>
Co-authored-by: CJ Lee <changjoon.lee@arenne.net>
This commit is contained in:
adeshpande 2024-07-14 21:12:04 -04:00 committed by GitHub
commit 5dcab4c4a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
148 changed files with 4015 additions and 7382 deletions

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

@ -18,7 +18,7 @@ repos:
- id: python-check-blanket-noqa
name: Precision flake ignores
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.9
rev: v0.5.1
hooks:
- id: ruff
name: ruff lint
@ -41,7 +41,7 @@ repos:
flake8-simplify==0.14.1,
]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
rev: v1.10.1
hooks:
- id: mypy
additional_dependencies:

View file

@ -21,7 +21,8 @@
Manim is an animation engine for explanatory math videos. It's used to create precise animations programmatically, as demonstrated in the videos of [3Blue1Brown](https://www.3blue1brown.com/).
> NOTE: This repository is maintained by the Manim Community and is not associated with Grant Sanderson or 3Blue1Brown in any way (although we are definitely indebted to him for providing his work to the world). If you would like to study how Grant makes his videos, head over to his repository ([3b1b/manim](https://github.com/3b1b/manim)). This fork is updated more frequently than his, and it's recommended to use this fork if you'd like to use Manim for your own projects.
> [!NOTE]
> This repository is maintained by the Manim Community and is not associated with Grant Sanderson or 3Blue1Brown in any way (although we are definitely indebted to him for providing his work to the world). If you would like to study how Grant makes his videos, head over to his repository ([3b1b/manim](https://github.com/3b1b/manim)). This fork is updated more frequently than his, and it's recommended to use this fork if you'd like to use Manim for your own projects.
## Table of Contents:
@ -35,7 +36,8 @@ Manim is an animation engine for explanatory math videos. It's used to create pr
## Installation
> **WARNING:** These instructions are for the community version _only_. Trying to use these instructions to install [3b1b/manim](https://github.com/3b1b/manim) or instructions there to install this version will cause problems. Read [this](https://docs.manim.community/en/stable/faq/installation.html#why-are-there-different-versions-of-manim) and decide which version you wish to install, then only follow the instructions for your desired version.
> [!WARNING]
> These instructions are for the community version _only_. Trying to use these instructions to install [3b1b/manim](https://github.com/3b1b/manim) or instructions there to install this version will cause problems. Read [this](https://docs.manim.community/en/stable/faq/installation.html#why-are-there-different-versions-of-manim) and decide which version you wish to install, then only follow the instructions for your desired version.
Manim requires a few dependencies that must be installed prior to using it. If you
want to try it out first before installing it locally, you can do so
@ -71,7 +73,7 @@ In order to view the output of this scene, save the code in a file called `examp
manim -p -ql example.py SquareToCircle
```
You should see your native video player program pop up and play a simple scene in which a square is transformed into a circle. You may find some more simple examples within this
You should see a window pop up and play a simple scene in which a square is transformed into a circle. You may find some more simple examples within this
[GitHub repository](example_scenes). You can also visit the [official gallery](https://docs.manim.community/en/stable/examples.html) for more advanced examples.
Manim also ships with a `%%manim` IPython magic which allows to use it conveniently in JupyterLab (as well as classic Jupyter) notebooks. See the
@ -84,13 +86,15 @@ The general usage of Manim is as follows:
![manim-illustration](https://raw.githubusercontent.com/ManimCommunity/manim/main/docs/source/_static/command.png)
The `-p` flag in the command above is for previewing, meaning the video file will automatically open when it is done rendering. The `-ql` flag is for a faster rendering at a lower quality.
The `-p` flag in the command above is for previewing, meaning a window will show up to render it in real time.
The `-ql` flag is for a faster rendering at a lower quality.
Some other useful flags include:
- `-s` to skip to the end and just show the final frame.
- `-n <number>` to skip ahead to the `n`'th animation of a scene.
- `-f` show the file in the file browser.
- `-w` to actually write the result into a video file.
For a thorough list of command line arguments, visit the [documentation](https://docs.manim.community/en/stable/guides/configuration.html).

View file

@ -5,7 +5,6 @@
from __future__ import annotations
import cairo
import moderngl
# If it is running Doctest the current directory
@ -34,7 +33,6 @@ def pytest_report_header(config):
info = ctx.info
ctx.release()
return (
f"\nCairo Version: {cairo.cairo_version()}",
"\nOpenGL information",
"------------------",
f"vendor: {info['GL_VENDOR'].strip()}",

View file

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

@ -163,8 +163,8 @@ msgid "Creating a custom animation"
msgstr "Skapa en egen animation"
#: ../../source/tutorials/building_blocks.rst:299
msgid "Even though Manim has many built-in animations, you will find times when you need to smoothly animate from one state of a :class:`~.Mobject` to another. If you find yourself in that situation, then you can define your own custom animation. You start by extending the :class:`~.Animation` class and overriding its :meth:`~.Animation.interpolate_mobject`. The :meth:`~.Animation.interpolate_mobject` method receives alpha as a parameter that starts at 0 and changes throughout the animation. So, you just have to manipulate self.mobject inside Animation according to the alpha value in its interpolate_mobject method. Then you get all the benefits of :class:`~.Animation` such as playing it for different run times or using different rate functions."
msgstr "Även om Manim har många inbyggda animationer, kommer det vara stunder när du behöver animera smidigt från ett tillstånd av ett :class:`~.Mobject` till ett annat. Om du befinner dig i den situationen kan du definiera en egen animation. Du börjar med att utöka :class:`~.Animation`-klassen och åsidosätter dess :meth:`~.Animation.interpolate_mobject`. :meth:`~.Animation.interpolate_mobject` -metoden får alfa som en parameter som startar vid 0 och ändras under hela animationen. Så du behöver bara manipulera self.mobject inuti Animation enligt alfa värdet i dess interpolate_mobject-metod. Då får du alla fördelar med :class:`~.Animation` såsom att spela det under olika körtider eller använda olika hastighetsfunktioner."
msgid "Even though Manim has many built-in animations, you will find times when you need to smoothly animate from one state of a :class:`~.Mobject` to another. If you find yourself in that situation, then you can define your own custom animation. You start by extending the :class:`~.Animation` class and overriding its :meth:`~.Animation.interpolate`. The :meth:`~.Animation.interpolate` method receives alpha as a parameter that starts at 0 and changes throughout the animation. So, you just have to manipulate self.mobject inside Animation according to the alpha value in its interpolate method. Then you get all the benefits of :class:`~.Animation` such as playing it for different run times or using different rate functions."
msgstr "Även om Manim har många inbyggda animationer, kommer det vara stunder när du behöver animera smidigt från ett tillstånd av ett :class:`~.Mobject` till ett annat. Om du befinner dig i den situationen kan du definiera en egen animation. Du börjar med att utöka :class:`~.Animation`-klassen och åsidosätter dess :meth:`~.Animation.interpolate`. :meth:`~.Animation.interpolate` -metoden får alfa som en parameter som startar vid 0 och ändras under hela animationen. Så du behöver bara manipulera self.mobject inuti Animation enligt alfa värdet i dess interpolate-metod. Då får du alla fördelar med :class:`~.Animation` såsom att spela det under olika körtider eller använda olika hastighetsfunktioner."
#: ../../source/tutorials/building_blocks.rst:306
msgid "Let's say you start with a number and want to create a :class:`~.Transform` animation that transforms it to a target number. You can do it using :class:`~.FadeTransform`, which will fade out the starting number and fade in the target number. But when we think about transforming a number from one to another, an intuitive way of doing it is by incrementing or decrementing it smoothly. Manim has a feature that allows you to customize this behavior by defining your own custom animation."
@ -175,12 +175,12 @@ msgid "You can start by creating your own ``Count`` class that extends :class:`~
msgstr "Du kan börja med att skapa din egen ``Count`` klass som utökar klassen :class:`~.Animation`. Klassen kan ha en konstruktor med tre argument, ett :class:`~.DecimalNumber` Mobject, start and end. Konstruktorn kommer att skicka :class:`~.DecimalNumber` Mobject till superkonstruktorn (i detta fall konstruktorn i :class:`~.Animation`) och kommer att ställa in start och slut."
#: ../../source/tutorials/building_blocks.rst:315
msgid "The only thing that you need to do is to define how you want it to look at every step of the animation. Manim provides you with the alpha value in the :meth:`~.Animation.interpolate_mobject` method based on frame rate of video, rate function, and run time of animation played. The alpha parameter holds a value between 0 and 1 representing the step of the currently playing animation. For example, 0 means the beginning of the animation, 0.5 means halfway through the animation, and 1 means the end of the animation."
msgstr "Det enda du behöver göra är att definiera hur du vill att det ska se ut för varje steg i animationen. Manim ger dig alfavärdet i :meth:`~. nimation.interpolate_mobject' metoden baserat på bildhastigheten av videon, hastighetsfunktionen och körtiden för den animation som spelas. Alfaparametern har ett värde mellan 0 och 1 som representerar steget i den aktuella animationen. Till exempel betyder 0 början av animationen, 0,5 betyder halvvägs genom animationen och 1 betyder slutet av animationen."
msgid "The only thing that you need to do is to define how you want it to look at every step of the animation. Manim provides you with the alpha value in the :meth:`~.Animation.interpolate` method based on frame rate of video, rate function, and run time of animation played. The alpha parameter holds a value between 0 and 1 representing the step of the currently playing animation. For example, 0 means the beginning of the animation, 0.5 means halfway through the animation, and 1 means the end of the animation."
msgstr "Det enda du behöver göra är att definiera hur du vill att det ska se ut för varje steg i animationen. Manim ger dig alfavärdet i :meth:`~. nimation.interpolate' metoden baserat på bildhastigheten av videon, hastighetsfunktionen och körtiden för den animation som spelas. Alfaparametern har ett värde mellan 0 och 1 som representerar steget i den aktuella animationen. Till exempel betyder 0 början av animationen, 0,5 betyder halvvägs genom animationen och 1 betyder slutet av animationen."
#: ../../source/tutorials/building_blocks.rst:320
msgid "In the case of the ``Count`` animation, you just have to figure out a way to determine the number to display at the given alpha value and then set that value in the :meth:`~.Animation.interpolate_mobject` method of the ``Count`` animation. Suppose you are starting at 50 and incrementing until the :class:`~.DecimalNumber` reaches 100 at the end of the animation."
msgstr "I fallet med ``Count`` animationen, behöver du bara hitta ett sätt att bestämma talet som ska visas för det givna alfavärdet och sedan ange det värdet i :meth:`~.Animation.interpolate_mobject` -metoden för ``Count``-animationen. Antag att du börjar med 50 och inkrementerar värdet tills :class:`~.DecimalNumber` når 100 i slutet av animationen."
msgid "In the case of the ``Count`` animation, you just have to figure out a way to determine the number to display at the given alpha value and then set that value in the :meth:`~.Animation.interpolate` method of the ``Count`` animation. Suppose you are starting at 50 and incrementing until the :class:`~.DecimalNumber` reaches 100 at the end of the animation."
msgstr "I fallet med ``Count`` animationen, behöver du bara hitta ett sätt att bestämma talet som ska visas för det givna alfavärdet och sedan ange det värdet i :meth:`~.Animation.interpolate` -metoden för ``Count``-animationen. Antag att du börjar med 50 och inkrementerar värdet tills :class:`~.DecimalNumber` når 100 i slutet av animationen."
#: ../../source/tutorials/building_blocks.rst:323
msgid "If alpha is 0, you want the value to be 50."

View file

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

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.

View file

@ -24,8 +24,8 @@ to the bottom of the file:
.. code-block:: python
with tempconfig({"quality": "medium_quality", "disable_caching": True}):
scene = SceneName()
scene.render()
manager = Manager(SceneName)
manager.render()
Where ``SceneName`` is the name of the scene you want to run. You can then run the
file directly, and can thus follow the instructions for most profilers.
@ -58,8 +58,8 @@ to ``square_to_circle.py``:
with tempconfig({"quality": "medium_quality", "disable_caching": True}):
scene = SquareToCircle()
scene.render()
manager = Manager(SquareToCircle)
manager.render()
Now run the following in the terminal:

View file

@ -1,11 +1,11 @@
A deep dive into Manim's internals
==================================
**Author:** `Benjamin Hackl <https://benjamin-hackl.at>`__
**Authors:** `Benjamin Hackl <https://benjamin-hackl.at>`__ and `Aarush Deshpande <https://github.com/JasonGrace2282>`__
.. admonition:: Disclaimer
This guide reflects the state of the library as of version ``v0.16.0``
This guide reflects the state of the library as of version ``v0.20.0``
and primarily treats the Cairo renderer. The situation in the latest
version of Manim might be different; in case of substantial deviations
we will add a note below.
@ -84,7 +84,7 @@ discussing the contents of the following chapters on a very high level.
to prepare a scene for rendering; right until the point where the user-overridden
``construct`` method is ran. This includes a brief discussion on using Manim's CLI
versus other means of rendering (e.g., via Jupyter notebooks, or in your Python
script by calling the :meth:`.Scene.render` method yourself).
script by calling the :meth:`.Manager.render` method yourself).
- `Mobject Initialization`_: For the second chapter we dive into creating and handling
Mobjects, the basic elements that should be displayed in our scene.
We discuss the :class:`.Mobject` base class, how there are essentially
@ -107,6 +107,25 @@ discussing the contents of the following chapters on a very high level.
:meth:`.Scene.construct` has been run, the library combines the partial movie
files to one video.
.. hint::
As we move forward, try to keep in mind the responsibilities of every
class we introduce. We'll talk more about them in detail, but here's a brief
overview
* :class:`.Scene` is responsible for managing the classes :class:`Mobject`, :class:`.Animation`,
and :class:`.Camera`.
* :class:`.Manager` is responsible for coordinating the :class:`.Scene`, :class:`.Renderer`,
and :class:`.FileWriter`.
* :class:`.FileWriter` is responsible for writing frames and partial movie files, as well
as combining them all into a final movie file.
* :class:`.Renderer` is an abstract class which has to be subclassed.
It's job is to take information related to the :class:`.Camera`, and the mobjects
on the :class:`.Scene` at a certain frame, and to return the pixels in a frame.
And with that, let us get *in medias res*.
Preliminaries
@ -123,8 +142,8 @@ like
::
with tempconfig({"quality": "medium_quality", "preview": True}):
scene = ToyExample()
scene.render()
manager = Manager(ToyExample)
manager.render()
or whether you are rendering the code in a Jupyter notebook, you are still telling your
python interpreter to import the library. The usual pattern used to do this is
@ -202,8 +221,8 @@ have created a file ``toy_example.py`` which looks like this::
self.play(FadeOut(blue_circle, small_dot))
with tempconfig({"quality": "medium_quality", "preview": True}):
scene = ToyExample()
scene.render()
manager = Manager(ToyExample)
manager.render()
With such a file, the desired scene is rendered by simply running this Python
script via ``python toy_example.py``. Then, as described above, the library
@ -218,10 +237,10 @@ dictionary, and upon leaving the context the original version of the
configuration is restored. TL;DR: it provides a fancy way of temporarily setting
configuration options.
Inside the context manager, two things happen: an actual ``ToyExample``-scene
object is instantiated, and the ``render`` method is called. Every way of using
Inside the context manager, two things happen: a :class:`.Manager` is created for
the ``ToyExample``-scene, and the ``render`` method is called. Every way of using
Manim ultimately does something along of these lines, the library always instantiates
the scene object and then calls its ``render`` method. To illustrate that this
the manager of the scene object and then calls its ``render`` method. To illustrate that this
really is the case, let us briefly look at the two most common ways of rendering
scenes:
@ -243,54 +262,75 @@ and the code creating the scene class and calling its render method is located
`here <https://github.com/ManimCommunity/manim/blob/ac1ee9a683ce8b92233407351c681f7d71a4f2db/manim/utils/ipython_magic.py#L137-L138>`__.
Now that we know that either way, a :class:`.Scene` object is created, let us investigate
what Manim does when that happens. When instantiating our scene object
Now that we know that either way, a :class:`.Manager` for a :class:`.Scene` object is created, let us investigate
what Manim does when that happens. When instantiating our manager
::
scene = ToyExample()
manager = Manager(ToyExample)
the ``Scene.__init__`` method is called, given that we did not implement our own initialization
method. Inspecting the corresponding code (see
`here <https://github.com/ManimCommunity/manim/blob/main/manim/scene/scene.py>`__)
reveals that ``Scene.__init__`` first sets several attributes of the scene objects that do not
depend on any configuration options set in ``config``. Then the scene inspects the value of
``config.renderer``, and based on its value, either instantiates a ``CairoRenderer`` or an
``OpenGLRenderer`` object and assigns it to its ``renderer`` attribute.
The :meth:`.Manager.__init__` method is called. Looking at the source code (`here <https://github.com/ManimCommunity/manim/blob/experimental/manim/manager.py>`__),
we see that the :meth:`.Scene.__init__` method is called,
given that we did not implement our own initialization
method. Inspecting the corresponding code (see `here <https://github.com/ManimCommunity/manim/blob/main/manim/scene/scene.py>`__)
reveals that :class:`Scene.__init__` first sets several attributes of the scene objects that do not
depend on any configuration options set in ``config``. It then initializes it's :class:`.Camera`.
The purpose of a :class:`.Camera` is to keep track of what you can see in the scene. Think of it
as a pair of eyes, that limit how far you can look sideways and vertically.
The scene then asks its renderer to initialize the scene by calling
The :class:`.Scene` also sets up :attr:`.Scene.mobjects`. This attribute keeps track of all the :class:`.Mobject`
that have been added to the scene.
::
The :class:`.Manager` then continues on to create a :class:`.Window`, which is the popopen interactive window,
and creates the renderer::
self.renderer.init_scene(self)
self.renderer = self.create_renderer()
self.renderer.use_window()
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 ``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.
If you hover over :attr:`.Manager.renderer`, you might see that the type is a :class:`.RendererProtocol`.
A :class:`~typing.Protocol` is a contract for a class. It says that whatever the class is, it will implement
the methods defined inside the protocol. In this case, it means that the renderer will have all the methods
defined in :class:`.RendererProtocol`.
.. warning::
.. note::
Currently, there is a lot of interplay between a scene and its renderer. This is a flaw
in Manim's current architecture, and we are working on reducing this interdependency to
achieve a less convoluted code flow.
The point of using :class:`~typing.Protocol` is so that in the future, plugins
can swap out the renderer with their own version - either for speed, or for a different
behavior.
After the renderer has been instantiated and initialized its file writer, the scene populates
further initial attributes (notable mention: the ``mobjects`` attribute which keeps track
of the mobjects that have been added to the scene). It is then done with its instantiation
and ready to be rendered.
For the rest of this article to take a concrete example, we'll use :class:`.OpenGLRenderer`.
Finally, the :class:`.Manager` creates a :class:`.FileWriter`. This is the object that actually
writes the partial movie files.
The rest of this article is concerned with the last line in our toy example script::
scene.render()
manager.render()
This is where the actual magic happens.
.. note::
TODO TO REVIEWERS - Replace this link with the proper permanent link
Inspecting the `implementation of the render method <https://github.com/ManimCommunity/manim/blob/df1a60421ea1119cbbbd143ef288d294851baaac/manim/scene/scene.py#L211>`__
reveals that there are several hooks that can be used for pre- or postprocessing
a scene. Unsurprisingly, :meth:`.Scene.render` describes the full *render cycle*
we see that there are two passes of rendering.
.. note::
As of the experimental branch at June 30th, 2024, two pass rendering
does not exist. This will proceed to explain the single pass rendering system.
Looking around, we find that there are several hooks that can be used for pre- or postprocessing
a scene (check out :meth:`.Manager._setup`, and :meth:`.Manager._tear_down`).
.. note::
You might notice :attr:`.Manager.virtual_animation_start_time` and :attr:`.Manager.real_animation_start_time`
when looking through :meth:`.Manager._setup`. These will be explained later.
Unsurprisingly, :meth:`.Manager.render` describes the full *render cycle*
of a scene. During this life cycle, there are three custom methods whose base
implementation is empty and that can be overwritten to suit your purposes. In
the order they are called, these customizable methods are:
@ -308,14 +348,14 @@ the order they are called, these customizable methods are:
Python scripts).
After these three methods are run, the animations have been fully rendered,
and Manim calls :meth:`.CairoRenderer.scene_finished` to gracefully
and Manim calls :meth:`.Manager.tear_down` 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 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.
**Back in our toy example,** the call to :meth:`.Scene.render` first
**Back in our toy example,** the call to :meth:`.Manager.render` first
triggers :meth:`.Scene.setup` (which only consists of ``pass``), followed by
a call of :meth:`.Scene.construct`. At this point, our *animation script*
is run, starting with the initialization of ``orange_square``.
@ -348,16 +388,12 @@ of :class:`.Mobject`, you will find that not too much happens in there:
- and finally, ``init_colors`` is called.
Digging deeper, you will find that :meth:`.Mobject.reset_points` simply
sets the ``points`` attribute of the mobject to an empty NumPy vector,
sets the ``points`` attribute of the mobject to an empty NumPy array,
while the other two methods, :meth:`.Mobject.generate_points` and
:meth:`.Mobject.init_colors` are just implemented as ``pass``.
This makes sense: :class:`.Mobject` is not supposed to be used as
an *actual* object that is displayed on screen; in fact the camera
(which we will discuss later in more detail; it is the class that is,
for the Cairo renderer, responsible for "taking a picture" of the
current scene) does not process "pure" :class:`Mobjects <.Mobject>`
in any way, they *cannot* even appear in the rendered output.
an *actual* object that is displayed on screen.
This is where different types of mobjects come into play. Roughly
speaking, the Cairo renderer setup knows three different types of
@ -376,24 +412,24 @@ mobjects that can be rendered:
As just mentioned, :class:`VMobjects <.VMobject>` represent vectorized
mobjects. To render a :class:`.VMobject`, the camera looks at the
``points`` attribute of a :class:`.VMobject` and divides it into sets
of four points each. Each of these sets is then used to construct a
cubic Bézier curve with the first and last entry describing the
end points of the curve ("anchors"), and the second and third entry
describing the control points in between ("handles").
:attr:`~.VMobject.points` attribute of a :class:`.VMobject` and divides it into sets
of three points each. Each of these sets is then used to construct a
quadratic Bézier curve with the first and last entry describing the
end points of the curve ("anchors"), and the second entry
describing the control points in between ("handle").
.. hint::
To learn more about Bézier curves, take a look at the excellent
online textbook `A Primer on Bézier curves <https://pomax.github.io/bezierinfo/>`__
by `Pomax <https://twitter.com/TheRealPomax>`__ -- there is a playground representing
cubic Bézier curves `in §1 <https://pomax.github.io/bezierinfo/#introduction>`__,
quadratic Bézier curves `in §1 <https://pomax.github.io/bezierinfo/#introduction>`__,
the red and yellow points are "anchors", and the green and blue
points are "handles".
In contrast to :class:`.Mobject`, :class:`.VMobject` can be displayed
on screen (even though, technically, it is still considered a base class).
To illustrate how points are processed, consider the following short example
of a :class:`.VMobject` with 8 points (and thus made out of 8/4 = 2 cubic
of a :class:`.VMobject` with 6 points (and thus made out of 6/3 = 2 cubic
Bézier curves). The resulting :class:`.VMobject` is drawn in green.
The handles are drawn as red dots with a line to their closest anchor.
@ -430,6 +466,7 @@ The handles are drawn as red dots with a line to their closest anchor.
.. warning::
Manually setting the points of your :class:`.VMobject` is usually
discouraged; there are specialized methods that can take care of
that for you -- but it might be relevant when implementing your own,
@ -561,59 +598,12 @@ is not a "flat" list of mobjects, but a list of mobjects which
might contain mobjects themselves, and so on.
Stepping through the code in :meth:`.Scene.add`, we see that first
it is checked whether we are currently using the OpenGL renderer
(which we are not) -- adding mobjects to the scene works slightly
different (and actually easier!) for the OpenGL renderer. Then, the
code branch for the Cairo renderer is entered and the list of so-called
foreground mobjects (which are rendered on top of all other mobjects)
is added to the list of passed mobjects. This is to ensure that the
foreground mobjects will stay above of the other mobjects, even after
adding the new ones. In our case, the list of foreground mobjects
is actually empty, and nothing changes.
we remove all the mobjects that are being added -- this is to make
sure we don't add a :class:`.Mobject` twice! After that, we can safely
add it to :attr:`.Scene.mobjects`.
Next, :meth:`.Scene.restructure_mobjects` is called with the list
of mobjects to be added as the ``to_remove`` argument, which might
sound odd at first. Practically, this ensures that mobjects are not
added twice, as mentioned above: if they were present in the scene
``Scene.mobjects`` list before (even if they were contained as a
child of some other mobject), they are first removed from the list.
The way :meth:`.Scene.restrucutre_mobjects` works is rather aggressive:
It always operates on a given list of mobjects; in the ``add`` method
two different lists occur: the default one, ``Scene.mobjects`` (no extra
keyword argument is passed), and ``Scene.moving_mobjects`` (which we will
discuss later in more detail). It iterates through all of the members of
the list, and checks whether any of the mobjects passed in ``to_remove``
are contained as children (in any nesting level). If so, **their parent
mobject is deconstructed** and their siblings are inserted directly
one level higher. Consider the following example::
>>> from manim import Scene, Square, Circle, Group
>>> test_scene = Scene()
>>> mob1 = Square()
>>> mob2 = Circle()
>>> mob_group = Group(mob1, mob2)
>>> test_scene.add(mob_group)
<manim.scene.scene.Scene object at ...>
>>> test_scene.mobjects
[Group]
>>> test_scene.restructure_mobjects(to_remove=[mob1])
<manim.scene.scene.Scene object at ...>
>>> test_scene.mobjects
[Circle]
Note that the group is disbanded and the circle moves into the
root layer of mobjects in ``test_scene.mobjects``.
After the mobject list is "restructured", the mobject to be added
are simply appended to ``Scene.mobjects``. In our toy example,
the ``Scene.mobjects`` list is actually empty, so the
``restructure_mobjects`` method does not actually do anything. The
``orange_square`` is simply added to ``Scene.mobjects``, and as
the aforementioned ``Scene.moving_mobjects`` list is, at this point,
also still empty, nothing happens and :meth:`.Scene.add` returns.
We will hear more about the ``moving_mobject`` list when we discuss
the render loop. Before we do that, let us look at the next line
We will hear more from :class:`.Scene` soon.
Before we do that, let us look at the next line
of code in our toy example, which includes the initialization of
an animation class,
::
@ -642,11 +632,11 @@ the run time of animations is also fixed and known beforehand.
The initialization of animations actually is not very exciting,
:meth:`.Animation.__init__` merely sets some attributes derived
from the passed keyword arguments and additionally ensures that
the ``Animation.starting_mobject`` and ``Animation.mobject``
the :attr:`~Animation.starting_mobject` and :attr:`~.Animation.mobject`
attributes are populated. Once the animation is played, the
``starting_mobject`` attribute holds an unmodified copy of the
:attr:`~.Animation.starting_mobject` attribute holds an unmodified copy of the
mobject the animation is attached to; during the initialization
it is set to a placeholder mobject. The ``mobject`` attribute
it is set to a placeholder mobject. The :attr:`~.Animation.mobject` attribute
is set to the mobject the animation is attached to.
Animations have a few special methods which are called during the
@ -681,77 +671,80 @@ animation (like its ``run_time``, the ``rate_func``, etc.) are
processed there -- and then the animation object is fully
initialized and ready to be played.
The Animation Buffer
^^^^^^^^^^^^^^^^^^^^
There's an attribute of animations that we have glossed
over, and that is :attr:`.Animation.buffer`, of type :class:`.SceneBuffer`.
The :attr:`~.Animation.buffer` is the animations way of communicating
with what happens on the scene. If you want to modify
the scene during the interpolation stage (outside of :meth:`~.Animation.begin` or :meth:`~.Animation.finish`),
the attribute :attr:`.Animation.apply_buffer` is what tells the scene that the buffer
should be processed.
For example, an animation that adds a circle to the scene every frame might look like this
.. code-block:: python
class CircleAnimation(Animation):
def begin(self) -> None:
self.circles = VGroup()
def interpolate(self, alpha: float) -> None:
# create and arrange the circles
self.circles.add(Circle())
self.circles().arrange()
# add the new circle to the scene
self.buffer.add(self.circles[-1])
# make sure the scene actually realizes something changed
self.apply_buffer = True
Every time the :class:`.Scene` applies the buffer, it gets emptied out
for use the next time.
The ``play`` call: preparing to enter Manim's render loop
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
We are finally there, the render loop is in our reach. Let us
walk through the code that is run when :meth:`.Scene.play` is called.
.. hint::
.. note::
Recall that this article is specifically about the Cairo renderer.
Up to here, things were more or less the same for the OpenGL renderer
as well; while some base mobjects might be different, the control flow
and lifecycle of mobjects is still more or less the same. There are more
substantial differences when it comes to the rendering loop.
In the future, control will not be passed to the Manager.
Instead, the Scene will keep track of every animation and
at the very end, the Manager will render everything.
As you will see when inspecting the method, :meth:`.Scene.play` almost
immediately passes over to the ``play`` method of the renderer,
in our case :class:`.CairoRenderer.play`. The one thing :meth:`.Scene.play`
takes care of is the management of subcaptions that you might have
passed to it (see the the documentation of :meth:`.Scene.play` and
:meth:`.Scene.add_subcaption` for more information).
immediately passes over to the :class:`~.Manager._play` method of the :class:`.Manager`.
The one thing :meth:`.Scene.play` does before that is preparing the animations.
Whenever :attr:`.Mobject.animate` is called, it creates a new object called a
:class:`._AnimationBuilder`. We have to make sure to convert that into an actual
animation by calling it's :meth:`._AnimationBuilder.build` method.
We also have to update the animations with the correct rate functions, lag ratios,
and run time.
.. note::
Methods in :class:`.Manager` starting with an underscore ``_`` are intended to be
private, and are not guaranteed to be stable across versions of Manim. The :class:`.Manager`
class provides some "public" methods (methods not prefixed with ``_``) that can be overridden to
change the behavior of the program.
.. warning::
As has been said before, the communication between scene and renderer
is not in a very clean state at this point, so the following paragraphs
might be confusing if you don't run a debugger and step through the
code yourself a bit.
Inside :meth:`.CairoRenderer.play`, the renderer first checks whether
it may skip rendering of the current play call. This might happen, for example,
when ``-s`` is passed to the CLI (i.e., only the last frame should be rendered),
or when the ``-n`` flag is passed and the current play call is outside of the
specified render bounds. The "skipping status" is updated in form of the
call to :meth:`.CairoRenderer.update_skipping_status`.
Next, the renderer asks the scene to process the animations in the play
call so that renderer obtains all of the information it needs. To
be more concrete, :meth:`.Scene.compile_animation_data` is called,
which then takes care of several things:
- The method processes all animations and the keyword arguments passed
to the initial :meth:`.Scene.play` call. In particular, this means
that it makes sure all arguments passed to the play call are actually
animations (or ``.animate`` syntax calls, which are also assembled to
be actual :class:`.Animation`-objects at that point). It also propagates
any animation-related keyword arguments (like ``run_time``,
or ``rate_func``) passed to :class:`.Scene.play` to each individual
animation. The processed animations are then stored in the ``animations``
attribute of the scene (which the renderer later reads...).
- It adds all mobjects to which the animations that are played are
bound to to the scene (provided the animation is not an mobject-introducing
animation -- for these, the addition to the scene happens later).
- In case the played animation is a :class:`.Wait` animation (this is the
case in a :meth:`.Scene.wait` call), the method checks whether a static
image should be rendered, or whether the render loop should be processed
as usual (see :meth:`.Scene.should_update_mobjects` for the exact conditions,
basically it checks whether there are any time-dependent updater functions
and so on).
- Finally, the method determines the total run time of the play call (which
at this point is computed as the maximum of the run times of the passed
animations). This is stored in the ``duration`` attribute of the scene.
Subcaptions and audio is still in progress
After the animation data has been compiled by the scene, the renderer
continues to prepare for entering the render loop. It now checks the
skipping status which has been determined before. If the renderer can
skip this play call, it does so: it sets the current play call hash (which
we will get back to in a moment) to ``None`` and increases the time of the
renderer by the determined animation run time.
After the :class:`.Scene` has done all the processing of animations,
it hands out control to the :class:`.Manager`. The :class:`.Manager`
then updates the skipping status of the :class:`.Scene`. This makes sure
that if ``-s`` or ``-n`` is used for sections, the scene does the correct
thing.
Otherwise, the renderer checks whether or not Manim's caching system should
The next important line is::
self._write_hashed_movie_file()
Here, the :class:`.Manager` checks whether or not Manim's caching system should
be used. The idea of the caching system is simple: for every play call, a
hash value is computed, which is then stored and upon re-rendering the scene,
the hash is generated again and checked against the stored value. If it is the
@ -761,40 +754,28 @@ to learn more, the :func:`.get_hash_from_play_call` function in the
:mod:`.utils.hashing` module is essentially the entry point to the caching
mechanism.
In the event that the animation has to be rendered, the renderer asks
its :class:`.SceneFileWriter` to open an output container. The process
In the event that the animation has to be rendered, the manager asks
its :class:`.FileWriter` 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.
First, it literally *begins* all of the animations by calling their
setup methods (:meth:`.Animation._setup_scene`, :meth:`.Animation.begin`).
setup methods (:meth:`.Animation.begin`).
In doing so, the mobjects that are newly introduced by an animation
(like via :class:`.Create` etc.) are added to the scene. Furthermore, the
animation suspends updater functions being called on its mobject, and
it sets its mobject to the state that corresponds to the first frame
of the animation.
After this has happened for all animations in the current ``play`` call,
the Cairo renderer determines which of the scene's mobjects can be
painted statically to the background, and which ones have to be
redrawn every frame. It does so by calling
:meth:`.Scene.get_moving_and_static_mobjects`, and the resulting
partition of mobjects is stored in the corresponding ``moving_mobjects``
and ``static_mobjects`` attributes.
.. note::
.. NOTE::
Implementation of figuring out which mobjects have to be redrawn
is still in progress.
The mechanism that determines static and moving mobjects is
specific for the Cairo renderer, the OpenGL renderer works differently.
Basically, moving mobjects are determined by checking whether they,
any of their children, or any of the mobjects "below" them (in the
sense of the order in which mobjects are processed in the scene)
either have an update function attached, or whether they appear
in one of the current animations. See the implementation of
:meth:`.Scene.get_moving_mobjects` for more details.
Up to this very point, we did not actually render any (partial)
image or movie files from the scene yet. This is, however, about to change.
@ -835,68 +816,28 @@ Time to render some frames.
The render loop (for real this time)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Now we get to the meat of rendering, which happens in :meth:`.Manager._progress_through_animations`.
As mentioned above, due to the mechanism that determines static and moving
mobjects in the scene, the renderer knows which mobjects it can paint
statically to the background of the scene. Practically, this means that
it partially renders a scene (to produce a background image), and then
when iterating through the time progression of the animation only the
"moving mobjects" are re-painted on top of the static background.
The renderer calls :meth:`.CairoRenderer.save_static_frame_data`, which
first checks whether there are currently any static mobjects, and if there
are, it updates the frame (only with the static mobjects; more about how
exactly this works in a moment) and then saves a NumPy array representing
the rendered frame in the ``static_image`` attribute. In our toy example,
there are no static mobjects, and so the ``static_image`` attribute is
simply set to ``None``.
Next, the renderer asks the scene whether the current animation is
a "frozen frame" animation, which would mean that the renderer actually
does not have to repaint the moving mobjects in every frame of the time
progression. It can then just take the latest static frame, and display it
throughout the animation.
.. NOTE::
An animation is considered a "frozen frame" animation if only a
static :class:`.Wait` animation is played. See the description
of :meth:`.Scene.compile_animation_data` above, or the
implementation of :meth:`.Scene.should_update_mobjects` for
more details.
If this is not the case (just as in our toy example), the renderer
then calls the :meth:`.Scene.play_internal` method, which is the
integral part of the render loop (in which the library steps through
the time progression of the animation and renders the corresponding
frames).
Within :meth:`.Scene.play_internal`, the following steps are performed:
- The scene determines the run time of the animations by calling
:meth:`.Scene.get_run_time`. This method basically takes the maximum
- The manager determines the run time of the animations by calling
:meth:`.Manager._calc_run_time`. This method basically takes the maximum
``run_time`` attribute of all of the animations passed to the
:meth:`.Scene.play` call.
- Then the *time progression* is constructed via the (internal)
:meth:`.Scene._get_animation_time_progression` method, which wraps
the actual :meth:`.Scene.get_time_progression` method. The time
progression is a ``tqdm`` `progress bar object <https://tqdm.github.io>`__
for an iterator over ``np.arange(0, run_time, 1 / config.frame_rate)``. In
- Then, the progressbar is created by :meth:`.Manager._create_progressbar`,
which returns a ``tqdm`` `progress bar object <https://tqdm.github.io>`__
object (from the ``tqdm`` library), or a fake progressbar if
:attr:`.ManimConfig.write_to_movie` is ``False``.
- Then the *time progression* is constructed via
:meth:`.Manager._calc_time_progression` method, which returns
``np.arange(0, run_time, 1 / config.frame_rate)``. In
other words, the time progression holds the time stamps (relative to the
current animations, so starting at 0 and ending at the total animation run time,
with the step size determined by the render frame rate) of the timeline where
a new animation frame should be rendered.
- Then the scene iterates over the time progression: for each time stamp ``t``,
:meth:`.Scene.update_to_time` is called, which ...
- ... first computes the time passed since the last update (which might be 0,
especially for the initial call) and references it as ``dt``,
- then (in the order in which the animations are passed to :meth:`.Scene.play`)
calls :meth:`.Animation.update_mobjects` to trigger all updater functions that
are attached to the respective animation except for the "main mobject" of
the animation (that is, for example, for :class:`.Transform` the unmodified
copies of start and target mobject -- see :meth:`.Animation.get_all_mobjects_to_update`
for more details),
we find the time difference between the current and previous frame (AKA ``dt``).
We then update the animations in the scene using ``dt`` by
- iterating over each animation
- next, we update the animations mobjects
- then the relative time progression with respect to the current animation
is computed (``alpha = t / animation.run_time``), which is then used to
update the state of the animation with a call to :meth:`.Animation.interpolate`.
@ -904,62 +845,29 @@ Within :meth:`.Scene.play_internal`, the following steps are performed:
of all mobjects in the scene, all meshes, and finally those attached to
the scene itself are run.
After updating the animations, we pass ``dt`` to :meth:`.Manager._update_frame` which...
- ... updates the total time passed
- Updates all the mobjects by calling :meth:`.Scene._update_mobjects`. This in turn
iterates over all the mobjects on the screen and updates them.
- After that, the current state of the scene is computed by :meth:`.Scene.get_state`,
which returns a :class:`.SceneState`.
- The state is then passed into :meth:`.Manager._render_frame`, which gets
the renderer to create the pixels. With :class:`.OpenGLRenderer`, this
also updates the window. :meth:`~.Manager._render_frame` also checks if it should write a frame,
and if so, writes a frame via the :class:`.FileWriter`.
- Finally, it uses a concept of virtual time vs real time to see
if the right amount of time has passed in the window. The virtual
time is the amount of time that is supposed to have passed (that is, ``t``).
The real time is how much time has actually passed in the window
(current time - start time of play). If the animations are progressing
faster than they would in real life, it will slow down the window by calling
:meth:`~.Manager._update_frame` with ``dt=0`` until that's no longer the case.
This is to make sure that animations never go too fast: it doesn't do anything if
animations are too slow!
At this point, the internal (Python) state of all mobjects has been updated
to match the currently processed timestamp. If rendering should not be skipped,
then it is now time to *take a picture*!
.. NOTE::
The update of the internal state (iteration over the time progression) happens
*always* once :meth:`.Scene.play_internal` is entered. This ensures that even
if frames do not need to be rendered (because, e.g., the ``-n`` CLI flag has
been passed, something has been cached, or because we might be in a *Section*
with skipped rendering), updater functions still run correctly, and the state
of the first frame that *is* rendered is kept consistent.
To render an image, the scene calls the corresponding method of its renderer,
:meth:`.CairoRenderer.render` and passes just the list of *moving mobjects* (remember,
the *static mobjects* are assumed to have already been painted statically to
the background of the scene). All of the hard work then happens when the renderer
updates its current frame via a call to :meth:`.CairoRenderer.update_frame`:
First, the renderer prepares its :class:`.Camera` by checking whether the renderer
has a ``static_image`` different from ``None`` stored already. If so, it sets the
image as the *background image* of the camera via :meth:`.Camera.set_frame_to_background`,
and otherwise it just resets the camera via :meth:`.Camera.reset`. The camera is then
asked to capture the scene with a call to :meth:`.Camera.capture_mobjects`.
Things get a bit technical here, and at some point it is more efficient to
delve into the implementation -- but here is a summary of what happens once the
camera is asked to capture the scene:
- First, a flat list of mobjects is created (so submobjects get extracted from
their parents). This list is then processed in groups of the same type of
mobjects (e.g., a batch of vectorized mobjects, followed by a batch of image mobjects,
followed by more vectorized mobjects, etc. -- in many cases there will just be
one batch of vectorized mobjects).
- Depending on the type of the currently processed batch, the camera uses dedicated
*display functions* to convert the :class:`.Mobject` Python object to
a NumPy array stored in the camera's ``pixel_array`` attribute.
The most important example in that context is the display function for
vectorized mobjects, :meth:`.Camera.display_multiple_vectorized_mobjects`,
or the more particular (in case you did not add a background image to your
:class:`.VMobject`), :meth:`.Camera.display_multiple_non_background_colored_vmobjects`.
This method first gets the current Cairo context, and then, for every (vectorized)
mobject in the batch, calls :meth:`.Camera.display_vectorized`. There,
the actual background stroke, fill, and then stroke of the mobject is
drawn onto the context. See :meth:`.Camera.apply_stroke` and
:meth:`.Camera.set_cairo_context_color` for more details -- but it does not get
much deeper than that, in the latter method the actual Bézier curves
determined by the points of the mobject are drawn; this is where the low-level
interaction with Cairo happens.
After all batches have been processed, the camera has an image representation
of the Scene at the current time stamp in form of a NumPy array stored in its
``pixel_array`` attribute. The renderer then takes this array and passes it to
its :class:`.SceneFileWriter`. This concludes one iteration of the render loop,
and once the time progression has been processed completely, a final bit
of cleanup is performed before the :meth:`.Scene.play_internal` call is completed.
to match the currently processed timestamp.
A TL;DR for the render loop, in the context of our toy example, reads as follows:
@ -968,23 +876,20 @@ A TL;DR for the render loop, in the context of our toy example, reads as follows
medium render quality, the frame rate is 30 frames per second, and so the time
progression with steps ``[0, 1/30, 2/30, ..., 89/30]`` is created.
- In the internal render loop, each of these time stamps is processed:
there are no updater functions, so effectively the scene updates the
there are no updater functions, so effectively the manager updates the
state of the transformation animation to the desired time stamp (for example,
at time stamp ``t = 45/30``, the animation is completed to a rate of
``alpha = 0.5``).
- Then the scene asks the renderer to do its job. The renderer asks its camera
to capture the scene, the only mobject that needs to be processed at this point
is the main mobject attached to the transformation; the camera converts the
current state of the mobject to entries in a NumPy array. The renderer passes
this array to the file writer.
- Then the manager asks the renderer to do its job. The renderer then produces
the pixels, which are then fed into the :class:`.FileWriter`.
- At the end of the loop, 90 frames have been passed to the file writer.
Completing the render loop
^^^^^^^^^^^^^^^^^^^^^^^^^^
The last few steps in the :meth:`.Scene.play_internal` call are not too
The last few steps in the :meth:`.Manager._play` call are not too
exciting: for every animation, the corresponding :meth:`.Animation.finish`
and :meth:`.Animation.clean_up_from_scene` methods are called.
method is called.
.. NOTE::
@ -999,10 +904,7 @@ and :meth:`.Animation.clean_up_from_scene` methods are called.
would be slightly longer than 1 second. We decided against this at some point.
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 output container that has
been opened for this animation: a partial movie file is written.
in the terminal.
This pretty much concludes the walkthrough of a :class:`.Scene.play` call,
and actually there is not too much more to say for our toy example either: at
@ -1025,5 +927,4 @@ which triggers the combination of the partial movie files into the final product
And there you go! This is a more or less detailed description of how Manim works
under the hood. While we did not discuss every single line of code in detail
in this walkthrough, it should still give you a fairly good idea of how the general
structural design of the library and at least the Cairo rendering flow in particular
looks like.
structural design of the library looks like.

View file

@ -17,46 +17,15 @@ follow `Homebrew's installation instructions <https://docs.brew.sh/Installation>
(and is recommended to) be installed natively.
Required Dependencies
---------------------
Installing Manim
----------------
To install all required dependencies for installing Manim (namely: Python,
and some required Python packages), run:
As of July/2024, brew can install Manim including all required dependencies.
To install Manim:
.. code-block:: bash
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
*About This Mac* and check the entry next to *Chip*), some additional dependencies
are required, namely:
.. code-block:: bash
brew install pango pkg-config scipy
After all required dependencies are installed, simply run:
.. code-block:: bash
pip3 install manim
to install Manim.
.. note::
A frequent source for installation problems is if ``pip3``
does not point to the correct Python installation on your system.
To check this, run ``pip3 -V``: for macOS Intel, the path should
start with ``/usr/local``, and for Apple Silicon with
``/opt/homebrew``. If this is not the case, you either forgot
to modify your shell profile (``.zprofile``) during the installation
of Homebrew, or did not reload your shell (e.g., by opening a new
terminal) after doing so. It is also possible that some other
software (like Pycharm) changed the ``PATH`` variable to fix this,
make sure that the Homebrew-related lines in your ``.zprofile`` are
at the very end of the file.
brew install manim
.. _macos-optional-dependencies:

View file

@ -106,7 +106,7 @@ specified in Poetry as:
[tool.poetry.plugins."manim.plugins"]
"name" = "object_reference"
.. versionremoved:: 0.19.0
.. versionremoved:: 0.18.1
Plugins should be imported explicitly to be usable in user code. The plugin
system will probably be refactored in the future to provide a more structured

View file

@ -297,9 +297,9 @@ Creating a custom animation
Even though Manim has many built-in animations, you will find times when you need to smoothly animate from one state of a :class:`~.Mobject` to another.
If you find yourself in that situation, then you can define your own custom animation.
You start by extending the :class:`~.Animation` class and overriding its :meth:`~.Animation.interpolate_mobject`.
The :meth:`~.Animation.interpolate_mobject` method receives alpha as a parameter that starts at 0 and changes throughout the animation.
So, you just have to manipulate self.mobject inside Animation according to the alpha value in its interpolate_mobject method.
You start by extending the :class:`~.Animation` class and overriding its :meth:`~.Animation.interpolate`.
The :meth:`~.Animation.interpolate` method receives alpha as a parameter that starts at 0 and changes throughout the animation.
So, you just have to manipulate self.mobject inside Animation according to the alpha value in its interpolate method.
Then you get all the benefits of :class:`~.Animation` such as playing it for different run times or using different rate functions.
Let's say you start with a number and want to create a :class:`~.Transform` animation that transforms it to a target number.
@ -312,11 +312,11 @@ The class can have a constructor with three arguments, a :class:`~.DecimalNumber
The constructor will pass the :class:`~.DecimalNumber` Mobject to the super constructor (in this case, the :class:`~.Animation` constructor) and will set start and end.
The only thing that you need to do is to define how you want it to look at every step of the animation.
Manim provides you with the alpha value in the :meth:`~.Animation.interpolate_mobject` method based on frame rate of video, rate function, and run time of animation played.
Manim provides you with the alpha value in the :meth:`~.Animation.interpolate` method based on frame rate of video, rate function, and run time of animation played.
The alpha parameter holds a value between 0 and 1 representing the step of the currently playing animation.
For example, 0 means the beginning of the animation, 0.5 means halfway through the animation, and 1 means the end of the animation.
In the case of the ``Count`` animation, you just have to figure out a way to determine the number to display at the given alpha value and then set that value in the :meth:`~.Animation.interpolate_mobject` method of the ``Count`` animation.
In the case of the ``Count`` animation, you just have to figure out a way to determine the number to display at the given alpha value and then set that value in the :meth:`~.Animation.interpolate` method of the ``Count`` animation.
Suppose you are starting at 50 and incrementing until the :class:`~.DecimalNumber` reaches 100 at the end of the animation.
* If alpha is 0, you want the value to be 50.
@ -331,7 +331,7 @@ Once you have defined your ``Count`` animation, you can play it in your :class:`
.. manim:: CountingScene
:ref_classes: Animation DecimalNumber
:ref_methods: Animation.interpolate_mobject Scene.play
:ref_methods: Animation.interpolate Scene.play
class Count(Animation):
def __init__(self, number: DecimalNumber, start: float, end: float, **kwargs) -> None:
@ -341,7 +341,7 @@ Once you have defined your ``Count`` animation, you can play it in your :class:`
self.start = start
self.end = end
def interpolate_mobject(self, alpha: float) -> None:
def interpolate(self, alpha: float) -> None:
# Set value of DecimalNumber according to alpha
value = self.start + (alpha * (self.end - self.start))
self.mobject.set_value(value)

View file

@ -1,7 +1,8 @@
import time
import numpy as np
import pyglet
# import pyglet
from pyglet.gl import Config
from pyglet.window import Window
@ -64,7 +65,7 @@ if __name__ == "__main__":
# exit(0)
renderer.use_window()
clock = pyglet.clock.get_default()
# clock = pyglet.clock.get_default()
def update_circle(dt):
vm.move_to((np.sin(dt) * 4, np.cos(dt) * 4, -1))
@ -98,7 +99,7 @@ if __name__ == "__main__":
@win.event
def on_draw():
dt = clock.update_time()
# dt = clock.update_time()
renderer.render(camera, [vm2, vm3, vm4, clock_mobject, vm])
# update_circle(counter)
@ -135,7 +136,7 @@ if __name__ == "__main__":
if virtual_time >= run_time:
animation.finish()
buffer = str(animation.buffer)
print(f"{buffer = }")
print(f"buffer = {buffer}")
has_finished = True
else:
animation.update_mobjects(dt)

View file

@ -5,7 +5,7 @@ from pyglet.window import Window
import manim.utils.color.manim_colors as col
from manim._config import tempconfig
from manim.camera.camera import OpenGLCameraFrame
from manim.camera.camera import Camera
from manim.constants import OUT, RIGHT, UP
from manim.mobject.geometry.arc import Circle
from manim.mobject.geometry.polygram import Square
@ -47,7 +47,7 @@ if __name__ == "__main__":
# print(vm.fill_color)
# print(vm.stroke_color)
camera = OpenGLCameraFrame()
camera = Camera()
camera.save_state()
renderer.init_camera(camera)

View file

@ -4,10 +4,31 @@ from manim import *
class Test(Scene):
def construct(self) -> None:
s = Square()
self.add(s)
self.play(Rotate(s, PI / 2))
self.play(FadeOut(s))
sq = RegularPolygon(6)
c = Circle()
st = Star(color=YELLOW, fill_color=YELLOW)
self.play(Succession(*[Create(x) for x in VGroup(s, c, st).arrange()]))
st = Star()
VGroup(sq, c, st).arrange()
self.play(
Succession(
Create(sq),
DrawBorderThenFill(c),
Create(st),
)
)
self.play(FadeOut(VGroup(*self.mobjects)))
with tempconfig({"renderer": "opengl", "preview": True, "parallel": False}):
Manager(Test).render()
if __name__ == "__main__":
with tempconfig(
{
"preview": True,
"write_to_movie": False,
"disable_caching": True,
"frame_rate": 60,
"disable_caching_warning": True,
}
):
Manager(Test).render()

View file

@ -10,90 +10,89 @@ __version__ = version(__name__)
# Importing the config module should be the first thing we do, since other
# modules depend on the global config dict for initialization.
from ._config import *
from manim._config import *
# many scripts depend on this -> has to be loaded first
from .utils.commands import *
from manim.utils.commands import *
# isort: on
import numpy as np
from .animation.animation import *
from .animation.changing import *
from .animation.composition import *
from .animation.creation import *
from .animation.fading import *
from .animation.growing import *
from .animation.indication import *
from .animation.movement import *
from .animation.numbers import *
from .animation.rotation import *
from .animation.specialized import *
from .animation.speedmodifier import *
from .animation.transform import *
from .animation.transform_matching_parts import *
from .animation.updaters.mobject_update_utils import *
from .animation.updaters.update import *
from .constants import *
from .mobject.frame import *
from .mobject.geometry.arc import *
from .mobject.geometry.boolean_ops import *
from .mobject.geometry.labeled import *
from .mobject.geometry.line import *
from .mobject.geometry.polygram import *
from .mobject.geometry.shape_matchers import *
from .mobject.geometry.tips import *
from .mobject.graph import *
from .mobject.graphing.coordinate_systems import *
from .mobject.graphing.functions import *
from .mobject.graphing.number_line import *
from .mobject.graphing.probability import *
from .mobject.graphing.scale import *
from .mobject.logo import *
from .mobject.matrix import *
from .mobject.mobject import *
from .mobject.opengl.dot_cloud import *
from .mobject.opengl.opengl_point_cloud_mobject import *
from .mobject.svg.brace import *
from .mobject.svg.svg_mobject import *
from .mobject.table import *
from .mobject.text.code_mobject import *
from .mobject.text.numbers import *
from .mobject.text.tex_mobject import *
from .mobject.text.text_mobject import *
from .mobject.three_d.polyhedra import *
from .mobject.three_d.three_d_utils import *
from .mobject.three_d.three_dimensions import *
from .mobject.types.image_mobject import *
from .mobject.types.point_cloud_mobject import *
from .mobject.types.vectorized_mobject import *
from .mobject.value_tracker import *
from .mobject.vector_field import *
from .renderer.render_manager import *
from .scene.scene import *
from .scene.scene_file_writer import *
from .scene.section import *
from .scene.vector_space_scene import *
from .utils import color, rate_functions, unit
from .utils.bezier import *
from .utils.color import *
from .utils.config_ops import *
from .utils.debug import *
from .utils.file_ops import *
from .utils.images import *
from .utils.iterables import *
from .utils.paths import *
from .utils.rate_functions import *
from .utils.simple_functions import *
from .utils.sounds import *
from .utils.space_ops import *
from .utils.tex import *
from .utils.tex_templates import *
from manim.animation.animation import *
from manim.animation.changing import *
from manim.animation.composition import *
from manim.animation.creation import *
from manim.animation.fading import *
from manim.animation.growing import *
from manim.animation.indication import *
from manim.animation.movement import *
from manim.animation.numbers import *
from manim.animation.rotation import *
from manim.animation.specialized import *
from manim.animation.speedmodifier import *
from manim.animation.transform import *
from manim.animation.transform_matching_parts import *
from manim.animation.updaters.mobject_update_utils import *
from manim.animation.updaters.update import *
from manim.constants import *
from manim.file_writer import *
from manim.manager import *
from manim.mobject.frame import *
from manim.mobject.geometry.arc import *
from manim.mobject.geometry.boolean_ops import *
from manim.mobject.geometry.labeled import *
from manim.mobject.geometry.line import *
from manim.mobject.geometry.polygram import *
from manim.mobject.geometry.shape_matchers import *
from manim.mobject.geometry.tips import *
from manim.mobject.graph import *
from manim.mobject.graphing.coordinate_systems import *
from manim.mobject.graphing.functions import *
from manim.mobject.graphing.number_line import *
from manim.mobject.graphing.probability import *
from manim.mobject.graphing.scale import *
from manim.mobject.logo import *
from manim.mobject.matrix import *
from manim.mobject.mobject import *
from manim.mobject.opengl.dot_cloud import *
from manim.mobject.opengl.opengl_point_cloud_mobject import *
from manim.mobject.svg.brace import *
from manim.mobject.svg.svg_mobject import *
from manim.mobject.table import *
from manim.mobject.text.code_mobject import *
from manim.mobject.text.numbers import *
from manim.mobject.text.tex_mobject import *
from manim.mobject.text.text_mobject import *
from manim.mobject.three_d.polyhedra import *
from manim.mobject.three_d.three_d_utils import *
from manim.mobject.three_d.three_dimensions import *
from manim.mobject.types.image_mobject import *
from manim.mobject.types.point_cloud_mobject import *
from manim.mobject.types.vectorized_mobject import *
from manim.mobject.value_tracker import *
from manim.mobject.vector_field import *
from manim.scene.scene import *
from manim.scene.vector_space_scene import *
from manim.utils import color, rate_functions, unit
from manim.utils.bezier import *
from manim.utils.color import *
from manim.utils.config_ops import *
from manim.utils.debug import *
from manim.utils.file_ops import *
from manim.utils.images import *
from manim.utils.iterables import *
from manim.utils.paths import *
from manim.utils.rate_functions import *
from manim.utils.simple_functions import *
from manim.utils.sounds import *
from manim.utils.space_ops import *
from manim.utils.tex import *
from manim.utils.tex_templates import *
try:
from IPython import get_ipython
from .utils.ipython_magic import ManimMagic
from manim.utils.ipython_magic import ManimMagic
except ImportError:
pass
else:
@ -101,4 +100,4 @@ else:
if ipy is not None:
ipy.register_magics(ManimMagic)
from .plugins import *
from manim.plugins import *

View file

@ -7,18 +7,18 @@
# Each of the following will be set to True if the corresponding CLI flag
# is present when executing manim. If the flag is not present, they will
# be set to the value found here. For example, running manim with the -w
# flag will set WRITE_TO_MOVIE to True. However, since the default value
# of WRITE_TO_MOVIE defined in this file is also True, running manim
# without the -w value will also output a movie file. To change that, set
# WRITE_TO_MOVIE = False so that running manim without the -w flag will not
# generate a movie file. Note all of the following accept boolean values.
# be set to the value found here. For example, running manim with the --format=mp4
# flag will set FORMAT to mp4. However, since the default value
# of FORMAT defined in this file is also mp4, running manim
# without the --format=mp4 value will also output an mp4 movie file. To change that, set
# FORMAT = webm so that running manim without the --format=mp4 flag will not
# generate an mp4 movie file.
# --notify_outdated_version
notify_outdated_version = True
# -w, --write_to_movie
write_to_movie = True
write_to_movie = False
format = mp4
@ -94,7 +94,7 @@ text_dir = {media_dir}/texts
partial_movie_dir = {video_dir}/partial_movie_files/{scene_name}
# --renderer [cairo|opengl]
renderer = cairo
renderer = opengl
# --enable_gui
enable_gui = False

View file

@ -316,7 +316,6 @@ class ManimConfig(MutableMapping):
"write_to_movie",
"zero_pad",
"force_window",
"parallel",
"no_latex_cleanup",
"preview_command",
}
@ -324,6 +323,19 @@ class ManimConfig(MutableMapping):
def __init__(self) -> None:
self._d: dict[str, Any | None] = {k: None for k in self._OPTS}
def _warn_about_config_options(self) -> None:
"""Warns about incorrect config options, or permutations of config options."""
logger = logging.getLogger("manim")
if self.format == "webm":
logger.warning(
"Output format set as webm, this can be slower than other formats",
)
if not self.preview and not self.write_to_movie:
logger.warning(
"preview and write_to_movie disabled, this is a dry run. Try passing -p or -w."
)
# behave like a dict
def __iter__(self) -> Iterator[str]:
return iter(self._d)
@ -590,7 +602,6 @@ class ManimConfig(MutableMapping):
"use_projection_stroke_shaders",
"enable_wireframe",
"force_window",
"parallel",
"no_latex_cleanup",
]:
setattr(self, key, parser["CLI"].getboolean(key, fallback=False))
@ -939,6 +950,7 @@ class ManimConfig(MutableMapping):
def notify_outdated_version(self, value: bool) -> None:
self._set_boolean("notify_outdated_version", value)
# TODO: Rename to write_to_file
@property
def write_to_movie(self) -> bool:
"""Whether to render the scene to a movie file (-w)."""
@ -1054,18 +1066,7 @@ class ManimConfig(MutableMapping):
val,
[None, "png", "gif", "mp4", "mov", "webm"],
)
if self.format == "webm":
logging.getLogger("manim").warning(
"Output format set as webm, this can be slower than other formats",
)
@property
def in_parallel(self) -> None:
return self._d["parallel"]
@in_parallel.setter
def in_parallel(self, val: bool) -> None:
self._set_boolean("parallel", val)
self.resolve_movie_file_extension(self.transparent)
@property
def ffmpeg_loglevel(self) -> str:

View file

@ -2,6 +2,8 @@
from __future__ import annotations
import numpy as np
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
from .. import config, logger
@ -11,7 +13,7 @@ from ..mobject.mobject import Mobject
from ..mobject.opengl import opengl_mobject
from ..utils.rate_functions import linear, smooth
from .protocol import AnimationProtocol
from .scene_buffer import SceneBuffer
from .scene_buffer import SceneBuffer, SceneOperation
__all__ = ["Animation", "Wait", "override_animation"]
@ -72,9 +74,9 @@ class Animation(AnimationProtocol):
.. NOTE::
In the current implementation of this class, the specified rate function is applied
within :meth:`.Animation.interpolate_mobject` call as part of the call to
within :meth:`.Animation.interpolate` call as part of the call to
:meth:`.Animation.interpolate_submobject`. For subclasses of :class:`.Animation`
that are implemented by overriding :meth:`interpolate_mobject`, the rate function
that are implemented by overriding :meth:`interpolate`, the rate function
has to be applied manually (e.g., by passing ``self.rate_func(alpha)`` instead
of just ``alpha``).
@ -135,7 +137,7 @@ class Animation(AnimationProtocol):
run_time: float = DEFAULT_ANIMATION_RUN_TIME,
rate_func: Callable[[float], float] = smooth,
reverse_rate_function: bool = False,
name: str | None = None,
name: str = "",
remover: bool = False, # remove a mobject from the screen at end of animation
suspend_mobject_updating: bool = True,
introducer: bool = False,
@ -147,7 +149,7 @@ class Animation(AnimationProtocol):
self.run_time: float = run_time
self.rate_func: Callable[[float], float] = rate_func
self.reverse_rate_function: bool = reverse_rate_function
self.name: str | None = name
self.name: str = name
self.remover: bool = remover
self.introducer: bool = introducer
self.suspend_mobject_updating: bool = suspend_mobject_updating
@ -218,7 +220,7 @@ class Animation(AnimationProtocol):
# TODO: Figure out a way to check
# if self.mobject in scene.get_mobject_family
if self.is_introducer():
if self.introducer:
self.buffer.add(self.mobject)
def finish(self) -> None:
@ -231,15 +233,16 @@ class Animation(AnimationProtocol):
if self.suspend_mobject_updating and self.mobject is not None:
self.mobject.resume_updating()
# TODO: remove on_finish
self._on_finish(self.buffer)
if self.is_remover():
if self.remover:
self.buffer.remove(self.mobject)
def create_starting_mobject(self) -> Mobject:
def create_starting_mobject(self) -> OpenGLMobject:
# Keep track of where the mobject starts
return self.mobject.copy()
def get_all_mobjects(self) -> Sequence[Mobject]:
def get_all_mobjects(self) -> Sequence[OpenGLMobject]:
"""Get all mobjects involved in the animation.
Ordering must match the ordering of arguments to interpolate_submobject
@ -274,13 +277,19 @@ class Animation(AnimationProtocol):
This is used in animations that are proxies around
other animations, like :class:`.AnimationGroup`
"""
self.buffer.remove(*buffer.to_remove)
for to_replace_pairs in buffer.to_replace:
self.buffer.replace(*to_replace_pairs)
self.buffer.add(*buffer.to_add)
for op, args, kwargs in buffer:
match op:
case SceneOperation.ADD:
self.buffer.add(*args, **kwargs)
case SceneOperation.REMOVE:
self.buffer.remove(*args, **kwargs)
case SceneOperation.REPLACE:
self.buffer.replace(*args, **kwargs)
case o:
raise NotImplementedError(f"Unknown operation {o}")
buffer.clear()
def get_all_mobjects_to_update(self) -> list[Mobject]:
def get_all_mobjects_to_update(self) -> Sequence[OpenGLMobject]:
"""Get all mobjects to be updated during the animation.
Returns
@ -307,19 +316,6 @@ class Animation(AnimationProtocol):
# TODO: stop using alpha as parameter name in different meanings.
def interpolate(self, alpha: float) -> None:
"""Set the animation progress.
This method gets called for every frame during an animation.
Parameters
----------
alpha
The relative time to set the animation to, 0 meaning the start, 1 meaning
the end.
"""
self.interpolate_mobject(alpha)
def interpolate_mobject(self, alpha: float) -> None:
"""Interpolates the mobject of the :class:`Animation` based on alpha value.
Parameters
@ -332,17 +328,16 @@ class Animation(AnimationProtocol):
families = tuple(self.get_all_families_zipped())
for i, mobs in enumerate(families):
sub_alpha = self.get_sub_alpha(alpha, i, len(families))
self.interpolate_submobject(*mobs, sub_alpha)
self.interpolate_submobject(*mobs, sub_alpha) # type: ignore
def interpolate_submobject(
self,
submobject: Mobject,
starting_submobject: Mobject,
submobject: OpenGLMobject,
starting_submobject: OpenGLMobject,
# target_copy: Mobject, #Todo: fix - signature of interpolate_submobject differs in Transform().
alpha: float,
) -> Animation:
# Typically implemented by subclass
raise NotImplementedError()
raise NotImplementedError("Implement in subclass")
def get_sub_alpha(self, alpha: float, index: int, num_submobjects: int) -> float:
"""Get the animation progress of any submobjects subanimation.
@ -368,13 +363,14 @@ class Animation(AnimationProtocol):
full_length = (num_submobjects - 1) * lag_ratio + 1
value = alpha * full_length
lower = index * lag_ratio
raw_sub_alpha = np.clip((value - lower), 0, 1)
if self.reverse_rate_function:
return self.rate_func(1 - (value - lower))
return self.rate_func(1 - raw_sub_alpha)
else:
return self.rate_func(value - lower)
return self.rate_func(raw_sub_alpha)
# Getters and setters
def set_run_time(self, run_time: float) -> Animation:
def set_run_time(self, run_time: float) -> Self:
"""Set the run time of the animation.
Parameters
@ -454,31 +450,12 @@ class Animation(AnimationProtocol):
self.name = name
return self
def is_remover(self) -> bool:
"""Test if the animation is a remover.
Returns
-------
bool
``True`` if the animation is a remover, ``False`` otherwise.
"""
return self.remover
def is_introducer(self) -> bool:
"""Test if the animation is an introducer.
Returns
-------
bool
``True`` if the animation is an introducer, ``False`` otherwise.
"""
return self.introducer
def prepare_animation(
anim: AnimationProtocol
| mobject._AnimationBuilder
| opengl_mobject._AnimationBuilder,
| opengl_mobject._AnimationBuilder
| opengl_mobject.OpenGLMobject,
) -> Animation:
r"""Returns either an unchanged animation, or the animation built
from a passed animation factory.
@ -551,10 +528,8 @@ class Wait(Animation):
self.duration: float = run_time
self.stop_condition = stop_condition
self.is_static_wait: bool = frozen_frame
self.is_static_wait: bool = bool(frozen_frame)
super().__init__(None, run_time=run_time, rate_func=rate_func, **kwargs)
# quick fix to work in opengl setting:
self.mobject.shader_wrapper_list = []
def begin(self) -> None:
pass
@ -611,7 +586,7 @@ def override_animation(
_F = TypeVar("_F", bound=Callable)
def decorator(func: _F) -> _F:
func._override_animation = animation_class
func._override_animation = animation_class # type: ignore
return func
return decorator

View file

@ -16,10 +16,6 @@ from manim.utils.iterables import remove_list_redundancies
from manim.utils.parameter_parsing import flatten_iterable_parameters
from manim.utils.rate_functions import linear
from ..animation.animation import Animation, prepare_animation
from ..constants import RendererType
from ..mobject.mobject import Group, Mobject
if TYPE_CHECKING:
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup
from manim.mobject.types.vectorized_mobject import VGroup
@ -70,7 +66,7 @@ class AnimationGroup(Animation):
self.group = group
if self.group is None:
mobjects = remove_list_redundancies(
[anim.mobject for anim in self.animations if not anim.is_introducer()],
[anim.mobject for anim in self.animations if not anim.introducer],
)
if config["renderer"] == RendererType.OPENGL:
self.group = OpenGLGroup(*mobjects)
@ -234,10 +230,6 @@ class Succession(AnimationGroup):
)
self.update_active_animation(0)
for anim in self.animations:
if not anim.is_introducer() and anim.mobject is not None:
self.buffer.add(anim.mobject)
def finish(self) -> None:
while self.active_animation is not None:
self.next_animation()

View file

@ -475,7 +475,7 @@ class SpiralIn(Animation):
super().__init__(shapes, introducer=True, **kwargs)
def interpolate_mobject(self, alpha: float) -> None:
def interpolate(self, alpha: float) -> None:
alpha = self.rate_func(alpha)
for original_shape, shape in zip(self.shapes, self.mobject):
shape.restore()
@ -529,7 +529,7 @@ class ShowIncreasingSubsets(Animation):
**kwargs,
)
def interpolate_mobject(self, alpha: float) -> None:
def interpolate(self, alpha: float) -> None:
n_submobs = len(self.all_submobs)
value = (
1 - self.rate_func(alpha)

View file

@ -27,14 +27,14 @@ from manim.mobject.opengl.opengl_mobject import OpenGLMobject
from ..animation.transform import Transform
from ..constants import ORIGIN
from ..mobject.mobject import Group, Mobject
from ..mobject.mobject import Group
if TYPE_CHECKING:
pass
class _Fade(Transform):
"""Fade :class:`~.Mobject` s in or out.
"""Fade :class:`~.OpenGLMobject` s in or out.
Parameters
----------
@ -53,9 +53,9 @@ class _Fade(Transform):
def __init__(
self,
*mobjects: Mobject,
*mobjects: OpenGLMobject,
shift: np.ndarray | None = None,
target_position: np.ndarray | Mobject | None = None,
target_position: np.ndarray | OpenGLMobject | None = None,
scale: float = 1,
**kwargs,
) -> None:
@ -69,7 +69,7 @@ class _Fade(Transform):
self.point_target = False
if shift is None:
if target_position is not None:
if isinstance(target_position, (Mobject, OpenGLMobject)):
if isinstance(target_position, OpenGLMobject):
target_position = target_position.get_center()
shift = target_position - mobject.get_center()
self.point_target = True
@ -79,7 +79,7 @@ class _Fade(Transform):
self.scale_factor = scale
super().__init__(mobject, **kwargs)
def _create_faded_mobject(self, fade_in: bool) -> Mobject:
def _create_faded_mobject(self, fade_in: bool) -> OpenGLMobject:
"""Create a faded, shifted and scaled copy of the mobject.
Parameters
@ -89,7 +89,7 @@ class _Fade(Transform):
Returns
-------
Mobject
OpenGLMobject
The faded, shifted and scaled copy of the mobject.
"""
faded_mobject = self.mobject.copy()
@ -101,7 +101,7 @@ class _Fade(Transform):
class FadeIn(_Fade):
"""Fade in :class:`~.Mobject` s.
"""Fade in :class:`~.OpenGLMobject` s.
Parameters
----------
@ -138,7 +138,7 @@ class FadeIn(_Fade):
"""
def __init__(self, *mobjects: Mobject, **kwargs) -> None:
def __init__(self, *mobjects: OpenGLMobject, **kwargs) -> None:
super().__init__(*mobjects, introducer=True, **kwargs)
def create_target(self):
@ -149,7 +149,7 @@ class FadeIn(_Fade):
class FadeOut(_Fade):
"""Fade out :class:`~.Mobject` s.
"""Fade out :class:`~.OpenGLMobject` s.
Parameters
----------
@ -186,7 +186,7 @@ class FadeOut(_Fade):
"""
def __init__(self, *mobjects: Mobject, **kwargs) -> None:
def __init__(self, *mobjects: OpenGLMobject, **kwargs) -> None:
super().__init__(*mobjects, remover=True, **kwargs)
def create_target(self):

View file

@ -126,7 +126,7 @@ class PhaseFlow(Animation):
**kwargs,
)
def interpolate_mobject(self, alpha: float) -> None:
def interpolate(self, alpha: float) -> None:
if hasattr(self, "last_alpha"):
dt = self.virtual_time * (
self.rate_func(alpha) - self.rate_func(self.last_alpha)
@ -162,6 +162,6 @@ class MoveAlongPath(Animation):
mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs
)
def interpolate_mobject(self, alpha: float) -> None:
def interpolate(self, alpha: float) -> None:
point = self.path.point_from_proportion(self.rate_func(alpha))
self.mobject.move_to(point)

View file

@ -31,7 +31,7 @@ class ChangingDecimal(Animation):
if not isinstance(decimal_mob, DecimalNumber):
raise TypeError("ChangingDecimal can only take in a DecimalNumber")
def interpolate_mobject(self, alpha: float) -> None:
def interpolate(self, alpha: float) -> None:
self.mobject.set_value(self.number_update_func(self.rate_func(alpha)))

View file

@ -4,49 +4,61 @@ from __future__ import annotations
__all__ = ["Rotating", "Rotate"]
from collections.abc import Sequence
from typing import TYPE_CHECKING, Callable
from typing import TYPE_CHECKING
import numpy as np
from ..animation.animation import Animation
from ..animation.transform import Transform
from ..constants import OUT, PI, TAU
from ..utils.rate_functions import linear
from manim.animation.animation import Animation
from manim.constants import ORIGIN, OUT, PI, TAU
from manim.utils.rate_functions import linear
if TYPE_CHECKING:
from ..mobject.mobject import Mobject
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
from manim.typing import RateFunc
class Rotating(Animation):
def __init__(
self,
mobject: Mobject,
mobject: OpenGLMobject,
angle: float = TAU,
axis: np.ndarray = OUT,
radians: np.ndarray = TAU,
about_point: np.ndarray | None = None,
about_edge: np.ndarray | None = None,
run_time: float = 5,
rate_func: Callable[[float], float] = linear,
run_time: float = 5.0,
rate_func: RateFunc = linear,
suspend_mobject_updating: bool = False,
**kwargs,
) -> None:
):
super().__init__(
mobject,
run_time=run_time,
rate_func=rate_func,
suspend_mobject_updating=suspend_mobject_updating,
**kwargs,
)
self.angle = angle
self.axis = axis
self.radians = radians
self.about_point = about_point
self.about_edge = about_edge
super().__init__(mobject, run_time=run_time, rate_func=rate_func, **kwargs)
def interpolate_mobject(self, alpha: float) -> None:
self.mobject.become(self.starting_mobject)
def interpolate(self, alpha: float) -> None:
pairs = zip(
self.mobject.family_members_with_points(),
self.starting_mobject.family_members_with_points(),
)
for sm1, sm2 in pairs:
sm1.points[:] = sm2.points
self.mobject.rotate(
self.rate_func(alpha) * self.radians,
self.rate_func(alpha) * self.angle,
axis=self.axis,
about_point=self.about_point,
about_edge=self.about_edge,
)
class Rotate(Transform):
class Rotate(Rotating):
"""Animation that rotates a Mobject.
Parameters
@ -67,7 +79,6 @@ class Rotate(Transform):
Examples
--------
.. manim:: UsingRotate
class UsingRotate(Scene):
def construct(self):
self.play(
@ -79,36 +90,23 @@ class Rotate(Transform):
),
Rotate(Square(side_length=0.5), angle=2*PI, rate_func=linear),
)
"""
def __init__(
self,
mobject: Mobject,
mobject: OpenGLMobject,
angle: float = PI,
axis: np.ndarray = OUT,
about_point: Sequence[float] | None = None,
about_edge: Sequence[float] | None = None,
run_time: float = 1,
about_edge: np.ndarray = ORIGIN,
**kwargs,
) -> None:
if "path_arc" not in kwargs:
kwargs["path_arc"] = angle
if "path_arc_axis" not in kwargs:
kwargs["path_arc_axis"] = axis
self.angle = angle
self.axis = axis
self.about_edge = about_edge
self.about_point = about_point
if self.about_point is None:
self.about_point = mobject.get_center()
super().__init__(mobject, path_arc_centers=self.about_point, **kwargs)
def create_target(self) -> Mobject:
target = self.mobject.copy()
target.rotate(
self.angle,
axis=self.axis,
about_point=self.about_point,
about_edge=self.about_edge,
):
super().__init__(
mobject,
angle,
axis,
run_time=run_time,
about_edge=about_edge,
introducer=True,
**kwargs,
)
return target

View file

@ -1,10 +1,21 @@
from __future__ import annotations
from typing import final
from collections.abc import Iterator
from enum import Enum
from typing import TYPE_CHECKING, Any, final
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
__all__ = ["SceneBuffer"]
if TYPE_CHECKING:
from collections.abc import Sequence
__all__ = ["SceneBuffer", "SceneOperation"]
class SceneOperation(Enum):
ADD = "add"
REMOVE = "remove"
REPLACE = "replace"
@final
@ -19,42 +30,55 @@ class SceneBuffer:
It is the scenes job to clear the buffer in between the beginning
and end of animations.
To iterate over the operations, simply iterate over the buffer.
Example
-------
.. code-block:: pycon
>>> buffer = SceneBuffer()
>>> buffer.add(Square())
>>> buffer.remove(Circle())
>>> buffer.replace(Square(), Circle())
>>> for operation in buffer:
... print(operation)
(SceneOperation.ADD, (Square(),), {})
(SceneOperation.REMOVE, (Circle(),), {})
(SceneOperation.REPLACE, (Square(), Circle()), {})
"""
def __init__(self) -> None:
self.to_remove: list[Mobject] = []
self.to_add: list[Mobject] = []
self.to_replace: list[tuple[Mobject, ...]] = []
self.deferred = False
self.operations: list[
tuple[SceneOperation, Sequence[OpenGLMobject], dict[str, Any]]
] = []
def add(self, *mobs: Mobject) -> None:
self._check_deferred()
self.to_add.extend(mobs)
def add(self, *mobs: OpenGLMobject, **kwargs: Any) -> None:
"""Add mobjects to the scene."""
self.operations.append((SceneOperation.ADD, mobs, kwargs))
def remove(self, *mobs: Mobject) -> None:
self._check_deferred()
self.to_remove.extend(mobs)
def remove(self, *mobs: OpenGLMobject, **kwargs: Any) -> None:
"""Remove mobjects from the scene."""
self.operations.append((SceneOperation.REMOVE, mobs, kwargs))
def replace(self, mob: Mobject, *replacements: Mobject) -> None:
self._check_deferred()
self.to_replace.append((mob, *replacements))
def replace(
self, mob: OpenGLMobject, *replacements: OpenGLMobject, **kwargs: Any
) -> None:
"""Replace a ``mob`` with ``replacements`` on the scene."""
self.operations.append((SceneOperation.REPLACE, (mob, *replacements), kwargs))
def clear(self) -> None:
self.to_remove.clear()
self.to_add.clear()
def deferred_clear(self) -> None:
"""Clear ``self`` on next operation"""
self.deferred = True
def _check_deferred(self) -> None:
if self.deferred:
self.clear()
self.deferred = False
"""Clear the buffer."""
self.operations.clear()
def __str__(self) -> str:
to_add = self.to_add
to_remove = self.to_remove
return f"{type(self).__name__}({to_add=}, {to_remove=})"
operations = self.operations
return f"{type(self).__name__}({operations=})"
__repr__ = __str__
def __iter__(
self,
) -> Iterator[tuple[SceneOperation, Sequence[OpenGLMobject], dict[str, Any]]]:
return iter(self.operations)

View file

@ -2,6 +2,8 @@
from __future__ import annotations
from manim.typing import PathFuncType
__all__ = [
"Transform",
"ReplacementTransform",
@ -125,12 +127,12 @@ class Transform(Animation):
def __init__(
self,
mobject: Mobject | None,
target_mobject: Mobject | None = None,
mobject: OpenGLMobject | None,
target_mobject: OpenGLMobject | None = None,
path_func: Callable | None = None,
path_arc: float = 0,
path_arc_axis: np.ndarray = OUT,
path_arc_centers: np.ndarray = None,
path_arc_centers: np.ndarray | None = None,
replace_mobject_with_target_in_scene: bool = False,
**kwargs,
) -> None:
@ -151,8 +153,8 @@ class Transform(Animation):
self.replace_mobject_with_target_in_scene: bool = (
replace_mobject_with_target_in_scene
)
self.target_mobject: Mobject = (
target_mobject if target_mobject is not None else Mobject()
self.target_mobject: OpenGLMobject = (
target_mobject if target_mobject is not None else OpenGLMobject()
)
super().__init__(mobject, **kwargs)
@ -171,19 +173,13 @@ class Transform(Animation):
@property
def path_func(
self,
) -> Callable[
[Iterable[np.ndarray], Iterable[np.ndarray], float],
Iterable[np.ndarray],
]:
) -> PathFuncType:
return self._path_func
@path_func.setter
def path_func(
self,
path_func: Callable[
[Iterable[np.ndarray], Iterable[np.ndarray], float],
Iterable[np.ndarray],
],
path_func: PathFuncType,
) -> None:
if path_func is not None:
self._path_func = path_func
@ -193,13 +189,17 @@ class Transform(Animation):
# call so that the actual target_mobject stays
# preserved.
self.target_mobject = self.create_target()
self.target_copy = self.target_mobject.copy()
# Note, this potentially changes the structure
# of both mobject and target_mobject
if self.mobject.is_aligned_with(self.target_mobject):
self.target_copy = self.target_mobject
else:
self.target_copy = self.target_mobject.copy()
self.mobject.align_data_and_family(self.target_copy)
super().begin()
def create_target(self) -> Mobject:
def create_target(self) -> OpenGLMobject:
# Has no meaningful effect here, but may be useful
# in subclasses
return self.target_mobject
@ -212,7 +212,7 @@ class Transform(Animation):
self.buffer.remove(self.mobject)
self.buffer.add(self.target_mobject)
def get_all_mobjects(self) -> Sequence[Mobject]:
def get_all_mobjects(self) -> Sequence[OpenGLMobject]:
return [
self.mobject,
self.starting_mobject,
@ -230,9 +230,9 @@ class Transform(Animation):
def interpolate_submobject(
self,
submobject: Mobject,
starting_submobject: Mobject,
target_copy: Mobject,
submobject: OpenGLMobject,
starting_submobject: OpenGLMobject,
target_copy: OpenGLMobject,
alpha: float,
) -> Transform:
submobject.interpolate(starting_submobject, target_copy, alpha, self.path_func)
@ -287,7 +287,9 @@ class ReplacementTransform(Transform):
"""
def __init__(self, mobject: Mobject, target_mobject: Mobject, **kwargs) -> None:
def __init__(
self, mobject: OpenGLMobject, target_mobject: OpenGLMobject, **kwargs
) -> None:
super().__init__(
mobject, target_mobject, replace_mobject_with_target_in_scene=True, **kwargs
)
@ -298,7 +300,9 @@ class TransformFromCopy(Transform):
Performs a reversed Transform
"""
def __init__(self, mobject: Mobject, target_mobject: Mobject, **kwargs) -> None:
def __init__(
self, mobject: OpenGLMobject, target_mobject: OpenGLMobject, **kwargs
) -> None:
super().__init__(target_mobject, mobject, **kwargs)
def interpolate(self, alpha: float) -> None:
@ -337,8 +341,8 @@ class ClockwiseTransform(Transform):
def __init__(
self,
mobject: Mobject,
target_mobject: Mobject,
mobject: OpenGLMobject,
target_mobject: OpenGLMobject,
path_arc: float = -np.pi,
**kwargs,
) -> None:
@ -386,8 +390,8 @@ class CounterclockwiseTransform(Transform):
def __init__(
self,
mobject: Mobject,
target_mobject: Mobject,
mobject: OpenGLMobject,
target_mobject: OpenGLMobject,
path_arc: float = np.pi,
**kwargs,
) -> None:
@ -420,11 +424,11 @@ class MoveToTarget(Transform):
"""
def __init__(self, mobject: Mobject, **kwargs) -> None:
def __init__(self, mobject: OpenGLMobject, **kwargs) -> None:
self.check_validity_of_input(mobject)
super().__init__(mobject, mobject.target, **kwargs)
def check_validity_of_input(self, mobject: Mobject) -> None:
def check_validity_of_input(self, mobject: OpenGLMobject) -> None:
if not hasattr(mobject, "target"):
raise ValueError(
"MoveToTarget called on mobject" "without attribute 'target'",
@ -477,7 +481,7 @@ class ApplyMethod(Transform):
)
assert isinstance(method.__self__, (Mobject, OpenGLMobject))
def create_target(self) -> Mobject:
def create_target(self) -> OpenGLMobject:
method = self.method
# Make sure it's a list so that args.pop() works
args = list(self.method_args)
@ -523,7 +527,9 @@ class ApplyPointwiseFunction(ApplyMethod):
class ApplyPointwiseFunctionToCenter(ApplyPointwiseFunction):
def __init__(self, function: types.MethodType, mobject: Mobject, **kwargs) -> None:
def __init__(
self, function: types.MethodType, mobject: OpenGLMobject, **kwargs
) -> None:
self.function = function
super().__init__(mobject.move_to, **kwargs)
@ -613,7 +619,9 @@ class Restore(ApplyMethod):
class ApplyFunction(Transform):
def __init__(self, function: types.MethodType, mobject: Mobject, **kwargs) -> None:
def __init__(
self, function: types.MethodType, mobject: OpenGLMobject, **kwargs
) -> None:
self.function = function
super().__init__(mobject, **kwargs)
@ -684,6 +692,7 @@ class ApplyComplexFunction(ApplyMethod):
super().__init__(method, function, **kwargs)
def _init_path_func(self) -> None:
# TODO: this seems broken?
func1 = self.function(complex(1))
self.path_arc = np.log(func1).imag
super()._init_path_func()

View file

@ -143,7 +143,7 @@ class TransformMatchingAbstractBase(AnimationGroup):
key = self.get_mobject_key(sm)
if key not in shape_map:
if config["renderer"] == RendererType.OPENGL:
shape_map[key] = OpenGLVGroup()
shape_map[key] = VGroup()
else:
shape_map[key] = VGroup()
shape_map[key].add(sm)

View file

@ -4,11 +4,7 @@ from __future__ import annotations
__all__ = [
"assert_is_mobject_method",
"always",
"f_always",
"always_redraw",
"always_shift",
"always_rotate",
"turn_animation_into_updater",
"cycle_animation",
]
@ -19,13 +15,11 @@ from typing import TYPE_CHECKING, Callable
import numpy as np
from manim.constants import DEGREES, RIGHT
from manim.mobject.mobject import Mobject
from manim.opengl import OpenGLMobject
from manim.utils.space_ops import normalize
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
if TYPE_CHECKING:
from manim.animation.animation import Animation
from manim.animation.protocol import AnimationProtocol
def assert_is_mobject_method(method: Callable) -> None:
@ -34,32 +28,6 @@ def assert_is_mobject_method(method: Callable) -> None:
assert isinstance(mobject, (Mobject, OpenGLMobject))
def always(method: Callable, *args, **kwargs) -> Mobject:
assert_is_mobject_method(method)
mobject = method.__self__
func = method.__func__
mobject.add_updater(lambda m: func(m, *args, **kwargs))
return mobject
def f_always(method: Callable[[Mobject], None], *arg_generators, **kwargs) -> Mobject:
"""
More functional version of always, where instead
of taking in args, it takes in functions which output
the relevant arguments.
"""
assert_is_mobject_method(method)
mobject = method.__self__
func = method.__func__
def updater(mob):
args = [arg_generator() for arg_generator in arg_generators]
func(mob, *args, **kwargs)
mobject.add_updater(updater)
return mobject
def always_redraw(func: Callable[[], Mobject]) -> Mobject:
"""Redraw the mobject constructed by a function every frame.
@ -105,80 +73,8 @@ def always_redraw(func: Callable[[], Mobject]) -> Mobject:
return mob
def always_shift(
mobject: Mobject, direction: np.ndarray[np.float64] = RIGHT, rate: float = 0.1
) -> Mobject:
"""A mobject which is continuously shifted along some direction
at a certain rate.
Parameters
----------
mobject
The mobject to shift.
direction
The direction to shift. The vector is normalized, the specified magnitude
is not relevant.
rate
Length in Manim units which the mobject travels in one
second along the specified direction.
Examples
--------
.. manim:: ShiftingSquare
class ShiftingSquare(Scene):
def construct(self):
sq = Square().set_fill(opacity=1)
tri = Triangle()
VGroup(sq, tri).arrange(LEFT)
# construct a square which is continuously
# shifted to the right
always_shift(sq, RIGHT, rate=5)
self.add(sq)
self.play(tri.animate.set_fill(opacity=1))
"""
mobject.add_updater(lambda m, dt: m.shift(dt * rate * normalize(direction)))
return mobject
def always_rotate(mobject: Mobject, rate: float = 20 * DEGREES, **kwargs) -> Mobject:
"""A mobject which is continuously rotated at a certain rate.
Parameters
----------
mobject
The mobject to be rotated.
rate
The angle which the mobject is rotated by
over one second.
kwags
Further arguments to be passed to :meth:`.Mobject.rotate`.
Examples
--------
.. manim:: SpinningTriangle
class SpinningTriangle(Scene):
def construct(self):
tri = Triangle().set_fill(opacity=1).set_z_index(2)
sq = Square().to_edge(LEFT)
# will keep spinning while there is an animation going on
always_rotate(tri, rate=2*PI, about_point=ORIGIN)
self.add(tri, sq)
self.play(sq.animate.to_edge(RIGHT), rate_func=linear, run_time=1)
"""
mobject.add_updater(lambda m, dt: m.rotate(dt * rate, **kwargs))
return mobject
def turn_animation_into_updater(
animation: Animation, cycle: bool = False, **kwargs
animation: AnimationProtocol, cycle: bool = False, **kwargs
) -> Mobject:
"""
Add an updater to the animation's mobject which applies
@ -227,5 +123,5 @@ def turn_animation_into_updater(
return mobject
def cycle_animation(animation: Animation, **kwargs) -> Mobject:
def cycle_animation(animation: AnimationProtocol, **kwargs) -> Mobject:
return turn_animation_into_updater(animation, cycle=True, **kwargs)

View file

@ -33,12 +33,12 @@ class UpdateFromFunc(Animation):
mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs
)
def interpolate_mobject(self, alpha: float) -> None:
def interpolate(self, alpha: float) -> None:
self.update_function(self.mobject)
class UpdateFromAlphaFunc(UpdateFromFunc):
def interpolate_mobject(self, alpha: float) -> None:
def interpolate(self, alpha: float) -> None:
self.update_function(self.mobject, self.rate_func(alpha))
@ -51,7 +51,7 @@ class MaintainPositionRelativeTo(Animation):
)
super().__init__(mobject, **kwargs)
def interpolate_mobject(self, alpha: float) -> None:
def interpolate(self, alpha: float) -> None:
target = self.tracked_mobject.get_center()
location = self.mobject.get_center()
self.mobject.shift(target - location + self.diff)

View file

@ -1,28 +1,18 @@
from __future__ import annotations
import itertools as it
import math
from collections.abc import Iterable
from typing import Any
import moderngl
import numpy as np
from PIL import Image
from scipy.spatial.transform import Rotation
from manim import config, logger
from manim.mobject.opengl.opengl_mobject import OpenGLMobject, OpenGLPoint
from manim.renderer.shader_wrapper import ShaderWrapper
from manim.utils.color import BLACK, color_to_rgba
from manim._config import config
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
from ..constants import *
from ..utils.simple_functions import fdiv
from ..utils.space_ops import normalize
class Camera(OpenGLMobject):
fps: int = 30
def __init__(
self,
frame_shape: tuple[float, float] = (config.frame_width, config.frame_height),
@ -156,355 +146,3 @@ class Camera(OpenGLMobject):
to_camera = self.get_inverse_camera_rotation_matrix()[2]
dist = self.get_focal_distance()
return self.get_center() + dist * to_camera
# TODO: This is already ported to the renderer and now is useless, leavefor now for compoatibilty reasons
class OpenGLCamera:
def __init__(
self,
ctx: moderngl.Context | None = None,
background_image: str | None = None,
frame_config: dict = {},
pixel_width: int = config.pixel_width,
pixel_height: int = config.pixel_height,
fps: int = config.frame_rate,
# Note: frame height and width will be resized to match the pixel aspect rati
background_color=BLACK,
background_opacity: float = 1.0,
# Points in vectorized mobjects with norm greater
# than this value will be rescaled
max_allowable_norm: float = 1.0,
image_mode: str = "RGBA",
n_channels: int = 4,
pixel_array_dtype: type = np.uint8,
light_source_position: np.ndarray = np.array([-10, 10, 10]),
# Although vector graphics handle antialiasing fine
# without multisampling, for 3d scenes one might want
# to set samples to be greater than 0.
samples: int = 0,
) -> None:
self.background_image = background_image
self.pixel_width = pixel_width
self.pixel_height = pixel_height
self.fps = fps
self.max_allowable_norm = max_allowable_norm
self.image_mode = image_mode
self.n_channels = n_channels
self.pixel_array_dtype = pixel_array_dtype
self.light_source_position = light_source_position
self.samples = samples
self.rgb_max_val: float = np.iinfo(self.pixel_array_dtype).max
self.background_color: list[float] = list(
color_to_rgba(background_color, background_opacity)
)
self.init_frame(**frame_config)
self.init_context(ctx)
self.init_shaders()
self.init_textures()
self.init_light_source()
self.refresh_perspective_uniforms()
# A cached map from mobjects to their associated list of render groups
# so that these render groups are not regenerated unnecessarily for static
# mobjects
self.mob_to_render_groups: dict = {}
def init_frame(self, **config) -> None:
self.frame = OpenGLCameraFrame(**config)
def init_context(self, ctx: moderngl.Context | None = None) -> None:
if ctx is None:
ctx = moderngl.create_standalone_context()
fbo = self.get_fbo(ctx, 0)
else:
fbo = ctx.detect_framebuffer()
self.ctx = ctx
self.fbo = fbo
self.set_ctx_blending()
# For multisample antisampling
fbo_msaa = self.get_fbo(ctx, self.samples)
fbo_msaa.use()
self.fbo_msaa = fbo_msaa
def set_ctx_blending(self, enable: bool = True) -> None:
if enable:
self.ctx.enable(moderngl.BLEND)
else:
self.ctx.disable(moderngl.BLEND)
def set_ctx_depth_test(self, enable: bool = True) -> None:
if enable:
self.ctx.enable(moderngl.DEPTH_TEST)
else:
self.ctx.disable(moderngl.DEPTH_TEST)
def init_light_source(self) -> None:
self.light_source = OpenGLPoint(self.light_source_position)
# Methods associated with the frame buffer
def get_fbo(self, ctx: moderngl.Context, samples: int = 0) -> moderngl.Framebuffer:
pw = self.pixel_width
ph = self.pixel_height
return ctx.framebuffer(
color_attachments=ctx.texture(
(pw, ph), components=self.n_channels, samples=samples
),
depth_attachment=ctx.depth_renderbuffer((pw, ph), samples=samples),
)
def clear(self) -> None:
self.fbo.clear(*self.background_color)
self.fbo_msaa.clear(*self.background_color)
def reset_pixel_shape(self, new_width: int, new_height: int) -> None:
self.pixel_width = new_width
self.pixel_height = new_height
self.refresh_perspective_uniforms()
def get_raw_fbo_data(self, dtype: str = "f1") -> bytes:
# Copy blocks from the fbo_msaa to the drawn fbo using Blit
# pw, ph = (self.pixel_width, self.pixel_height)
# gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, self.fbo_msaa.glo)
# gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, self.fbo.glo)
# gl.glBlitFramebuffer(
# 0, 0, pw, ph, 0, 0, pw, ph, gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR
# )
self.ctx.copy_framebuffer(self.fbo, self.fbo_msaa)
return self.fbo.read(
viewport=self.fbo.viewport,
components=self.n_channels,
dtype=dtype,
)
def get_image(self) -> Image.Image:
return Image.frombytes(
"RGBA",
self.get_pixel_shape(),
self.get_raw_fbo_data(),
"raw",
"RGBA",
0,
-1,
)
def get_pixel_array(self) -> np.ndarray:
raw = self.get_raw_fbo_data(dtype="f4")
flat_arr = np.frombuffer(raw, dtype="f4")
arr = flat_arr.reshape([*reversed(self.fbo.size), self.n_channels])
arr = arr[::-1]
# Convert from float
return (self.rgb_max_val * arr).astype(self.pixel_array_dtype)
def get_texture(self):
texture = self.ctx.texture(
size=self.fbo.size, components=4, data=self.get_raw_fbo_data(), dtype="f4"
)
return texture
# Getting camera attributes
def get_pixel_shape(self) -> tuple[int, int]:
return self.fbo.viewport[2:4]
# return (self.pixel_width, self.pixel_height)
def get_pixel_width(self) -> int:
return self.get_pixel_shape()[0]
def get_pixel_height(self) -> int:
return self.get_pixel_shape()[1]
def get_frame_height(self) -> float:
return self.frame.get_height()
def get_frame_width(self) -> float:
return self.frame.get_width()
def get_frame_shape(self) -> tuple[float, float]:
return (self.get_frame_width(), self.get_frame_height())
def get_frame_center(self) -> np.ndarray:
return self.frame.get_center()
def get_location(self) -> tuple[float, float, float] | np.ndarray:
return self.frame.get_implied_camera_location()
def resize_frame_shape(self, fixed_dimension: bool = False) -> None:
"""
Changes frame_shape to match the aspect ratio
of the pixels, where fixed_dimension determines
whether frame_height or frame_width
remains fixed while the other changes accordingly.
"""
pixel_height = self.get_pixel_height()
pixel_width = self.get_pixel_width()
frame_height = self.get_frame_height()
frame_width = self.get_frame_width()
aspect_ratio = fdiv(pixel_width, pixel_height)
if not fixed_dimension:
frame_height = frame_width / aspect_ratio
else:
frame_width = aspect_ratio * frame_height
self.frame.set_height(frame_height)
self.frame.set_width(frame_width)
# Rendering
def capture(self, *mobjects: OpenGLMobject) -> None:
self.refresh_perspective_uniforms()
for mobject in mobjects:
for render_group in self.get_render_group_list(mobject):
self.render(render_group)
def render(self, render_group: dict[str, Any]) -> None:
shader_wrapper: ShaderWrapper = render_group["shader_wrapper"]
shader_program = render_group["prog"]
self.set_shader_uniforms(shader_program, shader_wrapper)
self.set_ctx_depth_test(shader_wrapper.depth_test)
render_group["vao"].render(int(shader_wrapper.render_primitive))
if render_group["single_use"]:
self.release_render_group(render_group)
def get_render_group_list(self, mobject: OpenGLMobject) -> Iterable[dict[str, Any]]:
if mobject.is_changing():
return self.generate_render_group_list(mobject)
# Otherwise, cache result for later use
key = id(mobject)
if key not in self.mob_to_render_groups:
self.mob_to_render_groups[key] = list(
self.generate_render_group_list(mobject)
)
return self.mob_to_render_groups[key]
def generate_render_group_list(
self, mobject: OpenGLMobject
) -> Iterable[dict[str, Any]]:
return (
self.get_render_group(sw, single_use=mobject.is_changing())
for sw in mobject.get_shader_wrapper_list()
)
def get_render_group(
self, shader_wrapper: ShaderWrapper, single_use: bool = True
) -> dict[str, Any]:
# Data buffers
vbo = self.ctx.buffer(shader_wrapper.vert_data.tobytes())
if shader_wrapper.vert_indices is None:
ibo = None
else:
vert_index_data = shader_wrapper.vert_indices.astype("i4").tobytes()
if vert_index_data:
ibo = self.ctx.buffer(vert_index_data)
else:
ibo = None
# Program an vertex array
shader_program, vert_format = self.get_shader_program(shader_wrapper) # type: ignore
vao = self.ctx.vertex_array(
program=shader_program,
content=[(vbo, vert_format, *shader_wrapper.vert_attributes)],
index_buffer=ibo,
)
return {
"vbo": vbo,
"ibo": ibo,
"vao": vao,
"prog": shader_program,
"shader_wrapper": shader_wrapper,
"single_use": single_use,
}
def release_render_group(self, render_group: dict[str, Any]) -> None:
for key in ["vbo", "ibo", "vao"]:
if render_group[key] is not None:
render_group[key].release()
def refresh_static_mobjects(self) -> None:
for render_group in it.chain(*self.mob_to_render_groups.values()):
self.release_render_group(render_group)
self.mob_to_render_groups = {}
# Shaders
def init_shaders(self) -> None:
# Initialize with the null id going to None
self.id_to_shader_program: dict[int, tuple[moderngl.Program, str] | None] = {
hash(""): None
}
def get_shader_program(
self, shader_wrapper: ShaderWrapper
) -> tuple[moderngl.Program, str] | None:
sid = shader_wrapper.get_program_id()
if sid not in self.id_to_shader_program:
# Create shader program for the first time, then cache
# in the id_to_shader_program dictionary
program = self.ctx.program(**shader_wrapper.get_program_code())
vert_format = moderngl.detect_format(
program, shader_wrapper.vert_attributes
)
self.id_to_shader_program[sid] = (program, vert_format)
return self.id_to_shader_program[sid]
def set_shader_uniforms(
self,
shader: moderngl.Program,
shader_wrapper: ShaderWrapper,
) -> None:
for name, path in shader_wrapper.texture_paths.items():
tid = self.get_texture_id(path)
shader[name].value = tid
for name, value in it.chain(
self.perspective_uniforms.items(), shader_wrapper.uniforms.items()
):
if name in shader:
if isinstance(value, np.ndarray) and value.ndim > 0:
value = tuple(value)
shader[name].value = value
else:
logger.debug(f"Uniform {name} not found in shader {shader}")
def refresh_perspective_uniforms(self) -> None:
frame = self.frame
# Orient light
rotation = frame.get_inverse_camera_rotation_matrix()
offset = frame.get_center()
light_pos = np.dot(rotation, self.light_source.get_location() + offset)
cam_pos = self.frame.get_implied_camera_location() # TODO
self.perspective_uniforms = {
"frame_shape": frame.get_shape(),
"pixel_shape": self.get_pixel_shape(),
"camera_offset": tuple(offset),
"camera_rotation": tuple(np.array(rotation).T.flatten()),
"camera_position": tuple(cam_pos),
"light_source_position": tuple(light_pos),
"focal_distance": frame.get_focal_distance(),
}
def init_textures(self) -> None:
self.n_textures: int = 0
self.path_to_texture: dict[str, tuple[int, moderngl.Texture]] = {}
def get_texture_id(self, path: str) -> int:
if path not in self.path_to_texture:
if self.n_textures == 15: # I have no clue why this is needed
self.n_textures += 1
tid = self.n_textures
self.n_textures += 1
im = Image.open(path).convert("RGBA")
texture = self.ctx.texture(
size=im.size,
components=len(im.getbands()),
data=im.tobytes(),
)
texture.use(location=tid)
self.path_to_texture[path] = (tid, texture)
return self.path_to_texture[path][0]
def release_texture(self, path: str):
tid_and_texture = self.path_to_texture.pop(path, None)
if tid_and_texture:
tid_and_texture[1].release()
return self

View file

@ -6,11 +6,12 @@ your Manim installation.
from __future__ import annotations
import sys
import timeit
import click
import cloup
from .checks import HEALTH_CHECKS
from manim.cli.checkhealth.checks import HEALTH_CHECKS
__all__ = ["checkhealth"]
@ -62,7 +63,7 @@ def checkhealth():
import manim as mn
class CheckHealthDemo(mn.Scene):
def construct(self):
def _inner_construct(self):
banner = mn.ManimBanner().shift(mn.UP * 0.5)
self.play(banner.create())
self.wait(0.5)
@ -79,5 +80,13 @@ def checkhealth():
mn.FadeOut(text_tex_group, shift=mn.DOWN),
)
def construct(self):
self.execution_time = timeit.timeit(self._inner_construct, number=1)
with mn.tempconfig({"preview": True, "disable_caching": True}):
CheckHealthDemo().render()
manager = mn.Manager(CheckHealthDemo)
manager.render()
click.echo(
f"Scene rendered in {manager.scene.execution_time:.2f} seconds."
)

View file

@ -18,14 +18,15 @@ from typing import cast
import cloup
from ... import __version__, config, console, error_console, logger
from ..._config import tempconfig
from ...constants import EPILOG, RendererType
from ...utils.module_ops import scene_classes_from_file
from .ease_of_access_options import ease_of_access_options
from .global_options import global_options
from .output_options import output_options
from .render_options import render_options
from manim import __version__, config, console, error_console, logger
from manim._config import tempconfig
from manim.cli.render.ease_of_access_options import ease_of_access_options
from manim.cli.render.global_options import global_options
from manim.cli.render.output_options import output_options
from manim.cli.render.render_options import render_options
from manim.constants import EPILOG, RendererType
from manim.manager import Manager
from manim.utils.module_ops import scene_classes_from_file
__all__ = ["render"]
@ -95,10 +96,10 @@ def render(
while keep_running:
for SceneClass in scene_classes_from_file(file):
with tempconfig({}):
scene = SceneClass()
rerun = scene.render()
manager = Manager(SceneClass)
rerun = manager.render()
if rerun or config["write_all"]:
scene.num_plays = 0
manager.scene.num_plays = 0
continue
else:
keep_running = False

View file

@ -99,7 +99,6 @@ global_options = option_group(
help="Renders animations without outputting image or video files and disables the window",
default=False,
),
option("--parallel", default=True, help="Renders all animations in parallel"),
option(
"--no_latex_cleanup",
is_flag=True,

View file

@ -21,10 +21,11 @@ output_options = option_group(
help="Zero padding for PNG file names.",
),
option(
"-w",
"--write_to_movie",
is_flag=True,
default=None,
help="Write the video rendered with opengl to a file.",
help="Write the video to a file.",
),
option(
"--media_dir",

View file

@ -1,13 +1,14 @@
from __future__ import annotations
import numpy as np
from typing_extensions import Any, Self
from manim.event_handler.event_listener import EventListener
from manim.event_handler.event_type import EventType
class EventDispatcher:
def __init__(self):
def __init__(self) -> None:
self.event_listeners: dict[EventType, list[EventListener]] = {
event_type: [] for event_type in EventType
}
@ -16,12 +17,12 @@ class EventDispatcher:
self.pressed_keys: set[int] = set()
self.draggable_object_listeners: list[EventListener] = []
def add_listener(self, event_listener: EventListener):
def add_listener(self, event_listener: EventListener) -> Self:
assert isinstance(event_listener, EventListener)
self.event_listeners[event_listener.event_type].append(event_listener)
return self
def remove_listener(self, event_listener: EventListener):
def remove_listener(self, event_listener: EventListener) -> Self:
assert isinstance(event_listener, EventListener)
try:
while event_listener in self.event_listeners[event_listener.event_type]:
@ -31,7 +32,7 @@ class EventDispatcher:
pass
return self
def dispatch(self, event_type: EventType, **event_data):
def dispatch(self, event_type: EventType, **event_data: Any) -> bool | None:
if event_type == EventType.MouseMotionEvent:
self.mouse_point = event_data["point"]
elif event_type == EventType.MouseDragEvent:

View file

@ -1,33 +1,33 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Callable
if TYPE_CHECKING:
from typing import Callable
from typing_extensions import Any
import manim.mobject.opengl.opengl_mobject as glmob
from manim.event_handler.event_type import EventType
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
class EventListener:
def __init__(
self,
mobject: glmob.OpenGLMobject,
mobject: OpenGLMobject,
event_type: EventType,
event_callback: Callable[[glmob.OpenGLMobject, dict[str, str]], None],
):
event_callback: Callable[[OpenGLMobject, dict[str, str]], None],
) -> None:
self.mobject = mobject
self.event_type = event_type
self.callback = event_callback
def __eq__(self, o: object) -> bool:
def __eq__(self, other: Any) -> bool:
return_val = False
if isinstance(o, EventListener):
if isinstance(other, EventListener):
try:
return_val = (
self.callback == o.callback
and self.mobject == o.mobject
and self.event_type == o.event_type
self.callback == other.callback
and self.mobject == other.mobject
and self.event_type == other.event_type
)
except Exception:
pass

View file

@ -0,0 +1,16 @@
from __future__ import annotations
from abc import ABC, abstractmethod
class WindowABC(ABC):
is_closing: bool
@abstractmethod
def swap_buffers(self) -> None: ...
@abstractmethod
def close(self) -> None: ...
@abstractmethod
def clear(self) -> None: ...

View file

@ -0,0 +1,4 @@
from __future__ import annotations
from .file_writer import FileWriter
from .sections import *

View file

@ -1,18 +1,25 @@
"""The interface between scenes and ffmpeg."""
from __future__ import annotations
__all__ = ["SceneFileWriter"]
__all__ = ["FileWriter"]
import json
import shutil
from pathlib import Path
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
import av
import numpy as np
import srt
from PIL import Image
from pydub import AudioSegment
from tqdm import tqdm as ProgressDisplay
from manim import config
from manim import __version__
from manim._config import config, logger
from manim._config.logger_utils import set_file_logger
from manim.file_writer.protocols import FileWriterProtocol
from manim.file_writer.sections import DefaultSectionType, Section
from manim.utils.file_ops import (
add_extension_if_not_present,
add_version_before_extension,
@ -25,56 +32,46 @@ from manim.utils.file_ops import (
from manim.utils.sounds import get_full_sound_file_path
if TYPE_CHECKING:
from PIL.Image import Image
from manim.scene import Scene
from manim.typing import PixelArray
class SceneFileWriter:
def __init__(
self,
scene: Scene,
write_to_movie: bool = False,
break_into_partial_movies: bool = False,
save_pngs: bool = False, # TODO, this currently does nothing
png_mode: str = "RGBA",
save_last_frame: bool = False,
movie_file_extension: str = ".mp4",
# What python file is generating this scene
input_file_path: str = "",
# Where should this be written
output_directory: str | None = None,
file_name: str | None = None,
open_file_upon_completion: bool = False,
show_file_location_upon_completion: bool = False,
quiet: bool = False,
total_frames: int = 0,
progress_description_len: int = 40,
):
self.scene: Scene = scene
self.write_to_movie = write_to_movie
self.break_into_partial_movies = break_into_partial_movies
self.save_pngs = save_pngs
self.png_mode = png_mode
self.save_last_frame = save_last_frame
self.movie_file_extension = movie_file_extension
self.input_file_path = input_file_path
self.output_directory = output_directory
self.file_name = file_name
self.open_file_upon_completion = open_file_upon_completion
self.show_file_location_upon_completion = show_file_location_upon_completion
self.quiet = quiet
self.total_frames = total_frames
self.progress_description_len = progress_description_len
class FileWriter(FileWriterProtocol):
"""
FileWriter is the object that actually writes the animations
played, into video files, using FFMPEG.
This is mostly for Manim's internal use. You will rarely, if ever,
have to use the methods for this class, unless tinkering with the very
fabric of Manim's reality.
# State during file writing
self.writing_process: sp.Popen | None = None
self.progress_display: ProgressDisplay | None = None
self.ended_with_interrupt: bool = False
self.init_output_directories()
Attributes
----------
sections : list of :class:`.Section`
used to segment scene
sections_output_dir : :class:`pathlib.Path`
where are section videos stored
output_name : str
name of movie without extension and basis for section video names
Some useful attributes are:
"write_to_movie" (bool=False)
Whether or not to write the animations into a video file.
"movie_file_extension" (str=".mp4")
The file-type extension of the outputted video.
"partial_movie_files"
List of all the partial-movie files.
"""
force_output_as_scene_name = False
def __init__(self, scene_name: str) -> None:
self.init_output_directories(scene_name)
self.init_audio()
self.frame_count = 0
self.partial_movie_files: list[str] = []
self.num_plays = 0
self.partial_movie_files: list[str | None] = []
self.subcaptions: list[srt.Subtitle] = []
self.sections: list[Section] = []
# first section gets automatically created for convenience
@ -83,7 +80,7 @@ class SceneFileWriter:
name="autocreated", type=DefaultSectionType.NORMAL, skip_animations=False
)
def init_output_directories(self, scene_name):
def init_output_directories(self, scene_name: str) -> None:
"""Initialise output directories.
Notes
@ -101,7 +98,7 @@ class SceneFileWriter:
else:
module_name = ""
if SceneFileWriter.force_output_as_scene_name:
if self.force_output_as_scene_name:
self.output_name = Path(scene_name)
elif config["output_file"] and not config["write_all"]:
self.output_name = config.get_dir("output_file")
@ -193,7 +190,7 @@ class SceneFileWriter:
),
)
def add_partial_movie_file(self, hash_animation: str):
def add_partial_movie_file(self, hash_animation: str | None) -> None:
"""Adds a new partial movie file path to `scene.partial_movie_files` and current section from a hash.
This method will compute the path from the hash. In addition to that it adds the new animation to the current section.
@ -213,12 +210,12 @@ class SceneFileWriter:
else:
new_partial_movie_file = str(
self.partial_movie_directory
/ f"{hash_animation}{config['movie_file_extension']}"
/ f"{hash_animation}{config.movie_file_extension}"
)
self.partial_movie_files.append(new_partial_movie_file)
self.sections[-1].partial_movie_files.append(new_partial_movie_file)
def get_resolution_directory(self):
def get_resolution_directory(self) -> str:
"""Get the name of the resolution directory directly containing
the video file.
@ -243,15 +240,21 @@ class SceneFileWriter:
:class:`str`
The name of the directory.
"""
pixel_height = config["pixel_height"]
frame_rate = config["frame_rate"]
pixel_height = config.pixel_height
frame_rate = config.frame_rate
return f"{pixel_height}p{frame_rate}"
# Sound
def init_audio(self) -> None:
self.includes_sound: bool = False
"""
Preps the writer for adding audio to the movie.
"""
self.includes_sound = False
def create_audio_segment(self) -> None:
"""
Creates an empty, silent, Audio Segment.
"""
self.audio_segment = AudioSegment.silent()
def add_audio_segment(
@ -260,15 +263,31 @@ class SceneFileWriter:
time: float | None = None,
gain_to_background: float | None = None,
) -> None:
"""
This method adds an audio segment from an
AudioSegment type object and suitable parameters.
Parameters
----------
new_segment
The audio segment to add
time
the timestamp at which the
sound should be added.
gain_to_background
The gain of the segment from the background.
"""
if not self.includes_sound:
self.includes_sound = True
self.create_audio_segment()
segment = self.audio_segment
curr_end = segment.duration_seconds
curr_end: float = segment.duration_seconds
if time is None:
time = curr_end
if time < 0:
raise Exception("Adding sound at timestamp < 0")
raise ValueError("Adding sound at timestamp < 0")
new_end = time + new_segment.duration_seconds
diff = new_end - curr_end
@ -288,16 +307,37 @@ class SceneFileWriter:
sound_file: str,
time: float | None = None,
gain: float | None = None,
gain_to_background: float | None = None,
**kwargs: Any,
) -> None:
"""
This method adds an audio segment from a sound file.
Parameters
----------
sound_file
The path to the sound file.
time
The timestamp at which the audio should be added.
gain
The gain of the given audio segment.
**kwargs
This method uses add_audio_segment, so any keyword arguments
used there can be referenced here.
"""
file_path = get_full_sound_file_path(sound_file)
new_segment = AudioSegment.from_file(file_path)
if gain:
new_segment = new_segment.apply_gain(gain)
self.add_audio_segment(new_segment, time, gain_to_background)
self.add_audio_segment(new_segment, time, **kwargs)
# Writers
def begin_animation(self, allow_write: bool = False, file_path=None):
def begin_animation(
self, allow_write: bool = False, file_path: str | None = None
) -> None:
"""
Used internally by manim to stream the animation to FFMPEG for
displaying or writing to a file.
@ -310,7 +350,7 @@ class SceneFileWriter:
if write_to_movie() and allow_write:
self.open_partial_movie_stream(file_path=file_path)
def end_animation(self, allow_write: bool = False):
def end_animation(self, allow_write: bool = False) -> None:
"""
Internally used by Manim to stop streaming to
FFMPEG gracefully.
@ -322,10 +362,9 @@ class SceneFileWriter:
"""
if write_to_movie() and allow_write:
self.close_partial_movie_stream()
self.num_plays += 1
def write_frame(
self, frame_or_renderer: np.ndarray | OpenGLRenderer, num_frames: int = 1
):
def write_frame(self, frame: PixelArray, num_frames: int = 1) -> None:
"""
Used internally by Manim to write a frame to
the FFMPEG input buffer.
@ -338,11 +377,6 @@ class SceneFileWriter:
The number of times to write frame.
"""
if write_to_movie():
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(...)`
@ -354,47 +388,43 @@ class SceneFileWriter:
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)
)
if is_png_format() and not config.dry_run:
image = Image.fromarray(frame)
target_dir = self.image_file_path.parent / self.image_file_path.stem
extension = self.image_file_path.suffix
self.output_image(
image,
target_dir,
extension,
config["zero_pad"],
config.zero_pad,
)
def output_image(self, image: Image.Image, target_dir, ext, zero_pad: bool):
def output_image(
self, image: Image.Image, target_dir: str | Path, ext: str, zero_pad: int
) -> None:
if zero_pad:
image.save(f"{target_dir}{str(self.frame_count).zfill(zero_pad)}{ext}")
else:
image.save(f"{target_dir}{self.frame_count}{ext}")
self.frame_count += 1
def save_final_image(self, image: np.ndarray):
def save_image(self, image: PixelArray) -> None:
"""
The name is a misnomer. This method saves the image
passed to it as an in the default image directory.
Saves an image in the default image directory.
Parameters
----------
image
The pixel array of the image to save.
"""
if config["dry_run"]:
return
if not config["output_file"]:
self.image_file_path = add_version_before_extension(self.image_file_path)
image.save(self.image_file_path)
image_processed = Image.fromarray(image)
image_processed.save(self.image_file_path)
self.print_file_ready_message(self.image_file_path)
def finish(self):
def finish(self) -> None:
"""
Finishes writing to the FFMPEG buffer or writing images
to output directory.
@ -404,8 +434,6 @@ class SceneFileWriter:
frame in the default image directory.
"""
if write_to_movie():
if hasattr(self, "writing_process"):
self.writing_process.terminate()
self.combine_to_movie()
if config.save_sections:
self.combine_to_section_videos()
@ -419,18 +447,18 @@ class SceneFileWriter:
if self.subcaptions:
self.write_subcaption_file()
def open_partial_movie_stream(self, file_path=None):
def open_partial_movie_stream(self, file_path: str | None = None) -> 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]
file_path = self.partial_movie_files[self.num_plays]
self.partial_movie_file_path = file_path
fps = config["frame_rate"]
if fps == int(fps): # fps is integer
fps = config.frame_rate
if fps == int(fps):
fps = int(fps)
partial_movie_file_codec = "libx264"
@ -463,7 +491,7 @@ class SceneFileWriter:
self.video_container = video_container
self.video_stream = stream
def close_partial_movie_stream(self):
def close_partial_movie_stream(self) -> None:
"""Close the currently opened video container.
Used internally by Manim to first flush the remaining packages
@ -476,11 +504,11 @@ class SceneFileWriter:
self.video_container.close()
logger.info(
f"Animation {self.renderer.num_plays} : Partial movie file written in %(path)s",
f"Animation {self.num_plays} : Partial movie file written in %(path)s",
{"path": f"'{self.partial_movie_file_path}'"},
)
def is_already_cached(self, hash_invocation: str):
def is_already_cached(self, hash_invocation: str) -> bool:
"""Will check if a file named with `hash_invocation` exists.
Parameters
@ -505,9 +533,9 @@ class SceneFileWriter:
self,
input_files: list[str],
output_file: Path,
create_gif=False,
includes_sound=False,
):
create_gif: bool = False,
includes_sound: bool = False,
) -> None:
file_list = self.partial_movie_directory / "partial_movie_file_list.txt"
logger.debug(
f"Partial movie files to combine ({len(input_files)} files): %(p)s",
@ -605,7 +633,7 @@ class SceneFileWriter:
partial_movies_input.close()
output_container.close()
def combine_to_movie(self):
def combine_to_movie(self) -> None:
"""Used internally by Manim to combine the separate
partial movie files that make up a Scene into a single
video file for that Scene.
@ -730,16 +758,16 @@ class SceneFileWriter:
with (self.sections_output_dir / f"{self.output_name}.json").open("w") as file:
json.dump(sections_index, file, indent=4)
def clean_cache(self):
def clean_cache(self) -> None:
"""Will clean the cache by removing the oldest partial_movie_files."""
cached_partial_movies = [
(self.partial_movie_directory / file_name)
for file_name in self.partial_movie_directory.iterdir()
if file_name != "partial_movie_file_list.txt"
]
if len(cached_partial_movies) > config["max_files_cached"]:
if len(cached_partial_movies) > config.max_files_cached:
number_files_to_delete = (
len(cached_partial_movies) - config["max_files_cached"]
len(cached_partial_movies) - config.max_files_cached
)
oldest_files_to_delete = sorted(
cached_partial_movies,
@ -748,11 +776,11 @@ class SceneFileWriter:
for file_to_delete in oldest_files_to_delete:
file_to_delete.unlink()
logger.info(
f"The partial movie directory is full (> {config['max_files_cached']} files). Therefore, manim has removed the {number_files_to_delete} oldest file(s)."
f"The partial movie directory is full (> {config.max_files_cached} files). Therefore, manim has removed the {number_files_to_delete} oldest file(s)."
" You can change this behaviour by changing max_files_cached in config.",
)
def flush_cache_directory(self):
def flush_cache_directory(self) -> None:
"""Delete all the cached partial movie files"""
cached_partial_movies = [
self.partial_movie_directory / file_name
@ -766,7 +794,7 @@ class SceneFileWriter:
{"par_dir": self.partial_movie_directory},
)
def write_subcaption_file(self):
def write_subcaption_file(self) -> None:
"""Writes the subcaption file."""
if config.output_file is None:
return
@ -774,7 +802,8 @@ class SceneFileWriter:
subcaption_file.write_text(srt.compose(self.subcaptions), encoding="utf-8")
logger.info(f"Subcaption file has been written as {subcaption_file}")
def print_file_ready_message(self, file_path):
def print_file_ready_message(self, file_path: str | Path) -> None:
"""Prints the "File Ready" message to STDOUT."""
config["output_file"] = file_path
logger.info("\nFile ready at %(file_path)s\n", {"file_path": f"'{file_path}'"})
config.output_file = str(file_path)
logger.info(f"\nFile ready at {str(file_path)!r}\n")

View file

@ -0,0 +1,31 @@
from __future__ import annotations
from typing import Protocol
from manim.typing import PixelArray
class FileWriterProtocol(Protocol):
"""Protocol for a file writer.
This is mainly useful for testing purposes, to create
a mock file writer. However, it can be used in plugins.
"""
num_plays: int
def __init__(self, scene_name: str) -> None: ...
def begin_animation(self, allow_write: bool = False) -> None: ...
def end_animation(self, allow_write: bool = False) -> None: ...
def is_already_cached(self, hash_invocation: str) -> bool: ...
def add_partial_movie_file(self, hash_animation: str) -> None: ...
def write_frame(self, frame: PixelArray) -> None: ...
def finish(self) -> None: ...
def save_image(self, image: PixelArray) -> None: ...

View file

@ -100,5 +100,5 @@ class Section:
**video_metadata,
)
def __repr__(self):
def __repr__(self) -> str:
return f"<Section '{self.name}' stored in '{self.video}'>"

413
manim/manager.py Normal file
View file

@ -0,0 +1,413 @@
from __future__ import annotations
__all__ = ["Manager"]
import contextlib
import platform
import time
from collections.abc import Iterable, Sequence
from typing import TYPE_CHECKING, Callable, Generic, TypeVar
import numpy as np
from tqdm import tqdm
from manim import config, logger
from manim.event_handler.window import WindowABC
from manim.file_writer import FileWriter
from manim.plugins import Hooks, plugins
from manim.scene.scene import Scene, SceneState
from manim.utils.exceptions import EndSceneEarlyException
from manim.utils.hashing import get_hash_from_play_call
if TYPE_CHECKING:
import numpy.typing as npt
from typing_extensions import Any
from manim.animation.protocol import AnimationProtocol
from manim.file_writer.protocols import FileWriterProtocol
from manim.renderer.renderer import RendererProtocol
Scene_co = TypeVar("Scene_co", covariant=True, bound=Scene)
class Manager(Generic[Scene_co]):
"""
The Brain of Manim
.. admonition:: Warning for Developers
Only methods of this class that are not prefixed with an
underscore (``_``) are stable. If you override any of the
``_`` methods, consider pinning your version of Manim.
Usage
-----
.. code-block:: python
class Manimation(Scene):
def construct(self):
self.play(FadeIn(Circle()))
Manager(Manimation).render()
"""
def __init__(self, scene_cls: type[Scene_co]) -> None:
# scene
self.scene: Scene_co = scene_cls(manager=self)
if not isinstance(self.scene, Scene):
raise ValueError(f"{self.scene!r} is not an instance of Scene")
self.time = 0.0
# Initialize window, if applicable
self.window = self.create_window()
# this must be done AFTER instantiating a window
self.renderer = self.create_renderer()
self.renderer.use_window()
# file writer
self.file_writer: FileWriterProtocol = self.create_file_writer()
self._write_files = config.write_to_movie
# keep these as instance methods so subclasses
# have access to everything
def create_renderer(self) -> RendererProtocol:
"""Create and return a renderer instance.
This can be overridden in subclasses (plugins), if more processing
is needed.
Returns
-------
An instance of a renderer
"""
renderer = plugins.renderer()
if config.preview:
renderer.use_window()
return renderer
def create_window(self) -> WindowABC | None:
"""Create and return a window instance.
This can be overridden in subclasses (plugins), if more
processing is needed.
Returns
-------
A window if previewing, else None
"""
return plugins.window() if config.preview else None
def create_file_writer(self) -> FileWriterProtocol:
"""Create and returna file writer instance.
This can be overridden in subclasses (plugins), if more
processing is needed.
Returns
-------
A file writer satisfying :class:`.FileWriterProtocol`
"""
return FileWriter(self.scene.get_default_scene_name())
def setup(self) -> None:
"""Set up processes and manager"""
self.scene.setup()
# these are used for making sure it feels like the correct
# amount of time has passed in the window instead of rendering
# at full speed
self.virtual_animation_start_time = 0.0
self.real_animation_start_time = time.perf_counter()
def render(self) -> None:
"""
Entry point to running a Manim class
Example
-------
.. code-block:: python
class MyScene(Scene):
def construct(self):
self.play(Create(Circle()))
with tempconfig({"preview": True}):
Manager(MyScene).render()
"""
config._warn_about_config_options()
self._render_first_pass()
self._render_second_pass()
def _render_first_pass(self) -> None:
"""
Temporarily use the normal single pass
rendering system
"""
self.setup()
with contextlib.suppress(EndSceneEarlyException):
self.scene.construct()
self.post_contruct()
self._interact()
self.tear_down()
def _render_second_pass(self) -> None:
"""
In the future, this method could be used
for two pass rendering
"""
...
def post_contruct(self) -> None:
"""Run post-construct hooks, and clean up the file writer."""
for hook in plugins.hooks[Hooks.POST_CONSTRUCT]:
hook(self)
if self.file_writer.num_plays:
self.file_writer.finish()
# otherwise no animations were played
elif config.write_to_movie or config.save_last_frame:
self.render_state(write_to_file=False)
# FIXME: for some reason the OpenGLRenderer does not give out the
# correct frame values here
frame = self.renderer.get_pixels()
# NOTE: add hooks for post-processing (e.g. gaussian blur)?
self.file_writer.save_image(frame)
self._write_files = False
def tear_down(self) -> None:
"""Tear down the scene and the window."""
self.scene.tear_down()
if config.save_last_frame:
self._update_frame(0)
if self.window is not None:
self.window.close()
self.window = None
def _interact(self) -> None:
"""Live interaction with the Window"""
if self.window is None:
return
logger.info(
"\nTips: Using the keys `d`, `f`, or `z` "
"you can interact with the scene. "
"Press `command + q` or `esc` to quit"
)
# TODO: Replace with actual dt instead
# of hardcoded dt
dt = 1 / config.frame_rate
while not self.window.is_closing:
self._update_frame(dt)
def _update_frame(self, dt: float, *, write_to_file: bool | None = None) -> None:
"""Update the current frame by ``dt``
Parameters
----------
dt : the time in between frames
write_to_file : Whether to write the result to the output stream.
Default value checks :attr:`_write_files` to see if it should be written.
"""
self.time += dt
self.scene._update_mobjects(dt)
self.scene.time = self.time
if self.window is not None:
self.window.clear()
self.render_state(write_to_file=write_to_file)
if self.window is not None:
self.window.swap_buffers()
# This recursively updates the window with dt=0 until the correct
# amount of time has passed
# TODO: do ^ better with less overhead
vt = self.time - self.virtual_animation_start_time
rt = time.perf_counter() - self.real_animation_start_time
if rt < vt:
self._update_frame(0, write_to_file=False)
def _play(self, *animations: AnimationProtocol) -> None:
"""Play a bunch of animations"""
if self.window is not None:
self.real_animation_start_time = time.perf_counter()
self.virtual_animation_start_time = self.time
self._write_hashed_movie_file(animations)
self.scene.begin_animations(animations)
self._progress_through_animations(animations)
self.scene.finish_animations(animations)
self.scene.post_play()
self.file_writer.end_animation(allow_write=self._write_files)
def _write_hashed_movie_file(self, animations: Sequence[AnimationProtocol]) -> None:
"""Compute the hash of a self.play call, and write it to a file
Essentially, a series of methods that need to be called to successfully
render a frame.
"""
if not config.write_to_movie:
return
if config.disable_caching:
if not config.disable_caching_warning:
logger.info("Caching disabled...")
hash_current_play = f"uncached_{self.file_writer.num_plays:05}"
else:
hash_current_play = get_hash_from_play_call(
self.scene,
self.scene.camera,
animations,
self.scene.mobjects,
)
if self.file_writer.is_already_cached(hash_current_play):
logger.info(
f"Animation {self.file_writer.num_plays} : Using cached data (hash : {hash_current_play})"
)
# TODO: think about how to skip
raise NotImplementedError(
"Skipping cached animations is not implemented yet"
)
self.file_writer.add_partial_movie_file(hash_current_play)
self.file_writer.begin_animation(allow_write=self._write_files)
def _create_progressbar(
self, total: float, description: str, **kwargs: Any
) -> tqdm | contextlib.nullcontext[NullProgressBar]:
"""Create a progressbar"""
if not config.write_to_movie or not config.progress_bar:
return contextlib.nullcontext(NullProgressBar())
else:
return tqdm(
total=total,
unit="frames",
desc=description % {"num": self.file_writer.num_plays},
ascii=True if platform.system() == "Windows" else None,
leave=config.progress_bar == "leave",
disable=config.progress_bar == "none",
**kwargs,
)
# TODO: change to a single wait animation
def _wait(
self,
duration: float,
*,
stop_condition: Callable[[], bool] | None = None,
) -> None:
self.scene.pre_play()
self._write_hashed_movie_file(animations=[])
update_mobjects = self.scene.should_update_mobjects()
condition = stop_condition or (lambda: False)
progression = self._calc_time_progression(duration)
with self._create_progressbar(
progression.shape[0], "Waiting %(num)d: "
) as progress:
last_t = 0
for t in progression:
dt, last_t = t - last_t, t
if update_mobjects:
self._update_frame(dt)
if condition():
progress.update(duration - t)
break
else:
# if we don't need to update mobjects
# we can just leave the mobjects on the window
# and increment the time
# but we still have to write frames
self.time += dt
self.write_frame()
progress.update(1)
self.scene.post_play()
self.file_writer.end_animation(allow_write=self._write_files)
def _progress_through_animations(
self, animations: Sequence[AnimationProtocol]
) -> None:
last_t = 0.0
run_time = self._calc_runtime(animations)
progression = self._calc_time_progression(run_time)
with self._create_progressbar(
progression.shape[0],
f"Animation %(num)d: {animations[0]}{', etc.' if len(animations) > 1 else ''}",
) as progress:
for t in progression:
dt, last_t = t - last_t, t
self.scene._update_animations(animations, t, dt)
self._update_frame(dt)
progress.update(1)
def _calc_time_progression(self, run_time: float) -> npt.NDArray[np.float64]:
"""Compute the time values at which to evaluate the animation"""
return np.arange(0, run_time, 1 / config.frame_rate)
def _calc_runtime(self, animations: Iterable[AnimationProtocol]) -> float:
"""Calculate the runtime of an iterable of animations.
.. warning::
If animations is a generator, this will consume the generator.
"""
return max(animation.get_run_time() for animation in animations)
def render_state(self, write_to_file: bool | None = None) -> None:
"""Render the current state of the scene.
Any extra kwargs are passed to :meth:`_render_frame`.
"""
state = self.scene.get_state()
self._render_frame(state, write_file=write_to_file)
def _render_frame(
self, state: SceneState, *, write_file: bool | None = None
) -> None:
"""Renders a frame based on a state, and writes it to a file.
Any extra kwargs are passed to :meth:`write_frame`.
"""
# render the frame to the window
# TODO: change self.scene.camera to state.camera
self.renderer.render(self.scene.camera, state.mobjects)
should_write = write_file if write_file is not None else self._write_files
if should_write:
self.write_frame()
def write_frame(self) -> None:
"""Take a frame from the renderer and write it in the file writer."""
frame = self.renderer.get_pixels()
self.file_writer.write_frame(frame)
class NullProgressBar:
"""Fake progressbar."""
def update(self, _: Any) -> None: ...

View file

@ -275,7 +275,7 @@ class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
else:
return super().get_start()
def get_length(self) -> np.floating:
def get_length(self) -> float:
start, end = self.get_start_and_end()
return np.linalg.norm(start - end)

View file

@ -42,8 +42,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,
@ -64,22 +64,38 @@ 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)
init_points = generate_points
def _account_for_buff(self, buff: float) -> Self:
def _account_for_buff(self, buff: float) -> Self | None:
if buff == 0:
return
#
@ -94,7 +110,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)

View file

@ -23,9 +23,8 @@ import numpy as np
from manim.constants import *
from manim.mobject.geometry.arc import ArcBetweenPoints
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
from manim.mobject.types.vectorized_mobject import VGroup
from manim.utils.color import BLUE, WHITE, ParsableManimColor
from manim.utils.iterables import adjacent_n_tuples, adjacent_pairs
from manim.utils.space_ops import angle_between_vectors, normalize, regular_vertices
@ -37,7 +36,7 @@ if TYPE_CHECKING:
from manim.utils.color import ParsableManimColor
class Polygram(VMobject, metaclass=ConvertToOpenGL):
class Polygram(OpenGLVMobject):
"""A generalized :class:`Polygon`, allowing for disconnected sets of edges.
Parameters
@ -249,17 +248,17 @@ class Polygram(VMobject, metaclass=ConvertToOpenGL):
if evenly_distribute_anchors:
# Determine the average length of each curve
nonZeroLengthArcs = [arc for arc in arcs if len(arc.points) > 4]
if len(nonZeroLengthArcs):
non_zero_length_arcs = [arc for arc in arcs if len(arc.points) > 4]
if len(non_zero_length_arcs):
totalArcLength = sum(
[arc.get_arc_length() for arc in nonZeroLengthArcs]
[arc.get_arc_length() for arc in non_zero_length_arcs]
)
totalCurveCount = (
sum([len(arc.points) for arc in nonZeroLengthArcs]) / 4
sum([len(arc.points) for arc in non_zero_length_arcs]) / 4
)
averageLengthPerCurve = totalArcLength / totalCurveCount
average_length_per_curve = totalArcLength / totalCurveCount
else:
averageLengthPerCurve = 1
average_length_per_curve = 1
# To ensure that we loop through starting with last
arcs = [arcs[-1], *arcs[:-1]]
@ -273,7 +272,7 @@ class Polygram(VMobject, metaclass=ConvertToOpenGL):
# Make sure anchors are evenly distributed, if necessary
if evenly_distribute_anchors:
line.insert_n_curves(
ceil(line.get_length() / averageLengthPerCurve)
ceil(line.get_length() / average_length_per_curve) # type: ignore
)
new_points.extend(line.points)
@ -720,7 +719,7 @@ class RoundedRectangle(Rectangle):
self.round_corners(self.corner_radius)
class Cutout(VMobject, metaclass=ConvertToOpenGL):
class Cutout(OpenGLVMobject):
"""A shape with smaller cutouts.
Parameters

View file

@ -561,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
@ -1559,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"
@ -1768,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

@ -21,11 +21,10 @@ from typing import TYPE_CHECKING, Callable, Literal
import numpy as np
from manim import config, logger
from manim.constants import *
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from .. import config, logger
from ..constants import *
from ..utils.color import (
from manim.utils.color import (
BLACK,
WHITE,
YELLOW_C,
@ -34,28 +33,27 @@ from ..utils.color import (
color_gradient,
interpolate_color,
)
from ..utils.exceptions import MultiAnimationOverrideException
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
from manim.utils.exceptions import MultiAnimationOverrideException
from manim.utils.iterables import list_update, remove_list_redundancies
from manim.utils.paths import straight_path
from manim.utils.space_ops import angle_between_vectors, normalize, rotation_matrix
if TYPE_CHECKING:
from typing_extensions import Self, TypeAlias
from manim.animation.animation import Animation
from manim.typing import (
FunctionOverride,
Image,
ManimFloat,
ManimInt,
MappingFunction,
PathFuncType,
PixelArray,
Point3D,
Point3D_Array,
Vector3D,
)
from ..animation.animation import Animation
TimeBasedUpdater: TypeAlias = Callable[["Mobject", float], object]
NonTimeBasedUpdater: TypeAlias = Callable[["Mobject"], object]
Updater: TypeAlias = NonTimeBasedUpdater | TimeBasedUpdater
@ -149,7 +147,7 @@ class Mobject:
return self._assert_valid_submobjects_internal(submobjects, Mobject)
def _assert_valid_submobjects_internal(
self, submobjects: list[Mobject], mob_class: type[Mobject]
self, submobjects: Iterable[Mobject], mob_class: type[Mobject]
) -> Self:
for i, submob in enumerate(submobjects):
if not isinstance(submob, mob_class):
@ -825,9 +823,9 @@ class Mobject:
# Displaying
def get_image(self, camera=None) -> Image:
def get_image(self, camera=None) -> PixelArray:
if camera is None:
from ..camera.cairo_camera import CairoCamera as Camera
from manim.camera.cairo_camera import CairoCamera as Camera
camera = Camera()
camera.capture_mobject(self)

View file

@ -11,7 +11,7 @@ import sys
from dataclasses import dataclass
from functools import partialmethod, wraps
from math import ceil
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Generic
import moderngl
import numpy as np
@ -50,26 +50,36 @@ if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
from typing import Any, Callable, Union
from typing_extensions import Self, TypeAlias
import numpy.typing as npt
from typing_extensions import Concatenate, ParamSpec, Self, TypeAlias
from manim.animation.animation import Animation
from manim.renderer.renderer import RendererData
from manim.typing import PathFuncType, Point3D, Point3D_Array
TimeBasedUpdater: TypeAlias = Callable[
["OpenGLMobject", float], "OpenGLMobject | None"
]
NonTimeUpdater: TypeAlias = Callable[["OpenGLMobject"], "OpenGLMobject" | None]
NonTimeUpdater: TypeAlias = Callable[["OpenGLMobject"], "OpenGLMobject | None"]
Updater: TypeAlias = Union[TimeBasedUpdater, NonTimeUpdater]
PointUpdateFunction: TypeAlias = Callable[[np.ndarray], np.ndarray]
from manim.renderer.renderer import RendererData
from manim.typing import PathFuncType
T = TypeVar("T", bound=RendererData)
_F = TypeVar("_F", bound=Callable[..., Any])
M = TypeVar("M", bound="OpenGLMobject")
T = TypeVar("T")
P = ParamSpec("P")
R = TypeVar("R", bound="RendererData")
T_co = TypeVar("T_co", covariant=True, bound="OpenGLMobject")
UNIFORM_DTYPE = np.float64
def stash_mobject_pointers(func: _F) -> _F:
def stash_mobject_pointers(
func: Callable[Concatenate[M, P], T],
) -> Callable[Concatenate[M, P], T]:
@wraps(func)
def wrapper(self, *args, **kwargs):
def wrapper(self: M, *args: P.args, **kwargs: P.kwargs):
uncopied_attrs = ["parents", "target", "saved_state"]
stash = {}
for attr in uncopied_attrs:
@ -105,7 +115,9 @@ class MobjectStatus:
points_changed: bool = False
class OpenGLMobject:
# it's generic in its renderer, which is a little bit cursed
# In the future, it should be replaced with a RendererData protocol
class OpenGLMobject(Generic[R]):
"""Mathematical Object: base class for objects that can be displayed on screen.
Attributes
@ -166,7 +178,8 @@ class OpenGLMobject:
self.data: dict[str, np.ndarray] = {}
self.uniforms: dict[str, float | np.ndarray] = {}
self.renderer_data: T | None = None
# TODO replace with protocol
self.renderer_data: R | None = None
self.status = MobjectStatus()
self.init_data()
@ -304,9 +317,9 @@ class OpenGLMobject:
# Typically implemented in subclass, unless purposefully left blank
pass
def set_data(self, data):
for key in data:
self.data[key] = data[key]
def set_data(self, data: dict[str, Any]):
for key, value in data.items():
self.data[key] = value
return self
def set_uniforms(self, uniforms):
@ -316,8 +329,12 @@ class OpenGLMobject:
self.uniforms[key] = value
return self
# https://github.com/python/typing/issues/802
# so we hack around it by doing | Self
# but this causes issues in Scene.play which only
# accepts _AnimationBuilder/Animations, not Mobjects
@property
def animate(self) -> _AnimationBuilder:
def animate(self) -> _AnimationBuilder[Self] | Self:
"""Used to animate the application of a method.
.. warning::
@ -669,13 +686,15 @@ class OpenGLMobject:
parent.refresh_bounding_box()
return self
def are_points_touching(self, points, buff: float = 0) -> np.ndarray:
def are_points_touching(
self, points: Point3D_Array, buff: float = 0
) -> npt.NDArray[bool]:
bb = self.get_bounding_box()
mins = bb[0] - buff
maxs = bb[2] + buff
return ((points >= mins) * (points <= maxs)).all(1)
def is_point_touching(self, point, buff=MED_SMALL_BUFF):
def is_point_touching(self, point: Point3D, buff: float = MED_SMALL_BUFF) -> bool:
return self.are_points_touching(np.array(point, ndmin=2), buff)[0]
def is_touching(self, mobject: OpenGLMobject, buff: float = 1e-2) -> bool:
@ -709,13 +728,22 @@ class OpenGLMobject:
def split(self) -> list[OpenGLMobject]:
return self.submobjects
def assemble_family(self) -> Self:
def note_changed_family(self) -> Self:
"""Updates bounding boxes and updater statuses.
This used to be called ``assemble_family``
.. warning::
Remove the above remark about ``assemble_family`` before experimental
is merged, it's a note to MrDiver and other devs
"""
sub_families = (sm.get_family() for sm in self.submobjects)
self.family = [self, *uniq_chain(*sub_families)]
self.refresh_has_updater_status()
self.refresh_bounding_box()
for parent in self.parents:
parent.assemble_family()
parent.note_changed_family()
return self
def get_family(self, recurse=True) -> list[OpenGLMobject]:
@ -817,7 +845,7 @@ class OpenGLMobject:
self.submobjects.append(mobject)
if self not in mobject.parents:
mobject.parents.append(self)
self.assemble_family()
self.note_changed_family()
return self
def remove(self, *mobjects: OpenGLMobject, reassemble: bool = True) -> Self:
@ -848,7 +876,7 @@ class OpenGLMobject:
if self in mobject.parents:
mobject.parents.remove(self)
if reassemble:
self.assemble_family()
self.note_changed_family()
return self
def add_to_back(self, *mobjects: OpenGLMobject) -> Self:
@ -905,7 +933,7 @@ class OpenGLMobject:
old_submob.parents.remove(self)
self.submobjects[index] = new_submob
new_submob.parents.append(self)
self.assemble_family()
self.note_changed_family()
return self
def insert_submobject(self, index: int, mobject: OpenGLMobject):
@ -934,7 +962,7 @@ class OpenGLMobject:
if mobject not in self.submobjects:
self.submobjects.insert(index, mobject)
self.assemble_family()
self.note_changed_family()
return self
def set_submobjects(self, submobject_list: list[OpenGLMobject]):
@ -1316,7 +1344,7 @@ class OpenGLMobject:
for submob in self.submobjects:
submob.shuffle(recurse=True)
random.shuffle(self.submobjects)
self.assemble_family()
self.note_changed_family()
return self
def reverse_submobjects(self, recursive=False):
@ -1344,7 +1372,7 @@ class OpenGLMobject:
if recursive:
for submob in self.submobjects:
submob.reverse_submobjects(recursive=True)
self.assemble_family()
self.note_changed_family()
# Copying
@ -1512,6 +1540,7 @@ class OpenGLMobject:
def init_updaters(self) -> None:
self.time_based_updaters: list[TimeBasedUpdater] = []
self.non_time_updaters: list[NonTimeUpdater] = []
# so that we don't have to refind updaters
self.has_updaters: bool = False
self.updating_suspended: bool = False
@ -2635,6 +2664,16 @@ class OpenGLMobject:
# Alignment
def is_aligned_with(self, mobject: OpenGLMobject) -> bool:
return (
len(self.data) == len(mobject.data)
and len(self.submobjects) == len(mobject.submobjects)
and all(
sm1.is_aligned_with(sm2)
for sm1, sm2 in zip(self.submobjects, mobject.submobjects)
)
)
def align_data_and_family(self, mobject):
self.align_family(mobject)
self.align_data(mobject)
@ -3152,8 +3191,8 @@ class OpenGLPoint(OpenGLMobject):
self.set_points(np.array(new_loc, ndmin=2, dtype=float))
class _AnimationBuilder:
def __init__(self, mobject):
class _AnimationBuilder(Generic[T_co]):
def __init__(self, mobject: T_co):
self.mobject = mobject
self.mobject.generate_target()
@ -3165,7 +3204,7 @@ class _AnimationBuilder:
self.cannot_pass_args = False
self.anim_args = {}
def __call__(self, **kwargs) -> _AnimationBuilder:
def __call__(self, **kwargs) -> _AnimationBuilder[T_co]:
if self.cannot_pass_args:
raise ValueError(
"Animation arguments must be passed before accessing methods and can only be passed once",
@ -3176,7 +3215,7 @@ class _AnimationBuilder:
return self
def __getattr__(self, method_name):
def __getattr__(self, method_name: str):
method = getattr(self.mobject.target, method_name)
has_overridden_animation = hasattr(method, "_override_animate")
@ -3204,7 +3243,7 @@ class _AnimationBuilder:
return update_target
def build(self):
def build(self) -> Animation:
from manim.animation.transform import _MethodAnimation
if self.overridden_animation:

View file

@ -16,15 +16,15 @@ from manim.mobject.opengl.opengl_mobject import (
)
from manim.utils.bezier import (
bezier,
bezier_remap,
get_quadratic_approximation_of_cubic,
get_smooth_cubic_bezier_handle_points,
get_smooth_quadratic_bezier_handle_points,
integer_interpolate,
interpolate,
inverse_interpolate,
partial_quadratic_bezier_points,
partial_bezier_points,
proportions_along_bezier_curve_for_point,
quadratic_bezier_remap,
)
from manim.utils.color import *
from manim.utils.deprecation import deprecated
@ -94,9 +94,11 @@ class OpenGLVMobject(OpenGLMobject):
fill_color = color
if stroke_color is None:
stroke_color = color
self.fill_color: Iterable[ManimColor] = listify(ManimColor.parse(fill_color))
self.fill_color: Sequence[ManimColor] = listify(ManimColor.parse(fill_color))
self.set_fill(opacity=fill_opacity)
self.stroke_color = listify(ManimColor.parse(stroke_color))
self.stroke_color: Sequence[ManimColor] = listify(
ManimColor.parse(stroke_color)
)
self.set_stroke(opacity=stroke_opacity)
if stroke_width is None:
stroke_width = DEFAULT_STROKE_WIDTH
@ -115,7 +117,7 @@ class OpenGLVMobject(OpenGLMobject):
super().__init__(**kwargs)
# self.refresh_unit_normal()
def get_group_class(self):
def get_group_class(self) -> type[OpenGLVGroup]: # type: ignore
return OpenGLVGroup
@staticmethod
@ -207,7 +209,7 @@ class OpenGLVMobject(OpenGLMobject):
def set_fill(
self,
color: ParsableManimColor | Iterable[ParsableManimColor] | None = None,
color: ParsableManimColor | Sequence[ParsableManimColor] | None = None,
opacity: float | None = None,
recurse: bool = True,
) -> Self:
@ -276,7 +278,7 @@ class OpenGLVMobject(OpenGLMobject):
def set_backstroke(
self,
color: Color | Iterable[Color] | None = None,
color: ManimColor | Iterable[ManimColor] | None = None,
width: float | Iterable[float] = 3,
background: bool = True,
) -> Self:
@ -292,11 +294,9 @@ class OpenGLVMobject(OpenGLMobject):
def set_style(
self,
fill_color: ParsableManimColor | Iterable[ParsableManimColor] | None = None,
fill_opacity: float | Iterable[float] | None = None,
fill_rgba: np.ndarray | None = None,
fill_opacity: float | None = None,
stroke_color: ParsableManimColor | Iterable[ParsableManimColor] | None = None,
stroke_opacity: float | Iterable[float] | None = None,
stroke_rgba: np.ndarray | None = None,
stroke_width: float | Iterable[float] | None = None,
stroke_background: bool = True,
reflectiveness: float | None = None,
@ -564,7 +564,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:])
],
)
@ -639,7 +639,8 @@ class OpenGLVMobject(OpenGLMobject):
elif mode == "jagged":
new_subpath[1::nppc] = 0.5 * (anchors[:-1] + anchors[1:])
submob.append_points(new_subpath)
submob.refresh_triangulation()
# TODO: not implemented
# submob.refresh_triangulation()
return self
def make_smooth(self):
@ -1197,9 +1198,9 @@ class OpenGLVMobject(OpenGLMobject):
return normal
# Alignment
def align_points(self, vmobject):
def align_points(self, vmobject: OpenGLVMobject) -> Self:
# TODO: This shortcut can be a bit over eager. What if they have the same length, but different subpath lengths?
if self.get_num_points() == len(vmobject.points):
if self.get_num_points() == vmobject.get_num_points():
return
for mob in self, vmobject:
@ -1273,14 +1274,13 @@ class OpenGLVMobject(OpenGLMobject):
return self
def insert_n_curves_to_point_list(self, n: int, points: np.ndarray) -> np.ndarray:
"""Given an array of k points defining a bezier curves
(anchors and handles), returns points defining exactly
k + n bezier curves.
"""Given an array of 3k points defining a Bézier curve (anchors and
handles), return 3(k+n) points defining exactly k + n Bézier curves.
Parameters
----------
n
Number of desired curves.
Number of desired curves to insert.
points
Starting points.
@ -1289,34 +1289,16 @@ class OpenGLVMobject(OpenGLMobject):
np.ndarray
Points generated.
"""
nppc = self.n_points_per_curve
if len(points) == 1:
nppc = self.n_points_per_curve
return np.repeat(points, nppc * n, 0)
bezier_groups = self.get_bezier_tuples_from_points(points)
norms = np.array([get_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_points = []
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 += partial_quadratic_bezier_points(group, a1, a2)
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_color(self, mobject1, mobject2, alpha):
attrs = [
@ -1396,7 +1378,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,
@ -1404,24 +1386,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
@ -1448,21 +1424,6 @@ class OpenGLVMobject(OpenGLMobject):
# Related to triangulation
def set_points(self, points):
super().set_points(points)
return self
def append_points(self, points):
return super().append_points(points)
def reverse_points(self):
return super().reverse_points()
def set_data(self, data):
super().set_data(data)
return self
# TODO, how to be smart about tangents here?
def apply_function(self, function, make_smooth=False, **kwargs):
super().apply_function(function, **kwargs)
if self.make_smooth_after_applying_functions or make_smooth:
@ -1566,14 +1527,14 @@ class OpenGLVGroup(OpenGLVMobject):
return self
@deprecated(
since="0.18.2",
until="0.19.0",
since="0.20.0",
until="0.21.0",
message="OpenGL has no concept of z_index. Use set_z instead",
)
def set_z_index(self, z: float) -> Self:
return self.set_z(z)
def add(self, *vmobjects: OpenGLVMobject): # type: ignore
def add(self, *vmobjects: OpenGLVMobject):
"""Checks if all passed elements are an instance of OpenGLVMobject and then add them to submobjects
Parameters

View file

@ -5,6 +5,7 @@ from __future__ import annotations
__all__ = ["Brace", "BraceLabel", "ArcBrace", "BraceText", "BraceBetweenPoints"]
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

@ -30,9 +30,9 @@ from manim.constants import *
from manim.mobject.geometry.arc import Circle
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.opengl.opengl_vectorized_mobject import OpenGLVMobject
from manim.mobject.types.vectorized_mobject import VectorizedPoint, VGroup
from manim.utils.color import (
ManimColor,
ParsableManimColor,
@ -41,12 +41,12 @@ from manim.utils.iterables import tuplify
from manim.utils.space_ops import normalize, perpendicular_bisector, z_to_vector
class ThreeDVMobject(VMobject, metaclass=ConvertToOpenGL):
class ThreeDVMobject(OpenGLVMobject):
def __init__(self, shade_in_3d: bool = True, **kwargs):
super().__init__(shade_in_3d=shade_in_3d, **kwargs)
class Surface(VGroup, metaclass=ConvertToOpenGL):
class Surface(VGroup):
"""Creates a Parametric Surface using a checkerboard pattern.
Parameters
@ -613,17 +613,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()
@ -653,6 +654,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
@ -707,6 +714,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,
@ -1149,14 +1165,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

@ -23,7 +23,7 @@ from ...utils.color import (
)
from ...utils.iterables import stretch_array_to_length
__all__ = ["PMobject", "Mobject1D", "Mobject2D", "PGroup", "PointCloudDot", "Point"]
__all__ = ["PMobject", "Mobject1D", "Mobject2D", "PGroup", "PointCloudDot"]
class PMobject(Mobject, metaclass=ConvertToOpenGL):
@ -199,7 +199,7 @@ class PMobject(Mobject, metaclass=ConvertToOpenGL):
def get_point_mobject(self, center=None):
if center is None:
center = self.get_center()
return Point(center)
return PMobject().set_points([center])
def interpolate_color(self, mobject1, mobject2, alpha):
self.rgbas = interpolate(mobject1.rgbas, mobject2.rgbas, alpha)
@ -348,37 +348,3 @@ class PointCloudDot(Mobject1D):
)
],
)
class Point(PMobject):
"""A mobject representing a point.
Examples
--------
.. manim:: ExamplePoint
:save_last_frame:
class ExamplePoint(Scene):
def construct(self):
colorList = [RED, GREEN, BLUE, YELLOW]
for i in range(200):
point = Point(location=[0.63 * np.random.randint(-4, 4), 0.37 * np.random.randint(-4, 4), 0], color=np.random.choice(colorList))
self.add(point)
for i in range(200):
point = Point(location=[0.37 * np.random.randint(-4, 4), 0.63 * np.random.randint(-4, 4), 0], color=np.random.choice(colorList))
self.add(point)
self.add(point)
"""
def __init__(self, location=ORIGIN, color=BLACK, **kwargs):
self.location = location
super().__init__(color=color, **kwargs)
def init_points(self):
self.reset_points()
self.generate_points()
self.set_points([self.location])
def generate_points(self):
self.add_points([self.location])

View file

@ -32,7 +32,8 @@ from ...constants import *
from ...mobject.mobject import Mobject
from ...utils.bezier import (
bezier,
get_smooth_handle_points,
bezier_remap,
get_smooth_cubic_bezier_handle_points,
integer_interpolate,
interpolate,
partial_bezier_points,
@ -46,6 +47,7 @@ from ...utils.space_ops import rotate_vector, shoelace_direction
if TYPE_CHECKING:
from typing_extensions import Self
# TODO
# - Change cubic curve groups to have 4 points instead of 3
# - Change sub_path idea accordingly
@ -923,8 +925,8 @@ 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)
elif mode == "jagged":
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]
a2 = anchors[1:]
@ -1577,40 +1579,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):
@ -1657,61 +1630,92 @@ class VMobject(Mobject):
vmobject: VMobject,
a: float,
b: float,
):
"""Given two bounds a and b, transforms the points of the self vmobject into the points of the vmobject
passed as parameter with respect to the bounds. Points here stand for control points of the bezier curves (anchors and handles)
) -> Self:
"""Given a 2nd :class:`.VMobject` ``vmobject``, a lower bound ``a`` and
an upper bound ``b``, modify this :class:`.VMobject`'s points to
match the portion of the Bézier spline described by ``vmobject.points``
with the parameter ``t`` between ``a`` and ``b``.
Parameters
----------
vmobject
The vmobject that will serve as a model.
The :class:`.VMobject` that will serve as a model.
a
upper-bound.
The lower bound for ``t``.
b
lower-bound
The upper bound for ``t``
Returns
-------
:class:`VMobject`
``self``
:class:`.VMobject`
The :class:`.VMobject` itself, after the transformation.
Raises
------
TypeError
If ``vmobject`` is not an instance of :class:`VMobject`.
"""
assert isinstance(vmobject, VMobject)
if not isinstance(vmobject, VMobject):
raise TypeError(
f"Expected a VMobject, got value {vmobject} of type "
f"{type(vmobject).__name__}."
)
# Partial curve includes three portions:
# - A middle section, which matches the curve exactly
# - A start, which is some ending portion of an inner cubic
# - An end, which is the starting portion of a later inner cubic
# - A middle section, which matches the curve exactly.
# - A start, which is some ending portion of an inner cubic.
# - An end, which is the starting portion of a later inner cubic.
if a <= 0 and b >= 1:
self.set_points(vmobject.points)
return self
bezier_quads = vmobject.get_cubic_bezier_tuples()
num_cubics = len(bezier_quads)
# The following two lines will compute which bezier curves of the given mobject need to be processed.
# The residue basically indicates de proportion of the selected bezier curve that have to be selected.
# Ex : if lower_index is 3, and lower_residue is 0.4, then the algorithm will append to the points 0.4 of the third bezier curve
lower_index, lower_residue = integer_interpolate(0, num_cubics, a)
upper_index, upper_residue = integer_interpolate(0, num_cubics, b)
self.clear_points()
if num_cubics == 0:
num_curves = vmobject.get_num_curves()
if num_curves == 0:
self.clear_points()
return self
# The following two lines will compute which Bézier curves of the given Mobject must be processed.
# The residue indicates the proportion of the selected Bézier curve which must be selected.
#
# Example: if num_curves is 10, a is 0.34 and b is 0.78, then:
# - lower_index is 3 and lower_residue is 0.4, which means the algorithm will look at the 3rd Bézier
# and select its part which ranges from t=0.4 to t=1.
# - upper_index is 7 and upper_residue is 0.8, which means the algorithm will look at the 7th Bézier
# and select its part which ranges from t=0 to t=0.8.
lower_index, lower_residue = integer_interpolate(0, num_curves, a)
upper_index, upper_residue = integer_interpolate(0, num_curves, b)
nppc = self.n_points_per_curve
# If both indices coincide, get a part of a single Bézier curve.
if lower_index == upper_index:
self.append_points(
partial_bezier_points(
bezier_quads[lower_index],
lower_residue,
upper_residue,
),
# Look at the "lower_index"-th Bézier curve and select its part from
# t=lower_residue to t=upper_residue.
self.points = partial_bezier_points(
vmobject.points[nppc * lower_index : nppc * (lower_index + 1)],
lower_residue,
upper_residue,
)
else:
self.append_points(
partial_bezier_points(bezier_quads[lower_index], lower_residue, 1),
# Allocate space for (upper_index-lower_index+1) Bézier curves.
self.points = np.empty((nppc * (upper_index - lower_index + 1), self.dim))
# Look at the "lower_index"-th Bezier curve and select its part from
# t=lower_residue to t=1. This is the first curve in self.points.
self.points[:nppc] = partial_bezier_points(
vmobject.points[nppc * lower_index : nppc * (lower_index + 1)],
lower_residue,
1,
)
for quad in bezier_quads[lower_index + 1 : upper_index]:
self.append_points(quad)
self.append_points(
partial_bezier_points(bezier_quads[upper_index], 0, upper_residue),
# If there are more curves between the "lower_index"-th and the
# "upper_index"-th Béziers, add them all to self.points.
self.points[nppc:-nppc] = vmobject.points[
nppc * (lower_index + 1) : nppc * upper_index
]
# Look at the "upper_index"-th Bézier curve and select its part from
# t=0 to t=upper_residue. This is the last curve in self.points.
self.points[-nppc:] = partial_bezier_points(
vmobject.points[nppc * upper_index : nppc * (upper_index + 1)],
0,
upper_residue,
)
return self
def get_subcurve(self, a: float, b: float) -> VMobject:
@ -1882,7 +1886,7 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
f"submobject{'s' if len(self.submobjects) > 0 else ''}"
)
def add(self, *vmobjects: VMobject):
def add(self, *vmobjects: OpenGLVMobject):
"""Checks if all passed elements are an instance of VMobject and then add them to submobjects
Parameters
@ -1930,8 +1934,7 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
(gr-circle_red).animate.shift(RIGHT)
)
"""
if not all(isinstance(m, (VMobject, OpenGLVMobject)) for m in vmobjects):
raise TypeError("All submobjects must be of type VMobject")
# leave here because the docstring is useful
return super().add(*vmobjects)
def __add__(self, vmobject):
@ -2146,7 +2149,7 @@ class VDict(VMobject, metaclass=ConvertToOpenGL):
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}' is not present in the VDict")
super().remove(self.submob_dict[key])
del self.submob_dict[key]
return self

View file

@ -1,17 +0,0 @@
from __future__ import annotations
try:
from dearpygui import dearpygui as dpg
except ImportError:
pass
from manim.mobject.opengl.dot_cloud import *
from manim.mobject.opengl.opengl_image_mobject import *
from manim.mobject.opengl.opengl_mobject import *
from manim.mobject.opengl.opengl_point_cloud_mobject import *
from manim.mobject.opengl.opengl_surface import *
from manim.mobject.opengl.opengl_three_dimensions import *
from manim.mobject.opengl.opengl_vectorized_mobject import *
from ..utils.opengl import *

View file

@ -2,9 +2,12 @@ from __future__ import annotations
from manim import config, logger
from .plugin_config import Hooks, plugins
from .plugins_flags import get_plugins, list_plugins
__all__ = [
"plugins",
"Hooks",
"get_plugins",
"list_plugins",
]

View file

@ -0,0 +1,98 @@
from __future__ import annotations
from collections.abc import Callable
from enum import Enum
from typing import TYPE_CHECKING
from pydantic import BaseModel
from manim.event_handler.window import WindowABC
from manim.renderer.opengl_renderer import OpenGLRenderer
from manim.renderer.opengl_renderer_window import Window
from manim.renderer.renderer import RendererProtocol
if TYPE_CHECKING:
from typing_extensions import TypeAlias
from manim.manager import Manager
HookFunction: TypeAlias = Callable[[Manager], object]
__all__ = (
"plugins",
"Hooks",
)
class Hooks(Enum):
POST_CONSTRUCT = "post_construct"
class PluginConfig(BaseModel):
"""Plugin abilities that should be customizable by the user.
Parameters
----------
renderer : The renderer class to use for rendering scenes.
window: The window class to use for displaying the scene.
Examples
--------
.. code-block:: pycon
>>> from manim import plugins
>>> plugins.renderer.__name__
'OpenGLRenderer'
>>> class MyRenderer(OpenGLRenderer):
... '''My custom renderer
...
... All this actually has to do is implement
... the RendererProtocol.
... '''
>>> plugins.renderer = MyRenderer
>>> plugins.renderer.__name__
'MyRenderer'
>>> plugins.renderer = 3
Traceback (most recent call last):
...
pydantic_core._pydantic_core.ValidationError: 1 validation error for PluginConfig
renderer
Input should be a subclass of RendererProtocol [type=is_subclass_of, input_value=3, input_type=int]
For further information visit https://errors.pydantic.dev/2.8/v/is_subclass_of
"""
class Config:
# runtime check Protocols (must be runtime_checkable Protocols)
allow_arbitrary_types = True
# validate setting attributes
validate_assignment = True
extra = "forbid"
renderer: type[RendererProtocol]
window: type[WindowABC]
# not included in pydantic because Manager is undefined
# due to circular imports and __future__.annotations
# instead we do validation manually via :meth:`.register`
_hooks: dict[Hooks, list[HookFunction]] = {hook: [] for hook in Hooks}
@property
def hooks(self) -> dict[Hooks, list[HookFunction]]:
return self._hooks
def register(self, hooks: dict[Hooks, list[HookFunction]]) -> None:
"""Register hooks to run at specific points in the program."""
for hook, functions in hooks.items():
if not all(callable(func) for func in functions):
raise ValueError("All hooks must be callables!")
if not isinstance(hook, Hooks):
raise ValueError(
f"Unknown hook type {hook}, must be an instance of enum {Hooks}"
)
self._hooks[hook].extend(functions)
plugins = PluginConfig(renderer=OpenGLRenderer, window=Window)

View file

@ -2,14 +2,9 @@
from __future__ import annotations
import sys
from importlib.metadata import entry_points
from typing import Any
if sys.version_info < (3, 10):
from importlib_metadata import entry_points
else:
from importlib.metadata import entry_points
from manim import console
__all__ = ["list_plugins"]

View file

@ -4,13 +4,12 @@ import typing
import numpy as np
from manim import config, logger
from manim.camera.camera import Camera
from manim.mobject.mobject import Mobject
from manim.utils.exceptions import EndSceneEarlyException
from manim.utils.hashing import get_hash_from_play_call
from .. import config, logger
from ..mobject.mobject import Mobject
from ..scene.scene_file_writer import SceneFileWriter
from ..utils.exceptions import EndSceneEarlyException
from ..utils.iterables import list_update
from manim.utils.iterables import list_update
if typing.TYPE_CHECKING:
import types
@ -32,7 +31,7 @@ class CairoRenderer:
def __init__(
self,
file_writer_class=SceneFileWriter,
file_writer_class=None,
camera_class=None,
skip_animations=False,
**kwargs,

View file

@ -1,467 +0,0 @@
from __future__ import annotations
import os
import platform
import shutil
import subprocess as sp
import sys
from pathlib import Path
from typing import TYPE_CHECKING
import numpy as np
from pydub import AudioSegment
from tqdm import tqdm as ProgressDisplay
from manim import config
from manim._config import logger as log
from manim.utils.file_ops import (
add_extension_if_not_present,
get_sorted_integer_files,
guarantee_existence,
)
from manim.utils.sounds import get_full_sound_file_path
if TYPE_CHECKING:
from PIL.Image import Image
class FileWriter:
def __init__(
self,
file_name: str,
write_to_movie: bool = False,
break_into_partial_movies: bool = False,
save_pngs: bool = False, # TODO, this currently does nothing
png_mode: str = "RGBA",
save_last_frame: bool = False,
movie_file_extension: str = ".mp4",
# What python file is generating this scene
input_file_path: str = "",
# Where should this be written
output_directory: str | None = None,
open_file_upon_completion: bool = False,
show_file_location_upon_completion: bool = False,
quiet: bool = False,
total_frames: int = 0,
progress_description_len: int = 40,
):
self.frames: list[Image] = []
self.write_to_movie = write_to_movie
self.break_into_partial_movies = break_into_partial_movies
self.save_pngs = save_pngs
self.png_mode = png_mode
self.save_last_frame = save_last_frame
self.movie_file_extension = movie_file_extension
self.input_file_path = input_file_path
self.output_directory = output_directory
self.file_name = file_name
self.open_file_upon_completion = open_file_upon_completion
self.show_file_location_upon_completion = show_file_location_upon_completion
self.quiet = quiet
self.total_frames = total_frames
self.progress_description_len = progress_description_len
# State during file writing
self.writing_process: sp.Popen | None = None
self.progress_display: ProgressDisplay | None = None
self.ended_with_interrupt: bool = False
self.init_output_directories()
self.init_audio()
# Output directories and files
def init_output_directories(self) -> None:
out_dir = self.output_directory or ""
scene_name = Path(self.file_name)
if self.save_last_frame:
image_dir = guarantee_existence(Path(out_dir) / "images")
image_file = add_extension_if_not_present(scene_name, ".png")
self.image_file_path = Path(image_dir) / image_file
if self.write_to_movie:
movie_dir = guarantee_existence(Path(out_dir) / "videos")
movie_file = add_extension_if_not_present(
scene_name, self.movie_file_extension
)
self.movie_file_path = Path(movie_dir) / movie_file
if self.break_into_partial_movies:
self.partial_movie_directory = guarantee_existence(
Path(movie_dir) / "partial_movie_files" / scene_name,
)
# A place to save mobjects
self.saved_mobject_directory = Path(out_dir) / "mobjects" / scene_name
def add_frames(self, *frames: Image) -> None:
self.frames.extend(frames)
def get_default_module_directory(self) -> str:
path, _ = os.path.splitext(self.input_file_path)
path = path.removeprefix("_")
return path
# Directory getters
def get_image_file_path(self) -> str:
return self.image_file_path
# Sound
def init_audio(self) -> None:
self.includes_sound: bool = False
def create_audio_segment(self) -> None:
self.audio_segment = AudioSegment.silent()
def add_audio_segment(
self,
new_segment: AudioSegment,
time: float | None = None,
gain_to_background: float | None = None,
) -> None:
if not self.includes_sound:
self.includes_sound = True
self.create_audio_segment()
segment = self.audio_segment
curr_end = segment.duration_seconds
if time is None:
time = curr_end
if time < 0:
raise Exception("Adding sound at timestamp < 0")
new_end = time + new_segment.duration_seconds
diff = new_end - curr_end
if diff > 0:
segment = segment.append(
AudioSegment.silent(int(np.ceil(diff * 1000))),
crossfade=0,
)
self.audio_segment = segment.overlay(
new_segment,
position=int(1000 * time),
gain_during_overlay=gain_to_background,
)
def add_sound(
self,
sound_file: str,
time: float | None = None,
gain: float | None = None,
gain_to_background: float | None = None,
) -> None:
file_path = get_full_sound_file_path(sound_file)
new_segment = AudioSegment.from_file(file_path)
if gain:
new_segment = new_segment.apply_gain(gain)
self.add_audio_segment(new_segment, time, gain_to_background)
# Writers
def begin(self) -> None:
if not self.break_into_partial_movies and self.write_to_movie:
self.open_movie_pipe(self.get_movie_file_path())
def begin_animation(self) -> None:
if self.break_into_partial_movies and self.write_to_movie:
# self.open_movie_pipe(self.get_next_partial_movie_path())
...
def end_animation(self) -> None:
if self.break_into_partial_movies and self.write_to_movie:
# self.close_movie_pipe()
...
def finish(self) -> None:
if self.write_to_movie:
if self.break_into_partial_movies:
self.combine_movie_files()
else:
self.close_movie_pipe()
if self.includes_sound:
self.add_sound_to_video()
self.print_file_ready_message(self.get_movie_file_path())
if self.save_last_frame:
self.save_final_image(self.scene.get_image())
if self.should_open_file():
self.open_file()
def open_movie_pipe(self, file_path: str) -> None:
stem, ext = os.path.splitext(file_path)
self.final_file_path = file_path
self.temp_file_path = stem + "_temp" + ext
fps = self.scene.camera.fps
width, height = self.scene.camera.get_pixel_shape()
command = [
config.ffmpeg_executable,
"-y", # overwrite output file if it exists
"-f",
"rawvideo",
"-s",
f"{width}x{height}", # size of one frame
"-pix_fmt",
"rgba",
"-r",
str(fps), # frames per second
"-i",
"-", # The input comes from a pipe
"-vf",
"vflip",
"-an", # Tells FFMPEG not to expect any audio
"-loglevel",
"error",
]
if self.movie_file_extension == ".mov":
# This is if the background of the exported
# video should be transparent.
command += [
"-vcodec",
"prores_ks",
]
elif self.movie_file_extension != ".gif":
command += [
"-vcodec",
"libx264",
"-pix_fmt",
"yuv420p",
]
command += [self.temp_file_path]
self.writing_process = sp.Popen(command, stdin=sp.PIPE)
if self.total_frames > 0 and not self.quiet:
self.progress_display = ProgressDisplay(
range(self.total_frames),
# bar_format="{l_bar}{bar}|{n_fmt}/{total_fmt}",
leave=False,
ascii=True if platform.system() == "Windows" else None,
dynamic_ncols=True,
)
self.set_progress_display_description()
def has_progress_display(self):
return self.progress_display is not None
def set_progress_display_description(
self, file: str = "", sub_desc: str = ""
) -> None:
if self.progress_display is None:
return
desc_len = self.progress_description_len
if not file:
file = os.path.split(self.get_movie_file_path())[1]
full_desc = f"{file} {sub_desc}"
if len(full_desc) > desc_len:
full_desc = full_desc[: desc_len - 3] + "..."
else:
full_desc += " " * (desc_len - len(full_desc))
self.progress_display.set_description(full_desc)
def write_frame(self, frame: Image) -> None:
if self.write_to_movie:
self.writing_process.stdin.write(frame.tobytes("utf-8"))
if self.progress_display is not None:
self.progress_display.update()
def close_movie_pipe(self) -> None:
self.writing_process.stdin.close()
self.writing_process.wait()
self.writing_process.terminate()
if self.progress_display is not None:
self.progress_display.close()
if not self.ended_with_interrupt:
shutil.move(self.temp_file_path, self.final_file_path)
else:
self.movie_file_path = self.temp_file_path
def combine_movie_files(self) -> None:
kwargs = {
"remove_non_integer_files": True,
"extension": self.movie_file_extension,
}
if self.scene.start_at_animation_number is not None:
kwargs["min_index"] = self.scene.start_at_animation_number
if self.scene.end_at_animation_number is not None:
kwargs["max_index"] = self.scene.end_at_animation_number
else:
kwargs["remove_indices_greater_than"] = self.scene.num_plays - 1
partial_movie_files = get_sorted_integer_files(
self.partial_movie_directory, **kwargs
)
if len(partial_movie_files) == 0:
log.warning("No animations in this scene")
return
# Write a file partial_file_list.txt containing all
# partial movie files
file_list = Path(self.partial_movie_directory) / "partial_movie_file_list.txt"
with open(file_list, "w") as fp:
for pf_path in partial_movie_files:
if os.name == "nt":
pf_path = pf_path.replace("\\", "/")
fp.write(f"file '{pf_path}'\n")
movie_file_path = self.get_movie_file_path()
commands = [
config.ffmpeg_executable,
"-y", # overwrite output file if it exists
"-f",
"concat",
"-safe",
"0",
"-i",
file_list,
"-loglevel",
"error",
"-c",
"copy",
movie_file_path,
]
if not self.includes_sound:
commands.insert(-1, "-an")
combine_process = sp.Popen(commands)
combine_process.wait()
def add_sound_to_video(self) -> None:
movie_file_path = self.get_movie_file_path()
stem, ext = os.path.splitext(movie_file_path)
sound_file_path = stem + ".wav"
# Makes sure sound file length will match video file
self.add_audio_segment(AudioSegment.silent(0))
self.audio_segment.export(
sound_file_path,
bitrate="312k",
)
temp_file_path = stem + "_temp" + ext
commands = [
config.ffmpeg_executable,
"-i",
movie_file_path,
"-i",
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",
"error",
# "-shortest",
temp_file_path,
]
sp.call(commands)
shutil.move(temp_file_path, movie_file_path)
os.remove(sound_file_path)
def save_final_image(self, image: Image) -> None:
file_path = self.get_image_file_path()
image.save(file_path)
self.print_file_ready_message(file_path)
def print_file_ready_message(self, file_path: str) -> None:
if not self.quiet:
log.info(f"File ready at {file_path}")
def should_open_file(self) -> bool:
return any(
(
self.show_file_location_upon_completion,
self.open_file_upon_completion,
)
)
def combine_to_section_videos(self) -> None:
"""Concatenate partial movie files for each section."""
self.finish_last_section()
sections_index: list[dict[str, Any]] = []
for section in self.sections:
# only if section does want to be saved
if section.video is not None:
logger.info(f"Combining partial files for section '{section.name}'")
self.combine_files(
section.get_clean_partial_movie_files(),
self.sections_output_dir / section.video,
)
sections_index.append(section.get_dict(self.sections_output_dir))
with (self.sections_output_dir / f"{self.output_name}.json").open("w") as file:
json.dump(sections_index, file, indent=4)
def clean_cache(self):
"""Will clean the cache by removing the oldest partial_movie_files."""
cached_partial_movies = [
(self.partial_movie_directory / file_name)
for file_name in self.partial_movie_directory.iterdir()
if file_name != "partial_movie_file_list.txt"
]
if len(cached_partial_movies) > config["max_files_cached"]:
number_files_to_delete = (
len(cached_partial_movies) - config["max_files_cached"]
)
oldest_files_to_delete = sorted(
cached_partial_movies,
key=lambda path: path.stat().st_atime,
)[:number_files_to_delete]
for file_to_delete in oldest_files_to_delete:
file_to_delete.unlink()
logger.info(
f"The partial movie directory is full (> {config['max_files_cached']} files). Therefore, manim has removed the {number_files_to_delete} oldest file(s)."
" You can change this behaviour by changing max_files_cached in config.",
)
def flush_cache_directory(self):
"""Delete all the cached partial movie files"""
cached_partial_movies = [
self.partial_movie_directory / file_name
for file_name in self.partial_movie_directory.iterdir()
if file_name != "partial_movie_file_list.txt"
]
for f in cached_partial_movies:
f.unlink()
logger.info(
f"Cache flushed. {len(cached_partial_movies)} file(s) deleted in %(par_dir)s.",
{"par_dir": self.partial_movie_directory},
)
def open_file(self) -> None:
if self.quiet:
curr_stdout = sys.stdout
sys.stdout = open(os.devnull, "w")
current_os = platform.system()
file_paths = []
if self.save_last_frame:
file_paths.append(self.get_image_file_path())
if self.write_to_movie:
file_paths.append(self.get_movie_file_path())
for file_path in file_paths:
if current_os == "Windows":
os.startfile(file_path)
else:
commands = []
if current_os == "Linux":
commands.append("xdg-open")
elif current_os.startswith("CYGWIN"):
commands.append("cygstart")
else: # Assume macOS
commands.append("open")
if self.show_file_location_upon_completion:
commands.append("-R")
commands.append(file_path)
FNULL = open(os.devnull, "w")
sp.call(commands, stdout=FNULL, stderr=sp.STDOUT)
FNULL.close()
if self.quiet:
sys.stdout.close()
sys.stdout = curr_stdout

View file

@ -11,7 +11,8 @@ from manim.camera.camera import Camera
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
from manim.renderer.buffers.buffer import STD140BufferFormat
from manim.renderer.opengl_shader_program import load_shader_program_by_folder
from manim.renderer.renderer import ImageType, Renderer, RendererData, RendererProtocol
from manim.renderer.renderer import Renderer, RendererData, RendererProtocol
from manim.typing import PixelArray
from manim.utils.iterables import listify
from manim.utils.space_ops import cross2d, earclip_triangulation, z_to_vector
@ -121,7 +122,7 @@ def get_triangulation(self: OpenGLVMobject, normal_vector=None):
# Triangulate
inner_verts = points[inner_vert_indices]
inner_tri_indices = inner_vert_indices[earclip_triangulation(inner_verts, rings)]
inner_tri_indices = inner_vert_indices[earclip_triangulation(inner_verts, rings)] # type: ignore
tri_indices = np.hstack([indices, inner_tri_indices])
self.triangulation = tri_indices
@ -194,9 +195,8 @@ class ProgramManager:
def write_uniforms(prog, uniforms):
for name in prog:
member = prog[name]
if isinstance(member, gl.Uniform):
if name in uniforms:
member.value = uniforms[name]
if isinstance(member, gl.Uniform) and name in uniforms:
member.value = uniforms[name]
@staticmethod
def bind_to_uniform_block(uniform_buffer_object: gl.Buffer, idx: int = 0):
@ -214,7 +214,6 @@ class OpenGLRenderer(Renderer, RendererProtocol):
background_color: c.ManimColor = color.BLACK,
background_opacity: float = 1.0,
background_image: str | None = None,
substitute_output_fbo: gl.Framebuffer | None = None,
) -> None:
super().__init__()
self.pixel_width = pixel_width
@ -226,7 +225,7 @@ class OpenGLRenderer(Renderer, RendererProtocol):
# Initializing Context
logger.debug("Initializing OpenGL context and framebuffers")
self.ctx: gl.Context = gl.create_context()
self.ctx: gl.Context = gl.create_context(standalone=not config.preview)
# Those are the actual buffers that are used for rendering
self.stencil_texture = self.ctx.texture(
@ -373,7 +372,7 @@ class OpenGLRenderer(Renderer, RendererProtocol):
format = gl.detect_format(self.render_texture_program, frame_data.dtype.names)
vao = self.ctx.vertex_array(
program=self.render_texture_program,
content=[(vbo, format, *frame_data.dtype.names)],
content=[(vbo, format, *frame_data.dtype.names)], # type: ignore
)
self.ctx.copy_framebuffer(self.render_target_texture_fbo, self.color_buffer_fbo)
self.render_target_texture.use(0)
@ -408,12 +407,15 @@ class OpenGLRenderer(Renderer, RendererProtocol):
# return data, data_size
def render_image(self, mob):
raise NotImplementedError # TODO
raise NotImplementedError
def render_previous(self, camera: Camera) -> None:
raise NotImplementedError
def render_vmobject(self, mob: OpenGLVMobject) -> None: # type: ignore
def render_mesh(self, mob) -> None:
raise NotImplementedError
def render_vmobject(self, mob: OpenGLVMobject) -> None:
self.stencil_buffer_fbo.use()
self.stencil_buffer_fbo.clear()
self.render_target_fbo.use()
@ -512,10 +514,13 @@ class OpenGLRenderer(Renderer, RendererProtocol):
np.array(range(len(sub.points))),
)
def get_pixels(self) -> ImageType:
def get_pixels(self) -> PixelArray:
raw = self.output_fbo.read(components=4, dtype="f1", clamp=True) # RGBA, floats
buf = np.frombuffer(raw, dtype=np.uint8).reshape((1080, 1920, -1))
return buf
y, x = self.output_fbo.viewport[2:4]
buf = np.frombuffer(raw, dtype=np.uint8).reshape((x, y, 4))
# this actually has the right type (uint8) but due to
# numpy typing being bad, we have to type: ignore it
return buf[::-1] # type: ignore
class GLVMobjectManager:

View file

@ -2,16 +2,18 @@ from __future__ import annotations
import moderngl_window as mglw
import numpy as np
from moderngl_window.context.pyglet.window import Window as FunWindow
from moderngl_window.context.pyglet.window import Window as PygletWindow
from moderngl_window.timers.clock import Timer
from screeninfo import get_monitors
from .. import __version__, config
from manim import __version__, config
from manim.event_handler.window import WindowABC
__all__ = ["Window"]
class Window(FunWindow):
class Window(PygletWindow, WindowABC):
name = "Manim Community"
fullscreen: bool = False
resizable: bool = False
gl_version: tuple[int, int] = (3, 3)

View file

@ -1,248 +0,0 @@
from __future__ import annotations
import time
from collections.abc import Iterable
from typing import TYPE_CHECKING, Any, Callable
import numpy as np
from manim import config, logger
from manim.constants import RendererType
from manim.renderer.cairo_renderer import CairoRenderer
from manim.utils.exceptions import EndSceneEarlyException
from ..scene.scene import Scene, SceneState
from .opengl_file_writer import FileWriter
from .opengl_renderer import OpenGLRenderer
from .opengl_renderer_window import Window
if TYPE_CHECKING:
from manim.animation.protocol import AnimationProtocol
from ..camera.camera import Camera
from .renderer import RendererProtocol
__all__ = ("Manager",)
class Manager:
"""
The Brain of Manim
.. note::
The only method of this class officially guaranteed to be
stable is :meth:`~.Manager.render`. Any other methods documented
are purely for development
Usage
-----
.. code-block:: python
class Manimation(Scene):
def construct(self):
self.play(FadeIn(Circle()))
Manager(Manimation).render()
"""
def __init__(self, scene_cls: type[Scene]) -> None:
# scene
self.scene: Scene = scene_cls(self)
if not isinstance(self.scene, Scene):
raise ValueError(f"{self.scene!r} is not an instance of Scene")
self.time = 0
# Initialize window, if applicable
if config.preview:
self.window = Window()
else:
self.window = None
# this must be done AFTER instantiating a window
self.renderer = self.create_renderer()
self.renderer.use_window()
# file writer
self.file_writer = FileWriter(self.scene.get_default_scene_name()) # TODO
@property
def camera(self) -> Camera:
return self.scene.camera
def create_renderer(self) -> RendererProtocol:
match config.renderer:
case RendererType.OPENGL:
return OpenGLRenderer()
case RendererType.CAIRO:
return CairoRenderer()
case rendertype:
raise ValueError(f"Invalid Config Renderer type {rendertype}")
def _setup(self) -> None:
"""Set up processes and manager"""
if self.file_writer.has_progress_display():
self.scene.show_animation_progress = False
self.scene.setup()
self.virtual_animation_start_time = 0
self.real_animation_start_time = time.perf_counter()
def render(self) -> None:
"""
Entry point to running a Manim class
Example
-------
.. code-block:: python
class MyScene(Scene):
def construct(self):
self.play(Create(Circle()))
with tempconfig({"preview": True}):
Manager(MyScene).render()
"""
self._render_first_pass()
self._render_second_pass()
self._interact()
def _render_first_pass(self) -> None:
"""
Temporarily use the normal single pass
rendering system
"""
self._setup()
try:
self.scene.construct()
self._interact()
except EndSceneEarlyException:
pass
except KeyboardInterrupt:
# Get rid keyboard interrupt symbols
print("", end="\r")
self.file_writer.ended_with_interrupt = True
self._tear_down()
def _render_second_pass(self) -> None:
"""
In the future, this method could be used
for two pass rendering
"""
...
def _tear_down(self):
self.scene.tear_down()
if config.save_last_frame:
self._update_frame(0)
self.file_writer.finish()
if self.window is not None:
self.window.close()
self.window = None
def _interact(self) -> None:
if self.window is None:
return
logger.info(
"\nTips: Using the keys `d`, `f`, or `z` "
+ "you can interact with the scene. "
+ "Press `command + q` or `esc` to quit"
)
self.scene.skip_animations = False
self.scene.refresh_static_mobjects()
while not self.window.is_closing:
# TODO: Replace with actual dt instead
# of hardcoded dt
dt = 1 / self.camera.fps
self._update_frame(dt)
def _update_frame(self, dt: float):
self.time += dt
self.scene._update_mobjects(dt)
if self.window is not None:
self.window.clear()
state = self.scene.get_state()
self._render_frame(state)
if self.window is not None:
self.window.swap_buffers()
vt = self.time - self.virtual_animation_start_time
rt = time.perf_counter() - self.real_animation_start_time
if rt < vt:
self._update_frame(0)
def _play(self, *animations: AnimationProtocol):
self.scene.pre_play()
if self.window is not None:
self.real_animation_start_time = time.perf_counter()
self.virtual_animation_start_time = self.time
self.scene.begin_animations(animations)
self._progress_through_animations(animations)
self.scene.finish_animations(animations)
if self.scene.skip_animations and self.window is not None:
self._update_frame(dt=0)
self.scene.post_play()
def _wait(
self, duration: float, *, stop_condition: Callable[[], bool] | None = None
):
self.scene.pre_play()
update_mobjects = (
self.scene.should_update_mobjects()
) # TODO: this method needs to be implemented
condition = stop_condition or (lambda: False)
last_t = 0
for t in self._calc_time_progression(duration):
if update_mobjects:
dt, last_t = t - last_t, t
self._update_frame(dt)
if condition():
break
else:
self.renderer.render_previous(self.camera)
self.scene.post_play()
def _progress_through_animations(self, animations: Iterable[AnimationProtocol]):
last_t = 0
run_time = self._calc_runtime(animations)
for t in self._calc_time_progression(run_time):
dt, last_t = t - last_t, t
self.scene._update_animations(animations, t, dt)
self._update_frame(dt)
def _calc_time_progression(self, run_time: float) -> Iterable[float]:
return np.arange(0, run_time, 1 / self.camera.fps)
def _calc_runtime(self, animations: Iterable[AnimationProtocol]):
return max(animation.get_run_time() for animation in animations)
def _render_frame(self, state: SceneState) -> Any | None:
"""Renders a frame based on a state, and writes it to a file"""
data = self._send_scene_to_renderer(state)
# result = self.file_writer.write(data)
def _send_scene_to_renderer(self, state: SceneState):
"""Renders the State"""
result = self.renderer.render(self.scene.camera, state.mobjects)
return result

View file

@ -1,10 +1,7 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Protocol
import numpy as np
from typing_extensions import TypeAlias
from typing import TYPE_CHECKING, Protocol, runtime_checkable
from manim._config import logger
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
@ -12,11 +9,10 @@ from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
from manim.mobject.types.image_mobject import ImageMobject
if TYPE_CHECKING:
from collections.abc import Callable, Iterable, Sequence
from collections.abc import Iterable
from manim.camera.camera import Camera
ImageType: TypeAlias = np.ndarray
from manim.typing import PixelArray
class RendererData:
@ -24,13 +20,19 @@ class RendererData:
class Renderer(ABC):
"""Abstract class that handles dispatching mobjects to their specialized mobjects.
Specifically, it maps :class:`.OpenGLVMobject` to :meth:`render_vmobject`, :class:`.ImageMobject`
to :meth:`render_image`, etc.
"""
def __init__(self):
self.capabilities = [
(OpenGLVMobject, self.render_vmobject), # type: ignore
(ImageMobject, self.render_image), # type: ignore
(OpenGLVMobject, self.render_vmobject),
(ImageMobject, self.render_image),
]
def render(self, camera, renderables: Iterable[OpenGLMobject]) -> None: # Image
def render(self, camera: Camera, renderables: Iterable[OpenGLMobject]) -> None:
self.pre_render(camera)
for mob in renderables:
for type_, render_func in self.capabilities:
@ -45,11 +47,11 @@ class Renderer(ABC):
@abstractmethod
def pre_render(self, camera: Camera):
raise NotImplementedError
"""Actions before rendering any :class:`.OpenGLMobject`"""
@abstractmethod
def post_render(self):
raise NotImplementedError
"""Actions before rendering any :class:`.OpenGLMobject`"""
@abstractmethod
def render_vmobject(self, mob: OpenGLVMobject):
@ -60,28 +62,23 @@ class Renderer(ABC):
raise NotImplementedError
# Note: runtime checking is slow,
# but it only happens once or twice so it should be fine
@runtime_checkable
class RendererProtocol(Protocol):
capabilities: Sequence[
tuple[type[OpenGLMobject], Callable[[type[OpenGLMobject]], object]]
]
"""The Protocol a renderer must implement to be used in :class:`.Manager`."""
def render(self, camera: Camera, renderables: Iterable[OpenGLMobject]) -> None: ...
def render(self, camera: Camera, renderables: Iterable[OpenGLMobject]) -> None:
"""Render a group of Mobjects"""
...
def render_previous(self, camera: Camera) -> None: ...
def use_window(self) -> None:
"""Hook called after instantiation."""
...
def pre_render(self, camera) -> object: ...
def post_render(self) -> object: ...
def use_window(self): ...
def render_vmobject(self, mob: OpenGLVMobject) -> object: ...
def render_mesh(self, mob) -> None: ...
def render_image(self, mob: ImageMobject) -> None: ...
def get_pixels(self) -> ImageType: ...
def get_pixels(self) -> PixelArray:
"""Get the pixels that should be written to a file."""
...
# NOTE: The user should expect depth between renderers not to be handled discussed at 03.09.2023 Between jsonv and MrDiver
@ -97,7 +94,7 @@ class RendererProtocol(Protocol):
# def add(img1, img2):
# raise NotImplementedError
# def subtract(*images: List[Image]):
# def subtract(*images: List[PixelArray]):
# raise NotImplementedError
# def mix():

View file

@ -1,42 +1,30 @@
from __future__ import annotations
import inspect
import os
import random
from collections import OrderedDict
from collections.abc import Sequence
from collections import OrderedDict, deque
from typing import TYPE_CHECKING
import numpy as np
from IPython.terminal import pt_inputhooks
from IPython.terminal.embed import InteractiveShellEmbed
from pyglet.window import key
from tqdm import tqdm as ProgressDisplay
from manim import logger
from manim import config, logger
from manim.animation.animation import prepare_animation
from manim.animation.scene_buffer import SceneBuffer, SceneOperation
from manim.camera.camera import Camera
from manim.constants import DEFAULT_WAIT_TIME
from manim.event_handler import EVENT_DISPATCHER
from manim.event_handler.event_type import EventType
from manim.mobject.frame import FullScreenRectangle
from manim.mobject.mobject import Group, Point, _AnimationBuilder
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
from manim.utils.color import RED
from manim.utils.deprecation import deprecated
from manim.utils.exceptions import EndSceneEarlyException
from manim.utils.family_ops import extract_mobject_family_members
from manim.utils.iterables import list_difference_update
from manim.utils.module_ops import get_module
if TYPE_CHECKING:
from collections.abc import Iterable
from collections.abc import Iterable, Reversible, Sequence
from typing import Any, Callable
from manim.animation.protocol import AnimationProtocol as Animation
from manim.animation.scene_buffer import SceneBuffer
from manim.renderer.render_manager import Manager
from manim.animation.protocol import AnimationProtocol
from manim.manager import Manager
# TODO: these keybindings should be made configurable
@ -48,55 +36,52 @@ QUIT_KEY = "q"
class Scene:
random_seed: int = 0
"""The Canvas of Manim.
You can use it by putting the following into a
file ``manimation.py``
.. manim:: SceneWithSettings
class SceneWithSettings(Scene):
# set configuration attributes
random_seed = 1
# all the action happens here
def construct(self):
self.play(Create(ManimBanner()))
And then run ``manim -p manimation.py``. To write the result to a file,
do ``manim -w manimation.py``.
Attributes
----------
random_seed : The seed for random and numpy.random
pan_sensitivity :
"""
random_seed: int | None = None
pan_sensitivity: float = 3.0
max_num_saved_states: int = 50
default_camera_config: dict = {}
default_window_config: dict = {}
default_file_writer_config: dict = {}
def __init__(
self,
manager: Manager | None = None,
window_config: dict = {},
camera_config: dict = {},
skip_animations: bool = False,
always_update_mobjects: bool = False,
start_at_animation_number: int | None = None,
end_at_animation_number: int | None = None,
leave_progress_bars: bool = False,
preview: bool = True,
presenter_mode: bool = False,
show_animation_progress: bool = False,
embed_exception_mode: str = "",
embed_error_sound: bool = False,
):
self.skip_animations = skip_animations
self.always_update_mobjects = always_update_mobjects
self.start_at_animation_number = start_at_animation_number
self.end_at_animation_number = end_at_animation_number
self.leave_progress_bars = leave_progress_bars
self.preview = preview
self.presenter_mode = presenter_mode
self.show_animation_progress = show_animation_progress
self.embed_exception_mode = embed_exception_mode
self.embed_error_sound = embed_error_sound
always_update_mobjects: bool = False
start_at_animation_number: int = 0
end_at_animation_number: int | None = None
presenter_mode: bool = False
embed_exception_mode: str = ""
embed_error_sound: bool = False
def __init__(self, manager: Manager):
# Core state of the scene
self.camera: Camera = Camera()
self.camera.save_state()
self.manager = manager
self.mobjects: list[Mobject] = []
self.id_to_mobject_map: dict[int, Mobject] = {}
self.mobjects: list[OpenGLMobject] = []
self.num_plays: int = 0
# the time is updated by the manager
self.time: float = 0
self.skip_time: float = 0
self.original_skipping_status: bool = self.skip_animations
self.undo_stack = []
self.redo_stack = []
if self.start_at_animation_number is not None:
self.skip_animations = True
self.undo_stack: deque[SceneState] = deque()
self.redo_stack: list[SceneState] = []
# Items associated with interaction
self.mouse_point = Point()
@ -123,21 +108,18 @@ class Scene:
return name
def process_buffer(self, buffer: SceneBuffer) -> None:
self.remove(*buffer.to_remove)
for to_replace_pairs in buffer.to_replace:
self.replace(*to_replace_pairs)
self.add(*buffer.to_add)
for op, args, kwargs in buffer:
match op:
case SceneOperation.ADD:
self.add(*args, **kwargs)
case SceneOperation.REMOVE:
self.remove(*args, **kwargs)
case SceneOperation.REPLACE:
self.replace(*args, **kwargs)
case o:
raise NotImplementedError(f"Unknown operation {o}")
buffer.clear()
@deprecated(message="Use Manager(Scene).render()")
@classmethod
def run(cls) -> None:
from ..renderer.render_manager import Manager
return Manager(cls).render()
render = run
def setup(self) -> None:
"""
This method is used to set up scenes to do any setup
@ -149,109 +131,13 @@ class Scene:
The entrypoint to animations in Manim.
Should be overridden in the subclass to produce animations
"""
raise RuntimeError("Could not find the construct method, did you misspell it?")
def tear_down(self) -> None:
"""
This method is used to clean up scenes
"""
def embed(
self,
close_scene_on_exit: bool = True,
show_animation_progress: bool = True,
) -> None:
if not self.preview:
return # Embed is only relevant with a preview
self.stop_skipping()
self.update_frame()
self.save_state()
self.show_animation_progress = show_animation_progress
# Create embedded IPython terminal to be configured
shell = InteractiveShellEmbed.instance()
# Use the locals namespace of the caller
caller_frame = inspect.currentframe().f_back
local_ns = dict(caller_frame.f_locals)
# Add a few custom shortcuts
local_ns.update(
play=self.play,
wait=self.wait,
add=self.add,
remove=self.remove,
clear=self.clear,
save_state=self.save_state,
undo=self.undo,
redo=self.redo,
i2g=self.i2g,
i2m=self.i2m,
)
# Enables gui interactions during the embed
def inputhook(context):
while not context.input_is_ready():
if not self.is_window_closing():
self.update_frame(dt=0)
if self.is_window_closing():
shell.ask_exit()
pt_inputhooks.register("manim", inputhook)
shell.enable_gui("manim")
# This is hacky, but there's an issue with ipython which is that
# when you define lambda's or list comprehensions during a shell session,
# they are not aware of local variables in the surrounding scope. Because
# That comes up a fair bit during scene construction, to get around this,
# we (admittedly sketchily) update the global namespace to match the local
# namespace, since this is just a shell session anyway.
shell.events.register(
"pre_run_cell", lambda: shell.user_global_ns.update(shell.user_ns)
)
# Operation to run after each ipython command
def post_cell_func():
self.refresh_static_mobjects()
if not self.is_window_closing():
self.update_frame(dt=0, ignore_skipping=True)
self.save_state()
shell.events.register("post_run_cell", post_cell_func)
# Flash border, and potentially play sound, on exceptions
def custom_exc(shell, etype, evalue, tb, tb_offset=None):
# still show the error don't just swallow it
shell.showtraceback((etype, evalue, tb), tb_offset=tb_offset)
if self.embed_error_sound:
os.system("printf '\a'")
rect = FullScreenRectangle().set_stroke(RED, 30).set_fill(opacity=0)
rect.fix_in_frame()
from manim.animation.fading import FadeIn
from manim.utils.rate_functions import there_and_back
self.play(
FadeIn(rect, run_time=0.5, rate_func=there_and_back, remover=True)
)
shell.set_custom_exc((Exception,), custom_exc)
# Set desired exception mode
shell.magic(f"xmode {self.embed_exception_mode}")
# Launch shell
shell(
local_ns=local_ns,
# Pretend like we're embedding in the caller function, not here
stack_depth=2,
# Specify that the present module is the caller's, not here
module=get_module(caller_frame.f_globals["__file__"]),
)
# End scene when exiting an embed
if close_scene_on_exit:
raise EndSceneEarlyException()
# Only these methods should touch the camera
# Related to updating
@ -269,77 +155,29 @@ class Scene:
"""
# always rerender by returning True
# TODO: Apply caching here
return True
# wait_animation = self.animations[0]
# if wait_animation.is_static_wait is None:
# should_update = (
# self.always_update_mobjects
# or self.updaters
# or wait_animation.stop_condition is not None
# or any(
# mob.has_time_based_updater()
# for mob in self.get_mobject_family_members()
# )
# )
# wait_animation.is_static_wait = not should_update
# return not wait_animation.is_static_wait
return self.always_update_mobjects or any(
mob.has_updaters for mob in self.mobjects
)
def has_time_based_updaters(self) -> bool:
return any(
[
sm.has_time_based_updater()
for mob in self.mobjects
for sm in mob.get_family()
]
sm.has_time_based_updater()
for mob in self.mobjects
for sm in mob.get_family()
)
# Related to time
def get_time(self) -> float:
return self.time
def increment_time(self, dt: float) -> None:
self.time += dt
# Related to internal mobject organization
def get_top_level_mobjects(self) -> list[Mobject]:
# Return only those which are not in the family
# of another mobject from the scene
mobjects = self.get_mobjects()
families = [m.get_family() for m in mobjects]
def is_top_level(mobject):
num_families = sum([(mobject in family) for family in families])
return num_families == 1
return list(filter(is_top_level, mobjects))
def get_mobject_family_members(self) -> list[Mobject]:
return extract_mobject_family_members(self.mobjects)
def add(self, *new_mobjects: Mobject):
def add(self, *new_mobjects: OpenGLMobject):
"""
Mobjects will be displayed, from background to
foreground in the order with which they are added.
"""
self.remove(*new_mobjects)
self.mobjects += new_mobjects
self.id_to_mobject_map.update(
{id(sm): sm for m in new_mobjects for sm in m.get_family()}
)
return self
def add_mobjects_among(self, values: Iterable):
"""
This is meant mostly for quick prototyping,
e.g. to add all mobjects defined up to a point,
call self.add_mobjects_among(locals().values())
"""
self.add(*filter(lambda m: isinstance(m, Mobject), values))
return self
def remove(self, *mobjects_to_remove: Mobject):
def remove(self, *mobjects_to_remove: OpenGLMobject):
"""
Removes anything in mobjects from scenes mobject list, but in the event that one
of the items to be removed is a member of the family of an item in mobject_list,
@ -356,19 +194,20 @@ class Scene:
self.mobjects = list_difference_update(self.mobjects, mob.get_family())
return self
def replace(self, mobject: Mobject, *replacements: Mobject):
"""Replace one mobject in the scene with another, preserving draw order.
def replace(self, mobject: OpenGLMobject, *replacements: OpenGLMobject):
"""Replace one Mobject in the scene with one or more other Mobjects,
preserving draw order.
If ``old_mobject`` is a submobject of some other Mobject (e.g. a
:class:`.Group`), the new_mobject will replace it inside the group,
without otherwise changing the parent mobject.
If ``mobject`` is a submobject of some other :class:`OpenGLMobject`
(e.g. a :class:`.Group`), the ``replacements`` will replace it inside
the group, without otherwise changing the parent mobject.
Parameters
----------
old_mobject
mobject
The mobject to be replaced. Must be present in the scene.
new_mobject
A mobject which must not already be in the scene.
replacements
One or more Mobjects which must not already be in the scene.
"""
if mobject in self.mobjects:
@ -426,73 +265,31 @@ class Scene:
"""
self.updaters = [f for f in self.updaters if f is not func]
def restructure_mobjects(
self,
to_remove: Mobject,
mobject_list_name: str = "mobjects",
extract_families: bool = True,
):
"""
tl:wr
If your scene has a Group(), and you removed a mobject from the Group,
this dissolves the group and puts the rest of the mobjects directly
in self.mobjects or self.foreground_mobjects.
In cases where the scene contains a group, e.g. Group(m1, m2, m3), but one
of its submobjects is removed, e.g. scene.remove(m1), the list of mobjects
will be edited to contain other submobjects, but not m1, e.g. it will now
insert m2 and m3 to where the group once was.
Parameters
----------
to_remove
The Mobject to remove.
mobject_list_name
The list of mobjects ("mobjects", "foreground_mobjects" etc) to remove from.
extract_families
Whether the mobject's families should be recursively extracted.
Returns
-------
Scene
The Scene mobject with restructured Mobjects.
"""
if extract_families:
to_remove = extract_mobject_family_members(
to_remove,
use_z_index=self.renderer.camera.use_z_index,
)
_list = getattr(self, mobject_list_name)
new_list = self.get_restructured_mobject_list(_list, to_remove)
setattr(self, mobject_list_name, new_list)
def bring_to_front(self, *mobjects: Mobject):
def bring_to_front(self, *mobjects: OpenGLMobject):
self.add(*mobjects)
return self
def bring_to_back(self, *mobjects: Mobject):
def bring_to_back(self, *mobjects: OpenGLMobject):
self.remove(*mobjects)
self.mobjects = list(mobjects) + self.mobjects
self.mobjects = [*mobjects, *self.mobjects]
return self
def clear(self):
self.mobjects = []
self.mobjects.clear()
return self
def get_mobjects(self) -> list[Mobject]:
def get_mobjects(self) -> Sequence[OpenGLMobject]:
return list(self.mobjects)
def get_mobject_copies(self) -> list[Mobject]:
def get_mobject_copies(self) -> Sequence[OpenGLMobject]:
return [m.copy() for m in self.mobjects]
def point_to_mobject(
self,
point: np.ndarray,
search_set: Iterable[Mobject] | None = None,
search_set: Reversible[OpenGLMobject] | None = None,
buff: float = 0,
) -> Mobject | None:
) -> OpenGLMobject | None:
"""
E.g. if clicking on the scene, this returns the top layer mobject
under a given point
@ -510,76 +307,23 @@ class Scene:
else:
return Group(*mobjects)
def id_to_mobject(self, id_value):
return self.id_to_mobject_map[id_value]
def ids_to_group(self, *id_values):
return self.get_group(
*filter(lambda x: x is not None, map(self.id_to_mobject, id_values))
)
def i2g(self, *id_values):
return self.ids_to_group(*id_values)
def i2m(self, id_value):
return self.id_to_mobject(id_value)
# Related to skipping
def update_skipping_status(self) -> None:
if (self.start_at_animation_number is not None) and (
self.num_plays == self.start_at_animation_number
):
self.skip_time = self.time
if not self.original_skipping_status:
self.stop_skipping()
if (self.end_at_animation_number is not None) and (
self.num_plays >= self.end_at_animation_number
):
raise EndSceneEarlyException()
def stop_skipping(self) -> None:
self.virtual_animation_start_time = self.time
self.skip_animations = False
# Methods associated with running animations
def pre_play(self) -> None:
"""To be implemented in subclasses."""
def get_wait_time_progression(
self, duration: float, stop_condition: Callable[[], bool] | None = None
) -> list[float] | np.ndarray | ProgressDisplay:
kw: dict[str, Any] = {"desc": f"{self.num_plays} Waiting"}
if stop_condition is not None:
kw["n_iterations"] = -1 # So it doesn't show % progress
kw["override_skip_animations"] = True
return self.get_time_progression(duration, **kw)
def pre_play(self): # Doesn't exist in Main
if self.presenter_mode and self.num_plays == 0:
self.hold_loop()
self.update_skipping_status()
# if not self.skip_animations:
# self.file_writer.begin_animation()
self.refresh_static_mobjects()
def post_play(self):
# if not self.skip_animations:
# self.manager.file_writer.end_animation()
def post_play(self) -> None:
self.num_plays += 1
def refresh_static_mobjects(self) -> None:
# self.camera.refresh_static_mobjects()
...
def begin_animations(self, animations: Iterable[Animation]) -> None:
def begin_animations(self, animations: Iterable[AnimationProtocol]) -> None:
for animation in animations:
animation.begin()
self.process_buffer(animation.buffer)
def _update_animations(self, animations: Iterable[Animation], t: float, dt: float):
def _update_animations(
self, animations: Iterable[AnimationProtocol], t: float, dt: float
):
for animation in animations:
animation.update_mobjects(dt)
alpha = t / animation.get_run_time()
@ -588,19 +332,16 @@ class Scene:
self.process_buffer(animation.buffer)
animation.apply_buffer = False
def finish_animations(self, animations: Iterable[Animation]) -> None:
def finish_animations(self, animations: Iterable[AnimationProtocol]) -> None:
for animation in animations:
animation.finish()
self.process_buffer(animation.buffer)
if self.skip_animations:
self._update_mobjects(self.manager._calc_runtime(animations))
else:
self._update_mobjects(0)
def play(
self,
*proto_animations: Animation | _AnimationBuilder,
# the OpenGLMobject is a side-effect of the return type of animate, it will
# raise a ValueError
*proto_animations: AnimationProtocol | _AnimationBuilder | OpenGLMobject,
run_time: float | None = None,
rate_func: Callable[[float], float] | None = None,
lag_ratio: float | None = None,
@ -608,6 +349,7 @@ class Scene:
if len(proto_animations) == 0:
logger.warning("Called Scene.play with no animations")
return
# build animationbuilders
animations = [prepare_animation(x) for x in proto_animations]
for anim in animations:
anim.update_rate_info(run_time, rate_func, lag_ratio)
@ -623,8 +365,6 @@ class Scene:
ignore_presenter_mode: bool = False,
):
self.manager._wait(duration, stop_condition=stop_condition)
# self.pre_play()
# self.update_mobjects(dt=0) # Any problems with this?
# if (
# self.presenter_mode
# and not self.skip_animations
@ -633,35 +373,10 @@ class Scene:
# if note:
# logger.info(note)
# self.hold_loop()
# else:
# time_progression = self.get_wait_time_progression(duration, stop_condition)
# last_t = 0
# for t in time_progression:
# dt = t - last_t
# last_t = t
# self.update_frame(dt)
# self.emit_frame()
# if stop_condition is not None and stop_condition():
# break
# self.refresh_static_mobjects()
# self.post_play()
def wait_until(self, stop_condition: Callable[[], bool], max_time: float = 60):
self.wait(max_time, stop_condition=stop_condition)
def force_skipping(self):
self.original_skipping_status = self.skip_animations
self.skip_animations = True
return self
def revert_to_original_skipping_status(self):
if hasattr(self, "original_skipping_status"):
self.skip_animations = self.original_skipping_status
return self
def emit_frame(self) -> None:
pass
def add_sound(
self,
sound_file: str,
@ -669,13 +384,10 @@ class Scene:
gain: float | None = None,
gain_to_background: float | None = None,
):
if self.skip_animations:
return
time = self.get_time() + time_offset
raise NotImplementedError("TODO")
time = self.time + time_offset
self.file_writer.add_sound(sound_file, time, gain, gain_to_background)
# Helpers for interactive development
def get_state(self) -> SceneState:
return SceneState(self)
@ -683,7 +395,7 @@ class Scene:
scene_state.restore_scene(self)
def save_state(self) -> None:
if not self.preview:
if not config.preview:
return
state = self.get_state()
if self.undo_stack and state.mobjects_match(self.undo_stack[-1]):
@ -691,40 +403,19 @@ class Scene:
self.redo_stack = []
self.undo_stack.append(state)
if len(self.undo_stack) > self.max_num_saved_states:
self.undo_stack.pop(0)
self.undo_stack.popleft()
def undo(self):
if self.undo_stack:
self.redo_stack.append(self.get_state())
self.restore_state(self.undo_stack.pop())
self.refresh_static_mobjects()
def redo(self):
if self.redo_stack:
self.undo_stack.append(self.get_state())
self.restore_state(self.redo_stack.pop())
self.refresh_static_mobjects()
# TODO: reimplement checkpoint feature with CE's section API
def save_mobject_to_file(
self, mobject: Mobject, file_path: str | None = None
) -> None:
return
if file_path is None:
file_path = self.file_writer.get_saved_mobject_path(mobject)
if file_path is None:
return
mobject.save_to_file(file_path)
def load_mobject(self, file_name):
if os.path.exists(file_name):
path = file_name
else:
directory = self.file_writer.get_saved_mobject_directory()
path = os.path.join(directory, file_name)
return Mobject.load(path)
# Event handling
def on_mouse_motion(self, point: np.ndarray, d_point: np.ndarray) -> None:
@ -840,7 +531,6 @@ class Scene:
self.hold_on_wait = False
def on_resize(self, width: int, height: int) -> None:
# self.camera.reset_pixel_shape(width, height)
pass
def on_show(self) -> None:
@ -854,7 +544,7 @@ class Scene:
class SceneState:
def __init__(self, scene: Scene, ignore: list[Mobject] | None = None) -> None:
def __init__(self, scene: Scene, ignore: list[OpenGLMobject] | None = None) -> None:
self.time = scene.time
self.num_plays = scene.num_plays
self.camera = scene.camera.copy()
@ -873,7 +563,7 @@ class SceneState:
self.mobjects_to_copies[mob] = mob.copy()
@property
def mobjects(self) -> Sequence[Mobject]:
def mobjects(self) -> Sequence[OpenGLMobject]:
return tuple(self.mobjects_to_copies.keys())
def __eq__(self, state: Any) -> bool:

View file

@ -77,10 +77,10 @@ __all__ = [
"FunctionOverride",
"PathFuncType",
"MappingFunction",
"Image",
"GrayscaleImage",
"RGBImage",
"RGBAImage",
"PixelArray",
"GrayscalePixelArray",
"RGBPixelArray",
"RGBAPixelArray",
"StrPath",
"StrOrBytesPath",
]
@ -576,13 +576,16 @@ PathFuncType: TypeAlias = Callable[[Point3D, Point3D, float], Point3D]
MappingFunction: TypeAlias = Callable[[Point3D], Point3D]
"""A function mapping a `Point3D` to another `Point3D`."""
RateFunc: TypeAlias = Callable[[float], float]
r"""A rate function :math:`f: [0, 1] \to [0, 1]`."""
"""
[CATEGORY]
Image types
"""
Image: TypeAlias = npt.NDArray[ManimInt]
PixelArray: TypeAlias = npt.NDArray[ManimInt]
"""``shape: (height, width) | (height, width, 3) | (height, width, 4)``
A rasterized image with a height of ``height`` pixels and a width of
@ -595,24 +598,24 @@ lightness (for greyscale images), an `RGB_Array_Int` or an
`RGBA_Array_Int`.
"""
GrayscaleImage: TypeAlias = Image
GrayscalePixelArray: TypeAlias = PixelArray
"""``shape: (height, width)``
A 100% opaque grayscale `Image`, where every pixel value is a
A 100% opaque grayscale `PixelArray`, where every pixel value is a
`ManimInt` indicating its lightness (black -> gray -> white).
"""
RGBImage: TypeAlias = Image
RGBPixelArray: TypeAlias = PixelArray
"""``shape: (height, width, 3)``
A 100% opaque `Image` in color, where every pixel value is an
A 100% opaque `PixelArray` in color, where every pixel value is an
`RGB_Array_Int` object.
"""
RGBAImage: TypeAlias = Image
RGBAPixelArray: TypeAlias = PixelArray
"""``shape: (height, width, 4)``
An `Image` in color where pixels can be transparent. Every pixel
A `PixelArray` in color where pixels can be transparent. Every pixel
value is an `RGBA_Array_Int` object.
"""

File diff suppressed because it is too large Load diff

View file

@ -30,8 +30,6 @@ def handle_caching_play(func: Callable[..., None]):
# method has to be deleted.
def wrapper(self, scene, *args, **kwargs):
self.skip_animations = self._original_skipping_status
self.update_skipping_status()
animations = scene.compile_animations(*args, **kwargs)
scene.add_mobjects_from_animations(animations)
if self.skip_animations:

View file

@ -16,7 +16,7 @@ if TYPE_CHECKING:
__all__ = ["AliasAttrDocumenter"]
ALIAS_DOCS_DICT, DATA_DICT = parse_module_attributes()
ALIAS_DOCS_DICT, DATA_DICT, TYPEVAR_DICT = parse_module_attributes()
ALIAS_LIST = [
alias_name
for module_dict in ALIAS_DOCS_DICT.values()
@ -100,10 +100,11 @@ class AliasAttrDocumenter(Directive):
def run(self) -> list[nodes.Element]:
module_name = self.arguments[0]
# Slice module_name[6:] to remove the "manim." prefix which is
# not present in the keys of the DICTs
module_alias_dict = ALIAS_DOCS_DICT.get(module_name[6:], None)
module_attrs_list = DATA_DICT.get(module_name[6:], None)
module_name = module_name.removeprefix("manim.")
module_alias_dict = ALIAS_DOCS_DICT.get(module_name, None)
module_attrs_list = DATA_DICT.get(module_name, None)
module_typevars = TYPEVAR_DICT.get(module_name, None)
content = nodes.container()
@ -161,6 +162,11 @@ class AliasAttrDocumenter(Directive):
for A in ALIAS_LIST:
alias_doc = alias_doc.replace(f"`{A}`", f":class:`~.{A}`")
# also hyperlink the TypeVars from that module
if module_typevars is not None:
for T in module_typevars:
alias_doc = alias_doc.replace(f"`{T}`", f":class:`{T}`")
# Add all the lines with 4 spaces behind, to consider all the
# documentation as a paragraph INSIDE the `.. class::` block
doc_lines = alias_doc.split("\n")
@ -172,6 +178,37 @@ class AliasAttrDocumenter(Directive):
self.state.nested_parse(unparsed, 0, alias_container)
category_alias_container += alias_container
# then add the module TypeVars section
if module_typevars is not None:
module_typevars_section = nodes.section(ids=[f"{module_name}.typevars"])
content += module_typevars_section
# Use a rubric (title-like), just like in `module.rst`
module_typevars_section += nodes.rubric(text="TypeVar's")
# name: str
# definition: TypeVarDict = dict[str, str]
for name, definition in module_typevars.items():
# Using the `.. class::` directive is CRUCIAL, since
# function/method parameters are always annotated via
# classes - therefore Sphinx expects a class
unparsed = ViewList(
[
f".. class:: {name}",
"",
" .. parsed-literal::",
"",
f" {definition}",
"",
]
)
# Parse the reST text into a fresh container
# https://www.sphinx-doc.org/en/master/extdev/markupapi.html#parsing-directive-content-as-rest
typevar_container = nodes.container()
self.state.nested_parse(unparsed, 0, typevar_container)
module_typevars_section += typevar_container
# Then, add the traditional "Module Attributes" section
if module_attrs_list is not None:
module_attrs_section = nodes.section(ids=[f"{module_name}.data"])

View file

@ -82,6 +82,7 @@ from __future__ import annotations
import csv
import itertools as it
import os
import re
import shutil
import sys
@ -296,7 +297,7 @@ class ManimDirective(Directive):
code = [
"from manim import *",
*user_code,
f"{clsname}().render()",
f"Manager({clsname}).render()",
]
try:
@ -350,7 +351,7 @@ class ManimDirective(Directive):
rendering_times_file_path = Path("../rendering_times.csv")
def _write_rendering_stats(scene_name: str, run_time: str, file_name: str) -> None:
def _write_rendering_stats(scene_name: str, run_time: float, file_name: str) -> None:
with rendering_times_file_path.open("a") as file:
csv.writer(file).writerow(
[

View file

@ -26,6 +26,10 @@ ModuleLevelAliasDict: TypeAlias = dict[str, AliasCategoryDict]
classified by category in different `AliasCategoryDict` objects.
"""
ModuleTypeVarDict: TypeAlias = dict[str, str]
"""Dictionary containing every :class:`TypeVar` defined in a module."""
AliasDocsDict: TypeAlias = dict[str, ModuleLevelAliasDict]
"""Dictionary which, for every module in Manim, contains documentation
about their module-level attributes which are explicitly defined as
@ -39,8 +43,12 @@ by Sphinx via the ``data`` role, hence the name) which are NOT
explicitly defined as :class:`TypeAlias`.
"""
TypeVarDict: TypeAlias = dict[str, ModuleTypeVarDict]
"""A dictionary mapping module names to dictionaries of :class:`TypeVar` objects."""
ALIAS_DOCS_DICT: AliasDocsDict = {}
DATA_DICT: DataDict = {}
TYPEVAR_DICT: TypeVarDict = {}
MANIM_ROOT = Path(__file__).resolve().parent.parent.parent
@ -50,7 +58,7 @@ MANIM_ROOT = Path(__file__).resolve().parent.parent.parent
# ruff: noqa: E721
def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]:
def parse_module_attributes() -> tuple[AliasDocsDict, DataDict, TypeVarDict]:
"""Read all files, generate Abstract Syntax Trees from them, and
extract useful information about the type aliases defined in the
files: the category they belong to, their definition and their
@ -58,19 +66,24 @@ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]:
Returns
-------
ALIAS_DOCS_DICT : `AliasDocsDict`
ALIAS_DOCS_DICT : :class:`AliasDocsDict`
A dictionary containing the information from all the type
aliases in Manim. See `AliasDocsDict` for more information.
aliases in Manim. See :class:`AliasDocsDict` for more information.
DATA_DICT : `DataDict`
DATA_DICT : :class:`DataDict`
A dictionary containing the names of all DOCUMENTED
module-level attributes which are not a :class:`TypeAlias`.
TYPEVAR_DICT : :class:`TypeVarDict`
A dictionary containing the definitions of :class:`TypeVar` objects,
organized by modules.
"""
global ALIAS_DOCS_DICT
global DATA_DICT
global TYPEVAR_DICT
if ALIAS_DOCS_DICT or DATA_DICT:
return ALIAS_DOCS_DICT, DATA_DICT
if ALIAS_DOCS_DICT or DATA_DICT or TYPEVAR_DICT:
return ALIAS_DOCS_DICT, DATA_DICT, TYPEVAR_DICT
for module_path in MANIM_ROOT.rglob("*.py"):
module_name = module_path.resolve().relative_to(MANIM_ROOT)
@ -85,6 +98,9 @@ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]:
category_dict: AliasCategoryDict | None = None
alias_info: AliasInfo | None = None
# For storing TypeVars
module_typevars: ModuleTypeVarDict = {}
# For storing regular module attributes
data_list: list[str] = []
data_name: str | None = None
@ -172,6 +188,19 @@ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]:
alias_info = category_dict[alias_name]
continue
# Check if it is a typing.TypeVar
elif (
type(node) is ast.Assign
and type(node.targets[0]) is ast.Name
and type(node.value) is ast.Call
and type(node.value.func) is ast.Name
and node.value.func.id.endswith("TypeVar")
):
module_typevars[node.targets[0].id] = ast.unparse(
node.value
).replace("_", r"\_")
continue
# If here, the node is not a TypeAlias definition
alias_info = None
@ -185,7 +214,9 @@ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]:
else:
target = None
if type(target) is ast.Name:
if type(target) is ast.Name and not (
type(node) is ast.Assign and target.id not in module_typevars
):
data_name = target.id
else:
data_name = None
@ -194,5 +225,7 @@ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]:
ALIAS_DOCS_DICT[module_name] = module_dict
if len(data_list) > 0:
DATA_DICT[module_name] = data_list
if module_typevars:
TYPEVAR_DICT[module_name] = module_typevars
return ALIAS_DOCS_DICT, DATA_DICT
return ALIAS_DOCS_DICT, DATA_DICT, TYPEVAR_DICT

View file

@ -7,15 +7,26 @@ __all__ = [
"restructure_list_to_exclude_certain_family_members",
]
from typing import TYPE_CHECKING
def extract_mobject_family_members(mobject_list, only_those_with_points=False):
if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
def extract_mobject_family_members(
mobject_list: Iterable[Mobject], only_those_with_points: bool = False
) -> Sequence[Mobject]:
result = list(it.chain(*(mob.get_family() for mob in mobject_list)))
if only_those_with_points:
result = [mob for mob in result if mob.has_points()]
return result
def restructure_list_to_exclude_certain_family_members(mobject_list, to_remove):
def restructure_list_to_exclude_certain_family_members(
mobject_list: Iterable[Mobject], to_remove: Iterable[Mobject]
) -> Sequence[Mobject]:
"""
Removes anything in to_remove from mobject_list, but in the event that one of
the items to be removed is a member of the family of an item in mobject_list,
@ -43,8 +54,8 @@ def restructure_list_to_exclude_certain_family_members(mobject_list, to_remove):
def recursive_mobject_remove(
mobjects: List[Mobject], to_remove: Set[Mobject]
) -> Tuple[List[Mobject], bool]:
mobjects: list[Mobject], to_remove: set[Mobject]
) -> tuple[Sequence[Mobject], bool]:
"""
Takes in a list of mobjects, together with a set of mobjects to remove.
The first component of what's removed is a new list such that any mobject

View file

@ -30,11 +30,9 @@ from typing import TYPE_CHECKING
import numpy as np
if TYPE_CHECKING:
from ..scene.scene_file_writer import SceneFileWriter
from manim.scene.scene_file_writer import SceneFileWriter
from manim import __version__, config, logger
from .. import console
from manim import __version__, config, console, logger
def is_mp4_format() -> bool:
@ -314,10 +312,12 @@ def get_sorted_integer_files(
full_path = os.path.join(directory, file)
if index_str.isdigit():
index = int(index_str)
if remove_indices_greater_than is not None:
if index > remove_indices_greater_than:
os.remove(full_path)
continue
if (
remove_indices_greater_than is not None
and index > remove_indices_greater_than
):
os.remove(full_path)
continue
if extension is not None and not file.endswith(extension):
continue
if index >= min_index and index < max_index:
@ -325,4 +325,4 @@ def get_sorted_integer_files(
elif remove_non_integer_files:
os.remove(full_path)
indexed_files.sort(key=lambda p: p[0])
return list(map(lambda p: os.path.join(directory, p[1]), indexed_files))
return [os.path.join(directory, p[1]) for p in indexed_files]

View file

@ -2,27 +2,31 @@
from __future__ import annotations
import collections
import copy
import inspect
import json
import typing
import zlib
from collections.abc import Callable, Hashable
from time import perf_counter
from types import FunctionType, MappingProxyType, MethodType, ModuleType
from typing import Any
import numpy as np
from manim.animation.animation import Animation
from manim.camera.camera import Camera
from manim.mobject.mobject import Mobject
from .. import config, logger
if typing.TYPE_CHECKING:
from typing_extensions import TypeVar
from manim.animation.protocol import AnimationProtocol
from manim.camera.camera import Camera
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
from manim.scene.scene import Scene
T = TypeVar("T")
S = TypeVar("S", default=str)
__all__ = ["KEYS_TO_FILTER_OUT", "get_hash_from_play_call", "get_json"]
# Sometimes there are elements that are not suitable for hashing (too long or
@ -59,7 +63,7 @@ class _Memoizer:
cls._already_processed.clear()
@classmethod
def check_already_processed_decorator(cls: _Memoizer, is_method: bool = False):
def check_already_processed_decorator(cls, is_method: bool = False):
"""Decorator to handle the arguments that goes through the decorated function.
Returns _ALREADY_PROCESSED_PLACEHOLDER if the obj has been processed, or lets
the decorated function call go ahead.
@ -102,7 +106,7 @@ class _Memoizer:
return cls._handle_already_processed(obj, lambda x: x)
@classmethod
def mark_as_processed(cls, obj: Any) -> None:
def mark_as_processed(cls, obj: Any) -> str:
"""Marks an object as processed.
Parameters
@ -131,7 +135,7 @@ class _Memoizer:
# It makes no sense (and it'd slower) to memoize objects of these primitive
# types. Hence, we simply return the object.
return obj
if isinstance(obj, collections.abc.Hashable):
if isinstance(obj, Hashable):
try:
return cls._return(obj, hash, default_function)
except TypeError:
@ -144,11 +148,11 @@ class _Memoizer:
@classmethod
def _return(
cls,
obj: typing.Any,
obj: T,
obj_to_membership_sign: typing.Callable[[Any], int],
default_func,
default_func: Callable[[T], str],
memoizing=True,
) -> str | Any:
) -> str:
obj_membership_sign = obj_to_membership_sign(obj)
if obj_membership_sign in cls._already_processed:
return cls.ALREADY_PROCESSED_PLACEHOLDER
@ -173,7 +177,7 @@ class _Memoizer:
class _CustomEncoder(json.JSONEncoder):
def default(self, obj: Any):
def default(self, o: Any):
"""
This method is used to serialize objects to JSON format.
@ -196,11 +200,11 @@ class _CustomEncoder(json.JSONEncoder):
Python object that JSON encoder will recognize
"""
if not (isinstance(obj, ModuleType)) and isinstance(
obj,
if not (isinstance(o, ModuleType)) and isinstance(
o,
(MethodType, FunctionType),
):
cvars = inspect.getclosurevars(obj)
cvars = inspect.getclosurevars(o)
cvardict = {**copy.copy(cvars.globals), **copy.copy(cvars.nonlocals)}
for i in list(cvardict):
# NOTE : All module types objects are removed, because otherwise it
@ -208,7 +212,7 @@ class _CustomEncoder(json.JSONEncoder):
if isinstance(cvardict[i], ModuleType):
del cvardict[i]
try:
code = inspect.getsource(obj)
code = inspect.getsource(o)
except (OSError, TypeError):
# This happens when rendering videos included in the documentation
# within doctests and should be replaced by a solution avoiding
@ -216,23 +220,23 @@ class _CustomEncoder(json.JSONEncoder):
# See https://github.com/ManimCommunity/manim/pull/402.
code = ""
return self._cleaned_iterable({"code": code, "nonlocals": cvardict})
elif isinstance(obj, np.ndarray):
if obj.size > 1000:
obj = np.resize(obj, (100, 100))
return f"TRUNCATED ARRAY: {repr(obj)}"
elif isinstance(o, np.ndarray):
if o.size > 1000:
o = np.resize(o, (100, 100))
return f"TRUNCATED ARRAY: {repr(o)}"
# We return the repr and not a list to avoid the JsonEncoder to iterate over it.
return repr(obj)
elif hasattr(obj, "__dict__"):
temp = getattr(obj, "__dict__")
return repr(o)
elif hasattr(o, "__dict__"):
temp = getattr(o, "__dict__")
# MappingProxy is scene-caching nightmare. It contains all of the object methods and attributes. We skip it as the mechanism will at some point process the object, but instantiated.
# Indeed, there is certainly no case where scene-caching will receive only a non instancied object, as this is never used in the library or encouraged to be used user-side.
if isinstance(temp, MappingProxyType):
return "MappingProxy"
return self._cleaned_iterable(temp)
elif isinstance(obj, np.uint8):
return int(obj)
elif isinstance(o, np.uint8):
return int(o)
# Serialize it with only the type of the object. You can change this to whatever string when debugging the serialization process.
return str(type(obj))
return str(type(o))
def _cleaned_iterable(self, iterable: typing.Iterable[Any]):
"""Check for circular reference at each iterable that will go through the JSONEncoder, as well as key of the wrong format.
@ -325,8 +329,8 @@ def get_json(obj: dict):
def get_hash_from_play_call(
scene_object: Scene,
camera_object: Camera,
animations_list: typing.Iterable[Animation],
current_mobjects_list: typing.Iterable[Mobject],
animations_list: typing.Iterable[AnimationProtocol],
current_mobjects_list: typing.Iterable[OpenGLMobject],
) -> str:
"""Take the list of animations and a list of mobjects and output their hashes. This is meant to be used for `scene.play` function.

View file

@ -10,6 +10,7 @@ from typing import Any
from manim import config, logger, tempconfig
from manim.__main__ import main
from manim.manager import Manager
from manim.renderer.shader import shader_program_cache
__all__ = ["ManimMagic"]
@ -127,19 +128,20 @@ else:
args = main(modified_args, standalone_mode=False, prog_name="manim")
with tempconfig(local_ns.get("config", {})):
config.digest_args(args)
manager: Manager | None = None
try:
SceneClass = local_ns[config["scene_names"][0]]
scene = SceneClass()
scene.render()
manager = Manager(SceneClass)
manager.render()
finally:
# Shader cache becomes invalid as the context is destroyed
shader_program_cache.clear()
# Close OpenGL window here instead of waiting for the main thread to
# finish causing the window to stay open and freeze
if scene.window is not None:
scene.window.close()
if manager is not None and manager.window is not None:
manager.window.close()
if config["output_file"] is None:
logger.info("No output file produced")
@ -166,7 +168,11 @@ else:
# set explicitly.
embed = "google.colab" in str(get_ipython())
if file_type.startswith("image"):
if file_type is None:
raise Exception(
"Could not guess file type, please contact the developers"
)
elif file_type.startswith("image"):
result = Image(filename=config["output_file"])
else:
result = Video(

View file

@ -6,15 +6,24 @@ import re
import sys
import types
import warnings
from pathlib import Path
from typing import TYPE_CHECKING
from manim import config, console, constants, logger
from manim.file_writer import FileWriter
if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
from pathlib import Path
from typing_extensions import Any
from manim.scene.scene import Scene
from .. import config, console, constants, logger
from ..scene.scene_file_writer import SceneFileWriter
__all__ = ["scene_classes_from_file"]
def get_module(file_name: Path):
def get_module(file_name: Path) -> types.ModuleType:
if str(file_name) == "-":
module = types.ModuleType("input_scenes")
logger.info(
@ -56,10 +65,10 @@ def get_module(file_name: Path):
raise FileNotFoundError(f"{file_name} not found")
def get_scene_classes_from_module(module):
from ..scene.scene import Scene
def get_scene_classes_from_module(module: types.ModuleType) -> list[type[Scene]]:
from manim.scene.scene import Scene
def is_child_scene(obj, module):
def is_child_scene(obj: Any, module: types.ModuleType) -> bool:
return (
inspect.isclass(obj)
and issubclass(obj, Scene)
@ -73,7 +82,7 @@ def get_scene_classes_from_module(module):
]
def get_scenes_to_render(scene_classes):
def get_scenes_to_render(scene_classes: Sequence[type[Scene]]) -> list[type[Scene]]:
if not scene_classes:
logger.error(constants.NO_SCENE_MESSAGE)
return []
@ -97,9 +106,9 @@ def get_scenes_to_render(scene_classes):
return prompt_user_for_choice(scene_classes)
def prompt_user_for_choice(scene_classes):
def prompt_user_for_choice(scene_classes: Iterable[type[Scene]]) -> list[type[Scene]]:
num_to_class = {}
SceneFileWriter.force_output_as_scene_name = True
FileWriter.force_output_as_scene_name = True
for count, scene_class in enumerate(scene_classes, 1):
name = scene_class.__name__
console.print(f"{count}: {name}", style="logging.level.info")
@ -125,8 +134,8 @@ def prompt_user_for_choice(scene_classes):
def scene_classes_from_file(
file_path: Path, require_single_scene=False, full_list=False
):
file_path: Path, require_single_scene: bool = False, full_list: bool = False
) -> type[Scene] | list[type[Scene]]:
module = get_module(file_path)
all_scene_classes = get_scene_classes_from_module(module)
if full_list:

View file

@ -114,9 +114,7 @@ def clip(a, min_a, max_a):
return a
def fdiv(
a: Scalable, b: Scalable, zero_over_zero_value: Scalable | None = None
) -> Scalable:
def fdiv(a: float, b: float, zero_over_zero_value: float | None = None) -> float:
if zero_over_zero_value is not None:
out = np.full_like(a, zero_over_zero_value)
where = np.logical_or(a != 0, b != 0)

View file

@ -6,13 +6,14 @@ __all__ = [
"get_full_sound_file_path",
]
from pathlib import Path
from .. import config
from ..utils.file_ops import seek_full_path_from_defaults
from manim import config
from manim.utils.file_ops import seek_full_path_from_defaults
# Still in use by add_sound() function in scene_file_writer.py
def get_full_sound_file_path(sound_file_name):
def get_full_sound_file_path(sound_file_name: str) -> Path:
return seek_full_path_from_defaults(
sound_file_name,
default_dir=config.get_dir("assets_dir"),

View file

@ -301,16 +301,6 @@ def get_norm(vector: np.ndarray) -> float:
return np.linalg.norm(vector)
def normalize(vect: list[float], fall_back: list[float] | None = None) -> np.ndarray:
norm = get_norm(vect)
if norm > 0:
return np.array(vect) / norm
elif fall_back is not None:
return np.array(fall_back)
else:
return np.zeros(len(vect))
def z_to_vector(vector: np.ndarray) -> np.ndarray:
"""
Returns some matrix in SO(3) which takes the z-axis to the
@ -373,12 +363,16 @@ def angle_between_vectors(v1: np.ndarray, v2: np.ndarray) -> float:
)
def normalize(vect: np.ndarray | tuple[float], fall_back=None) -> np.ndarray:
norm = np.linalg.norm(vect)
def normalize(
vect: npt.NDArray[float], fall_back: npt.NDArray[float] | None = None
) -> npt.NDArray[float]:
norm = get_norm(vect)
if norm > 0:
return np.array(vect) / norm
elif fall_back is not None:
return np.array(fall_back)
else:
return fall_back or np.zeros(len(vect))
return np.zeros(len(vect))
def normalize_along_axis(array: np.ndarray, axis: np.ndarray) -> np.ndarray:

View file

@ -7,9 +7,12 @@ from pathlib import Path
import numpy as np
from manim import logger
from manim.typing import PixelArray
from ._show_diff import show_diff_helper
__all__ = ["_FramesTester", "_ControlDataWriter"]
FRAME_ABSOLUTE_TOLERANCE = 1.01
FRAME_MISMATCH_RATIO_TOLERANCE = 1e-5
@ -37,7 +40,7 @@ class _FramesTester:
f"when there are {self._number_frames} control frames for this test."
)
def check_frame(self, frame_number: int, frame: np.ndarray):
def check_frame(self, frame_number: int, frame: PixelArray):
assert frame_number < self._number_frames, (
f"The tested scene is at frame number {frame_number} "
f"when there are {self._number_frames} control frames."
@ -74,7 +77,7 @@ class _FramesTester:
self._frames[frame_number],
self._file_path.name,
)
raise e
raise e from e
class _ControlDataWriter(_FramesTester):
@ -84,7 +87,7 @@ class _ControlDataWriter(_FramesTester):
self._number_frames_written: int = 0
# Actually write a frame.
def check_frame(self, index: int, frame: np.ndarray):
def check_frame(self, frame_number: int, frame: np.ndarray):
frame = frame[np.newaxis, ...]
self.frames = np.concatenate((self.frames, frame))
self._number_frames_written += 1

View file

@ -5,6 +5,8 @@ import warnings
import numpy as np
__all__ = ["show_diff_helper"]
def show_diff_helper(
frame_number: int,

View file

@ -2,76 +2,61 @@ from __future__ import annotations
from typing import Callable
from manim.file_writer.protocols import FileWriterProtocol
from manim.scene.scene import Scene
from manim.scene.scene_file_writer import SceneFileWriter
from ._frames_testers import _FramesTester
__all__ = ["_make_test_scene_class", "_make_scene_file_writer_class"]
def _make_test_scene_class(
base_scene: type[Scene],
construct_test: Callable[[Scene], None],
test_renderer,
construct_test: Callable[[Scene], object],
) -> type[Scene]:
class _TestedScene(base_scene):
def __init__(self, *args, **kwargs):
super().__init__(renderer=test_renderer, *args, **kwargs)
def construct(self):
from manim import config
construct_test(self)
# Manim hack to render the very last frame (normally the last frame is not the very end of the animation)
if self.animations is not None:
self.update_to_time(self.get_run_time(self.animations))
self.renderer.render(self, 1, self.moving_mobjects)
self.wait(1 / config.frame_rate)
return _TestedScene
def _make_test_renderer_class(from_renderer):
# Just for inheritance.
class _TestRenderer(from_renderer):
pass
return _TestRenderer
class DummySceneFileWriter(SceneFileWriter):
class DummySceneFileWriter(FileWriterProtocol):
"""Delegate of SceneFileWriter used to test the frames."""
def __init__(self, renderer, scene_name, **kwargs):
super().__init__(renderer, scene_name, **kwargs)
self.i = 0
def __init__(self, scene_name: str):
# we still need num_plays to satisfy the protocol
self.num_plays = 0
self.frames = 0
def init_output_directories(self, scene_name):
def begin_animation(self, allow_write: bool = False):
pass
def add_partial_movie_file(self, hash_animation):
def end_animation(self, allow_write: bool = False):
self.num_plays += 1
def is_already_cached(self, hash_invocation: str) -> bool:
return False
def add_partial_movie_file(self, hash_animation: str) -> None:
pass
def begin_animation(self, allow_write=True):
def write_frame(self, frame):
self.frames += 1
def finish(self):
pass
def end_animation(self, allow_write):
pass
def combine_to_movie(self):
pass
def combine_to_section_videos(self):
pass
def clean_cache(self):
pass
def write_frame(self, frame_or_renderer, num_frames=1):
self.i += 1
def _make_scene_file_writer_class(tester: _FramesTester) -> type[SceneFileWriter]:
def _make_scene_file_writer_class(tester: _FramesTester) -> type[FileWriterProtocol]:
class TestSceneFileWriter(DummySceneFileWriter):
def write_frame(self, frame_or_renderer, num_frames=1):
tester.check_frame(self.i, frame_or_renderer)
super().write_frame(frame_or_renderer, num_frames=num_frames)
def write_frame(self, frame):
tester.check_frame(self.frames, frame)
super().write_frame(frame)
return TestSceneFileWriter

View file

@ -5,37 +5,31 @@ import inspect
from pathlib import Path
from typing import Callable
import cairo
import pytest
from _pytest.fixtures import FixtureRequest
from manim import Scene
from manim import Manager, Scene
from manim._config import tempconfig
from manim._config.utils import ManimConfig
from manim.camera.three_d_camera import ThreeDCamera
from manim.renderer.cairo_renderer import CairoRenderer
from manim.scene.three_d_scene import ThreeDScene
from ._frames_testers import _ControlDataWriter, _FramesTester
from ._test_class_makers import (
DummySceneFileWriter,
_make_scene_file_writer_class,
_make_test_renderer_class,
_make_test_scene_class,
)
__all__ = ["frames_comparison"]
SCENE_PARAMETER_NAME = "scene"
_tests_root_dir_path = Path(__file__).absolute().parents[2]
PATH_CONTROL_DATA = _tests_root_dir_path / Path("control_data", "graphical_units_data")
MIN_CAIRO_VERSION = 11800
def frames_comparison(
func=None,
func: Callable[..., object] | None = None,
*,
last_frame: bool = True,
renderer_class=CairoRenderer,
base_scene=Scene,
base_scene: type[Scene] = Scene,
**custom_config,
):
"""Compares the frames generated by the test with control frames previously registered.
@ -44,14 +38,12 @@ def frames_comparison(
control frames for a given test, pass ``--set_test`` flag to pytest
while running the test.
Note that this decorator can be use with or without parentheses.
Note that this decorator can be used with or without parentheses.
Parameters
----------
last_frame
whether the test should test the last frame, by default True.
renderer_class
The base renderer to use (OpenGLRenderer/CairoRenderer), by default CairoRenderer
base_scene
The base class for the scene (ThreeDScene, etc.), by default Scene
@ -65,8 +57,8 @@ def frames_comparison(
SCENE_PARAMETER_NAME
not in inspect.getfullargspec(tested_scene_construct).args
):
raise Exception(
f"Invalid graphical test function test function : must have '{SCENE_PARAMETER_NAME}'as one of the parameters.",
raise ValueError(
f"Invalid graphical test function test function : must have {SCENE_PARAMETER_NAME!r} as one of the parameters.",
)
# Exclude "scene" from the argument list of the signature.
@ -74,22 +66,17 @@ def frames_comparison(
functools.partial(tested_scene_construct, scene=None),
)
if "__module_test__" not in tested_scene_construct.__globals__:
raise Exception(
module_name = tested_scene_construct.__globals__.get("__module_test__")
if module_name is None:
raise AttributeError(
"There is no module test name indicated for the graphical unit test. You have to declare __module_test__ in the test file.",
)
module_name = tested_scene_construct.__globals__.get("__module_test__")
test_name = tested_scene_construct.__name__[len("test_") :]
test_name = tested_scene_construct.__name__.removeprefix("test_")
@functools.wraps(tested_scene_construct)
# The "request" parameter is meant to be used as a fixture by pytest. See below.
def wrapper(*args, request: FixtureRequest, tmp_path, **kwargs):
# check for cairo version
if (
renderer_class is CairoRenderer
and cairo.cairo_version() < MIN_CAIRO_VERSION
):
pytest.skip("Cairo version is too old. Skipping cairo graphical tests.")
def wrapper(*args, request: pytest.FixtureRequest, tmp_path, **kwargs):
# Wraps the test_function to a construct method, to "freeze" the eventual additional arguments (parametrizations fixtures).
construct = functools.partial(tested_scene_construct, *args, **kwargs)
@ -99,23 +86,21 @@ def frames_comparison(
# Example: if "length" is parametrized from 0 to 20, the kwargs
# will be once with {"length" : 1}, etc.
test_name_with_param = test_name + "_".join(
f"_{str(tup[0])}[{str(tup[1])}]" for tup in kwargs.items()
f"_{k}[{v}]" for k, v in kwargs.items()
)
config_tests = _config_test(last_frame)
config_tests["text_dir"] = tmp_path
config_tests["tex_dir"] = tmp_path
config_tests.text_dir = tmp_path
config_tests.tex_dir = tmp_path
if last_frame:
config_tests["frame_rate"] = 1
config_tests["dry_run"] = True
config_tests.frame_rate = 1
else:
config_tests.write_to_movie = True
setting_test = request.config.getoption("--set_test")
try:
test_file_path = tested_scene_construct.__globals__["__file__"]
except Exception:
test_file_path = None
test_file_path = tested_scene_construct.__globals__.get("__file__")
real_test = _make_test_comparing_frames(
file_path=_control_data_path(
test_file_path,
@ -125,7 +110,6 @@ def frames_comparison(
),
base_scene=base_scene,
construct=construct,
renderer_class=renderer_class,
is_set_test_data_test=setting_test,
last_frame=last_frame,
show_diff=request.config.getoption("--show_diff"),
@ -146,13 +130,13 @@ def frames_comparison(
inspect.Parameter("tmp_path", inspect.Parameter.KEYWORD_ONLY),
]
new_sig = old_sig.replace(parameters=parameters)
wrapper.__signature__ = new_sig
wrapper.__signature__ = new_sig # type: ignore
# Reach a bit into pytest internals to hoist the marks from our wrapped
# function.
setattr(wrapper, "pytestmark", [])
wrapper.pytestmark = [] # type: ignore # Do we really need this?
new_marks = getattr(tested_scene_construct, "pytestmark", [])
wrapper.pytestmark = new_marks
wrapper.pytestmark = new_marks # type: ignore
return wrapper
# Case where the decorator is called with and without parentheses.
@ -165,8 +149,7 @@ def frames_comparison(
def _make_test_comparing_frames(
file_path: Path,
base_scene: type[Scene],
construct: Callable[[Scene], None],
renderer_class: type, # Renderer type, there is no superclass renderer yet .....
construct: Callable[[Scene], object],
is_set_test_data_test: bool,
last_frame: bool,
show_diff: bool,
@ -203,30 +186,20 @@ def _make_test_comparing_frames(
if not last_frame
else DummySceneFileWriter
)
testRenderer = _make_test_renderer_class(renderer_class)
def real_test():
with frames_tester.testing():
sceneTested = _make_test_scene_class(
scene_tested: type[Scene] = _make_test_scene_class(
base_scene=base_scene,
construct_test=construct,
# NOTE this is really ugly but it's due to the very bad design of the two renderers.
# If you pass a custom renderer to the Scene, the Camera class given as an argument in the Scene
# is not passed to the renderer. See __init__ of Scene.
# This potentially prevents OpenGL testing.
test_renderer=(
testRenderer(file_writer_class=file_writer_class)
if base_scene is not ThreeDScene
else testRenderer(
file_writer_class=file_writer_class,
camera_class=ThreeDCamera,
)
), # testRenderer(file_writer_class=file_writer_class),
)
scene_tested = sceneTested(skip_animations=True)
scene_tested.render()
manager = Manager(scene_tested)
manager.file_writer = file_writer_class(
manager.scene.get_default_scene_name()
)
manager.render()
if last_frame:
frames_tester.check_frame(-1, scene_tested.renderer.get_frame())
frames_tester.check_frame(-1, manager.renderer.get_pixels())
return real_test

1029
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -45,7 +45,6 @@ networkx = ">=2.6"
notebook = { version = ">=6.0.0", optional = true }
numpy = ">=1.26"
Pillow = ">=9.1"
pycairo = ">=1.13,<2.0.0"
pyopengl = "^3.1.6"
pydub = ">=0.20.0"
Pygments = ">=2.0.0"
@ -58,6 +57,7 @@ svgelements = ">=1.8.0"
tqdm = ">=4.0.0"
typing-extensions = ">=4.0.0"
watchdog = ">=2.0.0"
pydantic = "^2.8.0"
[tool.poetry.extras]
jupyterlab = ["jupyterlab", "notebook"]
@ -103,6 +103,7 @@ types-Pygments = "^2.17.0.0"
[tool.pytest.ini_options]
markers = "slow: Mark the test as slow. Can be skipped with --skip_slow"
addopts = "--no-cov-on-fail --cov=manim --cov-report xml --cov-report term -n auto --dist=loadfile --durations=0"
doctest_optionflags = "IGNORE_EXCEPTION_DETAIL"
[tool.isort]
profile = "black"

View file

@ -5,7 +5,7 @@ from pathlib import Path
import pytest
from manim import config, tempconfig
import manim
def pytest_addoption(parser):
@ -45,6 +45,20 @@ def pytest_collection_modifyitems(config, items):
item.add_marker(slow_skip)
@pytest.fixture
def config():
saved = manim.config.copy()
# we need to return the actual config so that tests
# using tempconfig pass
yield manim.config
manim.config.update(saved)
@pytest.fixture
def dry_run(config):
config.dry_run = True
@pytest.fixture(scope="session")
def python_version():
# use the same python executable as it is running currently
@ -63,9 +77,9 @@ def reset_cfg_file():
@pytest.fixture
def using_opengl_renderer():
"""Standard fixture for running with opengl that makes tests use a standard_config.cfg with a temp dir."""
with tempconfig({"renderer": "opengl"}):
yield
# as a special case needed to manually revert back to cairo
# due to side effects of setting the renderer
config.renderer = "cairo"
"""Standard fixture for running with opengl that makes tests use a standard_config.cfg with a temp dir.
.. warning::
As of experimental, this fixture is deprecated and should not be using
"""

View file

@ -7,11 +7,11 @@ from pathlib import Path
import numpy as np
from manim import config, logger
from manim import Manager, logger
from manim.scene.scene import Scene
def set_test_scene(scene_object: type[Scene], module_name: str):
def set_test_scene(scene_object: type[Scene], module_name: str, config):
"""Function used to set up the test data for a new feature. This will basically set up a pre-rendered frame for a scene. This is meant to be used only
when setting up tests. Please refer to the wiki.
@ -29,29 +29,29 @@ def set_test_scene(scene_object: type[Scene], module_name: str):
set_test_scene(DotTest, "geometry")
"""
config["write_to_movie"] = False
config["disable_caching"] = True
config["format"] = "png"
config["pixel_height"] = 480
config["pixel_width"] = 854
config["frame_rate"] = 15
config.write_to_movie = False
config.disable_caching = True
config.format = "png"
config.pixel_height = 480
config.pixel_width = 854
config.frame_rate = 15
with tempfile.TemporaryDirectory() as tmpdir:
temp_path = Path(tmpdir)
config["text_dir"] = temp_path / "text"
config["tex_dir"] = temp_path / "tex"
scene = scene_object(skip_animations=True)
scene.render()
data = scene.renderer.get_frame()
manager = Manager(scene_object)
manager.render()
data = manager.renderer.get_pixels()
assert not np.all(
data == np.array([0, 0, 0, 255]),
), f"Control data generated for {str(scene)} only contains empty pixels."
), f"Control data generated for {manager.scene!s} only contains empty pixels."
assert data.shape == (480, 854, 4)
tests_directory = Path(__file__).absolute().parent.parent
path_control_data = Path(tests_directory) / "control_data" / "graphical_units_data"
path = Path(path_control_data) / module_name
if not path.is_dir():
path.mkdir(parents=True)
np.savez_compressed(path / str(scene), frame_data=data)
logger.info(f"Test data for {str(scene)} saved in {path}\n")
np.savez_compressed(path / str(manager.scene), frame_data=data)
logger.info(f"Test data for {str(manager.scene)} saved in {path}\n")

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import pytest
from manim import FadeIn, Scene, config
from manim import FadeIn, Manager, Scene
@pytest.mark.parametrize(
@ -10,13 +10,15 @@ from manim import FadeIn, Scene, config
[0, -1],
)
def test_animation_forbidden_run_time(run_time):
test_scene = Scene()
manager = Manager(Scene)
test_scene = manager.scene
with pytest.raises(ValueError, match="Please set the run_time to be positive"):
test_scene.play(FadeIn(None, run_time=run_time))
def test_animation_run_time_shorter_than_frame_rate(caplog):
test_scene = Scene()
def test_animation_run_time_shorter_than_frame_rate(caplog, config):
manager = Manager(Scene)
test_scene = manager.scene
test_scene.play(FadeIn(None, run_time=1 / (config.frame_rate + 1)))
assert (
"Original run time of FadeIn(Mobject) is shorter than current frame rate"
@ -26,7 +28,8 @@ def test_animation_run_time_shorter_than_frame_rate(caplog):
@pytest.mark.parametrize("frozen_frame", [False, True])
def test_wait_run_time_shorter_than_frame_rate(caplog, frozen_frame):
test_scene = Scene()
manager = Manager(Scene)
test_scene = manager.scene
test_scene.wait(1e-9, frozen_frame=frozen_frame)
assert (
"Original run time of Wait(Mobject) is shorter than current frame rate"

View file

@ -4,15 +4,24 @@ from unittest.mock import MagicMock
import pytest
from manim.animation.animation import Animation, Wait
from manim.animation.composition import AnimationGroup, Succession
from manim.animation.creation import Create, Write
from manim.animation.fading import FadeIn, FadeOut
from manim.constants import DOWN, UP
from manim.mobject.geometry.arc import Circle
from manim.mobject.geometry.line import Line
from manim.mobject.geometry.polygram import RegularPolygon, Square
from manim.scene.scene import Scene
from manim import (
DOWN,
UP,
Animation,
AnimationGroup,
Circle,
Create,
FadeIn,
FadeOut,
Line,
Manager,
RegularPolygon,
Scene,
Square,
Succession,
Wait,
Write,
)
def test_succession_timing():
@ -22,7 +31,6 @@ def test_succession_timing():
animation_4s = FadeOut(line, shift=DOWN, run_time=4.0)
succession = Succession(animation_1s, animation_4s)
assert succession.get_run_time() == 5.0
succession._setup_scene(MagicMock())
succession.begin()
assert succession.active_index == 0
# The first animation takes 20% of the total run time.
@ -138,7 +146,8 @@ def test_animationgroup_with_wait():
def test_animationgroup_is_passing_remover_to_animations(
animation_remover, animation_group_remover
):
scene = Scene()
manager = Manager(Scene)
scene = manager.scene
sqr_animation = Create(Square(), remover=animation_remover)
circ_animation = Write(Circle(), remover=animation_remover)
animation_group = AnimationGroup(
@ -153,7 +162,8 @@ def test_animationgroup_is_passing_remover_to_animations(
def test_animationgroup_is_passing_remover_to_nested_animationgroups():
scene = Scene()
manager = Manager(Scene)
scene = manager.scene
sqr_animation = Create(Square())
circ_animation = Write(Circle(), remover=True)
polygon_animation = Create(RegularPolygon(5))

View file

@ -3,7 +3,7 @@ from __future__ import annotations
import numpy as np
import pytest
from manim import AddTextLetterByLetter, Text, config
from manim import AddTextLetterByLetter, Text
def test_non_empty_text_creation():
@ -25,7 +25,7 @@ def test_whitespace_text_creation():
AddTextLetterByLetter(Text(" "))
def test_run_time_for_non_empty_text():
def test_run_time_for_non_empty_text(config):
"""Ensure the run_time is calculated correctly for non-empty text."""
s = Text("Hello")
run_time_per_char = 0.1

View file

@ -2,7 +2,7 @@ from __future__ import annotations
from pathlib import Path
from manim import BraceLabel, Mobject, config
from manim import BraceLabel, Mobject
def test_mobject_copy():
@ -18,7 +18,7 @@ def test_mobject_copy():
assert orig.submobjects[i] is not copy.submobjects[i]
def test_bracelabel_copy(tmp_path):
def test_bracelabel_copy(tmp_path, config):
"""Test that a copy is a deepcopy."""
# For this test to work, we need to tweak some folders temporarily
original_text_dir = config["text_dir"]

View file

@ -1,20 +1,20 @@
from __future__ import annotations
from manim import Mobject, config, tempconfig
from manim import Mobject
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
def test_metaclass_registry():
def test_metaclass_registry(config):
class SomeTestMobject(Mobject, metaclass=ConvertToOpenGL):
pass
assert SomeTestMobject in ConvertToOpenGL._converted_classes
with tempconfig({"renderer": "opengl"}):
assert OpenGLMobject in SomeTestMobject.__bases__
assert Mobject not in SomeTestMobject.__bases__
config.renderer = "opengl"
assert OpenGLMobject in SomeTestMobject.__bases__
assert Mobject not in SomeTestMobject.__bases__
config.renderer = "cairo"
assert Mobject in SomeTestMobject.__bases__
assert OpenGLMobject not in SomeTestMobject.__bases__
config.renderer = "cairo"
assert Mobject in SomeTestMobject.__bases__
assert OpenGLMobject not in SomeTestMobject.__bases__

View file

@ -2,13 +2,11 @@ from __future__ import annotations
import numpy as np
from manim import RendererType, config
from manim.constants import RIGHT
from manim.mobject.geometry.polygram import Square
def test_Data():
config.renderer = RendererType.OPENGL
def test_Data(using_opengl_renderer):
a = Square().move_to(RIGHT)
data_bb = a.data["bounding_box"]
np.testing.assert_array_equal(
@ -39,6 +37,3 @@ def test_Data():
)
np.testing.assert_array_equal(a.bounding_box, data_bb)
config.renderer = (
RendererType.CAIRO
) # needs to be here or else the following cairo tests fail

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import pytest
from manim import DiGraph, Graph, Scene, Text, tempconfig
from manim import DiGraph, Graph, Manager, Scene, Text, tempconfig
from manim.mobject.graph import _layouts
@ -93,7 +93,8 @@ def test_graph_remove_edges():
def test_custom_animation_mobject_list():
G = Graph([1, 2, 3], [(1, 2), (2, 3)])
scene = Scene()
manager = Manager(Scene)
scene = manager.scene
scene.add(G)
assert scene.mobjects == [G]
with tempconfig({"dry_run": True, "quality": "low_quality"}):

View file

@ -5,15 +5,15 @@ from pathlib import Path
import numpy as np
import pytest
from manim import MathTex, SingleStringMathTex, Tex, TexTemplate, config, tempconfig
from manim import MathTex, SingleStringMathTex, Tex, TexTemplate, tempconfig
def test_MathTex():
def test_MathTex(config):
MathTex("a^2 + b^2 = c^2")
assert Path(config.media_dir, "Tex", "e4be163a00cf424f.svg").exists()
def test_SingleStringMathTex():
def test_SingleStringMathTex(config):
SingleStringMathTex("test")
assert Path(config.media_dir, "Tex", "8ce17c7f5013209f.svg").exists()
@ -27,7 +27,7 @@ def test_double_braces_testing(text_input, length_sub):
assert len(t1.submobjects) == length_sub
def test_tex():
def test_tex(config):
Tex("The horse does not eat cucumber salad.")
assert Path(config.media_dir, "Tex", "c3945e23e546c95a.svg").exists()
@ -45,7 +45,7 @@ def test_tex_temp_directory(tmpdir, monkeypatch):
assert Path("media", "Tex", "c3945e23e546c95a.svg").exists()
def test_percent_char_rendering():
def test_percent_char_rendering(config):
Tex(r"\%")
assert Path(config.media_dir, "Tex", "4a583af4d19a3adf.tex").exists()
@ -194,7 +194,7 @@ def test_error_in_nested_context(capsys):
\end{align}
"""
with pytest.raises(ValueError) as err:
with pytest.raises(ValueError):
Tex(invalid_tex)
stdout = str(capsys.readouterr().out)
@ -202,25 +202,25 @@ def test_error_in_nested_context(capsys):
assert r"\begin{frame}" not in stdout
def test_tempconfig_resetting_tex_template():
def test_tempconfig_resetting_tex_template(config):
my_template = TexTemplate()
my_template.preamble = "Custom preamble!"
tex_template_config_value = config.tex_template
with tempconfig({"tex_template": my_template}):
assert config.tex_template.preamble == "Custom preamble!"
assert config.tex_template.preamble != "Custom preamble!"
def test_tex_garbage_collection(tmpdir, monkeypatch):
def test_tex_garbage_collection(tmpdir, monkeypatch, config):
monkeypatch.chdir(tmpdir)
Path(tmpdir, "media").mkdir()
config.media_dir = "media"
with tempconfig({"media_dir": "media"}):
tex_without_log = Tex("Hello World!") # d771330b76d29ffb.tex
assert Path("media", "Tex", "d771330b76d29ffb.tex").exists()
assert not Path("media", "Tex", "d771330b76d29ffb.log").exists()
tex_without_log = Tex("Hello World!") # d771330b76d29ffb.tex
assert Path("media", "Tex", "d771330b76d29ffb.tex").exists()
assert not Path("media", "Tex", "d771330b76d29ffb.log").exists()
with tempconfig({"media_dir": "media", "no_latex_cleanup": True}):
tex_with_log = Tex("Hello World, again!") # da27670a37b08799.tex
assert Path("media", "Tex", "da27670a37b08799.log").exists()
config.no_latex_cleanup = True
tex_with_log = Tex("Hello World, again!") # da27670a37b08799.tex
assert Path("media", "Tex", "da27670a37b08799.log").exists()

View file

@ -10,7 +10,8 @@ def test_zoom():
s2.set_x(10)
with tempconfig({"dry_run": True, "quality": "low_quality"}):
scene = MovingCameraScene()
manager = Manager(MovingCameraScene)
scene = manager.scene
scene.add(s1, s2)
scene.play(scene.camera.auto_zoom([s1, s2]))

View file

@ -4,104 +4,105 @@ import datetime
import pytest
from manim import Circle, FadeIn, Group, Mobject, Scene, Square, tempconfig
from manim import Circle, FadeIn, Group, Manager, Mobject, Scene, Square
from manim.animation.animation import Wait
def test_scene_add_remove():
with tempconfig({"dry_run": True}):
scene = Scene()
assert len(scene.mobjects) == 0
scene.add(Mobject())
assert len(scene.mobjects) == 1
scene.add(*(Mobject() for _ in range(10)))
assert len(scene.mobjects) == 11
def test_scene_add_remove(dry_run):
manager = Manager(Scene)
scene = manager.scene
assert len(scene.mobjects) == 0
scene.add(Mobject())
assert len(scene.mobjects) == 1
scene.add(*(Mobject() for _ in range(10)))
assert len(scene.mobjects) == 11
# Check that adding a mobject twice does not actually add it twice
repeated = Mobject()
scene.add(repeated)
assert len(scene.mobjects) == 12
scene.add(repeated)
assert len(scene.mobjects) == 12
# Check that adding a mobject twice does not actually add it twice
repeated = Mobject()
scene.add(repeated)
assert len(scene.mobjects) == 12
scene.add(repeated)
assert len(scene.mobjects) == 12
# Check that Scene.add() returns the Scene (for chained calls)
assert scene.add(Mobject()) is scene
to_remove = Mobject()
scene = Scene()
scene.add(to_remove)
scene.add(*(Mobject() for _ in range(10)))
assert len(scene.mobjects) == 11
scene.remove(to_remove)
assert len(scene.mobjects) == 10
scene.remove(to_remove)
assert len(scene.mobjects) == 10
# Check that Scene.add() returns the Scene (for chained calls)
assert scene.add(Mobject()) is scene
to_remove = Mobject()
manager = Manager(Scene)
scene = manager.scene
scene.add(to_remove)
scene.add(*(Mobject() for _ in range(10)))
assert len(scene.mobjects) == 11
scene.remove(to_remove)
assert len(scene.mobjects) == 10
scene.remove(to_remove)
assert len(scene.mobjects) == 10
# Check that Scene.remove() returns the instance (for chained calls)
assert scene.add(Mobject()) is scene
# Check that Scene.remove() returns the instance (for chained calls)
assert scene.add(Mobject()) is scene
def test_scene_time():
with tempconfig({"dry_run": True}):
scene = Scene()
assert scene.renderer.time == 0
scene.wait(2)
assert scene.renderer.time == 2
scene.play(FadeIn(Circle()), run_time=0.5)
assert pytest.approx(scene.renderer.time) == 2.5
scene.renderer._original_skipping_status = True
scene.play(FadeIn(Square()), run_time=5) # this animation gets skipped.
assert pytest.approx(scene.renderer.time) == 7.5
def test_scene_time(dry_run):
manager = Manager(Scene)
scene = manager.scene
assert scene.renderer.time == 0
scene.wait(2)
assert scene.renderer.time == 2
scene.play(FadeIn(Circle()), run_time=0.5)
assert pytest.approx(scene.renderer.time) == 2.5
scene.renderer._original_skipping_status = True
scene.play(FadeIn(Square()), run_time=5) # this animation gets skipped.
assert pytest.approx(scene.renderer.time) == 7.5
def test_subcaption():
with tempconfig({"dry_run": True}):
scene = Scene()
scene.add_subcaption("Testing add_subcaption", duration=1, offset=0)
scene.wait()
scene.play(
Wait(),
run_time=2,
subcaption="Testing Scene.play subcaption interface",
subcaption_duration=1.5,
subcaption_offset=0.5,
)
subcaptions = scene.renderer.file_writer.subcaptions
assert len(subcaptions) == 2
assert subcaptions[0].start == datetime.timedelta(seconds=0)
assert subcaptions[0].end == datetime.timedelta(seconds=1)
assert subcaptions[0].content == "Testing add_subcaption"
assert subcaptions[1].start == datetime.timedelta(seconds=1.5)
assert subcaptions[1].end == datetime.timedelta(seconds=3)
assert subcaptions[1].content == "Testing Scene.play subcaption interface"
def test_subcaption(dry_run):
manager = Manager(Scene)
scene = manager.scene
scene.add_subcaption("Testing add_subcaption", duration=1, offset=0)
scene.wait()
scene.play(
Wait(),
run_time=2,
subcaption="Testing Scene.play subcaption interface",
subcaption_duration=1.5,
subcaption_offset=0.5,
)
subcaptions = scene.renderer.file_writer.subcaptions
assert len(subcaptions) == 2
assert subcaptions[0].start == datetime.timedelta(seconds=0)
assert subcaptions[0].end == datetime.timedelta(seconds=1)
assert subcaptions[0].content == "Testing add_subcaption"
assert subcaptions[1].start == datetime.timedelta(seconds=1.5)
assert subcaptions[1].end == datetime.timedelta(seconds=3)
assert subcaptions[1].content == "Testing Scene.play subcaption interface"
def test_replace():
def test_replace(dry_run):
def assert_names(mobjs, names):
assert len(mobjs) == len(names)
for i in range(0, len(mobjs)):
assert mobjs[i].name == names[i]
with tempconfig({"dry_run": True}):
scene = Scene()
manager = Manager(Scene)
scene = manager.scene
first = Mobject(name="first")
second = Mobject(name="second")
third = Mobject(name="third")
fourth = Mobject(name="fourth")
first = Mobject(name="first")
second = Mobject(name="second")
third = Mobject(name="third")
fourth = Mobject(name="fourth")
scene.add(first)
scene.add(Group(second, third, name="group"))
scene.add(fourth)
assert_names(scene.mobjects, ["first", "group", "fourth"])
assert_names(scene.mobjects[1], ["second", "third"])
scene.add(first)
scene.add(Group(second, third, name="group"))
scene.add(fourth)
assert_names(scene.mobjects, ["first", "group", "fourth"])
assert_names(scene.mobjects[1], ["second", "third"])
alpha = Mobject(name="alpha")
beta = Mobject(name="beta")
alpha = Mobject(name="alpha")
beta = Mobject(name="beta")
scene.replace(first, alpha)
assert_names(scene.mobjects, ["alpha", "group", "fourth"])
assert_names(scene.mobjects[1], ["second", "third"])
scene.replace(first, alpha)
assert_names(scene.mobjects, ["alpha", "group", "fourth"])
assert_names(scene.mobjects[1], ["second", "third"])
scene.replace(second, beta)
assert_names(scene.mobjects, ["alpha", "group", "fourth"])
assert_names(scene.mobjects[1], ["beta", "third"])
scene.replace(second, beta)
assert_names(scene.mobjects, ["alpha", "group", "fourth"])
assert_names(scene.mobjects[1], ["beta", "third"])

View file

@ -4,7 +4,7 @@ import struct
import wave
from pathlib import Path
from manim import Scene
from manim import Manager, Scene
def test_add_sound(tmpdir):
@ -19,5 +19,6 @@ def test_add_sound(tmpdir):
f.close()
scene = Scene()
manager = Manager(Scene)
scene = manager.scene
scene.add_sound(sound_loc)

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