├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── error-when-using.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── docs.yml │ └── publish.yml ├── .gitignore ├── LICENSE.md ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── example.py ├── make.bat ├── requirements.txt └── source │ ├── _static │ └── icon.png │ ├── conf.py │ ├── development │ ├── about.rst │ ├── changelog.rst │ └── contributing.rst │ ├── documentation │ ├── animation │ │ └── index.rst │ ├── camera │ │ └── index.rst │ ├── constants.rst │ ├── custom_config.rst │ ├── mobject │ │ └── index.rst │ ├── scene │ │ └── index.rst │ ├── shaders │ │ └── index.rst │ └── utils │ │ └── index.rst │ ├── getting_started │ ├── configuration.rst │ ├── example_scenes.rst │ ├── installation.rst │ ├── quickstart.rst │ ├── structure.rst │ └── whatsnew.rst │ ├── index.rst │ └── manim_example_ext.py ├── example_scenes.py ├── logo ├── cropped.png ├── graph.png ├── logo.py ├── transparent_graph.png ├── white_with_name.png ├── with_name.png └── with_subtext.png ├── manimlib ├── __init__.py ├── __main__.py ├── animation │ ├── __init__.py │ ├── animation.py │ ├── composition.py │ ├── creation.py │ ├── fading.py │ ├── growing.py │ ├── indication.py │ ├── movement.py │ ├── numbers.py │ ├── rotation.py │ ├── specialized.py │ ├── transform.py │ ├── transform_matching_parts.py │ └── update.py ├── camera │ ├── __init__.py │ ├── camera.py │ └── camera_frame.py ├── config.py ├── constants.py ├── default_config.yml ├── event_handler │ ├── __init__.py │ ├── event_dispatcher.py │ ├── event_listner.py │ └── event_type.py ├── extract_scene.py ├── logger.py ├── mobject │ ├── __init__.py │ ├── boolean_ops.py │ ├── changing.py │ ├── coordinate_systems.py │ ├── frame.py │ ├── functions.py │ ├── geometry.py │ ├── interactive.py │ ├── matrix.py │ ├── mobject.py │ ├── mobject_update_utils.py │ ├── number_line.py │ ├── numbers.py │ ├── probability.py │ ├── shape_matchers.py │ ├── svg │ │ ├── __init__.py │ │ ├── brace.py │ │ ├── drawings.py │ │ ├── old_tex_mobject.py │ │ ├── special_tex.py │ │ ├── string_mobject.py │ │ ├── svg_mobject.py │ │ ├── tex_mobject.py │ │ └── text_mobject.py │ ├── three_dimensions.py │ ├── types │ │ ├── __init__.py │ │ ├── dot_cloud.py │ │ ├── image_mobject.py │ │ ├── point_cloud_mobject.py │ │ ├── surface.py │ │ └── vectorized_mobject.py │ ├── value_tracker.py │ └── vector_field.py ├── module_loader.py ├── scene │ ├── __init__.py │ ├── interactive_scene.py │ ├── scene.py │ ├── scene_embed.py │ └── scene_file_writer.py ├── shader_wrapper.py ├── shaders │ ├── image │ │ ├── frag.glsl │ │ └── vert.glsl │ ├── inserts │ │ ├── NOTE.md │ │ ├── complex_functions.glsl │ │ ├── emit_gl_Position.glsl │ │ ├── finalize_color.glsl │ │ ├── get_unit_normal.glsl │ │ └── get_xyz_to_uv.glsl │ ├── mandelbrot_fractal │ │ ├── frag.glsl │ │ └── vert.glsl │ ├── newton_fractal │ │ ├── frag.glsl │ │ └── vert.glsl │ ├── quadratic_bezier │ │ ├── depth │ │ │ ├── frag.glsl │ │ │ ├── geom.glsl │ │ │ └── vert.glsl │ │ ├── fill │ │ │ ├── frag.glsl │ │ │ ├── geom.glsl │ │ │ └── vert.glsl │ │ └── stroke │ │ │ ├── frag.glsl │ │ │ ├── geom.glsl │ │ │ └── vert.glsl │ ├── simple_vert.glsl │ ├── surface │ │ ├── frag.glsl │ │ └── vert.glsl │ ├── textured_surface │ │ ├── frag.glsl │ │ └── vert.glsl │ └── true_dot │ │ ├── frag.glsl │ │ ├── geom.glsl │ │ └── vert.glsl ├── tex_templates.yml ├── typing.py ├── utils │ ├── __init__.py │ ├── bezier.py │ ├── cache.py │ ├── color.py │ ├── debug.py │ ├── dict_ops.py │ ├── directories.py │ ├── family_ops.py │ ├── file_ops.py │ ├── images.py │ ├── iterables.py │ ├── paths.py │ ├── rate_functions.py │ ├── shaders.py │ ├── simple_functions.py │ ├── sounds.py │ ├── space_ops.py │ ├── tex.py │ ├── tex_file_writing.py │ └── tex_to_symbol_count.py └── window.py ├── pyproject.toml ├── requirements.txt ├── setup.cfg └── setup.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Describe the bug 11 | 12 | 13 | **Code**: 14 | 15 | 16 | **Wrong display or Error traceback**: 17 | 18 | 19 | ### Additional context 20 | 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Ask A Question 4 | url: https://github.com/3b1b/manim/discussions/categories/q-a 5 | about: Please ask questions you encountered here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/error-when-using.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Error when using 3 | about: The error you encountered while using manim 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Describe the error 11 | 12 | 13 | ### Code and Error 14 | **Code**: 15 | 16 | 17 | **Error**: 18 | 19 | 20 | ### Environment 21 | **OS System**: 22 | **manim version**: master 23 | **python version**: 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | ## Motivation 6 | 7 | 8 | ## Proposed changes 9 | 10 | - 11 | - 12 | - 13 | 14 | ## Test 15 | 16 | **Code**: 17 | 18 | **Result**: -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'docs/**' 7 | pull_request: 8 | paths: 9 | - 'docs/**' 10 | 11 | jobs: 12 | docs: 13 | runs-on: ubuntu-latest 14 | name: build up document and deploy 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@master 19 | 20 | - name: Install sphinx and manim env 21 | run: | 22 | pip3 install --upgrade pip 23 | sudo apt install python3-setuptools libpango1.0-dev 24 | pip3 install -r docs/requirements.txt 25 | pip3 install -r requirements.txt 26 | 27 | - name: Build document with Sphinx 28 | run: | 29 | cd docs 30 | export PATH="$PATH:/home/runner/.local/bin" 31 | export SPHINXBUILD="python3 -m sphinx" 32 | make html 33 | 34 | - name: Deploy to GitHub pages 35 | if: ${{ github.event_name == 'push' }} 36 | uses: JamesIves/github-pages-deploy-action@3.7.1 37 | with: 38 | ACCESS_TOKEN: ${{ secrets.DOC_DEPLOY_TOKEN }} 39 | BRANCH: gh-pages 40 | FOLDER: docs/build/html 41 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | python: ["py37", "py38", "py39", "py310"] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: '3.8' 23 | 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install setuptools wheel twine build 28 | 29 | - name: Build wheels 30 | run: python setup.py bdist_wheel --python-tag ${{ matrix.python }} 31 | 32 | - name: Upload wheels 33 | env: 34 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 35 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 36 | run: | 37 | twine upload dist/* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/python 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | manimlib.egg-info/ 19 | 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | pip-wheel-metadata/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | pytestdebug.log 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | doc/_build/ 81 | 82 | # PyBuilder 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | .python-version 94 | pyrightconfig.json 95 | 96 | # pipenv 97 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 98 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 99 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 100 | # install all needed dependencies. 101 | #Pipfile.lock 102 | 103 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 104 | __pypackages__/ 105 | 106 | # Celery stuff 107 | celerybeat-schedule 108 | celerybeat.pid 109 | 110 | # SageMath parsed files 111 | *.sage.py 112 | 113 | # Environments 114 | .env 115 | .venv 116 | env/ 117 | venv/ 118 | ENV/ 119 | env.bak/ 120 | venv.bak/ 121 | pythonenv* 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # profiling data 145 | .prof 146 | 147 | # End of https://www.toptal.com/developers/gitignore/api/python 148 | # Custom exclusions: 149 | .DS_Store 150 | 151 | # For manim 152 | /videos 153 | /custom_config.yml 154 | test.py 155 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2023 3Blue1Brown LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft manimlib 2 | recursive-exclude manimlib *.pyc *.DS_Store -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/example.py: -------------------------------------------------------------------------------- 1 | from manimlib import * 2 | 3 | class SquareToCircle(Scene): 4 | def construct(self): 5 | circle = Circle() 6 | circle.set_fill(BLUE, opacity=0.5) 7 | circle.set_stroke(BLUE_E, width=4) 8 | square = Square() 9 | 10 | self.play(ShowCreation(square)) 11 | self.wait() 12 | self.play(ReplacementTransform(square, circle)) 13 | self.wait() 14 | # Try typing the following lines 15 | # self.play(circle.animate.stretch(4, dim=0)) 16 | # self.play(Rotate(circle, TAU / 4)) 17 | # self.play(circle.animate.shift(2 * RIGHT), circle.animate.scale(0.25)) 18 | # circle.insert_n_curves(10) 19 | # self.play(circle.animate.apply_complex_function(lambda z: z**2)) 20 | 21 | class SquareToCircleEmbed(Scene): 22 | def construct(self): 23 | circle = Circle() 24 | circle.set_fill(BLUE, opacity=0.5) 25 | circle.set_stroke(BLUE_E, width=4) 26 | 27 | self.add(circle) 28 | self.wait() 29 | self.play(circle.animate.stretch(4, dim=0)) 30 | self.wait(1.5) 31 | self.play(Rotate(circle, TAU / 4)) 32 | self.wait(1.5) 33 | self.play(circle.animate.shift(2 * RIGHT), circle.animate.scale(0.25)) 34 | self.wait(1.5) 35 | circle.insert_n_curves(10) 36 | self.play(circle.animate.apply_complex_function(lambda z: z**2)) 37 | self.wait(2) 38 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx==3.0.3 2 | sphinx-copybutton 3 | furo==2020.10.5b9 4 | Jinja2 -------------------------------------------------------------------------------- /docs/source/_static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3b1b/manim/41613db7eca9eabbb40aa830631e5206c9282f0f/docs/source/_static/icon.png -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | sys.path.insert(0, os.path.abspath(".")) 4 | sys.path.insert(0, os.path.abspath('../../')) 5 | 6 | 7 | project = 'manim' 8 | copyright = '- This document has been placed in the public domain.' 9 | author = 'TonyCrane' 10 | 11 | release = '' 12 | 13 | extensions = [ 14 | 'sphinx.ext.todo', 15 | 'sphinx.ext.githubpages', 16 | 'sphinx.ext.mathjax', 17 | 'sphinx.ext.intersphinx', 18 | 'sphinx.ext.autodoc', 19 | 'sphinx.ext.coverage', 20 | 'sphinx.ext.napoleon', 21 | 'sphinx_copybutton', 22 | 'manim_example_ext' 23 | ] 24 | 25 | autoclass_content = 'both' 26 | mathjax_path = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" 27 | 28 | templates_path = ['_templates'] 29 | source_suffix = '.rst' 30 | master_doc = 'index' 31 | pygments_style = 'default' 32 | 33 | html_static_path = ["_static"] 34 | html_css_files = [ 35 | "https://cdn.jsdelivr.net/gh/manim-kindergarten/CDN@master/manimgl_assets/custom.css", 36 | "https://cdn.jsdelivr.net/gh/manim-kindergarten/CDN@master/manimgl_assets/colors.css" 37 | ] 38 | html_theme = 'furo' # pip install furo==2020.10.5b9 39 | html_favicon = '_static/icon.png' 40 | html_logo = '../../logo/transparent_graph.png' 41 | html_theme_options = { 42 | "sidebar_hide_name": True, 43 | } 44 | -------------------------------------------------------------------------------- /docs/source/development/about.rst: -------------------------------------------------------------------------------- 1 | About 2 | ===== 3 | 4 | About Manim 5 | ----------- 6 | 7 | Manim is an animation engine for explanatory math videos. 8 | You can use it to make math videos (or other fields) like 3Blue1Brown. 9 | 10 | There are mainly two versions here: 11 | 12 | - `3b1b/manim `_ : Maintained by Grant Sanderson of 3Blue1Brown. 13 | 14 | Using OpenGL and its GLSL language to use GPU for rendering. It has higher efficiency, 15 | faster rendering speed, and supports real-time rendering and interaction. 16 | 17 | - `ManimCommunity/manim `_ : Maintained by Manim Community Dev Team. 18 | 19 | Using multiple backend rendering. There is better documentation and 20 | a more open contribution community. 21 | 22 | About this documentation 23 | ------------------------ 24 | 25 | This documentation is based on the version in `3b1b/manim `_. 26 | Created by `TonyCrane `_ ("鹤翔万里" in Chinese) and in production. 27 | 28 | Among them, the ``manim_example_ext`` extension for Sphinx refers to 29 | `the documentation of ManimCommunity `_. 30 | 31 | If you want to contribute to manim or this document, please see: :doc:`contributing` -------------------------------------------------------------------------------- /docs/source/development/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Accept any contribution you make :) 5 | 6 | - **Contribute to the manim source code**: 7 | 8 | Please fork to your own repository and make changes, submit a pull request, and fill in 9 | the motivation for the change following the instructions in the template. We will check 10 | your pull request in detail (this usually takes a while, please be patient) 11 | 12 | - **Contribute to the documentation**: 13 | 14 | Also submit a pull request and write down the main changes. 15 | 16 | - **If you find a bug in the code**: 17 | 18 | Please open an issue and fill in the found problem and your environment according 19 | to the template. (But please note that if you think this problem is just a problem 20 | of yourself, rather than a problem of source code, it is recommended that you ask a 21 | question in the `Q&A category `_ 22 | of the discussion page) 23 | 24 | - **You are welcome to share the content you made with manim**: 25 | 26 | Post it in the `show and tell category `_ 27 | of the discussion page. 28 | 29 | - **You are also welcome to share some of your suggestions and ideas**: 30 | 31 | Post them in the `ideas category `_ 32 | of the discussion page. 33 | 34 | How to build this documentation 35 | ------------------------------- 36 | 37 | - Clone the 3b1b/manim repository 38 | 39 | .. code-block:: sh 40 | 41 | git clone https://github.com/3b1b/manim.git 42 | # or your own repo 43 | # git clone https://github.com//manim.git 44 | cd manim 45 | 46 | - Install python package dependencies 47 | 48 | .. code-block:: sh 49 | 50 | pip install -r docs/requirements.txt 51 | 52 | - Go to the ``docs/`` folder and build 53 | 54 | .. code-block:: sh 55 | 56 | cd docs/ 57 | make html 58 | 59 | - The output document is located in ``docs/build/html/`` -------------------------------------------------------------------------------- /docs/source/documentation/animation/index.rst: -------------------------------------------------------------------------------- 1 | Animation (TODO) 2 | ================ -------------------------------------------------------------------------------- /docs/source/documentation/camera/index.rst: -------------------------------------------------------------------------------- 1 | Camera (TODO) 2 | ============= -------------------------------------------------------------------------------- /docs/source/documentation/custom_config.rst: -------------------------------------------------------------------------------- 1 | custom_config 2 | ============== 3 | 4 | ``directories`` 5 | --------------- 6 | 7 | - ``mirror_module_path`` 8 | (``True`` or ``False``) Whether to create a folder named the name of the 9 | running file under the ``output`` path, and save the output (``images/`` 10 | or ``videos/``) in it. 11 | 12 | - ``base`` 13 | The root directory that will hold files, such as video files manim renders, 14 | or image resources that it pulls from 15 | 16 | - ``output`` 17 | Output file path, the videos will be saved in the ``videos/`` folder under it, 18 | and the pictures will be saved in the ``images/`` folder under it. 19 | 20 | For example, if you set ``output`` to ``"/.../manim/output"`` and 21 | ``mirror_module_path`` to ``False``, then you exported ``Scene1`` in the code 22 | file and saved the last frame, then the final directory structure will be like: 23 | 24 | .. code-block:: text 25 | :emphasize-lines: 9, 11 26 | 27 | manim/ 28 | ├── manimlib/ 29 | │ ├── animation/ 30 | │ ├── ... 31 | │ ├── default_config.yml 32 | │ └── window.py 33 | ├── output/ 34 | │ ├── images 35 | │ │ └── Scene1.png 36 | │ └── videos 37 | │ └── Scene1.mp4 38 | ├── code.py 39 | └── custom_config.yml 40 | 41 | But if you set ``mirror_module_path`` to ``True``, the directory structure will be: 42 | 43 | .. code-block:: text 44 | :emphasize-lines: 8 45 | 46 | manim/ 47 | ├── manimlib/ 48 | │ ├── animation/ 49 | │ ├── ... 50 | │ ├── default_config.yml 51 | │ └── window.py 52 | ├── output/ 53 | │ └── code/ 54 | │ ├── images 55 | │ │ └── Scene1.png 56 | │ └── videos 57 | │ └── Scene1.mp4 58 | ├── code.py 59 | └── custom_config.yml 60 | 61 | - ``raster_images`` 62 | The directory for storing raster images to be used in the code (including 63 | ``.jpg``, ``.jpeg``, ``.png`` and ``.gif``), which will be read by ``ImageMobject``. 64 | 65 | - ``vector_images`` 66 | The directory for storing vector images to be used in the code (including 67 | ``.svg`` and ``.xdv``), which will be read by ``SVGMobject``. 68 | 69 | - ``sounds`` 70 | The directory for storing sound files to be used in ``Scene.add_sound()`` ( 71 | including ``.wav`` and ``.mp3``). 72 | 73 | - ``cache`` 74 | The directory for storing temporarily generated cache files, including 75 | ``Tex`` cache, ``Text`` cache and storage of object points. 76 | 77 | 78 | ``window`` 79 | ---------- 80 | 81 | - ``position_string`` 82 | The relative position of the playback window on the display (two characters, 83 | the first character means upper(U) / middle(O) / lower(D), the second character 84 | means left(L) / middle(O) / right(R)). 85 | 86 | - ``monitor_index`` 87 | If using multiple monitors, which one should the window show up in? 88 | 89 | - ``full_screen`` 90 | Should the preview window be full screen. If not, it defaults to half the screen 91 | 92 | - ``position`` 93 | This is an option to more manually set the default window position, in pixel 94 | coordinates, e.g. (500, 300) 95 | 96 | - ``size`` 97 | Option to more manually set the default window size, in pixel coordinates, 98 | e.g. (1920, 1080) 99 | 100 | 101 | ``camera`` 102 | ---------- 103 | 104 | - ``resolution`` 105 | Resolution to render at, e.g. (1920, 1080) 106 | 107 | - ``background_color`` 108 | Default background color of scenes 109 | 110 | - ``fps`` 111 | Framerate 112 | 113 | - ``background_opacity`` 114 | Opacity of the background 115 | 116 | 117 | ``file_writer`` 118 | --------------- 119 | Configuration specifying how files are written, e.g. what ffmpeg parameters to use 120 | 121 | 122 | ``scene`` 123 | ------- 124 | Some default configuration for the Scene class 125 | 126 | 127 | ``text`` 128 | ------- 129 | 130 | - ``font`` 131 | Default font of Text 132 | 133 | - ``text_alignment`` 134 | Default text alignment for LaTeX 135 | 136 | ``tex`` 137 | ------- 138 | 139 | - ``template`` 140 | Which configuration from the manimlib/tex_template.yml file should be used 141 | to determine the latex compiler to use, and what preamble to include for 142 | rendering tex. 143 | 144 | 145 | ``sizes`` 146 | --------- 147 | 148 | Valuess for various constants used in manimm to specify distances, like the height 149 | of the frame, the value of SMALL_BUFF, LARGE_BUFF, etc. 150 | 151 | 152 | ``colors`` 153 | ---------- 154 | 155 | Color pallete to use, determining values of color constants like RED, BLUE_E, TEAL, etc. 156 | 157 | ``loglevel`` 158 | ------------ 159 | 160 | Can be DEBUG / INFO / WARNING / ERROR / CRITICAL 161 | 162 | 163 | ``universal_import_line`` 164 | ------------------------- 165 | 166 | Import line that need to execute when entering interactive mode directly. 167 | 168 | 169 | ``ignore_manimlib_modules_on_reload`` 170 | ------------------------------------- 171 | 172 | When calling ``reload`` during the interactive mode, imported modules are 173 | by default reloaded, in case the user writing a scene which pulls from various 174 | other files they have written. By default, modules withinn the manim library will 175 | be ignored, but one developing manim may want to set this to be False so that 176 | edits to the library are reloaded as well. 177 | -------------------------------------------------------------------------------- /docs/source/documentation/mobject/index.rst: -------------------------------------------------------------------------------- 1 | Mobject (TODO) 2 | ============== -------------------------------------------------------------------------------- /docs/source/documentation/scene/index.rst: -------------------------------------------------------------------------------- 1 | Scene (TODO) 2 | ============ -------------------------------------------------------------------------------- /docs/source/documentation/shaders/index.rst: -------------------------------------------------------------------------------- 1 | Shaders (TODO) 2 | ============== -------------------------------------------------------------------------------- /docs/source/documentation/utils/index.rst: -------------------------------------------------------------------------------- 1 | Utils (TODO) 2 | ============ -------------------------------------------------------------------------------- /docs/source/getting_started/configuration.rst: -------------------------------------------------------------------------------- 1 | CLI flags and configuration 2 | =========================== 3 | 4 | Command Line Interface 5 | ---------------------- 6 | 7 | To run manim, you need to enter the directory at the same level as ``manimlib/`` 8 | and enter the command in the following format into terminal: 9 | 10 | .. code-block:: sh 11 | 12 | manimgl .py 13 | # or 14 | manim-render .py 15 | 16 | - ``.py`` : The python file you wrote. Needs to be at the same level as ``manimlib/``, otherwise you need to use an absolute path or a relative path. 17 | - ```` : The scene you want to render here. If it is not written or written incorrectly, it will list all for you to choose. And if there is only one ``Scene`` in the file, this class will be rendered directly. 18 | - ```` : CLI flags. 19 | 20 | Some useful flags 21 | ^^^^^^^^^^^^^^^^^ 22 | 23 | - ``-w`` to write the scene to a file. 24 | - ``-o`` to write the scene to a file and open the result. 25 | - ``-s`` to skip to the end and just show the final frame. 26 | 27 | - ``-so`` will save the final frame to an image and show it. 28 | 29 | - ``-n `` to skip ahead to the ``n``\ ’th animation of a scene. 30 | - ``-f`` to make the playback window fullscreen. 31 | 32 | All supported flags 33 | ^^^^^^^^^^^^^^^^^^^ 34 | 35 | ========================================================== ====== ===================================================================================================================================================================================================== 36 | flag abbr function 37 | ========================================================== ====== ===================================================================================================================================================================================================== 38 | ``--help`` ``-h`` Show the help message and exit 39 | ``--version`` ``-v`` Display the version of manimgl 40 | ``--write_file`` ``-w`` Render the scene as a movie file 41 | ``--skip_animations`` ``-s`` Skip to the last frame 42 | ``--low_quality`` ``-l`` Render at a low quality (for faster rendering) 43 | ``--medium_quality`` ``-m`` Render at a medium quality 44 | ``--hd`` Render at a 1080p quality 45 | ``--uhd`` Render at a 4k quality 46 | ``--full_screen`` ``-f`` Show window in full screen 47 | ``--presenter_mode`` ``-p`` Scene will stay paused during wait calls until space bar or right arrow is hit, like a slide show 48 | ``--save_pngs`` ``-g`` Save each frame as a png 49 | ``--gif`` ``-i`` Save the video as gif 50 | ``--transparent`` ``-t`` Render to a movie file with an alpha channel 51 | ``--quiet`` ``-q`` 52 | ``--write_all`` ``-a`` Write all the scenes from a file 53 | ``--open`` ``-o`` Automatically open the saved file once its done 54 | ``--finder`` Show the output file in finder 55 | ``--config`` Guide for automatic configuration 56 | ``--file_name FILE_NAME`` Name for the movie or image file 57 | ``--start_at_animation_number START_AT_ANIMATION_NUMBER`` ``-n`` Start rendering not from the first animation, but from another, specified by its index. If you passing two comma separated values, e.g. "3,6", it will end the rendering at the second value. 58 | ``--embed [EMBED]`` ``-e`` Creates a new file where the line ``self.embed`` is inserted into the Scenes construct method. If a string is passed in, the line will be inserted below the last line of code including that string. 59 | ``--resolution RESOLUTION`` ``-r`` Resolution, passed as "WxH", e.g. "1920x1080" 60 | ``--fps FPS`` Frame rate, as an integer 61 | ``--color COLOR`` ``-c`` Background color 62 | ``--leave_progress_bars`` Leave progress bars displayed in terminal 63 | ``--video_dir VIDEO_DIR`` Directory to write video 64 | ``--config_file CONFIG_FILE`` Path to the custom configuration file 65 | ``--log-level LOG_LEVEL`` Level of messages to Display, can be DEBUG / INFO / WARNING / ERROR / CRITICAL 66 | ``--autoreload`` Automatically reload Python modules to pick up code changes across during an interactive embedding 67 | ========================================================== ====== ===================================================================================================================================================================================================== 68 | 69 | custom_config 70 | -------------- 71 | 72 | In order to perform more configuration (about directories, etc.) and permanently 73 | change the default value (you don't have to add flags to the command every time), 74 | you can modify ``custom_config.yml``. The meaning of each option is in 75 | page :doc:`../documentation/custom_config`. 76 | 77 | You can also use different ``custom_config.yml`` for different directories, such as 78 | following the directory structure: 79 | 80 | .. code-block:: text 81 | 82 | manim/ 83 | ├── manimlib/ 84 | │ ├── animation/ 85 | │ ├── ... 86 | │ ├── default_config.yml 87 | │ └── window.py 88 | ├── project/ 89 | │ ├── code.py 90 | │ └── custom_config.yml 91 | └── custom_config.yml 92 | 93 | When you enter the ``project/`` folder and run ``manimgl code.py ``, 94 | it will overwrite ``manim/default_config.yml`` with ``custom_config.yml`` 95 | in the ``project`` folder. 96 | 97 | Alternatively, you can use ``--config_file`` flag in CLI to specify configuration file manually. 98 | 99 | .. code-block:: sh 100 | 101 | manimgl project/code.py --config_file /path/to/custom_config.yml -------------------------------------------------------------------------------- /docs/source/getting_started/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Manim runs on Python 3.7 or higher. 5 | 6 | System requirements are: 7 | 8 | - `FFmpeg `__ 9 | - `OpenGL `__ (included in python package ``PyOpenGL``) 10 | - `LaTeX `__ (optional, if you want to use LaTeX) 11 | - `Pango `__ (only for Linux) 12 | 13 | 14 | Install FFmpeg 15 | -------------- 16 | 17 | 18 | 19 | Install FFmpeg Windows 20 | ------------------------ 21 | .. code-block:: cmd 22 | 23 | choco install ffmpeg 24 | 25 | 26 | # Install FFmepeg Linux 27 | ---------------------------- 28 | .. code-block:: sh 29 | 30 | $ sudo apt update 31 | $ sudo apt install ffmpeg 32 | $ ffmpeg -version 33 | 34 | # Install FFmpeg MacOS 35 | ---------------------------- 36 | - Download This ZIP file `https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z`(if the link is not working download this zip file from there original website) 37 | 38 | 39 | 40 | Directly 41 | -------- 42 | 43 | .. code-block:: sh 44 | 45 | # Install manimgl 46 | pip install manimgl 47 | 48 | # Try it out 49 | manimgl 50 | 51 | If you want to hack on manimlib itself, clone this repository and in 52 | that directory execute: 53 | 54 | .. code-block:: sh 55 | 56 | # Install python requirements 57 | pip install -e . 58 | 59 | # Try it out 60 | manimgl example_scenes.py OpeningManimExample 61 | # or 62 | manim-render example_scenes.py OpeningManimExample 63 | 64 | If you run the above command and no error message appears, 65 | then you have successfully installed all the environments required by manim. 66 | 67 | Directly (Windows) 68 | ------------------ 69 | 70 | 1. `Install 71 | FFmpeg `__, and make sure that its path is in the PATH environment variable. 72 | 2. Install a LaTeX distribution. 73 | `TeXLive-full `__ is recommended. 74 | 3. Install the remaining Python packages. 75 | 76 | .. code-block:: sh 77 | 78 | git clone https://github.com/3b1b/manim.git 79 | cd manim 80 | pip install -e . 81 | manimgl example_scenes.py OpeningManimExample 82 | 83 | For Anaconda 84 | ------------ 85 | 86 | - Install FFmpeg and LaTeX as above. 87 | - Create a conda environment using 88 | 89 | .. code-block:: sh 90 | 91 | git clone https://github.com/3b1b/manim.git 92 | cd manim 93 | conda create -n manim python=3.8 94 | conda activate manim 95 | pip install -e . 96 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Manim's documentation 2 | ===================== 3 | 4 | .. image:: https://cdn.jsdelivr.net/gh/3b1b/manim@master/logo/white_with_name.png 5 | 6 | Manim is an animation engine for explanatory math videos. It's used to create precise animations programmatically, as seen in the videos 7 | at `3Blue1Brown `_. 8 | 9 | And here is a Chinese version of this documentation: https://docs.manim.org.cn/ 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | :caption: Getting Started 14 | 15 | getting_started/installation 16 | getting_started/quickstart 17 | getting_started/configuration 18 | getting_started/example_scenes 19 | getting_started/config 20 | getting_started/structure 21 | getting_started/whatsnew 22 | 23 | .. toctree:: 24 | :maxdepth: 2 25 | :caption: Documentation 26 | 27 | documentation/constants 28 | documentation/custom_config 29 | 30 | .. toctree:: 31 | :maxdepth: 2 32 | :caption: Development 33 | 34 | development/changelog 35 | development/contributing 36 | development/about 37 | -------------------------------------------------------------------------------- /docs/source/manim_example_ext.py: -------------------------------------------------------------------------------- 1 | from docutils import nodes 2 | from docutils.parsers.rst import directives, Directive 3 | 4 | import jinja2 5 | import os 6 | 7 | 8 | class skip_manim_node(nodes.Admonition, nodes.Element): 9 | pass 10 | 11 | 12 | def visit(self, node, name=""): 13 | self.visit_admonition(node, name) 14 | 15 | 16 | def depart(self, node): 17 | self.depart_admonition(node) 18 | 19 | 20 | class ManimExampleDirective(Directive): 21 | has_content = True 22 | required_arguments = 1 23 | optional_arguments = 0 24 | option_spec = { 25 | "hide_code": bool, 26 | "media": str, 27 | } 28 | final_argument_whitespace = True 29 | 30 | def run(self): 31 | hide_code = "hide_code" in self.options 32 | scene_name = self.arguments[0] 33 | media_file_name = self.options["media"] 34 | 35 | source_block = [ 36 | ".. code-block:: python", 37 | "", 38 | *[" " + line for line in self.content], 39 | ] 40 | source_block = "\n".join(source_block) 41 | 42 | state_machine = self.state_machine 43 | document = state_machine.document 44 | 45 | if any(media_file_name.endswith(ext) for ext in [".png", ".jpg", ".gif"]): 46 | is_video = False 47 | else: 48 | is_video = True 49 | 50 | rendered_template = jinja2.Template(TEMPLATE).render( 51 | scene_name=scene_name, 52 | scene_name_lowercase=scene_name.lower(), 53 | hide_code=hide_code, 54 | is_video=is_video, 55 | media_file_name=media_file_name, 56 | source_block=source_block, 57 | ) 58 | state_machine.insert_input( 59 | rendered_template.split("\n"), source=document.attributes["source"] 60 | ) 61 | 62 | return [] 63 | 64 | 65 | def setup(app): 66 | app.add_node(skip_manim_node, html=(visit, depart)) 67 | 68 | setup.app = app 69 | setup.config = app.config 70 | setup.confdir = app.confdir 71 | 72 | app.add_directive("manim-example", ManimExampleDirective) 73 | 74 | metadata = {"parallel_read_safe": False, "parallel_write_safe": True} 75 | return metadata 76 | 77 | 78 | TEMPLATE = r""" 79 | {% if not hide_code %} 80 | 81 | .. raw:: html 82 | 83 |
84 | 85 | {% endif %} 86 | 87 | {% if is_video %} 88 | .. raw:: html 89 | 90 | 91 | {% else %} 92 | .. image:: {{ media_file_name }} 93 | :align: center 94 | :name: {{ scene_name_lowercase }} 95 | {% endif %} 96 | 97 | {% if not hide_code %} 98 | .. raw:: html 99 | 100 |
{{ scene_name }}
101 | 102 | {{ source_block }} 103 | {% endif %} 104 | 105 | .. raw:: html 106 | 107 |
108 | """ -------------------------------------------------------------------------------- /logo/cropped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3b1b/manim/41613db7eca9eabbb40aa830631e5206c9282f0f/logo/cropped.png -------------------------------------------------------------------------------- /logo/graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3b1b/manim/41613db7eca9eabbb40aa830631e5206c9282f0f/logo/graph.png -------------------------------------------------------------------------------- /logo/logo.py: -------------------------------------------------------------------------------- 1 | from manimlib.imports import * 2 | 3 | NEW_BLUE = "#68a8e1" 4 | 5 | class Thumbnail(GraphScene): 6 | CONFIG = { 7 | "y_max": 8, 8 | "y_axis_height": 5, 9 | } 10 | 11 | def construct(self): 12 | self.show_function_graph() 13 | 14 | def show_function_graph(self): 15 | self.setup_axes(animate=False) 16 | def func(x): 17 | return 0.1 * (x + 3-5) * (x - 3-5) * (x-5) + 5 18 | 19 | def rect(x): 20 | return 2.775*(x-1.5)+3.862 21 | recta = self.get_graph(rect,x_min=-1,x_max=5) 22 | graph = self.get_graph(func,x_min=0.2,x_max=9) 23 | graph.set_color(NEW_BLUE) 24 | input_tracker_p1 = ValueTracker(1.5) 25 | input_tracker_p2 = ValueTracker(3.5) 26 | 27 | def get_x_value(input_tracker): 28 | return input_tracker.get_value() 29 | 30 | def get_y_value(input_tracker): 31 | return graph.underlying_function(get_x_value(input_tracker)) 32 | 33 | def get_x_point(input_tracker): 34 | return self.coords_to_point(get_x_value(input_tracker), 0) 35 | 36 | def get_y_point(input_tracker): 37 | return self.coords_to_point(0, get_y_value(input_tracker)) 38 | 39 | def get_graph_point(input_tracker): 40 | return self.coords_to_point(get_x_value(input_tracker), get_y_value(input_tracker)) 41 | 42 | def get_v_line(input_tracker): 43 | return DashedLine(get_x_point(input_tracker), get_graph_point(input_tracker), stroke_width=2) 44 | 45 | def get_h_line(input_tracker): 46 | return DashedLine(get_graph_point(input_tracker), get_y_point(input_tracker), stroke_width=2) 47 | # 48 | input_triangle_p1 = RegularPolygon(n=3, start_angle=TAU / 4) 49 | output_triangle_p1 = RegularPolygon(n=3, start_angle=0) 50 | for triangle in input_triangle_p1, output_triangle_p1: 51 | triangle.set_fill(WHITE, 1) 52 | triangle.set_stroke(width=0) 53 | triangle.scale(0.1) 54 | # 55 | input_triangle_p2 = RegularPolygon(n=3, start_angle=TAU / 4) 56 | output_triangle_p2 = RegularPolygon(n=3, start_angle=0) 57 | for triangle in input_triangle_p2, output_triangle_p2: 58 | triangle.set_fill(WHITE, 1) 59 | triangle.set_stroke(width=0) 60 | triangle.scale(0.1) 61 | 62 | # 63 | x_label_p1 = Tex("a") 64 | output_label_p1 = Tex("f(a)") 65 | x_label_p2 = Tex("b") 66 | output_label_p2 = Tex("f(b)") 67 | v_line_p1 = get_v_line(input_tracker_p1) 68 | v_line_p2 = get_v_line(input_tracker_p2) 69 | h_line_p1 = get_h_line(input_tracker_p1) 70 | h_line_p2 = get_h_line(input_tracker_p2) 71 | graph_dot_p1 = Dot(color=WHITE) 72 | graph_dot_p2 = Dot(color=WHITE) 73 | 74 | # reposition mobjects 75 | x_label_p1.next_to(v_line_p1, DOWN) 76 | x_label_p2.next_to(v_line_p2, DOWN) 77 | output_label_p1.next_to(h_line_p1, LEFT) 78 | output_label_p2.next_to(h_line_p2, LEFT) 79 | input_triangle_p1.next_to(v_line_p1, DOWN, buff=0) 80 | input_triangle_p2.next_to(v_line_p2, DOWN, buff=0) 81 | output_triangle_p1.next_to(h_line_p1, LEFT, buff=0) 82 | output_triangle_p2.next_to(h_line_p2, LEFT, buff=0) 83 | graph_dot_p1.move_to(get_graph_point(input_tracker_p1)) 84 | graph_dot_p2.move_to(get_graph_point(input_tracker_p2)) 85 | 86 | 87 | # 88 | self.play( 89 | ShowCreation(graph), 90 | ) 91 | # Animacion del punto a 92 | self.add_foreground_mobject(graph_dot_p1) 93 | self.add_foreground_mobject(graph_dot_p2) 94 | self.play( 95 | DrawBorderThenFill(input_triangle_p1), 96 | Write(x_label_p1), 97 | ShowCreation(v_line_p1), 98 | GrowFromCenter(graph_dot_p1), 99 | ShowCreation(h_line_p1), 100 | Write(output_label_p1), 101 | DrawBorderThenFill(output_triangle_p1), 102 | DrawBorderThenFill(input_triangle_p2), 103 | Write(x_label_p2), 104 | ShowCreation(v_line_p2), 105 | GrowFromCenter(graph_dot_p2), 106 | ShowCreation(h_line_p2), 107 | Write(output_label_p2), 108 | DrawBorderThenFill(output_triangle_p2), 109 | run_time=0.5 110 | ) 111 | self.add( 112 | input_triangle_p2, 113 | x_label_p2, 114 | graph_dot_p2, 115 | v_line_p2, 116 | h_line_p2, 117 | output_triangle_p2, 118 | output_label_p2, 119 | ) 120 | ################### 121 | pendiente_recta = self.get_secant_slope_group( 122 | 1.9, recta, dx = 1.4, 123 | df_label = None, 124 | dx_label = None, 125 | dx_line_color = PURPLE, 126 | df_line_color= ORANGE, 127 | ) 128 | grupo_secante = self.get_secant_slope_group( 129 | 1.5, graph, dx = 2, 130 | df_label = None, 131 | dx_label = None, 132 | dx_line_color = "#942357", 133 | df_line_color= "#3f7d5c", 134 | secant_line_color = RED, 135 | ) 136 | 137 | 138 | self.add( 139 | input_triangle_p2, 140 | graph_dot_p2, 141 | v_line_p2, 142 | h_line_p2, 143 | output_triangle_p2, 144 | ) 145 | self.play(FadeIn(grupo_secante)) 146 | 147 | kwargs = { 148 | "x_min" : 4, 149 | "x_max" : 9, 150 | "fill_opacity" : 0.75, 151 | "stroke_width" : 0.25, 152 | } 153 | self.graph=graph 154 | iteraciones=6 155 | 156 | 157 | self.rect_list = self.get_riemann_rectangles_list( 158 | graph, iteraciones,start_color=PURPLE,end_color=ORANGE, **kwargs 159 | ) 160 | flat_rects = self.get_riemann_rectangles( 161 | self.get_graph(lambda x : 0), dx = 0.5,start_color=invert_color(PURPLE),end_color=invert_color(ORANGE),**kwargs 162 | ) 163 | rects = self.rect_list[0] 164 | self.transform_between_riemann_rects( 165 | flat_rects, rects, 166 | replace_mobject_with_target_in_scene = True, 167 | run_time=0.9 168 | ) 169 | 170 | # adding manim 171 | picture = Group(*self.mobjects) 172 | picture.scale(0.6).to_edge(LEFT, buff=SMALL_BUFF) 173 | manim = TexText("Manim").set_height(1.5) \ 174 | .next_to(picture, RIGHT) \ 175 | .shift(DOWN * 0.7) 176 | self.add(manim) 177 | -------------------------------------------------------------------------------- /logo/transparent_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3b1b/manim/41613db7eca9eabbb40aa830631e5206c9282f0f/logo/transparent_graph.png -------------------------------------------------------------------------------- /logo/white_with_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3b1b/manim/41613db7eca9eabbb40aa830631e5206c9282f0f/logo/white_with_name.png -------------------------------------------------------------------------------- /logo/with_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3b1b/manim/41613db7eca9eabbb40aa830631e5206c9282f0f/logo/with_name.png -------------------------------------------------------------------------------- /logo/with_subtext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3b1b/manim/41613db7eca9eabbb40aa830631e5206c9282f0f/logo/with_subtext.png -------------------------------------------------------------------------------- /manimlib/__init__.py: -------------------------------------------------------------------------------- 1 | import pkg_resources 2 | 3 | __version__ = pkg_resources.get_distribution("manimgl").version 4 | 5 | from typing import TYPE_CHECKING 6 | 7 | if TYPE_CHECKING: 8 | from manimlib.typing import * 9 | 10 | from manimlib.constants import * 11 | 12 | from manimlib.window import * 13 | 14 | from manimlib.animation.animation import * 15 | from manimlib.animation.composition import * 16 | from manimlib.animation.creation import * 17 | from manimlib.animation.fading import * 18 | from manimlib.animation.growing import * 19 | from manimlib.animation.indication import * 20 | from manimlib.animation.movement import * 21 | from manimlib.animation.numbers import * 22 | from manimlib.animation.rotation import * 23 | from manimlib.animation.specialized import * 24 | from manimlib.animation.transform import * 25 | from manimlib.animation.transform_matching_parts import * 26 | from manimlib.animation.update import * 27 | 28 | from manimlib.camera.camera import * 29 | 30 | from manimlib.mobject.boolean_ops import * 31 | from manimlib.mobject.changing import * 32 | from manimlib.mobject.coordinate_systems import * 33 | from manimlib.mobject.frame import * 34 | from manimlib.mobject.functions import * 35 | from manimlib.mobject.geometry import * 36 | from manimlib.mobject.interactive import * 37 | from manimlib.mobject.matrix import * 38 | from manimlib.mobject.mobject import * 39 | from manimlib.mobject.mobject_update_utils import * 40 | from manimlib.mobject.number_line import * 41 | from manimlib.mobject.numbers import * 42 | from manimlib.mobject.probability import * 43 | from manimlib.mobject.shape_matchers import * 44 | from manimlib.mobject.svg.brace import * 45 | from manimlib.mobject.svg.drawings import * 46 | from manimlib.mobject.svg.string_mobject import * 47 | from manimlib.mobject.svg.svg_mobject import * 48 | from manimlib.mobject.svg.special_tex import * 49 | from manimlib.mobject.svg.tex_mobject import * 50 | from manimlib.mobject.svg.text_mobject import * 51 | from manimlib.mobject.three_dimensions import * 52 | from manimlib.mobject.types.dot_cloud import * 53 | from manimlib.mobject.types.image_mobject import * 54 | from manimlib.mobject.types.point_cloud_mobject import * 55 | from manimlib.mobject.types.surface import * 56 | from manimlib.mobject.types.vectorized_mobject import * 57 | from manimlib.mobject.value_tracker import * 58 | from manimlib.mobject.vector_field import * 59 | 60 | from manimlib.scene.interactive_scene import * 61 | from manimlib.scene.scene import * 62 | 63 | from manimlib.utils.bezier import * 64 | from manimlib.utils.cache import * 65 | from manimlib.utils.color import * 66 | from manimlib.utils.dict_ops import * 67 | from manimlib.utils.debug import * 68 | from manimlib.utils.directories import * 69 | from manimlib.utils.file_ops import * 70 | from manimlib.utils.images import * 71 | from manimlib.utils.iterables import * 72 | from manimlib.utils.paths import * 73 | from manimlib.utils.rate_functions import * 74 | from manimlib.utils.simple_functions import * 75 | from manimlib.utils.shaders import * 76 | from manimlib.utils.sounds import * 77 | from manimlib.utils.space_ops import * 78 | from manimlib.utils.tex import * 79 | -------------------------------------------------------------------------------- /manimlib/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from addict import Dict 3 | 4 | from manimlib import __version__ 5 | from manimlib.config import manim_config 6 | from manimlib.config import parse_cli 7 | import manimlib.extract_scene 8 | from manimlib.utils.cache import clear_cache 9 | from manimlib.window import Window 10 | 11 | 12 | from IPython.terminal.embed import KillEmbedded 13 | 14 | 15 | from typing import TYPE_CHECKING 16 | if TYPE_CHECKING: 17 | from argparse import Namespace 18 | 19 | 20 | def run_scenes(): 21 | """ 22 | Runs the scenes in a loop and detects when a scene reload is requested. 23 | """ 24 | # Create a new dict to be able to upate without 25 | # altering global configuration 26 | scene_config = Dict(manim_config.scene) 27 | run_config = manim_config.run 28 | 29 | if run_config.show_in_window: 30 | # Create a reusable window 31 | window = Window(**manim_config.window) 32 | scene_config.update(window=window) 33 | 34 | while True: 35 | try: 36 | # Blocking call since a scene may init an IPython shell() 37 | scenes = manimlib.extract_scene.main(scene_config, run_config) 38 | for scene in scenes: 39 | scene.run() 40 | return 41 | except KillEmbedded: 42 | # Requested via the `exit_raise` IPython runline magic 43 | # by means of the reload_scene() command 44 | pass 45 | except KeyboardInterrupt: 46 | break 47 | 48 | 49 | def main(): 50 | """ 51 | Main entry point for ManimGL. 52 | """ 53 | print(f"ManimGL \033[32mv{__version__}\033[0m") 54 | 55 | args = parse_cli() 56 | if args.version and args.file is None: 57 | return 58 | if args.clear_cache: 59 | clear_cache() 60 | 61 | run_scenes() 62 | 63 | 64 | if __name__ == "__main__": 65 | main() 66 | -------------------------------------------------------------------------------- /manimlib/animation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3b1b/manim/41613db7eca9eabbb40aa830631e5206c9282f0f/manimlib/animation/__init__.py -------------------------------------------------------------------------------- /manimlib/animation/composition.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from manimlib.animation.animation import Animation 4 | from manimlib.animation.animation import prepare_animation 5 | from manimlib.mobject.mobject import _AnimationBuilder 6 | from manimlib.mobject.mobject import Group 7 | from manimlib.mobject.types.vectorized_mobject import VGroup 8 | from manimlib.mobject.types.vectorized_mobject import VMobject 9 | from manimlib.utils.bezier import integer_interpolate 10 | from manimlib.utils.bezier import interpolate 11 | from manimlib.utils.iterables import remove_list_redundancies 12 | from manimlib.utils.simple_functions import clip 13 | 14 | from typing import TYPE_CHECKING, Union, Iterable 15 | AnimationType = Union[Animation, _AnimationBuilder] 16 | 17 | if TYPE_CHECKING: 18 | from typing import Callable, Optional 19 | 20 | from manimlib.mobject.mobject import Mobject 21 | from manimlib.scene.scene import Scene 22 | 23 | 24 | DEFAULT_LAGGED_START_LAG_RATIO = 0.05 25 | 26 | 27 | class AnimationGroup(Animation): 28 | def __init__( 29 | self, 30 | *args: AnimationType | Iterable[AnimationType], 31 | run_time: float = -1, # If negative, default to sum of inputed animation runtimes 32 | lag_ratio: float = 0.0, 33 | group: Optional[Mobject] = None, 34 | group_type: Optional[type] = None, 35 | **kwargs 36 | ): 37 | animations = args[0] if isinstance(args[0], Iterable) else args 38 | self.animations = [prepare_animation(anim) for anim in animations] 39 | self.build_animations_with_timings(lag_ratio) 40 | self.max_end_time = max((awt[2] for awt in self.anims_with_timings), default=0) 41 | self.run_time = self.max_end_time if run_time < 0 else run_time 42 | self.lag_ratio = lag_ratio 43 | mobs = remove_list_redundancies([a.mobject for a in self.animations]) 44 | if group is not None: 45 | self.group = group 46 | elif group_type is not None: 47 | self.group = group_type(*mobs) 48 | elif all(isinstance(anim.mobject, VMobject) for anim in animations): 49 | self.group = VGroup(*mobs) 50 | else: 51 | self.group = Group(*mobs) 52 | 53 | super().__init__( 54 | self.group, 55 | run_time=self.run_time, 56 | lag_ratio=lag_ratio, 57 | **kwargs 58 | ) 59 | 60 | def get_all_mobjects(self) -> Mobject: 61 | return self.group 62 | 63 | def begin(self) -> None: 64 | self.group.set_animating_status(True) 65 | for anim in self.animations: 66 | anim.begin() 67 | # self.init_run_time() 68 | 69 | def finish(self) -> None: 70 | self.group.set_animating_status(False) 71 | for anim in self.animations: 72 | anim.finish() 73 | 74 | def clean_up_from_scene(self, scene: Scene) -> None: 75 | for anim in self.animations: 76 | anim.clean_up_from_scene(scene) 77 | 78 | def update_mobjects(self, dt: float) -> None: 79 | for anim in self.animations: 80 | anim.update_mobjects(dt) 81 | 82 | def calculate_max_end_time(self) -> None: 83 | self.max_end_time = max( 84 | (awt[2] for awt in self.anims_with_timings), 85 | default=0, 86 | ) 87 | if self.run_time < 0: 88 | self.run_time = self.max_end_time 89 | 90 | def build_animations_with_timings(self, lag_ratio: float) -> None: 91 | """ 92 | Creates a list of triplets of the form 93 | (anim, start_time, end_time) 94 | """ 95 | self.anims_with_timings = [] 96 | curr_time = 0 97 | for anim in self.animations: 98 | start_time = curr_time 99 | end_time = start_time + anim.get_run_time() 100 | self.anims_with_timings.append( 101 | (anim, start_time, end_time) 102 | ) 103 | # Start time of next animation is based on the lag_ratio 104 | curr_time = interpolate( 105 | start_time, end_time, lag_ratio 106 | ) 107 | 108 | def interpolate(self, alpha: float) -> None: 109 | # Note, if the run_time of AnimationGroup has been 110 | # set to something other than its default, these 111 | # times might not correspond to actual times, 112 | # e.g. of the surrounding scene. Instead they'd 113 | # be a rescaled version. But that's okay! 114 | time = alpha * self.max_end_time 115 | for anim, start_time, end_time in self.anims_with_timings: 116 | anim_time = end_time - start_time 117 | if anim_time == 0: 118 | sub_alpha = 0 119 | else: 120 | sub_alpha = clip((time - start_time) / anim_time, 0, 1) 121 | anim.interpolate(sub_alpha) 122 | 123 | 124 | class Succession(AnimationGroup): 125 | def __init__( 126 | self, 127 | *animations: Animation, 128 | lag_ratio: float = 1.0, 129 | **kwargs 130 | ): 131 | super().__init__(*animations, lag_ratio=lag_ratio, **kwargs) 132 | 133 | def begin(self) -> None: 134 | assert len(self.animations) > 0 135 | self.active_animation = self.animations[0] 136 | self.active_animation.begin() 137 | 138 | def finish(self) -> None: 139 | self.active_animation.finish() 140 | 141 | def update_mobjects(self, dt: float) -> None: 142 | self.active_animation.update_mobjects(dt) 143 | 144 | def interpolate(self, alpha: float) -> None: 145 | index, subalpha = integer_interpolate( 146 | 0, len(self.animations), alpha 147 | ) 148 | animation = self.animations[index] 149 | if animation is not self.active_animation: 150 | self.active_animation.finish() 151 | animation.begin() 152 | self.active_animation = animation 153 | animation.interpolate(subalpha) 154 | 155 | 156 | class LaggedStart(AnimationGroup): 157 | def __init__( 158 | self, 159 | *animations, 160 | lag_ratio: float = DEFAULT_LAGGED_START_LAG_RATIO, 161 | **kwargs 162 | ): 163 | super().__init__(*animations, lag_ratio=lag_ratio, **kwargs) 164 | 165 | 166 | class LaggedStartMap(LaggedStart): 167 | def __init__( 168 | self, 169 | anim_func: Callable[[Mobject], Animation], 170 | group: Mobject, 171 | run_time: float = 2.0, 172 | lag_ratio: float = DEFAULT_LAGGED_START_LAG_RATIO, 173 | **kwargs 174 | ): 175 | anim_kwargs = dict(kwargs) 176 | anim_kwargs.pop("lag_ratio", None) 177 | super().__init__( 178 | *(anim_func(submob, **anim_kwargs) for submob in group), 179 | run_time=run_time, 180 | lag_ratio=lag_ratio, 181 | group=group 182 | ) 183 | -------------------------------------------------------------------------------- /manimlib/animation/growing.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from manimlib.animation.transform import Transform 4 | 5 | from typing import TYPE_CHECKING 6 | 7 | if TYPE_CHECKING: 8 | import numpy as np 9 | 10 | from manimlib.mobject.geometry import Arrow 11 | from manimlib.mobject.mobject import Mobject 12 | from manimlib.typing import ManimColor 13 | 14 | 15 | class GrowFromPoint(Transform): 16 | def __init__( 17 | self, 18 | mobject: Mobject, 19 | point: np.ndarray, 20 | point_color: ManimColor = None, 21 | **kwargs 22 | ): 23 | self.point = point 24 | self.point_color = point_color 25 | super().__init__(mobject, **kwargs) 26 | 27 | def create_target(self) -> Mobject: 28 | return self.mobject.copy() 29 | 30 | def create_starting_mobject(self) -> Mobject: 31 | start = super().create_starting_mobject() 32 | start.scale(0) 33 | start.move_to(self.point) 34 | if self.point_color is not None: 35 | start.set_color(self.point_color) 36 | return start 37 | 38 | 39 | class GrowFromCenter(GrowFromPoint): 40 | def __init__(self, mobject: Mobject, **kwargs): 41 | point = mobject.get_center() 42 | super().__init__(mobject, point, **kwargs) 43 | 44 | 45 | class GrowFromEdge(GrowFromPoint): 46 | def __init__(self, mobject: Mobject, edge: np.ndarray, **kwargs): 47 | point = mobject.get_bounding_box_point(edge) 48 | super().__init__(mobject, point, **kwargs) 49 | 50 | 51 | class GrowArrow(GrowFromPoint): 52 | def __init__(self, arrow: Arrow, **kwargs): 53 | point = arrow.get_start() 54 | super().__init__(arrow, point, **kwargs) 55 | -------------------------------------------------------------------------------- /manimlib/animation/movement.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from manimlib.animation.animation import Animation 4 | from manimlib.utils.rate_functions import linear 5 | 6 | from typing import TYPE_CHECKING 7 | 8 | if TYPE_CHECKING: 9 | from typing import Callable, Sequence 10 | 11 | import numpy as np 12 | 13 | from manimlib.mobject.mobject import Mobject 14 | from manimlib.mobject.types.vectorized_mobject import VMobject 15 | 16 | 17 | class Homotopy(Animation): 18 | apply_function_config: dict = dict() 19 | 20 | def __init__( 21 | self, 22 | homotopy: Callable[[float, float, float, float], Sequence[float]], 23 | mobject: Mobject, 24 | run_time: float = 3.0, 25 | **kwargs 26 | ): 27 | """ 28 | Homotopy is a function from 29 | (x, y, z, t) to (x', y', z') 30 | """ 31 | self.homotopy = homotopy 32 | super().__init__(mobject, run_time=run_time, **kwargs) 33 | 34 | def function_at_time_t(self, t: float) -> Callable[[np.ndarray], Sequence[float]]: 35 | def result(p): 36 | return self.homotopy(*p, t) 37 | return result 38 | 39 | def interpolate_submobject( 40 | self, 41 | submob: Mobject, 42 | start: Mobject, 43 | alpha: float 44 | ) -> None: 45 | submob.match_points(start) 46 | submob.apply_function( 47 | self.function_at_time_t(alpha), 48 | **self.apply_function_config 49 | ) 50 | 51 | 52 | class SmoothedVectorizedHomotopy(Homotopy): 53 | apply_function_config: dict = dict(make_smooth=True) 54 | 55 | 56 | class ComplexHomotopy(Homotopy): 57 | def __init__( 58 | self, 59 | complex_homotopy: Callable[[complex, float], complex], 60 | mobject: Mobject, 61 | **kwargs 62 | ): 63 | """ 64 | Given a function form (z, t) -> w, where z and w 65 | are complex numbers and t is time, this animates 66 | the state over time 67 | """ 68 | def homotopy(x, y, z, t): 69 | c = complex_homotopy(complex(x, y), t) 70 | return (c.real, c.imag, z) 71 | 72 | super().__init__(homotopy, mobject, **kwargs) 73 | 74 | 75 | class PhaseFlow(Animation): 76 | def __init__( 77 | self, 78 | function: Callable[[np.ndarray], np.ndarray], 79 | mobject: Mobject, 80 | virtual_time: float | None = None, 81 | suspend_mobject_updating: bool = False, 82 | rate_func: Callable[[float], float] = linear, 83 | run_time: float =3.0, 84 | **kwargs 85 | ): 86 | self.function = function 87 | self.virtual_time = virtual_time or run_time 88 | super().__init__( 89 | mobject, 90 | rate_func=rate_func, 91 | run_time=run_time, 92 | suspend_mobject_updating=suspend_mobject_updating, 93 | **kwargs 94 | ) 95 | 96 | def interpolate_mobject(self, alpha: float) -> None: 97 | if hasattr(self, "last_alpha"): 98 | dt = self.virtual_time * (alpha - self.last_alpha) 99 | self.mobject.apply_function( 100 | lambda p: p + dt * self.function(p) 101 | ) 102 | self.last_alpha = alpha 103 | 104 | 105 | class MoveAlongPath(Animation): 106 | def __init__( 107 | self, 108 | mobject: Mobject, 109 | path: VMobject, 110 | suspend_mobject_updating: bool = False, 111 | **kwargs 112 | ): 113 | self.path = path 114 | super().__init__(mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs) 115 | 116 | def interpolate_mobject(self, alpha: float) -> None: 117 | point = self.path.quick_point_from_proportion(self.rate_func(alpha)) 118 | self.mobject.move_to(point) 119 | -------------------------------------------------------------------------------- /manimlib/animation/numbers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from manimlib.animation.animation import Animation 4 | from manimlib.mobject.numbers import DecimalNumber 5 | from manimlib.utils.bezier import interpolate 6 | from manimlib.utils.simple_functions import clip 7 | 8 | from typing import TYPE_CHECKING 9 | 10 | if TYPE_CHECKING: 11 | from typing import Callable 12 | 13 | 14 | class ChangingDecimal(Animation): 15 | def __init__( 16 | self, 17 | decimal_mob: DecimalNumber, 18 | number_update_func: Callable[[float], float], 19 | suspend_mobject_updating: bool = False, 20 | **kwargs 21 | ): 22 | assert isinstance(decimal_mob, DecimalNumber) 23 | self.number_update_func = number_update_func 24 | super().__init__( 25 | decimal_mob, 26 | suspend_mobject_updating=suspend_mobject_updating, 27 | **kwargs 28 | ) 29 | self.mobject = decimal_mob 30 | 31 | def interpolate_mobject(self, alpha: float) -> None: 32 | true_alpha = self.time_spanned_alpha(alpha) 33 | new_value = self.number_update_func(true_alpha) 34 | self.mobject.set_value(new_value) 35 | 36 | 37 | class ChangeDecimalToValue(ChangingDecimal): 38 | def __init__( 39 | self, 40 | decimal_mob: DecimalNumber, 41 | target_number: float | complex, 42 | **kwargs 43 | ): 44 | start_number = decimal_mob.number 45 | super().__init__( 46 | decimal_mob, 47 | lambda a: interpolate(start_number, target_number, a), 48 | **kwargs 49 | ) 50 | 51 | 52 | class CountInFrom(ChangingDecimal): 53 | def __init__( 54 | self, 55 | decimal_mob: DecimalNumber, 56 | source_number: float | complex = 0, 57 | **kwargs 58 | ): 59 | start_number = decimal_mob.get_value() 60 | super().__init__( 61 | decimal_mob, 62 | lambda a: interpolate(source_number, start_number, clip(a, 0, 1)), 63 | **kwargs 64 | ) 65 | -------------------------------------------------------------------------------- /manimlib/animation/rotation.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from manimlib.animation.animation import Animation 4 | from manimlib.constants import ORIGIN, OUT 5 | from manimlib.constants import PI, TAU 6 | from manimlib.utils.rate_functions import linear 7 | from manimlib.utils.rate_functions import smooth 8 | 9 | from typing import TYPE_CHECKING 10 | 11 | if TYPE_CHECKING: 12 | import numpy as np 13 | from typing import Callable 14 | from manimlib.mobject.mobject import Mobject 15 | 16 | 17 | class Rotating(Animation): 18 | def __init__( 19 | self, 20 | mobject: Mobject, 21 | angle: float = TAU, 22 | axis: np.ndarray = OUT, 23 | about_point: np.ndarray | None = None, 24 | about_edge: np.ndarray | None = None, 25 | run_time: float = 5.0, 26 | rate_func: Callable[[float], float] = linear, 27 | suspend_mobject_updating: bool = False, 28 | **kwargs 29 | ): 30 | self.angle = angle 31 | self.axis = axis 32 | self.about_point = about_point 33 | self.about_edge = about_edge 34 | super().__init__( 35 | mobject, 36 | run_time=run_time, 37 | rate_func=rate_func, 38 | suspend_mobject_updating=suspend_mobject_updating, 39 | **kwargs 40 | ) 41 | 42 | def interpolate_mobject(self, alpha: float) -> None: 43 | pairs = zip( 44 | self.mobject.family_members_with_points(), 45 | self.starting_mobject.family_members_with_points(), 46 | ) 47 | for sm1, sm2 in pairs: 48 | for key in sm1.pointlike_data_keys: 49 | sm1.data[key][:] = sm2.data[key] 50 | self.mobject.rotate( 51 | self.rate_func(self.time_spanned_alpha(alpha)) * self.angle, 52 | axis=self.axis, 53 | about_point=self.about_point, 54 | about_edge=self.about_edge, 55 | ) 56 | 57 | 58 | class Rotate(Rotating): 59 | def __init__( 60 | self, 61 | mobject: Mobject, 62 | angle: float = PI, 63 | axis: np.ndarray = OUT, 64 | run_time: float = 1, 65 | rate_func: Callable[[float], float] = smooth, 66 | about_edge: np.ndarray = ORIGIN, 67 | **kwargs 68 | ): 69 | super().__init__( 70 | mobject, angle, axis, 71 | run_time=run_time, 72 | rate_func=rate_func, 73 | about_edge=about_edge, 74 | **kwargs 75 | ) 76 | -------------------------------------------------------------------------------- /manimlib/animation/specialized.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from manimlib.animation.composition import LaggedStart 4 | from manimlib.animation.transform import Restore 5 | from manimlib.constants import BLACK, WHITE 6 | from manimlib.mobject.geometry import Circle 7 | from manimlib.mobject.types.vectorized_mobject import VGroup 8 | 9 | from typing import TYPE_CHECKING 10 | 11 | if TYPE_CHECKING: 12 | import numpy as np 13 | from manimlib.typing import ManimColor 14 | 15 | 16 | class Broadcast(LaggedStart): 17 | def __init__( 18 | self, 19 | focal_point: np.ndarray, 20 | small_radius: float = 0.0, 21 | big_radius: float = 5.0, 22 | n_circles: int = 5, 23 | start_stroke_width: float = 8.0, 24 | color: ManimColor = WHITE, 25 | run_time: float = 3.0, 26 | lag_ratio: float = 0.2, 27 | remover: bool = True, 28 | **kwargs 29 | ): 30 | self.focal_point = focal_point 31 | self.small_radius = small_radius 32 | self.big_radius = big_radius 33 | self.n_circles = n_circles 34 | self.start_stroke_width = start_stroke_width 35 | self.color = color 36 | 37 | circles = VGroup() 38 | for x in range(n_circles): 39 | circle = Circle( 40 | radius=big_radius, 41 | stroke_color=BLACK, 42 | stroke_width=0, 43 | ) 44 | circle.add_updater(lambda c: c.move_to(focal_point)) 45 | circle.save_state() 46 | circle.set_width(small_radius * 2) 47 | circle.set_stroke(color, start_stroke_width) 48 | circles.add(circle) 49 | super().__init__( 50 | *map(Restore, circles), 51 | run_time=run_time, 52 | lag_ratio=lag_ratio, 53 | remover=remover, 54 | **kwargs 55 | ) 56 | -------------------------------------------------------------------------------- /manimlib/animation/update.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from manimlib.animation.animation import Animation 4 | 5 | from typing import TYPE_CHECKING 6 | 7 | if TYPE_CHECKING: 8 | from typing import Callable 9 | 10 | from manimlib.mobject.mobject import Mobject 11 | 12 | 13 | class UpdateFromFunc(Animation): 14 | """ 15 | update_function of the form func(mobject), presumably 16 | to be used when the state of one mobject is dependent 17 | on another simultaneously animated mobject 18 | """ 19 | def __init__( 20 | self, 21 | mobject: Mobject, 22 | update_function: Callable[[Mobject], Mobject | None], 23 | suspend_mobject_updating: bool = False, 24 | **kwargs 25 | ): 26 | self.update_function = update_function 27 | super().__init__( 28 | mobject, 29 | suspend_mobject_updating=suspend_mobject_updating, 30 | **kwargs 31 | ) 32 | 33 | def interpolate_mobject(self, alpha: float) -> None: 34 | self.update_function(self.mobject) 35 | 36 | 37 | class UpdateFromAlphaFunc(Animation): 38 | def __init__( 39 | self, 40 | mobject: Mobject, 41 | update_function: Callable[[Mobject, float], Mobject | None], 42 | suspend_mobject_updating: bool = False, 43 | **kwargs 44 | ): 45 | self.update_function = update_function 46 | super().__init__(mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs) 47 | 48 | def interpolate_mobject(self, alpha: float) -> None: 49 | self.update_function(self.mobject, alpha) 50 | 51 | 52 | class MaintainPositionRelativeTo(Animation): 53 | def __init__( 54 | self, 55 | mobject: Mobject, 56 | tracked_mobject: Mobject, 57 | **kwargs 58 | ): 59 | self.tracked_mobject = tracked_mobject 60 | self.diff = mobject.get_center() - tracked_mobject.get_center() 61 | super().__init__(mobject, **kwargs) 62 | 63 | def interpolate_mobject(self, alpha: float) -> None: 64 | target = self.tracked_mobject.get_center() 65 | location = self.mobject.get_center() 66 | self.mobject.shift(target - location + self.diff) 67 | -------------------------------------------------------------------------------- /manimlib/camera/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3b1b/manim/41613db7eca9eabbb40aa830631e5206c9282f0f/manimlib/camera/__init__.py -------------------------------------------------------------------------------- /manimlib/constants.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import numpy as np 3 | 4 | from typing import TYPE_CHECKING 5 | if TYPE_CHECKING: 6 | from typing import List 7 | from manimlib.typing import ManimColor, Vect3 8 | 9 | # See manimlib/default_config.yml 10 | from manimlib.config import manim_config 11 | 12 | 13 | DEFAULT_RESOLUTION: tuple[int, int] = manim_config.camera.resolution 14 | DEFAULT_PIXEL_WIDTH: int = DEFAULT_RESOLUTION[0] 15 | DEFAULT_PIXEL_HEIGHT: int = DEFAULT_RESOLUTION[1] 16 | 17 | # Sizes relevant to default camera frame 18 | ASPECT_RATIO: float = DEFAULT_PIXEL_WIDTH / DEFAULT_PIXEL_HEIGHT 19 | FRAME_HEIGHT: float = manim_config.sizes.frame_height 20 | FRAME_WIDTH: float = FRAME_HEIGHT * ASPECT_RATIO 21 | FRAME_SHAPE: tuple[float, float] = (FRAME_WIDTH, FRAME_HEIGHT) 22 | FRAME_Y_RADIUS: float = FRAME_HEIGHT / 2 23 | FRAME_X_RADIUS: float = FRAME_WIDTH / 2 24 | 25 | 26 | # Helpful values for positioning mobjects 27 | SMALL_BUFF: float = manim_config.sizes.small_buff 28 | MED_SMALL_BUFF: float = manim_config.sizes.med_small_buff 29 | MED_LARGE_BUFF: float = manim_config.sizes.med_large_buff 30 | LARGE_BUFF: float = manim_config.sizes.large_buff 31 | 32 | DEFAULT_MOBJECT_TO_EDGE_BUFF: float = manim_config.sizes.default_mobject_to_edge_buff 33 | DEFAULT_MOBJECT_TO_MOBJECT_BUFF: float = manim_config.sizes.default_mobject_to_mobject_buff 34 | 35 | 36 | # Standard vectors 37 | ORIGIN: Vect3 = np.array([0., 0., 0.]) 38 | UP: Vect3 = np.array([0., 1., 0.]) 39 | DOWN: Vect3 = np.array([0., -1., 0.]) 40 | RIGHT: Vect3 = np.array([1., 0., 0.]) 41 | LEFT: Vect3 = np.array([-1., 0., 0.]) 42 | IN: Vect3 = np.array([0., 0., -1.]) 43 | OUT: Vect3 = np.array([0., 0., 1.]) 44 | X_AXIS: Vect3 = np.array([1., 0., 0.]) 45 | Y_AXIS: Vect3 = np.array([0., 1., 0.]) 46 | Z_AXIS: Vect3 = np.array([0., 0., 1.]) 47 | 48 | NULL_POINTS = np.array([[0., 0., 0.]]) 49 | 50 | # Useful abbreviations for diagonals 51 | UL: Vect3 = UP + LEFT 52 | UR: Vect3 = UP + RIGHT 53 | DL: Vect3 = DOWN + LEFT 54 | DR: Vect3 = DOWN + RIGHT 55 | 56 | TOP: Vect3 = FRAME_Y_RADIUS * UP 57 | BOTTOM: Vect3 = FRAME_Y_RADIUS * DOWN 58 | LEFT_SIDE: Vect3 = FRAME_X_RADIUS * LEFT 59 | RIGHT_SIDE: Vect3 = FRAME_X_RADIUS * RIGHT 60 | 61 | # Angles 62 | PI: float = np.pi 63 | TAU: float = 2 * PI 64 | DEG: float = TAU / 360 65 | DEGREES = DEG # Many older animations use the full name 66 | # Nice to have a constant for readability 67 | # when juxtaposed with expressions like 30 * DEG 68 | RADIANS: float = 1 69 | 70 | # Related to Text 71 | NORMAL: str = "NORMAL" 72 | ITALIC: str = "ITALIC" 73 | OBLIQUE: str = "OBLIQUE" 74 | BOLD: str = "BOLD" 75 | 76 | DEFAULT_STROKE_WIDTH: float = manim_config.vmobject.default_stroke_width 77 | 78 | # Colors 79 | BLUE_E: ManimColor = manim_config.colors.blue_e 80 | BLUE_D: ManimColor = manim_config.colors.blue_d 81 | BLUE_C: ManimColor = manim_config.colors.blue_c 82 | BLUE_B: ManimColor = manim_config.colors.blue_b 83 | BLUE_A: ManimColor = manim_config.colors.blue_a 84 | TEAL_E: ManimColor = manim_config.colors.teal_e 85 | TEAL_D: ManimColor = manim_config.colors.teal_d 86 | TEAL_C: ManimColor = manim_config.colors.teal_c 87 | TEAL_B: ManimColor = manim_config.colors.teal_b 88 | TEAL_A: ManimColor = manim_config.colors.teal_a 89 | GREEN_E: ManimColor = manim_config.colors.green_e 90 | GREEN_D: ManimColor = manim_config.colors.green_d 91 | GREEN_C: ManimColor = manim_config.colors.green_c 92 | GREEN_B: ManimColor = manim_config.colors.green_b 93 | GREEN_A: ManimColor = manim_config.colors.green_a 94 | YELLOW_E: ManimColor = manim_config.colors.yellow_e 95 | YELLOW_D: ManimColor = manim_config.colors.yellow_d 96 | YELLOW_C: ManimColor = manim_config.colors.yellow_c 97 | YELLOW_B: ManimColor = manim_config.colors.yellow_b 98 | YELLOW_A: ManimColor = manim_config.colors.yellow_a 99 | GOLD_E: ManimColor = manim_config.colors.gold_e 100 | GOLD_D: ManimColor = manim_config.colors.gold_d 101 | GOLD_C: ManimColor = manim_config.colors.gold_c 102 | GOLD_B: ManimColor = manim_config.colors.gold_b 103 | GOLD_A: ManimColor = manim_config.colors.gold_a 104 | RED_E: ManimColor = manim_config.colors.red_e 105 | RED_D: ManimColor = manim_config.colors.red_d 106 | RED_C: ManimColor = manim_config.colors.red_c 107 | RED_B: ManimColor = manim_config.colors.red_b 108 | RED_A: ManimColor = manim_config.colors.red_a 109 | MAROON_E: ManimColor = manim_config.colors.maroon_e 110 | MAROON_D: ManimColor = manim_config.colors.maroon_d 111 | MAROON_C: ManimColor = manim_config.colors.maroon_c 112 | MAROON_B: ManimColor = manim_config.colors.maroon_b 113 | MAROON_A: ManimColor = manim_config.colors.maroon_a 114 | PURPLE_E: ManimColor = manim_config.colors.purple_e 115 | PURPLE_D: ManimColor = manim_config.colors.purple_d 116 | PURPLE_C: ManimColor = manim_config.colors.purple_c 117 | PURPLE_B: ManimColor = manim_config.colors.purple_b 118 | PURPLE_A: ManimColor = manim_config.colors.purple_a 119 | GREY_E: ManimColor = manim_config.colors.grey_e 120 | GREY_D: ManimColor = manim_config.colors.grey_d 121 | GREY_C: ManimColor = manim_config.colors.grey_c 122 | GREY_B: ManimColor = manim_config.colors.grey_b 123 | GREY_A: ManimColor = manim_config.colors.grey_a 124 | WHITE: ManimColor = manim_config.colors.white 125 | BLACK: ManimColor = manim_config.colors.black 126 | GREY_BROWN: ManimColor = manim_config.colors.grey_brown 127 | DARK_BROWN: ManimColor = manim_config.colors.dark_brown 128 | LIGHT_BROWN: ManimColor = manim_config.colors.light_brown 129 | PINK: ManimColor = manim_config.colors.pink 130 | LIGHT_PINK: ManimColor = manim_config.colors.light_pink 131 | GREEN_SCREEN: ManimColor = manim_config.colors.green_screen 132 | ORANGE: ManimColor = manim_config.colors.orange 133 | PURE_RED: ManimColor = manim_config.colors.pure_red 134 | PURE_GREEN: ManimColor = manim_config.colors.pure_green 135 | PURE_BLUE: ManimColor = manim_config.colors.pure_blue 136 | 137 | MANIM_COLORS: List[ManimColor] = list(manim_config.colors.values()) 138 | 139 | # Abbreviated names for the "median" colors 140 | BLUE: ManimColor = BLUE_C 141 | TEAL: ManimColor = TEAL_C 142 | GREEN: ManimColor = GREEN_C 143 | YELLOW: ManimColor = YELLOW_C 144 | GOLD: ManimColor = GOLD_C 145 | RED: ManimColor = RED_C 146 | MAROON: ManimColor = MAROON_C 147 | PURPLE: ManimColor = PURPLE_C 148 | GREY: ManimColor = GREY_C 149 | 150 | COLORMAP_3B1B: List[ManimColor] = [BLUE_E, GREEN, YELLOW, RED] 151 | 152 | # Default mobject colors should be configurable just like background color 153 | # DEFAULT_MOBJECT_COLOR is mainly for text, tex, line, etc... mobjects. Default is WHITE 154 | # DEFAULT_LIGHT_COLOR is mainly for things like axes, arrows, annulus and other lightly colored mobjects. Default is GREY_B 155 | DEFAULT_MOBJECT_COLOR: ManimColor = manim_config.mobject.default_mobject_color or WHITE 156 | DEFAULT_LIGHT_COLOR: ManimColor = manim_config.mobject.default_light_color or GREY_B 157 | 158 | DEFAULT_VMOBJECT_STROKE_COLOR : ManimColor = manim_config.vmobject.default_stroke_color or GREY_A 159 | DEFAULT_VMOBJECT_FILL_COLOR : ManimColor = manim_config.vmobject.default_fill_color or GREY_C 160 | -------------------------------------------------------------------------------- /manimlib/default_config.yml: -------------------------------------------------------------------------------- 1 | # This file determines the default configuration for how manim is 2 | # run, including names for directories it will write to, default 3 | # parameters for various classes, style choices, etc. To customize 4 | # your own, create a custom_config.yml file in whatever directory 5 | # you are running manim. For 3blue1brown, for instance, mind is 6 | # here: https://github.com/3b1b/videos/blob/master/custom_config.yml 7 | 8 | # Alternatively, you can create it wherever you like, and on running 9 | # manim, pass in `--config_file /path/to/custom/config/file.yml` 10 | 11 | directories: 12 | # Set this to true if you want the path to video files 13 | # to match the directory structure of the path to the 14 | # source code generating that video 15 | mirror_module_path: False 16 | # Manim may write to and read from the file system, e.g. 17 | # to render videos and to look for svg/png assets. This 18 | # will specify where those assets live, with a base directory, 19 | # and various subdirectory names within it 20 | base: "" 21 | subdirs: 22 | # Where should manim output video and image files? 23 | output: "videos" 24 | # If you want to use images, manim will look to these folders to find them 25 | raster_images: "raster_images" 26 | vector_images: "vector_images" 27 | # If you want to use sounds, manim will look here to find it. 28 | sounds: "sounds" 29 | # Place for other forms of data relevant to any projects, like csv's 30 | data: "data" 31 | # When downloading, say an image, where will it go? 32 | downloads: "downloads" 33 | # For certain object types, especially Tex and Text, manim will save information 34 | # to file to prevent the need to re-compute, e.g. recompiling the latex. By default, 35 | # it stores this saved data to whatever directory appdirs.user_cache_dir("manim") returns, 36 | # but here a user can specify a different cache location 37 | cache: "" 38 | window: 39 | # The position of window on screen. UR -> Upper Right, and likewise DL -> Down and Left, 40 | # UO would be upper middle, etc. 41 | position_string: UR 42 | # If using multiple monitors, which one should show the window 43 | monitor_index: 0 44 | # If not full screen, the default to give it half the screen width 45 | full_screen: False 46 | # Other optional specifications that override the above include: 47 | # position: (500, 500) # Specific position, in pixel coordinates, for upper right corner 48 | # size: (1920, 1080) # Specific size, in pixels 49 | camera: 50 | resolution: (1920, 1080) 51 | background_color: "#333333" 52 | fps: 30 53 | background_opacity: 1.0 54 | file_writer: 55 | # What command to use for ffmpeg 56 | ffmpeg_bin: "ffmpeg" 57 | # Parameters to pass into ffmpeg 58 | video_codec: "libx264" 59 | pixel_format: "yuv420p" 60 | saturation: 1.0 61 | gamma: 1.0 62 | # Most of the scene configuration will come from CLI arguments, 63 | # but defaults can be set here 64 | scene: 65 | show_animation_progress: False 66 | leave_progress_bars: False 67 | # When skipping animations, should a single frame be rendered 68 | # at the end of each play call? 69 | preview_while_skipping: True 70 | # How long does a scene pause on Scene.wait calls 71 | default_wait_time: 1.0 72 | vmobject: 73 | default_stroke_width: 4.0 74 | default_stroke_color: "#DDDDDD" # Default is GREY_A 75 | default_fill_color: "#888888" # Default is GREY_C 76 | mobject: 77 | default_mobject_color: "#FFFFFF" # Default is WHITE 78 | default_light_color: "#BBBBBB" # Default is GREY_B 79 | tex: 80 | # See tex_templates.yml 81 | template: "default" 82 | text: 83 | # font: "Cambria Math" 84 | font: "Consolas" 85 | alignment: "LEFT" 86 | embed: 87 | exception_mode: "Verbose" 88 | autoreload: False 89 | resolution_options: 90 | # When the user passes in -l, -m, --hd or --uhd, these are the corresponding 91 | # resolutions 92 | low: (854, 480) 93 | med: (1280, 720) 94 | high: (1920, 1080) 95 | 4k: (3840, 2160) 96 | sizes: 97 | # This determines the scale of the manim coordinate system with respect to 98 | # the viewing frame 99 | frame_height: 8.0 100 | # These determine the constants SMALL_BUFF, MED_SMALL_BUFF, etc., useful 101 | # for nudging things around and having default spacing values 102 | small_buff: 0.1 103 | med_small_buff: 0.25 104 | med_large_buff: 0.5 105 | large_buff: 1.0 106 | # Default buffers used in Mobject.next_to or Mobject.to_edge 107 | default_mobject_to_edge_buff: 0.5 108 | default_mobject_to_mobject_buff: 0.25 109 | key_bindings: 110 | pan_3d: "d" 111 | pan: "f" 112 | reset: "r" 113 | quit: "q" # Together with command 114 | select: "s" 115 | unselect: "u" 116 | grab: "g" 117 | x_grab: "h" 118 | y_grab: "v" 119 | resize: "t" 120 | color: "c" 121 | information: "i" 122 | cursor: "k" 123 | colors: 124 | blue_e: "#1C758A" 125 | blue_d: "#29ABCA" 126 | blue_c: "#58C4DD" 127 | blue_b: "#9CDCEB" 128 | blue_a: "#C7E9F1" 129 | teal_e: "#49A88F" 130 | teal_d: "#55C1A7" 131 | teal_c: "#5CD0B3" 132 | teal_b: "#76DDC0" 133 | teal_a: "#ACEAD7" 134 | green_e: "#699C52" 135 | green_d: "#77B05D" 136 | green_c: "#83C167" 137 | green_b: "#A6CF8C" 138 | green_a: "#C9E2AE" 139 | yellow_e: "#E8C11C" 140 | yellow_d: "#F4D345" 141 | yellow_c: "#FFFF00" 142 | yellow_b: "#FFEA94" 143 | yellow_a: "#FFF1B6" 144 | gold_e: "#C78D46" 145 | gold_d: "#E1A158" 146 | gold_c: "#F0AC5F" 147 | gold_b: "#F9B775" 148 | gold_a: "#F7C797" 149 | red_e: "#CF5044" 150 | red_d: "#E65A4C" 151 | red_c: "#FC6255" 152 | red_b: "#FF8080" 153 | red_a: "#F7A1A3" 154 | maroon_e: "#94424F" 155 | maroon_d: "#A24D61" 156 | maroon_c: "#C55F73" 157 | maroon_b: "#EC92AB" 158 | maroon_a: "#ECABC1" 159 | purple_e: "#644172" 160 | purple_d: "#715582" 161 | purple_c: "#9A72AC" 162 | purple_b: "#B189C6" 163 | purple_a: "#CAA3E8" 164 | grey_e: "#222222" 165 | grey_d: "#444444" 166 | grey_c: "#888888" 167 | grey_b: "#BBBBBB" 168 | grey_a: "#DDDDDD" 169 | white: "#FFFFFF" 170 | black: "#000000" 171 | grey_brown: "#736357" 172 | dark_brown: "#8B4513" 173 | light_brown: "#CD853F" 174 | pink: "#D147BD" 175 | light_pink: "#DC75CD" 176 | green_screen: "#00FF00" 177 | orange: "#FF862F" 178 | pure_red: "#FF0000" 179 | pure_green: "#00FF00" 180 | pure_blue: "#0000FF" 181 | # Can be DEBUG / INFO / WARNING / ERROR / CRITICAL 182 | log_level: "INFO" 183 | universal_import_line: "from manimlib import *" 184 | ignore_manimlib_modules_on_reload: True 185 | -------------------------------------------------------------------------------- /manimlib/event_handler/__init__.py: -------------------------------------------------------------------------------- 1 | from manimlib.event_handler.event_dispatcher import EventDispatcher 2 | 3 | 4 | # This is supposed to be a Singleton 5 | # i.e., during runtime there should be only one object of Event Dispatcher 6 | EVENT_DISPATCHER = EventDispatcher() 7 | -------------------------------------------------------------------------------- /manimlib/event_handler/event_dispatcher.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | from manimlib.event_handler.event_listner import EventListener 6 | from manimlib.event_handler.event_type import EventType 7 | 8 | 9 | class EventDispatcher(object): 10 | def __init__(self): 11 | self.event_listners: dict[ 12 | EventType, list[EventListener] 13 | ] = { 14 | event_type: [] 15 | for event_type in EventType 16 | } 17 | self.mouse_point = np.array((0., 0., 0.)) 18 | self.mouse_drag_point = np.array((0., 0., 0.)) 19 | self.pressed_keys: set[int] = set() 20 | self.draggable_object_listners: list[EventListener] = [] 21 | 22 | def add_listner(self, event_listner: EventListener): 23 | assert isinstance(event_listner, EventListener) 24 | self.event_listners[event_listner.event_type].append(event_listner) 25 | return self 26 | 27 | def remove_listner(self, event_listner: EventListener): 28 | assert isinstance(event_listner, EventListener) 29 | try: 30 | while event_listner in self.event_listners[event_listner.event_type]: 31 | self.event_listners[event_listner.event_type].remove(event_listner) 32 | except: 33 | # raise ValueError("Handler is not handling this event, so cannot remove it.") 34 | pass 35 | return self 36 | 37 | def dispatch(self, event_type: EventType, **event_data): 38 | if event_type == EventType.MouseMotionEvent: 39 | self.mouse_point = event_data["point"] 40 | elif event_type == EventType.MouseDragEvent: 41 | self.mouse_drag_point = event_data["point"] 42 | elif event_type == EventType.KeyPressEvent: 43 | self.pressed_keys.add(event_data["symbol"]) # Modifiers? 44 | elif event_type == EventType.KeyReleaseEvent: 45 | self.pressed_keys.difference_update({event_data["symbol"]}) # Modifiers? 46 | elif event_type == EventType.MousePressEvent: 47 | self.draggable_object_listners = [ 48 | listner 49 | for listner in self.event_listners[EventType.MouseDragEvent] 50 | if listner.mobject.is_point_touching(self.mouse_point) 51 | ] 52 | elif event_type == EventType.MouseReleaseEvent: 53 | self.draggable_object_listners = [] 54 | 55 | propagate_event = None 56 | 57 | if event_type == EventType.MouseDragEvent: 58 | for listner in self.draggable_object_listners: 59 | assert isinstance(listner, EventListener) 60 | propagate_event = listner.callback(listner.mobject, event_data) 61 | if propagate_event is not None and propagate_event is False: 62 | return propagate_event 63 | 64 | elif event_type.value.startswith('mouse'): 65 | for listner in self.event_listners[event_type]: 66 | if listner.mobject.is_point_touching(self.mouse_point): 67 | propagate_event = listner.callback( 68 | listner.mobject, event_data) 69 | if propagate_event is not None and propagate_event is False: 70 | return propagate_event 71 | 72 | elif event_type.value.startswith('key'): 73 | for listner in self.event_listners[event_type]: 74 | propagate_event = listner.callback(listner.mobject, event_data) 75 | if propagate_event is not None and propagate_event is False: 76 | return propagate_event 77 | 78 | return propagate_event 79 | 80 | def get_listners_count(self) -> int: 81 | return sum([len(value) for key, value in self.event_listners.items()]) 82 | 83 | def get_mouse_point(self) -> np.ndarray: 84 | return self.mouse_point 85 | 86 | def get_mouse_drag_point(self) -> np.ndarray: 87 | return self.mouse_drag_point 88 | 89 | def is_key_pressed(self, symbol: int) -> bool: 90 | return (symbol in self.pressed_keys) 91 | 92 | __iadd__ = add_listner 93 | __isub__ = remove_listner 94 | __call__ = dispatch 95 | __len__ = get_listners_count 96 | -------------------------------------------------------------------------------- /manimlib/event_handler/event_listner.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | if TYPE_CHECKING: 6 | from typing import Callable 7 | 8 | from manimlib.event_handler.event_type import EventType 9 | from manimlib.mobject.mobject import Mobject 10 | 11 | 12 | class EventListener(object): 13 | def __init__( 14 | self, 15 | mobject: Mobject, 16 | event_type: EventType, 17 | event_callback: Callable[[Mobject, dict[str]]] 18 | ): 19 | self.mobject = mobject 20 | self.event_type = event_type 21 | self.callback = event_callback 22 | 23 | def __eq__(self, o: object) -> bool: 24 | return_val = False 25 | try: 26 | return_val = self.callback == o.callback \ 27 | and self.mobject == o.mobject \ 28 | and self.event_type == o.event_type 29 | except: 30 | pass 31 | return return_val 32 | -------------------------------------------------------------------------------- /manimlib/event_handler/event_type.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class EventType(Enum): 5 | MouseMotionEvent = 'mouse_motion_event' 6 | MousePressEvent = 'mouse_press_event' 7 | MouseReleaseEvent = 'mouse_release_event' 8 | MouseDragEvent = 'mouse_drag_event' 9 | MouseScrollEvent = 'mouse_scroll_event' 10 | KeyPressEvent = 'key_press_event' 11 | KeyReleaseEvent = 'key_release_event' 12 | -------------------------------------------------------------------------------- /manimlib/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from rich.logging import RichHandler 4 | 5 | __all__ = ["log"] 6 | 7 | 8 | FORMAT = "%(message)s" 9 | logging.basicConfig( 10 | level=logging.WARNING, format=FORMAT, datefmt="[%X]", handlers=[RichHandler()] 11 | ) 12 | 13 | log = logging.getLogger("manimgl") 14 | -------------------------------------------------------------------------------- /manimlib/mobject/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3b1b/manim/41613db7eca9eabbb40aa830631e5206c9282f0f/manimlib/mobject/__init__.py -------------------------------------------------------------------------------- /manimlib/mobject/boolean_ops.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | import pathops 5 | 6 | from manimlib.mobject.types.vectorized_mobject import VMobject 7 | 8 | 9 | # Boolean operations between 2D mobjects 10 | # Borrowed from https://github.com/ManimCommunity/manim/ 11 | 12 | def _convert_vmobject_to_skia_path(vmobject: VMobject) -> pathops.Path: 13 | path = pathops.Path() 14 | for submob in vmobject.family_members_with_points(): 15 | for subpath in submob.get_subpaths(): 16 | quads = vmobject.get_bezier_tuples_from_points(subpath) 17 | start = subpath[0] 18 | path.moveTo(*start[:2]) 19 | for p0, p1, p2 in quads: 20 | path.quadTo(*p1[:2], *p2[:2]) 21 | if vmobject.consider_points_equal(subpath[0], subpath[-1]): 22 | path.close() 23 | return path 24 | 25 | 26 | def _convert_skia_path_to_vmobject( 27 | path: pathops.Path, 28 | vmobject: VMobject 29 | ) -> VMobject: 30 | PathVerb = pathops.PathVerb 31 | current_path_start = np.array([0.0, 0.0, 0.0]) 32 | for path_verb, points in path: 33 | if path_verb == PathVerb.CLOSE: 34 | vmobject.add_line_to(current_path_start) 35 | else: 36 | points = np.hstack((np.array(points), np.zeros((len(points), 1)))) 37 | if path_verb == PathVerb.MOVE: 38 | for point in points: 39 | current_path_start = point 40 | vmobject.start_new_path(point) 41 | elif path_verb == PathVerb.CUBIC: 42 | vmobject.add_cubic_bezier_curve_to(*points) 43 | elif path_verb == PathVerb.LINE: 44 | vmobject.add_line_to(points[0]) 45 | elif path_verb == PathVerb.QUAD: 46 | vmobject.add_quadratic_bezier_curve_to(*points) 47 | else: 48 | raise Exception(f"Unsupported: {path_verb}") 49 | return vmobject.reverse_points() 50 | 51 | 52 | class Union(VMobject): 53 | def __init__(self, *vmobjects: VMobject, **kwargs): 54 | if len(vmobjects) < 2: 55 | raise ValueError("At least 2 mobjects needed for Union.") 56 | super().__init__(**kwargs) 57 | outpen = pathops.Path() 58 | paths = [ 59 | _convert_vmobject_to_skia_path(vmobject) 60 | for vmobject in vmobjects 61 | ] 62 | pathops.union(paths, outpen.getPen()) 63 | _convert_skia_path_to_vmobject(outpen, self) 64 | 65 | 66 | class Difference(VMobject): 67 | def __init__(self, subject: VMobject, clip: VMobject, **kwargs): 68 | super().__init__(**kwargs) 69 | outpen = pathops.Path() 70 | pathops.difference( 71 | [_convert_vmobject_to_skia_path(subject)], 72 | [_convert_vmobject_to_skia_path(clip)], 73 | outpen.getPen(), 74 | ) 75 | _convert_skia_path_to_vmobject(outpen, self) 76 | 77 | 78 | class Intersection(VMobject): 79 | def __init__(self, *vmobjects: VMobject, **kwargs): 80 | if len(vmobjects) < 2: 81 | raise ValueError("At least 2 mobjects needed for Intersection.") 82 | super().__init__(**kwargs) 83 | outpen = pathops.Path() 84 | pathops.intersection( 85 | [_convert_vmobject_to_skia_path(vmobjects[0])], 86 | [_convert_vmobject_to_skia_path(vmobjects[1])], 87 | outpen.getPen(), 88 | ) 89 | new_outpen = outpen 90 | for _i in range(2, len(vmobjects)): 91 | new_outpen = pathops.Path() 92 | pathops.intersection( 93 | [outpen], 94 | [_convert_vmobject_to_skia_path(vmobjects[_i])], 95 | new_outpen.getPen(), 96 | ) 97 | outpen = new_outpen 98 | _convert_skia_path_to_vmobject(outpen, self) 99 | 100 | 101 | class Exclusion(VMobject): 102 | def __init__(self, *vmobjects: VMobject, **kwargs): 103 | if len(vmobjects) < 2: 104 | raise ValueError("At least 2 mobjects needed for Exclusion.") 105 | super().__init__(**kwargs) 106 | outpen = pathops.Path() 107 | pathops.xor( 108 | [_convert_vmobject_to_skia_path(vmobjects[0])], 109 | [_convert_vmobject_to_skia_path(vmobjects[1])], 110 | outpen.getPen(), 111 | ) 112 | new_outpen = outpen 113 | for _i in range(2, len(vmobjects)): 114 | new_outpen = pathops.Path() 115 | pathops.xor( 116 | [outpen], 117 | [_convert_vmobject_to_skia_path(vmobjects[_i])], 118 | new_outpen.getPen(), 119 | ) 120 | outpen = new_outpen 121 | _convert_skia_path_to_vmobject(outpen, self) 122 | -------------------------------------------------------------------------------- /manimlib/mobject/changing.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | from manimlib.constants import BLUE_B, BLUE_D, BLUE_E, GREY_BROWN, DEFAULT_MOBJECT_COLOR 6 | from manimlib.mobject.mobject import Mobject 7 | from manimlib.mobject.types.vectorized_mobject import VGroup 8 | from manimlib.mobject.types.vectorized_mobject import VMobject 9 | from manimlib.utils.rate_functions import smooth 10 | 11 | from typing import TYPE_CHECKING 12 | 13 | if TYPE_CHECKING: 14 | from typing import Callable, List, Iterable 15 | from manimlib.typing import ManimColor, Vect3, Self 16 | 17 | 18 | class AnimatedBoundary(VGroup): 19 | def __init__( 20 | self, 21 | vmobject: VMobject, 22 | colors: List[ManimColor] = [BLUE_D, BLUE_B, BLUE_E, GREY_BROWN], 23 | max_stroke_width: float = 3.0, 24 | cycle_rate: float = 0.5, 25 | back_and_forth: bool = True, 26 | draw_rate_func: Callable[[float], float] = smooth, 27 | fade_rate_func: Callable[[float], float] = smooth, 28 | **kwargs 29 | ): 30 | super().__init__(**kwargs) 31 | self.vmobject: VMobject = vmobject 32 | self.colors = colors 33 | self.max_stroke_width = max_stroke_width 34 | self.cycle_rate = cycle_rate 35 | self.back_and_forth = back_and_forth 36 | self.draw_rate_func = draw_rate_func 37 | self.fade_rate_func = fade_rate_func 38 | 39 | self.boundary_copies: list[VMobject] = [ 40 | vmobject.copy().set_style( 41 | stroke_width=0, 42 | fill_opacity=0 43 | ) 44 | for x in range(2) 45 | ] 46 | self.add(*self.boundary_copies) 47 | self.total_time: float = 0 48 | self.add_updater( 49 | lambda m, dt: self.update_boundary_copies(dt) 50 | ) 51 | 52 | def update_boundary_copies(self, dt: float) -> Self: 53 | # Not actual time, but something which passes at 54 | # an altered rate to make the implementation below 55 | # cleaner 56 | time = self.total_time * self.cycle_rate 57 | growing, fading = self.boundary_copies 58 | colors = self.colors 59 | msw = self.max_stroke_width 60 | vmobject = self.vmobject 61 | 62 | index = int(time % len(colors)) 63 | alpha = time % 1 64 | draw_alpha = self.draw_rate_func(alpha) 65 | fade_alpha = self.fade_rate_func(alpha) 66 | 67 | if self.back_and_forth and int(time) % 2 == 1: 68 | bounds = (1 - draw_alpha, 1) 69 | else: 70 | bounds = (0, draw_alpha) 71 | self.full_family_become_partial(growing, vmobject, *bounds) 72 | growing.set_stroke(colors[index], width=msw) 73 | 74 | if time >= 1: 75 | self.full_family_become_partial(fading, vmobject, 0, 1) 76 | fading.set_stroke( 77 | color=colors[index - 1], 78 | width=(1 - fade_alpha) * msw 79 | ) 80 | 81 | self.total_time += dt 82 | return self 83 | 84 | def full_family_become_partial( 85 | self, 86 | mob1: VMobject, 87 | mob2: VMobject, 88 | a: float, 89 | b: float 90 | ) -> Self: 91 | family1 = mob1.family_members_with_points() 92 | family2 = mob2.family_members_with_points() 93 | for sm1, sm2 in zip(family1, family2): 94 | sm1.pointwise_become_partial(sm2, a, b) 95 | return self 96 | 97 | 98 | class TracedPath(VMobject): 99 | def __init__( 100 | self, 101 | traced_point_func: Callable[[], Vect3], 102 | time_traced: float = np.inf, 103 | time_per_anchor: float = 1.0 / 15, 104 | stroke_width: float | Iterable[float] = 2.0, 105 | stroke_color: ManimColor = DEFAULT_MOBJECT_COLOR, 106 | **kwargs 107 | ): 108 | super().__init__(**kwargs) 109 | self.traced_point_func = traced_point_func 110 | self.time_traced = time_traced 111 | self.time_per_anchor = time_per_anchor 112 | self.time: float = 0 113 | self.traced_points: list[np.ndarray] = [] 114 | self.add_updater(lambda m, dt: m.update_path(dt)) 115 | self.always.set_stroke(stroke_color, stroke_width) 116 | 117 | def update_path(self, dt: float) -> Self: 118 | if dt == 0: 119 | return self 120 | point = self.traced_point_func().copy() 121 | self.traced_points.append(point) 122 | 123 | if self.time_traced < np.inf: 124 | n_relevant_points = int(self.time_traced / dt + 0.5) 125 | n_tps = len(self.traced_points) 126 | if n_tps < n_relevant_points: 127 | points = self.traced_points + [point] * (n_relevant_points - n_tps) 128 | else: 129 | points = self.traced_points[n_tps - n_relevant_points:] 130 | # Every now and then refresh the list 131 | if n_tps > 10 * n_relevant_points: 132 | self.traced_points = self.traced_points[-n_relevant_points:] 133 | else: 134 | points = self.traced_points 135 | 136 | if points: 137 | self.set_points_smoothly(points) 138 | 139 | self.time += dt 140 | return self 141 | 142 | 143 | class TracingTail(TracedPath): 144 | def __init__( 145 | self, 146 | mobject_or_func: Mobject | Callable[[], np.ndarray], 147 | time_traced: float = 1.0, 148 | stroke_width: float | Iterable[float] = (0, 3), 149 | stroke_opacity: float | Iterable[float] = (0, 1), 150 | stroke_color: ManimColor = DEFAULT_MOBJECT_COLOR, 151 | **kwargs 152 | ): 153 | if isinstance(mobject_or_func, Mobject): 154 | func = mobject_or_func.get_center 155 | else: 156 | func = mobject_or_func 157 | super().__init__( 158 | func, 159 | time_traced=time_traced, 160 | stroke_width=stroke_width, 161 | stroke_opacity=stroke_opacity, 162 | stroke_color=stroke_color, 163 | **kwargs 164 | ) 165 | self.add_updater(lambda m: m.set_stroke(width=stroke_width, opacity=stroke_opacity)) 166 | -------------------------------------------------------------------------------- /manimlib/mobject/frame.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from manimlib.constants import BLACK, GREY_E 4 | from manimlib.constants import FRAME_HEIGHT 5 | from manimlib.mobject.geometry import Rectangle 6 | 7 | from typing import TYPE_CHECKING 8 | if TYPE_CHECKING: 9 | from manimlib.typing import ManimColor 10 | 11 | 12 | class ScreenRectangle(Rectangle): 13 | def __init__( 14 | self, 15 | aspect_ratio: float = 16.0 / 9.0, 16 | height: float = 4, 17 | **kwargs 18 | ): 19 | super().__init__( 20 | width=aspect_ratio * height, 21 | height=height, 22 | **kwargs 23 | ) 24 | 25 | 26 | class FullScreenRectangle(ScreenRectangle): 27 | def __init__( 28 | self, 29 | height: float = FRAME_HEIGHT, 30 | fill_color: ManimColor = GREY_E, 31 | fill_opacity: float = 1, 32 | stroke_width: float = 0, 33 | **kwargs, 34 | ): 35 | super().__init__( 36 | height=height, 37 | fill_color=fill_color, 38 | fill_opacity=fill_opacity, 39 | stroke_width=stroke_width, 40 | **kwargs 41 | ) 42 | 43 | 44 | class FullScreenFadeRectangle(FullScreenRectangle): 45 | def __init__( 46 | self, 47 | stroke_width: float = 0.0, 48 | fill_color: ManimColor = BLACK, 49 | fill_opacity: float = 0.7, 50 | **kwargs, 51 | ): 52 | super().__init__( 53 | stroke_width=stroke_width, 54 | fill_color=fill_color, 55 | fill_opacity=fill_opacity, 56 | ) 57 | -------------------------------------------------------------------------------- /manimlib/mobject/functions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from isosurfaces import plot_isoline 4 | import numpy as np 5 | 6 | from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS 7 | from manimlib.constants import YELLOW 8 | from manimlib.mobject.types.vectorized_mobject import VMobject 9 | 10 | from typing import TYPE_CHECKING 11 | 12 | if TYPE_CHECKING: 13 | from typing import Callable, Sequence, Tuple 14 | from manimlib.typing import ManimColor, Vect3 15 | 16 | 17 | class ParametricCurve(VMobject): 18 | def __init__( 19 | self, 20 | t_func: Callable[[float], Sequence[float] | Vect3], 21 | t_range: Tuple[float, float, float] = (0, 1, 0.1), 22 | epsilon: float = 1e-8, 23 | # TODO, automatically figure out discontinuities 24 | discontinuities: Sequence[float] = [], 25 | use_smoothing: bool = True, 26 | **kwargs 27 | ): 28 | self.t_func = t_func 29 | self.t_range = t_range 30 | self.epsilon = epsilon 31 | self.discontinuities = discontinuities 32 | self.use_smoothing = use_smoothing 33 | super().__init__(**kwargs) 34 | 35 | def get_point_from_function(self, t: float) -> Vect3: 36 | return np.array(self.t_func(t)) 37 | 38 | def init_points(self): 39 | t_min, t_max, step = self.t_range 40 | 41 | jumps = np.array(self.discontinuities) 42 | jumps = jumps[(jumps > t_min) & (jumps < t_max)] 43 | boundary_times = [t_min, t_max, *(jumps - self.epsilon), *(jumps + self.epsilon)] 44 | boundary_times.sort() 45 | for t1, t2 in zip(boundary_times[0::2], boundary_times[1::2]): 46 | t_range = [*np.arange(t1, t2, step), t2] 47 | points = np.array([self.t_func(t) for t in t_range]) 48 | self.start_new_path(points[0]) 49 | self.add_points_as_corners(points[1:]) 50 | if self.use_smoothing: 51 | self.make_smooth(approx=True) 52 | if not self.has_points(): 53 | self.set_points(np.array([self.t_func(t_min)])) 54 | return self 55 | 56 | def get_t_func(self): 57 | return self.t_func 58 | 59 | def get_function(self): 60 | if hasattr(self, "underlying_function"): 61 | return self.underlying_function 62 | if hasattr(self, "function"): 63 | return self.function 64 | 65 | def get_x_range(self): 66 | if hasattr(self, "x_range"): 67 | return self.x_range 68 | 69 | 70 | class FunctionGraph(ParametricCurve): 71 | def __init__( 72 | self, 73 | function: Callable[[float], float], 74 | x_range: Tuple[float, float, float] = (-8, 8, 0.25), 75 | color: ManimColor = YELLOW, 76 | **kwargs 77 | ): 78 | self.function = function 79 | self.x_range = x_range 80 | 81 | def parametric_function(t): 82 | return [t, function(t), 0] 83 | 84 | super().__init__(parametric_function, self.x_range, **kwargs) 85 | 86 | 87 | class ImplicitFunction(VMobject): 88 | def __init__( 89 | self, 90 | func: Callable[[float, float], float], 91 | x_range: Tuple[float, float] = (-FRAME_X_RADIUS, FRAME_X_RADIUS), 92 | y_range: Tuple[float, float] = (-FRAME_Y_RADIUS, FRAME_Y_RADIUS), 93 | min_depth: int = 5, 94 | max_quads: int = 1500, 95 | use_smoothing: bool = False, 96 | joint_type: str = 'no_joint', 97 | **kwargs 98 | ): 99 | super().__init__(joint_type=joint_type, **kwargs) 100 | 101 | p_min, p_max = ( 102 | np.array([x_range[0], y_range[0]]), 103 | np.array([x_range[1], y_range[1]]), 104 | ) 105 | curves = plot_isoline( 106 | fn=lambda u: func(u[0], u[1]), 107 | pmin=p_min, 108 | pmax=p_max, 109 | min_depth=min_depth, 110 | max_quads=max_quads, 111 | ) # returns a list of lists of 2D points 112 | curves = [ 113 | np.pad(curve, [(0, 0), (0, 1)]) 114 | for curve in curves 115 | if curve != [] 116 | ] # add z coord as 0 117 | for curve in curves: 118 | self.start_new_path(curve[0]) 119 | self.add_points_as_corners(curve[1:]) 120 | if use_smoothing: 121 | self.make_smooth() 122 | -------------------------------------------------------------------------------- /manimlib/mobject/mobject_update_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import inspect 4 | 5 | from manimlib.constants import DEG 6 | from manimlib.constants import RIGHT 7 | from manimlib.mobject.mobject import Mobject 8 | from manimlib.utils.simple_functions import clip 9 | 10 | from typing import TYPE_CHECKING 11 | 12 | if TYPE_CHECKING: 13 | from typing import Callable 14 | 15 | import numpy as np 16 | 17 | from manimlib.animation.animation import Animation 18 | 19 | 20 | def assert_is_mobject_method(method): 21 | assert inspect.ismethod(method) 22 | mobject = method.__self__ 23 | assert isinstance(mobject, Mobject) 24 | 25 | 26 | def always(method, *args, **kwargs): 27 | assert_is_mobject_method(method) 28 | mobject = method.__self__ 29 | func = method.__func__ 30 | mobject.add_updater(lambda m: func(m, *args, **kwargs)) 31 | return mobject 32 | 33 | 34 | def f_always(method, *arg_generators, **kwargs): 35 | """ 36 | More functional version of always, where instead 37 | of taking in args, it takes in functions which output 38 | the relevant arguments. 39 | """ 40 | assert_is_mobject_method(method) 41 | mobject = method.__self__ 42 | func = method.__func__ 43 | 44 | def updater(mob): 45 | args = [ 46 | arg_generator() 47 | for arg_generator in arg_generators 48 | ] 49 | func(mob, *args, **kwargs) 50 | 51 | mobject.add_updater(updater) 52 | return mobject 53 | 54 | 55 | def always_redraw(func: Callable[..., Mobject], *args, **kwargs) -> Mobject: 56 | mob = func(*args, **kwargs) 57 | mob.add_updater(lambda m: mob.become(func(*args, **kwargs))) 58 | return mob 59 | 60 | 61 | def always_shift( 62 | mobject: Mobject, 63 | direction: np.ndarray = RIGHT, 64 | rate: float = 0.1 65 | ) -> Mobject: 66 | mobject.add_updater( 67 | lambda m, dt: m.shift(dt * rate * direction) 68 | ) 69 | return mobject 70 | 71 | 72 | def always_rotate( 73 | mobject: Mobject, 74 | rate: float = 20 * DEG, 75 | **kwargs 76 | ) -> Mobject: 77 | mobject.add_updater( 78 | lambda m, dt: m.rotate(dt * rate, **kwargs) 79 | ) 80 | return mobject 81 | 82 | 83 | def turn_animation_into_updater( 84 | animation: Animation, 85 | cycle: bool = False, 86 | **kwargs 87 | ) -> Mobject: 88 | """ 89 | Add an updater to the animation's mobject which applies 90 | the interpolation and update functions of the animation 91 | 92 | If cycle is True, this repeats over and over. Otherwise, 93 | the updater will be popped uplon completion 94 | """ 95 | mobject = animation.mobject 96 | animation.update_rate_info(**kwargs) 97 | animation.suspend_mobject_updating = False 98 | animation.begin() 99 | animation.total_time = 0 100 | 101 | def update(m, dt): 102 | run_time = animation.get_run_time() 103 | time_ratio = animation.total_time / run_time 104 | if cycle: 105 | alpha = time_ratio % 1 106 | else: 107 | alpha = clip(time_ratio, 0, 1) 108 | if alpha >= 1: 109 | animation.finish() 110 | m.remove_updater(update) 111 | return 112 | animation.interpolate(alpha) 113 | animation.update_mobjects(dt) 114 | animation.total_time += dt 115 | 116 | mobject.add_updater(update) 117 | return mobject 118 | 119 | 120 | def cycle_animation(animation: Animation, **kwargs) -> Mobject: 121 | return turn_animation_into_updater( 122 | animation, cycle=True, **kwargs 123 | ) 124 | -------------------------------------------------------------------------------- /manimlib/mobject/shape_matchers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from colour import Color 4 | 5 | from manimlib.config import manim_config 6 | from manimlib.constants import BLACK, RED, YELLOW, DEFAULT_MOBJECT_COLOR 7 | from manimlib.constants import DL, DOWN, DR, LEFT, RIGHT, UL, UR 8 | from manimlib.constants import SMALL_BUFF 9 | from manimlib.mobject.geometry import Line 10 | from manimlib.mobject.geometry import Rectangle 11 | from manimlib.mobject.types.vectorized_mobject import VGroup 12 | from manimlib.mobject.types.vectorized_mobject import VMobject 13 | 14 | from typing import TYPE_CHECKING 15 | 16 | if TYPE_CHECKING: 17 | from typing import Sequence 18 | from manimlib.mobject.mobject import Mobject 19 | from manimlib.typing import ManimColor, Self 20 | 21 | 22 | class SurroundingRectangle(Rectangle): 23 | def __init__( 24 | self, 25 | mobject: Mobject, 26 | buff: float = SMALL_BUFF, 27 | color: ManimColor = YELLOW, 28 | **kwargs 29 | ): 30 | super().__init__(color=color, **kwargs) 31 | self.buff = buff 32 | self.surround(mobject) 33 | if mobject.is_fixed_in_frame(): 34 | self.fix_in_frame() 35 | 36 | def surround(self, mobject, buff=None) -> Self: 37 | self.mobject = mobject 38 | self.buff = buff if buff is not None else self.buff 39 | super().surround(mobject, self.buff) 40 | return self 41 | 42 | def set_buff(self, buff) -> Self: 43 | self.buff = buff 44 | self.surround(self.mobject) 45 | return self 46 | 47 | 48 | class BackgroundRectangle(SurroundingRectangle): 49 | def __init__( 50 | self, 51 | mobject: Mobject, 52 | color: ManimColor = None, 53 | stroke_width: float = 0, 54 | stroke_opacity: float = 0, 55 | fill_opacity: float = 0.75, 56 | buff: float = 0, 57 | **kwargs 58 | ): 59 | if color is None: 60 | color = manim_config.camera.background_color 61 | super().__init__( 62 | mobject, 63 | color=color, 64 | stroke_width=stroke_width, 65 | stroke_opacity=stroke_opacity, 66 | fill_opacity=fill_opacity, 67 | buff=buff, 68 | **kwargs 69 | ) 70 | self.original_fill_opacity = fill_opacity 71 | 72 | def pointwise_become_partial(self, mobject: Mobject, a: float, b: float) -> Self: 73 | self.set_fill(opacity=b * self.original_fill_opacity) 74 | return self 75 | 76 | def set_style( 77 | self, 78 | stroke_color: ManimColor | None = None, 79 | stroke_width: float | None = None, 80 | fill_color: ManimColor | None = None, 81 | fill_opacity: float | None = None, 82 | family: bool = True, 83 | **kwargs 84 | ) -> Self: 85 | # Unchangeable style, except for fill_opacity 86 | VMobject.set_style( 87 | self, 88 | stroke_color=BLACK, 89 | stroke_width=0, 90 | fill_color=BLACK, 91 | fill_opacity=fill_opacity 92 | ) 93 | return self 94 | 95 | def get_fill_color(self) -> Color: 96 | return Color(self.color) 97 | 98 | 99 | class Cross(VGroup): 100 | def __init__( 101 | self, 102 | mobject: Mobject, 103 | stroke_color: ManimColor = RED, 104 | stroke_width: float | Sequence[float] = [0, 6, 0], 105 | **kwargs 106 | ): 107 | super().__init__( 108 | Line(UL, DR), 109 | Line(UR, DL), 110 | ) 111 | self.insert_n_curves(20) 112 | self.replace(mobject, stretch=True) 113 | self.set_stroke(stroke_color, width=stroke_width) 114 | 115 | 116 | class Underline(Line): 117 | def __init__( 118 | self, 119 | mobject: Mobject, 120 | buff: float = SMALL_BUFF, 121 | stroke_color=DEFAULT_MOBJECT_COLOR, 122 | stroke_width: float | Sequence[float] = [0, 3, 3, 0], 123 | stretch_factor=1.2, 124 | **kwargs 125 | ): 126 | super().__init__(LEFT, RIGHT, **kwargs) 127 | if not isinstance(stroke_width, (float, int)): 128 | self.insert_n_curves(len(stroke_width) - 2) 129 | self.set_stroke(stroke_color, stroke_width) 130 | self.set_width(mobject.get_width() * stretch_factor) 131 | self.next_to(mobject, DOWN, buff=buff) 132 | -------------------------------------------------------------------------------- /manimlib/mobject/svg/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3b1b/manim/41613db7eca9eabbb40aa830631e5206c9282f0f/manimlib/mobject/svg/__init__.py -------------------------------------------------------------------------------- /manimlib/mobject/svg/brace.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import math 4 | import copy 5 | 6 | import numpy as np 7 | 8 | from manimlib.constants import DEFAULT_MOBJECT_TO_MOBJECT_BUFF, SMALL_BUFF 9 | from manimlib.constants import DOWN, LEFT, ORIGIN, RIGHT, DL, DR, UL, UP 10 | from manimlib.constants import PI 11 | from manimlib.animation.composition import AnimationGroup 12 | from manimlib.animation.fading import FadeIn 13 | from manimlib.animation.growing import GrowFromCenter 14 | from manimlib.mobject.svg.tex_mobject import Tex 15 | from manimlib.mobject.svg.tex_mobject import TexText 16 | from manimlib.mobject.svg.text_mobject import Text 17 | from manimlib.mobject.types.vectorized_mobject import VGroup 18 | from manimlib.mobject.types.vectorized_mobject import VMobject 19 | from manimlib.utils.iterables import listify 20 | from manimlib.utils.space_ops import get_norm 21 | 22 | from typing import TYPE_CHECKING 23 | 24 | if TYPE_CHECKING: 25 | from typing import Iterable 26 | 27 | from manimlib.animation.animation import Animation 28 | from manimlib.mobject.mobject import Mobject 29 | from manimlib.typing import Vect3 30 | 31 | 32 | class Brace(Tex): 33 | def __init__( 34 | self, 35 | mobject: Mobject, 36 | direction: Vect3 = DOWN, 37 | buff: float = 0.2, 38 | tex_string: str = R"\underbrace{\qquad}", 39 | **kwargs 40 | ): 41 | super().__init__(tex_string, **kwargs) 42 | 43 | angle = -math.atan2(*direction[:2]) + PI 44 | mobject.rotate(-angle, about_point=ORIGIN) 45 | left = mobject.get_corner(DL) 46 | right = mobject.get_corner(DR) 47 | target_width = right[0] - left[0] 48 | 49 | self.tip_point_index = np.argmin(self.get_all_points()[:, 1]) 50 | self.set_initial_width(target_width) 51 | self.shift(left - self.get_corner(UL) + buff * DOWN) 52 | for mob in mobject, self: 53 | mob.rotate(angle, about_point=ORIGIN) 54 | 55 | def set_initial_width(self, width: float): 56 | width_diff = width - self.get_width() 57 | if width_diff > 0: 58 | for tip, rect, vect in [(self[0], self[1], RIGHT), (self[5], self[4], LEFT)]: 59 | rect.set_width( 60 | width_diff / 2 + rect.get_width(), 61 | about_edge=vect, stretch=True 62 | ) 63 | tip.shift(-width_diff / 2 * vect) 64 | else: 65 | self.set_width(width, stretch=True) 66 | return self 67 | 68 | def put_at_tip( 69 | self, 70 | mob: Mobject, 71 | use_next_to: bool = True, 72 | **kwargs 73 | ): 74 | if use_next_to: 75 | mob.next_to( 76 | self.get_tip(), 77 | np.round(self.get_direction()), 78 | **kwargs 79 | ) 80 | else: 81 | mob.move_to(self.get_tip()) 82 | buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_MOBJECT_BUFF) 83 | shift_distance = mob.get_width() / 2.0 + buff 84 | mob.shift(self.get_direction() * shift_distance) 85 | return self 86 | 87 | def get_text(self, text: str, **kwargs) -> Text: 88 | buff = kwargs.pop("buff", SMALL_BUFF) 89 | text_mob = Text(text, **kwargs) 90 | self.put_at_tip(text_mob, buff=buff) 91 | return text_mob 92 | 93 | def get_tex(self, *tex: str, **kwargs) -> Tex: 94 | buff = kwargs.pop("buff", SMALL_BUFF) 95 | tex_mob = Tex(*tex, **kwargs) 96 | self.put_at_tip(tex_mob, buff=buff) 97 | return tex_mob 98 | 99 | def get_tip(self) -> np.ndarray: 100 | # Very specific to the LaTeX representation 101 | # of a brace, but it's the only way I can think 102 | # of to get the tip regardless of orientation. 103 | return self.get_all_points()[self.tip_point_index] 104 | 105 | def get_direction(self) -> np.ndarray: 106 | vect = self.get_tip() - self.get_center() 107 | return vect / get_norm(vect) 108 | 109 | 110 | class BraceLabel(VMobject): 111 | label_constructor: type = Tex 112 | 113 | def __init__( 114 | self, 115 | obj: VMobject | list[VMobject], 116 | text: str | Iterable[str], 117 | brace_direction: np.ndarray = DOWN, 118 | label_scale: float = 1.0, 119 | label_buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFF, 120 | **kwargs 121 | ) -> None: 122 | super().__init__(**kwargs) 123 | self.brace_direction = brace_direction 124 | self.label_scale = label_scale 125 | self.label_buff = label_buff 126 | 127 | if isinstance(obj, list): 128 | obj = VGroup(*obj) 129 | self.brace = Brace(obj, brace_direction, **kwargs) 130 | 131 | self.label = self.label_constructor(*listify(text), **kwargs) 132 | self.label.scale(self.label_scale) 133 | 134 | self.brace.put_at_tip(self.label, buff=self.label_buff) 135 | self.set_submobjects([self.brace, self.label]) 136 | 137 | def creation_anim( 138 | self, 139 | label_anim: Animation = FadeIn, 140 | brace_anim: Animation = GrowFromCenter 141 | ) -> AnimationGroup: 142 | return AnimationGroup(brace_anim(self.brace), label_anim(self.label)) 143 | 144 | def shift_brace(self, obj: VMobject | list[VMobject], **kwargs): 145 | if isinstance(obj, list): 146 | obj = VMobject(*obj) 147 | self.brace = Brace(obj, self.brace_direction, **kwargs) 148 | self.brace.put_at_tip(self.label) 149 | self.submobjects[0] = self.brace 150 | return self 151 | 152 | def change_label(self, *text: str, **kwargs): 153 | self.label = self.label_constructor(*text, **kwargs) 154 | if self.label_scale != 1: 155 | self.label.scale(self.label_scale) 156 | 157 | self.brace.put_at_tip(self.label) 158 | self.submobjects[1] = self.label 159 | return self 160 | 161 | def change_brace_label(self, obj: VMobject | list[VMobject], *text: str): 162 | self.shift_brace(obj) 163 | self.change_label(*text) 164 | return self 165 | 166 | def copy(self): 167 | copy_mobject = copy.copy(self) 168 | copy_mobject.brace = self.brace.copy() 169 | copy_mobject.label = self.label.copy() 170 | copy_mobject.set_submobjects([copy_mobject.brace, copy_mobject.label]) 171 | 172 | return copy_mobject 173 | 174 | 175 | class BraceText(BraceLabel): 176 | label_constructor: type = TexText 177 | 178 | 179 | class LineBrace(Brace): 180 | def __init__(self, line: Line, direction=UP, **kwargs): 181 | angle = line.get_angle() 182 | line.rotate(-angle) 183 | super().__init__(line, direction, **kwargs) 184 | line.rotate(angle) 185 | self.rotate(angle, about_point=line.get_center()) 186 | -------------------------------------------------------------------------------- /manimlib/mobject/svg/special_tex.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from manimlib.constants import MED_SMALL_BUFF, DEFAULT_MOBJECT_COLOR, GREY_C 4 | from manimlib.constants import DOWN, LEFT, RIGHT, UP 5 | from manimlib.constants import FRAME_WIDTH 6 | from manimlib.constants import MED_LARGE_BUFF, SMALL_BUFF 7 | from manimlib.mobject.geometry import Line 8 | from manimlib.mobject.types.vectorized_mobject import VGroup 9 | from manimlib.mobject.svg.tex_mobject import TexText 10 | 11 | 12 | from typing import TYPE_CHECKING 13 | 14 | if TYPE_CHECKING: 15 | from manimlib.typing import ManimColor, Vect3 16 | 17 | 18 | class BulletedList(VGroup): 19 | def __init__( 20 | self, 21 | *items: str, 22 | buff: float = MED_LARGE_BUFF, 23 | aligned_edge: Vect3 = LEFT, 24 | **kwargs 25 | ): 26 | labelled_content = [R"\item " + item for item in items] 27 | tex_string = "\n".join([ 28 | R"\begin{itemize}", 29 | *labelled_content, 30 | R"\end{itemize}" 31 | ]) 32 | tex_text = TexText(tex_string, isolate=labelled_content, **kwargs) 33 | lines = (tex_text.select_part(part) for part in labelled_content) 34 | 35 | super().__init__(*lines) 36 | 37 | self.arrange(DOWN, buff=buff, aligned_edge=aligned_edge) 38 | 39 | def fade_all_but(self, index: int, opacity: float = 0.25, scale_factor=0.7) -> None: 40 | max_dot_height = max([item[0].get_height() for item in self.submobjects]) 41 | for i, part in enumerate(self.submobjects): 42 | trg_dot_height = (1.0 if i == index else scale_factor) * max_dot_height 43 | part.set_fill(opacity=(1.0 if i == index else opacity)) 44 | part.scale(trg_dot_height / part[0].get_height(), about_edge=LEFT) 45 | 46 | 47 | class TexTextFromPresetString(TexText): 48 | tex: str = "" 49 | default_color: ManimColor = DEFAULT_MOBJECT_COLOR 50 | 51 | def __init__(self, **kwargs): 52 | super().__init__( 53 | self.tex, 54 | color=kwargs.pop("color", self.default_color), 55 | **kwargs 56 | ) 57 | 58 | 59 | class Title(TexText): 60 | def __init__( 61 | self, 62 | *text_parts: str, 63 | font_size: int = 72, 64 | include_underline: bool = True, 65 | underline_width: float = FRAME_WIDTH - 2, 66 | # This will override underline_width 67 | match_underline_width_to_text: bool = False, 68 | underline_buff: float = SMALL_BUFF, 69 | underline_style: dict = dict(stroke_width=2, stroke_color=GREY_C), 70 | **kwargs 71 | ): 72 | super().__init__(*text_parts, font_size=font_size, **kwargs) 73 | self.to_edge(UP, buff=MED_SMALL_BUFF) 74 | if include_underline: 75 | underline = Line(LEFT, RIGHT, **underline_style) 76 | underline.next_to(self, DOWN, buff=underline_buff) 77 | if match_underline_width_to_text: 78 | underline.match_width(self) 79 | else: 80 | underline.set_width(underline_width) 81 | self.add(underline) 82 | self.underline = underline 83 | -------------------------------------------------------------------------------- /manimlib/mobject/types/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3b1b/manim/41613db7eca9eabbb40aa830631e5206c9282f0f/manimlib/mobject/types/__init__.py -------------------------------------------------------------------------------- /manimlib/mobject/types/dot_cloud.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import moderngl 4 | import numpy as np 5 | 6 | from manimlib.constants import GREY_C, YELLOW 7 | from manimlib.constants import ORIGIN, NULL_POINTS 8 | from manimlib.mobject.mobject import Mobject 9 | from manimlib.mobject.types.point_cloud_mobject import PMobject 10 | from manimlib.utils.iterables import resize_with_interpolation 11 | 12 | from typing import TYPE_CHECKING 13 | 14 | if TYPE_CHECKING: 15 | import numpy.typing as npt 16 | from typing import Sequence, Tuple 17 | from manimlib.typing import ManimColor, Vect3, Vect3Array, Self 18 | 19 | 20 | DEFAULT_DOT_RADIUS = 0.05 21 | DEFAULT_GLOW_DOT_RADIUS = 0.2 22 | DEFAULT_GRID_HEIGHT = 6 23 | DEFAULT_BUFF_RATIO = 0.5 24 | 25 | 26 | class DotCloud(PMobject): 27 | shader_folder: str = "true_dot" 28 | render_primitive: int = moderngl.POINTS 29 | data_dtype: Sequence[Tuple[str, type, Tuple[int]]] = [ 30 | ('point', np.float32, (3,)), 31 | ('radius', np.float32, (1,)), 32 | ('rgba', np.float32, (4,)), 33 | ] 34 | 35 | def __init__( 36 | self, 37 | points: Vect3Array = NULL_POINTS, 38 | color: ManimColor = GREY_C, 39 | opacity: float = 1.0, 40 | radius: float = DEFAULT_DOT_RADIUS, 41 | glow_factor: float = 0.0, 42 | anti_alias_width: float = 2.0, 43 | **kwargs 44 | ): 45 | self.radius = radius 46 | self.glow_factor = glow_factor 47 | self.anti_alias_width = anti_alias_width 48 | 49 | super().__init__( 50 | color=color, 51 | opacity=opacity, 52 | **kwargs 53 | ) 54 | self.set_radius(self.radius) 55 | 56 | if points is not None: 57 | self.set_points(points) 58 | 59 | def init_uniforms(self) -> None: 60 | super().init_uniforms() 61 | self.uniforms["glow_factor"] = self.glow_factor 62 | self.uniforms["anti_alias_width"] = self.anti_alias_width 63 | 64 | def to_grid( 65 | self, 66 | n_rows: int, 67 | n_cols: int, 68 | n_layers: int = 1, 69 | buff_ratio: float | None = None, 70 | h_buff_ratio: float = 1.0, 71 | v_buff_ratio: float = 1.0, 72 | d_buff_ratio: float = 1.0, 73 | height: float = DEFAULT_GRID_HEIGHT, 74 | ) -> Self: 75 | n_points = n_rows * n_cols * n_layers 76 | points = np.repeat(range(n_points), 3, axis=0).reshape((n_points, 3)) 77 | points[:, 0] = points[:, 0] % n_cols 78 | points[:, 1] = (points[:, 1] // n_cols) % n_rows 79 | points[:, 2] = points[:, 2] // (n_rows * n_cols) 80 | self.set_points(points.astype(float)) 81 | 82 | if buff_ratio is not None: 83 | v_buff_ratio = buff_ratio 84 | h_buff_ratio = buff_ratio 85 | d_buff_ratio = buff_ratio 86 | 87 | radius = self.get_radius() 88 | ns = [n_cols, n_rows, n_layers] 89 | brs = [h_buff_ratio, v_buff_ratio, d_buff_ratio] 90 | self.set_radius(0) 91 | for n, br, dim in zip(ns, brs, range(3)): 92 | self.rescale_to_fit(2 * radius * (1 + br) * (n - 1), dim, stretch=True) 93 | self.set_radius(radius) 94 | if height is not None: 95 | self.set_height(height) 96 | self.center() 97 | return self 98 | 99 | @Mobject.affects_data 100 | def set_radii(self, radii: npt.ArrayLike) -> Self: 101 | n_points = self.get_num_points() 102 | radii = np.array(radii).reshape((len(radii), 1)) 103 | self.data["radius"][:] = resize_with_interpolation(radii, n_points) 104 | self.refresh_bounding_box() 105 | return self 106 | 107 | def get_radii(self) -> np.ndarray: 108 | return self.data["radius"] 109 | 110 | @Mobject.affects_data 111 | def set_radius(self, radius: float) -> Self: 112 | data = self.data if self.get_num_points() > 0 else self._data_defaults 113 | data["radius"][:] = radius 114 | self.refresh_bounding_box() 115 | return self 116 | 117 | def get_radius(self) -> float: 118 | return self.get_radii().max() 119 | 120 | def scale_radii(self, scale_factor: float) -> Self: 121 | self.set_radius(scale_factor * self.get_radii()) 122 | return self 123 | 124 | def set_glow_factor(self, glow_factor: float) -> Self: 125 | self.uniforms["glow_factor"] = glow_factor 126 | return self 127 | 128 | def get_glow_factor(self) -> float: 129 | return self.uniforms["glow_factor"] 130 | 131 | def compute_bounding_box(self) -> Vect3Array: 132 | bb = super().compute_bounding_box() 133 | radius = self.get_radius() 134 | bb[0] += np.full((3,), -radius) 135 | bb[2] += np.full((3,), radius) 136 | return bb 137 | 138 | def scale( 139 | self, 140 | scale_factor: float | npt.ArrayLike, 141 | scale_radii: bool = True, 142 | **kwargs 143 | ) -> Self: 144 | super().scale(scale_factor, **kwargs) 145 | if scale_radii: 146 | self.set_radii(scale_factor * self.get_radii()) 147 | return self 148 | 149 | def make_3d( 150 | self, 151 | reflectiveness: float = 0.5, 152 | gloss: float = 0.1, 153 | shadow: float = 0.2 154 | ) -> Self: 155 | self.set_shading(reflectiveness, gloss, shadow) 156 | self.apply_depth_test() 157 | return self 158 | 159 | 160 | class TrueDot(DotCloud): 161 | def __init__(self, center: Vect3 = ORIGIN, **kwargs): 162 | super().__init__(points=np.array([center]), **kwargs) 163 | 164 | 165 | class GlowDots(DotCloud): 166 | def __init__( 167 | self, 168 | points: Vect3Array = NULL_POINTS, 169 | color: ManimColor = YELLOW, 170 | radius: float = DEFAULT_GLOW_DOT_RADIUS, 171 | glow_factor: float = 2.0, 172 | **kwargs, 173 | ): 174 | super().__init__( 175 | points, 176 | color=color, 177 | radius=radius, 178 | glow_factor=glow_factor, 179 | **kwargs, 180 | ) 181 | 182 | 183 | class GlowDot(GlowDots): 184 | def __init__(self, center: Vect3 = ORIGIN, **kwargs): 185 | super().__init__(points=np.array([center]), **kwargs) 186 | -------------------------------------------------------------------------------- /manimlib/mobject/types/image_mobject.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | import moderngl 5 | from PIL import Image 6 | 7 | from manimlib.constants import DL, DR, UL, UR 8 | from manimlib.mobject.mobject import Mobject 9 | from manimlib.utils.bezier import inverse_interpolate 10 | from manimlib.utils.images import get_full_raster_image_path 11 | from manimlib.utils.iterables import listify 12 | from manimlib.utils.iterables import resize_with_interpolation 13 | 14 | from typing import TYPE_CHECKING 15 | 16 | if TYPE_CHECKING: 17 | from typing import Sequence, Tuple 18 | from manimlib.typing import Vect3 19 | 20 | 21 | class ImageMobject(Mobject): 22 | shader_folder: str = "image" 23 | data_dtype: Sequence[Tuple[str, type, Tuple[int]]] = [ 24 | ('point', np.float32, (3,)), 25 | ('im_coords', np.float32, (2,)), 26 | ('opacity', np.float32, (1,)), 27 | ] 28 | render_primitive: int = moderngl.TRIANGLES 29 | 30 | def __init__( 31 | self, 32 | filename: str, 33 | height: float = 4.0, 34 | **kwargs 35 | ): 36 | self.height = height 37 | self.image_path = get_full_raster_image_path(filename) 38 | self.image = Image.open(self.image_path) 39 | super().__init__(texture_paths={"Texture": self.image_path}, **kwargs) 40 | 41 | def init_data(self) -> None: 42 | super().init_data(length=6) 43 | self.data["point"][:] = [UL, DL, UR, DR, UR, DL] 44 | self.data["im_coords"][:] = [(0, 0), (0, 1), (1, 0), (1, 1), (1, 0), (0, 1)] 45 | self.data["opacity"][:] = self.opacity 46 | 47 | def init_points(self) -> None: 48 | size = self.image.size 49 | self.set_width(2 * size[0] / size[1], stretch=True) 50 | self.set_height(self.height) 51 | 52 | @Mobject.affects_data 53 | def set_opacity(self, opacity: float, recurse: bool = True): 54 | self.data["opacity"][:, 0] = resize_with_interpolation( 55 | np.array(listify(opacity)), 56 | self.get_num_points() 57 | ) 58 | return self 59 | 60 | def set_color(self, color, opacity=None, recurse=None): 61 | return self 62 | 63 | def point_to_rgb(self, point: Vect3) -> Vect3: 64 | x0, y0 = self.get_corner(UL)[:2] 65 | x1, y1 = self.get_corner(DR)[:2] 66 | x_alpha = inverse_interpolate(x0, x1, point[0]) 67 | y_alpha = inverse_interpolate(y0, y1, point[1]) 68 | if not (0 <= x_alpha <= 1) and (0 <= y_alpha <= 1): 69 | # TODO, raise smarter exception 70 | raise Exception("Cannot sample color from outside an image") 71 | 72 | pw, ph = self.image.size 73 | rgb = self.image.getpixel(( 74 | int((pw - 1) * x_alpha), 75 | int((ph - 1) * y_alpha), 76 | ))[:3] 77 | return np.array(rgb) / 255 78 | -------------------------------------------------------------------------------- /manimlib/mobject/types/point_cloud_mobject.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | from manimlib.mobject.mobject import Mobject 6 | from manimlib.utils.color import color_gradient 7 | from manimlib.utils.color import color_to_rgba 8 | from manimlib.utils.iterables import resize_with_interpolation 9 | 10 | from typing import TYPE_CHECKING 11 | 12 | if TYPE_CHECKING: 13 | from typing import Callable 14 | from manimlib.typing import ManimColor, Vect3, Vect3Array, Vect4Array, Self 15 | 16 | 17 | class PMobject(Mobject): 18 | def set_points(self, points: Vect3Array): 19 | if len(points) == 0: 20 | points = np.zeros((0, 3)) 21 | super().set_points(points) 22 | self.resize_points(len(points)) 23 | return self 24 | 25 | def add_points( 26 | self, 27 | points: Vect3Array, 28 | rgbas: Vect4Array | None = None, 29 | color: ManimColor | None = None, 30 | opacity: float | None = None 31 | ) -> Self: 32 | """ 33 | points must be a Nx3 numpy array, as must rgbas if it is not None 34 | """ 35 | self.append_points(points) 36 | # rgbas array will have been resized with points 37 | if color is not None: 38 | if opacity is None: 39 | opacity = self.data["rgba"][-1, 3] 40 | rgbas = np.repeat( 41 | [color_to_rgba(color, opacity)], 42 | len(points), 43 | axis=0 44 | ) 45 | if rgbas is not None: 46 | self.data["rgba"][-len(rgbas):] = rgbas 47 | return self 48 | 49 | def add_point(self, point: Vect3, rgba=None, color=None, opacity=None) -> Self: 50 | rgbas = None if rgba is None else [rgba] 51 | self.add_points([point], rgbas, color, opacity) 52 | return self 53 | 54 | @Mobject.affects_data 55 | def set_color_by_gradient(self, *colors: ManimColor) -> Self: 56 | self.data["rgba"][:] = np.array(list(map( 57 | color_to_rgba, 58 | color_gradient(colors, self.get_num_points()) 59 | ))) 60 | return self 61 | 62 | @Mobject.affects_data 63 | def match_colors(self, pmobject: PMobject) -> Self: 64 | self.data["rgba"][:] = resize_with_interpolation( 65 | pmobject.data["rgba"], self.get_num_points() 66 | ) 67 | return self 68 | 69 | @Mobject.affects_data 70 | def filter_out(self, condition: Callable[[np.ndarray], bool]) -> Self: 71 | for mob in self.family_members_with_points(): 72 | mob.data = mob.data[~np.apply_along_axis(condition, 1, mob.get_points())] 73 | return self 74 | 75 | @Mobject.affects_data 76 | def sort_points(self, function: Callable[[Vect3], None] = lambda p: p[0]) -> Self: 77 | """ 78 | function is any map from R^3 to R 79 | """ 80 | for mob in self.family_members_with_points(): 81 | indices = np.argsort( 82 | np.apply_along_axis(function, 1, mob.get_points()) 83 | ) 84 | mob.data[:] = mob.data[indices] 85 | return self 86 | 87 | @Mobject.affects_data 88 | def ingest_submobjects(self) -> Self: 89 | self.data = np.vstack([ 90 | sm.data for sm in self.get_family() 91 | ]) 92 | return self 93 | 94 | def point_from_proportion(self, alpha: float) -> np.ndarray: 95 | index = alpha * (self.get_num_points() - 1) 96 | return self.get_points()[int(index)] 97 | 98 | @Mobject.affects_data 99 | def pointwise_become_partial(self, pmobject: PMobject, a: float, b: float) -> Self: 100 | lower_index = int(a * pmobject.get_num_points()) 101 | upper_index = int(b * pmobject.get_num_points()) 102 | self.data = pmobject.data[lower_index:upper_index].copy() 103 | return self 104 | 105 | 106 | class PGroup(PMobject): 107 | def __init__(self, *pmobs: PMobject, **kwargs): 108 | if not all([isinstance(m, PMobject) for m in pmobs]): 109 | raise Exception("All submobjects must be of type PMobject") 110 | super().__init__(**kwargs) 111 | self.add(*pmobs) 112 | -------------------------------------------------------------------------------- /manimlib/mobject/value_tracker.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | from manimlib.mobject.mobject import Mobject 5 | from manimlib.utils.iterables import listify 6 | 7 | from typing import TYPE_CHECKING 8 | 9 | if TYPE_CHECKING: 10 | from manimlib.typing import Self 11 | 12 | 13 | class ValueTracker(Mobject): 14 | """ 15 | Not meant to be displayed. Instead the position encodes some 16 | number, often one which another animation or continual_animation 17 | uses for its update function, and by treating it as a mobject it can 18 | still be animated and manipulated just like anything else. 19 | """ 20 | value_type: type = np.float64 21 | 22 | def __init__( 23 | self, 24 | value: float | complex | np.ndarray = 0, 25 | **kwargs 26 | ): 27 | self.value = value 28 | super().__init__(**kwargs) 29 | 30 | def init_uniforms(self) -> None: 31 | super().init_uniforms() 32 | self.uniforms["value"] = np.array( 33 | listify(self.value), 34 | dtype=self.value_type, 35 | ) 36 | 37 | def get_value(self) -> float | complex | np.ndarray: 38 | result = self.uniforms["value"] 39 | if len(result) == 1: 40 | return result[0] 41 | return result 42 | 43 | def set_value(self, value: float | complex | np.ndarray) -> Self: 44 | self.uniforms["value"][:] = value 45 | return self 46 | 47 | def increment_value(self, d_value: float | complex) -> None: 48 | self.set_value(self.get_value() + d_value) 49 | 50 | 51 | class ExponentialValueTracker(ValueTracker): 52 | """ 53 | Operates just like ValueTracker, except it encodes the value as the 54 | exponential of a position coordinate, which changes how interpolation 55 | behaves 56 | """ 57 | 58 | def get_value(self) -> float | complex: 59 | return np.exp(ValueTracker.get_value(self)) 60 | 61 | def set_value(self, value: float | complex): 62 | return ValueTracker.set_value(self, np.log(value)) 63 | 64 | 65 | class ComplexValueTracker(ValueTracker): 66 | value_type: type = np.complex128 67 | -------------------------------------------------------------------------------- /manimlib/scene/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3b1b/manim/41613db7eca9eabbb40aa830631e5206c9282f0f/manimlib/scene/__init__.py -------------------------------------------------------------------------------- /manimlib/shaders/image/frag.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | uniform sampler2D Texture; 4 | 5 | in vec2 v_im_coords; 6 | in float v_opacity; 7 | 8 | out vec4 frag_color; 9 | 10 | void main() { 11 | frag_color = texture(Texture, v_im_coords); 12 | frag_color.a *= v_opacity; 13 | } -------------------------------------------------------------------------------- /manimlib/shaders/image/vert.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | uniform sampler2D Texture; 4 | 5 | in vec3 point; 6 | in vec2 im_coords; 7 | in float opacity; 8 | 9 | out vec2 v_im_coords; 10 | out float v_opacity; 11 | 12 | // Analog of import for manim only 13 | #INSERT emit_gl_Position.glsl 14 | 15 | void main(){ 16 | v_im_coords = im_coords; 17 | v_opacity = opacity; 18 | emit_gl_Position(point); 19 | } -------------------------------------------------------------------------------- /manimlib/shaders/inserts/NOTE.md: -------------------------------------------------------------------------------- 1 | There seems to be no analog to #include in C++ for OpenGL shaders. While there are other options for sharing code between shaders, a lot of them aren't great, especially if the goal is to have all the logic for which specific bits of code to share handled in the shader file itself. So the way manim currently works is to replace any line which looks like 2 | 3 | #INSERT 4 | 5 | with the code from one of the files in this folder. 6 | 7 | The functions in this file may include declarations of uniforms, so one should not re-declare those in the surrounding context. 8 | -------------------------------------------------------------------------------- /manimlib/shaders/inserts/complex_functions.glsl: -------------------------------------------------------------------------------- 1 | vec2 complex_mult(vec2 z, vec2 w){ 2 | return vec2(z.x * w.x - z.y * w.y, z.x * w.y + z.y * w.x); 3 | } 4 | 5 | vec2 complex_div(vec2 z, vec2 w){ 6 | return complex_mult(z, vec2(w.x, -w.y)) / (w.x * w.x + w.y * w.y); 7 | } 8 | 9 | vec2 complex_pow(vec2 z, int n){ 10 | vec2 result = vec2(1.0, 0.0); 11 | for(int i = 0; i < n; i++){ 12 | result = complex_mult(result, z); 13 | } 14 | return result; 15 | } -------------------------------------------------------------------------------- /manimlib/shaders/inserts/emit_gl_Position.glsl: -------------------------------------------------------------------------------- 1 | uniform float is_fixed_in_frame; 2 | uniform mat4 view; 3 | uniform float focal_distance; 4 | uniform vec3 frame_rescale_factors; 5 | uniform vec4 clip_plane; 6 | 7 | void emit_gl_Position(vec3 point){ 8 | vec4 result = vec4(point, 1.0); 9 | // This allows for smooth transitions between objects fixed and unfixed from frame 10 | result = mix(view * result, result, is_fixed_in_frame); 11 | // Essentially a projection matrix 12 | result.xyz *= frame_rescale_factors; 13 | result.w = 1.0 - result.z; 14 | // Flip and scale to prevent premature clipping 15 | result.z *= -0.1; 16 | gl_Position = result; 17 | 18 | if(clip_plane.xyz != vec3(0.0, 0.0, 0.0)){ 19 | gl_ClipDistance[0] = dot(vec4(point, 1.0), clip_plane); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /manimlib/shaders/inserts/finalize_color.glsl: -------------------------------------------------------------------------------- 1 | uniform vec3 light_position; 2 | uniform vec3 camera_position; 3 | uniform vec3 shading; 4 | 5 | vec3 float_to_color(float value, float min_val, float max_val, vec3[9] colormap_data){ 6 | float alpha = clamp((value - min_val) / (max_val - min_val), 0.0, 1.0); 7 | int disc_alpha = min(int(alpha * 8), 7); 8 | return mix( 9 | colormap_data[disc_alpha], 10 | colormap_data[disc_alpha + 1], 11 | 8.0 * alpha - disc_alpha 12 | ); 13 | } 14 | 15 | 16 | vec4 add_light(vec4 color, vec3 point, vec3 unit_normal){ 17 | if(shading == vec3(0.0)) return color; 18 | 19 | float reflectiveness = shading.x; 20 | float gloss = shading.y; 21 | float shadow = shading.z; 22 | 23 | vec4 result = color; 24 | vec3 to_camera = normalize(camera_position - point); 25 | vec3 to_light = normalize(light_position - point); 26 | 27 | float light_to_normal = dot(to_light, unit_normal); 28 | // When unit normal points towards light, brighten 29 | float bright_factor = max(light_to_normal, 0) * reflectiveness; 30 | // For glossy surface, add extra shine if light beam goes towards camera 31 | vec3 light_reflection = reflect(-to_light, unit_normal); 32 | float light_to_cam = dot(light_reflection, to_camera); 33 | float shine = gloss * exp(-3 * pow(1 - light_to_cam, 2)); 34 | bright_factor += shine; 35 | 36 | result.rgb = mix(result.rgb, vec3(1.0), bright_factor); 37 | if (light_to_normal < 0){ 38 | // Darken 39 | result.rgb = mix( 40 | result.rgb, 41 | vec3(0.0), 42 | max(-light_to_normal, 0) * shadow 43 | ); 44 | } 45 | return result; 46 | } 47 | 48 | vec4 finalize_color(vec4 color, vec3 point, vec3 unit_normal){ 49 | ///// INSERT COLOR FUNCTION HERE ///// 50 | // The line above may be replaced by arbitrary code snippets, as per 51 | // the method Mobject.set_color_by_code 52 | return add_light(color, point, unit_normal); 53 | } -------------------------------------------------------------------------------- /manimlib/shaders/inserts/get_unit_normal.glsl: -------------------------------------------------------------------------------- 1 | vec3 get_unit_normal(vec3 p0, vec3 p1, vec3 p2){ 2 | float tol = 1e-6; 3 | vec3 v1 = normalize(p1 - p0); 4 | vec3 v2 = normalize(p2 - p0); 5 | vec3 cp = cross(v1, v2); 6 | float cp_norm = length(cp); 7 | 8 | if(cp_norm > tol) return cp / cp_norm; 9 | 10 | // Otherwise, three pionts form a line, so find 11 | // a normal vector to that line in the plane shared 12 | // with the z-axis 13 | vec3 comb = v1 + v2; 14 | cp = cross(cross(comb, vec3(0.0, 0.0, 1.0)), comb); 15 | cp_norm = length(cp); 16 | if(cp_norm > tol) return cp / cp_norm; 17 | 18 | // Otherwise, the points line up with the z-axis. 19 | return vec3(0.0, -1.0, 0.0); 20 | } -------------------------------------------------------------------------------- /manimlib/shaders/inserts/get_xyz_to_uv.glsl: -------------------------------------------------------------------------------- 1 | vec2 xs_on_clean_parabola(vec3 b0, vec3 b1, vec3 b2){ 2 | /* 3 | Given three control points for a quadratic bezier, 4 | this returns the two values (x0, x2) such that the 5 | section of the parabola y = x^2 between those values 6 | is isometric to the given quadratic bezier. 7 | 8 | Adapated from https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html 9 | */ 10 | vec3 dd = 2 * b1 - b0 - b2; 11 | 12 | float u0 = dot(b1 - b0, dd); 13 | float u2 = dot(b2 - b1, dd); 14 | float cp = length(cross(b2 - b0, dd)); 15 | 16 | return vec2(u0 / cp, u2 / cp); 17 | } 18 | 19 | 20 | mat4 map_triangles(vec3 src0, vec3 src1, vec3 src2, vec3 dst0, vec3 dst1, vec3 dst2){ 21 | /* 22 | Return an affine transform which maps the triangle (src0, src1, src2) 23 | onto the triangle (dst0, dst1, dst2) 24 | */ 25 | mat4 src_mat = mat4( 26 | src0, 1.0, 27 | src1, 1.0, 28 | src2, 1.0, 29 | vec4(1.0) 30 | ); 31 | mat4 dst_mat = mat4( 32 | dst0, 1.0, 33 | dst1, 1.0, 34 | dst2, 1.0, 35 | vec4(1.0) 36 | ); 37 | return dst_mat * inverse(src_mat); 38 | } 39 | 40 | 41 | mat4 rotation(vec3 axis, float cos_angle){ 42 | float c = cos_angle; 43 | float s = sqrt(1 - c * c); // Sine of the angle 44 | float oc = 1.0 - c; 45 | float ax = axis.x; 46 | float ay = axis.y; 47 | float az = axis.z; 48 | 49 | return mat4( 50 | oc * ax * ax + c, oc * ax * ay + az * s, oc * az * ax - ay * s, 0.0, 51 | oc * ax * ay - az * s, oc * ay * ay + c, oc * ay * az + ax * s, 0.0, 52 | oc * az * ax + ay * s, oc * ay * az - ax * s, oc * az * az + c, 0.0, 53 | 0.0, 0.0, 0.0, 1.0 54 | ); 55 | } 56 | 57 | 58 | mat4 map_onto_x_axis(vec3 src0, vec3 src1){ 59 | mat4 shift = mat4(1.0); 60 | shift[3].xyz = -src0; 61 | 62 | // Find rotation matrix between unit vectors in each direction 63 | vec3 vect = normalize(src1 - src0); 64 | // No rotation needed 65 | if(vect.x > 1 - 1e-6) return shift; 66 | 67 | // Equivalent to cross(vect, vec3(1, 0, 0)) 68 | vec3 axis = normalize(vec3(0.0, vect.z, -vect.y)); 69 | mat4 rotate = rotation(axis, vect.x); 70 | return rotate * shift; 71 | } 72 | 73 | 74 | mat4 get_xyz_to_uv( 75 | vec3 b0, vec3 b1, vec3 b2, 76 | float threshold, 77 | out bool exceeds_threshold 78 | ){ 79 | /* 80 | Populates the matrix `result` with an affine transformation which maps a set of 81 | quadratic bezier controls points into a new coordinate system such that the bezier 82 | curve coincides with y = x^2. 83 | 84 | If the x-range under this part of the curve exceeds `threshold`, this returns false 85 | and populates result a matrix mapping b0 and b2 onto the x-axis 86 | */ 87 | vec2 xs = xs_on_clean_parabola(b0, b1, b2); 88 | float x0 = xs[0]; 89 | float x1 = 0.5 * (xs[0] + xs[1]); 90 | float x2 = xs[1]; 91 | // Portions of the parabola y = x^2 where abs(x) exceeds 92 | // this value are treated as straight lines. 93 | exceeds_threshold = (min(x0, x2) > threshold || max(x0, x2) < -threshold); 94 | if(exceeds_threshold){ 95 | return map_onto_x_axis(b0, b2); 96 | } 97 | // This triangle on the xy plane should be isometric 98 | // to (b0, b1, b2), and it should define a quadratic 99 | // bezier segment aligned with y = x^2 100 | vec3 dst0 = vec3(x0, x0 * x0, 0.0); 101 | vec3 dst1 = vec3(x1, x0 * x2, 0.0); 102 | vec3 dst2 = vec3(x2, x2 * x2, 0.0); 103 | return map_triangles(b0, b1, b2, dst0, dst1, dst2); 104 | } 105 | -------------------------------------------------------------------------------- /manimlib/shaders/mandelbrot_fractal/frag.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | uniform vec2 parameter; 4 | uniform float opacity; 5 | uniform float n_steps; 6 | uniform float mandelbrot; 7 | 8 | uniform vec3 color0; 9 | uniform vec3 color1; 10 | uniform vec3 color2; 11 | uniform vec3 color3; 12 | uniform vec3 color4; 13 | uniform vec3 color5; 14 | uniform vec3 color6; 15 | uniform vec3 color7; 16 | uniform vec3 color8; 17 | 18 | in vec3 xyz_coords; 19 | 20 | out vec4 frag_color; 21 | 22 | #INSERT finalize_color.glsl 23 | #INSERT complex_functions.glsl 24 | 25 | const int MAX_DEGREE = 5; 26 | 27 | void main() { 28 | vec3 color_map[9] = vec3[9]( 29 | color0, color1, color2, color3, 30 | color4, color5, color6, color7, color8 31 | ); 32 | vec3 color; 33 | 34 | vec2 z; 35 | vec2 c; 36 | 37 | if(bool(mandelbrot)){ 38 | c = xyz_coords.xy; 39 | z = vec2(0.0, 0.0); 40 | }else{ 41 | c = parameter; 42 | z = xyz_coords.xy; 43 | } 44 | 45 | float outer_bound = 2.0; 46 | bool stable = true; 47 | for(int n = 0; n < int(n_steps); n++){ 48 | z = complex_mult(z, z) + c; 49 | if(length(z) > outer_bound){ 50 | float float_n = float(n); 51 | float_n += log(outer_bound) / log(length(z)); 52 | float_n += 0.5 * length(c); 53 | color = float_to_color(sqrt(float_n), 1.5, 8.0, color_map); 54 | stable = false; 55 | break; 56 | } 57 | } 58 | if(stable){ 59 | color = vec3(0.0, 0.0, 0.0); 60 | } 61 | 62 | frag_color = finalize_color( 63 | vec4(color, opacity), 64 | xyz_coords, 65 | vec3(0.0, 0.0, 1.0) 66 | ); 67 | } -------------------------------------------------------------------------------- /manimlib/shaders/mandelbrot_fractal/vert.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | in vec3 point; 4 | out vec3 xyz_coords; 5 | 6 | uniform float scale_factor; 7 | uniform vec3 offset; 8 | 9 | #INSERT emit_gl_Position.glsl 10 | 11 | void main(){ 12 | xyz_coords = (point - offset) / scale_factor; 13 | emit_gl_Position(point); 14 | } -------------------------------------------------------------------------------- /manimlib/shaders/newton_fractal/frag.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | uniform vec4 color0; 4 | uniform vec4 color1; 5 | uniform vec4 color2; 6 | uniform vec4 color3; 7 | uniform vec4 color4; 8 | 9 | uniform vec2 coef0; 10 | uniform vec2 coef1; 11 | uniform vec2 coef2; 12 | uniform vec2 coef3; 13 | uniform vec2 coef4; 14 | uniform vec2 coef5; 15 | 16 | uniform vec2 root0; 17 | uniform vec2 root1; 18 | uniform vec2 root2; 19 | uniform vec2 root3; 20 | uniform vec2 root4; 21 | 22 | uniform float n_roots; 23 | uniform float n_steps; 24 | uniform float julia_highlight; 25 | uniform float saturation_factor; 26 | uniform float black_for_cycles; 27 | uniform float is_parameter_space; 28 | 29 | in vec3 xyz_coords; 30 | 31 | out vec4 frag_color; 32 | 33 | #INSERT finalize_color.glsl 34 | #INSERT complex_functions.glsl 35 | 36 | const int MAX_DEGREE = 5; 37 | const float CLOSE_ENOUGH = 1e-3; 38 | 39 | 40 | vec2 poly(vec2 z, vec2[MAX_DEGREE + 1] coefs){ 41 | vec2 result = vec2(0.0); 42 | for(int n = 0; n < int(n_roots) + 1; n++){ 43 | result += complex_mult(coefs[n], complex_pow(z, n)); 44 | } 45 | return result; 46 | } 47 | 48 | vec2 dpoly(vec2 z, vec2[MAX_DEGREE + 1] coefs){ 49 | vec2 result = vec2(0.0); 50 | for(int n = 1; n < int(n_roots) + 1; n++){ 51 | result += n * complex_mult(coefs[n], complex_pow(z, n - 1)); 52 | } 53 | return result; 54 | } 55 | 56 | vec2 seek_root(vec2 z, vec2[MAX_DEGREE + 1] coefs, int max_steps, out float n_iters){ 57 | float last_len; 58 | float curr_len; 59 | float threshold = CLOSE_ENOUGH; 60 | 61 | for(int i = 0; i < max_steps; i++){ 62 | last_len = curr_len; 63 | n_iters = float(i); 64 | vec2 step = complex_div(poly(z, coefs), dpoly(z, coefs)); 65 | curr_len = length(step); 66 | if(curr_len < threshold){ 67 | break; 68 | } 69 | z = z - step; 70 | } 71 | n_iters -= log(curr_len) / log(threshold); 72 | 73 | return z; 74 | } 75 | 76 | 77 | void main() { 78 | vec2[MAX_DEGREE + 1] coefs = vec2[MAX_DEGREE + 1](coef0, coef1, coef2, coef3, coef4, coef5); 79 | vec2[MAX_DEGREE] roots = vec2[MAX_DEGREE](root0, root1, root2, root3, root4); 80 | vec4[MAX_DEGREE] colors = vec4[MAX_DEGREE](color0, color1, color2, color3, color4); 81 | 82 | vec2 z = xyz_coords.xy; 83 | 84 | if(is_parameter_space > 0){ 85 | // In this case, pixel should correspond to one of the roots 86 | roots[2] = xyz_coords.xy; 87 | vec2 r0 = roots[0]; 88 | vec2 r1 = roots[1]; 89 | vec2 r2 = roots[2]; 90 | 91 | // It is assumed that the polynomial is cubid... 92 | coefs[0] = -complex_mult(complex_mult(r0, r1), r2); 93 | coefs[1] = complex_mult(r0, r1) + complex_mult(r0, r2) + complex_mult(r1, r2); 94 | coefs[2] = -(r0 + r1 + r2); 95 | coefs[3] = vec2(1.0, 0.0); 96 | 97 | // Seed value is always center of the roots 98 | z = -coefs[2] / 3.0; 99 | } 100 | 101 | float n_iters; 102 | vec2 found_root = seek_root(z, coefs, int(n_steps), n_iters); 103 | 104 | vec4 color = vec4(0.0); 105 | float min_dist = 1e10; 106 | float dist; 107 | for(int i = 0; i < int(n_roots); i++){ 108 | dist = distance(roots[i], found_root); 109 | if(dist < min_dist){ 110 | min_dist = dist; 111 | color = colors[i]; 112 | } 113 | } 114 | color *= 1.0 + (0.01 * saturation_factor) * (n_iters - 2 * saturation_factor); 115 | 116 | if(black_for_cycles > 0 && min_dist > CLOSE_ENOUGH){ 117 | color = vec4(0.0, 0.0, 0.0, 1.0); 118 | } 119 | 120 | if(julia_highlight > 0.0){ 121 | float radius = julia_highlight; 122 | vec2[4] samples = vec2[4]( 123 | z + vec2(radius, 0.0), 124 | z + vec2(-radius, 0.0), 125 | z + vec2(0.0, radius), 126 | z + vec2(0.0, -radius) 127 | ); 128 | for(int i = 0; i < 4; i++){ 129 | for(int j = 0; j < n_steps; j++){ 130 | vec2 z = samples[i]; 131 | z = z - complex_div(poly(z, coefs), dpoly(z, coefs)); 132 | samples[i] = z; 133 | } 134 | } 135 | float max_dist = 0.0; 136 | for(int i = 0; i < 4; i++){ 137 | max_dist = max(max_dist, distance(samples[i], samples[(i + 1) % 4])); 138 | } 139 | color *= 1.0 * smoothstep(0, 0.1, max_dist); 140 | } 141 | 142 | frag_color = finalize_color( 143 | color, 144 | xyz_coords, 145 | vec3(0.0, 0.0, 1.0) 146 | ); 147 | } -------------------------------------------------------------------------------- /manimlib/shaders/newton_fractal/vert.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | in vec3 point; 4 | out vec3 xyz_coords; 5 | 6 | uniform float scale_factor; 7 | uniform vec3 offset; 8 | 9 | #INSERT emit_gl_Position.glsl 10 | 11 | void main(){ 12 | xyz_coords = (point - offset) / scale_factor; 13 | emit_gl_Position(point); 14 | } -------------------------------------------------------------------------------- /manimlib/shaders/quadratic_bezier/depth/frag.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | out float frag_depth; 4 | 5 | void main() { 6 | frag_depth = gl_FragCoord.z; 7 | } 8 | -------------------------------------------------------------------------------- /manimlib/shaders/quadratic_bezier/depth/geom.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | layout (triangles) in; 4 | layout (triangle_strip, max_vertices = 6) out; 5 | 6 | in vec3 verts[3]; 7 | in vec3 v_base_point[3]; 8 | 9 | out float depth; 10 | 11 | #INSERT emit_gl_Position.glsl 12 | 13 | 14 | void emit_triangle(vec3 points[3]){ 15 | for(int i = 0; i < 3; i++){ 16 | emit_gl_Position(points[i]); 17 | EmitVertex(); 18 | } 19 | EndPrimitive(); 20 | } 21 | 22 | 23 | void main(){ 24 | // Curves are marked as ended when the handle after 25 | // the first anchor is set equal to that anchor 26 | if (verts[0] == verts[1]) return; 27 | 28 | // Emit two triangles 29 | emit_triangle(vec3[3](v_base_point[0], verts[0], verts[2])); 30 | emit_triangle(vec3[3](verts[0], verts[1], verts[2])); 31 | } 32 | 33 | -------------------------------------------------------------------------------- /manimlib/shaders/quadratic_bezier/depth/vert.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | in vec3 point; 4 | in vec3 base_normal; 5 | 6 | out vec3 verts; 7 | out vec3 v_base_point; 8 | 9 | void main(){ 10 | verts = point; 11 | v_base_point = base_normal; 12 | } -------------------------------------------------------------------------------- /manimlib/shaders/quadratic_bezier/fill/frag.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | uniform bool winding; 4 | 5 | in vec4 color; 6 | in float fill_all; 7 | in float orientation; 8 | in vec2 uv_coords; 9 | 10 | out vec4 frag_color; 11 | 12 | void main() { 13 | if (color.a == 0) discard; 14 | frag_color = color; 15 | /* 16 | We want negatively oriented triangles to be canceled with positively 17 | oriented ones. The easiest way to do this is to give them negative alpha, 18 | and change the blend function to just add them. However, this messes with 19 | usual blending, so instead the following line is meant to let this canceling 20 | work even for the normal blending equation: 21 | 22 | (1 - alpha) * dst + alpha * src 23 | 24 | We want the effect of blending with a positively oriented triangle followed 25 | by a negatively oriented one to return to whatever the original frag value 26 | was. You can work out this will work if the alpha for negative orientations 27 | is changed to -alpha / (1 - alpha). This has a singularity at alpha = 1, 28 | so we cap it at a value very close to 1. Effectively, the purpose of this 29 | cap is to make sure the original fragment color can be recovered even after 30 | blending with an (alpha = 1) color. 31 | */ 32 | float a = 0.95 * frag_color.a; 33 | if(orientation < 0) a = -a / (1 - a); 34 | frag_color.a = a; 35 | 36 | if (bool(fill_all)) return; 37 | 38 | float x = uv_coords.x; 39 | float y = uv_coords.y; 40 | float Fxy = (y - x * x); 41 | if(Fxy < 0) discard; 42 | } 43 | -------------------------------------------------------------------------------- /manimlib/shaders/quadratic_bezier/fill/geom.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | layout (triangles) in; 4 | layout (triangle_strip, max_vertices = 6) out; 5 | 6 | in vec3 verts[3]; 7 | in vec4 v_color[3]; 8 | in vec3 v_base_normal[3]; 9 | 10 | out vec4 color; 11 | out float fill_all; 12 | out float orientation; 13 | // uv space is where the curve coincides with y = x^2 14 | out vec2 uv_coords; 15 | 16 | // A quadratic bezier curve with these points coincides with y = x^2 17 | const vec2 SIMPLE_QUADRATIC[3] = vec2[3]( 18 | vec2(0.0, 0.0), 19 | vec2(0.5, 0), 20 | vec2(1.0, 1.0) 21 | ); 22 | 23 | // Analog of import for manim only 24 | #INSERT emit_gl_Position.glsl 25 | #INSERT finalize_color.glsl 26 | 27 | 28 | void emit_triangle(vec3 points[3], vec4 v_color[3], vec3 unit_normal){ 29 | orientation = sign(determinant(mat3( 30 | unit_normal, 31 | points[1] - points[0], 32 | points[2] - points[0] 33 | ))); 34 | 35 | for(int i = 0; i < 3; i++){ 36 | uv_coords = SIMPLE_QUADRATIC[i]; 37 | color = finalize_color(v_color[i], points[i], unit_normal); 38 | emit_gl_Position(points[i]); 39 | EmitVertex(); 40 | } 41 | EndPrimitive(); 42 | } 43 | 44 | 45 | void main(){ 46 | // Curves are marked as ended when the handle after 47 | // the first anchor is set equal to that anchor 48 | if (verts[0] == verts[1]) return; 49 | 50 | // Check zero fill 51 | if (vec3(v_color[0].a, v_color[1].a, v_color[2].a) == vec3(0.0, 0.0, 0.0)) return; 52 | 53 | vec3 base_point = v_base_normal[0]; 54 | vec3 unit_normal = v_base_normal[1]; 55 | // Emit main triangle 56 | fill_all = 1.0; 57 | emit_triangle( 58 | vec3[3](base_point, verts[0], verts[2]), 59 | vec4[3](v_color[1], v_color[0], v_color[2]), 60 | unit_normal 61 | ); 62 | // Edge triangle 63 | fill_all = 0.0; 64 | emit_triangle( 65 | vec3[3](verts[0], verts[1], verts[2]), 66 | vec4[3](v_color[0], v_color[1], v_color[2]), 67 | unit_normal 68 | ); 69 | } 70 | 71 | -------------------------------------------------------------------------------- /manimlib/shaders/quadratic_bezier/fill/vert.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | in vec3 point; 4 | in vec4 fill_rgba; 5 | in vec3 base_normal; 6 | 7 | out vec3 verts; // Bezier control point 8 | out vec4 v_color; 9 | out vec3 v_base_normal; 10 | 11 | void main(){ 12 | verts = point; 13 | v_color = fill_rgba; 14 | v_base_normal = base_normal; 15 | } -------------------------------------------------------------------------------- /manimlib/shaders/quadratic_bezier/stroke/frag.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | // Distance to the curve, and half the curve width, both as 4 | // a ratio of the antialias width 5 | in float dist_to_aaw; 6 | in float half_width_to_aaw; 7 | in vec4 color; 8 | 9 | out vec4 frag_color; 10 | 11 | void main() { 12 | frag_color = color; 13 | // sdf for the region around the curve we wish to color. 14 | float signed_dist_to_region = abs(dist_to_aaw) - half_width_to_aaw; 15 | frag_color.a *= smoothstep(0.5, -0.5, signed_dist_to_region); 16 | // This line is replaced in VShaderWrapper 17 | // MODIFY FRAG COLOR 18 | } -------------------------------------------------------------------------------- /manimlib/shaders/quadratic_bezier/stroke/vert.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | uniform float frame_scale; 4 | uniform float is_fixed_in_frame; 5 | uniform float scale_stroke_with_zoom; 6 | 7 | in vec3 point; 8 | in vec4 stroke_rgba; 9 | in float stroke_width; 10 | in float joint_angle; 11 | in vec3 unit_normal; 12 | 13 | // Bezier control point 14 | out vec3 verts; 15 | 16 | out vec4 v_color; 17 | out float v_stroke_width; 18 | out float v_joint_angle; 19 | out vec3 v_unit_normal; 20 | 21 | const float STROKE_WIDTH_CONVERSION = 0.01; 22 | 23 | void main(){ 24 | verts = point; 25 | v_color = stroke_rgba; 26 | v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width * mix(frame_scale, 1, scale_stroke_with_zoom); 27 | v_joint_angle = joint_angle; 28 | v_unit_normal = unit_normal; 29 | } -------------------------------------------------------------------------------- /manimlib/shaders/simple_vert.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | in vec3 point; 4 | 5 | #INSERT emit_gl_Position.glsl 6 | 7 | void main(){ 8 | emit_gl_Position(point); 9 | } -------------------------------------------------------------------------------- /manimlib/shaders/surface/frag.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | in vec4 v_color; 4 | out vec4 frag_color; 5 | 6 | void main() { 7 | frag_color = v_color; 8 | } -------------------------------------------------------------------------------- /manimlib/shaders/surface/vert.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | in vec3 point; 4 | in vec3 d_normal_point; 5 | in vec4 rgba; 6 | 7 | out vec4 v_color; 8 | 9 | #INSERT emit_gl_Position.glsl 10 | #INSERT get_unit_normal.glsl 11 | #INSERT finalize_color.glsl 12 | 13 | const float EPSILON = 1e-10; 14 | 15 | void main(){ 16 | emit_gl_Position(point); 17 | vec3 unit_normal = normalize(d_normal_point - point); 18 | v_color = finalize_color(rgba, point, unit_normal); 19 | } -------------------------------------------------------------------------------- /manimlib/shaders/textured_surface/frag.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | uniform sampler2D LightTexture; 4 | uniform sampler2D DarkTexture; 5 | uniform float num_textures; 6 | 7 | in vec3 v_point; 8 | in vec3 v_unit_normal; 9 | in vec2 v_im_coords; 10 | in float v_opacity; 11 | 12 | out vec4 frag_color; 13 | 14 | #INSERT finalize_color.glsl 15 | 16 | const float dark_shift = 0.2; 17 | 18 | void main() { 19 | vec4 color = texture(LightTexture, v_im_coords); 20 | if(num_textures == 2.0){ 21 | vec4 dark_color = texture(DarkTexture, v_im_coords); 22 | float dp = dot( 23 | normalize(light_position - v_point), 24 | v_unit_normal 25 | ); 26 | float alpha = smoothstep(-dark_shift, dark_shift, dp); 27 | color = mix(dark_color, color, alpha); 28 | } 29 | if (color.a == 0) discard; 30 | 31 | frag_color = finalize_color( 32 | color, 33 | v_point, 34 | v_unit_normal 35 | ); 36 | frag_color.a = v_opacity; 37 | } -------------------------------------------------------------------------------- /manimlib/shaders/textured_surface/vert.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | in vec3 point; 4 | in vec3 d_normal_point; 5 | in vec2 im_coords; 6 | in float opacity; 7 | 8 | out vec3 v_point; 9 | out vec3 v_unit_normal; 10 | out vec2 v_im_coords; 11 | out float v_opacity; 12 | 13 | uniform float is_sphere; 14 | uniform vec3 center; 15 | 16 | #INSERT emit_gl_Position.glsl 17 | #INSERT get_unit_normal.glsl 18 | 19 | const float EPSILON = 1e-10; 20 | 21 | void main(){ 22 | v_point = point; 23 | v_unit_normal = normalize(d_normal_point - point);; 24 | v_im_coords = im_coords; 25 | v_opacity = opacity; 26 | emit_gl_Position(point); 27 | } -------------------------------------------------------------------------------- /manimlib/shaders/true_dot/frag.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | uniform float glow_factor; 4 | uniform mat4 perspective; 5 | 6 | in vec4 color; 7 | in float scaled_aaw; 8 | in vec3 point; 9 | in vec3 to_cam; 10 | in vec3 center; 11 | in float radius; 12 | in vec2 uv_coords; 13 | 14 | out vec4 frag_color; 15 | 16 | // This includes a declaration of uniform vec3 shading 17 | #INSERT finalize_color.glsl 18 | 19 | void main() { 20 | float r = length(uv_coords.xy); 21 | if(r > 1.0) discard; 22 | 23 | frag_color = color; 24 | 25 | if(glow_factor > 0){ 26 | frag_color.a *= pow(1 - r, glow_factor); 27 | } 28 | 29 | if(shading != vec3(0.0)){ 30 | vec3 point_3d = point + radius * sqrt(1 - r * r) * to_cam; 31 | vec3 normal = normalize(point_3d - center); 32 | frag_color = finalize_color(frag_color, point_3d, normal); 33 | } 34 | 35 | frag_color.a *= smoothstep(1.0, 1.0 - scaled_aaw, r); 36 | } -------------------------------------------------------------------------------- /manimlib/shaders/true_dot/geom.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | layout (points) in; 4 | layout (triangle_strip, max_vertices = 4) out; 5 | 6 | uniform float pixel_size; 7 | uniform float anti_alias_width; 8 | uniform float frame_scale; 9 | uniform vec3 camera_position; 10 | 11 | in vec3 v_point[1]; 12 | in float v_radius[1]; 13 | in vec4 v_rgba[1]; 14 | 15 | out vec4 color; 16 | out float scaled_aaw; 17 | out vec3 point; 18 | out vec3 to_cam; 19 | out vec3 center; 20 | out float radius; 21 | out vec2 uv_coords; 22 | 23 | #INSERT emit_gl_Position.glsl 24 | 25 | void main(){ 26 | color = v_rgba[0]; 27 | radius = v_radius[0]; 28 | center = v_point[0]; 29 | scaled_aaw = (anti_alias_width * pixel_size) / v_radius[0]; 30 | 31 | to_cam = normalize(camera_position - v_point[0]); 32 | vec3 right = v_radius[0] * normalize(cross(vec3(0, 1, 1), to_cam)); 33 | vec3 up = v_radius[0] * normalize(cross(to_cam, right)); 34 | 35 | for(int i = -1; i < 2; i += 2){ 36 | for(int j = -1; j < 2; j += 2){ 37 | point = v_point[0] + i * right + j * up; 38 | uv_coords = vec2(i, j); 39 | emit_gl_Position(point); 40 | EmitVertex(); 41 | } 42 | } 43 | EndPrimitive(); 44 | } -------------------------------------------------------------------------------- /manimlib/shaders/true_dot/vert.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | in vec3 point; 4 | in float radius; 5 | in vec4 rgba; 6 | 7 | out vec3 v_point; 8 | out float v_radius; 9 | out vec4 v_rgba; 10 | 11 | 12 | void main(){ 13 | v_point = point; 14 | v_radius = radius; 15 | v_rgba = rgba; 16 | } -------------------------------------------------------------------------------- /manimlib/typing.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | if TYPE_CHECKING: 4 | from typing import Union, Tuple, Annotated, Literal, Iterable, Dict 5 | from colour import Color 6 | import numpy as np 7 | import re 8 | 9 | try: 10 | from typing_extensions import Self 11 | except ImportError: 12 | from typing import Self 13 | 14 | # Abbreviations for a common types 15 | ManimColor = Union[str, Color, None] 16 | RangeSpecifier = Tuple[float, float, float] | Tuple[float, float] 17 | 18 | 19 | Span = tuple[int, int] 20 | SingleSelector = Union[ 21 | str, 22 | re.Pattern, 23 | tuple[Union[int, None], Union[int, None]], 24 | ] 25 | Selector = Union[SingleSelector, Iterable[SingleSelector]] 26 | 27 | UniformDict = Dict[str, float | bool | np.ndarray | tuple] 28 | 29 | # These are various alternate names for np.ndarray meant to specify 30 | # certain shapes. 31 | # 32 | # In theory, these annotations could be used to check arrays sizes 33 | # at runtime, but at the moment nothing actually uses them, and 34 | # the names are here primarily to enhance readibility and allow 35 | # for some stronger type checking if numpy has stronger typing 36 | # in the future 37 | FloatArray = np.ndarray[int, np.dtype[np.float64]] 38 | Vect2 = Annotated[FloatArray, Literal[2]] 39 | Vect3 = Annotated[FloatArray, Literal[3]] 40 | Vect4 = Annotated[FloatArray, Literal[4]] 41 | VectN = Annotated[FloatArray, Literal["N"]] 42 | Matrix3x3 = Annotated[FloatArray, Literal[3, 3]] 43 | VectArray = Annotated[FloatArray, Literal["N", 1]] 44 | Vect2Array = Annotated[FloatArray, Literal["N", 2]] 45 | Vect3Array = Annotated[FloatArray, Literal["N", 3]] 46 | Vect4Array = Annotated[FloatArray, Literal["N", 4]] 47 | VectNArray = Annotated[FloatArray, Literal["N", "M"]] 48 | -------------------------------------------------------------------------------- /manimlib/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3b1b/manim/41613db7eca9eabbb40aa830631e5206c9282f0f/manimlib/utils/__init__.py -------------------------------------------------------------------------------- /manimlib/utils/cache.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from diskcache import Cache 5 | from contextlib import contextmanager 6 | from functools import wraps 7 | 8 | from manimlib.utils.directories import get_cache_dir 9 | from manimlib.utils.simple_functions import hash_string 10 | 11 | from typing import TYPE_CHECKING 12 | 13 | if TYPE_CHECKING: 14 | T = TypeVar('T') 15 | 16 | 17 | CACHE_SIZE = 1e9 # 1 Gig 18 | _cache = Cache(get_cache_dir(), size_limit=CACHE_SIZE) 19 | 20 | 21 | def cache_on_disk(func: Callable[..., T]) -> Callable[..., T]: 22 | @wraps(func) 23 | def wrapper(*args, **kwargs): 24 | key = hash_string(f"{func.__name__}{args}{kwargs}") 25 | value = _cache.get(key) 26 | if value is None: 27 | value = func(*args, **kwargs) 28 | _cache.set(key, value) 29 | return value 30 | return wrapper 31 | 32 | 33 | def clear_cache(): 34 | _cache.clear() 35 | -------------------------------------------------------------------------------- /manimlib/utils/color.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from colour import Color 4 | from colour import hex2rgb 5 | from colour import rgb2hex 6 | import numpy as np 7 | import random 8 | from matplotlib import pyplot 9 | 10 | from manimlib.constants import COLORMAP_3B1B 11 | from manimlib.constants import WHITE 12 | from manimlib.utils.bezier import interpolate 13 | from manimlib.utils.iterables import resize_with_interpolation 14 | 15 | from typing import TYPE_CHECKING 16 | 17 | if TYPE_CHECKING: 18 | from typing import Iterable, Sequence, Callable 19 | from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array, Vect4Array, NDArray 20 | 21 | 22 | def color_to_rgb(color: ManimColor) -> Vect3: 23 | if isinstance(color, str): 24 | return hex_to_rgb(color) 25 | elif isinstance(color, Color): 26 | return np.array(color.get_rgb()) 27 | else: 28 | raise Exception("Invalid color type") 29 | 30 | 31 | def color_to_rgba(color: ManimColor, alpha: float = 1.0) -> Vect4: 32 | return np.array([*color_to_rgb(color), alpha]) 33 | 34 | 35 | def rgb_to_color(rgb: Vect3 | Sequence[float]) -> Color: 36 | try: 37 | return Color(rgb=tuple(rgb)) 38 | except ValueError: 39 | return Color(WHITE) 40 | 41 | 42 | def rgba_to_color(rgba: Vect4) -> Color: 43 | return rgb_to_color(rgba[:3]) 44 | 45 | 46 | def rgb_to_hex(rgb: Vect3 | Sequence[float]) -> str: 47 | return rgb2hex(rgb, force_long=True).upper() 48 | 49 | 50 | def hex_to_rgb(hex_code: str) -> Vect3: 51 | return np.array(hex2rgb(hex_code)) 52 | 53 | 54 | def invert_color(color: ManimColor) -> Color: 55 | return rgb_to_color(1.0 - color_to_rgb(color)) 56 | 57 | 58 | def color_to_int_rgb(color: ManimColor) -> np.ndarray[int, np.dtype[np.uint8]]: 59 | return (255 * color_to_rgb(color)).astype('uint8') 60 | 61 | 62 | def color_to_int_rgba(color: ManimColor, opacity: float = 1.0) -> np.ndarray[int, np.dtype[np.uint8]]: 63 | alpha = int(255 * opacity) 64 | return np.array([*color_to_int_rgb(color), alpha], dtype=np.uint8) 65 | 66 | 67 | def color_to_hex(color: ManimColor) -> str: 68 | return Color(color).get_hex_l().upper() 69 | 70 | 71 | def hex_to_int(rgb_hex: str) -> int: 72 | return int(rgb_hex[1:], 16) 73 | 74 | 75 | def int_to_hex(rgb_int: int) -> str: 76 | return f"#{rgb_int:06x}".upper() 77 | 78 | 79 | def color_gradient( 80 | reference_colors: Iterable[ManimColor], 81 | length_of_output: int 82 | ) -> list[Color]: 83 | if length_of_output == 0: 84 | return [] 85 | rgbs = list(map(color_to_rgb, reference_colors)) 86 | alphas = np.linspace(0, (len(rgbs) - 1), length_of_output) 87 | floors = alphas.astype('int') 88 | alphas_mod1 = alphas % 1 89 | # End edge case 90 | alphas_mod1[-1] = 1 91 | floors[-1] = len(rgbs) - 2 92 | return [ 93 | rgb_to_color(np.sqrt(interpolate(rgbs[i]**2, rgbs[i + 1]**2, alpha))) 94 | for i, alpha in zip(floors, alphas_mod1) 95 | ] 96 | 97 | 98 | def interpolate_color( 99 | color1: ManimColor, 100 | color2: ManimColor, 101 | alpha: float 102 | ) -> Color: 103 | rgb = np.sqrt(interpolate(color_to_rgb(color1)**2, color_to_rgb(color2)**2, alpha)) 104 | return rgb_to_color(rgb) 105 | 106 | 107 | def interpolate_color_by_hsl( 108 | color1: ManimColor, 109 | color2: ManimColor, 110 | alpha: float 111 | ) -> Color: 112 | hsl1 = np.array(Color(color1).get_hsl()) 113 | hsl2 = np.array(Color(color2).get_hsl()) 114 | return Color(hsl=interpolate(hsl1, hsl2, alpha)) 115 | 116 | 117 | def average_color(*colors: ManimColor) -> Color: 118 | rgbs = np.array(list(map(color_to_rgb, colors))) 119 | return rgb_to_color(np.sqrt((rgbs**2).mean(0))) 120 | 121 | 122 | def random_color() -> Color: 123 | return Color(rgb=tuple(np.random.random(3))) 124 | 125 | 126 | def random_bright_color( 127 | hue_range: tuple[float, float] = (0.0, 1.0), 128 | saturation_range: tuple[float, float] = (0.5, 0.8), 129 | luminance_range: tuple[float, float] = (0.5, 1.0), 130 | ) -> Color: 131 | return Color(hsl=( 132 | interpolate(*hue_range, random.random()), 133 | interpolate(*saturation_range, random.random()), 134 | interpolate(*luminance_range, random.random()), 135 | )) 136 | 137 | 138 | def get_colormap_from_colors(colors: Iterable[ManimColor]) -> Callable[[Sequence[float]], Vect4Array]: 139 | """ 140 | Returns a funciton which takes in values between 0 and 1, and returns 141 | a corresponding list of rgba values 142 | """ 143 | rgbas = np.array([color_to_rgba(color) for color in colors]) 144 | 145 | def func(values): 146 | alphas = np.clip(values, 0, 1) 147 | scaled_alphas = alphas * (len(rgbas) - 1) 148 | indices = scaled_alphas.astype(int) 149 | next_indices = np.clip(indices + 1, 0, len(rgbas) - 1) 150 | inter_alphas = scaled_alphas % 1 151 | inter_alphas = inter_alphas.repeat(4).reshape((len(indices), 4)) 152 | result = interpolate(rgbas[indices], rgbas[next_indices], inter_alphas) 153 | return result 154 | 155 | return func 156 | 157 | 158 | def get_color_map(map_name: str) -> Callable[[Sequence[float]], Vect4Array]: 159 | if map_name == "3b1b_colormap": 160 | return get_colormap_from_colors(COLORMAP_3B1B) 161 | return pyplot.get_cmap(map_name) 162 | 163 | 164 | # Delete this? 165 | def get_colormap_list( 166 | map_name: str = "viridis", 167 | n_colors: int = 9 168 | ) -> Vect3Array: 169 | """ 170 | Options for map_name: 171 | 3b1b_colormap 172 | magma 173 | inferno 174 | plasma 175 | viridis 176 | cividis 177 | twilight 178 | twilight_shifted 179 | turbo 180 | """ 181 | from matplotlib.cm import cmaps_listed 182 | 183 | if map_name == "3b1b_colormap": 184 | rgbs = np.array([color_to_rgb(color) for color in COLORMAP_3B1B]) 185 | else: 186 | rgbs = cmaps_listed[map_name].colors # Make more general? 187 | return resize_with_interpolation(np.array(rgbs), n_colors) 188 | -------------------------------------------------------------------------------- /manimlib/utils/debug.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from manimlib.constants import BLACK 4 | from manimlib.logger import log 5 | from manimlib.mobject.numbers import Integer 6 | from manimlib.mobject.types.vectorized_mobject import VGroup 7 | 8 | from typing import TYPE_CHECKING 9 | 10 | if TYPE_CHECKING: 11 | from manimlib.mobject.mobject import Mobject 12 | 13 | 14 | def print_family(mobject: Mobject, n_tabs: int = 0) -> None: 15 | """For debugging purposes""" 16 | log.debug("\t" * n_tabs + str(mobject) + " " + str(id(mobject))) 17 | for submob in mobject.submobjects: 18 | print_family(submob, n_tabs + 1) 19 | 20 | 21 | def index_labels( 22 | mobject: Mobject, 23 | label_height: float = 0.15 24 | ) -> VGroup: 25 | labels = VGroup() 26 | for n, submob in enumerate(mobject): 27 | label = Integer(n) 28 | label.set_height(label_height) 29 | label.move_to(submob) 30 | label.set_backstroke(BLACK, 5) 31 | labels.add(label) 32 | return labels 33 | -------------------------------------------------------------------------------- /manimlib/utils/dict_ops.py: -------------------------------------------------------------------------------- 1 | import itertools as it 2 | import numpy as np 3 | 4 | 5 | def merge_dicts_recursively(*dicts): 6 | """ 7 | Creates a dict whose keyset is the union of all the 8 | input dictionaries. The value for each key is based 9 | on the first dict in the list with that key. 10 | 11 | dicts later in the list have higher priority 12 | 13 | When values are dictionaries, it is applied recursively 14 | """ 15 | result = dict() 16 | all_items = it.chain(*[d.items() for d in dicts]) 17 | for key, value in all_items: 18 | if key in result and isinstance(result[key], dict) and isinstance(value, dict): 19 | result[key] = merge_dicts_recursively(result[key], value) 20 | else: 21 | result[key] = value 22 | return result 23 | -------------------------------------------------------------------------------- /manimlib/utils/directories.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import tempfile 5 | import appdirs 6 | 7 | 8 | from manimlib.config import manim_config 9 | from manimlib.config import get_manim_dir 10 | from manimlib.utils.file_ops import guarantee_existence 11 | 12 | 13 | def get_directories() -> dict[str, str]: 14 | return manim_config.directories 15 | 16 | 17 | def get_cache_dir() -> str: 18 | return get_directories()["cache"] or appdirs.user_cache_dir("manim") 19 | 20 | 21 | def get_temp_dir() -> str: 22 | return get_directories()["temporary_storage"] or tempfile.gettempdir() 23 | 24 | 25 | def get_downloads_dir() -> str: 26 | return get_directories()["downloads"] or appdirs.user_cache_dir("manim_downloads") 27 | 28 | 29 | def get_output_dir() -> str: 30 | return guarantee_existence(get_directories()["output"]) 31 | 32 | 33 | def get_raster_image_dir() -> str: 34 | return get_directories()["raster_images"] 35 | 36 | 37 | def get_vector_image_dir() -> str: 38 | return get_directories()["vector_images"] 39 | 40 | 41 | def get_sound_dir() -> str: 42 | return get_directories()["sounds"] 43 | 44 | 45 | def get_shader_dir() -> str: 46 | return os.path.join(get_manim_dir(), "manimlib", "shaders") 47 | -------------------------------------------------------------------------------- /manimlib/utils/family_ops.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | if TYPE_CHECKING: 6 | from typing import Iterable, List, Set, Tuple 7 | 8 | from manimlib.mobject.mobject import Mobject 9 | 10 | 11 | def extract_mobject_family_members( 12 | mobject_list: Iterable[Mobject], 13 | exclude_pointless: bool = False 14 | ) -> list[Mobject]: 15 | return [ 16 | sm 17 | for mob in mobject_list 18 | for sm in mob.get_family() 19 | if (not exclude_pointless) or sm.has_points() 20 | ] 21 | 22 | 23 | def recursive_mobject_remove(mobjects: List[Mobject], to_remove: Set[Mobject]) -> Tuple[List[Mobject], bool]: 24 | """ 25 | Takes in a list of mobjects, together with a set of mobjects to remove. 26 | 27 | The first component of what's removed is a new list such that any mobject 28 | with one of the elements from `to_remove` in its family is no longer in 29 | the list, and in its place are its family members which aren't in `to_remove` 30 | 31 | The second component is a boolean value indicating whether any removals were made 32 | """ 33 | result = [] 34 | found_in_list = False 35 | for mob in mobjects: 36 | if mob in to_remove: 37 | found_in_list = True 38 | continue 39 | # Recursive call 40 | sub_list, found_in_submobjects = recursive_mobject_remove( 41 | mob.submobjects, to_remove 42 | ) 43 | if found_in_submobjects: 44 | result.extend(sub_list) 45 | found_in_list = True 46 | else: 47 | result.append(mob) 48 | return result, found_in_list -------------------------------------------------------------------------------- /manimlib/utils/file_ops.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from pathlib import Path 5 | import hashlib 6 | 7 | import numpy as np 8 | import validators 9 | import urllib.request 10 | 11 | import manimlib.utils.directories 12 | from manimlib.utils.simple_functions import hash_string 13 | 14 | from typing import TYPE_CHECKING 15 | 16 | if TYPE_CHECKING: 17 | from typing import Iterable 18 | 19 | 20 | def guarantee_existence(path: str | Path) -> Path: 21 | path = Path(path) 22 | path.mkdir(parents=True, exist_ok=True) 23 | return path.absolute() 24 | 25 | 26 | def find_file( 27 | file_name: str, 28 | directories: Iterable[str] | None = None, 29 | extensions: Iterable[str] | None = None 30 | ) -> Path: 31 | # Check if this is a file online first, and if so, download 32 | # it to a temporary directory 33 | if validators.url(file_name): 34 | suffix = Path(file_name).suffix 35 | file_hash = hash_string(file_name) 36 | folder = manimlib.utils.directories.get_downloads_dir() 37 | 38 | path = Path(folder, file_hash).with_suffix(suffix) 39 | urllib.request.urlretrieve(file_name, path) 40 | return path 41 | 42 | # Check if what was passed in is already a valid path to a file 43 | if os.path.exists(file_name): 44 | return Path(file_name) 45 | 46 | # Otherwise look in local file system 47 | directories = directories or [""] 48 | extensions = extensions or [""] 49 | possible_paths = ( 50 | Path(directory, file_name + extension) 51 | for directory in directories 52 | for extension in extensions 53 | ) 54 | for path in possible_paths: 55 | if path.exists(): 56 | return path 57 | raise IOError(f"{file_name} not Found") 58 | -------------------------------------------------------------------------------- /manimlib/utils/images.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | from PIL import Image 5 | 6 | from manimlib.utils.directories import get_raster_image_dir 7 | from manimlib.utils.directories import get_vector_image_dir 8 | from manimlib.utils.file_ops import find_file 9 | 10 | from typing import TYPE_CHECKING 11 | 12 | if TYPE_CHECKING: 13 | from typing import Iterable 14 | 15 | 16 | def get_full_raster_image_path(image_file_name: str) -> str: 17 | return find_file( 18 | image_file_name, 19 | directories=[get_raster_image_dir()], 20 | extensions=[".jpg", ".jpeg", ".png", ".gif", ""] 21 | ) 22 | 23 | 24 | def get_full_vector_image_path(image_file_name: str) -> str: 25 | return find_file( 26 | image_file_name, 27 | directories=[get_vector_image_dir()], 28 | extensions=[".svg", ".xdv", ""], 29 | ) 30 | 31 | 32 | def invert_image(image: Iterable) -> Image.Image: 33 | arr = np.array(image) 34 | arr = (255 * np.ones(arr.shape)).astype(arr.dtype) - arr 35 | return Image.fromarray(arr) 36 | -------------------------------------------------------------------------------- /manimlib/utils/iterables.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from colour import Color 4 | 5 | import numpy as np 6 | import random 7 | 8 | from typing import TYPE_CHECKING 9 | 10 | if TYPE_CHECKING: 11 | from typing import Callable, Iterable, Sequence, TypeVar 12 | 13 | T = TypeVar("T") 14 | S = TypeVar("S") 15 | 16 | 17 | def remove_list_redundancies(lst: Sequence[T]) -> list[T]: 18 | """ 19 | Remove duplicate elements while preserving order. 20 | Keeps the last occurrence of each element 21 | """ 22 | return list(reversed(dict.fromkeys(reversed(lst)))) 23 | 24 | 25 | def list_update(l1: Iterable[T], l2: Iterable[T]) -> list[T]: 26 | """ 27 | Used instead of list(set(l1).update(l2)) to maintain order, 28 | making sure duplicates are removed from l1, not l2. 29 | """ 30 | return remove_list_redundancies([*l1, *l2]) 31 | 32 | 33 | def list_difference_update(l1: Iterable[T], l2: Iterable[T]) -> list[T]: 34 | return [e for e in l1 if e not in l2] 35 | 36 | 37 | def adjacent_n_tuples(objects: Sequence[T], n: int) -> zip[tuple[T, ...]]: 38 | return zip(*[ 39 | [*objects[k:], *objects[:k]] 40 | for k in range(n) 41 | ]) 42 | 43 | 44 | def adjacent_pairs(objects: Sequence[T]) -> zip[tuple[T, T]]: 45 | return adjacent_n_tuples(objects, 2) 46 | 47 | 48 | def batch_by_property( 49 | items: Iterable[T], 50 | property_func: Callable[[T], S] 51 | ) -> list[tuple[T, S]]: 52 | """ 53 | Takes in a list, and returns a list of tuples, (batch, prop) 54 | such that all items in a batch have the same output when 55 | put into property_func, and such that chaining all these 56 | batches together would give the original list (i.e. order is 57 | preserved) 58 | """ 59 | batch_prop_pairs = [] 60 | curr_batch = [] 61 | curr_prop = None 62 | for item in items: 63 | prop = property_func(item) 64 | if prop != curr_prop: 65 | # Add current batch 66 | if len(curr_batch) > 0: 67 | batch_prop_pairs.append((curr_batch, curr_prop)) 68 | # Redefine curr 69 | curr_prop = prop 70 | curr_batch = [item] 71 | else: 72 | curr_batch.append(item) 73 | if len(curr_batch) > 0: 74 | batch_prop_pairs.append((curr_batch, curr_prop)) 75 | return batch_prop_pairs 76 | 77 | 78 | def listify(obj: object) -> list: 79 | if isinstance(obj, str): 80 | return [obj] 81 | try: 82 | return list(obj) 83 | except TypeError: 84 | return [obj] 85 | 86 | 87 | def shuffled(iterable: Iterable) -> list: 88 | as_list = list(iterable) 89 | random.shuffle(as_list) 90 | return as_list 91 | 92 | 93 | def resize_array(nparray: np.ndarray, length: int) -> np.ndarray: 94 | if len(nparray) == length: 95 | return nparray 96 | return np.resize(nparray, (length, *nparray.shape[1:])) 97 | 98 | 99 | def resize_preserving_order(nparray: np.ndarray, length: int) -> np.ndarray: 100 | if len(nparray) == 0: 101 | return np.resize(nparray, length) 102 | if len(nparray) == length: 103 | return nparray 104 | indices = np.arange(length) * len(nparray) // length 105 | return nparray[indices] 106 | 107 | 108 | def resize_with_interpolation(nparray: np.ndarray, length: int) -> np.ndarray: 109 | if len(nparray) == length: 110 | return nparray 111 | if len(nparray) == 1 or array_is_constant(nparray): 112 | return nparray[:1].repeat(length, axis=0) 113 | if length == 0: 114 | return np.zeros((0, *nparray.shape[1:])) 115 | cont_indices = np.linspace(0, len(nparray) - 1, length) 116 | return np.array([ 117 | (1 - a) * nparray[lh] + a * nparray[rh] 118 | for ci in cont_indices 119 | for lh, rh, a in [(int(ci), int(np.ceil(ci)), ci % 1)] 120 | ]) 121 | 122 | 123 | def make_even( 124 | iterable_1: Sequence[T], 125 | iterable_2: Sequence[S] 126 | ) -> tuple[Sequence[T], Sequence[S]]: 127 | len1 = len(iterable_1) 128 | len2 = len(iterable_2) 129 | if len1 == len2: 130 | return iterable_1, iterable_2 131 | new_len = max(len1, len2) 132 | return ( 133 | [iterable_1[(n * len1) // new_len] for n in range(new_len)], 134 | [iterable_2[(n * len2) // new_len] for n in range(new_len)] 135 | ) 136 | 137 | 138 | def arrays_match(arr1: np.ndarray, arr2: np.ndarray) -> bool: 139 | return arr1.shape == arr2.shape and (arr1 == arr2).all() 140 | 141 | 142 | def array_is_constant(arr: np.ndarray) -> bool: 143 | return len(arr) > 0 and (arr == arr[0]).all() 144 | 145 | 146 | def cartesian_product(*arrays: np.ndarray): 147 | """ 148 | Copied from https://stackoverflow.com/a/11146645 149 | """ 150 | la = len(arrays) 151 | dtype = np.result_type(*arrays) 152 | arr = np.empty([len(a) for a in arrays] + [la], dtype=dtype) 153 | for i, a in enumerate(np.ix_(*arrays)): 154 | arr[..., i] = a 155 | return arr.reshape(-1, la) 156 | 157 | 158 | def hash_obj(obj: object) -> int: 159 | if isinstance(obj, dict): 160 | return hash(tuple(sorted([ 161 | (hash_obj(k), hash_obj(v)) for k, v in obj.items() 162 | ]))) 163 | 164 | if isinstance(obj, set): 165 | return hash(tuple(sorted(hash_obj(e) for e in obj))) 166 | 167 | if isinstance(obj, (tuple, list)): 168 | return hash(tuple(hash_obj(e) for e in obj)) 169 | 170 | if isinstance(obj, Color): 171 | return hash(obj.get_rgb()) 172 | 173 | return hash(obj) 174 | -------------------------------------------------------------------------------- /manimlib/utils/paths.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import math 4 | 5 | import numpy as np 6 | 7 | from manimlib.constants import OUT 8 | from manimlib.utils.bezier import interpolate 9 | from manimlib.utils.space_ops import get_norm 10 | from manimlib.utils.space_ops import rotation_matrix_transpose 11 | 12 | from typing import TYPE_CHECKING 13 | 14 | if TYPE_CHECKING: 15 | from typing import Callable 16 | from manimlib.typing import Vect3, Vect3Array 17 | 18 | 19 | STRAIGHT_PATH_THRESHOLD = 0.01 20 | 21 | 22 | def straight_path( 23 | start_points: np.ndarray, 24 | end_points: np.ndarray, 25 | alpha: float 26 | ) -> np.ndarray: 27 | """ 28 | Same function as interpolate, but renamed to reflect 29 | intent of being used to determine how a set of points move 30 | to another set. For instance, it should be a specific case 31 | of path_along_arc 32 | """ 33 | return interpolate(start_points, end_points, alpha) 34 | 35 | 36 | def path_along_arc( 37 | arc_angle: float, 38 | axis: Vect3 = OUT 39 | ) -> Callable[[Vect3Array, Vect3Array, float], Vect3Array]: 40 | """ 41 | If vect is vector from start to end, [vect[:,1], -vect[:,0]] is 42 | perpendicular to vect in the left direction. 43 | """ 44 | if abs(arc_angle) < STRAIGHT_PATH_THRESHOLD: 45 | return straight_path 46 | if get_norm(axis) == 0: 47 | axis = OUT 48 | unit_axis = axis / get_norm(axis) 49 | 50 | def path(start_points, end_points, alpha): 51 | vects = end_points - start_points 52 | centers = start_points + 0.5 * vects 53 | if arc_angle != np.pi: 54 | centers += np.cross(unit_axis, vects / 2.0) / math.tan(arc_angle / 2) 55 | rot_matrix_T = rotation_matrix_transpose(alpha * arc_angle, unit_axis) 56 | return centers + np.dot(start_points - centers, rot_matrix_T) 57 | 58 | return path 59 | 60 | 61 | def clockwise_path() -> Callable[[Vect3Array, Vect3Array, float], Vect3Array]: 62 | return path_along_arc(-np.pi) 63 | 64 | 65 | def counterclockwise_path() -> Callable[[Vect3Array, Vect3Array, float], Vect3Array]: 66 | return path_along_arc(np.pi) 67 | -------------------------------------------------------------------------------- /manimlib/utils/rate_functions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | from manimlib.utils.bezier import bezier 6 | 7 | from typing import TYPE_CHECKING 8 | 9 | if TYPE_CHECKING: 10 | from typing import Callable 11 | 12 | 13 | def linear(t: float) -> float: 14 | return t 15 | 16 | 17 | def smooth(t: float) -> float: 18 | # Zero first and second derivatives at t=0 and t=1. 19 | # Equivalent to bezier([0, 0, 0, 1, 1, 1]) 20 | s = 1 - t 21 | return (t**3) * (10 * s * s + 5 * s * t + t * t) 22 | 23 | 24 | def rush_into(t: float) -> float: 25 | return 2 * smooth(0.5 * t) 26 | 27 | 28 | def rush_from(t: float) -> float: 29 | return 2 * smooth(0.5 * (t + 1)) - 1 30 | 31 | 32 | def slow_into(t: float) -> float: 33 | return np.sqrt(1 - (1 - t) * (1 - t)) 34 | 35 | 36 | def double_smooth(t: float) -> float: 37 | if t < 0.5: 38 | return 0.5 * smooth(2 * t) 39 | else: 40 | return 0.5 * (1 + smooth(2 * t - 1)) 41 | 42 | 43 | def there_and_back(t: float) -> float: 44 | new_t = 2 * t if t < 0.5 else 2 * (1 - t) 45 | return smooth(new_t) 46 | 47 | 48 | def there_and_back_with_pause(t: float, pause_ratio: float = 1. / 3) -> float: 49 | a = 2. / (1. - pause_ratio) 50 | if t < 0.5 - pause_ratio / 2: 51 | return smooth(a * t) 52 | elif t < 0.5 + pause_ratio / 2: 53 | return 1 54 | else: 55 | return smooth(a - a * t) 56 | 57 | 58 | def running_start(t: float, pull_factor: float = -0.5) -> float: 59 | return bezier([0, 0, pull_factor, pull_factor, 1, 1, 1])(t) 60 | 61 | 62 | def overshoot(t: float, pull_factor: float = 1.5) -> float: 63 | return bezier([0, 0, pull_factor, pull_factor, 1, 1])(t) 64 | 65 | 66 | def not_quite_there( 67 | func: Callable[[float], float] = smooth, 68 | proportion: float = 0.7 69 | ) -> Callable[[float], float]: 70 | def result(t): 71 | return proportion * func(t) 72 | return result 73 | 74 | 75 | def wiggle(t: float, wiggles: float = 2) -> float: 76 | return there_and_back(t) * np.sin(wiggles * np.pi * t) 77 | 78 | 79 | def squish_rate_func( 80 | func: Callable[[float], float], 81 | a: float = 0.4, 82 | b: float = 0.6 83 | ) -> Callable[[float], float]: 84 | def result(t): 85 | if a == b: 86 | return a 87 | elif t < a: 88 | return func(0) 89 | elif t > b: 90 | return func(1) 91 | else: 92 | return func((t - a) / (b - a)) 93 | 94 | return result 95 | 96 | # Stylistically, should this take parameters (with default values)? 97 | # Ultimately, the functionality is entirely subsumed by squish_rate_func, 98 | # but it may be useful to have a nice name for with nice default params for 99 | # "lingering", different from squish_rate_func's default params 100 | 101 | 102 | def lingering(t: float) -> float: 103 | return squish_rate_func(lambda t: t, 0, 0.8)(t) 104 | 105 | 106 | def exponential_decay(t: float, half_life: float = 0.1) -> float: 107 | # The half-life should be rather small to minimize 108 | # the cut-off error at the end 109 | return 1 - np.exp(-t / half_life) 110 | -------------------------------------------------------------------------------- /manimlib/utils/shaders.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import re 5 | from functools import lru_cache 6 | import moderngl 7 | from PIL import Image 8 | import numpy as np 9 | 10 | from manimlib.utils.directories import get_shader_dir 11 | from manimlib.utils.file_ops import find_file 12 | 13 | from typing import TYPE_CHECKING 14 | 15 | if TYPE_CHECKING: 16 | from typing import Sequence, Optional 17 | 18 | 19 | # Global maps to reflect uniform status 20 | PROGRAM_UNIFORM_MIRRORS: dict[int, dict[str, float | tuple]] = dict() 21 | 22 | 23 | @lru_cache() 24 | def image_path_to_texture(path: str, ctx: moderngl.Context) -> moderngl.Texture: 25 | im = Image.open(path).convert("RGBA") 26 | return ctx.texture( 27 | size=im.size, 28 | components=len(im.getbands()), 29 | data=im.tobytes(), 30 | ) 31 | 32 | 33 | @lru_cache() 34 | def get_shader_program( 35 | ctx: moderngl.context.Context, 36 | vertex_shader: str, 37 | fragment_shader: Optional[str] = None, 38 | geometry_shader: Optional[str] = None, 39 | ) -> moderngl.Program: 40 | return ctx.program( 41 | vertex_shader=vertex_shader, 42 | fragment_shader=fragment_shader, 43 | geometry_shader=geometry_shader, 44 | ) 45 | 46 | 47 | def set_program_uniform( 48 | program: moderngl.Program, 49 | name: str, 50 | value: float | tuple | np.ndarray 51 | ) -> bool: 52 | """ 53 | Sets a program uniform, and also keeps track of a dictionary 54 | of previously set uniforms for that program so that it 55 | doesn't needlessly reset it, requiring an exchange with gpu 56 | memory, if it sees the same value again. 57 | 58 | Returns True if changed the program, False if it left it as is. 59 | """ 60 | 61 | pid = id(program) 62 | if pid not in PROGRAM_UNIFORM_MIRRORS: 63 | PROGRAM_UNIFORM_MIRRORS[pid] = dict() 64 | uniform_mirror = PROGRAM_UNIFORM_MIRRORS[pid] 65 | 66 | if type(value) is np.ndarray and value.ndim > 0: 67 | value = tuple(value.flatten()) 68 | if uniform_mirror.get(name, None) == value: 69 | return False 70 | 71 | try: 72 | program[name].value = value 73 | except KeyError: 74 | return False 75 | uniform_mirror[name] = value 76 | return True 77 | 78 | 79 | @lru_cache() 80 | def get_shader_code_from_file(filename: str) -> str | None: 81 | if not filename: 82 | return None 83 | 84 | try: 85 | filepath = find_file( 86 | filename, 87 | directories=[get_shader_dir(), "/"], 88 | extensions=[], 89 | ) 90 | except IOError: 91 | return None 92 | 93 | with open(filepath, "r") as f: 94 | result = f.read() 95 | 96 | # To share functionality between shaders, some functions are read in 97 | # from other files an inserted into the relevant strings before 98 | # passing to ctx.program for compiling 99 | # Replace "#INSERT " lines with relevant code 100 | insertions = re.findall(r"^#INSERT .*\.glsl$", result, flags=re.MULTILINE) 101 | for line in insertions: 102 | inserted_code = get_shader_code_from_file( 103 | os.path.join("inserts", line.replace("#INSERT ", "")) 104 | ) 105 | result = result.replace(line, inserted_code) 106 | return result 107 | 108 | 109 | def get_colormap_code(rgb_list: Sequence[float]) -> str: 110 | data = ",".join( 111 | "vec3({}, {}, {})".format(*rgb) 112 | for rgb in rgb_list 113 | ) 114 | return f"vec3[{len(rgb_list)}]({data})" 115 | -------------------------------------------------------------------------------- /manimlib/utils/simple_functions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from functools import lru_cache 4 | import hashlib 5 | import inspect 6 | import math 7 | 8 | import numpy as np 9 | 10 | from typing import TYPE_CHECKING 11 | if TYPE_CHECKING: 12 | from typing import Callable, TypeVar, Iterable 13 | from manimlib.typing import FloatArray 14 | 15 | Scalable = TypeVar("Scalable", float, FloatArray) 16 | 17 | 18 | 19 | def sigmoid(x: float | FloatArray): 20 | return 1.0 / (1 + np.exp(-x)) 21 | 22 | 23 | @lru_cache(maxsize=10) 24 | def choose(n: int, k: int) -> int: 25 | return math.comb(n, k) 26 | 27 | 28 | def gen_choose(n: int, r: int) -> int: 29 | return int(np.prod(range(n, n - r, -1)) / math.factorial(r)) 30 | 31 | 32 | def get_num_args(function: Callable) -> int: 33 | return function.__code__.co_argcount 34 | 35 | 36 | def get_parameters(function: Callable) -> Iterable[str]: 37 | return inspect.signature(function).parameters.keys() 38 | 39 | 40 | def clip(a: float, min_a: float, max_a: float) -> float: 41 | if a < min_a: 42 | return min_a 43 | elif a > max_a: 44 | return max_a 45 | return a 46 | 47 | 48 | def arr_clip(arr: np.ndarray, min_a: float, max_a: float) -> np.ndarray: 49 | arr[arr < min_a] = min_a 50 | arr[arr > max_a] = max_a 51 | return arr 52 | 53 | 54 | def fdiv(a: Scalable, b: Scalable, zero_over_zero_value: Scalable | None = None) -> Scalable: 55 | """ 56 | Less heavyweight name for np.true_divide, enabling 57 | default behavior for 0/0 58 | """ 59 | if zero_over_zero_value is not None: 60 | out = np.full_like(a, zero_over_zero_value) 61 | where = np.logical_or(a != 0, b != 0) 62 | else: 63 | out = None 64 | where = True 65 | 66 | return np.true_divide(a, b, out=out, where=where) 67 | 68 | 69 | def binary_search( 70 | function: Callable[[float], float], 71 | target: float, 72 | lower_bound: float, 73 | upper_bound: float, 74 | tolerance:float = 1e-4 75 | ) -> float | None: 76 | lh = lower_bound 77 | rh = upper_bound 78 | mh = (lh + rh) / 2 79 | while abs(rh - lh) > tolerance: 80 | lx, mx, rx = [function(h) for h in (lh, mh, rh)] 81 | if lx == target: 82 | return lx 83 | if rx == target: 84 | return rx 85 | 86 | if lx <= target and rx >= target: 87 | if mx > target: 88 | rh = mh 89 | else: 90 | lh = mh 91 | elif lx > target and rx < target: 92 | lh, rh = rh, lh 93 | else: 94 | return None 95 | mh = (lh + rh) / 2 96 | return mh 97 | 98 | 99 | def hash_string(string: str, n_bytes=16) -> str: 100 | hasher = hashlib.sha256(string.encode()) 101 | return hasher.hexdigest()[:n_bytes] 102 | -------------------------------------------------------------------------------- /manimlib/utils/sounds.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import subprocess 4 | import threading 5 | import platform 6 | 7 | from manimlib.utils.directories import get_sound_dir 8 | from manimlib.utils.file_ops import find_file 9 | 10 | 11 | def get_full_sound_file_path(sound_file_name: str) -> str: 12 | return find_file( 13 | sound_file_name, 14 | directories=[get_sound_dir()], 15 | extensions=[".wav", ".mp3", ""] 16 | ) 17 | 18 | 19 | def play_sound(sound_file): 20 | """Play a sound file using the system's audio player""" 21 | full_path = get_full_sound_file_path(sound_file) 22 | system = platform.system() 23 | 24 | if system == "Windows": 25 | # Windows 26 | subprocess.Popen( 27 | ["powershell", "-c", f"(New-Object Media.SoundPlayer '{full_path}').PlaySync()"], 28 | shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL 29 | ) 30 | elif system == "Darwin": 31 | # macOS 32 | subprocess.Popen( 33 | ["afplay", full_path], 34 | stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL 35 | ) 36 | else: 37 | subprocess.Popen( 38 | ["aplay", full_path], 39 | stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL 40 | ) 41 | -------------------------------------------------------------------------------- /manimlib/utils/tex.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | from functools import lru_cache 5 | 6 | from manimlib.utils.tex_to_symbol_count import TEX_TO_SYMBOL_COUNT 7 | 8 | 9 | @lru_cache 10 | def num_tex_symbols(tex: str) -> int: 11 | tex = remove_tex_environments(tex) 12 | commands_pattern = r""" 13 | (?P\\sqrt\[[0-9]+\])| # Special sqrt with number 14 | (?P\\[{}])| # Escaped braces 15 | (?P\\[a-zA-Z!,-/:;<>]+) # Regular commands 16 | """ 17 | total = 0 18 | pos = 0 19 | for match in re.finditer(commands_pattern, tex, re.VERBOSE): 20 | # Count normal characters up to this command 21 | total += sum(1 for c in tex[pos:match.start()] if c not in "^{} \n\t_$\\&") 22 | 23 | if match.group("sqrt"): 24 | total += len(match.group()) - 5 25 | elif match.group("escaped_brace"): 26 | total += 1 # Count escaped brace as one symbol 27 | else: 28 | total += TEX_TO_SYMBOL_COUNT.get(match.group(), 1) 29 | pos = match.end() 30 | 31 | # Count remaining characters 32 | total += sum(1 for c in tex[pos:] if c not in "^{} \n\t_$\\&") 33 | return total 34 | 35 | 36 | def remove_tex_environments(tex: str) -> str: 37 | # Handle \phantom{...} with any content 38 | tex = re.sub(r"\\phantom\{[^}]*\}", "", tex) 39 | # Handle other environment commands 40 | tex = re.sub(r"\\(begin|end)(\{\w+\})?(\{\w+\})?(\[\w+\])?", "", tex) 41 | return tex 42 | -------------------------------------------------------------------------------- /manimlib/utils/tex_file_writing.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import re 5 | import yaml 6 | import subprocess 7 | from functools import lru_cache 8 | 9 | from pathlib import Path 10 | import tempfile 11 | 12 | from manimlib.utils.cache import cache_on_disk 13 | from manimlib.config import manim_config 14 | from manimlib.config import get_manim_dir 15 | from manimlib.logger import log 16 | from manimlib.utils.simple_functions import hash_string 17 | 18 | 19 | def get_tex_template_config(template_name: str) -> dict[str, str]: 20 | name = template_name.replace(" ", "_").lower() 21 | template_path = os.path.join(get_manim_dir(), "manimlib", "tex_templates.yml") 22 | with open(template_path, encoding="utf-8") as tex_templates_file: 23 | templates_dict = yaml.safe_load(tex_templates_file) 24 | if name not in templates_dict: 25 | log.warning(f"Cannot recognize template {name}, falling back to 'default'.") 26 | name = "default" 27 | return templates_dict[name] 28 | 29 | 30 | @lru_cache 31 | def get_tex_config(template: str = "") -> tuple[str, str]: 32 | """ 33 | Returns a compiler and preamble to use for rendering LaTeX 34 | """ 35 | template = template or manim_config.tex.template 36 | config = get_tex_template_config(template) 37 | return config["compiler"], config["preamble"] 38 | 39 | 40 | def get_full_tex(content: str, preamble: str = ""): 41 | return "\n\n".join(( 42 | "\\documentclass[preview]{standalone}", 43 | preamble, 44 | "\\begin{document}", 45 | content, 46 | "\\end{document}" 47 | )) + "\n" 48 | 49 | 50 | @lru_cache(maxsize=128) 51 | def latex_to_svg( 52 | latex: str, 53 | template: str = "", 54 | additional_preamble: str = "", 55 | short_tex: str = "", 56 | show_message_during_execution: bool = True, 57 | ) -> str: 58 | """Convert LaTeX string to SVG string. 59 | 60 | Args: 61 | latex: LaTeX source code 62 | template: Path to a template LaTeX file 63 | additional_preamble: String including any added "\\usepackage{...}" style imports 64 | 65 | Returns: 66 | str: SVG source code 67 | 68 | Raises: 69 | LatexError: If LaTeX compilation fails 70 | NotImplementedError: If compiler is not supported 71 | """ 72 | if show_message_during_execution: 73 | message = f"Writing {(short_tex or latex)[:70]}..." 74 | else: 75 | message = "" 76 | 77 | compiler, preamble = get_tex_config(template) 78 | 79 | preamble = "\n".join([preamble, additional_preamble]) 80 | full_tex = get_full_tex(latex, preamble) 81 | return full_tex_to_svg(full_tex, compiler, message) 82 | 83 | 84 | @cache_on_disk 85 | def full_tex_to_svg(full_tex: str, compiler: str = "latex", message: str = ""): 86 | if message: 87 | print(message, end="\r") 88 | 89 | if compiler == "latex": 90 | dvi_ext = ".dvi" 91 | elif compiler == "xelatex": 92 | dvi_ext = ".xdv" 93 | else: 94 | raise NotImplementedError(f"Compiler '{compiler}' is not implemented") 95 | 96 | # Write intermediate files to a temporary directory 97 | with tempfile.TemporaryDirectory() as temp_dir: 98 | tex_path = Path(temp_dir, "working").with_suffix(".tex") 99 | dvi_path = tex_path.with_suffix(dvi_ext) 100 | 101 | # Write tex file 102 | tex_path.write_text(full_tex) 103 | 104 | # Run latex compiler 105 | process = subprocess.run( 106 | [ 107 | compiler, 108 | *(['-no-pdf'] if compiler == "xelatex" else []), 109 | "-interaction=batchmode", 110 | "-halt-on-error", 111 | f"-output-directory={temp_dir}", 112 | tex_path 113 | ], 114 | capture_output=True, 115 | text=True 116 | ) 117 | 118 | if process.returncode != 0: 119 | # Handle error 120 | error_str = "" 121 | log_path = tex_path.with_suffix(".log") 122 | if log_path.exists(): 123 | content = log_path.read_text() 124 | error_match = re.search(r"(?<=\n! ).*\n.*\n", content) 125 | if error_match: 126 | error_str = error_match.group() 127 | raise LatexError(error_str or "LaTeX compilation failed") 128 | 129 | # Run dvisvgm and capture output directly 130 | process = subprocess.run( 131 | [ 132 | "dvisvgm", 133 | dvi_path, 134 | "-n", # no fonts 135 | "-v", "0", # quiet 136 | "--stdout", # output to stdout instead of file 137 | ], 138 | capture_output=True 139 | ) 140 | 141 | # Return SVG string 142 | result = process.stdout.decode('utf-8') 143 | 144 | if message: 145 | print(" " * len(message), end="\r") 146 | 147 | return result 148 | 149 | 150 | class LatexError(Exception): 151 | pass 152 | -------------------------------------------------------------------------------- /manimlib/utils/tex_to_symbol_count.py: -------------------------------------------------------------------------------- 1 | TEX_TO_SYMBOL_COUNT = { 2 | R"\!": 0, 3 | R"\,": 0, 4 | R"\-": 0, 5 | R"\/": 0, 6 | R"\:": 0, 7 | R"\;": 0, 8 | R"\>": 0, 9 | R"\aa": 0, 10 | R"\AA": 0, 11 | R"\ae": 0, 12 | R"\AE": 0, 13 | R"\arccos": 6, 14 | R"\arcsin": 6, 15 | R"\arctan": 6, 16 | R"\arg": 3, 17 | R"\author": 0, 18 | R"\bf": 0, 19 | R"\bibliography": 0, 20 | R"\bibliographystyle": 0, 21 | R"\big": 0, 22 | R"\Big": 0, 23 | R"\bigodot": 4, 24 | R"\bigoplus": 5, 25 | R"\bigskip": 0, 26 | R"\bmod": 3, 27 | R"\boldmath": 0, 28 | R"\bottomfraction": 2, 29 | R"\bowtie": 2, 30 | R"\cal": 0, 31 | R"\cdots": 3, 32 | R"\centering": 0, 33 | R"\cite": 2, 34 | R"\cong": 2, 35 | R"\contentsline": 0, 36 | R"\cos": 3, 37 | R"\cosh": 4, 38 | R"\cot": 3, 39 | R"\coth": 4, 40 | R"\csc": 3, 41 | R"\date": 0, 42 | R"\dblfloatpagefraction": 2, 43 | R"\dbltopfraction": 2, 44 | R"\ddots": 3, 45 | R"\deg": 3, 46 | R"\det": 3, 47 | R"\dim": 3, 48 | R"\displaystyle": 0, 49 | R"\div": 2, 50 | R"\doteq": 2, 51 | R"\dotfill": 0, 52 | R"\dots": 3, 53 | R"\emph": 0, 54 | R"\exp": 3, 55 | R"\fbox": 4, 56 | R"\floatpagefraction": 2, 57 | R"\flushbottom": 0, 58 | R"\footnotesize": 0, 59 | R"\footnotetext": 0, 60 | R"\frame": 2, 61 | R"\framebox": 4, 62 | R"\fussy": 0, 63 | R"\gcd": 3, 64 | R"\ghost": 0, 65 | R"\glossary": 0, 66 | R"\hfill": 0, 67 | R"\hom": 3, 68 | R"\hookleftarrow": 2, 69 | R"\hookrightarrow": 2, 70 | R"\hrulefill": 0, 71 | R"\huge": 0, 72 | R"\Huge": 0, 73 | R"\hyphenation": 0, 74 | R"\iff": 2, 75 | R"\Im": 2, 76 | R"\index": 0, 77 | R"\inf": 3, 78 | R"\it": 0, 79 | R"\ker": 3, 80 | R"\l": 0, 81 | R"\L": 0, 82 | R"\label": 0, 83 | R"\large": 0, 84 | R"\Large": 0, 85 | R"\LARGE": 0, 86 | R"\ldots": 3, 87 | R"\lefteqn": 0, 88 | R"\left": 0, 89 | R"\lg": 2, 90 | R"\lim": 3, 91 | R"\liminf": 6, 92 | R"\limsup": 6, 93 | R"\linebreak": 0, 94 | R"\ln": 2, 95 | R"\log": 3, 96 | R"\longleftarrow": 2, 97 | R"\Longleftarrow": 2, 98 | R"\longleftrightarrow": 2, 99 | R"\Longleftrightarrow": 2, 100 | R"\longmapsto": 3, 101 | R"\longrightarrow": 2, 102 | R"\Longrightarrow": 2, 103 | R"\makebox": 0, 104 | R"\mapsto": 2, 105 | R"\markright": 0, 106 | R"\mathds": 0, 107 | R"\max": 3, 108 | R"\mbox": 0, 109 | R"\medskip": 0, 110 | R"\min": 3, 111 | R"\mit": 0, 112 | R"\models": 2, 113 | R"\ne": 2, 114 | R"\neq": 2, 115 | R"\newline": 0, 116 | R"\noindent": 0, 117 | R"\nolinebreak": 0, 118 | R"\nonumber": 0, 119 | R"\nopagebreak": 0, 120 | R"\normalmarginpar": 0, 121 | R"\normalsize": 0, 122 | R"\notin": 2, 123 | R"\o": 0, 124 | R"\O": 0, 125 | R"\obeycr": 0, 126 | R"\oe": 0, 127 | R"\OE": 0, 128 | R"\overbrace": 4, 129 | R"\pagebreak": 0, 130 | R"\pagenumbering": 0, 131 | R"\pageref": 2, 132 | R"\pmod": 5, 133 | R"\Pr": 2, 134 | R"\protect": 0, 135 | R"\qquad": 0, 136 | R"\quad": 0, 137 | R"\raggedbottom": 0, 138 | R"\raggedleft": 0, 139 | R"\raggedright": 0, 140 | R"\Re": 2, 141 | R"\ref": 2, 142 | R"\restorecr": 0, 143 | R"\reversemarginpar": 0, 144 | R"\right": 0, 145 | R"\rm": 0, 146 | R"\sc": 0, 147 | R"\scriptscriptstyle": 0, 148 | R"\scriptsize": 0, 149 | R"\scriptstyle": 0, 150 | R"\sec": 3, 151 | R"\sf": 0, 152 | R"\shortstack": 0, 153 | R"\sin": 3, 154 | R"\sinh": 4, 155 | R"\sl": 0, 156 | R"\sloppy": 0, 157 | R"\small": 0, 158 | R"\Small": 0, 159 | R"\smallskip": 0, 160 | R"\sqrt": 2, 161 | R"\ss": 0, 162 | R"\sup": 3, 163 | R"\tan": 3, 164 | R"\tanh": 4, 165 | R"\text": 0, 166 | R"\textbf": 0, 167 | R"\textfraction": 2, 168 | R"\textstyle": 0, 169 | R"\thicklines": 0, 170 | R"\thinlines": 0, 171 | R"\thinspace": 0, 172 | R"\tiny": 0, 173 | R"\title": 0, 174 | R"\today": 15, 175 | R"\topfraction": 2, 176 | R"\tt": 0, 177 | R"\typeout": 0, 178 | R"\unboldmath": 0, 179 | R"\underbrace": 6, 180 | R"\underline": 0, 181 | R"\value": 0, 182 | R"\vdots": 3, 183 | R"\vline": 0 184 | } -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | addict 2 | appdirs 3 | audioop-lts; python_version>='3.13' 4 | colour 5 | diskcache 6 | ipython>=8.18.0 7 | isosurfaces 8 | fontTools 9 | manimpango>=0.6.0 10 | mapbox-earcut 11 | matplotlib 12 | moderngl 13 | moderngl_window 14 | numpy 15 | Pillow 16 | pydub 17 | pygments 18 | PyOpenGL 19 | pyperclip 20 | pyyaml 21 | rich 22 | scipy 23 | screeninfo 24 | setuptools 25 | skia-pathops 26 | svgelements>=1.8.1 27 | sympy 28 | tqdm 29 | typing-extensions; python_version < "3.11" 30 | validators 31 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = manimgl 3 | version = 1.7.2 4 | author = Grant Sanderson 5 | author_email= grant@3blue1brown.com 6 | description = Animation engine for explanatory math videos 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown; charset=UTF-8 9 | home_page = https://github.com/3b1b/manim 10 | project_urls = 11 | Bug Tracker = https://github.com/3b1b/manim/issues 12 | Documentation = https://3b1b.github.io/manim/ 13 | Source Code = https://github.com/3b1b/manim 14 | license = MIT 15 | classifiers = 16 | Development Status :: 4 - Beta 17 | License :: OSI Approved :: MIT License 18 | Topic :: Scientific/Engineering 19 | Topic :: Multimedia :: Video 20 | Topic :: Multimedia :: Graphics 21 | Programming Language :: Python :: 3.7 22 | Programming Language :: Python :: 3.8 23 | Programming Language :: Python :: 3.9 24 | Programming Language :: Python :: 3.10 25 | Programming Language :: Python :: 3 :: Only 26 | Natural Language :: English 27 | 28 | [options] 29 | packages = find: 30 | include_package_data = True 31 | install_requires = 32 | addict 33 | appdirs 34 | audioop-lts; python_version >= "3.13" 35 | colour 36 | diskcache 37 | ipython>=8.18.0 38 | isosurfaces 39 | fontTools 40 | manimpango>=0.6.0 41 | mapbox-earcut 42 | matplotlib 43 | moderngl 44 | moderngl_window 45 | numpy 46 | Pillow 47 | pydub 48 | pygments 49 | PyOpenGL 50 | pyperclip 51 | pyyaml 52 | rich 53 | scipy 54 | screeninfo 55 | setuptools 56 | skia-pathops 57 | svgelements>=1.8.1 58 | sympy 59 | tqdm 60 | typing-extensions; python_version < "3.11" 61 | validators 62 | 63 | [options.entry_points] 64 | console_scripts = 65 | manimgl = manimlib.__main__:main 66 | manim-render = manimlib.__main__:main 67 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | setuptools.setup() --------------------------------------------------------------------------------