mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-22 10:01:47 +00:00
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 commit571f79be2c. * 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 commitdee29c390f. --------- 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 commite31c2077cd. * 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 commite53a1c8d6f. --------- 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 commit5fe256880d. * Revert "Revert "feat: Add animations that together simulate typing"" This reverts commit6a8244a157. * 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 commit904cfb46ae. * 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 commit904cfb46ae. * 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 commitf50efa4b88. * 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 commit48013f4a30. --------- 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:
parent
08264dcf76
commit
5dcab4c4a5
148 changed files with 4015 additions and 7382 deletions
4
.github/workflows/publish-docker.yml
vendored
4
.github/workflows/publish-docker.yml
vendored
|
|
@ -25,7 +25,7 @@ jobs:
|
|||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/arm64,linux/amd64
|
||||
push: true
|
||||
|
|
@ -61,7 +61,7 @@ jobs:
|
|||
print(f"tag_name={ref_tag}", file=f)
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/arm64,linux/amd64
|
||||
push: true
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
12
README.md
12
README.md
|
|
@ -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:
|
|||
|
||||

|
||||
|
||||
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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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()}",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -81,3 +81,4 @@ Index
|
|||
docs/examples
|
||||
docs/references
|
||||
docs/typings
|
||||
docs/types
|
||||
|
|
|
|||
134
docs/source/contributing/docs/types.rst
Normal file
134
docs/source/contributing/docs/types.rst
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
===================
|
||||
Choosing Type Hints
|
||||
===================
|
||||
In order to provide the best user experience,
|
||||
it's important that type hints are chosen correctly.
|
||||
With the large variety of types provided by Manim, choosing
|
||||
which one to use can be difficult. This guide aims to
|
||||
aid you in the process of choosing the right type for the scenario.
|
||||
|
||||
|
||||
The first step is figuring out which category your type hint fits into.
|
||||
|
||||
Coordinates
|
||||
-----------
|
||||
Coordinates encompass two main categories: points, and vectors.
|
||||
|
||||
|
||||
Points
|
||||
~~~~~~
|
||||
The purpose of points is pretty straightforward: they represent a point
|
||||
in space. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def status2D(coord: Point2D) -> None:
|
||||
x, y = coord
|
||||
print(f"Point at {x=},{y=}")
|
||||
|
||||
|
||||
def status3D(coord: Point3D) -> None:
|
||||
x, y, z = coord
|
||||
print(f"Point at {x=},{y=},{z=}")
|
||||
|
||||
|
||||
def get_statuses(coords: Point2D_Array | Point3D_Array) -> None:
|
||||
for coord in coords:
|
||||
if len(coord) == 2:
|
||||
# it's a Point2D
|
||||
status2D(coord)
|
||||
else:
|
||||
# it's a point3D
|
||||
status3D(coord)
|
||||
|
||||
It's important to realize that the status functions accepted both
|
||||
tuples/lists of the correct length, and ``NDArray``'s of the correct shape.
|
||||
If they only accepted ``NDArray``'s, we would use their ``Internal`` counterparts:
|
||||
:class:`~.typing.InternalPoint2D`, :class:`~.typing.InternalPoint3D`, :class:`~.typing.InternalPoint2D_Array` and :class:`~.typing.InternalPoint3D_Array`.
|
||||
|
||||
In general, the type aliases prefixed with ``Internal`` should never be used on
|
||||
user-facing classes and functions, but should be reserved for internal behavior.
|
||||
|
||||
Vectors
|
||||
~~~~~~~
|
||||
Vectors share many similarities to points. However, they have a different
|
||||
connotation. Vectors should be used to represent direction. For example,
|
||||
consider this slightly contrived function:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def shift_mobject(mob: Mobject, direction: Vector3D, scale_factor: float = 1) -> mob:
|
||||
return mob.shift(direction * scale_factor)
|
||||
|
||||
Here we see an important example of the difference. ``direction`` can not, and
|
||||
should not, be typed as a :class:`~.typing.Point3D` because the function does not accept tuples/lists,
|
||||
like ``direction=(0, 1, 0)``. You could type it as :class:`~.typing.InternalPoint3D` and
|
||||
the type checker and linter would be happy; however, this makes the code harder
|
||||
to understand.
|
||||
|
||||
As a general rule, if a parameter is called ``direction`` or ``axis``,
|
||||
it should be type hinted as some form of :class:`~.VectorND`.
|
||||
|
||||
.. warning::
|
||||
|
||||
This is not always true. For example, as of Manim 0.18.0, the direction
|
||||
parameter of the :class:`.Vector` Mobject should be ``Point2D | Point3D``,
|
||||
as it can also accept ``tuple[float, float]`` and ``tuple[float, float, float]``.
|
||||
|
||||
Colors
|
||||
------
|
||||
The interface Manim provides for working with colors is :class:`.ManimColor`.
|
||||
The main color types Manim supports are RGB, RGBA, and HSV. You will want
|
||||
to add type hints to a function depending on which type it uses. If any color will work,
|
||||
you will need something like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.utils.color import ParsableManimColor
|
||||
|
||||
# type hint stuff with ParsableManimColor
|
||||
|
||||
|
||||
|
||||
Béziers
|
||||
-------
|
||||
Manim internally represents a :class:`.Mobject` by a collection of points. In the case of :class:`.VMobject`,
|
||||
the most commonly used subclass of :class:`.Mobject`, these points represent Bézier curves,
|
||||
which are a way of representing a curve using a sequence of points.
|
||||
|
||||
.. note::
|
||||
|
||||
To learn more about Béziers, take a look at https://pomax.github.io/bezierinfo/
|
||||
|
||||
|
||||
Manim supports two different renderers, which each have different representations of
|
||||
Béziers: Cairo uses cubic Bézier curves, while OpenGL uses quadratic Bézier curves.
|
||||
|
||||
Type hints like :class:`~.typing.BezierPoints` represent a single bezier curve, and :class:`~.typing.BezierPath`
|
||||
represents multiple Bézier curves. A :class:`~.typing.Spline` is when the Bézier curves in a :class:`~.typing.BezierPath`
|
||||
forms a single connected curve. Manim also provides more specific type aliases when working with
|
||||
quadratic or cubic curves, and they are prefixed with their respective type (e.g. :class:`~.typing.CubicBezierPoints`,
|
||||
is a :class:`~.typing.BezierPoints` consisting of exactly 4 points representing a cubic Bézier curve).
|
||||
|
||||
|
||||
Functions
|
||||
---------
|
||||
Throughout the codebase, many different types of functions are used. The most obvious example
|
||||
is a rate function, which takes in a float and outputs a float (``Callable[[float], float]``).
|
||||
Another example is for overriding animations. One will often need to map a :class:`.Mobject`
|
||||
to an overridden :class:`.Animation`, and for that we have the :class:`~.typing.FunctionOverride` type hint.
|
||||
|
||||
:class:`~.typing.PathFuncType` and :class:`~.typing.MappingFunction` are more niche, but are related to moving objects
|
||||
along a path, or applying functions. If you need to use it, you'll know.
|
||||
|
||||
|
||||
Images
|
||||
------
|
||||
There are several representations of images in Manim. The most common is
|
||||
the representation as a NumPy array of floats representing the pixels of an image.
|
||||
This is especially common when it comes to the OpenGL renderer.
|
||||
|
||||
This is the use case of the :class:`~.typing.Image` type hint. Sometimes, Manim may use ``PIL.Image``,
|
||||
in which case one should use that type hint instead.
|
||||
Of course, if a more specific type of image is needed, it can be annotated as such.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
==============
|
||||
Adding Typings
|
||||
==============
|
||||
==================
|
||||
Typing Conventions
|
||||
==================
|
||||
|
||||
.. warning::
|
||||
This section is still a work in progress.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 *
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
16
manim/event_handler/window.py
Normal file
16
manim/event_handler/window.py
Normal 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: ...
|
||||
4
manim/file_writer/__init__.py
Normal file
4
manim/file_writer/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from .file_writer import FileWriter
|
||||
from .sections import *
|
||||
|
|
@ -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")
|
||||
31
manim/file_writer/protocols.py
Normal file
31
manim/file_writer/protocols.py
Normal 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: ...
|
||||
|
|
@ -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
413
manim/manager.py
Normal 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: ...
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 *
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
98
manim/plugins/plugin_config.py
Normal file
98
manim/plugins/plugin_config.py
Normal 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)
|
||||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
[
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import warnings
|
|||
|
||||
import numpy as np
|
||||
|
||||
__all__ = ["show_diff_helper"]
|
||||
|
||||
|
||||
def show_diff_helper(
|
||||
frame_number: int,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
1029
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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__
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"}):
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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]))
|
||||
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue