diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index d83c3d2d4f..0000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,14 +0,0 @@ -### If this is a support request: - -**Please attempt to solve the problem on your own before opening an issue.** -Between old issues, StackOverflow, and Google, you should be able to find -solutions to most of the common problems. - -Include at least: -1. Steps to reproduce the issue (e.g. the command you ran) -2. The unexpected behavior that occurred (e.g. error messages or screenshots) -3. The environment (e.g. operating system and version of manim) - - -### If this is a feature request: -Include the motivation for making this change. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 03304b3c2d..237d2ac9f6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,18 +1,24 @@ --- -name: Bug report -about: Create a report to help us improve -title: " [BUG-General] " +name: Manim bug +about: Report a bug or unexpected behavior when running Manim +title: "" labels: bug assignees: '' --- -**Describe the bug** - +## Description of bug / unexpected behavior + -**To Reproduce** - -
Problematic Code + +## Expected behavior + + + +## How to reproduce the issue + + +
Code for reproducing the problem ```py Paste your code here. @@ -20,10 +26,8 @@ Paste your code here.
-**Expected behavior** - -**Output Media Files** +## Additional media files
Images/GIFs @@ -32,41 +36,42 @@ Paste your code here.
-**Logs** -
Terminal output (Screenshots acceptable) + +## Logs +
Terminal output + ``` -PASTE HERE OR REMOVE IF PROVIDING SCREENSHOT +PASTE HERE OR PROVIDE LINK TO https://pastebin.com/ OR SIMILAR ``` - +
-**System Specifications** + +## System specifications
System Details -- OS (with version, e.g Windows 10 v2004 or macOS 10.15 (Catalina)): -- RAM: +- OS (with version, e.g Windows 10 v2004 or macOS 10.15 (Catalina)): +- RAM: - Python version (`python/py/python3 --version`): - Installed modules (provide output from `pip list`): ``` PASTE HERE ``` -
-
-Latex details +
LaTeX details -+ Distribution (e.g. TeX Live 2020): - - -+ Installed packages: ++ LaTeX distribution (e.g. TeX Live 2020): ++ Installed LaTeX packages: +
FFMPEG + Output of `ffmpeg -version`: ``` @@ -74,5 +79,5 @@ PASTE HERE ```
-**Additional context** -Add any other context about the problem here. +## Additional comments + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..67f641adac --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature request +about: Request a new feature for Manim +title: "" +labels: new feature +assignees: '' + +--- + +## Description of proposed feature + + + +## How can the new feature be used? + + + +## Additional comments + diff --git a/.github/ISSUE_TEMPLATE/installation_issue.md b/.github/ISSUE_TEMPLATE/installation_issue.md new file mode 100644 index 0000000000..3e99354a88 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/installation_issue.md @@ -0,0 +1,66 @@ +--- +name: Installation issue +about: Report issues with the installation process of Manim +title: "" +labels: bug, installation +assignees: '' + +--- + +#### Preliminaries + +- [ ] I have followed the latest version of the + [installation instructions](https://docs.manim.community/en/latest/installation.html). +- [ ] I have checked the [troubleshooting page](https://docs.manim.community/en/latest/installation/troubleshooting.html) and my problem is either not mentioned there, + or the solution given there does not help. + +## Description of error + + + +## Installation logs + + +
Terminal output + +``` +PASTE HERE OR PROVIDE LINK TO https://pastebin.com/ OR SIMILAR +``` + + + +
+ + +## System specifications + +
System Details + +- OS (with version, e.g Windows 10 v2004 or macOS 10.15 (Catalina)): +- RAM: +- Python version (`python/py/python3 --version`): +- Installed modules (provide output from `pip list`): +``` +PASTE HERE +``` +
+ +
LaTeX details + ++ LaTeX distribution (e.g. TeX Live 2020): ++ Installed LaTeX packages: + +
+ +
FFMPEG + +Output of `ffmpeg -version`: + +``` +PASTE HERE +``` +
+ +## Additional comments + diff --git a/.github/ISSUE_TEMPLATE/suggestion.md b/.github/ISSUE_TEMPLATE/suggestion.md new file mode 100644 index 0000000000..0d758dd5d8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/suggestion.md @@ -0,0 +1,17 @@ +--- +name: Suggestion +about: Make a suggestion for the enhancement of existing features +title: "" +labels: enhancement +assignees: '' + +--- + +## Enhancement proposal + + + +## Additional comments + diff --git a/.github/manimdependency.json b/.github/manimdependency.json index 960c1f900f..1d38675670 100644 --- a/.github/manimdependency.json +++ b/.github/manimdependency.json @@ -1,6 +1,6 @@ { "windows": { - "ffmpeg": "https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-4.3.1-2020-09-21-full_build.zip", + "ffmpeg": "https://github.com/GyanD/codexffmpeg/releases/download/4.3.1-2020-11-19/ffmpeg-4.3.1-2020-11-19-full_build.zip", "pango": "v0.1.0", "tinytex": [ "standalone", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dfed897489..b60acd4176 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python: [3.6, 3.7, 3.8] + python: [3.6, 3.7, 3.8, 3.9] steps: - name: Checkout the repository @@ -51,7 +51,7 @@ jobs: sudo apt update sudo apt install -y ffmpeg sudo apt-get -y install texlive texlive-latex-extra texlive-fonts-extra texlive-latex-recommended texlive-science texlive-fonts-extra tipa - echo "::add-path::$HOME/.poetry/bin" + echo "$HOME/.poetry/bin" >> $GITHUB_PATH - name: Install system dependencies (MacOS) if: runner.os == 'macOS' @@ -65,8 +65,8 @@ jobs: brew install pango brew install glib sudo tlmgr install standalone preview doublestroke relsize fundus-calligra wasysym physics dvisvgm.x86_64-darwin dvisvgm rsfs wasy cm-super - echo "::add-path::$HOME/.poetry/bin" - echo "::set-env name=PATH::$PATH" + echo "/Library/TeX/texbin" >> $GITHUB_PATH + echo "$HOME/.poetry/bin" >> $GITHUB_PATH - name: Cache Windows id: cache-windows @@ -106,13 +106,13 @@ jobs: run: | $env:Path += ";" + "$($PWD)\ManimCache\FFmpeg\bin" $env:Path += ";" + "$($PWD)\ManimCache\LatexWindows\TinyTeX\bin\win32" - $env:Path += ";" + "$($PWD)\ManimCache\Pango\pango" + $env:Path += ";" + "$($PWD)\ManimCache\Pango\pango" $env:Path = "$env:USERPROFILE\.poetry\bin;$($env:PATH)" - echo "::set-env name=Path::$env:Path" + echo "$env:Path" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - name: Install manim run: | - poetry install + poetry install -E js_renderer - name: Run tests run: poetry run pytest diff --git a/.github/workflows/dependent-issues.yml b/.github/workflows/dependent-issues.yml new file mode 100644 index 0000000000..5d6b081e06 --- /dev/null +++ b/.github/workflows/dependent-issues.yml @@ -0,0 +1,26 @@ +name: Dependent Issues + +on: + issues: + schedule: + - cron: '0/30 * * * *' # schedule daily check + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: z0al/dependent-issues@v1 + env: + # (Required) The token to use to make API calls to GitHub. + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + # (Optional) The label to use to mark dependent issues + label: dependent + + # (Optional) Enable checking for dependencies in issues. Enable by + # setting the value to "on". Default "off" + check_issues: on + + # (Optional) A comma-separated list of keywords. Default + # "depends on, blocked by" + keywords: depends on, blocked by diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 1e6d6e28b0..f6c415b0b4 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -26,3 +26,26 @@ jobs: - name: Publish release to pypi run: poetry publish --build + + - name: Get Upload URL + id: create_release + shell: bash + env: + access_token: ${{ secrets.GITHUB_TOKEN }} + tag_act: ${{ github.ref }} + run: | + ref_tag=$(python -c "print('${tag_act}'.split('/')[-1])") + res=$(curl -H "Accept: application/vnd.github.v3+json" -H "Authorization: token ${access_token}" https://api.github.com/repos/ManimCommunity/manim/releases/tags/${ref_tag}) + upload_url=$(python -c "import json;print(json.loads('''${res}''')['upload_url'])") + echo "::set-output name=upload_url::${upload_url}" + + - name: Upload Release Asset + id: upload-release + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: dist/*.tar.gz + asset_name: manimce-${{ steps.tag.outputs.tag }}.tar.gz + asset_content_type: application/gzip diff --git a/.gitignore b/.gitignore index f7da3fa521..4606afd95d 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,7 @@ docs/_build/ docs/build/ docs/source/_autosummary/ docs/source/reference/ +docs/source/_build/ # PyBuilder target/ diff --git a/README.md b/README.md index c8e231d425..64705bcf8a 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,46 @@ -![logo](https://raw.githubusercontent.com/ManimCommunity/manim/master/logo/banner.png) - -![CI](https://github.com/ManimCommunity/manim/workflows/CI/badge.svg) -[![Documentation Status](https://readthedocs.org/projects/manimce/badge/?version=latest)](https://manimce.readthedocs.io/en/latest/?badge=latest) -[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](http://choosealicense.com/licenses/mit/) -[![Manim Subreddit](https://img.shields.io/reddit/subreddit-subscribers/manim.svg?color=ff4301&label=reddit)](https://www.reddit.com/r/manim/) -[![Manim Discord](https://img.shields.io/discord/581738731934056449.svg?label=discord)](https://discord.gg/mMRrZQW) - -Manim is an animation engine for explanatory math videos. It's used to create precise animations programmatically, as seen in the videos at [3Blue1Brown](https://www.3blue1brown.com/). - -> NOTE: This repository is maintained by the Manim Community, and is not associated with Grant Sanderson or 3Blue1Brown in any way (though we are definitely indebted to him for providing his work to the world). If you want to study how Grant makes his videos, head over to his repository (3b1b/manim). This is a more frequently updated repository than that one, and is recommended if you want to use Manim for your own projects. +

+ +
+
+ MIT License + Reddit + Discord + Documentation Status + Docker image + CI +
+
+ An animation engine for explanatory math videos +

+ +
+ +`manim` is an animation engine for explanatory math videos. It's used to create precise animations programmatically, as demonstrated in the videos of [3Blue1Brown](https://www.3blue1brown.com/). + +> NOTE: This repository is maintained by the Manim Community, and is not associated with Grant Sanderson or 3Blue1Brown in any way (although we are definitely indebted to him for providing his work to the world). If you would like to study how Grant makes his videos, head over to his repository ([3b1b/manim](https://github.com/3b1b/manim)). This fork is updated more frequently than his, and it's recommended to use this fork if you'd like to use Manim for your own projects. ## Table of Contents: -- [Installation](#installation) -- [Usage](#usage) -- [Documentation](#documentation) -- [Help with Manim](#help-with-manim) -- [Contributing](#contributing) -- [License](#license) +- [Installation](#installation) +- [Usage](#usage) +- [Documentation](#documentation) +- [Help with Manim](#help-with-manim) +- [Contributing](#contributing) +- [License](#license) ## Installation -Manim has a few dependencies that need to be installed before it. Please visit -the -[documentation](https://manimce.readthedocs.io/en/latest/installation.html) -and follow the instructions according to your operating system. +Manim requires a few dependencies that must be installed prior to using it. Please visit the [documentation](https://manimce.readthedocs.io/en/latest/installation.html) and follow the appropriate instructions for your operating system. + +Once the dependencies have been installed, run the following in a terminal window: + +```bash +pip install manimce +``` ## Usage -Here is an example manim script: +Manim is an extremely versatile package. The following is an example `Scene` you can construct: ```python from manim import * @@ -46,34 +58,30 @@ class SquareToCircle(Scene): self.play(FadeOut(square)) ``` -Save this code in a file called `example.py`. Now open your terminal in the -folder where you saved the file and execute +In order to view the output of this scene, save the code in a file called `example.py`. Then, run the following in a terminal window: ```sh -manim example.py SquareToCircle -pl +manim example.py SquareToCircle -p -ql ``` -You should see your video player pop up and play a simple scene where a square -is transformed into a circle. You can find some more simple examples in the -[GitHub repository](https://github.com/ManimCommunity/manim/tree/master/example_scenes). -Visit the [official gallery](https://manimce.readthedocs.io/en/latest/examples.html) for more advanced examples. +You should see your native video player program pop up and play a simple scene in which a square is transformed into a circle. You may find some more simple examples within this +[GitHub repository](master/example_scenes). You can also visit the [official gallery](https://manimce.readthedocs.io/en/latest/examples.html) for more advanced examples. ## Command line arguments The general usage of manim is as follows: -![manim-illustration](https://raw.githubusercontent.com/ManimCommunity/manim/master/readme-assets/command.png) +![manim-illustration](https://raw.githubusercontent.com/ManimCommunity/manim/master/docs/source/_static/command.png) -The `-p` flag in the command above is for previewing, meaning the video file will automatically open when it is done rendering. The `-l` flag is for a faster rendering at a lower quality. +The `-p` flag in the command above is for previewing, meaning the video file will automatically open when it is done rendering. The `-ql` flag is for a faster rendering at a lower quality. Some other useful flags include: -- `-s` to skip to the end and just show the final frame. -- `-n ` to skip ahead to the `n`'th animation of a scene. -- `-f` show the file in the file browser. +- `-s` to skip to the end and just show the final frame. +- `-n ` to skip ahead to the `n`'th animation of a scene. +- `-f` show the file in the file browser. -For a thorough list of command line arguments, visit the -[documentation](https://manimce.readthedocs.io/en/latest/tutorials/configuration.html). +For a thorough list of command line arguments, visit the [documentation](https://manimce.readthedocs.io/en/latest/tutorials/configuration.html). ## Documentation @@ -81,21 +89,15 @@ Documentation is in progress at [ReadTheDocs](https://manimce.readthedocs.io/en/ ## Help with Manim -If you need help installing or using Manim, please take a look at [the Reddit -Community](https://www.reddit.com/r/manim) or the [Discord -Community](https://discord.gg/mMRrZQW). For bug reports and feature requests, -please open an issue. +If you need help installing or using Manim, feel free to reach out to our [Discord +Server](https://discord.gg/mMRrZQW) or [Reddit Community](https://www.reddit.com/r/manim). If you would like to submit bug report or feature request, please open an issue. ## Contributing -Is always welcome. In particular, there is a dire need for tests and -documentation. For guidelines please see the -[documentation](https://manimce.readthedocs.io/en/latest/contributing.html). -This project uses [Poetry](https://python-poetry.org/docs/) for management. You need to have poetry installed and available in your environment. -You can find more information about it in its [Documentation](https://manimce.readthedocs.io/en/latest/installation/for_dev.html) +Contributions to `manim` are always welcome. In particular, there is a dire need for tests and documentation. For contribution guidelines, please see the [documentation](https://manimce.readthedocs.io/en/latest/contributing.html). + +This project uses [Poetry](https://python-poetry.org/docs/) for management. You must have poetry installed and available in your environment. You can learn more `poetry` and how to use it at its [Documentation](https://manimce.readthedocs.io/en/latest/installation/for_dev.html). ## License -The software is double-licensed under the MIT license, with copyright -by 3blue1brown LLC (see LICENSE), and copyright by Manim Community -Developers (see LICENSE.community). +The software is double-licensed under the MIT license, with copyright by 3blue1brown LLC (see LICENSE), and copyright by Manim Community Developers (see LICENSE.community). diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000000..ab8192a0c0 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,36 @@ +FROM python:3.7-slim + +ARG MANIM_VERSION=stable + +RUN apt-get update -qq \ + && apt-get install --no-install-recommends -y \ + ffmpeg \ + gcc \ + git \ + libcairo2-dev \ + libffi-dev \ + pkg-config \ + wget + +# setup a minimal texlive installation +COPY ./texlive-profile.txt /tmp/ +ENV PATH=/usr/local/texlive/bin/x86_64-linux:$PATH +RUN wget -O /tmp/install-tl-unx.tar.gz http://mirror.ctan.org/systems/texlive/tlnet/install-tl-unx.tar.gz && \ + mkdir /tmp/install-tl && \ + tar -xzf /tmp/install-tl-unx.tar.gz -C /tmp/install-tl --strip-components=1 && \ + /tmp/install-tl/install-tl --profile=/tmp/texlive-profile.txt \ + && tlmgr install \ + amsmath babel-english cm-super doublestroke dvisvgm fundus-calligra \ + jknapltx latex-bin microtype ms physics preview ragged2e relsize rsfs \ + setspace standalone tipa wasy wasysym xcolor xkeyval + +# clone and build manim +RUN git clone --depth 1 --branch ${MANIM_VERSION} https://github.com/ManimCommunity/manim.git /opt/manim +WORKDIR /opt/manim +RUN pip install --no-cache . + +# create working directory for user to mount local directory into +WORKDIR /manim +RUN chmod 666 /manim + +CMD [ "/bin/bash" ] diff --git a/docker/texlive-profile.txt b/docker/texlive-profile.txt new file mode 100644 index 0000000000..722c0dc4a6 --- /dev/null +++ b/docker/texlive-profile.txt @@ -0,0 +1,10 @@ +selected_scheme scheme-minimal +TEXDIR /usr/local/texlive +TEXMFCONFIG ~/.texlive/texmf-config +TEXMFHOME ~/texmf +TEXMFLOCAL /usr/local/texlive/texmf-local +TEXMFSYSCONFIG /usr/local/texlive/texmf-config +TEXMFSYSVAR /usr/local/texlive/texmf-var +TEXMFVAR ~/.texlive/texmf-var +option_doc 0 +option_src 0 \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile index 1f5f413c9e..846802ddae 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -5,12 +5,14 @@ # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build -SOURCEDIR = source -BUILDDIR = build + +# Path base is the source directory +SOURCEDIR = . +BUILDDIR = ../build # Put it first so that "make" without argument is like "make help". help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + @(cd source; $(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)) .PHONY: help Makefile @@ -23,4 +25,4 @@ cleanall: clean # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + @(cd source; $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)) diff --git a/docs/make.bat b/docs/make.bat index 6247f7e231..9a8d336ae6 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,14 +1,16 @@ @ECHO OFF -pushd %~dp0 +pushd %~dp0\source REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) -set SOURCEDIR=source -set BUILDDIR=build + +REM The paths are taken from the source directory +set SOURCEDIR=. +set BUILDDIR=..\build if "%1" == "" goto help diff --git a/docs/requirements.txt b/docs/requirements.txt index efc99da41e..64e2524c06 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,5 @@ Sphinx==3.1.2 guzzle-sphinx-theme recommonmark>=0.5.0 +sphinx-copybutton +sphinxext-opengraph diff --git a/docs/source/_static/command.png b/docs/source/_static/command.png index 1fc7004ddd..33772374ce 100644 Binary files a/docs/source/_static/command.png and b/docs/source/_static/command.png differ diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css index 343a51bd12..ca553aa688 100644 --- a/docs/source/_static/custom.css +++ b/docs/source/_static/custom.css @@ -2,29 +2,185 @@ td { padding: 0px 10px 0px 10px; } +a { + color: #b0604b; +} + +a:hover { + color: #ad9088; +} + +p { + color: #353535; +} + +dl.function { + border-bottom: 1px solid #ccc; +} + +.sidebar-toc ul li a:hover { + background-color: #e07a5f; +} + +.text-logo { + background-color: #525893; +} + +h1 { + background-color: #d7d8e6; + border-bottom: 1px solid #525893; +} + +.admonition.tip { + border-left-color: #87c2a5; +} + +.admonition.tip .admonition-title { + color: #87c2a5; +} + +.admonition.note { + border-left-color: #525893; +} + +.admonition.note .admonition-title { + color: #525893; +} + +.admonition.warning { + border-left-color: #e07a5f; +} + +.admonition.warning .admonition-title { + color: #e07a5f; +} + img.align-center { width: 100%; margin-bottom: 20px; margin-top: 20px; } + img { display: block; max-width: 100%; height: auto; } + +div.highlight { + background-color: #fafafa; + margin: 8px 0; +} + +div.highlight pre { + background-color: inherit; + border-color: #525893; +} + +div.seealso { + background-color: #d9edf7; + border: 1px solid #525893; +} + +.footer-relations a.btn.btn-default { + display: flex; + align-items: center; + background-image: none; + color: #b0604b; + border-color: #b0604b; +} + +.footer-relations .pull-left a.btn.btn-default::before { + font-family: 'Glyphicons Halflings'; + content: "\e079"; + margin-right: 4px; + margin-left: -2px; +} + +.footer-relations .pull-right a.btn.btn-default::after { + font-family: 'Glyphicons Halflings'; + content: "\e080"; + margin-left: 4px; + margin-right: -2px; +} + +.footer-relations a.btn.btn-default:hover { + background-image: none; + background-color: #b0604b; + color: #fff; + border-color: #b0604b; + transition: all 0.3s ease; + text-shadow: none; +} + .manim-video { width: 100%; + padding: 8px 0; + outline: 0; +} + +.manim-example { + background-color: #525893; + margin-bottom: 50px; + box-shadow: 2px 2px 4px #ddd; +} + +.manim-example .manim-video { + padding: 0; +} + +.manim-example img { + margin-bottom: 0; } -.manim-example pre { +h5.example-header { + font-size: 18px; + font-weight: bold; + padding: 8px 16px; + color: white; + margin: 0; + font-family: inherit; +} + +.manim-example .highlight { + background-color: #fafafa; + border: 2px solid #525893; + padding: 8px 8px 16px 8px; + margin: 0; +} + +.manim-example .highlight pre { + background-color: inherit; border-left: none; margin: 0; - padding: 0 0 14px 0; + padding: 0 6px 0 6px; } -.manim-example { - border-left: 2px solid #eee; - border-radius: 4px; - padding: 14px 0 14px 20px; - margin: 20px 0; +.manim-example .admonition { + margin-top: 0; + padding: 12px 20px; +} + +.manim-example .admonition-title { + font-size: 14px; + font-weight: 500; + color: white; +} + +.manim-example .example-reference { + border: none; + background-color: inherit; +} + +.manim-example .example-reference p:not(:first-child) { + margin-bottom: 0; +} + +.manim-example .copybtn { + margin-right: 6px; + font-size: 18px; } + +.manim-example .copybtn:hover { + cursor: pointer; +} \ No newline at end of file diff --git a/docs/source/_static/favicon.ico b/docs/source/_static/favicon.ico new file mode 100644 index 0000000000..ea72e95122 Binary files /dev/null and b/docs/source/_static/favicon.ico differ diff --git a/docs/source/_templates/autosummary/module.rst b/docs/source/_templates/autosummary/module.rst index ba44c4759f..54a7dfafcb 100644 --- a/docs/source/_templates/autosummary/module.rst +++ b/docs/source/_templates/autosummary/module.rst @@ -15,17 +15,6 @@ {% endif %} {% endblock %} - {% block functions %} - {% if functions %} - .. rubric:: {{ _('Functions') }} - - .. autosummary:: - {% for item in functions %} - {{ item }} - {%- endfor %} - {% endif %} - {% endblock %} - {% block classes %} {% if classes %} .. rubric:: Classes @@ -39,6 +28,16 @@ {% endif %} {% endblock %} + {% block functions %} + {% if functions %} + .. rubric:: {{ _('Functions') }} + + {% for item in functions %} + .. autofunction:: {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + {% block exceptions %} {% if exceptions %} .. rubric:: {{ _('Exceptions') }} diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 73c79ad7be..5bbb6374ea 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -1,26 +1,76 @@ -********* +######### Changelog -********* +######### -manimce-v1.0.0-dev -================== +****** +v0.2.0 +****** :Date: TBD +Changes since Manim Community release v0.1.0 + +Fixes +===== + +#. JsRender is optional to install. (via :pr:`697`). + +Configuration +============= + +#. Removed the ``skip_animations`` config option and added the + ``Renderer.skip_animations`` attribute instead (via :pr:`696`). + +#. The global ``config`` dict has been replaced by a global ``config`` instance + of the new class :class:`~.ManimConfig`. This class has a dict-like API, so + this should not break user code, only make it more robust. See the + Configuration tutorial for details. + + +Documentation +============= + +#. Add ``:issue:`` and ``:pr:`` directives for simplifying linking to issues and + pull requests on GitHub (via :pr:`685`). + + +Mobjects, Scenes, and Animations +================================ + +#. The ``alignment`` attribute to Tex and MathTex has been removed in favour of ``tex_environment``. +#. :class:`~.Text` now uses Pango for rendering. ``PangoText`` has been removed. The old implementation is still available as a fallback as :class:`~.CairoText`. +#. **New**: Variations of :class:`~.Dot` have been added as :class:`~.AnnotationDot` + (a bigger dot with bolder stroke) and :class:`~.LabeledDot` (a dot containing a + label). +#. Scene.set_variables_as_attrs has been removed (via :pr:`692`). +#. Ensure that the axes for graphs (:class:`GraphScene`) always intersect (:pr:`580`). +#. Now Mobject.add_updater does not call the newly-added updater by default + (use ``call_updater=True`` instead) (via :pr:`710`) +#. VMobject now has methods to determine and change the direction of the points (via :pr:`647`). +#. Added BraceBetweenPoints (via :pr:`693`). +#. Added ArcPolygon and ArcPolygonFromArcs (via :pr:`707`). +#. Added Cutout (via :pr:`760`). + + +****** +v0.1.0 +****** + +:Date: October 21, 2020 + This is the first release of manimce after forking from 3b1b/manim. As such, developers have focused on cleaning up and refactoring the codebase while still maintaining backwards compatibility wherever possible. New Features ------------- - +============ Command line -^^^^^^^^^^^^ +------------ #. Output of 'manim --help' has been improved -#. Implement logging with the :code:`rich` library and a :code:`logger` object instead of plain ol` prints +#. Implement logging with the :code:`rich` library and a :code:`logger` object instead of plain ol' prints #. Added a flag :code:`--dry_run`, which doesn’t write any media #. Allow for running manim with :code:`python3 -m manim` #. Refactored Tex Template management. You can now use custom templates with command line args using :code:`--tex_template`! @@ -37,19 +87,30 @@ Command line Config system -^^^^^^^^^^^^^ +------------- #. Implement a :code:`manim.cfg` config file system, that consolidates the global configuration, the command line argument parsing, and some of the constants defined in :code:`constants.py` #. Added utilities for manipulating Manim’s :code:`.cfg` files. #. Added a subcommand structure for easier use of utilities managing :code:`.cfg` files +#. Also some variables have been moved from ``constants.py`` to the new config system: + + #. ``FRAME_HEIGHT`` to ``config["frame_width"]`` + #. ``TOP`` to ``config["frame_height"] / 2 * UP`` + #. ``BOTTOM`` to ``config["frame_height"] / 2 * DOWN`` + #. ``LEFT_SIDE`` to ``config["frame_width"] / 2 * LEFT`` + #. ``RIGHT_SIDE`` to ``config["frame_width"] / 2 * RIGHT`` + #. ``self.camera.frame_rate`` to ``config["frame_rate"]`` + + Mobjects, Scenes, and Animations -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +-------------------------------- #. Add customizable left and right bracket for :code:`Matrix` mobject and :code:`set_row_colors` method for matrix mobject #. Add :code:`AddTeXLetterByLetter` animation #. Enhanced GraphScene + #. You can now add arrow tips to axes #. extend axes a bit at the start and/or end #. have invisible axes @@ -71,13 +132,14 @@ Mobjects, Scenes, and Animations Documentation -------------- +============= #. Added clearer installation instructions, tutorials, examples, and API reference [WIP] Fixes ------ +===== + #. Initialization of directories has been moved to :code:`config.py`, and a bunch of bugs associated to file structure generation have been fixed #. Nonfunctional file :code:`media_dir.txt` has been removed #. Nonfunctional :code:`if` statements in :code:`scene_file_writer.py` have been removed @@ -87,7 +149,7 @@ Fixes Of interest to developers -------------------------- +========================= #. Python code formatting is now enforced by using the :code:`black` tool #. PRs now require two approving code reviews from community devs before they can be merged @@ -102,7 +164,8 @@ Of interest to developers #. Colors have moved to an Enum Other Changes --------------- +============= + #. Cleanup 3b1b Specific Files #. Rename package from manimlib to manim #. Move all imports to :code:`__init__`, so :code:`from manim import *` replaces :code:`from manimlib.imports import *` diff --git a/docs/source/conf.py b/docs/source/conf.py index 7624f2cc77..6cf5010236 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -11,20 +11,15 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. import os -import subprocess import sys from distutils.sysconfig import get_python_lib - +from pathlib import Path sys.path.insert(0, os.path.abspath(".")) if os.environ.get("READTHEDOCS") == "True": site_path = get_python_lib() - # bindings for pangocffi, cairocffi, pangocairocffi need to be generated - subprocess.run(["python", "pangocffi/ffi_build.py"], cwd=site_path) - subprocess.run(["python", "cairocffi/ffi_build.py"], cwd=site_path) - subprocess.run(["python", "pangocairocffi/ffi_build.py"], cwd=site_path) # we need to add ffmpeg to the path ffmpeg_path = os.path.join(site_path, "imageio_ffmpeg", "binaries") # the included binary is named ffmpeg-linux..., create a symlink @@ -52,18 +47,33 @@ extensions = [ "sphinx.ext.autodoc", "recommonmark", + "sphinx_copybutton", "sphinx.ext.napoleon", "sphinx.ext.autosummary", "sphinx.ext.doctest", + "sphinx.ext.extlinks", + "sphinx.ext.linkcode", + "sphinxext.opengraph", "manim_directive", ] # Automatically generate stub pages when using the .. autosummary directive autosummary_generate = True +# generate documentation from type hints +autodoc_typehints = "description" +autoclass_content = "both" + +# controls whether functions documented by the autofunction directive +# appear with their full module names +add_module_names = False + # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] +# Custom section headings in our documentation +napoleon_custom_sections = ["Tests", ("Test", "Tests")] + # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. @@ -79,6 +89,7 @@ html_theme_path = guzzle_sphinx_theme.html_theme_path() html_theme = "guzzle_sphinx_theme" +html_favicon = str(Path("_static/favicon.ico")) # There's a standing issue with Sphinx's new-style sidebars. This is a # workaround. Taken from @@ -95,3 +106,27 @@ # This specifies any additional css files that will override the theme's html_css_files = ["custom.css"] + +# source links to github +def linkcode_resolve(domain, info): + if domain != "py": + return None + if not info["module"]: + return None + filename = info["module"].replace(".", "/") + version = os.getenv("READTHEDOCS_VERSION", "master") + if version == "latest": + version = "master" + return f"https://github.com/ManimCommunity/manim/blob/{version}/{filename}.py" + + +# external links +extlinks = { + "issue": ("https://github.com/ManimCommunity/manim/issues/%s", "issue "), + "pr": ("https://github.com/ManimCommunity/manim/pull/%s", "pull request "), +} + +# opengraph settings +ogp_image = "https://www.manim.community/logo.png" +ogp_site_name = "Manim Community | Documentation" +ogp_site_url = "https://docs.manim.community/" diff --git a/docs/source/examples.rst b/docs/source/examples.rst index 037b230e9d..b9228ce673 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -1,14 +1,737 @@ +############### Example Gallery -============ - -.. toctree:: - - examples/shapes_images_positions - examples/annotations - examples/plots - examples/tex - examples/formulas - examples/3d - examples/camera_settings - examples/animations - examples/advanced_projects +############### + +This gallery contains a collection of best practice code snippets +together with their corresponding video/image output, illustrating +different functionalities all across the library. +These are all under the MIT licence, so feel free to copy & paste them to your projects. +Enjoy this taste of Manim! + +.. tip:: + + This gallery is not the only place in our documentation where you can see explicit + code and video examples: there are many more in our + :doc:`reference manual ` -- see, for example, our documentation for + the modules :mod:`~.tex_mobject`, :mod:`~.geometry`, :mod:`~.moving_camera_scene`, + and many more. + + Also, visit our `Twitter `_ for more + *manimations*! + + +.. contents:: Overview of thematic video categories + :backlinks: none + :local: + + +Basic Concepts +============== + +.. manim:: ManimCELogo + :save_last_frame: + :ref_classes: MathTex Circle Square Triangle + + class ManimCELogo(Scene): + def construct(self): + self.camera.background_color = "#ece6e2" + logo_green = "#87c2a5" + logo_blue = "#525893" + logo_red = "#e07a5f" + logo_black = "#343434" + ds_m = MathTex(r"\mathbb{M}", fill_color=logo_black).scale(7) + ds_m.shift(2.25 * LEFT + 1.5 * UP) + circle = Circle(color=logo_green, fill_opacity=1).shift(LEFT) + square = Square(color=logo_blue, fill_opacity=1).shift(UP) + triangle = Triangle(color=logo_red, fill_opacity=1).shift(RIGHT) + logo = VGroup(triangle, square, circle, ds_m) # order matters + logo.move_to(ORIGIN) + self.add(logo) + + +.. manim:: GradientImageFromArray + :save_last_frame: + :ref_classes: ImageMobject + + class GradientImageFromArray(Scene): + def construct(self): + n = 256 + imageArray = np.uint8( + [[i * 256 / n for i in range(0, n)] for _ in range(0, n)] + ) + image = ImageMobject(imageArray).scale(2) + self.add(image) + +.. manim:: BraceAnnotation + :save_last_frame: + :ref_classes: Brace + :ref_functions: Brace.get_text Brace.get_tex + + class BraceAnnotation(Scene): + def construct(self): + dot = Dot([-2, -1, 0]) + dot2 = Dot([2, 1, 0]) + line = Line(dot.get_center(), dot2.get_center()).set_color(ORANGE) + b1 = Brace(line) + b1text = b1.get_text("Horizontal distance") + b2 = Brace(line, direction=line.copy().rotate(PI / 2).get_unit_vector()) + b2text = b2.get_tex("x-x_1") + self.add(line, dot, dot2, b1, b2, b1text, b2text) + +.. manim:: VectorArrow + :save_last_frame: + :ref_classes: Dot Arrow NumberPlane Text + + class VectorArrow(Scene): + def construct(self): + dot = Dot(ORIGIN) + arrow = Arrow(ORIGIN, [2, 2, 0], buff=0) + numberplane = NumberPlane() + origin_text = Text('(0, 0)').next_to(dot, DOWN) + tip_text = Text('(2, 2)').next_to(arrow.get_end(), RIGHT) + self.add(numberplane, dot, arrow, origin_text, tip_text) + +.. manim:: BezierSpline + :save_last_frame: + :ref_classes: Line VGroup + :ref_functions: VMobject.add_cubic_bezier_curve + + class BezierSpline(Scene): + def construct(self): + np.random.seed(42) + area = 4 + + x1 = np.random.randint(-area, area) + y1 = np.random.randint(-area, area) + p1 = np.array([x1, y1, 0]) + destination_dot1 = Dot(point=p1).set_color(BLUE) + + x2 = np.random.randint(-area, area) + y2 = np.random.randint(-area, area) + p2 = np.array([x2, y2, 0]) + destination_dot2 = Dot(p2).set_color(RED) + + deltaP = p1 - p2 + deltaPNormalized = deltaP / get_norm(deltaP) + + theta = np.radians(90) + r = np.array( + ( + (np.cos(theta), -np.sin(theta), 0), + (np.sin(theta), np.cos(theta), 0), + (0, 0, 0), + ) + ) + senk = r.dot(deltaPNormalized) + offset = 0.1 + offset_along = 0.5 + offset_connect = 0.25 + + dest_line1_point1 = p1 + senk * offset - deltaPNormalized * offset_along + dest_line1_point2 = p2 + senk * offset + deltaPNormalized * offset_along + dest_line2_point1 = p1 - senk * offset - deltaPNormalized * offset_along + dest_line2_point2 = p2 - senk * offset + deltaPNormalized * offset_along + s1 = p1 - offset_connect * deltaPNormalized + s2 = p2 + offset_connect * deltaPNormalized + dest_line1 = Line(dest_line1_point1, dest_line1_point2) + dest_line2 = Line(dest_line2_point1, dest_line2_point2) + + Lp1s1 = Line(p1, s1) + + Lp1s1.add_cubic_bezier_curve( + s1, + s1 - deltaPNormalized * 0.1, + dest_line2_point1 + deltaPNormalized * 0.1, + dest_line2_point1 - deltaPNormalized * 0.01, + ) + Lp1s1.add_cubic_bezier_curve( + s1, + s1 - deltaPNormalized * 0.1, + dest_line1_point1 + deltaPNormalized * 0.1, + dest_line1_point1, + ) + + Lp2s2 = Line(p2, s2) + + Lp2s2.add_cubic_bezier_curve( + s2, + s2 + deltaPNormalized * 0.1, + dest_line2_point2 - deltaPNormalized * 0.1, + dest_line2_point2, + ) + Lp2s2.add_cubic_bezier_curve( + s2, + s2 + deltaPNormalized * 0.1, + dest_line1_point2 - deltaPNormalized * 0.1, + dest_line1_point2, + ) + + mobjects = VGroup( + Lp1s1, Lp2s2, dest_line1, dest_line2, destination_dot1, destination_dot2 + ) + + mobjects.scale(2) + self.add(mobjects) + + +Animations +========== + +.. manim:: PointMovingOnShapes + :ref_classes: Circle Dot Line GrowFromCenter Transform MoveAlongPath Rotating + + class PointMovingOnShapes(Scene): + def construct(self): + circle = Circle(radius=1, color=BLUE) + dot = Dot() + dot2 = dot.copy().shift(RIGHT) + self.add(dot) + + line = Line([3, 0, 0], [5, 0, 0]) + self.add(line) + + self.play(GrowFromCenter(circle)) + self.play(Transform(dot, dot2)) + self.play(MoveAlongPath(dot, circle), run_time=2, rate_func=linear) + self.play(Rotating(dot, about_point=[2, 0, 0]), run_time=1.5) + self.wait() + +.. manim:: MovingAround + :ref_functions: Mobject.shift VMobject.set_fill Mobject.scale Mobject.rotate + + class MovingAround(Scene): + def construct(self): + square = Square(color=BLUE, fill_opacity=1) + + self.play(square.shift, LEFT) + self.play(square.set_fill, ORANGE) + self.play(square.scale, 0.3) + self.play(square.rotate, 0.4) + +.. manim:: MovingFrameBox + :ref_modules: manim.mobject.svg.tex_mobject + :ref_classes: MathTex SurroundingRectangle + + class MovingFrameBox(Scene): + def construct(self): + text=MathTex( + "\\frac{d}{dx}f(x)g(x)=","f(x)\\frac{d}{dx}g(x)","+", + "g(x)\\frac{d}{dx}f(x)" + ) + self.play(Write(text)) + framebox1 = SurroundingRectangle(text[1], buff = .1) + framebox2 = SurroundingRectangle(text[3], buff = .1) + self.play( + ShowCreation(framebox1), + ) + self.wait() + self.play( + ReplacementTransform(framebox1,framebox2), + ) + self.wait() + +.. manim:: RotationUpdater + :ref_functions: Mobject.add_updater Mobject.remove_updater + + class RotationUpdater(Scene): + def construct(self): + def updater_forth(mobj, dt): + mobj.rotate_about_origin(dt) + def updater_back(mobj, dt): + mobj.rotate_about_origin(-dt) + line_reference = Line(ORIGIN, LEFT).set_color(WHITE) + line_moving = Line(ORIGIN, LEFT).set_color(YELLOW) + line_moving.add_updater(updater_forth) + self.add(line_reference, line_moving) + self.wait(2) + line_moving.remove_updater(updater_forth) + line_moving.add_updater(updater_back) + self.wait(2) + line_moving.remove_updater(updater_back) + self.wait(0.5) + +.. manim:: PointWithTrace + :ref_classes: Rotating + :ref_functions: VMobject.set_points_as_corners Mobject.add_updater + + class PointWithTrace(Scene): + def construct(self): + path = VMobject() + dot = Dot() + path.set_points_as_corners([dot.get_center(), dot.get_center()]) + def update_path(path): + previous_path = path.copy() + previous_path.add_points_as_corners([dot.get_center()]) + path.become(previous_path) + path.add_updater(update_path) + self.add(path, dot) + self.play(Rotating(dot, radians=PI, about_point=RIGHT, run_time=2)) + self.wait() + self.play(dot.shift, UP) + self.play(dot.shift, LEFT) + self.wait() + + +Plotting with Manim +=================== + +.. manim:: SinAndCosFunctionPlot + :save_last_frame: + :ref_modules: manim.scene.graph_scene + :ref_classes: MathTex + :ref_functions: GraphScene.setup_axes GraphScene.get_graph GraphScene.get_vertical_line_to_graph GraphScene.input_to_graph_point + + class SinAndCosFunctionPlot(GraphScene): + CONFIG = { + "x_min": -10, + "x_max": 10.3, + "num_graph_anchor_points": 100, + "y_min": -1.5, + "y_max": 1.5, + "graph_origin": ORIGIN, + "function_color": RED, + "axes_color": GREEN, + "x_labeled_nums": range(-10, 12, 2), + } + + def construct(self): + self.setup_axes(animate=False) + func_graph = self.get_graph(np.cos, self.function_color) + func_graph2 = self.get_graph(np.sin) + vert_line = self.get_vertical_line_to_graph(TAU, func_graph, color=YELLOW) + graph_lab = self.get_graph_label(func_graph, label="\\cos(x)") + graph_lab2 = self.get_graph_label(func_graph2, label="\\sin(x)", + x_val=-10, direction=UP / 2) + two_pi = MathTex(r"x = 2 \pi") + label_coord = self.input_to_graph_point(TAU, func_graph) + two_pi.next_to(label_coord, RIGHT + UP) + self.add(func_graph, func_graph2, vert_line, graph_lab, graph_lab2, two_pi) + +.. manim:: GraphAreaPlot + :save_last_frame: + :ref_modules: manim.scenes.graph_scene + :ref_functions: GraphScene.setup_axes GraphScene.get_graph GraphScene.get_vertical_line_to_graph GraphScene.get_area + + class GraphAreaPlot(GraphScene): + CONFIG = { + "x_min" : 0, + "x_max" : 5, + "y_min" : 0, + "y_max" : 6, + "y_tick_frequency" : 1, + "x_tick_frequency" : 1, + "x_labeled_nums" : [0,2,3] + } + def construct(self): + self.setup_axes() + curve1 = self.get_graph(lambda x: 4 * x - x ** 2, x_min=0, x_max=4) + curve2 = self.get_graph(lambda x: 0.8 * x ** 2 - 3 * x + 4, x_min=0, x_max=4) + line1 = self.get_vertical_line_to_graph(2, curve1, DashedLine, color=YELLOW) + line2 = self.get_vertical_line_to_graph(3, curve1, DashedLine, color=YELLOW) + area1 = self.get_area(curve1, 0.3, 0.6, dx_scaling=10, area_color=BLUE) + area2 = self.get_area(curve2, 2, 3, bounded=curve1) + self.add(curve1, curve2, line1, line2, area1, area2) + +.. manim:: HeatDiagramPlot + :save_last_frame: + :ref_modules: manim.scenes.graph_scene + :ref_functions: GraphScene.setup_axes GraphScene.coords_to_point + + class HeatDiagramPlot(GraphScene): + CONFIG = { + "y_axis_label": r"T[$^\circ C$]", + "x_axis_label": r"$\Delta Q$", + "y_min": -8, + "y_max": 30, + "x_min": 0, + "x_max": 40, + "y_labeled_nums": np.arange(-5, 34, 5), + "x_labeled_nums": np.arange(0, 40, 5), + } + + def construct(self): + data = [20, 0, 0, -5] + x = [0, 8, 38, 39] + self.setup_axes() + dot_collection = VGroup() + for time, val in enumerate(data): + dot = Dot().move_to(self.coords_to_point(x[time], val)) + self.add(dot) + dot_collection.add(dot) + l1 = Line(dot_collection[0].get_center(), dot_collection[1].get_center()) + l2 = Line(dot_collection[1].get_center(), dot_collection[2].get_center()) + l3 = Line(dot_collection[2].get_center(), dot_collection[3].get_center()) + self.add(l1, l2, l3) + + +Special Camera Settings +======================= + +.. manim:: FollowingGraphCamera + :ref_modules: manim.scene.moving_camera_scene + :ref_classes: GraphScene MovingCameraScene MoveAlongPath Restore + :ref_functions: Mobject.add_updater + + class FollowingGraphCamera(GraphScene, MovingCameraScene): + def setup(self): + GraphScene.setup(self) + MovingCameraScene.setup(self) + def construct(self): + self.camera_frame.save_state() + self.setup_axes(animate=False) + graph = self.get_graph(lambda x: np.sin(x), + color=BLUE, + x_min=0, + x_max=3 * PI + ) + moving_dot = Dot().move_to(graph.points[0]).set_color(ORANGE) + + dot_at_start_graph = Dot().move_to(graph.points[0]) + dot_at_end_grap = Dot().move_to(graph.points[-1]) + self.add(graph, dot_at_end_grap, dot_at_start_graph, moving_dot) + self.play( self.camera_frame.scale,0.5,self.camera_frame.move_to,moving_dot) + + def update_curve(mob): + mob.move_to(moving_dot.get_center()) + + self.camera_frame.add_updater(update_curve) + self.play(MoveAlongPath(moving_dot, graph, rate_func=linear)) + self.camera_frame.remove_updater(update_curve) + + self.play(Restore(self.camera_frame)) + +.. manim:: MovingZoomedSceneAround + :ref_modules: manim.scene.zoomed_scene + :ref_classes: ZoomedScene BackgroundRectangle UpdateFromFunc + :ref_functions: Mobject.add_updater ZoomedScene.get_zoomed_display_pop_out_animation + + class MovingZoomedSceneAround(ZoomedScene): + # contributed by TheoremofBeethoven, www.youtube.com/c/TheoremofBeethoven + CONFIG = { + "zoom_factor": 0.3, + "zoomed_display_height": 1, + "zoomed_display_width": 6, + "image_frame_stroke_width": 20, + "zoomed_camera_config": { + "default_frame_stroke_width": 3, + }, + } + + def construct(self): + dot = Dot().shift(UL * 2) + image = ImageMobject(np.uint8([[0, 100, 30, 200], + [255, 0, 5, 33]])) + image.set_height(7) + frame_text = Text("Frame", color=PURPLE).scale(1.4) + zoomed_camera_text = Text("Zoomed camera", color=RED).scale(1.4) + + self.add(image, dot) + zoomed_camera = self.zoomed_camera + zoomed_display = self.zoomed_display + frame = zoomed_camera.frame + zoomed_display_frame = zoomed_display.display_frame + + frame.move_to(dot) + frame.set_color(PURPLE) + zoomed_display_frame.set_color(RED) + zoomed_display.shift(DOWN) + + zd_rect = BackgroundRectangle(zoomed_display, fill_opacity=0, buff=MED_SMALL_BUFF) + self.add_foreground_mobject(zd_rect) + + unfold_camera = UpdateFromFunc(zd_rect, lambda rect: rect.replace(zoomed_display)) + + frame_text.next_to(frame, DOWN) + + self.play(ShowCreation(frame), FadeInFrom(frame_text, direction=DOWN)) + self.activate_zooming() + + self.play(self.get_zoomed_display_pop_out_animation(), unfold_camera) + zoomed_camera_text.next_to(zoomed_display_frame, DOWN) + self.play(FadeInFrom(zoomed_camera_text, direction=DOWN)) + # Scale in x y z + scale_factor = [0.5, 1.5, 0] + self.play( + frame.scale, scale_factor, + zoomed_display.scale, scale_factor, + FadeOut(zoomed_camera_text), + FadeOut(frame_text) + ) + self.wait() + self.play(ScaleInPlace(zoomed_display, 2)) + self.wait() + self.play(frame.shift, 2.5 * DOWN) + self.wait() + self.play(self.get_zoomed_display_pop_out_animation(), unfold_camera, rate_func=lambda t: smooth(1 - t)) + self.play(Uncreate(zoomed_display_frame), FadeOut(frame)) + self.wait() + +.. manim:: FixedInFrameMObjectTest + :save_last_frame: + :ref_classes: ThreeDScene + :ref_functions: ThreeDScene.set_camera_orientation ThreeDScene.add_fixed_in_frame_mobjects + + class FixedInFrameMObjectTest(ThreeDScene): + def construct(self): + axes = ThreeDAxes() + self.set_camera_orientation(phi=75 * DEGREES, theta=-45 * DEGREES) + text3d = Text("This is a 3D text") + self.add_fixed_in_frame_mobjects(text3d) + text3d.to_corner(UL) + self.add(axes) + self.wait() + +.. manim:: ThreeDLightSourcePosition + :save_last_frame: + :ref_classes: ThreeDScene ThreeDAxes ParametricSurface + :ref_functions: ThreeDScene.set_camera_orientation + + class ThreeDLightSourcePosition(ThreeDScene): + def construct(self): + axes = ThreeDAxes() + sphere = ParametricSurface( + lambda u, v: np.array([ + 1.5 * np.cos(u) * np.cos(v), + 1.5 * np.cos(u) * np.sin(v), + 1.5 * np.sin(u) + ]), v_min=0, v_max=TAU, u_min=-PI / 2, u_max=PI / 2, + checkerboard_colors=[RED_D, RED_E], resolution=(15, 32) + ) + self.renderer.camera.light_source.move_to(3*IN) # changes the source of the light + self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.add(axes, sphere) + +.. manim:: ThreeDCameraRotation + :ref_classes: ThreeDScene ThreeDAxes + :ref_functions: ThreeDScene.begin_ambient_camera_rotation ThreeDScene.stop_ambient_camera_rotation + + class ThreeDCameraRotation(ThreeDScene): + def construct(self): + axes = ThreeDAxes() + circle=Circle() + self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.add(circle,axes) + self.begin_ambient_camera_rotation(rate=0.1) + self.wait(3) + self.stop_ambient_camera_rotation() + self.move_camera(phi=75 * DEGREES, theta=30 * DEGREES) + self.wait() + +.. manim:: ThreeDCameraIllusionRotation + :ref_classes: ThreeDScene ThreeDAxes + :ref_functions: ThreeDScene.begin_3dillusion_camera_rotation ThreeDScene.stop_3dillusion_camera_rotation + + class ThreeDCameraIllusionRotation(ThreeDScene): + def construct(self): + axes = ThreeDAxes() + circle=Circle() + self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) + self.add(circle,axes) + self.begin_3dillusion_camera_rotation(rate=2) + self.wait(PI) + self.stop_3dillusion_camera_rotation() + +.. manim:: ThreeDFunctionPlot + :ref_classes: ThreeDScene ParametricSurface + + class ThreeDFunctionPlot(ThreeDScene): + def construct(self): + resolution_fa = 22 + self.set_camera_orientation(phi=75 * DEGREES, theta=-30 * DEGREES) + + def param_plane(u, v): + x = u + y = v + z = 0 + return np.array([x, y, z]) + + plane = ParametricSurface( + param_plane, + resolution=(resolution_fa, resolution_fa), + v_min=-2, + v_max=+2, + u_min=-2, + u_max=+2, + ) + plane.scale_about_point(2, ORIGIN) + + def param_gauss(u, v): + x = u + y = v + d = np.sqrt(x * x + y * y) + sigma, mu = 0.4, 0.0 + z = np.exp(-((d - mu) ** 2 / (2.0 * sigma ** 2))) + return np.array([x, y, z]) + + gauss_plane = ParametricSurface( + param_gauss, + resolution=(resolution_fa, resolution_fa), + v_min=-2, + v_max=+2, + u_min=-2, + u_max=+2, + ) + + gauss_plane.scale_about_point(2, ORIGIN) + gauss_plane.set_style(fill_opacity=1) + gauss_plane.set_style(stroke_color=GREEN) + gauss_plane.set_fill_by_checkerboard(GREEN, BLUE, opacity=0.1) + + axes = ThreeDAxes() + + self.add(axes) + self.play(Write(plane)) + self.play(Transform(plane, gauss_plane)) + self.wait() + + +Advanced Projects +================= + +.. manim:: OpeningManim + :ref_classes: Tex MathTex Write FadeInFrom LaggedStart NumberPlane ShowCreation + :ref_functions: NumberPlane.prepare_for_nonlinear_transform + + class OpeningManim(Scene): + def construct(self): + title = Tex("This is some \\LaTeX") + basel = MathTex("\\sum_{n=1}^\\infty " "\\frac{1}{n^2} = \\frac{\\pi^2}{6}") + VGroup(title, basel).arrange(DOWN) + self.play( + Write(title), + FadeInFrom(basel, UP), + ) + self.wait() + + transform_title = Tex("That was a transform") + transform_title.to_corner(UP + LEFT) + self.play( + Transform(title, transform_title), + LaggedStart(*map(lambda obj: FadeOutAndShift(obj, direction=DOWN), basel)), + ) + self.wait() + + grid = NumberPlane() + grid_title = Tex("This is a grid") + grid_title.scale(1.5) + grid_title.move_to(transform_title) + + self.add(grid, grid_title) # Make sure title is on top of grid + self.play( + FadeOut(title), + FadeInFrom(grid_title, direction=DOWN), + ShowCreation(grid, run_time=3, lag_ratio=0.1), + ) + self.wait() + + grid_transform_title = Tex( + "That was a non-linear function \\\\" "applied to the grid" + ) + grid_transform_title.move_to(grid_title, UL) + grid.prepare_for_nonlinear_transform() + self.play( + grid.apply_function, + lambda p: p + + np.array( + [ + np.sin(p[1]), + np.sin(p[0]), + 0, + ] + ), + run_time=3, + ) + self.wait() + self.play(Transform(grid_title, grid_transform_title)) + self.wait() + +.. manim:: SineCurveUnitCircle + :ref_classes: MathTex Circle Dot Line VGroup + :ref_functions: Mobject.add_updater Mobject.remove_updater always_redraw + + class SineCurveUnitCircle(Scene): + # contributed by heejin_park, https://infograph.tistory.com/230 + def construct(self): + self.show_axis() + self.show_circle() + self.move_dot_and_draw_curve() + self.wait() + + def show_axis(self): + x_start = np.array([-6,0,0]) + x_end = np.array([6,0,0]) + + y_start = np.array([-4,-2,0]) + y_end = np.array([-4,2,0]) + + x_axis = Line(x_start, x_end) + y_axis = Line(y_start, y_end) + + self.add(x_axis, y_axis) + self.add_x_labels() + + self.orgin_point = np.array([-4,0,0]) + self.curve_start = np.array([-3,0,0]) + + def add_x_labels(self): + x_labels = [ + MathTex("\pi"), MathTex("2 \pi"), + MathTex("3 \pi"), MathTex("4 \pi"), + ] + + for i in range(len(x_labels)): + x_labels[i].next_to(np.array([-1 + 2*i, 0, 0]), DOWN) + self.add(x_labels[i]) + + def show_circle(self): + circle = Circle(radius=1) + circle.move_to(self.orgin_point) + + self.add(circle) + self.circle = circle + + def move_dot_and_draw_curve(self): + orbit = self.circle + orgin_point = self.orgin_point + + dot = Dot(radius=0.08, color=YELLOW) + dot.move_to(orbit.point_from_proportion(0)) + self.t_offset = 0 + rate = 0.25 + + def go_around_circle(mob, dt): + self.t_offset += (dt * rate) + # print(self.t_offset) + mob.move_to(orbit.point_from_proportion(self.t_offset % 1)) + + def get_line_to_circle(): + return Line(orgin_point, dot.get_center(), color=BLUE) + + def get_line_to_curve(): + x = self.curve_start[0] + self.t_offset * 4 + y = dot.get_center()[1] + return Line(dot.get_center(), np.array([x,y,0]), color=YELLOW_A, stroke_width=2 ) + + + self.curve = VGroup() + self.curve.add(Line(self.curve_start,self.curve_start)) + def get_curve(): + last_line = self.curve[-1] + x = self.curve_start[0] + self.t_offset * 4 + y = dot.get_center()[1] + new_line = Line(last_line.get_end(),np.array([x,y,0]), color=YELLOW_D) + self.curve.add(new_line) + + return self.curve + + dot.add_updater(go_around_circle) + + origin_to_circle_line = always_redraw(get_line_to_circle) + dot_to_curve_line = always_redraw(get_line_to_curve) + sine_curve_line = always_redraw(get_curve) + + self.add(dot) + self.add(orbit, origin_to_circle_line, dot_to_curve_line, sine_curve_line) + self.wait(8.5) + + dot.remove_updater(go_around_circle) diff --git a/docs/source/examples/3d.rst b/docs/source/examples/3d.rst deleted file mode 100644 index 5ffddca167..0000000000 --- a/docs/source/examples/3d.rst +++ /dev/null @@ -1,81 +0,0 @@ -3D Scenes -================================= - -.. manim:: Example3DNo1 - :save_last_frame: - - class Example3DNo1(ThreeDScene): - def construct(self): - axes = ThreeDAxes() - sphere = ParametricSurface( - lambda u, v: np.array([ - 1.5 * np.cos(u) * np.cos(v), - 1.5 * np.cos(u) * np.sin(v), - 1.5 * np.sin(u) - ]), v_min=0, v_max=TAU, u_min=-PI / 2, u_max=PI / 2, - checkerboard_colors=[RED_D, RED_E], resolution=(15, 32) - ) - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) - self.add(axes, sphere) - -.. manim:: Example3DLightSourcePosition - :save_last_frame: - - class Example3DLightSourcePosition(ThreeDScene): - def construct(self): - axes = ThreeDAxes() - sphere = ParametricSurface( - lambda u, v: np.array([ - 1.5 * np.cos(u) * np.cos(v), - 1.5 * np.cos(u) * np.sin(v), - 1.5 * np.sin(u) - ]), v_min=0, v_max=TAU, u_min=-PI / 2, u_max=PI / 2, - checkerboard_colors=[RED_D, RED_E], resolution=(15, 32) - ) - self.renderer.camera.light_source.move_to(3*IN) # changes the source of the light - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) - self.add(axes, sphere) - -.. manim:: Example3DNo3 - - class Example3DNo3(ThreeDScene): - def construct(self): - axes = ThreeDAxes() - circle=Circle() - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) - self.add(circle,axes) - self.begin_ambient_camera_rotation(rate=0.1) - self.wait(3) - self.stop_ambient_camera_rotation() - self.move_camera(phi=75 * DEGREES, theta=30 * DEGREES) - self.wait() - -.. manim:: Example3DNo4 - - class Example3DNo4(ThreeDScene): - def construct(self): - axes = ThreeDAxes() - circle=Circle() - self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) - self.add(circle,axes) - self.begin_3dillusion_camera_rotation(rate=2) - self.wait(PI) - self.stop_3dillusion_camera_rotation() - -.. manim:: Example3DNo5 - :save_last_frame: - - class Example3DNo5(ThreeDScene): - def construct(self): - curve1 = ParametricFunction( - lambda u: np.array([ - 1.2 * np.cos(u), - 1.2 * np.sin(u), - u * 0.05 - ]), color=RED, t_min=-3 * TAU, t_max=5 * TAU, - ).set_shade_in_3d(True) - axes = ThreeDAxes() - self.add(axes, curve1) - self.set_camera_orientation(phi=80 * DEGREES, theta=-60 * DEGREES) - self.wait() - diff --git a/docs/source/examples/advanced_projects.rst b/docs/source/examples/advanced_projects.rst deleted file mode 100644 index bde7942575..0000000000 --- a/docs/source/examples/advanced_projects.rst +++ /dev/null @@ -1,332 +0,0 @@ -Advanced Projects -================================= - -.. manim:: OpeningManimExample - :quality: low - - class OpeningManimExample(Scene): - def construct(self): - title = Tex("This is some \\LaTeX") - basel = MathTex("\\sum_{n=1}^\\infty " "\\frac{1}{n^2} = \\frac{\\pi^2}{6}") - VGroup(title, basel).arrange(DOWN) - self.play( - Write(title), - FadeInFrom(basel, UP), - ) - self.wait() - - transform_title = Tex("That was a transform") - transform_title.to_corner(UP + LEFT) - self.play( - Transform(title, transform_title), - LaggedStart(*map(FadeOutAndShiftDown, basel)), - ) - self.wait() - - grid = NumberPlane() - grid_title = Tex("This is a grid") - grid_title.scale(1.5) - grid_title.move_to(transform_title) - - self.add(grid, grid_title) # Make sure title is on top of grid - self.play( - FadeOut(title), - FadeInFromDown(grid_title), - ShowCreation(grid, run_time=3, lag_ratio=0.1), - ) - self.wait() - - grid_transform_title = Tex( - "That was a non-linear function \\\\" "applied to the grid" - ) - grid_transform_title.move_to(grid_title, UL) - grid.prepare_for_nonlinear_transform() - self.play( - grid.apply_function, - lambda p: p - + np.array( - [ - np.sin(p[1]), - np.sin(p[0]), - 0, - ] - ), - run_time=3, - ) - self.wait() - self.play(Transform(grid_title, grid_transform_title)) - self.wait() - - -.. manim:: SquareToCircle - :quality: low - - class SquareToCircle(Scene): - def construct(self): - circle = Circle() - square = Square() - square.flip(RIGHT) - square.rotate(-3 * TAU / 8) - circle.set_fill(PINK, opacity=0.5) - - self.play(ShowCreation(square)) - self.play(Transform(square, circle)) - self.play(FadeOut(square)) - - -.. manim:: WarpSquare - :quality: low - - class WarpSquare(Scene): - def construct(self): - square = Square() - self.play( - ApplyPointwiseFunction( - lambda point: complex_to_R3(np.exp(R3_to_complex(point))), square - ) - ) - self.wait() - -.. manim:: WriteStuff - :quality: low - - class WriteStuff(Scene): - def construct(self): - example_text = Tex("This is a some text", tex_to_color_map={"text": YELLOW}) - example_tex = MathTex( - "\\sum_{k=1}^\\infty {1 \\over k^2} = {\\pi^2 \\over 6}", - ) - group = VGroup(example_text, example_tex) - group.arrange(DOWN) - group.set_width(config["frame_width"] - 2 * LARGE_BUFF) - - self.play(Write(example_text)) - self.play(Write(example_tex)) - self.wait() - -.. manim:: UpdatersExample - :quality: low - - class UpdatersExample(Scene): - def construct(self): - decimal = DecimalNumber( - 0, - show_ellipsis=True, - num_decimal_places=3, - include_sign=True, - ) - square = Square().to_edge(UP) - - decimal.add_updater(lambda d: d.next_to(square, RIGHT)) - decimal.add_updater(lambda d: d.set_value(square.get_center()[1])) - self.add(square, decimal) - self.play( - square.to_edge, - DOWN, - rate_func=there_and_back, - run_time=5, - ) - self.wait() - - -.. manim:: VDictExample - :quality: low - - class VDictExample(Scene): - def construct(self): - square = Square().set_color(RED) - circle = Circle().set_color(YELLOW).next_to(square, UP) - - # create dict from list of tuples each having key-mobject pair - pairs = [("s", square), ("c", circle)] - my_dict = VDict(pairs, show_keys=True) - - # display it just like a VGroup - self.play(ShowCreation(my_dict)) - self.wait() - - text = Tex("Some text").set_color(GREEN).next_to(square, DOWN) - - # add a key-value pair by wrapping it in a single-element list of tuple - # after attrs branch is merged, it will be easier like `.add(t=text)` - my_dict.add([("t", text)]) - self.wait() - - rect = Rectangle().next_to(text, DOWN) - # can also do key assignment like a python dict - my_dict["r"] = rect - - # access submobjects like a python dict - my_dict["t"].set_color(PURPLE) - self.play(my_dict["t"].scale, 3) - self.wait() - - # also supports python dict styled reassignment - my_dict["t"] = Tex("Some other text").set_color(BLUE) - self.wait() - - # remove submoject by key - my_dict.remove("t") - self.wait() - - self.play(Uncreate(my_dict["s"])) - self.wait() - - self.play(FadeOut(my_dict["c"])) - self.wait() - - self.play(FadeOutAndShift(my_dict["r"], DOWN)) - self.wait() - - # you can also make a VDict from an existing dict of mobjects - plain_dict = { - 1: Integer(1).shift(DOWN), - 2: Integer(2).shift(2 * DOWN), - 3: Integer(3).shift(3 * DOWN), - } - - vdict_from_plain_dict = VDict(plain_dict) - vdict_from_plain_dict.shift(1.5 * (UP + LEFT)) - self.play(ShowCreation(vdict_from_plain_dict)) - - # you can even use zip - vdict_using_zip = VDict(zip(["s", "c", "r"], [Square(), Circle(), Rectangle()])) - vdict_using_zip.shift(1.5 * RIGHT) - self.play(ShowCreation(vdict_using_zip)) - self.wait() - - -.. manim:: VariableExample - :quality: low - - class VariableExample(Scene): - def construct(self): - var = 0.5 - on_screen_var = Variable(var, Text("var"), num_decimal_places=3) - - # You can also change the colours for the label and value - on_screen_var.label.set_color(RED) - on_screen_var.value.set_color(GREEN) - - self.play(Write(on_screen_var)) - # The above line will just display the variable with - # its initial value on the screen. If you also wish to - # update it, you can do so by accessing the `tracker` attribute - self.wait() - var_tracker = on_screen_var.tracker - var = 10.5 - self.play(var_tracker.set_value, var) - self.wait() - - int_var = 0 - on_screen_int_var = Variable( - int_var, Text("int_var"), var_type=Integer - ).next_to(on_screen_var, DOWN) - on_screen_int_var.label.set_color(RED) - on_screen_int_var.value.set_color(GREEN) - - self.play(Write(on_screen_int_var)) - self.wait() - var_tracker = on_screen_int_var.tracker - var = 10.5 - self.play(var_tracker.set_value, var) - self.wait() - - # If you wish to have a somewhat more complicated label for your - # variable with subscripts, superscripts, etc. the default class - # for the label is MathTex - subscript_label_var = 10 - on_screen_subscript_var = Variable(subscript_label_var, "{a}_{i}").next_to( - on_screen_int_var, DOWN - ) - self.play(Write(on_screen_subscript_var)) - self.wait() - -.. manim:: ExampleSineCurve - - class ExampleSineCurve(Scene): - # contributed by heejin_park, https://infograph.tistory.com/230 - def construct(self): - self.show_axis() - self.show_circle() - self.move_dot_and_draw_curve() - self.wait() - - def show_axis(self): - x_start = np.array([-6,0,0]) - x_end = np.array([6,0,0]) - - y_start = np.array([-4,-2,0]) - y_end = np.array([-4,2,0]) - - x_axis = Line(x_start, x_end) - y_axis = Line(y_start, y_end) - - self.add(x_axis, y_axis) - self.add_x_labels() - - self.orgin_point = np.array([-4,0,0]) - self.curve_start = np.array([-3,0,0]) - - def add_x_labels(self): - x_labels = [ - MathTex("\pi"), MathTex("2 \pi"), - MathTex("3 \pi"), MathTex("4 \pi"), - ] - - for i in range(len(x_labels)): - x_labels[i].next_to(np.array([-1 + 2*i, 0, 0]), DOWN) - self.add(x_labels[i]) - - def show_circle(self): - circle = Circle(radius=1) - circle.move_to(self.orgin_point) - - self.add(circle) - self.circle = circle - - def move_dot_and_draw_curve(self): - orbit = self.circle - orgin_point = self.orgin_point - - dot = Dot(radius=0.08, color=YELLOW) - dot.move_to(orbit.point_from_proportion(0)) - self.t_offset = 0 - rate = 0.25 - - def go_around_circle(mob, dt): - self.t_offset += (dt * rate) - # print(self.t_offset) - mob.move_to(orbit.point_from_proportion(self.t_offset % 1)) - - def get_line_to_circle(): - return Line(orgin_point, dot.get_center(), color=BLUE) - - def get_line_to_curve(): - x = self.curve_start[0] + self.t_offset * 4 - y = dot.get_center()[1] - return Line(dot.get_center(), np.array([x,y,0]), color=YELLOW_A, stroke_width=2 ) - - - self.curve = VGroup() - self.curve.add(Line(self.curve_start,self.curve_start)) - def get_curve(): - last_line = self.curve[-1] - x = self.curve_start[0] + self.t_offset * 4 - y = dot.get_center()[1] - new_line = Line(last_line.get_end(),np.array([x,y,0]), color=YELLOW_D) - self.curve.add(new_line) - - return self.curve - - dot.add_updater(go_around_circle) - - origin_to_circle_line = always_redraw(get_line_to_circle) - dot_to_curve_line = always_redraw(get_line_to_curve) - sine_curve_line = always_redraw(get_curve) - - self.add(dot) - self.add(orbit, origin_to_circle_line, dot_to_curve_line, sine_curve_line) - self.wait(8.5) - - dot.remove_updater(go_around_circle) diff --git a/docs/source/examples/animations.rst b/docs/source/examples/animations.rst deleted file mode 100644 index 33eab8e9ca..0000000000 --- a/docs/source/examples/animations.rst +++ /dev/null @@ -1,108 +0,0 @@ -Animations -============ - - -Transformations -################# - -Some more examples will come soon here! - -Updaters -########## - -.. manim:: Updater1Example - - class Updater1Example(Scene): - def construct(self): - def my_rotation_updater(mobj,dt): - mobj.rotate_about_origin(dt) - line_reference = Line(ORIGIN, LEFT).set_color(WHITE) - line_moving = Line(ORIGIN, LEFT).set_color(BLUE) - line_moving.add_updater(my_rotation_updater) - self.add(line_reference, line_moving) - self.wait(PI) - -.. manim:: Updater2Example - - class Updater2Example(Scene): - def construct(self): - def updater_forth(mobj, dt): - mobj.rotate_about_origin(dt) - def updater_back(mobj, dt): - mobj.rotate_about_origin(-dt) - line_reference = Line(ORIGIN, LEFT).set_color(WHITE) - line_moving = Line(ORIGIN, LEFT).set_color(YELLOW) - line_moving.add_updater(updater_forth) - self.add(line_reference, line_moving) - self.wait(2) - line_moving.remove_updater(updater_forth) - line_moving.add_updater(updater_back) - self.wait(2) - line_moving.remove_updater(updater_back) - self.wait(0.5) - -.. manim:: Example3 - - class Example3(Scene): - def construct(self): - number_line = NumberLine() ##with all your parameters and stuff - pointer = Vector(DOWN) - label = MathTex("x").add_updater(lambda m: m.next_to(pointer, UP)) - - pointer_value = ValueTracker(0) - pointer.add_updater( - lambda m: m.next_to( number_line.n2p(pointer_value.get_value()), UP) - ) - self.add(number_line, pointer, label) - self.play(pointer_value.set_value, 5) - self.wait() - self.play(pointer_value.set_value, 3) - -.. manim:: Example4 - - class Example4(Scene): - def construct(self): - path = VMobject() - dot = Dot() - path.set_points_as_corners([dot.get_center(), dot.get_center()]) - def update_path(path): - previus_path = path.copy() - previus_path.add_points_as_corners([dot.get_center()]) - path.become(previus_path) - path.add_updater(update_path) - self.add(path, dot) - self.play(Rotating(dot, radians=PI, about_point=RIGHT, run_time=2)) - self.wait() - self.play(dot.shift, UP) - self.play(dot.shift, LEFT) - self.wait() - -.. manim:: Example1ValTracker - - class Example1ValTracker(Scene): - def construct(self): - dot_disp = Dot().set_color(RED) - self.add(dot_disp) - tick_start = 1 - tick_end = 2 - val_tracker = ValueTracker(tick_start) - def dot_updater(mob): - mob.set_y(val_tracker.get_value()) - dot_disp.add_updater(dot_updater) - self.play(val_tracker.set_value, tick_end, rate_func=linear) - self.wait() - -.. manim:: Example2ValTracker - - class Example2ValTracker(Scene): - def construct(self): - tick_start = 0 - tick_end = 2 * PI - val_tracker = ValueTracker(tick_start) - def my_rotation_updater(mobj): - mobj.rotate_about_origin(1 / 30) # be careful: This is framerate dependent! - line_reference = Line(ORIGIN, LEFT).set_color(WHITE) - line_moving = Line(ORIGIN, LEFT).set_color(ORANGE) - line_moving.add_updater(my_rotation_updater) - self.add(line_reference, line_moving) - self.play(val_tracker.set_value, tick_end, run_time=PI) \ No newline at end of file diff --git a/docs/source/examples/annotations.rst b/docs/source/examples/annotations.rst deleted file mode 100644 index 33a331bd95..0000000000 --- a/docs/source/examples/annotations.rst +++ /dev/null @@ -1,49 +0,0 @@ -Annotations -================================= - -.. manim:: AnnotateBrace - :save_last_frame: - - class AnnotateBrace(Scene): - def construct(self): - dot = Dot([0, 0, 0]) - dot2 = Dot([2, 1, 0]) - line = Line(dot.get_center(), dot2.get_center()).set_color(ORANGE) - b1 = Brace(line) - b1text = b1.get_text("Distance") - b2 = Brace(line, direction=line.copy().rotate(PI / 2).get_unit_vector()) - b2text = b2.get_tex("x-x_1") - self.add(dot, dot2, line, b1, b2, b1text, b2text) - -.. manim:: ExampleArrow - :quality: medium - :save_last_frame: - - class ExampleArrow(Scene): - def construct(self): - dot = Dot(ORIGIN) - arrow = Arrow(ORIGIN, [2, 2, 0], buff=0) - numberplane = NumberPlane() - origin_text = TextMobject('(0, 0)').next_to(dot, DOWN) - tip_text = TextMobject('(2, 2)').next_to(arrow.get_end(), RIGHT) - self.add(numberplane, dot, arrow, origin_text, tip_text) - -.. manim:: ExampleArrowTips - :quality: medium - :save_last_frame: - - from manim.mobject.geometry import ArrowTriangleTip, ArrowSquareTip, ArrowSquareFilledTip,\ - ArrowCircleTip, ArrowCircleFilledTip - class ExampleArrowTips(Scene): - def construct(self): - a00 = Arrow(start=[-2, 3, 0], end=[2, 3, 0], color=YELLOW) - a11 = Arrow(start=[-2, 2, 0], end=[2, 2, 0], tip_shape=ArrowTriangleTip) - a12 = Arrow(start=[-2, 1, 0], end=[2, 1, 0]) - a21 = Arrow(start=[-2, 0, 0], end=[2, 0, 0], tip_shape=ArrowSquareTip) - a22 = Arrow([-2, -1, 0], [2, -1, 0], tip_shape=ArrowSquareFilledTip) - a31 = Arrow([-2, -2, 0], [2, -2, 0], tip_shape=ArrowCircleTip) - a32 = Arrow([-2, -3, 0], [2, -3, 0], tip_shape=ArrowCircleFilledTip) - b11 = a11.copy().scale(0.5, scale_tips=True).next_to(a11, RIGHT) - b12 = a12.copy().scale(0.5, scale_tips=True).next_to(a12, RIGHT) - b21 = a21.copy().scale(0.5, scale_tips=True).next_to(a21, RIGHT) - self.add(a00, a11, a12, a21, a22, a31, a32, b11, b12, b21) diff --git a/docs/source/examples/camera_settings.rst b/docs/source/examples/camera_settings.rst deleted file mode 100644 index 5b28addf02..0000000000 --- a/docs/source/examples/camera_settings.rst +++ /dev/null @@ -1,215 +0,0 @@ -Camera Settings -================================= - -.. manim:: Example1 - - class Example1(MovingCameraScene): - def construct(self): - text = Text("Hello World") - self.add(text) - self.play(self.camera_frame.set_width, text.get_width() * 1.2) - self.wait() - -.. manim:: Example2a - - class Example2a(MovingCameraScene): - def construct(self): - text = Text("Hello World").set_color(BLUE) - self.add(text) - self.camera_frame.save_state() - self.play(self.camera_frame.set_width, text.get_width() * 1.2) - self.wait(0.3) - self.play(Restore(self.camera_frame)) - -.. manim:: Example2b - - class Example2b(MovingCameraScene): - def construct(self): - text = Text("Hello World").set_color(BLUE) - self.add(text) - self.play(self.camera_frame.set_width, text.get_width() * 1.2) - self.wait(0.3) - self.play(self.camera_frame.set_width, 14) - - -.. manim:: Example3 - - class Example3(MovingCameraScene): - def construct(self): - s = Square(color=RED, fill_opacity=0.5).move_to(2 * LEFT) - t = Triangle(color=GREEN, fill_opacity=0.5).move_to(2 * RIGHT) - self.add(s, t) - self.play(self.camera_frame.move_to, s) - self.wait(0.3) - self.play(self.camera_frame.move_to, t) - -.. manim:: Example4 - - class Example4(MovingCameraScene): - def construct(self): - s = Square(color=BLUE, fill_opacity=0.5).move_to(2 * LEFT) - t = Triangle(color=YELLOW, fill_opacity=0.5).move_to(2 * RIGHT) - self.add(s, t) - self.play(self.camera_frame.move_to, s, - self.camera_frame.set_width,s.get_width()*2) - self.wait(0.3) - self.play(self.camera_frame.move_to, t, - self.camera_frame.set_width,t.get_width()*2) - - self.play(self.camera_frame.move_to, ORIGIN, - self.camera_frame.set_width,14) - -.. manim:: Example5 - - class Example5(GraphScene, MovingCameraScene): - def setup(self): - GraphScene.setup(self) - MovingCameraScene.setup(self) - def construct(self): - self.camera_frame.save_state() - self.setup_axes(animate=False) - graph = self.get_graph(lambda x: np.sin(x), - color=WHITE, - x_min=0, - x_max=3 * PI - ) - dot_at_start_graph = Dot().move_to(graph.points[0]) - dot_at_end_grap = Dot().move_to(graph.points[-1]) - self.add(graph, dot_at_end_grap, dot_at_start_graph) - self.play(self.camera_frame.scale, 0.5, self.camera_frame.move_to, dot_at_start_graph) - self.play(self.camera_frame.move_to, dot_at_end_grap) - self.play(Restore(self.camera_frame)) - self.wait() - -.. manim:: Example6 - - class Example6(GraphScene, MovingCameraScene): - def setup(self): - GraphScene.setup(self) - MovingCameraScene.setup(self) - def construct(self): - self.camera_frame.save_state() - self.setup_axes(animate=False) - graph = self.get_graph(lambda x: np.sin(x), - color=BLUE, - x_min=0, - x_max=3 * PI - ) - moving_dot = Dot().move_to(graph.points[0]).set_color(ORANGE) - - dot_at_start_graph = Dot().move_to(graph.points[0]) - dot_at_end_grap = Dot().move_to(graph.points[-1]) - self.add(graph, dot_at_end_grap, dot_at_start_graph, moving_dot) - self.play( self.camera_frame.scale,0.5,self.camera_frame.move_to,moving_dot) - - def update_curve(mob): - mob.move_to(moving_dot.get_center()) - - self.camera_frame.add_updater(update_curve) - self.play(MoveAlongPath(moving_dot, graph, rate_func=linear)) - self.camera_frame.remove_updater(update_curve) - - self.play(Restore(self.camera_frame)) - - -Note: ZoomedScene is derived class of MovingCameraScene, -so one can use all functionality that were used before in the MovingCameraScene examples. - -.. manim:: ExampleZoom1 - - class ExampleZoom1(ZoomedScene): - def construct(self): - dot = Dot().set_color(GREEN) - self.add(dot) - self.wait(1) - self.activate_zooming(animate=False) - self.wait(1) - self.play(dot.shift, LEFT) - -.. manim:: ExampleZoom2 - - class ExampleZoom2(ZoomedScene): - CONFIG = { - "zoom_factor": 0.3, - "zoomed_display_height": 1, - "zoomed_display_width": 3, - "image_frame_stroke_width": 20, - "zoomed_camera_config": { - "default_frame_stroke_width": 3, - }, - } - def construct(self): - dot = Dot().set_color(GREEN) - sq = Circle(fill_opacity=1, radius=0.2).next_to(dot, RIGHT) - self.add(dot, sq) - self.wait(1) - self.activate_zooming(animate=False) - self.wait(1) - self.play(dot.shift, LEFT * 0.3) - - self.play(self.zoomed_camera.frame.scale, 4) - self.play(self.zoomed_camera.frame.shift, 0.5 * DOWN) - - -.. manim:: ExampleZoom3 - - class ExampleZoom3(ZoomedScene): - # contributed by TheoremofBeethoven, www.youtube.com/c/TheoremofBeethoven - CONFIG = { - "zoom_factor": 0.3, - "zoomed_display_height": 1, - "zoomed_display_width": 6, - "image_frame_stroke_width": 20, - "zoomed_camera_config": { - "default_frame_stroke_width": 3, - }, - } - - def construct(self): - dot = Dot().shift(UL * 2) - image = ImageMobject(np.uint8([[0, 100, 30, 200], - [255, 0, 5, 33]])) - image.set_height(7) - frame_text = TextMobject("Frame", color=PURPLE).scale(1.4) - zoomed_camera_text = TextMobject("Zoomed camera", color=RED).scale(1.4) - - self.add(image, dot) - zoomed_camera = self.zoomed_camera - zoomed_display = self.zoomed_display - frame = zoomed_camera.frame - zoomed_display_frame = zoomed_display.display_frame - - frame.move_to(dot) - frame.set_color(PURPLE) - zoomed_display_frame.set_color(RED) - zoomed_display.shift(DOWN) - - zd_rect = BackgroundRectangle(zoomed_display, fill_opacity=0, buff=MED_SMALL_BUFF) - self.add_foreground_mobject(zd_rect) - - unfold_camera = UpdateFromFunc(zd_rect, lambda rect: rect.replace(zoomed_display)) - - frame_text.next_to(frame, DOWN) - - self.play(ShowCreation(frame), FadeInFromDown(frame_text)) - self.activate_zooming() - - self.play(self.get_zoomed_display_pop_out_animation(), unfold_camera) - zoomed_camera_text.next_to(zoomed_display_frame, DOWN) - self.play(FadeInFromDown(zoomed_camera_text)) - # Scale in x y z - scale_factor = [0.5, 1.5, 0] - self.play( - frame.scale, scale_factor, - zoomed_display.scale, scale_factor, - FadeOut(zoomed_camera_text), - FadeOut(frame_text) - ) - self.wait() - self.play(ScaleInPlace(zoomed_display, 2)) - self.wait() - self.play(frame.shift, 2.5 * DOWN) - self.wait() - self.play(self.get_zoomed_display_pop_out_animation(), unfold_camera, rate_func=lambda t: smooth(1 - t)) - self.play(Uncreate(zoomed_display_frame), FadeOut(frame)) - self.wait() diff --git a/docs/source/examples/formulas.rst b/docs/source/examples/formulas.rst deleted file mode 100644 index 5f615f9638..0000000000 --- a/docs/source/examples/formulas.rst +++ /dev/null @@ -1,51 +0,0 @@ -Formulas -================================= - - - - -.. manim:: MoveFrameBox - - class MoveFrameBox(Scene): - def construct(self): - text=MathTex( - "\\frac{d}{dx}f(x)g(x)=","f(x)\\frac{d}{dx}g(x)","+", - "g(x)\\frac{d}{dx}f(x)" - ) - self.play(Write(text)) - framebox1 = SurroundingRectangle(text[1], buff = .1) - framebox2 = SurroundingRectangle(text[3], buff = .1) - self.play( - ShowCreation(framebox1), - ) - self.wait() - self.play( - ReplacementTransform(framebox1,framebox2), - ) - self.wait() - -.. manim:: MoveBraces - - class MoveBraces(Scene): - def construct(self): - text=MathTex( - "\\frac{d}{dx}f(x)g(x)=", #0 - "f(x)\\frac{d}{dx}g(x)", #1 - "+", #2 - "g(x)\\frac{d}{dx}f(x)" #3 - ) - self.play(Write(text)) - brace1 = Brace(text[1], UP, buff=SMALL_BUFF) - brace2 = Brace(text[3], UP, buff=SMALL_BUFF) - t1 = brace1.get_text("$g'f$") - t2 = brace2.get_text("$f'g$") - self.play( - GrowFromCenter(brace1), - FadeIn(t1), - ) - self.wait() - self.play( - ReplacementTransform(brace1,brace2), - ReplacementTransform(t1,t2) - ) - self.wait() diff --git a/docs/source/examples/plots.rst b/docs/source/examples/plots.rst deleted file mode 100644 index 2d5dc4ea7c..0000000000 --- a/docs/source/examples/plots.rst +++ /dev/null @@ -1,172 +0,0 @@ -Plotting with manim -================================= - -Examples to illustrate the use of :class:`.GraphScene` in manim. - - -.. manim:: Plot1 - :save_last_frame: - - class Plot1(GraphScene): - def construct(self): - self.setup_axes() - func_graph=self.get_graph(lambda x: np.sin(x)) - self.add(func_graph) - -.. manim:: Plot2yLabelNumbers - :save_last_frame: - - class Plot2yLabelNumbers(GraphScene): - CONFIG = { - "y_min": 0, - "y_max": 100, - "y_axis_config": {"tick_frequency": 10}, - "y_labeled_nums": np.arange(0, 100, 10) - } - - def construct(self): - self.setup_axes() - dot = Dot().move_to(self.coords_to_point(PI / 2, 20)) - func_graph = self.get_graph(lambda x: 20 * np.sin(x)) - self.add(dot,func_graph) - -.. manim:: Plot3DataPoints - :save_last_frame: - - class Plot3DataPoints(GraphScene): - CONFIG = { - "y_axis_label": r"Concentration [\%]", - "x_axis_label": "Time [s]", - } - - def construct(self): - data = [1, 2, 2, 4, 4, 1, 3] - self.setup_axes() - for time, dat in enumerate(data): - dot = Dot().move_to(self.coords_to_point(time, dat)) - self.add(dot) - -.. manim:: Plot3bGaussian - :save_last_frame: - - amp = 5 - mu = 3 - sig = 1 - - def gaussian(x): - return amp * np.exp((-1 / 2 * ((x - mu) / sig) ** 2)) - - class Plot3bGaussian(GraphScene): - def construct(self): - self.setup_axes() - graph = self.get_graph(gaussian, x_min=-1, x_max=10).set_stroke(width=5) - self.add(graph) - -.. manim:: Plot3cGaussian - :save_last_frame: - - class Plot3cGaussian(GraphScene): - def construct(self): - def gaussian(x): - amp = 5 - mu = 3 - sig = 1 - return amp * np.exp((-1 / 2 * ((x - mu) / sig) ** 2)) - self.setup_axes() - graph = self.get_graph(gaussian, x_min=-1, x_max=10) - graph.set_style(stroke_width=5, stroke_color=GREEN) - self.add(graph) - - -.. manim:: Plot4SinCos - :save_last_frame: - - class Plot4SinCos(GraphScene): - CONFIG = { - "x_min": -10, - "x_max": 10.3, - "num_graph_anchor_points": 100, - "y_min": -1.5, - "y_max": 1.5, - "graph_origin": ORIGIN, - "function_color": RED, - "axes_color": GREEN, - "x_labeled_nums": range(-10, 12, 2), - } - - def construct(self): - self.setup_axes(animate=False) - func_graph = self.get_graph(np.cos, self.function_color) - func_graph2 = self.get_graph(np.sin) - vert_line = self.get_vertical_line_to_graph(TAU, func_graph, color=YELLOW) - graph_lab = self.get_graph_label(func_graph, label="\\cos(x)") - graph_lab2 = self.get_graph_label(func_graph2, label="\\sin(x)", - x_val=-10, direction=UP / 2) - two_pi = MathTex(r"x = 2 \pi") - label_coord = self.input_to_graph_point(TAU, func_graph) - two_pi.next_to(label_coord, RIGHT + UP) - self.add(func_graph, func_graph2, vert_line, graph_lab, graph_lab2, two_pi) - -.. manim:: Plot5Area - :save_last_frame: - - class Plot5Area(GraphScene): - CONFIG = { - "x_min" : 0, - "x_max" : 5, - "y_min" : 0, - "y_max" : 6, - "y_tick_frequency" : 1, - "x_tick_frequency" : 1, - "x_labeled_nums" : [0,2,3] - } - def construct(self): - self.setup_axes() - curve1 = self.get_graph(lambda x: 4 * x - x ** 2, x_min=0, x_max=4) - curve2 = self.get_graph(lambda x: 0.8 * x ** 2 - 3 * x + 4, x_min=0, x_max=4) - line1 = self.get_vertical_line_to_graph(2, curve1, DashedLine, color=YELLOW) - line2 = self.get_vertical_line_to_graph(3, curve1, DashedLine, color=YELLOW) - area1 = self.get_area(curve1, 0.3, 0.6, dx_scaling=10, area_color=BLUE) - area2 = self.get_area(curve2, 2, 3, bounded=curve1) - self.add(curve1, curve2, line1, line2, area1, area2) - -.. manim:: Plot6HeatDiagram - :save_last_frame: - - class Plot6HeatDiagram(GraphScene): - CONFIG = { - "y_axis_label": r"T[$^\circ C$]", - "x_axis_label": r"$\Delta Q$", - "y_min": -8, - "y_max": 30, - "x_min": 0, - "x_max": 40, - "y_labeled_nums": np.arange(-5, 34, 5), - "x_labeled_nums": np.arange(0, 40, 5), - } - - def construct(self): - data = [20, 0, 0, -5] - x = [0, 8, 38, 39] - self.setup_axes() - dot_collection = VGroup() - for time, val in enumerate(data): - dot = Dot().move_to(self.coords_to_point(x[time], val)) - self.add(dot) - dot_collection.add(dot) - l1 = Line(dot_collection[0].get_center(), dot_collection[1].get_center()) - l2 = Line(dot_collection[1].get_center(), dot_collection[2].get_center()) - l3 = Line(dot_collection[2].get_center(), dot_collection[3].get_center()) - self.add(l1, l2, l3) - - -.. manim:: ParametricFunctionWithoutGraphScene - :save_last_frame: - - class ParametricFunctionWithoutGraphScene(Scene): - def func(self, t): - return np.array((np.sin(2 * t), np.sin(3 * t), 0)) - - def construct(self): - func = ParametricFunction(self.func, t_max = TAU, fill_opacity=0).set_color(RED) - self.add(func.scale(3)) diff --git a/docs/source/examples/shapes_images_positions.rst b/docs/source/examples/shapes_images_positions.rst deleted file mode 100644 index 297b3d62aa..0000000000 --- a/docs/source/examples/shapes_images_positions.rst +++ /dev/null @@ -1,46 +0,0 @@ -Shapes, Images and Positions -================================= - -.. manim:: Example1Shape - :save_last_frame: - - class Example1Shape(Scene): - def construct(self): - d = Dot() - c = Circle() - s = Square() - t = Triangle() - d.next_to(c, RIGHT) - s.next_to(c, LEFT) - t.next_to(c, DOWN) - self.add(d, c, s, t) - self.wait(1) - -.. manim:: Example1ImageFromArray - :save_last_frame: - - class Example1ImageFromArray(Scene): - def construct(self): - image = ImageMobject(np.uint8([[0, 100, 30, 200], - [255, 0, 5, 33]])) - image.set_height(7) - self.add(image) - -.. manim:: Example2ImageFromFile - :save_last_frame: - - class Example2ImageFromFile(Scene): - def construct(self): - # Use PIL when you want to import an image from the web - import requests - from PIL import Image - img = Image.open(requests.get("https://raw.githubusercontent.com/ManimCommunity/manim/master/logo/cropped.png", - stream=True).raw) - img_mobject = ImageMobject(img) - # this line, when you want to import your Image on your machine - # img_mobject = ImageMobject("") - img_mobject.scale(3) - self.add(img_mobject) - - -Note: Here can come the UnitDot Example. \ No newline at end of file diff --git a/docs/source/examples/tex.rst b/docs/source/examples/tex.rst deleted file mode 100644 index a64505931a..0000000000 --- a/docs/source/examples/tex.rst +++ /dev/null @@ -1,166 +0,0 @@ -Text and LaTeX -=============== - - -Text --------------- -The simplest way to add text to you animation is to use the :class:`~.Text` class. It uses the Cairo library to render text. -A newer addition to manim is the :class:`~.PangoText` class, which uses the Pango library. - -The Text() mobject -+++++++++++++++++++ - -.. manim:: Example1Text - :save_last_frame: - - class Example1Text(Scene): - def construct(self): - text = Text('Hello world').scale(3) - self.add(text) - -For more examples, see: :class:`~.Text`. - -The PangoText() mobject -+++++++++++++++++++++++ - -The :class:`~.PangoText` mobject uses the Pango library to render text. Use this whenever you want to use non-English alphabets like `你好` or `こんにちは` or `안녕하세요` or `مرحبا بالعالم`. - - -LaTeX -------------------- - -The Tex() mobject -+++++++++++++++++++ -Just as you can use :class:`~.Text` to add text to your videos, you can use :class:`~.Tex` to insert LaTeX. - -.. manim:: ExampleLaTeX - :save_last_frame: - - class ExampleLaTeX(Scene): - def construct(self): - tex = Tex(r'\LaTeX').scale(3) - self.add(tex) - -Note that we are using a raw string (``r'---'``) instead of a regular string (``'---'``). -This is because TeX code uses a lot of special characters - like ``\`` for example - -that have special meaning within a regular python string. An alternative would have -been to write ``\\`` as in ``Tex('\\LaTeX')``. - -The MathTex() mobject -++++++++++++++++++++++ -Anything enclosed in ``$`` signs is interpreted as maths-mode: - -.. manim:: Example1LaTeX - :save_last_frame: - - class Example1LaTeX(Scene): - def construct(self): - tex = Tex(r'$\xrightarrow{Hello}$ \LaTeX').scale(3) - self.add(tex) - -Whereas in a :class:`~.MathTex` mobject everything is math-mode by default and you would use ``\text{}`` to -insert regular text: - -.. manim:: Example1bLaTeX - :save_last_frame: - - class Example1bLaTeX(Scene): - def construct(self): - tex = MathTex(r'\xrightarrow{Hello}\text{ \LaTeX}').scale(3) - self.add(tex) - -LaTeX commands and keyword arguments -+++++++++++++++++++++++++++++++++++++ -We can use any standard LaTeX commands in the AMS maths packages. For example the ``mathtt`` text type. - -.. manim:: Example2LaTeX - :save_last_frame: - - class Example2LaTeX(Scene): - def construct(self): - tex = Tex(r'$\mathtt{Hello}$ \LaTeX').scale(3) - self.add(tex) - -On the manim side, the :class:`~.Tex` class also accepts attributes to change the appearance of the output. -This is very similar to the :class:`~.Text` class. For example, the ``color`` keyword changes the color of the TeX mobject: - -.. manim:: Example2bLaTeX - :save_last_frame: - - class Example2bLaTeX(Scene): - def construct(self): - tex = Tex(r'Hello \LaTeX', color=BLUE).scale(3) - self.add(tex) - -Extra LaTeX Packages -+++++++++++++++++++++ -Some commands require special packages to be loaded into the TeX template. For example, -to use the ``mathscr`` script, we need to add the ``mathrsfs`` package. Since this package isn't loaded -into manim's tex template by default, we add it manually: - -.. manim:: Example3LaTeX - :save_last_frame: - - class Example3LaTeX(Scene): - def construct(self): - myTemplate = TexTemplate() - myTemplate.add_to_preamble(r"\usepackage{mathrsfs}") - tex = Tex(r'$\mathscr{H} \rightarrow \mathbb{H}$}', tex_template=myTemplate).scale(3) - self.add(tex) - -Substrings and parts -+++++++++++++++++++++ -The TeX mobject can accept multiple strings as arguments. Afterwards you can refer to the individual -parts either by their index (like ``tex[1]``), or you can look them up by (parts of) the tex code like -in this example where we set the color of the ``\bigstar`` using :func:`~.set_color_by_tex`: - -.. manim:: Example4LaTeX - :save_last_frame: - - class Example4LaTeX(Scene): - def construct(self): - tex = Tex('Hello', r'$\bigstar$', r'\LaTeX').scale(3) - tex.set_color_by_tex('igsta', RED) - self.add(tex) - -LaTeX Maths Fonts - The Template Library -++++++++++++++++++++++++++++++++++++++++++++ -Changing fonts in LaTeX when typesetting mathematical formulae is a little bit more tricky than -with regular text. It requires changing the template that is used to compile the tex code. -Manim comes with a collection of :class:`~.TexFontTemplates` ready for you to use. These templates will all work -in maths mode: - -.. manim:: Example5LaTeX - :save_last_frame: - - class Example5LaTeX(Scene): - def construct(self): - tex = Tex(r'$f: A \rightarrow B$', tex_template=TexFontTemplates.french_cursive).scale(3) - self.add(tex) - -Manim also has a :class:`~.TexTemplateLibrary` containing the TeX templates used by 3Blue1Brown. One example -is the ctex template, used for typesetting Chinese. For this to work, the ctex LaTeX package -must be installed on your system. Furthermore, if you are only typesetting Text, you probably do not -need :class:`~.Tex` at all, and should use :class:`~.Text` or :class:`~.PangoText` instead. - -.. manim:: Example6LaTeX - :save_last_frame: - - class Example6LaTeX(Scene): - def construct(self): - tex = Tex('Hello 你好 \\LaTeX', tex_template=TexTemplateLibrary.ctex).scale(3) - self.add(tex) - - -Aligning formulae -++++++++++++++++++ -A :class:`~.MathTex` mobject is typeset in the LaTeX ``align*`` environment. This means you can use the ``&`` alignment -character when typesetting multiline formulae: - -.. manim:: Example7LaTeX - :save_last_frame: - - class Example7LaTeX(Scene): - def construct(self): - tex = MathTex(r'f(x) &= 3 + 2 + 1\\ &= 5 + 1 \\ &= 6').scale(2) - self.add(tex) diff --git a/docs/source/index.rst b/docs/source/index.rst index 866827c752..b26f5ed214 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -15,17 +15,24 @@ This project is still very much a work in progress, but I hope that the information here will make it easier for newcomers to get started using ``Manim``. +.. tip:: + + All content of the docs is licensed under the MIT license. Especially for the examples + you encounter: Feel free to use this code in your own projects! + .. toctree:: :maxdepth: 2 installation - reference - examples tutorials + examples + changelog + reference reporting_bugs contributing + Navigation ================== diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 1c9812d7f1..ff1daa7abc 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -30,7 +30,10 @@ can install it from PyPI via pip: pip install manimce -You can replace ``pip`` with ``pip3`` is you need to in your system. +You can replace ``pip`` with ``pip3`` if you need to in your system. + +Alternatively, you can work with Manim using our Docker image that can be +found at `Docker Hub `_. If you'd like to contribute to and/or help develop ``manim-community``, you can clone this repository to your local device. To do this, first make sure you diff --git a/docs/source/installation/for_dev.rst b/docs/source/installation/for_dev.rst index 923a94caaa..504b1a29ba 100644 --- a/docs/source/installation/for_dev.rst +++ b/docs/source/installation/for_dev.rst @@ -21,7 +21,7 @@ If you are a Windows developer and want to use PowerShell, you can use the below (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py -UseBasicParsing).Content | python -.. note:: Poetry can be installed by other ways also, such as with ``pip``. See ``_. +.. note:: Poetry can be installed by other ways also, such as with ``pip``. See ``_. If you are using MacOS with the Homebrew package manager, you can also install poetry with ``brew install poetry``. It will add it to ``PATH`` variable automatically. In order to make sure you have poetry installed correctly, try running: diff --git a/docs/source/installation/linux.rst b/docs/source/installation/linux.rst index e9a87ff04f..44c52d15be 100644 --- a/docs/source/installation/linux.rst +++ b/docs/source/installation/linux.rst @@ -11,7 +11,7 @@ The two necessary dependencies are cairo and ffmpeg. LaTeX is strongly recommended, as it is necessary to have access to the ``Tex`` and ``MathTex`` classes. Ubuntu/Mint/Debian -************* +****************** Before installing anything, make sure that your system is up to date. @@ -50,7 +50,7 @@ If you don't have python3-pip installed, install it: Fedora/CentOS/RHEL -************* +****************** To install cairo: diff --git a/docs/source/installation/troubleshooting.rst b/docs/source/installation/troubleshooting.rst index ba2889875c..2704233d99 100644 --- a/docs/source/installation/troubleshooting.rst +++ b/docs/source/installation/troubleshooting.rst @@ -3,6 +3,32 @@ Troubleshooting List of known installation problems. +(Windows) OSError: dlopen() failed to load a library: pango? +------------------------------------------------------------ + +If your manual installation of Manim (or even the installation using +Chocolatey) fails with the error + +.. code-block:: + + OSError: dlopen() failed to load a library: pango / pango-1 / pango-1.0 / pango-1.0-0 + +possibly combined with alerts warning about procedure entry points +``"deflateSetHeader"`` and ``"inflateReset2"`` that could not be +located, you might run into an issue with a patched version of ``zlib1.dll`` +shipped by Intel, `as described here `_. + +To resolve this issue, you can copy ``zlib1.dll`` from the directory +provided for the Pango binaries to the directory Manim is installed to. + +For a more global solution (try at your own risk!), try renaming the +file ``zlib1.dll`` located at ``C:\Program Files\Intel\Wifi\bin`` to +something like ``zlib1.dll.bak`` -- and then try installing Manim again +(either using ``pip install manimce`` or with Chocolatey). Ensure that +you are able to revert this name change in case you run into troubles +with your WiFi (we did not get any reports about such a problem, however). + + Some letters are missing from TextMobject/TexMobject output? ------------------------------------------------------------ @@ -15,7 +41,7 @@ uses. Which can be done by running: .. _dvisvgm-troubleshoot: - + Installation does not support converting PDF to SVG? ---------------------------------------------------- @@ -51,27 +77,27 @@ As soon as you have found the library, try (on Mac OS or Linux) .. code-block:: bash - export LIBS= + export LIBGS= dvisvgm -l -or (on Windows) +or (on Windows) -.. code-block:: cmd +.. code-block:: bat - set LIBS= + set LIBGS= dvisvgm -l You should now see ``ps dvips PostScript specials`` in the output. Refer to your operating system's documentation in order to find out how you can set or export the environment variable ``LIBGS`` automatically whenever you open a shell. -As a last check, you can run +As a last check, you can run .. code-block:: bash dvisvgm -V1 while still having ``LIBGS`` set to the correct path, of course. If ``dvisvgm`` can find your Ghostscript installation, it will be shown in the output together with the version number. - + If you do not have the necessary library on your system, please refer to your operating system's documentation in order to find out where you can get it and how you have to install it. If you are unable to solve your problem, check out the `dvisvgm FAQ `_. diff --git a/docs/source/installation/win.rst b/docs/source/installation/win.rst index 4023722b60..f9d301d13b 100644 --- a/docs/source/installation/win.rst +++ b/docs/source/installation/win.rst @@ -10,25 +10,25 @@ Installing using Chocolatey You can install manim very easily using chocolatey, by typing the following command. - .. code-block:: powershell - choco install manim + choco install manimce And then you can skip all the other steps and move to installing :ref:`latex-installation`. +Please see :doc:`troubleshooting` section for details about OSError. Pango Installation ****************** -These steps would get you `libpango-1.0-0.dll` to your ``PATH`` along -with other dependencies. You may probably have them before itself if -you have installed `GTK `_ or any ``GTK`` -based app like emacs. If you have it you can just add it to your +These steps would get you `libpango-1.0-0.dll` to your ``PATH`` along +with other dependencies. You may probably have them before itself if +you have installed `GTK `_ or any ``GTK`` +based app like emacs. If you have it you can just add it to your path and skip these steps. 1. Go to `Release Page - `_ + `_ and download the one according to your PC architechture. .. important:: Please download the ``zip`` file for architechture of python installed. @@ -37,7 +37,7 @@ path and skip these steps. 2. Extract the zip file using File Explorer or 7z to the loaction you want to install. .. code-block:: bash - + 7z x pango-windows-binaires-x64.zip -oC:\Pango 3. Finally, add it `PATH variable @@ -71,7 +71,7 @@ installed using ``tlmgr``. See https://www.tug.org/texlive/tlmgr.html for more i 2. Run the following command .. code-block:: powershell - + choco install manim-latex 3. Finally, check whether it installed properly by running an example scene. diff --git a/docs/source/manim_directive.py b/docs/source/manim_directive.py index 16ee8b4425..0575c1efad 100644 --- a/docs/source/manim_directive.py +++ b/docs/source/manim_directive.py @@ -21,6 +21,7 @@ def construct(self): render scenes that are defined within doctests, for example:: .. manim:: DirectiveDoctestExample + :ref_classes: Dot >>> dot = Dot(color=RED) >>> dot.color @@ -58,19 +59,61 @@ def construct(self): an image representing the last frame of the scene will be rendered and displayed, instead of a video. + ref_classes + A list of classes, separated by spaces, that is + rendered in a reference block after the source code. + + ref_functions + A list of functions and methods, separated by spaces, + that is rendered in a reference block after the source code. + """ +from docutils import nodes from docutils.parsers.rst import directives, Directive -from docutils.parsers.rst.directives.images import Image +from docutils.statemachine import StringList import jinja2 import os from os.path import relpath +from pathlib import Path +from typing import List import shutil +from manim import QUALITIES + classnamedict = {} +class skip_manim_node(nodes.Admonition, nodes.Element): + pass + + +def visit(self, node, name=""): + self.visit_admonition(node, name) + + +def depart(self, node): + self.depart_admonition(node) + + +def process_name_list(option_input: str, reference_type: str) -> List[str]: + r"""Reformats a string of space separated class names + as a list of strings containing valid Sphinx references. + + Tests + ----- + + :: + + >>> process_name_list("Tex TexTemplate", "class") + [":class:`~.Tex`", ":class:`~.TexTemplate`"] + >>> process_name_list("Scene.play Mobject.rotate", "func") + [":func:`~.Scene.play`", ":func:`~.Mobject.rotate`"] + """ + return [f":{reference_type}:`~.{name}`" for name in option_input.split()] + + class ManimDirective(Directive): r"""The manim directive, rendering videos while building the documentation. @@ -87,10 +130,20 @@ class ManimDirective(Directive): ), "save_as_gif": bool, "save_last_frame": bool, + "ref_modules": lambda arg: process_name_list(arg, "mod"), + "ref_classes": lambda arg: process_name_list(arg, "class"), + "ref_functions": lambda arg: process_name_list(arg, "func"), } final_argument_whitespace = True def run(self): + if "skip-manim" in self.state.document.settings.env.app.builder.tags.tags: + node = skip_manim_node() + self.state.nested_parse( + StringList(self.content[0]), self.content_offset, node + ) + return [node] + from manim import config global classnamedict @@ -106,29 +159,27 @@ def run(self): save_last_frame = "save_last_frame" in self.options assert not (save_as_gif and save_last_frame) - frame_rate = 30 - pixel_height = 480 - pixel_width = 854 + ref_content = ( + self.options.get("ref_modules", []) + + self.options.get("ref_classes", []) + + self.options.get("ref_functions", []) + ) + if ref_content: + ref_block = f""" +.. admonition:: Example References + :class: example-reference - if "quality" in self.options: - quality = self.options["quality"] - if quality == "low": - pixel_height = 480 - pixel_width = 854 - frame_rate = 15 - elif quality == "medium": - pixel_height = 720 - pixel_width = 1280 - frame_rate = 30 - elif quality == "high": - pixel_height = 1440 - pixel_width = 2560 - frame_rate = 60 - elif quality == "fourk": - pixel_height = 2160 - pixel_width = 3840 - frame_rate = 60 + {' '.join(ref_content)}""" + else: + ref_block = "" + if "quality" in self.options: + quality = f'{self.options["quality"]}_quality' + else: + quality = "example_quality" + frame_rate = QUALITIES[quality]["frame_rate"] + pixel_height = QUALITIES[quality]["pixel_height"] + pixel_width = QUALITIES[quality]["pixel_width"] qualitydir = f"{pixel_height}p{frame_rate}" state_machine = self.state_machine @@ -153,33 +204,19 @@ def run(self): ] source_block = "\n".join(source_block) - media_dir = os.path.join(setup.confdir, "media") - if not os.path.exists(media_dir): - os.mkdir(media_dir) - images_dir = os.path.join(media_dir, "images") - if not os.path.exists(images_dir): - os.mkdir(images_dir) - tex_dir = os.path.join(media_dir, "tex") - if not os.path.exists(tex_dir): - os.mkdir(tex_dir) - text_dir = os.path.join(media_dir, "text") - if not os.path.exists(text_dir): - os.mkdir(text_dir) - video_dir = os.path.join(media_dir, "videos") + config.media_dir = Path(setup.confdir) / "media" + config.images_dir = "{media_dir}/images" + config.video_dir = "{media_dir}/videos/{quality}" output_file = f"{clsname}-{classnamedict[clsname]}" + config.assets_dir = Path("_static") - file_writer_config_code = [ + config_code = [ f'config["frame_rate"] = {frame_rate}', f'config["pixel_height"] = {pixel_height}', f'config["pixel_width"] = {pixel_width}', - f'file_writer_config["media_dir"] = r"{media_dir}"', - f'file_writer_config["images_dir"] = r"{images_dir}"', - f'file_writer_config["tex_dir"] = r"{tex_dir}"', - f'file_writer_config["text_dir"] = r"{text_dir}"', - f'file_writer_config["video_dir"] = r"{video_dir}"', - f'file_writer_config["save_last_frame"] = {save_last_frame}', - f'file_writer_config["save_as_gif"] = {save_as_gif}', - f'file_writer_config["output_file"] = r"{output_file}"', + f'config["save_last_frame"] = {save_last_frame}', + f'config["save_as_gif"] = {save_as_gif}', + f'config["output_file"] = r"{output_file}"', ] user_code = self.content @@ -190,7 +227,7 @@ def run(self): code = [ "from manim import *", - *file_writer_config_code, + *config_code, *user_code, f"{clsname}().render()", ] @@ -199,25 +236,28 @@ def run(self): # copy video file to output directory if not (save_as_gif or save_last_frame): filename = f"{output_file}.mp4" - filesrc = os.path.join(video_dir, qualitydir, filename) + filesrc = config.get_dir("video_dir") / filename destfile = os.path.join(dest_dir, filename) shutil.copyfile(filesrc, destfile) elif save_as_gif: filename = f"{output_file}.gif" - filesrc = os.path.join(video_dir, qualitydir, filename) + filesrc = config.get_dir("video_dir") / filename elif save_last_frame: filename = f"{output_file}.png" - filesrc = os.path.join(images_dir, filename) + filesrc = config.get_dir("images_dir") / filename else: raise ValueError("Invalid combination of render flags received.") rendered_template = jinja2.Template(TEMPLATE).render( + clsname=clsname, + clsname_lowercase=clsname.lower(), hide_source=hide_source, filesrc_rel=os.path.relpath(filesrc, setup.confdir), output_file=output_file, save_last_frame=save_last_frame, save_as_gif=save_as_gif, source_block=source_block, + ref_block=ref_block, ) state_machine.insert_input( rendered_template.split("\n"), source=document.attributes["source"] @@ -229,9 +269,12 @@ def run(self): def setup(app): import manim + app.add_node(skip_manim_node, html=(visit, depart)) + setup.app = app setup.config = app.config setup.confdir = app.confdir + app.add_directive("manim", ManimDirective) metadata = {"parallel_read_safe": False, "parallel_write_safe": True} @@ -244,24 +287,31 @@ def setup(app):
-{{ source_block }} {% endif %} {% if not (save_as_gif or save_last_frame) %} .. raw:: html - + {% elif save_as_gif %} .. image:: /{{ filesrc_rel }} :align: center + :name: {{ clsname_lowercase }} {% elif save_last_frame %} .. image:: /{{ filesrc_rel }} :align: center + :name: {{ clsname_lowercase }} {% endif %} - {% if not hide_source %} .. raw:: html -
+
{{ clsname }}
+ +{{ source_block }} +{{ ref_block }} {% endif %} + +.. raw:: html + + """ diff --git a/docs/source/reference.rst b/docs/source/reference.rst index 88d275c268..d4e8d77b61 100644 --- a/docs/source/reference.rst +++ b/docs/source/reference.rst @@ -10,9 +10,9 @@ the :doc:`changelog`. .. currentmodule:: manim -******************** -Mathematical Objects -******************** +******** +Mobjects +******** .. autosummary:: :toctree: reference @@ -22,6 +22,7 @@ Mathematical Objects ~mobject.frame ~mobject.functions ~mobject.geometry + ~mobject.logo ~mobject.matrix ~mobject.mobject ~mobject.mobject_update_utils @@ -29,14 +30,12 @@ Mathematical Objects ~mobject.numbers ~mobject.probability ~mobject.shape_matchers - ~mobject.three_d_shading_utils ~mobject.three_d_utils ~mobject.three_dimensions ~mobject.value_tracker ~mobject.vector_field ~mobject.svg.brace ~mobject.svg.code_mobject - ~mobject.svg.drawings ~mobject.svg.svg_mobject ~mobject.svg.tex_mobject ~mobject.svg.text_mobject @@ -79,7 +78,6 @@ Animations ~animation.movement ~animation.numbers ~animation.rotation - ~animation.specialized ~animation.transform ~animation.update @@ -98,6 +96,19 @@ Cameras ~camera.three_d_camera +************* +Configuration +************* + +.. autosummary:: + :toctree: reference + + ~_config + ~_config.utils + ~_config.main_utils + ~_config.logger_utils + + ********* Utilities ********* @@ -129,14 +140,5 @@ Other modules .. autosummary:: :toctree: reference - _config constants container - - -.. This is here so that sphinx doesn't complain about changelog.rst not being - included in any toctree -.. toctree:: - :hidden: - - changelog diff --git a/docs/source/tutorials/configuration.rst b/docs/source/tutorials/configuration.rst index 344b3f6be1..372eefccbb 100644 --- a/docs/source/tutorials/configuration.rst +++ b/docs/source/tutorials/configuration.rst @@ -1,12 +1,80 @@ Configuration -============= +############# Manim provides an extensive configuration system that allows it to adapt to -many different use cases. The easiest way to do this is through the use of -command line (or *CLI*) arguments. +many different use cases. There are many configuration options that can be +configured at different times during the scene rendering process. Each option +can be configured programatically via `the ManimConfig class`_, or at the time +of command invocation via `command line arguments`_, or at the time the library +is first imported via `the config files`_. -Command Line Arguments +The ManimConfig class +********************* + +The most direct way of configuring manim is via the global ``config`` object, +which is an instance of :class:`.ManimConfig`. Each property of this class is +a config option that can be accessed either with standard attribute syntax, or +with dict-like syntax: + +.. code-block:: python + + >>> from manim import * + >>> config.background_color = WHITE + >>> config['background_color'] = WHITE + +The former is preferred; the latter is provided mostly for backwards +compatibility. + +Most classes, including :class:`.Camera`, :class:`.Mobject`, and +:class:`.Animation`, read some of their default configuration from the global +``config``. + +.. code-block:: python + + >>> Camera({}).background_color + + >>> config.background_color = RED # 0xfc6255 + >>> Camera({}).background_color + + +:class:`.ManimConfig` is designed to keep internal consistency. For example, +setting ``frame_y_radius`` will affect ``frame_height``: + +.. code-block:: python + + >>> config.frame_height + 8.0 + >>> config.frame_y_radius = 5.0 + >>> config.frame_height + 10.0 + +The global ``config`` object is mean to be the single source of truth for all +config options. All of the other ways of setting config options ultimately +change the values of the global ``config`` object. + +The following example illustrates the video resolution chosen for examples +rendered in our documentation with a reference frame. + +.. manim:: ShowScreenResolution + :save_last_frame: + + class ShowScreenResolution(Scene): + def construct(self): + pixel_height = config["pixel_height"] # 1080 is default + pixel_width = config["pixel_width"] # 1920 is default + frame_width = config["frame_width"] + frame_height = config["frame_height"] + self.add(Dot()) + d1 = Line(frame_width * LEFT / 2, frame_width * RIGHT / 2).to_edge(DOWN) + self.add(d1) + self.add(Text(str(pixel_width)).next_to(d1, UP)) + d2 = Line(frame_height * UP / 2, frame_height * DOWN / 2).to_edge(LEFT) + self.add(d2) + self.add(Text(str(pixel_height)).next_to(d2, RIGHT)) + + +Command line arguments ********************** Usually, manim is ran from the command line by executing @@ -22,104 +90,23 @@ high, and 4k quality, respectively. .. code-block:: bash - $ manim SceneName -l - -Another frequent flag is :code:`-p`, which makes manim show the rendered video -right after it's done rendering. - -There are in fact many more command line flags that manim accepts. All the -possible flags are shown by the following command. - -.. code-block:: bash - - $ manim -h - -The output looks as follows. - -.. testcode:: - :hide: - - import subprocess - result = subprocess.run(['manim', '-h'], stdout=subprocess.PIPE) - print(result.stdout.decode('utf-8')) + $ manim SceneName -ql -.. testoutput:: - :options: -ELLIPSIS, +NORMALIZE_WHITESPACE +These flags set the values of the config options ``config.pixel_width``, +``config.pixel_height``, ``config.frame_rate``, and ``config.quality``. - usage: manim [-h] [-o OUTPUT_FILE] [-p] [-f] [--leave_progress_bars] [-a] [-w] [-s] [-g] [-i] [--disable_caching] [--flush_cache] [--log_to_file] [-c BACKGROUND_COLOR] - [--background_opacity BACKGROUND_OPACITY] [--media_dir MEDIA_DIR] [--log_dir LOG_DIR] [--tex_template TEX_TEMPLATE] [--dry_run] [-t] [-q {k,p,h,m,l}] - [--low_quality] [--medium_quality] [--high_quality] [--production_quality] [--fourk_quality] [-l] [-m] [-e] [-k] [-r RESOLUTION] [-n FROM_ANIMATION_NUMBER] - [--use_js_renderer] [--js_renderer_path JS_RENDERER_PATH] [--config_file CONFIG_FILE] [--custom_folders] [-v {DEBUG,INFO,WARNING,ERROR,CRITICAL}] - [--progress_bar True/False] - {cfg} ... file [scene_names [scene_names ...]] - - Animation engine for explanatory math videos +Another frequent flag is ``-p`` ("preview"), which makes manim show the rendered video +right after it's done rendering. - positional arguments: - {cfg} - file path to file holding the python code for the scene - scene_names Name of the Scene class you want to see +.. note:: The ``-p`` flag does not change any properties of the global + ``config`` dict. The ``-p`` flag is only a command line convenience. - optional arguments: - -h, --help show this help message and exit - -o OUTPUT_FILE, --output_file OUTPUT_FILE - Specify the name of the output file, if it should be different from the scene class name - -p, --preview Automatically open the saved file once its done - -f, --show_in_file_browser - Show the output file in the File Browser - --leave_progress_bars - Leave progress bars displayed in terminal - -a, --write_all Write all the scenes from a file - -w, --write_to_movie Render the scene as a movie file (this is on by default) - -s, --save_last_frame - Save the last frame only (no movie file is generated) - -g, --save_pngs Save each frame as a png - -i, --save_as_gif Save the video as gif - --disable_caching Disable caching (will generate partial-movie-files anyway) - --flush_cache Remove all cached partial-movie-files - --log_to_file Log terminal output to file - -c BACKGROUND_COLOR, --background_color BACKGROUND_COLOR - Specify background color - --background_opacity BACKGROUND_OPACITY - Specify background opacity - --media_dir MEDIA_DIR - Directory to store media (including video files) - --log_dir LOG_DIR Directory to store log files - --tex_template TEX_TEMPLATE - Specify a custom TeX template file - --dry_run Do a dry run (render scenes but generate no output files) - -t, --transparent Render a scene with an alpha channel - -q {k,p,h,m,l}, --quality {k,p,h,m,l} - Render at specific quality, short form of the --*_quality flags - --low_quality Render at low quality - --medium_quality Render at medium quality - --high_quality Render at high quality - --production_quality Render at default production quality - --fourk_quality Render at 4K quality - -l DEPRECATED: USE -ql or --quality l - -m DEPRECATED: USE -qm or --quality m - -e DEPRECATED: USE -qh or --quality h - -k DEPRECATED: USE -qk or --quality k - -r RESOLUTION, --resolution RESOLUTION - Resolution, passed as "height,width". Overrides any quality flags, if present - -n FROM_ANIMATION_NUMBER, --from_animation_number FROM_ANIMATION_NUMBER - Start rendering at the specified animation index, instead of the first animation. If you pass in two comma separated values, e.g. '3,6', it will end - the rendering at the second value - --use_js_renderer Render animations using the javascript frontend - --js_renderer_path JS_RENDERER_PATH - Path to the javascript frontend - --config_file CONFIG_FILE - Specify the configuration file - --custom_folders Use the folders defined in the [custom_folders] section of the config file to define the output folder structure - -v {DEBUG,INFO,WARNING,ERROR,CRITICAL}, --verbosity {DEBUG,INFO,WARNING,ERROR,CRITICAL} - Verbosity level. Also changes the ffmpeg log level unless the latter is specified in the config - --progress_bar True/False - Display the progress bar - Made with <3 by the manim community devs +Examples +======== -For example, to render a scene in high quality, but only output the last frame -of the scene instead of the whole video, you can execute +To render a scene in high quality, but only output the last frame of the scene +instead of the whole video, you can execute .. code-block:: bash @@ -135,76 +122,88 @@ open the file after it is rendered. $ manim SceneName -o myscene -i -n 0,10 -c WHITE +.. tip:: There are many more command line flags that manim accepts. All the + possible flags are shown by executing ``manim -h``. A complete list + of CLI flags is at the end of this document. The config files **************** As the last example shows, executing manim from the command line may involve -using many flags at the same. This may become a nuisance if you must execute -the same file many times in a short time period, for example when making small -incremental tweaks to your scene script. For this purpose, manim can also be -configured using a configuration file. +using many flags at the same time. This may become a nuisance if you must +execute the same script many times in a short time period, for example when +making small incremental tweaks to your scene script. For this purpose, manim +can also be configured using a configuration file. A configuration file is a +file ending with the suffix ``.cfg``. To use a configuration file when rendering your scene, you must create a file -with name :code:`manim.cfg` in the same directory as your scene code. +with name ``manim.cfg`` in the same directory as your scene code. -.. warning:: The config file **must** be named :code:`manim.cfg`. Currently, - manim does not support config files with any other name. +.. warning:: The config file **must** be named ``manim.cfg``. Currently, manim + does not support config files with any other name. -The config file must start with a section header, usually :code:`[CLI]`. The +The config file must start with the section header ``[CLI]``. The configuration options under this header have the same name as the CLI flags, and serve the same purpose. Take for example the following config file. .. code-block:: [CLI] + # my config file output_file = myscene save_as_gif = True background_color = WHITE -Executing :code:`manim SceneName` on a directory that contains this -config file is equivalent to executing +Config files are read with the standard python library ``configparser``. In +particular, they will ignore any line that starts with a pound symbol ``#``. + +Now, executing the following command .. code-block:: bash $ manim SceneName -o myscene -i -c WHITE -on a directory that does not contain a config file. +is equivalent to executing the following command, provided that ``manim.cfg`` +is in the same directory as , + +.. code-block:: bash + + $ manim SceneName -.. note:: The names of the configuration options admissible in config files are - exactly the same as the **long names** of the corresponding command - line flags. For example, the :code:`-c` and - :code:`--background_color` flags are interchangeable, but the config - file only accepts :code:`background_color` as an admissible option. +.. tip:: The names of the configuration options admissible in config files are + exactly the same as the **long names** of the corresponding command + line flags. For example, the ``-c`` and ``--background_color`` flags + are interchangeable, but the config file only accepts + :code:`background_color` as an admissible option. -.. note:: Configuration options that do not have command line analogues will be - ignored. For a list of all the command line flags, see `Command Line - Arguments`_. +Since config files are meant to replace CLI flags, all CLI flags can be set via +a config file. Moreover, any config option can be set via a config file, +whether or not it has an associated CLI flag. For a list of all CLI flags and +all config options, see the bottom of this document. -Manim will look for a :code:`manim.cfg` config file in the same directory as -the file being rendered, and **not** in the directory of execution. For -example, +Manim will look for a ``manim.cfg`` config file in the same directory as the +file being rendered, and **not** in the directory of execution. For example, .. code-block:: bash - $ manim path/to/SceneName -o myscene -i -c WHITE + $ manim SceneName -o myscene -i -c WHITE -will use the config file found in :code:`path/to/SceneName`, if any. It will -**not** use the config file found in the current working directory, even if it -exists. In this way, the user may keep different config files for different -scenes or projects, and execute them with the right configuration from anywhere -in the system. +will use the config file found in ``path/to/file.py``, if any. It will **not** +use the config file found in the current working directory, even if it exists. +In this way, the user may keep different config files for different scenes or +projects, and execute them with the right configuration from anywhere in the +system. -.. note:: Config files will ignore any line that starts with a pound symbol - :code:`#`. +The file described here is called the **folder-wide** config file, because it +affects all scene scripts found in the same folder. The user config file -******************** +==================== As explained in the previous section, a :code:`manim.cfg` config file only -affects the scene scripts in its same directory. However, the user may also +affects the scene scripts in its same folder. However, the user may also create a special config file that will apply to all scenes rendered by that user. This is referred to as the **user-wide** config file, and it will apply regardless of where manim is executed from, and regardless of where the scene @@ -220,11 +219,9 @@ system. Here, :code:`UserDirectory` is the user's home folder. -.. note:: Config files that only apply to their own folder, explained in the - previous section, are called **folder-wide** config files. A user - may have many folder-wide config files, one per folder, but only one - **user-wide** config file. Different users in the same computer may - each have their own user-wide config file. +.. note:: A user may have many **folder-wide** config files, one per folder, + but only one **user-wide** config file. Different users in the same + computer may each have their own user-wide config file. .. warning:: Do not store scene scripts in the same folder as the user-wide config file. In this case, the behavior is undefined. @@ -234,7 +231,7 @@ user-wide config file and read its configuration. Cascading config files -********************** +====================== What happens if you execute manim and it finds both a folder-wide config file and a user-wide config file? Manim will read both files, but if they are @@ -273,12 +270,172 @@ SceneName -c RED` is equivalent to not using any config files and executing manim SceneName -o myscene -c RED -To summarize, the order of precedence for configuration options is: *user-wide -config file < folder-wide config file < CLI flags*. +There is also a **library-wide** config file that determines manim's default +behavior, and applies to every user of the library. It has the least +precedence, so any config options in the user-wide and any folder-wide files +will override the library-wide file. This is referred to as the *cascading* +config file system. + +.. warning:: **The user should not try to modify the library-wide file**. + Contributors should receive explicit confirmation from the core + developer team before modifying it. + + +Order of operations +******************* + +With so many different ways of configuring manim, it can be difficult to know +when each config option is being set. In fact, this will depend on how manim +is being used. + +If manim is imported from a module, then the configuration system will follow +these steps: + +1. The library-wide config file is loaded. +2. The user-wide and folder-wide files are loaded, if they exist. +3. All files found in the previous two steps are parsed in a single + :class:`ConfigParser` object, called ``parser``. This is where *cascading* + happens. +4. :class:`logging.Logger` is instantiated to create manim's global ``logger`` + object. It is configured using the "logger" section of the parser, + i.e. ``parser['logger']``. +5. :class:`ManimConfig` is instantiated to create the global ``config`` object. +6. The ``parser`` from step 3 is fed into the ``config`` from step 5 via + :meth:`ManimConfig.digest_parser`. +7. Both ``logger`` and ``config`` are exposed to the user. + +If manim is being invoked from the command line, all of the previous steps +happen, and are complemented by: + +8. The CLI flags are parsed and fed into ``config`` via + :meth:`~ManimConfig.digest_args`. +9. If the ``--config_file`` flag was used, a new :class:`ConfigParser` object + is created with the contents of the library-wide file, the user-wide file if + it exists, and the file passed via ``--config_file``. In this case, the + folder-wide file, if it exists, is ignored. +10. The new parser is fed into ``config``. +11. The rest of the CLI flags are processed. + +To summarize, the order of precedence for configuration options, from lowest to +highest precedence, is: + +1. Library-wide config file, +2. user-wide config file, if it exists, +3. folder-wide config file, if it exists OR custom config file, if passed via + ``--config_file``, +4. other CLI flags, and +5. any programmatic changes made after the config system is set. + + +A list of all config options +**************************** + +.. testcode:: + :hide: + + from manim._config import ManimConfig + from inspect import getmembers + sorted([n for n, _ in getmembers(ManimConfig, lambda v: isinstance(v, property))]) + +.. testoutput:: + :options: -ELLIPSIS, +NORMALIZE_WHITESPACE + + ['aspect_ratio', 'background_color', 'background_opacity', 'bottom', + 'custom_folders', 'disable_caching', 'dry_run', 'ffmpeg_loglevel', + 'flush_cache', 'frame_height', 'frame_rate', 'frame_size', 'frame_width', + 'frame_x_radius', 'frame_y_radius', 'from_animation_number', 'images_dir', + 'input_file', 'js_renderer_path', 'leave_progress_bars', 'left_side', + 'log_dir', 'log_to_file', 'max_files_cached', 'media_dir', + 'movie_file_extension', 'output_file', 'partial_movie_dir', 'pixel_height', + 'pixel_width', 'png_mode', 'preview', 'progress_bar', 'quality', + 'right_side', 'save_as_gif', 'save_last_frame', 'save_pngs', 'scene_names', + 'show_in_file_browser', 'skip_animations', 'sound', 'tex_dir', + 'tex_template', 'tex_template_file', 'text_dir', 'top', 'transparent', + 'upto_animation_number', 'use_js_renderer', 'verbosity', 'video_dir', + 'write_all', 'write_to_movie'] + + +A list of all CLI flags +*********************** + +.. testcode:: + :hide: + + import subprocess + result = subprocess.run(['manim', '-h'], stdout=subprocess.PIPE) + print(result.stdout.decode('utf-8')) + +.. testoutput:: + :options: -ELLIPSIS, +NORMALIZE_WHITESPACE + usage: manim [-h] [-o OUTPUT_FILE] [-p] [-f] [--leave_progress_bars] [-a] [-w] [-s] [-g] [-i] [--disable_caching] [--flush_cache] [--log_to_file] [-c BACKGROUND_COLOR] + [--background_opacity BACKGROUND_OPACITY] [--media_dir MEDIA_DIR] [--log_dir LOG_DIR] [--tex_template TEX_TEMPLATE] [--dry_run] [-t] [-q {k,p,h,m,l}] + [--low_quality] [--medium_quality] [--high_quality] [--production_quality] [--fourk_quality] [-l] [-m] [-e] [-k] [-r RESOLUTION] [-n FROM_ANIMATION_NUMBER] + [--use_js_renderer] [--js_renderer_path JS_RENDERER_PATH] [--config_file CONFIG_FILE] [--custom_folders] [-v {DEBUG,INFO,WARNING,ERROR,CRITICAL}] + [--progress_bar True/False] + {cfg} ... file [scene_names [scene_names ...]] -.. note:: There is also a **library-wide** config file that determines manim's - default behavior, and applies to every user of the library. It has - the least precedence, and **the user should not try to modify it**. - Developers should receive explicit confirmation from the core - developer team before modifying it. + Animation engine for explanatory math videos + + positional arguments: + {cfg} + file path to file holding the python code for the scene + scene_names Name of the Scene class you want to see + + optional arguments: + -h, --help show this help message and exit + -o OUTPUT_FILE, --output_file OUTPUT_FILE + Specify the name of the output file, if it should be different from the scene class name + -p, --preview Automatically open the saved file once its done + -f, --show_in_file_browser + Show the output file in the File Browser + --leave_progress_bars + Leave progress bars displayed in terminal + -a, --write_all Write all the scenes from a file + -w, --write_to_movie Render the scene as a movie file (this is on by default) + -s, --save_last_frame + Save the last frame only (no movie file is generated) + -g, --save_pngs Save each frame as a png + -i, --save_as_gif Save the video as gif + --disable_caching Disable caching (will generate partial-movie-files anyway) + --flush_cache Remove all cached partial-movie-files + --log_to_file Log terminal output to file + -c BACKGROUND_COLOR, --background_color BACKGROUND_COLOR + Specify background color + --background_opacity BACKGROUND_OPACITY + Specify background opacity + --media_dir MEDIA_DIR + Directory to store media (including video files) + --log_dir LOG_DIR Directory to store log files + --tex_template TEX_TEMPLATE + Specify a custom TeX template file + --dry_run Do a dry run (render scenes but generate no output files) + -t, --transparent Render a scene with an alpha channel + -q {k,p,h,m,l}, --quality {k,p,h,m,l} + Render at specific quality, short form of the --*_quality flags + --low_quality Render at low quality + --medium_quality Render at medium quality + --high_quality Render at high quality + --production_quality Render at default production quality + --fourk_quality Render at 4K quality + -l DEPRECATED: USE -ql or --quality l + -m DEPRECATED: USE -qm or --quality m + -e DEPRECATED: USE -qh or --quality h + -k DEPRECATED: USE -qk or --quality k + -r RESOLUTION, --resolution RESOLUTION + Resolution, passed as "height,width". Overrides any quality flags, if present + -n FROM_ANIMATION_NUMBER, --from_animation_number FROM_ANIMATION_NUMBER + Start rendering at the specified animation index, instead of the first animation. If you pass in two comma separated values, e.g. '3,6', it will end + the rendering at the second value + --use_js_renderer Render animations using the javascript frontend + --js_renderer_path JS_RENDERER_PATH + Path to the javascript frontend + --config_file CONFIG_FILE + Specify the configuration file + --custom_folders Use the folders defined in the [custom_folders] section of the config file to define the output folder structure + -v {DEBUG,INFO,WARNING,ERROR,CRITICAL}, --verbosity {DEBUG,INFO,WARNING,ERROR,CRITICAL} + Verbosity level. Also changes the ffmpeg log level unless the latter is specified in the config + --progress_bar True/False + Display the progress bar + + Made with <3 by the manim community devs diff --git a/logo/banner.png b/logo/cropped.png similarity index 100% rename from logo/banner.png rename to logo/cropped.png diff --git a/logo/dark/dark_background.png b/logo/dark/dark_background.png deleted file mode 100644 index 7f14b6359d..0000000000 Binary files a/logo/dark/dark_background.png and /dev/null differ diff --git a/logo/dark/dark_background.svg b/logo/dark/dark_background.svg index 4f25d2fc2b..f3aa92c909 100644 --- a/logo/dark/dark_background.svg +++ b/logo/dark/dark_background.svg @@ -7,41 +7,36 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - id="svg2051" + width="210mm" + height="297mm" + viewBox="0 0 210 297" version="1.1" - viewBox="0 0 145.87048 99.641436" - height="99.641434mm" - width="145.87048mm" - sodipodi:docname="logoB.svg" - inkscape:version="0.92.4 (5da689c313, 2019-01-14)"> + id="svg208" + inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)" + sodipodi:docname="dark.svg"> + - + inkscape:window-width="1915" + inkscape:window-height="2011" + inkscape:window-x="48" + inkscape:window-y="93" + inkscape:window-maximized="1" /> + id="metadata205"> @@ -52,34 +47,67 @@ - - - - - + + + + + + + + + diff --git a/logo/dark/transparent_background.png b/logo/dark/transparent_background.png deleted file mode 100644 index 392f1d9f77..0000000000 Binary files a/logo/dark/transparent_background.png and /dev/null differ diff --git a/logo/dark/transparent_background.svg b/logo/dark/transparent_background.svg index 86d3ee46e5..9ae1cef73b 100644 --- a/logo/dark/transparent_background.svg +++ b/logo/dark/transparent_background.svg @@ -7,41 +7,36 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - id="svg2051" + width="210mm" + height="297mm" + viewBox="0 0 210 297" version="1.1" - viewBox="0 0 147.36719 99.641436" - height="99.641434mm" - width="147.36719mm" - sodipodi:docname="logoBAlpha.svg" - inkscape:version="0.92.4 (5da689c313, 2019-01-14)"> + id="svg208" + inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)" + sodipodi:docname="dark_t.svg"> + - + inkscape:window-width="1915" + inkscape:window-height="1018" + inkscape:window-x="48" + inkscape:window-y="93" + inkscape:window-maximized="1" /> + id="metadata205"> @@ -52,27 +47,59 @@ - - - - + + + + + + + + diff --git a/logo/light/light_background.png b/logo/light/light_background.png deleted file mode 100644 index abbce96d54..0000000000 Binary files a/logo/light/light_background.png and /dev/null differ diff --git a/logo/light/light_background.svg b/logo/light/light_background.svg index 34ba8e2f38..1c3033b03a 100644 --- a/logo/light/light_background.svg +++ b/logo/light/light_background.svg @@ -7,41 +7,36 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - id="svg2051" + width="210mm" + height="297mm" + viewBox="0 0 210 297" version="1.1" - viewBox="0 0 155.19392 99.666109" - height="99.666107mm" - width="155.19392mm" - sodipodi:docname="logoW.svg" - inkscape:version="0.92.4 (5da689c313, 2019-01-14)"> + id="svg208" + inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)" + sodipodi:docname="light.svg"> + - + inkscape:window-width="1915" + inkscape:window-height="1018" + inkscape:window-x="48" + inkscape:window-y="93" + inkscape:window-maximized="1" /> + id="metadata205"> @@ -52,34 +47,67 @@ - - - - - + + + + + + + + + diff --git a/logo/light/transparent_background.png b/logo/light/transparent_background.png deleted file mode 100644 index 42b6a319d9..0000000000 Binary files a/logo/light/transparent_background.png and /dev/null differ diff --git a/logo/light/transparent_background.svg b/logo/light/transparent_background.svg index 83a8fe0d73..2fb30170b7 100644 --- a/logo/light/transparent_background.svg +++ b/logo/light/transparent_background.svg @@ -7,41 +7,36 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - id="svg2051" + width="210mm" + height="297mm" + viewBox="0 0 210 297" version="1.1" - viewBox="0 0 155.19392 99.666109" - height="99.666107mm" - width="155.19392mm" - sodipodi:docname="logoWAlpha.svg" - inkscape:version="0.92.4 (5da689c313, 2019-01-14)"> + id="svg208" + inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)" + sodipodi:docname="light_t.svg"> + - + inkscape:window-width="1915" + inkscape:window-height="1018" + inkscape:window-x="48" + inkscape:window-y="93" + inkscape:window-maximized="1" /> + id="metadata205"> @@ -52,27 +47,59 @@ - - - - + + + + + + + + diff --git a/logo/light_and_dark.png b/logo/light_and_dark.png deleted file mode 100644 index fac294c6ca..0000000000 Binary files a/logo/light_and_dark.png and /dev/null differ diff --git a/logo/light_and_dark.svg b/logo/light_and_dark.svg deleted file mode 100644 index ff69c45000..0000000000 --- a/logo/light_and_dark.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/manim/__init__.py b/manim/__init__.py index d54257be27..df7322a69f 100644 --- a/manim/__init__.py +++ b/manim/__init__.py @@ -2,7 +2,7 @@ # Importing the config module should be the first thing we do, since other # modules depend on the global config dict for initialization. -from .config import * +from ._config import * from .constants import * @@ -17,7 +17,6 @@ from .animation.movement import * from .animation.numbers import * from .animation.rotation import * -from .animation.specialized import * from .animation.transform import * from .animation.update import * @@ -34,6 +33,7 @@ from .mobject.frame import * from .mobject.functions import * from .mobject.geometry import * +from .mobject.logo import * from .mobject.matrix import * from .mobject.mobject import * from .mobject.number_line import * @@ -41,7 +41,6 @@ from .mobject.probability import * from .mobject.shape_matchers import * from .mobject.svg.brace import * -from .mobject.svg.drawings import * from .mobject.svg.svg_mobject import * from .mobject.svg.tex_mobject import * from .mobject.svg.text_mobject import * @@ -58,7 +57,11 @@ from .scene.graph_scene import * from .scene.moving_camera_scene import * from .scene.reconfigurable_scene import * -from .scene.js_scene import * + +try: + from .scene.js_scene import * +except ModuleNotFoundError: + pass # optional deps from .scene.scene import * from .scene.sample_space_scene import * from .scene.three_d_scene import * diff --git a/manim/__main__.py b/manim/__main__.py index 2ea9593d43..160fc48062 100644 --- a/manim/__main__.py +++ b/manim/__main__.py @@ -1,74 +1,83 @@ import os -import platform import sys import traceback -from . import logger, file_writer_config -from .config.config import camera_config, args -from .config import cfg_subcmds -from .utils.module_ops import ( +from manim import logger, config +from manim.utils.module_ops import ( get_module, get_scene_classes_from_module, get_scenes_to_render, ) -from .utils.file_ops import open_file as open_media_file -from .grpc.impl import frame_server_impl +from manim.utils.file_ops import open_file as open_media_file +from manim._config.main_utils import parse_args + +try: + from manim.grpc.impl import frame_server_impl +except ImportError: + frame_server_impl = None def open_file_if_needed(file_writer): - if file_writer_config["verbosity"] != "DEBUG": + if config["verbosity"] != "DEBUG": curr_stdout = sys.stdout sys.stdout = open(os.devnull, "w") - open_file = any( - [file_writer_config["preview"], file_writer_config["show_in_file_browser"]] - ) + open_file = any([config["preview"], config["show_in_file_browser"]]) + if open_file: - current_os = platform.system() file_paths = [] - if file_writer_config["save_last_frame"]: - file_paths.append(file_writer.get_image_file_path()) - if ( - file_writer_config["write_to_movie"] - and not file_writer_config["save_as_gif"] - ): - file_paths.append(file_writer.get_movie_file_path()) - if file_writer_config["save_as_gif"]: + if config["save_last_frame"]: + file_paths.append(file_writer.image_file_path) + if config["write_to_movie"] and not config["save_as_gif"]: + file_paths.append(file_writer.movie_file_path) + if config["save_as_gif"]: file_paths.append(file_writer.gif_file_path) for file_path in file_paths: - if file_writer_config["show_in_file_browser"]: + if config["show_in_file_browser"]: open_media_file(file_path, True) - if file_writer_config["preview"]: + if config["preview"]: open_media_file(file_path, False) - if file_writer_config["verbosity"] != "DEBUG": + if config["verbosity"] != "DEBUG": sys.stdout.close() sys.stdout = curr_stdout def main(): - if hasattr(args, "subcommands"): - if "cfg" in args.subcommands: - if args.cfg_subcommand is not None: - subcommand = args.cfg_subcommand - if subcommand == "write": + args = parse_args(sys.argv) + + if hasattr(args, "cmd"): + if args.cmd == "cfg": + if args.subcmd: + from manim._config import cfg_subcmds + + if args.subcmd == "write": cfg_subcmds.write(args.level, args.open) - elif subcommand == "show": + elif args.subcmd == "show": cfg_subcmds.show() - elif subcommand == "export": + elif args.subcmd == "export": cfg_subcmds.export(args.dir) else: - logger.error("No argument provided; Exiting...") + logger.error("No subcommand provided; Exiting...") + + # elif args.cmd == "some_other_cmd": + # something_else_here() else: - module = get_module(file_writer_config["input_file"]) + config.digest_args(args) + + module = get_module(config.get_dir("input_file")) all_scene_classes = get_scene_classes_from_module(module) scene_classes_to_render = get_scenes_to_render(all_scene_classes) for SceneClass in scene_classes_to_render: try: - if camera_config["use_js_renderer"]: + if config["use_js_renderer"]: + if frame_server_impl is None: + raise ImportError( + "Dependencies for JS renderer is not installed." + ) frame_server_impl.get(SceneClass).start() else: scene = SceneClass() diff --git a/manim/_config/__init__.py b/manim/_config/__init__.py new file mode 100644 index 0000000000..7dbf322346 --- /dev/null +++ b/manim/_config/__init__.py @@ -0,0 +1,75 @@ +"""Set the global config and logger.""" + +import logging +from contextlib import contextmanager + +from .logger_utils import make_logger +from .utils import make_config_parser, ManimConfig, ManimFrame + +__all__ = [ + "logger", + "console", + "config", + "frame", + "tempconfig", +] + +parser = make_config_parser() + +# The logger can be accessed from anywhere as manim.logger, or as +# logging.getLogger("manim"). The console must be accessed as manim.console. +# Throughout the codebase, use manim.console.print() instead of print(). +logger, console = make_logger(parser["logger"], parser["CLI"]["verbosity"]) +# TODO: temporary to have a clean terminal output when working with PIL or matplotlib +logging.getLogger("PIL").setLevel(logging.INFO) +logging.getLogger("matplotlib").setLevel(logging.INFO) + +config = ManimConfig().digest_parser(parser) +frame = ManimFrame(config) + + +# This has to go here because it needs access to this module's config +@contextmanager +def tempconfig(temp): + """Context manager that temporarily modifies the global ``config`` object. + + Inside the ``with`` statement, the modified config will be used. After + context manager exits, the config will be restored to its original state. + + Parameters + ---------- + temp : Union[:class:`ManimConfig`, :class:`dict`] + Object whose keys will be used to temporarily update the global + ``config``. + + Examples + -------- + + Use ``with tempconfig({...})`` to temporarily change the default values of + certain config options. + + .. code-block:: python + + >>> config['frame_height'] + 8.0 + >>> with tempconfig({'frame_height': 100.0}): + ... print(config['frame_height']) + 100.0 + >>> config['frame_height'] + 8.0 + + """ + global config + original = config.copy() + + temp = {k: v for k, v in temp.items() if k in original} + + # In order to change the config that every module has acces to, use + # update(), DO NOT use assignment. Assigning config = some_dict will just + # make the local variable named config point to a new dictionary, it will + # NOT change the dictionary that every module has a reference to. + config.update(temp) + try: + yield + finally: + config.update(original) # update, not assignment! diff --git a/manim/config/cfg_subcmds.py b/manim/_config/cfg_subcmds.py similarity index 85% rename from manim/config/cfg_subcmds.py rename to manim/_config/cfg_subcmds.py index f5e457f148..6427836de7 100644 --- a/manim/config/cfg_subcmds.py +++ b/manim/_config/cfg_subcmds.py @@ -6,24 +6,22 @@ The functions below can be called via the `manim cfg` subcommand. """ + import os -import configparser from ast import literal_eval -from .config_utils import _run_config, _paths_config_file, finalized_configs_dict -from ..utils.file_ops import guarantee_existence, open_file - -from rich.console import Console -from rich.style import Style -from rich.errors import StyleSyntaxError +from manim import console, config +from manim._config.utils import config_file_paths, make_config_parser +from manim.utils.file_ops import guarantee_existence, open_file __all__ = ["write", "show", "export"] -RICH_COLOUR_INSTRUCTIONS = """[red]The default colour is used by the input statement. +RICH_COLOUR_INSTRUCTIONS = """ +[red]The default colour is used by the input statement. If left empty, the default colour will be used.[/red] -[magenta] For a full list of styles, visit[/magenta] [green]https://rich.readthedocs.io/en/latest/style.html[/green]""" +[magenta] For a full list of styles, visit[/magenta] [green]https://rich.readthedocs.io/en/latest/style.html[/green] +""" RICH_NON_STYLE_ENTRIES = ["log.width", "log.height", "log.timestamps"] -console = Console() def value_from_string(value): @@ -114,8 +112,7 @@ def replace_keys(default): def write(level=None, openfile=False): - config = _run_config()[1] - config_paths = _paths_config_file() + [os.path.abspath("manim.cfg")] + config_paths = config_file_paths() console.print( "[yellow bold]Manim Configuration File Writer[/yellow bold]", justify="center" ) @@ -127,11 +124,12 @@ def write(level=None, openfile=False): CWD_CONFIG_MSG = f"""A configuration file at [yellow]{config_paths[2]}[/yellow] has been created. To save your config please save that file and place it in your current working directory, from where you run the manim command.""" + parser = make_config_parser() if not openfile: action = "save this as" - for category in config: + for category in parser: console.print(f"{category}", style="bold green underline") - default = config[category] + default = parser[category] if category == "logger": console.print(RICH_COLOUR_INSTRUCTIONS) default = replace_keys(default) @@ -178,7 +176,7 @@ def write(level=None, openfile=False): default = replace_keys(default) if category == "logger" else default - config[category] = dict(default) + parser[category] = dict(default) else: action = "open" @@ -194,40 +192,37 @@ def write(level=None, openfile=False): action_to_userpath = "" if action_to_userpath.lower() == "y" or level == "user": - cfg_file_path = os.path.join( - guarantee_existence(os.path.dirname(config_paths[1])), "manim.cfg" - ) + cfg_file_path = config_paths[1] + guarantee_existence(config_paths[1].parents[0]) console.print(USER_CONFIG_MSG) else: - cfg_file_path = os.path.join( - guarantee_existence(os.path.dirname(config_paths[2])), "manim.cfg" - ) + cfg_file_path = config_paths[2] + guarantee_existence(config_paths[2].parents[0]) console.print(CWD_CONFIG_MSG) with open(cfg_file_path, "w") as fp: - config.write(fp) + parser.write(fp) if openfile: open_file(cfg_file_path) def show(): - current_config = finalized_configs_dict() + parser = make_config_parser() rich_non_style_entries = [a.replace(".", "_") for a in RICH_NON_STYLE_ENTRIES] - for category in current_config: + for category in parser: console.print(f"{category}", style="bold green underline") - for entry in current_config[category]: + for entry in parser[category]: if category == "logger" and entry not in rich_non_style_entries: console.print(f"{entry} :", end="") console.print( - f" {current_config[category][entry]}", - style=current_config[category][entry], + f" {parser[category][entry]}", + style=parser[category][entry], ) else: - console.print(f"{entry} : {current_config[category][entry]}") + console.print(f"{entry} : {parser[category][entry]}") console.print("\n") def export(path): - config = _run_config()[1] if os.path.abspath(path) == os.path.abspath(os.getcwd()): console.print( """You are reading the config from the same directory you are exporting to. diff --git a/manim/config/default.cfg b/manim/_config/default.cfg similarity index 63% rename from manim/config/default.cfg rename to manim/_config/default.cfg index cc4d7be3ac..7647738f76 100644 --- a/manim/config/default.cfg +++ b/manim/_config/default.cfg @@ -68,21 +68,21 @@ upto_animation_number = -1 # Set them at the CLI with the corresponding flag, or set their default # values here. -# --media_dir. Note that video files will be put in a folder below media_dir, -# as well as Tex and texts. +# --media_dir media_dir = ./media -# --log_dir (by default "/logs", that will be put inside the media dir) -log_dir = logs +# --log_dir +log_dir = {media_dir}/logs -# # --video_dir -# video_dir = %(MEDIA_DIR)s/videos +# --assets_dir +assets_dir = ./ -# # --tex_dir -# tex_dir = %(MEDIA_DIR)s/Tex - -# # --text_dir -# text_dir = %(MEDIA_DIR)s/texts +# the following do not have CLI arguments but depend on media_dir +video_dir = {media_dir}/videos/{module_name}/{quality} +images_dir = {media_dir}/images/{module_name} +tex_dir = {media_dir}/Tex +text_dir = {media_dir}/texts +partial_movie_dir = {video_dir}/partial_movie_files/{scene_name} # --use_js_renderer use_js_renderer = False @@ -112,68 +112,18 @@ disable_caching = False # --tex_template tex_template = -# These override the previous by using -t, --transparent -[transparent] -png_mode = RGBA -movie_file_extension = .mov -background_opacity = 0 - -# These override the previous by using -k, --four_k -[fourk_quality] -pixel_height = 2160 -pixel_width = 3840 -frame_rate = 60 - -# These override the previous by using -e, --high_quality -[high_quality] -pixel_height = 1440 -pixel_width = 2560 -frame_rate = 60 - -# These override the previous by using -m, --medium_quality -[medium_quality] -pixel_height = 720 -pixel_width = 1280 -frame_rate = 30 - -# These override the previous by using -l, --low_quality -[low_quality] -pixel_height = 480 -pixel_width = 854 -frame_rate = 15 - -# These override the previous by using --dry_run -# Note --dry_run overrides all of -w, -a, -s, -g, -i -[dry_run] -write_to_movie = False -write_all = False -save_last_frame = False -save_pngs = False -save_as_gif = False - -# Streaming settings -[streaming] -live_stream_name = LiveStream -twitch_stream_key = YOUR_STREAM_KEY -streaming_protocol = tcp -streaming_ip = 127.0.0.1 -streaming_port = 2000 -streaming_client = ffplay -streaming_url = %(STREAMING_PROTOCOL)s://%(STREAMING_IP)s:%(STREAMING_PORT)s?listen -streaming_console_banner = Manim is now running in streaming mode. - Stream animations by passing them to manim.play(), e.g. - >>> c = Circle() - >>> manim.play(ShowCreation(c)) # Overrides the default output folders, NOT the output file names. Note that # if the custom_folders flag is present, the Tex and text files will not be put # under media_dir, as is the default. [custom_folders] media_dir = videos -video_dir = %(media_dir)s -images_dir = %(media_dir)s -text_dir = %(media_dir)s/temp_files -tex_dir = %(media_dir)s/temp_files +video_dir = {media_dir} +images_dir = {media_dir} +text_dir = {media_dir}/temp_files +tex_dir = {media_dir}/temp_files +log_dir = {media_dir}/temp_files +partial_movie_dir = {media_dir}/partial_movie_files/{scene_name} # Rich settings [logger] @@ -193,5 +143,6 @@ log_height = -1 log_timestamps = True [ffmpeg] -# Uncomment the following line to manually set the loglevel for ffmpeg. See ffmpeg manpage for accepted values -# loglevel = error +# Uncomment the following line to manually set the loglevel for ffmpeg. See +# ffmpeg manpage for accepted values +loglevel = ERROR diff --git a/manim/_config/logger_utils.py b/manim/_config/logger_utils.py new file mode 100644 index 0000000000..82d83132f2 --- /dev/null +++ b/manim/_config/logger_utils.py @@ -0,0 +1,194 @@ +"""Utilities to create and set the logger. + +Manim's logger can be accessed as ``manim.logger``, or as +``logging.getLogger("manim")``, once the library has been imported. Manim also +exports a second object, ``console``, which should be used to print on screen +messages that need not be logged. + +Both ``logger`` and ``console`` use the ``rich`` library to produce rich text +format. + +""" + +import os +import logging +import json +import copy + +from rich.console import Console +from rich.logging import RichHandler +from rich.theme import Theme +from rich import print as printf +from rich import errors, color + +HIGHLIGHTED_KEYWORDS = [ # these keywords are highlighted specially + "Played", + "animations", + "scene", + "Reading", + "Writing", + "script", + "arguments", + "Invalid", + "Aborting", + "module", + "File", + "Rendering", + "Rendered", +] + +WRONG_COLOR_CONFIG_MSG = """ +[logging.level.error]Your colour configuration couldn't be parsed. +Loading the default color configuration.[/logging.level.error] +""" + + +def make_logger(parser, verbosity): + """Make the manim logger and console. + + Parameters + ---------- + parser : :class:`configparser.ConfigParser` + A parser containing any .cfg files in use. + + verbosity : :class:`str` + The verbosity level of the logger. + + Returns + ------- + :class:`logging.Logger`, :class:`rich.Console` + The manim logger and console. Both use the theme returned by + :func:`parse_theme` + + See Also + -------- + :func:`~._config.utils.make_config_parser`, :func:`parse_theme` + + Notes + ----- + The ``parser`` is assumed to contain only the options related to + configuring the logger at the top level. + + """ + # Throughout the codebase, use Console.print() instead of print() + theme = parse_theme(parser) + console = Console(theme=theme) + + # set the rich handler + RichHandler.KEYWORDS = HIGHLIGHTED_KEYWORDS + rich_handler = RichHandler( + console=console, show_time=parser.getboolean("log_timestamps") + ) + + # finally, the logger + logger = logging.getLogger("manim") + logger.addHandler(rich_handler) + logger.setLevel(verbosity) + + return logger, console + + +def parse_theme(parser): + """Configure the rich style of logger and console output. + + Parameters + ---------- + parser : :class:`configparser.ConfigParser` + A parser containing any .cfg files in use. + + Returns + ------- + :class:`rich.Theme` + The rich theme to be used by the manim logger. + + See Also + -------- + :func:`make_logger`. + + """ + theme = {key.replace("_", "."): parser[key] for key in parser} + + theme["log.width"] = None if theme["log.width"] == "-1" else int(theme["log.width"]) + theme["log.height"] = ( + None if theme["log.height"] == "-1" else int(theme["log.height"]) + ) + theme["log.timestamps"] = False + try: + custom_theme = Theme( + { + k: v + for k, v in theme.items() + if k not in ["log.width", "log.height", "log.timestamps"] + } + ) + except (color.ColorParseError, errors.StyleSyntaxError): + printf(WRONG_COLOR_CONFIG_MSG) + custom_theme = None + + return custom_theme + + +def set_file_logger(config, verbosity): + """Add a file handler to manim logger. + + The path to the file is built using ``config.log_dir``. + + Parameters + ---------- + config : :class:`ManimConfig` + The global config, used to determine the log file path. + + verbosity : :class:`str` + The verbosity level of the logger. + + Notes + ----- + Calling this function changes the verbosity of all handlers assigned to + manim logger. + + """ + # Note: The log file name will be + # _.log, gotten from config. So it + # can differ from the real name of the scene. would only + # appear if scene name was provided when manim was called. + scene_name_suffix = "".join(config["scene_names"]) + scene_file_name = os.path.basename(config["input_file"]).split(".")[0] + log_file_name = ( + f"{scene_file_name}_{scene_name_suffix}.log" + if scene_name_suffix + else f"{scene_file_name}.log" + ) + log_file_path = config.get_dir("log_dir") / log_file_name + log_file_path.parent.mkdir(parents=True, exist_ok=True) + + file_handler = logging.FileHandler(log_file_path, mode="w") + file_handler.setFormatter(JSONFormatter()) + + logger = logging.getLogger("manim") + logger.addHandler(file_handler) + logger.info("Log file will be saved in %(logpath)s", {"logpath": log_file_path}) + + config.verbosity = verbosity + logger.setLevel(verbosity) + + +class JSONFormatter(logging.Formatter): + """A formatter that outputs logs in a custom JSON format. + + This class is used internally for testing purposes. + + """ + + def format(self, record): + """Format the record in a custom JSON format.""" + record_c = copy.deepcopy(record) + if record_c.args: + for arg in record_c.args: + record_c.args[arg] = "<>" + return json.dumps( + { + "levelname": record_c.levelname, + "module": record_c.module, + "message": super().format(record_c), + } + ) diff --git a/manim/_config/main_utils.py b/manim/_config/main_utils.py new file mode 100644 index 0000000000..a977e50e66 --- /dev/null +++ b/manim/_config/main_utils.py @@ -0,0 +1,433 @@ +"""Utilities called from ``__main__.py`` to interact with the config.""" + +import os +import argparse + +from manim import constants + + +__all__ = ["parse_args"] + + +def _find_subcommand(args): + """Return the subcommand that has been passed, if any. + + Parameters + ---------- + args : list + The argument list. + + Returns + ------- + Optional[:class:`str`] + If a subcommand is found, returns the string of its name. Returns None + otherwise. + + Notes + ----- + This assumes that "manim" is the first word in the argument list, and that + the subcommand will be the second word, if it exists. + + """ + subcmd = args[1] + if subcmd in [ + "cfg" + # , 'init', + ]: + return subcmd + else: + return None + + +def _init_cfg_subcmd(subparsers): + """Initialises the subparser for the `cfg` subcommand. + + Parameters + ---------- + subparsers : :class:`argparse._SubParsersAction` + The subparser object for which to add the sub-subparser for the cfg subcommand. + + Returns + ------- + :class:`argparse.ArgumentParser` + The parser that parser anything cfg subcommand related. + """ + cfg_related = subparsers.add_parser("cfg") + cfg_subparsers = cfg_related.add_subparsers(dest="cfg_subcommand") + + cfg_write_parser = cfg_subparsers.add_parser("write") + cfg_write_parser.add_argument( + "--level", + choices=["user", "cwd"], + default=None, + help="Specify if this config is for user or just the working directory.", + ) + cfg_write_parser.add_argument( + "--open", action="store_const", const=True, default=False + ) + cfg_subparsers.add_parser("show") + + cfg_export_parser = cfg_subparsers.add_parser("export") + cfg_export_parser.add_argument("--dir", default=os.getcwd()) + + return cfg_related + + +def _str2bool(s): + """Helper function that handles boolean CLI arguments.""" + if s == "True": + return True + elif s == "False": + return False + else: + raise argparse.ArgumentTypeError("True or False expected") + + +def parse_args(args): + """Parse CLI arguments. + + Parameters + ---------- + args : :class:`list` + A list of arguments; generally, this should be ``sys.argv``. + + Returns + ------- + :class:`argparse.Namespace` + An object returned by ``argparse.parse_args``. + + """ + if args[0] == "python" and args[1] == "-m": + args = args[2:] + + if len(args) == 1: + return _parse_args_no_subcmd(args) + + subcmd = _find_subcommand(args) + if subcmd == "cfg": + return _parse_args_cfg_subcmd(args) + # elif subcmd == some_other_future_subcmd: + # return _parse_args_some_other_subcmd(args) + elif subcmd is None: + return _parse_args_no_subcmd(args) + + +def _parse_args_cfg_subcmd(args): + """Parse arguments of the form 'manim cfg '.""" + parser = argparse.ArgumentParser( + description="Animation engine for explanatory math videos", + prog="manim cfg", + epilog="Made with <3 by the manim community devs", + ) + subparsers = parser.add_subparsers(help="subcommand", dest="subcmd") + + cfg_subparsers = { + subcmd: subparsers.add_parser(subcmd) for subcmd in ["write", "show", "export"] + } + + # Arguments for the write subcmd + cfg_subparsers["write"].add_argument( + "--level", + choices=["user", "cwd"], + default="cwd", + help="Specify if this config is for user or the working directory.", + ) + cfg_subparsers["write"].add_argument( + "--open", action="store_const", const=True, default=False + ) + + # Arguments for the export subcmd + cfg_subparsers["export"].add_argument("--dir", default=os.getcwd()) + + # Arguments for the show subcmd: currently no arguments + + # Recall the argument list looks like 'manim cfg ' so we + # only need to parse the remaining args + parsed = parser.parse_args(args[2:]) + parsed.cmd = "cfg" + parsed.cfg_subcommand = parsed.subcmd + + return parsed + + +def _parse_args_no_subcmd(args): + """Parse arguments of the form 'manim ', when no command is present.""" + parser = argparse.ArgumentParser( + description="Animation engine for explanatory math videos", + prog="manim", + usage=( + "%(prog)s file [flags] [scene [scene ...]]\n" + " %(prog)s {cfg,init} [opts]" + ), + epilog="Made with <3 by the manim community devs", + ) + + parser.add_argument( + "file", + help="Path to file holding the python code for the scene", + ) + parser.add_argument( + "scene_names", + nargs="*", + help="Name of the Scene class you want to see", + default=[""], + ) + parser.add_argument( + "-o", + "--output_file", + help="Specify the name of the output file, if " + "it should be different from the scene class name", + default="", + ) + + # The following use (action='store_const', const=True) instead of + # the built-in (action='store_true'). This is because the latter + # will default to False if not specified, while the former sets no + # default value. Since we want to set the default value in + # manim.cfg rather than here, we use the former. + parser.add_argument( + "-p", + "--preview", + action="store_const", + const=True, + help="Automatically open the saved file once its done", + ) + parser.add_argument( + "-f", + "--show_in_file_browser", + action="store_const", + const=True, + help="Show the output file in the File Browser", + ) + parser.add_argument( + "--sound", + action="store_const", + const=True, + help="Play a success/failure sound", + ) + parser.add_argument( + "--leave_progress_bars", + action="store_const", + const=True, + help="Leave progress bars displayed in terminal", + ) + parser.add_argument( + "-a", + "--write_all", + action="store_const", + const=True, + help="Write all the scenes from a file", + ) + parser.add_argument( + "-w", + "--write_to_movie", + action="store_const", + const=True, + help="Render the scene as a movie file (this is on by default)", + ) + parser.add_argument( + "-s", + "--save_last_frame", + action="store_const", + const=True, + help="Save the last frame only (no movie file is generated)", + ) + parser.add_argument( + "-g", + "--save_pngs", + action="store_const", + const=True, + help="Save each frame as a png", + ) + parser.add_argument( + "-i", + "--save_as_gif", + action="store_const", + const=True, + help="Save the video as gif", + ) + parser.add_argument( + "--disable_caching", + action="store_const", + const=True, + help="Disable caching (will generate partial-movie-files anyway)", + ) + parser.add_argument( + "--flush_cache", + action="store_const", + const=True, + help="Remove all cached partial-movie-files", + ) + parser.add_argument( + "--log_to_file", + action="store_const", + const=True, + help="Log terminal output to file", + ) + # The default value of the following is set in manim.cfg + parser.add_argument( + "-c", + "--background_color", + help="Specify background color", + ) + parser.add_argument( + "--media_dir", + help="Directory to store media (including video files)", + ) + parser.add_argument( + "--log_dir", + help="Directory to store log files", + ) + parser.add_argument( + "--tex_template", + help="Specify a custom TeX template file", + ) + + # All of the following use (action="store_true"). This means that + # they are by default False. In contrast to the previous ones that + # used (action="store_const", const=True), the following do not + # correspond to a single configuration option. Rather, they + # override several options at the same time. + + # The following overrides -w, -a, -g, and -i + parser.add_argument( + "--dry_run", + action="store_true", + help="Do a dry run (render scenes but generate no output files)", + ) + + # The following overrides PNG_MODE, MOVIE_FILE_EXTENSION, and + # BACKGROUND_OPACITY + parser.add_argument( + "-t", + "--transparent", + action="store_true", + help="Render a scene with an alpha channel", + ) + + # The following are mutually exclusive and each overrides + # FRAME_RATE, PIXEL_HEIGHT, and PIXEL_WIDTH, + parser.add_argument( + "-q", + "--quality", + choices=[ + constants.QUALITIES[q]["flag"] + for q in constants.QUALITIES + if constants.QUALITIES[q]["flag"] + ], + default=constants.DEFAULT_QUALITY_SHORT, + help="Render at specific quality, short form of the --*_quality flags", + ) + parser.add_argument( + "--low_quality", + action="store_true", + help="Render at low quality", + ) + parser.add_argument( + "--medium_quality", + action="store_true", + help="Render at medium quality", + ) + parser.add_argument( + "--high_quality", + action="store_true", + help="Render at high quality", + ) + parser.add_argument( + "--production_quality", + action="store_true", + help="Render at default production quality", + ) + parser.add_argument( + "--fourk_quality", + action="store_true", + help="Render at 4K quality", + ) + + # Deprecated quality flags + parser.add_argument( + "-l", + action="store_true", + help="DEPRECATED: USE -ql or --quality l", + ) + parser.add_argument( + "-m", + action="store_true", + help="DEPRECATED: USE -qm or --quality m", + ) + parser.add_argument( + "-e", + action="store_true", + help="DEPRECATED: USE -qh or --quality h", + ) + parser.add_argument( + "-k", + action="store_true", + help="DEPRECATED: USE -qk or --quality k", + ) + + # This overrides any of the above + parser.add_argument( + "-r", + "--resolution", + help='Resolution, passed as "height,width". ' + "Overrides the -l, -m, -e, and -k flags, if present", + ) + + # This sets FROM_ANIMATION_NUMBER and UPTO_ANIMATION_NUMBER + parser.add_argument( + "-n", + "--from_animation_number", + help="Start rendering at the specified animation index, " + "instead of the first animation. If you pass in two comma " + "separated values, e.g. '3,6', it will end " + "the rendering at the second value", + ) + + parser.add_argument( + "--use_js_renderer", + help="Render animations using the javascript frontend", + action="store_const", + const=True, + ) + + parser.add_argument( + "--js_renderer_path", + help="Path to the javascript frontend", + ) + + # Specify the manim.cfg file + parser.add_argument( + "--config_file", + help="Specify the configuration file", + ) + + # Specify whether to use the custom folders + parser.add_argument( + "--custom_folders", + action="store_true", + help="Use the folders defined in the [custom_folders] " + "section of the config file to define the output folder structure", + ) + + # Specify the verbosity + parser.add_argument( + "-v", + "--verbosity", + type=str, + help=( + "Verbosity level. Also changes the ffmpeg log level unless " + "the latter is specified in the config" + ), + choices=constants.VERBOSITY_CHOICES, + ) + + # Specify if the progress bar should be displayed + parser.add_argument( + "--progress_bar", + type=_str2bool, + help="Display the progress bar", + metavar="True/False", + ) + + return parser.parse_args(args[1:]) diff --git a/manim/_config/utils.py b/manim/_config/utils.py new file mode 100644 index 0000000000..14f8f4e693 --- /dev/null +++ b/manim/_config/utils.py @@ -0,0 +1,1361 @@ +"""Utilities to create and set the config. + +The main class exported by this module is :class:`ManimConfig`. This class +contains all configuration options, including frame geometry (e.g. frame +height/width, frame rate), output (e.g. directories, logging), styling +(e.g. background color, transparency), and general behavior (e.g. writing a +movie vs writing a single frame). + +See :doc:`/tutorials/configuration` for an introduction to Manim's configuration system. + +""" + +import os +import sys +import copy +import logging +import configparser +from pathlib import Path +from collections.abc import Mapping, MutableMapping + +import numpy as np +import colour + +from .. import constants +from ..utils.tex import TexTemplate, TexTemplateFromFile +from .logger_utils import set_file_logger + + +def config_file_paths(): + """The paths where ``.cfg`` files will be searched for. + + When manim is first imported, it processes any ``.cfg`` files it finds. This + function returns the locations in which these files are searched for. In + ascending order of precedence, these are: the library-wide config file, the + user-wide config file, and the folder-wide config file. + + The library-wide config file determines manim's default behavior. The + user-wide config file is stored in the user's home folder, and determines + the behavior of manim whenever the user invokes it from anywhere in the + system. The folder-wide config file only affects scenes that are in the + same folder. The latter two files are optional. + + These files, if they exist, are meant to loaded into a single + :class:`configparser.ConfigParser` object, and then processed by + :class:`ManimConfig`. + + Returns + ------- + List[:class:`Path`] + List of paths which may contain ``.cfg`` files, in ascending order of + precedence. + + See Also + -------- + :func:`make_config_parser`, :meth:`ManimConfig.digest_file`, + :meth:`ManimConfig.digest_parser` + + Notes + ----- + The location of the user-wide config file is OS-specific. + + """ + library_wide = Path.resolve(Path(__file__).parent / "default.cfg") + if sys.platform.startswith("win32"): + user_wide = Path.home() / "AppData" / "Roaming" / "Manim" / "manim.cfg" + else: + user_wide = Path.home() / ".config" / "manim" / "manim.cfg" + folder_wide = Path("manim.cfg") + return [library_wide, user_wide, folder_wide] + + +def make_config_parser(custom_file=None): + """Make a :class:`ConfigParser` object and load any ``.cfg`` files. + + The user-wide file, if it exists, overrides the library-wide file. The + folder-wide file, if it exists, overrides the other two. + + The folder-wide file can be ignored by passing ``custom_file``. However, + the user-wide and library-wide config files cannot be ignored. + + Parameters + ---------- + custom_file : :class:`str` + Path to a custom config file. If used, the folder-wide file in the + relevant directory will be ignored, if it exists. If None, the + folder-wide file will be used, if it exists. + + Returns + ------- + :class:`ConfigParser` + A parser containing the config options found in the .cfg files that + were found. It is guaranteed to contain at least the config options + found in the library-wide file. + + See Also + -------- + :func:`config_file_paths` + + """ + library_wide, user_wide, folder_wide = config_file_paths() + # From the documentation: "An application which requires initial values to + # be loaded from a file should load the required file or files using + # read_file() before calling read() for any optional files." + # https://docs.python.org/3/library/configparser.html#configparser.ConfigParser.read + parser = configparser.ConfigParser() + with open(library_wide) as file: + parser.read_file(file) # necessary file + + other_files = [user_wide, custom_file if custom_file else folder_wide] + parser.read(other_files) # optional files + + return parser + + +def _determine_quality(args): + old_qualities = { + "k": "fourk_quality", + "e": "high_quality", + "m": "medium_quality", + "l": "low_quality", + } + + for quality in constants.QUALITIES: + if quality == constants.DEFAULT_QUALITY: + # Skip so we prioritize anything that overwrites the default quality. + pass + elif getattr(args, quality, None) or ( + hasattr(args, "quality") + and args.quality is not None + and args.quality == constants.QUALITIES[quality]["flag"] + ): + return quality + + for quality in old_qualities: + if getattr(args, quality, None): + logging.getLogger("manim").warning( + f"Option -{quality} is deprecated please use the --quality/-q flag." + ) + return old_qualities[quality] + + return constants.DEFAULT_QUALITY + + +class ManimConfig(MutableMapping): + """Dict-like class storing all config options. + + The global ``config`` object is an instance of this class, and acts as a + single source of truth for all of the library's customizable behavior. + + The global ``config`` object is capable of digesting different types of + sources and converting them into a uniform interface. These sources are + (in ascending order of precedence): configuration files, command line + arguments, and programmatic changes. Regardless of how the user chooses to + set a config option, she can access its current value using + :class:`ManimConfig`'s attributes and properties. + + Notes + ----- + Each config option is implemented as a property of this class. + + Each config option can be set via a config file, using the full name of the + property. If a config option has an associated CLI flag, then the flag is + equal to the full name of the property. Those that admit an alternative + flag or no flag at all are documented in the individual property's + docstring. + + Examples + -------- + Each config option allows for dict syntax and attribute syntax. For + example, the following two lines are equivalent, + + .. code-block:: python + + >>> from manim import config, WHITE + >>> config.background_color = WHITE + >>> config['background_color'] = WHITE + + The former is preferred; the latter is provided mostly for backwards + compatibility. + + The config options are designed to keep internal consistency. For example, + setting ``frame_y_radius`` will affect ``frame_height``: + + .. code-block:: python + + >>> config.frame_height + 8.0 + >>> config.frame_y_radius = 5.0 + >>> config.frame_height + 10.0 + + There are many ways of interacting with config options. Take for example + the config option ``background_color``. There are three ways to change it: + via a config file, via CLI flags, or programmatically. + + To set the background color via a config file, save the following + ``manim.cfg`` file with the following contents. + + .. code-block:: + + [CLI] + background_color = WHITE + + In order to have this ``.cfg`` file apply to a manim scene, it needs to be + placed in the same directory as the script, + + .. code-block:: bash + + project/ + ├─scene.py + └─manim.cfg + + Now, when the user executes + + .. code-block:: bash + + manim scene.py + + the background of the scene will be set to ``WHITE``. This applies regardless + of where the manim command is invoked from. + + Command line arguments override ``.cfg`` files. In the previous example, + executing + + .. code-block:: bash + + manim scene.py -c BLUE + + will set the background color to BLUE, regardless of the conents of + ``manim.cfg``. + + Finally, any programmatic changes made within the scene script itself will + override the command line arguments. For example, if ``scene.py`` contains + the following + + .. code-block:: python + + from manim import * + config.background_color = RED + class MyScene(Scene): + # ... + + the background color will be set to RED, regardless of the contents of + ``manim.cfg`` or the CLI arguments used when invoking manim. + + """ + + _OPTS = { + "assets_dir", + "background_color", + "background_opacity", + "custom_folders", + "disable_caching", + "ffmpeg_loglevel", + "flush_cache", + "frame_height", + "frame_rate", + "frame_width", + "frame_x_radius", + "frame_y_radius", + "from_animation_number", + "images_dir", + "input_file", + "js_renderer_path", + "leave_progress_bars", + "log_dir", + "log_to_file", + "max_files_cached", + "media_dir", + "movie_file_extension", + "partial_movie_dir", + "pixel_height", + "pixel_width", + "png_mode", + "preview", + "progress_bar", + "save_as_gif", + "save_last_frame", + "save_pngs", + "scene_names", + "show_in_file_browser", + "sound", + "tex_dir", + "tex_template_file", + "text_dir", + "upto_animation_number", + "use_js_renderer", + "verbosity", + "video_dir", + "write_all", + "write_to_movie", + } + + def __init__(self): + self._d = {k: None for k in self._OPTS} + + # behave like a dict + def __iter__(self): + return iter(self._d) + + def __len__(self): + return len(self._d) + + def __contains__(self, key): + try: + self.__getitem__(key) + return True + except AttributeError: + return False + + def __getitem__(self, key): + return getattr(self, key) + + def __setitem__(self, key, val): + getattr(ManimConfig, key).fset(self, val) # fset is the property's setter + + def update(self, obj): + """Digest the options found in another :class:`ManimConfig` or in a dict. + + Similar to :meth:`dict.update`, replaces the values of this object with + those of ``obj``. + + Parameters + ---------- + obj : Union[:class:`ManimConfig`, :class:`dict`] + The object to copy values from. + + Returns + ------- + None + + Raises + ----- + :class:`AttributeError` + If ``obj`` is a dict but contains keys that do not belong to any + config options. + + See Also + -------- + :meth:`~ManimConfig.digest_file`, :meth:`~ManimConfig.digest_args`, + :meth:`~ManimConfig.digest_parser` + + """ + + if isinstance(obj, ManimConfig): + self._d.update(obj._d) + + elif isinstance(obj, dict): + # First update the underlying _d, then update other properties + _dict = {k: v for k, v in obj.items() if k in self._d} + for k, v in _dict.items(): + self[k] = v + + _dict = {k: v for k, v in obj.items() if k not in self._d} + for k, v in _dict.items(): + self[k] = v + + # don't allow to delete anything + def __delitem__(self, key): + raise AttributeError("'ManimConfig' object does not support item deletion") + + def __delattr__(self, key): + raise AttributeError("'ManimConfig' object does not support item deletion") + + # copy functions + def copy(self): + """Deepcopy the contents of this ManimConfig. + + Returns + ------- + :class:`ManimConfig` + A copy of this object containing no shared references. + + See Also + -------- + :func:`tempconfig` + + Notes + ----- + This is the main mechanism behind :func:`tempconfig`. + + """ + return copy.deepcopy(self) + + def __copy__(self): + """See ManimConfig.copy().""" + return copy.deepcopy(self) + + def __deepcopy__(self, memo): + """See ManimConfig.copy().""" + c = ManimConfig() + # Deepcopying the underlying dict is enough because all properties + # either read directly from it or compute their value on the fly from + # vaulues read directly from it. + c._d = copy.deepcopy(self._d, memo) + return c + + # helper type-checking methods + def _set_from_list(self, key, val, values): + """Set ``key`` to ``val`` if ``val`` is contained in ``values``.""" + if val in values: + self._d[key] = val + else: + raise ValueError(f"attempted to set {key} to {val}; must be in {values}") + + def _set_boolean(self, key, val): + """Set ``key`` to ``val`` if ``val`` is Boolean.""" + if val in [True, False]: + self._d[key] = val + else: + raise ValueError(f"{key} must be boolean") + + def _set_str(self, key, val): + """Set ``key`` to ``val`` if ``val`` is a string.""" + if isinstance(val, str): + self._d[key] = val + elif not val: + self._d[key] = "" + else: + raise ValueError(f"{key} must be str or falsy value") + + def _set_between(self, key, val, lo, hi): + """Set ``key`` to ``val`` if lo <= val <= hi.""" + if lo <= val <= hi: + self._d[key] = val + else: + raise ValueError(f"{key} must be {lo} <= {key} <= {hi}") + + def _set_pos_number(self, key, val, allow_inf): + """Set ``key`` to ``val`` if ``val`` is a positive integer.""" + if isinstance(val, int) and val > -1: + self._d[key] = val + elif allow_inf and (val == -1 or val == float("inf")): + self._d[key] = float("inf") + else: + raise ValueError( + f"{key} must be a non-negative integer (use -1 for infinity)" + ) + + # builders + def digest_parser(self, parser): + """Process the config options present in a :class:`ConfigParser` object. + + This method processes arbitrary parsers, not only those read from a + single file, whereas :meth:`~ManimConfig.digest_file` can only process one + file at a time. + + Parameters + ---------- + parser : :class:`ConfigParser` + An object reflecting the contents of one or many ``.cfg`` files. In + particular, it may reflect the contents of mulitple files that have + been parsed in a cascading fashion. + + Returns + ------- + self : :class:`ManimConfig` + This object, after processing the contents of ``parser``. + + See Also + -------- + :func:`make_config_parser`, :meth:`~.ManimConfig.digest_file`, + :meth:`~.ManimConfig.digest_args`, + + Notes + ----- + If there are multiple ``.cfg`` files to process, it is always more + efficient to parse them into a single :class:`ConfigParser` object + first, and then call this function once (instead of calling + :meth:`~.ManimConfig.digest_file` multiple times). + + Examples + -------- + To digest the config options set in two files, first create a + ConfigParser and parse both files and then digest the parser: + + .. code-block:: python + + parser = configparser.ConfigParser() + parser.read([file1, file2]) + config = ManimConfig().digest_parser(parser) + + In fact, the global ``config`` object is initialized like so: + + .. code-block:: python + + parser = make_config_parser() + config = ManimConfig().digest_parser(parser) + + """ + self._parser = parser + + # boolean keys + for key in [ + "write_to_movie", + "save_last_frame", + "write_all", + "save_pngs", + "save_as_gif", + "preview", + "show_in_file_browser", + "progress_bar", + "sound", + "leave_progress_bars", + "log_to_file", + "disable_caching", + "flush_cache", + "custom_folders", + "use_js_renderer", + ]: + setattr(self, key, parser["CLI"].getboolean(key, fallback=False)) + + # int keys + for key in [ + "from_animation_number", + "upto_animation_number", + "frame_rate", + "max_files_cached", + "pixel_height", + "pixel_width", + ]: + setattr(self, key, parser["CLI"].getint(key)) + + # str keys + for key in [ + "assets_dir", + "verbosity", + "media_dir", + "log_dir", + "video_dir", + "images_dir", + "text_dir", + "tex_dir", + "partial_movie_dir", + "input_file", + "output_file", + "png_mode", + "movie_file_extension", + "background_color", + "js_renderer_path", + ]: + setattr(self, key, parser["CLI"].get(key, fallback="", raw=True)) + + # float keys + for key in ["background_opacity"]: + setattr(self, key, parser["CLI"].getfloat(key)) + + # other logic + self["frame_height"] = 8.0 + self["frame_width"] = ( + self["frame_height"] * self["pixel_width"] / self["pixel_height"] + ) + + val = parser["CLI"].get("tex_template_file") + if val: + setattr(self, "tex_template_file", val) + + val = parser["ffmpeg"].get("loglevel") + if val: + setattr(self, "ffmpeg_loglevel", val) + + return self + + def digest_args(self, args): + """Process the config options present in CLI arguments. + + Parameters + ---------- + args : :class:`argparse.Namespace` + An object returned by :func:`.main_utils.parse_args()`. + + Returns + ------- + self : :class:`ManimConfig` + This object, after processing the contents of ``parser``. + + See Also + -------- + :func:`.main_utils.parse_args()`, :meth:`~.ManimConfig.digest_parser`, + :meth:`~.ManimConfig.digest_file` + + Notes + ----- + If ``args.config_file`` is a non-empty string, ``ManimConfig`` tries to digest the + contents of said file with :meth:`~ManimConfig.digest_file` before + digesting any other CLI arguments. + + """ + # if a config file has been passed, digest it first so that other CLI + # flags supersede it + if args.config_file: + self.digest_file(args.config_file) + + self.input_file = args.file + self.scene_names = args.scene_names if args.scene_names is not None else [] + self.output_file = args.output_file + + for key in [ + "preview", + "show_in_file_browser", + "sound", + "leave_progress_bars", + "write_to_movie", + "save_last_frame", + "save_pngs", + "save_as_gif", + "write_all", + "disable_caching", + "flush_cache", + "transparent", + "scene_names", + "verbosity", + "background_color", + ]: + if hasattr(args, key): + attr = getattr(args, key) + # if attr is None, then no argument was passed and we should + # not change the current config + if attr is not None: + self[key] = attr + + # dry_run is special because it can only be set to True + if hasattr(args, "dry_run"): + if getattr(args, "dry_run"): + self["dry_run"] = True + + for key in [ + "media_dir", # always set this one first + "log_dir", + "log_to_file", # always set this one last + ]: + if hasattr(args, key): + attr = getattr(args, key) + # if attr is None, then no argument was passed and we should + # not change the current config + if attr is not None: + self[key] = attr + + # The -s (--save_last_frame) flag invalidates -w (--write_to_movie). + if self["save_last_frame"]: + self["write_to_movie"] = False + + # Handle the -n flag. + nflag = args.from_animation_number + if nflag is not None: + if "," in nflag: + start, end = nflag.split(",") + self.from_animation_number = int(start) + self.upto_animation_number = int(end) + else: + self.from_animation_number = int(nflag) + + # Handle the quality flags + self.quality = _determine_quality(args) + + # Handle the -r flag. + rflag = args.resolution + if rflag is not None: + try: + h, w = rflag.split(",") + self.pixel_height = int(h) + self.pixel_width = int(w) + except ValueError: + raise ValueError( + f'invalid argument {rflag} for -r flag (must have a comma ",")' + ) + + # Handle --custom_folders + if args.custom_folders: + for opt in [ + "media_dir", + "video_dir", + "images_dir", + "text_dir", + "tex_dir", + "log_dir", + "partial_movie_dir", + ]: + self[opt] = self._parser["custom_folders"].get(opt, raw=True) + # --media_dir overrides the deaful.cfg file + if hasattr(args, "media_dir") and args.media_dir: + self.media_dir = args.media_dir + + return self + + def digest_file(self, filename): + """Process the config options present in a ``.cfg`` file. + + This method processes a single ``.cfg`` file, whereas + :meth:`~ManimConfig.digest_parser` can process arbitrary parsers, built + perhaps from multiple ``.cfg`` files. + + Parameters + ---------- + filename : :class:`str` + Path to the ``.cfg`` file. + + Returns + ------- + self : :class:`ManimConfig` + This object, after processing the contents of ``filename``. + + See Also + -------- + :meth:`~ManimConfig.digest_file`, :meth:`~ManimConfig.digest_args`, + :func:`make_config_parser` + + Notes + ----- + If there are multiple ``.cfg`` files to process, it is always more + efficient to parse them into a single :class:`ConfigParser` object + first and digesting them with one call to + :meth:`~ManimConfig.digest_parser`, instead of calling this method + multiple times. + + """ + if filename: + return self.digest_parser(make_config_parser(filename)) + + # config options are properties + preview = property( + lambda self: self._d["preview"], + lambda self, val: self._set_boolean("preview", val), + doc="Whether to play the rendered movie (-p).", + ) + + show_in_file_browser = property( + lambda self: self._d["show_in_file_browser"], + lambda self, val: self._set_boolean("show_in_file_browser", val), + doc="Whether to show the output file in the file browser (-f).", + ) + + progress_bar = property( + lambda self: self._d["progress_bar"], + lambda self, val: self._set_boolean("progress_bar", val), + doc="Whether to show progress bars while rendering animations.", + ) + + leave_progress_bars = property( + lambda self: self._d["leave_progress_bars"], + lambda self, val: self._set_boolean("leave_progress_bars", val), + doc="Whether to leave the progress bar for each animation.", + ) + + @property + def log_to_file(self): + """Whether to save logs to a file.""" + return self._d["log_to_file"] + + @log_to_file.setter + def log_to_file(self, val): + self._set_boolean("log_to_file", val) + if val: + if not os.path.exists(self["log_dir"]): + os.makedirs(self["log_dir"]) + set_file_logger(self, self["verbosity"]) + + sound = property( + lambda self: self._d["sound"], + lambda self, val: self._set_boolean("sound", val), + doc="Whether to play a sound to notify when a scene is rendered (no flag).", + ) + + write_to_movie = property( + lambda self: self._d["write_to_movie"], + lambda self, val: self._set_boolean("write_to_movie", val), + doc="Whether to render the scene to a movie file (-w).", + ) + + save_last_frame = property( + lambda self: self._d["save_last_frame"], + lambda self, val: self._set_boolean("save_last_frame", val), + doc="Whether to save the last frame of the scene as an image file (-s).", + ) + + write_all = property( + lambda self: self._d["write_all"], + lambda self, val: self._set_boolean("write_all", val), + doc="Whether to render all scenes in the input file (-a).", + ) + + save_pngs = property( + lambda self: self._d["save_pngs"], + lambda self, val: self._set_boolean("save_pngs", val), + doc="Whether to save all frames in the scene as images files (-g).", + ) + + save_as_gif = property( + lambda self: self._d["save_as_gif"], + lambda self, val: self._set_boolean("save_as_gif", val), + doc="Whether to save the rendered scene in .gif format (-i).", + ) + + @property + def verbosity(self): + """Logger verbosity; "DEBUG", "INFO", "WARNING", "ERROR", or "CRITICAL" (-v).""" + return self._d["verbosity"] + + @verbosity.setter + def verbosity(self, val): + """Verbosity level of the logger.""" + self._set_from_list( + "verbosity", + val, + ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + ) + logging.getLogger("manim").setLevel(val) + + ffmpeg_loglevel = property( + lambda self: self._d["ffmpeg_loglevel"], + lambda self, val: self._set_from_list( + "ffmpeg_loglevel", val, ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + ), + doc="Verbosity level of ffmpeg (no flag).", + ) + + pixel_width = property( + lambda self: self._d["pixel_width"], + lambda self, val: self._set_pos_number("pixel_width", val, False), + doc="Frame width in pixels (--resolution, -r).", + ) + + pixel_height = property( + lambda self: self._d["pixel_height"], + lambda self, val: self._set_pos_number("pixel_height", val, False), + doc="Frame height in pixels (--resolution, -r).", + ) + + aspect_ratio = property( + lambda self: self._d["pixel_width"] / self._d["pixel_height"], + doc="Aspect ratio (width / height) in pixels (--resolution, -r).", + ) + + frame_height = property( + lambda self: self._d["frame_height"], + lambda self, val: self._d.__setitem__("frame_height", val), + doc="Frame height in logical units (no flag).", + ) + + frame_width = property( + lambda self: self._d["frame_width"], + lambda self, val: self._d.__setitem__("frame_width", val), + doc="Frame width in logical units (no flag).", + ) + + frame_y_radius = property( + lambda self: self._d["frame_height"] / 2, + lambda self, val: ( + self._d.__setitem__("frame_y_radius", val) + or self._d.__setitem__("frame_height", 2 * val) + ), + doc="Half the frame height (no flag).", + ) + + frame_x_radius = property( + lambda self: self._d["frame_width"] / 2, + lambda self, val: ( + self._d.__setitem__("frame_x_radius", val) + or self._d.__setitem__("frame_width", 2 * val) + ), + doc="Half the frame width (no flag).", + ) + + top = property( + lambda self: self.frame_y_radius * constants.UP, + doc="Coordinate at the center top of the frame.", + ) + + bottom = property( + lambda self: self.frame_y_radius * constants.DOWN, + doc="Coordinate at the center bottom of the frame.", + ) + + left_side = property( + lambda self: self.frame_x_radius * constants.LEFT, + doc="Coordinate at the middle left of the frame.", + ) + + right_side = property( + lambda self: self.frame_x_radius * constants.RIGHT, + doc="Coordinate at the middle right of the frame.", + ) + + frame_rate = property( + lambda self: self._d["frame_rate"], + lambda self, val: self._d.__setitem__("frame_rate", val), + doc="Frame rate in frames per second (-q).", + ) + + background_color = property( + lambda self: self._d["background_color"], + lambda self, val: self._d.__setitem__("background_color", colour.Color(val)), + doc="Background color of the scene (-c).", + ) + + from_animation_number = property( + lambda self: self._d["from_animation_number"], + lambda self, val: self._d.__setitem__("from_animation_number", val), + doc="Start rendering animations at this number (-n).", + ) + + upto_animation_number = property( + lambda self: self._d["upto_animation_number"], + lambda self, val: self._set_pos_number("upto_animation_number", val, True), + doc="Stop rendering animations at this nmber. Use -1 to avoid skipping (-n).", + ) + + max_files_cached = property( + lambda self: self._d["max_files_cached"], + lambda self, val: self._set_pos_number("max_files_cached", val, True), + doc="Maximum number of files cached. Use -1 for infinity (no flag).", + ) + + flush_cache = property( + lambda self: self._d["flush_cache"], + lambda self, val: self._set_boolean("flush_cache", val), + doc="Whether to delete all the cached partial movie files.", + ) + + disable_caching = property( + lambda self: self._d["disable_caching"], + lambda self, val: self._set_boolean("disable_caching", val), + doc="Whether to use scene caching.", + ) + + png_mode = property( + lambda self: self._d["png_mode"], + lambda self, val: self._set_from_list("png_mode", val, ["RGB", "RGBA"]), + doc="Either RGA (no transparency) or RGBA (with transparency) (no flag).", + ) + + movie_file_extension = property( + lambda self: self._d["movie_file_extension"], + lambda self, val: self._set_from_list( + "movie_file_extension", val, [".mp4", ".mov"] + ), + doc="Either .mp4 or .mov (no flag).", + ) + + background_opacity = property( + lambda self: self._d["background_opacity"], + lambda self, val: self._set_between("background_opacity", val, 0, 1), + doc="A number between 0.0 (fully transparent) and 1.0 (fully opaque).", + ) + + frame_size = property( + lambda self: (self._d["pixel_width"], self._d["pixel_height"]), + lambda self, tup: ( + self._d.__setitem__("pixel_width", tup[0]) + or self._d.__setitem__("pixel_height", tup[1]) + ), + doc="Tuple with (pixel width, pixel height) (no flag).", + ) + + @property + def quality(self): + """Video quality (-q).""" + keys = ["pixel_width", "pixel_height", "frame_rate"] + q = {k: self[k] for k in keys} + for qual in constants.QUALITIES: + if all([q[k] == constants.QUALITIES[qual][k] for k in keys]): + return qual + else: + return None + + @quality.setter + def quality(self, qual): + if qual not in constants.QUALITIES: + raise KeyError(f"quality must be one of {list(constants.QUALITIES.keys())}") + q = constants.QUALITIES[qual] + self.frame_size = q["pixel_width"], q["pixel_height"] + self.frame_rate = q["frame_rate"] + + @property + def transparent(self): + """Whether the background opacity is 0.0 (-t).""" + return self._d["background_opacity"] == 0.0 + + @transparent.setter + def transparent(self, val): + if val: + self.png_mode = "RGBA" + self.movie_file_extension = ".mov" + self.background_opacity = 0.0 + else: + self.png_mode = "RGB" + self.movie_file_extension = ".mp4" + self.background_opacity = 1.0 + + @property + def dry_run(self): + """Whether dry run is enabled.""" + return ( + self.write_to_movie is False + and self.write_all is False + and self.save_last_frame is False + and self.save_pngs is False + and self.save_as_gif is False + ) + + @dry_run.setter + def dry_run(self, val): + if val: + self.write_to_movie = False + self.write_all = False + self.save_last_frame = False + self.save_pngs = False + self.save_as_gif = False + else: + raise ValueError( + "It is unclear what it means to set dry_run to " + "False. Instead, try setting each option " + "individually. (write_to_movie, write_alll, " + "save_last_frame, save_pngs, or save_as_gif)" + ) + + @property + def use_js_renderer(self): + """Whether to use JS renderer or not (default).""" + self._d["use_js_renderer"] + + @use_js_renderer.setter + def use_js_renderer(self, val): + self._d["use_js_renderer"] = val + if val: + self["disable_caching"] = True + + js_renderer_path = property( + lambda self: self._d["js_renderer_path"], + lambda self, val: self._d.__setitem__("js_renderer_path", val), + doc="Path to JS renderer.", + ) + + media_dir = property( + lambda self: self._d["media_dir"], + lambda self, val: self._set_dir("media_dir", val), + doc="Main output directory. See :meth:`ManimConfig.get_dir`.", + ) + + def get_dir(self, key, **kwargs): + """Resolve a config option that stores a directory. + + Config options that store directories may depend on one another. This + method is used to provide the actual directory to the end user. + + Parameters + ---------- + key : :class:`str` + The config option to be resolved. Must be an option ending in + ``'_dir'``, for example ``'media_dir'`` or ``'video_dir'``. + + kwargs : :class:`str` + Any strings to be used when resolving the directory. + + Returns + ------- + :class:`pathlib.Path` + Path to the requested directory. If the path resolves to the empty + string, return ``None`` instead. + + Raises + ------ + :class:`KeyError` + When ``key`` is not a config option that stores a directory and + thus :meth:`~ManimConfig.get_dir` is not appropriate; or when + ``key`` is appropriate but there is not enough information to + resolve the directory. + + Notes + ----- + Standard :meth:`str.format` syntax is used to resolve the paths so the + paths may contain arbitrary placeholders using f-string notation. + However, these will require ``kwargs`` to contain the required values. + + Examples + -------- + + The value of ``config.tex_dir`` is ``'{media_dir}/Tex'`` by default, + i.e. it is a subfolder of wherever ``config.media_dir`` is located. In + order to get the *actual* directory, use :meth:`~ManimConfig.get_dir`. + + .. code-block:: python + + >>> from manim import config + >>> config.tex_dir + '{media_dir}/Tex' + >>> config.media_dir + './media' + >>> config.get_dir("tex_dir").as_posix() + 'media/Tex' + + Resolving directories is done in a lazy way, at the last possible + moment, to reflect any changes in other config options: + + .. code-block:: python + + >>> config.media_dir = 'my_media_dir' + >>> config.get_dir("tex_dir").as_posix() + 'my_media_dir/Tex' + + Some directories depend on information that is not available to + :class:`ManimConfig`. For example, the default value of `video_dir` + includes the name of the input file and the video quality + (e.g. 480p15). This informamtion has to be supplied via ``kwargs``: + + .. code-block:: python + + >>> config.video_dir + '{media_dir}/videos/{module_name}/{quality}' + >>> config.get_dir("video_dir") + Traceback (most recent call last): + KeyError: 'video_dir {media_dir}/videos/{module_name}/{quality} requires the following keyword arguments: module_name' + >>> config.get_dir("video_dir", module_name="myfile").as_posix() + 'my_media_dir/videos/myfile/1080p60' + + Note the quality does not need to be passed as keyword argument since + :class:`ManimConfig` does store information about quality. + + Directories may be recursively defined. For example, the config option + ``partial_movie_dir`` depends on ``video_dir``, which in turn depends + on ``media_dir``: + + .. code-block:: python + + >>> config.partial_movie_dir + '{video_dir}/partial_movie_files/{scene_name}' + >>> config.get_dir("partial_movie_dir") + Traceback (most recent call last): + KeyError: 'partial_movie_dir {video_dir}/partial_movie_files/{scene_name} requires the following keyword arguments: scene_name' + >>> config.get_dir("partial_movie_dir", module_name="myfile", scene_name="myscene").as_posix() + 'my_media_dir/videos/myfile/1080p60/partial_movie_files/myscene' + + Standard f-string syntax is used. Arbitrary names can be used when + defining directories, as long as the corresponding values are passed to + :meth:`ManimConfig.get_dir` via ``kwargs``. + + .. code-block:: python + + >>> config.media_dir = "{dir1}/{dir2}" + >>> config.get_dir("media_dir") + Traceback (most recent call last): + KeyError: 'media_dir {dir1}/{dir2} requires the following keyword arguments: dir1' + >>> config.get_dir("media_dir", dir1='foo', dir2='bar').as_posix() + 'foo/bar' + >>> config.media_dir = "./media" + >>> config.get_dir("media_dir").as_posix() + 'media' + + """ + dirs = [ + "assets_dir", + "media_dir", + "video_dir", + "images_dir", + "text_dir", + "tex_dir", + "log_dir", + "input_file", + "output_file", + "partial_movie_dir", + ] + if key not in dirs: + raise KeyError( + "must pass one of " + "{media,video,images,text,tex,log}_dir " + "or {input,output}_file" + ) + + dirs.remove(key) # a path cannot contain itself + + all_args = {k: self._d[k] for k in dirs} + all_args.update(kwargs) + all_args["quality"] = f"{self.pixel_height}p{self.frame_rate}" + + path = self._d[key] + while "{" in path: + try: + path = path.format(**all_args) + except KeyError as exc: + raise KeyError( + f"{key} {self._d[key]} requires the following " + + "keyword arguments: " + + " ".join(exc.args) + ) from exc + + return Path(path) if path else None + + def _set_dir(self, key, val): + if isinstance(val, Path): + self._d.__setitem__(key, str(val)) + else: + self._d.__setitem__(key, val) + + assets_dir = property( + lambda self: self._d["assets_dir"], + lambda self, val: self._set_dir("assets_dir", val), + doc="Directory to locate video assets.", + ) + + log_dir = property( + lambda self: self._d["log_dir"], + lambda self, val: self._set_dir("log_dir", val), + doc="Directory to place logs. See :meth:`ManimConfig.get_dir`.", + ) + + video_dir = property( + lambda self: self._d["video_dir"], + lambda self, val: self._set_dir("video_dir", val), + doc="Directory to place videos (no flag). See :meth:`ManimConfig.get_dir`.", + ) + + images_dir = property( + lambda self: self._d["images_dir"], + lambda self, val: self._set_dir("images_dir", val), + doc="Directory to place images (no flag). See :meth:`ManimConfig.get_dir`.", + ) + + text_dir = property( + lambda self: self._d["text_dir"], + lambda self, val: self._set_dir("text_dir", val), + doc="Directory to place text (no flag). See :meth:`ManimConfig.get_dir`.", + ) + + tex_dir = property( + lambda self: self._d["tex_dir"], + lambda self, val: self._set_dir("tex_dir", val), + doc="Directory to place tex (no flag). See :meth:`ManimConfig.get_dir`.", + ) + + partial_movie_dir = property( + lambda self: self._d["partial_movie_dir"], + lambda self, val: self._set_dir("partial_movie_dir", val), + doc="Directory to place partial movie files (no flag). See :meth:`ManimConfig.get_dir`.", + ) + + custom_folders = property( + lambda self: self._d["custom_folders"], + lambda self, val: self._set_boolean("custom_folders", val), + doc="Whether to use custom folder output.", + ) + + input_file = property( + lambda self: self._d["input_file"], + lambda self, val: self._set_dir("input_file", val), + doc="Input file name.", + ) + + output_file = property( + lambda self: self._d["output_file"], + lambda self, val: self._set_dir("output_file", val), + doc="Output file name (-o).", + ) + + scene_names = property( + lambda self: self._d["scene_names"], + lambda self, val: self._d.__setitem__("scene_names", val), + doc="Scenes to play from file.", + ) + + @property + def tex_template(self): + """Template used when rendering Tex. See :class:`.TexTemplate`.""" + if not hasattr(self, "_tex_template") or not self._tex_template: + fn = self._d["tex_template_file"] + if fn: + self._tex_template = TexTemplateFromFile(filename=fn) + else: + self._tex_template = TexTemplate() + return self._tex_template + + @tex_template.setter + def tex_template(self, val): + if isinstance(val, (TexTemplateFromFile, TexTemplate)): + self._tex_template = val + + @property + def tex_template_file(self): + """File to read Tex template from (no flag). See :class:`.TexTemplateFromFile`.""" + return self._d["tex_template_file"] + + @tex_template_file.setter + def tex_template_file(self, val): + if val: + if not os.access(val, os.R_OK): + logging.getLogger("manim").warning( + f"Custom TeX template {val} not found or not readable." + ) + else: + self._d["tex_template_file"] = Path(val) + self._tex_template = TexTemplateFromFile(filename=val) + else: + self._d["tex_template_file"] = val # actually set the falsy value + self._tex_template = TexTemplate() # but don't use it + + +class ManimFrame(Mapping): + _OPTS = { + "pixel_width", + "pixel_height", + "aspect_ratio", + "frame_height", + "frame_width", + "frame_y_radius", + "frame_x_radius", + "top", + "bottom", + "left_side", + "right_side", + } + _CONSTANTS = { + "UP": np.array((0.0, 1.0, 0.0)), + "DOWN": np.array((0.0, -1.0, 0.0)), + "RIGHT": np.array((1.0, 0.0, 0.0)), + "LEFT": np.array((-1.0, 0.0, 0.0)), + "IN": np.array((0.0, 0.0, -1.0)), + "OUT": np.array((0.0, 0.0, 1.0)), + "ORIGIN": np.array((0.0, 0.0, 0.0)), + "X_AXIS": np.array((1.0, 0.0, 0.0)), + "Y_AXIS": np.array((0.0, 1.0, 0.0)), + "Z_AXIS": np.array((0.0, 0.0, 1.0)), + "UL": np.array((-1.0, 1.0, 0.0)), + "UR": np.array((1.0, 1.0, 0.0)), + "DL": np.array((-1.0, -1.0, 0.0)), + "DR": np.array((1.0, -1.0, 0.0)), + } + + def __init__(self, c): + if not isinstance(c, ManimConfig): + raise TypeError("argument must be instance of 'ManimConfig'") + # need to use __dict__ directly because setting attributes is not + # allowed (see __setattr__) + self.__dict__["_c"] = c + + # there are required by parent class Mapping to behave like a dict + def __getitem__(self, key): + if key in self._OPTS: + return self._c[key] + elif key in self._CONSTANTS: + return self._CONSTANTS[key] + else: + raise KeyError(key) + + def __iter__(self): + return iter(list(self._OPTS) + list(self._CONSTANTS)) + + def __len__(self): + return len(self._OPTS) + + # make this truly immutable + def __setattr__(self, attr, val): + raise TypeError("'ManimFrame' object does not support item assignment") + + def __setitem__(self, key, val): + raise TypeError("'ManimFrame' object does not support item assignment") + + def __delitem__(self, key): + raise TypeError("'ManimFrame' object does not support item deletion") + + +for opt in list(ManimFrame._OPTS) + list(ManimFrame._CONSTANTS): + setattr(ManimFrame, opt, property(lambda self, o=opt: self[o])) diff --git a/manim/animation/animation.py b/manim/animation/animation.py index 4f7516b27f..5fdd333af0 100644 --- a/manim/animation/animation.py +++ b/manim/animation/animation.py @@ -1,7 +1,7 @@ """Animate mobjects.""" -__all__ = ["Animation"] +__all__ = ["Animation", "Wait"] from copy import deepcopy @@ -131,7 +131,7 @@ def interpolate_mobject(self, alpha): self.interpolate_submobject(*mobs, sub_alpha) def interpolate_submobject(self, submobject, starting_sumobject, alpha): - # Typically ipmlemented by subclass + # Typically implemented by subclass pass def get_sub_alpha(self, alpha, index, num_submobjects): @@ -165,3 +165,25 @@ def set_name(self, name): def is_remover(self): return self.remover + + +class Wait(Animation): + def __init__(self, stop_condition=None, **kwargs): + digest_config(self, kwargs) + self.mobject = None + self.stop_condition = stop_condition + + def begin(self): + pass + + def finish(self): + pass + + def clean_up_from_scene(self, scene): + pass + + def update_mobjects(self, dt): + pass + + def interpolate(self, alpha): + pass diff --git a/manim/animation/composition.py b/manim/animation/composition.py index 21ef0845e0..a96b28eda3 100644 --- a/manim/animation/composition.py +++ b/manim/animation/composition.py @@ -3,7 +3,6 @@ from ..animation.animation import Animation from ..mobject.mobject import Group -from ..utils.bezier import integer_interpolate from ..utils.bezier import interpolate from ..utils.config_ops import digest_config from ..utils.iterables import remove_list_redundancies @@ -42,10 +41,14 @@ def __init__(self, *animations, **kwargs): def get_all_mobjects(self): return self.group + def get_run_time(self): + if super().get_run_time() is None: + self.init_run_time() + return super().get_run_time() + def begin(self): for anim in self.animations: anim.begin() - # self.init_run_time() def finish(self): for anim in self.animations: @@ -107,23 +110,41 @@ class Succession(AnimationGroup): def begin(self): assert len(self.animations) > 0 self.init_run_time() - self.active_animation = self.animations[0] - self.active_animation.begin() + self.update_active_animation(0) def finish(self): - self.active_animation.finish() + while self.active_animation is not None: + self.next_animation() def update_mobjects(self, dt): - self.active_animation.update_mobjects(dt) + if self.active_animation: + self.active_animation.update_mobjects(dt) + + def update_active_animation(self, index): + self.active_index = index + if index >= len(self.animations): + self.active_animation = None + self.active_start_time = None + self.active_end_time = None + else: + self.active_animation = self.animations[index] + self.active_animation.begin() + self.active_start_time = self.anims_with_timings[index][1] + self.active_end_time = self.anims_with_timings[index][2] + + def next_animation(self): + self.active_animation.finish() + self.update_active_animation(self.active_index + 1) def interpolate(self, alpha): - index, subalpha = integer_interpolate(0, len(self.animations), alpha) - animation = self.animations[index] - if animation is not self.active_animation: - self.active_animation.finish() - animation.begin() - self.active_animation = animation - animation.interpolate(subalpha) + current_time = interpolate(0, self.run_time, alpha) + while self.active_end_time is not None and current_time >= self.active_end_time: + self.next_animation() + if self.active_animation: + elapsed = current_time - self.active_start_time + active_run_time = self.active_animation.get_run_time() + subalpha = elapsed / active_run_time if active_run_time != 0.0 else 1.0 + self.active_animation.interpolate(subalpha) class LaggedStart(AnimationGroup): diff --git a/manim/animation/creation.py b/manim/animation/creation.py index 4964f85a3d..96ad38b206 100644 --- a/manim/animation/creation.py +++ b/manim/animation/creation.py @@ -29,18 +29,49 @@ class ShowPartial(Animation): + """Abstract class for Animations that show the VMobject partially. + + Raises + ------ + :class:`TypeError` + If ``mobject`` is not an instance of :class:`~.VMobject`. + + See Also + -------- + :class:`ShowCreation`, :class:`~.ShowPassingFlash` + """ - Abstract class for ShowCreation and ShowPassingFlash - """ + + def __init__(self, mobject, **kwargs): + if not isinstance(mobject, VMobject): + raise TypeError("This Animation only works on vectorized mobjects") + super().__init__(mobject, **kwargs) def interpolate_submobject(self, submob, start_submob, alpha): submob.pointwise_become_partial(start_submob, *self.get_bounds(alpha)) def get_bounds(self, alpha): - raise NotImplementedError() + raise NotImplementedError("Please use ShowCreation or ShowPassingFlash") class ShowCreation(ShowPartial): + """Incrementally shows the VMobject. + + Examples + -------- + .. manim:: ShowCreationScene + + class ShowCreationScene(Scene): + def construct(self): + self.play(ShowCreation(Square())) + + + See Also + -------- + :class:`~.ShowPassingFlash` + + """ + CONFIG = { "lag_ratio": 1, } diff --git a/manim/animation/fading.py b/manim/animation/fading.py index 1e485435ce..7d8694ed17 100644 --- a/manim/animation/fading.py +++ b/manim/animation/fading.py @@ -30,6 +30,31 @@ class FadeOut(Transform): + """A transform fading out the given mobject. + + Examples + -------- + + .. manim:: PlaneFadeOut + + class PlaneFadeOut(Scene): + def construct(self): + sq1 = Square() + sq2 = Square() + sq3 = Square() + sq1.next_to(sq2, LEFT) + sq3.next_to(sq2, RIGHT) + circ = Circle() + circ.next_to(sq2, DOWN) + + self.add(sq1, sq2, sq3, circ) + self.wait() + + self.play(FadeOut(sq1), FadeOut(sq2), FadeOut(sq3)) + self.wait() + + """ + CONFIG = { "remover": True, "lag_ratio": DEFAULT_FADE_LAG_RATIO, diff --git a/manim/animation/indication.py b/manim/animation/indication.py index cb1bcd0e14..b92c45a8e6 100644 --- a/manim/animation/indication.py +++ b/manim/animation/indication.py @@ -148,6 +148,23 @@ def interpolate_mobject(self, alpha): class ShowPassingFlash(ShowPartial): + """Show only a sliver of the VMobject each frame. + + Examples + -------- + .. manim:: ShowPassingFlashScene + + class ShowPassingFlashScene(Scene): + def construct(self): + self.play(ShowPassingFlash(Square())) + + + See Also + -------- + :class:`~.ShowCreation` + + """ + CONFIG = { "time_width": 0.1, "remover": True, diff --git a/manim/animation/specialized.py b/manim/animation/specialized.py deleted file mode 100644 index b4c83ae846..0000000000 --- a/manim/animation/specialized.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Animations for highly specialiced applications.""" - -__all__ = ["MoveCar", "Broadcast"] - - -import operator as op - -from ..animation.composition import LaggedStart -from ..animation.transform import ApplyMethod -from ..animation.transform import Restore -from ..mobject.geometry import Circle -from ..mobject.svg.drawings import Car -from ..mobject.types.vectorized_mobject import VGroup -from ..utils.config_ops import digest_config -from ..utils.space_ops import get_norm -from ..utils.color import BLACK, WHITE - - -class MoveCar(ApplyMethod): - CONFIG = { - "moving_forward": True, - "run_time": 5, - } - - def __init__(self, car, target_point, **kwargs): - self.check_if_input_is_car(car) - self.target_point = target_point - super().__init__(car.move_to, target_point, **kwargs) - - def check_if_input_is_car(self, car): - if not isinstance(car, Car): - raise TypeError("MoveCar must take in Car object") - - def begin(self): - super().begin() - car = self.mobject - distance = get_norm( - op.sub( - self.target_mobject.get_right(), - self.starting_mobject.get_right(), - ) - ) - if not self.moving_forward: - distance *= -1 - tire_radius = car.get_tires()[0].get_width() / 2 - self.total_tire_radians = -distance / tire_radius - - def interpolate_mobject(self, alpha): - ApplyMethod.interpolate_mobject(self, alpha) - if alpha == 0: - return - radians = alpha * self.total_tire_radians - for tire in self.mobject.get_tires(): - tire.rotate_in_place(radians) - - -class Broadcast(LaggedStart): - CONFIG = { - "small_radius": 0.0, - "big_radius": 5, - "n_circles": 5, - "start_stroke_width": 8, - "color": WHITE, - "remover": True, - "lag_ratio": 0.2, - "run_time": 3, - "remover": True, - } - - def __init__(self, focal_point, **kwargs): - digest_config(self, kwargs) - circles = VGroup() - for x in range(self.n_circles): - circle = Circle( - radius=self.big_radius, - stroke_color=BLACK, - stroke_width=0, - ) - circle.add_updater(lambda c: c.move_to(focal_point)) - circle.save_state() - circle.set_width(self.small_radius * 2) - circle.set_stroke(self.color, self.start_stroke_width) - circles.add(circle) - animations = [Restore(circle) for circle in circles] - super().__init__(*animations, **kwargs) diff --git a/manim/animation/transform.py b/manim/animation/transform.py index e9960ff656..4b8b60546a 100644 --- a/manim/animation/transform.py +++ b/manim/animation/transform.py @@ -165,9 +165,9 @@ def check_validity_of_input(self, mobject): class ApplyMethod(Transform): def __init__(self, method, *args, **kwargs): """ - method is a method of Mobject, *args are arguments for + method is a method of Mobject, ``args`` are arguments for that method. Key word arguments should be passed in - as the last arg, as a dict, since **kwargs is for + as the last arg, as a dict, since ``kwargs`` is for configuration of the transform itself Relies on the fact that mobject methods return the mobject @@ -200,6 +200,26 @@ def create_target(self): class ApplyPointwiseFunction(ApplyMethod): + """Animation that applies a pointwise function to a mobject. + + Examples + -------- + + .. manim:: WarpSquare + :quality: low + + class WarpSquare(Scene): + def construct(self): + square = Square() + self.play( + ApplyPointwiseFunction( + lambda point: complex_to_R3(np.exp(R3_to_complex(point))), square + ) + ) + self.wait() + + """ + CONFIG = {"run_time": DEFAULT_POINTWISE_FUNCTION_RUN_TIME} def __init__(self, function, mobject, **kwargs): diff --git a/manim/camera/camera.py b/manim/camera/camera.py index 6fb59497e9..b92e8b298c 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -3,7 +3,7 @@ __all__ = ["Camera", "BackgroundColoredVMobjectDisplayer"] - +from functools import reduce import itertools as it import operator as op import time @@ -14,17 +14,16 @@ import cairo import numpy as np -from .. import logger, config, camera_config +from .. import logger, config from ..constants import * from ..mobject.types.image_mobject import AbstractImageMobject from ..mobject.mobject import Mobject from ..mobject.types.point_cloud_mobject import PMobject from ..mobject.types.vectorized_mobject import VMobject -from ..utils.color import color_to_int_rgba, BLACK +from ..utils.color import color_to_int_rgba from ..utils.config_ops import digest_config from ..utils.images import get_full_raster_image_path from ..utils.iterables import list_difference_update -from ..utils.iterables import remove_list_redundancies from ..utils.simple_functions import fdiv from ..utils.space_ops import angle_of_vector from ..utils.space_ops import get_norm @@ -53,8 +52,6 @@ class Camera(object): # Note: frame height and width will be resized to match # the pixel aspect ratio "frame_center": ORIGIN, - "background_color": BLACK, - "background_opacity": 1, # Points in vectorized mobjects with norm greater # than this value will be rescaled. "image_mode": "RGBA", @@ -94,6 +91,9 @@ def __init__(self, video_quality_config, background=None, **kwargs): ]: setattr(self, attr, kwargs.get(attr, config[attr])) + for attr in ["background_color", "background_opacity"]: + setattr(self, f"_{attr}", kwargs.get(attr, config[attr])) + # This one is in the same boat as the above, but it doesn't have the # same name as the corresponding key so it has to be handled on its own self.max_allowable_norm = config["frame_width"] @@ -117,6 +117,24 @@ def __deepcopy__(self, memo): self.canvas = None return copy.copy(self) + @property + def background_color(self): + return self._background_color + + @background_color.setter + def background_color(self, color): + self._background_color = color + self.init_background() + + @property + def background_opacity(self): + return self._background_opacity + + @background_opacity.setter + def background_opacity(self, alpha): + self._background_opacity = alpha + self.init_background() + def type_or_raise(self, mobject): """Return the type of mobject, if it is a type that can be rendered. @@ -1039,9 +1057,7 @@ def adjusted_thickness(self, thickness): """ # TODO: This seems...unsystematic - big_sum = op.add( - camera_config["default_pixel_height"], camera_config["default_pixel_width"] - ) + big_sum = op.add(config["pixel_height"], config["pixel_width"]) this_sum = op.add(self.pixel_height, self.pixel_width) factor = fdiv(big_sum, this_sum) return 1 + (thickness - 1) / factor diff --git a/manim/camera/multi_camera.py b/manim/camera/multi_camera.py index 1cec584620..02ad2e032a 100644 --- a/manim/camera/multi_camera.py +++ b/manim/camera/multi_camera.py @@ -19,11 +19,11 @@ def __init__( ): """Initalises the MultiCamera - Parameters: - ----------- - *image_mobjects_from_cameras : ImageMobject + Parameters + ---------- + image_mobjects_from_cameras : ImageMobject - **kwargs + kwargs Any valid keyword arguments of MovingCamera. """ self.image_mobjects_from_cameras = [] @@ -34,7 +34,7 @@ def __init__( def add_image_mobject_from_camera(self, image_mobject_from_camera): """Adds an ImageMobject that's been obtained from the camera - into the list `self.image_mobject_from_cameras` + into the list ``self.image_mobject_from_cameras`` Parameters ---------- diff --git a/manim/camera/three_d_camera.py b/manim/camera/three_d_camera.py index 55271aeeb3..d2e2d4c6ed 100644 --- a/manim/camera/three_d_camera.py +++ b/manim/camera/three_d_camera.py @@ -15,9 +15,9 @@ from ..mobject.types.point_cloud_mobject import Point from ..mobject.value_tracker import ValueTracker from ..utils.color import get_shaded_rgb -from ..utils.simple_functions import clip_in_place from ..utils.space_ops import rotation_about_z from ..utils.space_ops import rotation_matrix +from ..utils.family import extract_mobject_family_members class ThreeDCamera(Camera): @@ -280,7 +280,6 @@ def project_points(self, points): else: factor = distance / (distance - zs) factor[(distance - zs) < 0] = 10 ** 6 - # clip_in_place(factor, 0, 10**6) points[:, i] *= factor points = points + frame_center return points @@ -367,7 +366,7 @@ def add_fixed_in_frame_mobjects(self, *mobjects): **mobjects : Mobject The mobject to fix in frame. """ - for mobject in self.extract_mobject_family_members(mobjects): + for mobject in extract_mobject_family_members(mobjects): self.fixed_in_frame_mobjects.add(mobject) def remove_fixed_orientation_mobjects(self, *mobjects): diff --git a/manim/config/__init__.py b/manim/config/__init__.py deleted file mode 100644 index 05aa100e2a..0000000000 --- a/manim/config/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# Note that the global config dict is called 'config', just like the module -# itself. That's why we import the module first with a different name -# (_config), and then the dict. -from . import config as _config -from .config import config, tempconfig, file_writer_config, camera_config -from .logger import logger, console - -__all__ = [ - "_config", - "config", - "tempconfig", - "file_writer_config", - "camera_config", - "logger", - "console", -] diff --git a/manim/config/config.py b/manim/config/config.py deleted file mode 100644 index 3e7481c0db..0000000000 --- a/manim/config/config.py +++ /dev/null @@ -1,190 +0,0 @@ -""" -config.py ---------- -Process the manim.cfg file and the command line arguments into a single -config object. -""" - - -__all__ = ["file_writer_config", "config", "camera_config", "tempconfig"] - - -import os -import sys -from contextlib import contextmanager - -import colour - -from .. import constants -from .config_utils import ( - _determine_quality, - _run_config, - _init_dirs, - _from_command_line, -) - -from .logger import set_rich_logger, set_file_logger, logger -from ..utils.tex import TexTemplate, TexTemplateFromFile - -__all__ = ["file_writer_config", "config", "camera_config", "tempconfig"] - - -config = None - - -@contextmanager -def tempconfig(temp): - """Context manager that temporarily modifies the global config dict. - - The code block inside the ``with`` statement will use the modified config. - After the code block, the config will be restored to its original value. - - Parameters - ---------- - - temp : :class:`dict` - A dictionary whose keys will be used to temporarily update the global - config. - - Examples - -------- - Use ``with tempconfig({...})`` to temporarily change the default values of - certain objects. - - .. code_block:: python - - c = Camera() - c.frame_width == config['frame_width'] # -> True - with tempconfig({'frame_width': 100}): - c = Camera() - c.frame_width == config['frame_width'] # -> False - c.frame_width == 100 # -> True - - """ - global config - original = config.copy() - - temp = {k: v for k, v in temp.items() if k in original} - - # In order to change the config that every module has acces to, use - # update(), DO NOT use assignment. Assigning config = some_dict will just - # make the local variable named config point to a new dictionary, it will - # NOT change the dictionary that every module has a reference to. - config.update(temp) - try: - yield - finally: - config.update(original) # update, not assignment! - - -def _parse_config(config_parser, args): - """Parse config files and CLI arguments into a single dictionary.""" - # By default, use the CLI section of the digested .cfg files - default = config_parser["CLI"] - - # Handle the *_quality flags. These determine the section to read - # and are stored in 'camera_config'. Note the highest resolution - # passed as argument will be used. - quality = _determine_quality(args) - section = config_parser[quality if quality != constants.DEFAULT_QUALITY else "CLI"] - - # Loop over low quality for the keys, could be any quality really - config = {opt: section.getint(opt) for opt in config_parser["low_quality"]} - - config["default_pixel_height"] = default.getint("pixel_height") - config["default_pixel_width"] = default.getint("pixel_width") - # The -r, --resolution flag overrides the *_quality flags - if args.resolution is not None: - if "," in args.resolution: - height_str, width_str = args.resolution.split(",") - height, width = int(height_str), int(width_str) - else: - height = int(args.resolution) - width = int(16 * height / 9) - config.update({"pixel_height": height, "pixel_width": width}) - - # Handle the -c (--background_color) flag - if args.background_color is not None: - try: - background_color = colour.Color(args.background_color) - except AttributeError as err: - logger.warning("Please use a valid color.") - logger.error(err) - sys.exit(2) - else: - background_color = colour.Color(default["background_color"]) - config["background_color"] = background_color - - config["use_js_renderer"] = args.use_js_renderer or default.getboolean( - "use_js_renderer" - ) - - config["js_renderer_path"] = args.js_renderer_path or default.get( - "js_renderer_path" - ) - - # Set the rest of the frame properties - config["frame_height"] = 8.0 - config["frame_width"] = ( - config["frame_height"] * config["pixel_width"] / config["pixel_height"] - ) - config["frame_y_radius"] = config["frame_height"] / 2 - config["frame_x_radius"] = config["frame_width"] / 2 - config["top"] = config["frame_y_radius"] * constants.UP - config["bottom"] = config["frame_y_radius"] * constants.DOWN - config["left_side"] = config["frame_x_radius"] * constants.LEFT - config["right_side"] = config["frame_x_radius"] * constants.RIGHT - - # Handle the --tex_template flag, if the flag is absent read it from the config. - if args.tex_template: - tex_fn = os.path.expanduser(args.tex_template) - else: - tex_fn = default["tex_template"] if default["tex_template"] != "" else None - - if tex_fn is not None and not os.access(tex_fn, os.R_OK): - # custom template not available, fallback to default - logger.warning( - f"Custom TeX template {tex_fn} not found or not readable. " - "Falling back to the default template." - ) - tex_fn = None - config["tex_template_file"] = tex_fn - config["tex_template"] = ( - TexTemplateFromFile(filename=tex_fn) if tex_fn is not None else TexTemplate() - ) - - return config - - -args, config_parser, file_writer_config, successfully_read_files = _run_config() -logger.setLevel(file_writer_config["verbosity"]) -set_rich_logger(config_parser["logger"], file_writer_config["verbosity"]) - -if _from_command_line(): - logger.debug( - f"Read configuration files: {[os.path.abspath(cfgfile) for cfgfile in successfully_read_files]}" - ) - if not (hasattr(args, "subcommands")): - _init_dirs(file_writer_config) -config = _parse_config(config_parser, args) -if config["use_js_renderer"]: - file_writer_config["disable_caching"] = True -camera_config = config - -if file_writer_config["log_to_file"]: - # Note about log_file_name : The log file name will be the _.log - # get from the args (contained in file_writer_config). So it can differ from the real name of the scene. - # would only appear if scene name was provided on manim call - scene_name_suffix = "".join(file_writer_config["scene_names"]) - scene_file_name = os.path.basename(args.file).split(".")[0] - log_file_name = ( - f"{scene_file_name}_{scene_name_suffix}.log" - if scene_name_suffix - else f"{scene_file_name}.log" - ) - log_file_path = os.path.join( - file_writer_config["log_dir"], - log_file_name, - ) - set_file_logger(log_file_path) - logger.info("Log file will be saved in %(logpath)s", {"logpath": log_file_path}) diff --git a/manim/config/config_utils.py b/manim/config/config_utils.py deleted file mode 100644 index d97b8215a6..0000000000 --- a/manim/config/config_utils.py +++ /dev/null @@ -1,674 +0,0 @@ -""" -config_utils.py ---------------- - -Utility functions for parsing manim config files. - -""" - - -__all__ = [ - "_run_config", - "_paths_config_file", - "_from_command_line", - "finalized_configs_dict", -] - - -import argparse -import configparser -import logging -import os -import sys - -from .. import constants - - -def _parse_file_writer_config(config_parser, args): - """Parse config files and CLI arguments into a single dictionary.""" - # By default, use the CLI section of the digested .cfg files - default = config_parser["CLI"] - - # This will be the final file_writer_config dict exposed to the user - fw_config = {} - - # Handle input files and scenes. Note these cannot be set from - # the .cfg files, only from CLI arguments. - # If a subcommand is given, manim will not render a video and - # thus these specific input/output files are not needed. - if not (hasattr(args, "subcommands")): - fw_config["input_file"] = args.file - fw_config["scene_names"] = ( - args.scene_names if args.scene_names is not None else [] - ) - fw_config["output_file"] = args.output_file - - # Handle all options that are directly overridden by CLI - # arguments. Note ConfigParser options are all strings and each - # needs to be converted to the appropriate type. Thus, we do this - # in batches, depending on their type: booleans and strings - for boolean_opt in [ - "preview", - "show_in_file_browser", - "leave_progress_bars", - "write_to_movie", - "save_last_frame", - "save_pngs", - "save_as_gif", - "write_all", - "disable_caching", - "flush_cache", - "log_to_file", - ]: - - attr = getattr(args, boolean_opt) - fw_config[boolean_opt] = ( - default.getboolean(boolean_opt) if attr is None else attr - ) - # for str_opt in ['media_dir', 'video_dir', 'tex_dir', 'text_dir']: - for str_opt in ["media_dir"]: - attr = getattr(args, str_opt) - fw_config[str_opt] = os.path.relpath(default[str_opt]) if attr is None else attr - attr = getattr(args, "log_dir") - fw_config["log_dir"] = ( - os.path.join(fw_config["media_dir"], default["log_dir"]) - if attr is None - else attr - ) - dir_names = { - "video_dir": "videos", - "images_dir": "images", - "tex_dir": "Tex", - "text_dir": "texts", - } - for name in dir_names: - fw_config[name] = os.path.join(fw_config["media_dir"], dir_names[name]) - - # the --custom_folders flag overrides the default folder structure with the - # custom folders defined in the [custom_folders] section of the config file - fw_config["custom_folders"] = args.custom_folders - if fw_config["custom_folders"]: - fw_config["media_dir"] = config_parser["custom_folders"].get("media_dir") - for opt in ["video_dir", "images_dir", "tex_dir", "text_dir"]: - fw_config[opt] = config_parser["custom_folders"].get(opt) - - # Handle the -s (--save_last_frame) flag: invalidate the -w flag - # At this point the save_last_frame option has already been set by - # both CLI and the cfg file, so read the config dict directly - if fw_config["save_last_frame"]: - fw_config["write_to_movie"] = False - - # Handle the -t (--transparent) flag. This flag determines which - # section to use from the .cfg file. - section = config_parser["transparent"] if args.transparent else default - for opt in ["png_mode", "movie_file_extension", "background_opacity"]: - fw_config[opt] = section[opt] - - # Handle the -n flag. Read first from the cfg and then override with CLI. - # These two are integers -- use getint() - for opt in ["from_animation_number", "upto_animation_number"]: - fw_config[opt] = default.getint(opt) - if fw_config["upto_animation_number"] == -1: - fw_config["upto_animation_number"] = float("inf") - nflag = args.from_animation_number - if nflag is not None: - if "," in nflag: - start, end = nflag.split(",") - fw_config["from_animation_number"] = int(start) - fw_config["upto_animation_number"] = int(end) - else: - fw_config["from_animation_number"] = int(nflag) - - # Handle the --dry_run flag. This flag determines which section - # to use from the .cfg file. All options involved are boolean. - # Note this overrides the flags -w, -s, -a, -g, and -i. - if args.dry_run: - for opt in [ - "write_to_movie", - "save_last_frame", - "save_pngs", - "save_as_gif", - "write_all", - ]: - fw_config[opt] = config_parser["dry_run"].getboolean(opt) - if not fw_config["write_to_movie"]: - fw_config["disable_caching"] = True - # Read in the streaming section -- all values are strings - fw_config["streaming"] = { - opt: config_parser["streaming"][opt] - for opt in [ - "live_stream_name", - "twitch_stream_key", - "streaming_protocol", - "streaming_ip", - "streaming_protocol", - "streaming_client", - "streaming_port", - "streaming_port", - "streaming_console_banner", - ] - } - - # For internal use (no CLI flag) - fw_config["skip_animations"] = fw_config["save_last_frame"] - fw_config["max_files_cached"] = default.getint("max_files_cached") - if fw_config["max_files_cached"] == -1: - fw_config["max_files_cached"] = float("inf") - # Parse the verbosity flag to read in the log level - verbosity = getattr(args, "verbosity") - verbosity = default["verbosity"] if verbosity is None else verbosity - fw_config["verbosity"] = verbosity - - # Parse the ffmpeg log level in the config - ffmpeg_loglevel = config_parser["ffmpeg"].get("loglevel", None) - fw_config["ffmpeg_loglevel"] = ( - constants.FFMPEG_VERBOSITY_MAP[verbosity] - if ffmpeg_loglevel is None - else ffmpeg_loglevel - ) - - # Parse the progress_bar flag - progress_bar = getattr(args, "progress_bar") - if progress_bar is None: - progress_bar = default.getboolean("progress_bar") - fw_config["progress_bar"] = progress_bar - return fw_config - - -def _parse_cli(arg_list, input=True): - parser = argparse.ArgumentParser( - description="Animation engine for explanatory math videos", - epilog="Made with <3 by the manim community devs", - ) - if input: - # If the only command is `manim`, we want both subcommands like `cfg` - # and mandatory positional arguments like `file` to show up in the help section. - only_manim = len(sys.argv) == 1 - - if only_manim or _subcommand_name(): - subparsers = parser.add_subparsers(dest="subcommands") - - # More subcommands can be added here, with elif statements. - # If a help command is passed, we still want subcommands to show - # up, so we check for help commands as well before adding the - # subcommand's subparser. - if only_manim or _subcommand_name() in ["cfg", "--help", "-h"]: - cfg_related = _init_cfg_subcmd(subparsers) - - if only_manim or not _subcommand_name(ignore=["--help", "-h"]): - parser.add_argument( - "file", - help="path to file holding the python code for the scene", - ) - parser.add_argument( - "scene_names", - nargs="*", - help="Name of the Scene class you want to see", - default=[""], - ) - parser.add_argument( - "-o", - "--output_file", - help="Specify the name of the output file, if " - "it should be different from the scene class name", - default="", - ) - - # The following use (action='store_const', const=True) instead of - # the built-in (action='store_true'). This is because the latter - # will default to False if not specified, while the former sets no - # default value. Since we want to set the default value in - # manim.cfg rather than here, we use the former. - parser.add_argument( - "-p", - "--preview", - action="store_const", - const=True, - help="Automatically open the saved file once its done", - ) - parser.add_argument( - "-f", - "--show_in_file_browser", - action="store_const", - const=True, - help="Show the output file in the File Browser", - ) - parser.add_argument( - "--leave_progress_bars", - action="store_const", - const=True, - help="Leave progress bars displayed in terminal", - ) - parser.add_argument( - "-a", - "--write_all", - action="store_const", - const=True, - help="Write all the scenes from a file", - ) - parser.add_argument( - "-w", - "--write_to_movie", - action="store_const", - const=True, - help="Render the scene as a movie file (this is on by default)", - ) - parser.add_argument( - "-s", - "--save_last_frame", - action="store_const", - const=True, - help="Save the last frame only (no movie file is generated)", - ) - parser.add_argument( - "-g", - "--save_pngs", - action="store_const", - const=True, - help="Save each frame as a png", - ) - parser.add_argument( - "-i", - "--save_as_gif", - action="store_const", - const=True, - help="Save the video as gif", - ) - parser.add_argument( - "--disable_caching", - action="store_const", - const=True, - help="Disable caching (will generate partial-movie-files anyway)", - ) - parser.add_argument( - "--flush_cache", - action="store_const", - const=True, - help="Remove all cached partial-movie-files", - ) - parser.add_argument( - "--log_to_file", - action="store_const", - const=True, - help="Log terminal output to file", - ) - # The default value of the following is set in manim.cfg - parser.add_argument( - "-c", - "--background_color", - help="Specify background color", - ) - parser.add_argument( - "--background_opacity", - help="Specify background opacity", - ) - parser.add_argument( - "--media_dir", - help="Directory to store media (including video files)", - ) - parser.add_argument( - "--log_dir", - help="Directory to store log files", - ) - parser.add_argument( - "--tex_template", - help="Specify a custom TeX template file", - ) - - # All of the following use (action="store_true"). This means that - # they are by default False. In contrast to the previous ones that - # used (action="store_const", const=True), the following do not - # correspond to a single configuration option. Rather, they - # override several options at the same time. - - # The following overrides -w, -a, -g, and -i - parser.add_argument( - "--dry_run", - action="store_true", - help="Do a dry run (render scenes but generate no output files)", - ) - - # The following overrides PNG_MODE, MOVIE_FILE_EXTENSION, and - # BACKGROUND_OPACITY - parser.add_argument( - "-t", - "--transparent", - action="store_true", - help="Render a scene with an alpha channel", - ) - - # The following are mutually exclusive and each overrides - # FRAME_RATE, PIXEL_HEIGHT, and PIXEL_WIDTH, - parser.add_argument( - "-q", - "--quality", - choices=constants.QUALITIES.values(), - default=constants.DEFAULT_QUALITY_SHORT, - help="Render at specific quality, short form of the --*_quality flags", - ) - parser.add_argument( - "--low_quality", - action="store_true", - help="Render at low quality", - ) - parser.add_argument( - "--medium_quality", - action="store_true", - help="Render at medium quality", - ) - parser.add_argument( - "--high_quality", - action="store_true", - help="Render at high quality", - ) - parser.add_argument( - "--production_quality", - action="store_true", - help="Render at default production quality", - ) - parser.add_argument( - "--fourk_quality", - action="store_true", - help="Render at 4K quality", - ) - - # Deprecated quality flags - parser.add_argument( - "-l", - action="store_true", - help="DEPRECATED: USE -ql or --quality l", - ) - parser.add_argument( - "-m", - action="store_true", - help="DEPRECATED: USE -qm or --quality m", - ) - parser.add_argument( - "-e", - action="store_true", - help="DEPRECATED: USE -qh or --quality h", - ) - parser.add_argument( - "-k", - action="store_true", - help="DEPRECATED: USE -qk or --quality k", - ) - - # This overrides any of the above - parser.add_argument( - "-r", - "--resolution", - help='Resolution, passed as "height,width". ' - "Overrides any quality flags, if present", - ) - - # This sets FROM_ANIMATION_NUMBER and UPTO_ANIMATION_NUMBER - parser.add_argument( - "-n", - "--from_animation_number", - help="Start rendering at the specified animation index, " - "instead of the first animation. If you pass in two comma " - "separated values, e.g. '3,6', it will end " - "the rendering at the second value", - ) - - parser.add_argument( - "--use_js_renderer", - help="Render animations using the javascript frontend", - action="store_const", - const=True, - ) - - parser.add_argument( - "--js_renderer_path", - help="Path to the javascript frontend", - ) - - # Specify the manim.cfg file - parser.add_argument( - "--config_file", - help="Specify the configuration file", - ) - - # Specify whether to use the custom folders - parser.add_argument( - "--custom_folders", - action="store_true", - help="Use the folders defined in the [custom_folders] " - "section of the config file to define the output folder structure", - ) - - # Specify the verbosity - parser.add_argument( - "-v", - "--verbosity", - type=str, - help="Verbosity level. Also changes the ffmpeg log level unless the latter is specified in the config", - choices=constants.VERBOSITY_CHOICES, - ) - - # Specify if the progress bar should be displayed - def _str2bool(s): - if s == "True": - return True - elif s == "False": - return False - else: - raise argparse.ArgumentTypeError("True or False expected") - - parser.add_argument( - "--progress_bar", - type=_str2bool, - help="Display the progress bar", - metavar="True/False", - ) - parsed = parser.parse_args(arg_list) - if hasattr(parsed, "subcommands"): - if _subcommand_name() == "cfg": - setattr( - parsed, - "cfg_subcommand", - cfg_related.parse_args(sys.argv[2:]).cfg_subcommand, - ) - - return parsed - - -def _init_dirs(config): - # Make sure all folders exist - for folder in [ - config["media_dir"], - config["video_dir"], - config["tex_dir"], - config["text_dir"], - config["log_dir"], - ]: - if not os.path.exists(folder): - # If log_to_file is False, ignore log_dir - if folder is config["log_dir"] and (not config["log_to_file"]): - pass - else: - os.makedirs(folder) - - -def _from_command_line(): - """Determine if manim was called from the command line.""" - # Manim can be called from the command line in three different - # ways. The first two involve using the manim or manimcm commands. - # Note that some Windows CLIs replace those commands with the path - # to their executables, so we must check for this as well - prog = os.path.split(sys.argv[0])[-1] - from_cli_command = prog in ["manim", "manim.exe", "manimcm", "manimcm.exe"] - - # The third way involves using `python -m manim ...`. In this - # case, the CLI arguments passed to manim do not include 'manim', - # 'manimcm', or even 'python'. However, the -m flag will always - # be the first argument. - from_python_m = sys.argv[0] == "-m" - - return from_cli_command or from_python_m - - -def _from_dunder_main(): - dunder_main_path = os.path.join( - os.path.dirname(os.path.dirname(__file__)), "__main__.py" - ) - return sys.argv[0] == dunder_main_path - - -def _paths_config_file(): - library_wide = os.path.abspath( - os.path.join(os.path.dirname(__file__), "default.cfg") - ) - if sys.platform.startswith("win32"): - user_wide = os.path.expanduser( - os.path.join("~", "AppData", "Roaming", "Manim", "manim.cfg") - ) - else: - user_wide = os.path.expanduser( - os.path.join("~", ".config", "manim", "manim.cfg") - ) - return [library_wide, user_wide] - - -def _run_config(): - # Config files to be parsed, in ascending priority - config_files = _paths_config_file() - if _from_command_line() or _from_dunder_main(): - args = _parse_cli(sys.argv[1:]) - if not hasattr(args, "subcommands"): - if args.config_file is not None: - if os.path.exists(args.config_file): - config_files.append(args.config_file) - else: - raise FileNotFoundError( - f"Config file {args.config_file} doesn't exist" - ) - else: - script_directory_file_config = os.path.join( - os.path.dirname(args.file), "manim.cfg" - ) - if os.path.exists(script_directory_file_config): - config_files.append(script_directory_file_config) - else: - working_directory_file_config = os.path.join(os.getcwd(), "manim.cfg") - if os.path.exists(working_directory_file_config): - config_files.append(working_directory_file_config) - - else: - # In this case, we still need an empty args object. - args = _parse_cli([], input=False) - # Need to populate the options left out - args.file, args.scene_names, args.output_file = "", "", "" - - config_parser = configparser.ConfigParser() - successfully_read_files = config_parser.read(config_files) - - # this is for internal use when writing output files - file_writer_config = _parse_file_writer_config(config_parser, args) - return args, config_parser, file_writer_config, successfully_read_files - - -def finalized_configs_dict(): - config = _run_config()[1] - return {section: dict(config[section]) for section in config.sections()} - - -def _subcommand_name(ignore=()): - """Goes through sys.argv to check if any subcommand has been passed, - and returns the first such subcommand's name, if found. - - Parameters - ---------- - ignore : Iterable[:class:`str`], optional - List of NON_ANIM_UTILS to ignore when searching for subcommands, by default [] - - Returns - ------- - Optional[:class:`str`] - If a subcommand is found, returns the string of its name. Returns None if no - subcommand is found. - """ - NON_ANIM_UTILS = ["cfg", "--help", "-h"] - NON_ANIM_UTILS = [util for util in NON_ANIM_UTILS if util not in ignore] - - # If a subcommand is found, break out of the inner loop, and hit the break of the outer loop - # on the way out, effectively breaking out of both loops. The value of arg will be the - # subcommand to be taken. - # If no subcommand is found, none of the breaks are hit, and the else clause of the outer loop - # is run, setting arg to None. - - for item in NON_ANIM_UTILS: - for arg in sys.argv: - if arg == item: - break - else: - continue - break - else: - arg = None - - return arg - - -def _init_cfg_subcmd(subparsers): - """Initialises the subparser for the `cfg` subcommand. - - Parameters - ---------- - subparsers : :class:`argparse._SubParsersAction` - The subparser object for which to add the sub-subparser for the cfg subcommand. - - Returns - ------- - :class:`argparse.ArgumentParser` - The parser that parser anything cfg subcommand related. - """ - cfg_related = subparsers.add_parser( - "cfg", - ) - cfg_subparsers = cfg_related.add_subparsers(dest="cfg_subcommand") - - cfg_write_parser = cfg_subparsers.add_parser("write") - cfg_write_parser.add_argument( - "--level", - choices=["user", "cwd"], - default=None, - help="Specify if this config is for user or just the working directory.", - ) - cfg_write_parser.add_argument( - "--open", action="store_const", const=True, default=False - ) - cfg_subparsers.add_parser("show") - - cfg_export_parser = cfg_subparsers.add_parser("export") - cfg_export_parser.add_argument("--dir", default=os.getcwd()) - - return cfg_related - - -def _determine_quality(args): - old_qualities = { - "k": "fourk_quality", - "e": "high_quality", - "m": "medium_quality", - "l": "low_quality", - } - - for quality in constants.QUALITIES.keys(): - if quality == constants.DEFAULT_QUALITY: - # Skip so we prioritize anything that overwrites the default quality. - pass - elif getattr(args, quality) or ( - hasattr(args, "quality") and args.quality == constants.QUALITIES[quality] - ): - return quality - - for quality in old_qualities.keys(): - if getattr(args, quality): - logging.getLogger("manim").warning( - f"Option -{quality} is deprecated please use the --quality/-q flag." - ) - return old_qualities[quality] - - return constants.DEFAULT_QUALITY diff --git a/manim/config/logger.py b/manim/config/logger.py deleted file mode 100644 index 4ca7ee2eb8..0000000000 --- a/manim/config/logger.py +++ /dev/null @@ -1,120 +0,0 @@ -""" -logger.py ---------- -This is the logging library for manim. -This library uses rich for coloured log outputs. - -""" - - -__all__ = ["logger", "console"] - - -import configparser -import logging - -from rich.console import Console -from rich.logging import RichHandler -from rich.theme import Theme -from rich import print as printf -from rich import errors, color -import json -import copy - - -class JSONFormatter(logging.Formatter): - """Subclass of `:class:`logging.Formatter`, to build our own format of the logs (JSON).""" - - def format(self, record): - record_c = copy.deepcopy(record) - if record_c.args: - for arg in record_c.args: - record_c.args[arg] = "<>" - return json.dumps( - { - "levelname": record_c.levelname, - "module": record_c.module, - "message": super().format(record_c), - } - ) - - -def _parse_theme(config_logger): - theme = dict( - zip( - [key.replace("_", ".") for key in config_logger.keys()], - list(config_logger.values()), - ) - ) - - theme["log.width"] = None if theme["log.width"] == "-1" else int(theme["log.width"]) - - theme["log.height"] = ( - None if theme["log.height"] == "-1" else int(theme["log.height"]) - ) - theme["log.timestamps"] = False - try: - customTheme = Theme( - { - k: v - for k, v in theme.items() - if k not in ["log.width", "log.height", "log.timestamps"] - } - ) - except (color.ColorParseError, errors.StyleSyntaxError): - customTheme = None - printf( - "[logging.level.error]It seems your colour configuration couldn't be parsed. Loading the default color configuration...[/logging.level.error]" - ) - return customTheme - - -def set_rich_logger(config_logger, verbosity): - """Will set the RichHandler of the logger. - - Parameter - ---------- - config_logger :class: - Config object of the logger. - """ - theme = _parse_theme(config_logger) - global console - console = Console(theme=theme) - # These keywords Are Highlighted specially. - RichHandler.KEYWORDS = [ - "Played", - "animations", - "scene", - "Reading", - "Writing", - "script", - "arguments", - "Invalid", - "Aborting", - "module", - "File", - "Rendering", - "Rendered", - ] - rich_handler = RichHandler( - console=console, show_time=config_logger.getboolean("log_timestamps") - ) - global logger - rich_handler.setLevel(verbosity) - logger.addHandler(rich_handler) - - -def set_file_logger(log_file_path): - file_handler = logging.FileHandler(log_file_path, mode="w") - file_handler.setFormatter(JSONFormatter()) - global logger - logger.addHandler(file_handler) - - -logger = logging.getLogger("manim") -# The console is set to None as it will be changed by set_rich_logger. -console = None - -# TODO : This is only temporary to keep the terminal output clean when working with ImageMobject and matplotlib plots -logging.getLogger("PIL").setLevel(logging.INFO) -logging.getLogger("matplotlib").setLevel(logging.INFO) diff --git a/manim/constants.py b/manim/constants.py index 2f13f226ea..b0bf8a031f 100644 --- a/manim/constants.py +++ b/manim/constants.py @@ -141,12 +141,43 @@ class MyText(Text): # Video qualities QUALITIES = { - "fourk_quality": "k", - "production_quality": "p", - "high_quality": "h", - "medium_quality": "m", - "low_quality": "l", + "fourk_quality": { + "flag": "k", + "pixel_height": 2160, + "pixel_width": 3840, + "frame_rate": 60, + }, + "production_quality": { + "flag": "p", + "pixel_height": 1440, + "pixel_width": 2560, + "frame_rate": 60, + }, + "high_quality": { + "flag": "h", + "pixel_height": 1080, + "pixel_width": 1920, + "frame_rate": 60, + }, + "medium_quality": { + "flag": "m", + "pixel_height": 720, + "pixel_width": 1280, + "frame_rate": 30, + }, + "low_quality": { + "flag": "l", + "pixel_height": 480, + "pixel_width": 854, + "frame_rate": 15, + }, + "example_quality": { + "flag": None, + "pixel_height": 480, + "pixel_width": 854, + "frame_rate": 30, + }, } -DEFAULT_QUALITY = "production_quality" -DEFAULT_QUALITY_SHORT = QUALITIES[DEFAULT_QUALITY] +DEFAULT_QUALITY = "high_quality" +DEFAULT_QUALITY_SHORT = QUALITIES[DEFAULT_QUALITY]["flag"] diff --git a/manim/grpc/impl/frame_server_impl.py b/manim/grpc/impl/frame_server_impl.py index 8f0f2eac02..50a2524080 100644 --- a/manim/grpc/impl/frame_server_impl.py +++ b/manim/grpc/impl/frame_server_impl.py @@ -1,13 +1,10 @@ -from ...config import camera_config -from ...config import file_writer_config -from ...scene import scene +from ... import config from ..gen import frameserver_pb2 from ..gen import frameserver_pb2_grpc from ..gen import renderserver_pb2 from ..gen import renderserver_pb2_grpc from concurrent import futures -from google.protobuf import json_format -from watchdog.events import LoggingEventHandler, FileSystemEventHandler +from watchdog.events import FileSystemEventHandler from watchdog.observers import Observer import grpc import subprocess as sp @@ -19,7 +16,7 @@ get_scene_classes_from_module, get_scenes_to_render, ) -from ...config.logger import logger +from ... import logger from ...constants import JS_RENDERER_INFO @@ -53,7 +50,7 @@ def __init__(self, server, scene_class): except grpc._channel._InactiveRpcError: logger.warning(f"No frontend was detected at localhost:50052.") try: - sp.Popen(camera_config["js_renderer_path"]) + sp.Popen(config["js_renderer_path"]) except PermissionError: logger.info(JS_RENDERER_INFO) self.server.stop(None) @@ -125,7 +122,7 @@ def GetFrameAtTime(self, request, context): frame_response.frame_pending = True selected_scene.renderer_waiting = True return frame_response - elif selected_scene.skip_animations: + elif selected_scene.renderer.skip_animations: # Do nothing return else: @@ -194,7 +191,7 @@ def on_deleted(self, event): def on_modified(self, event): super().on_modified(event) - module = get_module(file_writer_config["input_file"]) + module = get_module(config["input_file"]) all_scene_classes = get_scene_classes_from_module(module) scene_classes_to_render = get_scenes_to_render(all_scene_classes) scene_class = scene_classes_to_render[0] @@ -250,7 +247,7 @@ def on_modified(self, event): try: stub.ManimStatus(request) except grpc._channel._InactiveRpcError: - sp.Popen(camera_config["js_renderer_path"]) + sp.Popen(config["js_renderer_path"]) def get(scene_class): diff --git a/manim/mobject/coordinate_systems.py b/manim/mobject/coordinate_systems.py index c77f0ed7a9..e90d573817 100644 --- a/manim/mobject/coordinate_systems.py +++ b/manim/mobject/coordinate_systems.py @@ -14,7 +14,6 @@ from ..mobject.number_line import NumberLine from ..mobject.svg.tex_mobject import MathTex from ..mobject.types.vectorized_mobject import VGroup -from ..utils.config_ops import digest_config from ..utils.config_ops import merge_dicts_recursively from ..utils.simple_functions import binary_search from ..utils.space_ops import angle_of_vector @@ -343,7 +342,7 @@ def get_lines_parallel_to_axis( The axis with which the lines will be perpendicular. ratio_faded_lines : :class:`float` - The number of faded lines between each non-faded line. + The ratio between the space between faded lines and the space between non-faded lines. freq : :class:`float` Frequency of non-faded lines (number of non-faded lines per graph unit). @@ -354,8 +353,9 @@ def get_lines_parallel_to_axis( The first (i.e the non-faded lines parallel to `axis_parallel_to`) and second (i.e the faded lines parallel to `axis_parallel_to`) sets of lines, respectively. """ line = Line(axis_parallel_to.get_start(), axis_parallel_to.get_end()) - dense_freq = ratio_faded_lines - step = (1 / dense_freq) * freq + if ratio_faded_lines == 0: # don't show faded lines + ratio_faded_lines = 1 # i.e. set ratio to 1 + step = (1 / ratio_faded_lines) * freq lines1 = VGroup() lines2 = VGroup() unit_vector_axis_perp_to = axis_perpendicular_to.get_unit_vector() diff --git a/manim/mobject/functions.py b/manim/mobject/functions.py index 08e9fb003b..f0ac9236d1 100644 --- a/manim/mobject/functions.py +++ b/manim/mobject/functions.py @@ -13,6 +13,40 @@ class ParametricFunction(VMobject): + """A parametric curve. + + Examples + -------- + + .. manim:: PlotParametricFunction + :save_last_frame: + + class PlotParametricFunction(Scene): + def func(self, t): + return np.array((np.sin(2 * t), np.sin(3 * t), 0)) + + def construct(self): + func = ParametricFunction(self.func, t_max = TAU, fill_opacity=0).set_color(RED) + self.add(func.scale(3)) + + .. manim:: ThreeDParametricSpring + :save_last_frame: + + class ThreeDParametricSpring(ThreeDScene): + def construct(self): + curve1 = ParametricFunction( + lambda u: np.array([ + 1.2 * np.cos(u), + 1.2 * np.sin(u), + u * 0.05 + ]), color=RED, t_min=-3 * TAU, t_max=5 * TAU, + ).set_shade_in_3d(True) + axes = ThreeDAxes() + self.add(axes, curve1) + self.set_camera_orientation(phi=80 * DEGREES, theta=-60 * DEGREES) + self.wait() + """ + CONFIG = { "t_min": 0, "t_max": 1, diff --git a/manim/mobject/geometry.py b/manim/mobject/geometry.py index bc06db8b5c..528c14f60c 100644 --- a/manim/mobject/geometry.py +++ b/manim/mobject/geometry.py @@ -1,5 +1,25 @@ -"""Mobjects that are simple geometric shapes.""" +r"""Mobjects that are simple geometric shapes. +Examples +-------- + +.. manim:: UsefulAnnotations + :save_last_frame: + + class UsefulAnnotations(Scene): + def construct(self): + m0 = SmallDot() + m1 = AnnotationDot() + m2 = LabeledDot("ii") + m3 = LabeledDot(MathTex(r"\alpha").set_color(ORANGE)) + m4 = CurvedArrow(ORIGIN, 2*LEFT) + m5 = CurvedDoubleArrow(ORIGIN, 2*RIGHT) + + self.add(m0, m1, m2, m3, m4, m5) + for i, mobj in enumerate(self.mobjects): + mobj.shift(DOWN * (i-3)) + +""" __all__ = [ "TipableVMobject", @@ -9,6 +29,8 @@ "CurvedDoubleArrow", "Circle", "Dot", + "AnnotationDot", + "LabeledDot", "SmallDot", "Ellipse", "AnnularSector", @@ -24,14 +46,16 @@ "CubicBezier", "Polygon", "RegularPolygon", + "ArcPolygon", + "ArcPolygonFromArcs", "Triangle", "ArrowTip", "Rectangle", "Square", "RoundedRectangle", + "Cutout", ] - import warnings import numpy as np import math @@ -52,7 +76,7 @@ from ..utils.space_ops import get_norm from ..utils.space_ops import normalize from ..utils.space_ops import rotate_vector -from ..utils.color import RED, WHITE, BLUE +from ..utils.color import * DEFAULT_DOT_RADIUS = 0.08 DEFAULT_SMALL_DOT_RADIUS = 0.04 @@ -143,17 +167,13 @@ def position_tip(self, tip, at_start=False): def reset_endpoints_based_on_tip(self, tip, at_start): if self.get_length() == 0: - # Zero length, put_start_and_end_on wouldn't - # work + # Zero length, put_start_and_end_on wouldn't work return self if at_start: self.put_start_and_end_on(tip.base, self.get_end()) else: - self.put_start_and_end_on( - self.get_start(), - tip.base, - ) + self.put_start_and_end_on(self.get_start(), tip.base) return self def asign_tip_attr(self, tip, at_start): @@ -258,9 +278,7 @@ def set_pre_positioned_points(self): [ np.cos(a) * RIGHT + np.sin(a) * UP for a in np.linspace( - self.start_angle, - self.start_angle + self.angle, - self.num_components, + self.start_angle, self.start_angle + self.angle, self.num_components ) ] ) @@ -274,12 +292,7 @@ def set_pre_positioned_points(self): # Use tangent vectors to deduce anchors handles1 = anchors[:-1] + (d_theta / 3) * tangent_vectors[:-1] handles2 = anchors[1:] - (d_theta / 3) * tangent_vectors[1:] - self.set_anchors_and_handles( - anchors[:-1], - handles1, - handles2, - anchors[1:], - ) + self.set_anchors_and_handles(anchors[:-1], handles1, handles2, anchors[1:]) def get_arc_center(self, warning=True): """ @@ -301,10 +314,7 @@ def get_arc_center(self, warning=True): n1 = rotate_vector(t1, TAU / 4) n2 = rotate_vector(t2, TAU / 4) try: - return line_intersection( - line1=(a1, a1 + n1), - line2=(a2, a2 + n2), - ) + return line_intersection(line1=(a1, a1 + n1), line2=(a2, a2 + n2)) except Exception: if warning: warnings.warn("Can't find Arc center, using ORIGIN instead") @@ -341,11 +351,7 @@ def __init__(self, start, end, angle=TAU / 4, radius=None, **kwargs): arc_height = radius - math.sqrt(radius ** 2 - halfdist ** 2) angle = math.acos((radius - arc_height) / radius) * sign - Arc.__init__( - self, - angle=angle, - **kwargs, - ) + Arc.__init__(self, angle=angle, **kwargs) if angle == 0: self.set_points_as_corners([LEFT, RIGHT]) self.put_start_and_end_on(start, end) @@ -390,7 +396,7 @@ def surround(self, mobject, dim_to_match=0, stretch=False, buffer_factor=1.2): self.replace(mobject, dim_to_match, stretch) self.set_width(np.sqrt(mobject.get_width() ** 2 + mobject.get_height() ** 2)) - self.scale(buffer_factor) + return self.scale(buffer_factor) def point_at_angle(self, angle): start_angle = angle_of_vector(self.points[0] - self.get_center()) @@ -410,11 +416,79 @@ def __init__(self, point=ORIGIN, **kwargs): class SmallDot(Dot): + """ + A dot with small radius + """ + + CONFIG = {"radius": DEFAULT_SMALL_DOT_RADIUS} + + +class AnnotationDot(Dot): + """ + A dot with bigger radius and bold stroke to annotate scenes. + """ + CONFIG = { - "radius": DEFAULT_SMALL_DOT_RADIUS, + "radius": DEFAULT_DOT_RADIUS * 1.3, + "stroke_width": 5, + "stroke_color": WHITE, + "fill_color": BLUE, } +class LabeledDot(Dot): + """A :class:`Dot` containing a label in its center. + + Parameters + ---------- + label : Union[:class:`str`, :class:`~.SingleStringMathTex`, :class:`~.Text`, :class:`~.Tex`] + The label of the :class:`Dot`. This is rendered as :class:`~.MathTex` + by default (i.e., when passing a :class:`str`), but other classes + representing rendered strings like :class:`~.Text` or :class:`~.Tex` + can be passed as well. + + radius : :class:`float` + The radius of the :class:`Dot`. If ``None`` (the default), the radius + is calculated based on the size of the ``label``. + + Examples + -------- + + .. manim:: SeveralLabeledDots + :save_last_frame: + + class SeveralLabeledDots(Scene): + def construct(self): + sq = Square(fill_color=RED, fill_opacity=1) + self.add(sq) + dot1 = LabeledDot(Tex("42", color=RED)) + dot2 = LabeledDot(MathTex("a", color=GREEN)) + dot3 = LabeledDot(Text("ii", color=BLUE)) + dot4 = LabeledDot("3") + dot1.next_to(sq, UL) + dot2.next_to(sq, UR) + dot3.next_to(sq, DL) + dot4.next_to(sq, DR) + self.add(dot1, dot2, dot3, dot4) + """ + + def __init__(self, label, radius=None, **kwargs) -> None: + if isinstance(label, str): + from manim import MathTex + + rendered_label = MathTex(label, color=BLACK) + else: + rendered_label = label + + if radius is None: + radius = ( + 0.1 + max(rendered_label.get_width(), rendered_label.get_height()) / 2 + ) + Dot.__init__(self, radius=radius, **kwargs) + rendered_label.move_to(self.get_center()) + self.add(rendered_label) + + class Ellipse(Circle): CONFIG = {"width": 2, "height": 1} @@ -477,10 +551,7 @@ def generate_points(self): class Line(TipableVMobject): - CONFIG = { - "buff": 0, - "path_arc": None, # angle of arc specified here - } + CONFIG = {"buff": 0, "path_arc": None} # angle of arc specified here def __init__(self, start=LEFT, end=RIGHT, **kwargs): digest_config(self, kwargs) @@ -558,13 +629,10 @@ def get_slope(self): return np.tan(self.get_angle()) def set_angle(self, angle): - self.rotate( - angle - self.get_angle(), - about_point=self.get_start(), - ) + return self.rotate(angle - self.get_angle(), about_point=self.get_start()) def set_length(self, length): - self.scale(length / self.get_length()) + return self.scale(length / self.get_length()) def set_opacity(self, opacity, family=True): # Overwrite default, which would set @@ -601,10 +669,7 @@ def calculate_num_dashes(self, positive_space_ratio): return 1 def calculate_positive_space_ratio(self): - return fdiv( - self.dash_length, - self.dash_length + self.dash_spacing, - ) + return fdiv(self.dash_length, self.dash_length + self.dash_spacing) def get_start(self): if len(self.submobjects) > 0: @@ -640,10 +705,7 @@ def __init__(self, vmob, alpha, **kwargs): class Elbow(VMobject): - CONFIG = { - "width": 0.2, - "angle": 0, - } + CONFIG = {"width": 0.2, "angle": 0} def __init__(self, **kwargs): VMobject.__init__(self, **kwargs) @@ -727,27 +789,19 @@ def reset_normal_vector(self): def get_default_tip_length(self): max_ratio = self.max_tip_length_to_length_ratio - return min( - self.tip_length, - max_ratio * self.get_length(), - ) + return min(self.tip_length, max_ratio * self.get_length()) def set_stroke_width_from_length(self): max_ratio = self.max_stroke_width_to_length_ratio self.set_stroke( - width=min( - self.initial_stroke_width, - max_ratio * self.get_length(), - ), + width=min(self.initial_stroke_width, max_ratio * self.get_length()), family=False, ) return self class Vector(Arrow): - CONFIG = { - "buff": 0, - } + CONFIG = {"buff": 0} def __init__(self, direction=RIGHT, **kwargs): if len(direction) == 2: @@ -773,9 +827,7 @@ def __init__(self, points, **kwargs): class Polygon(VMobject): - CONFIG = { - "color": BLUE, - } + CONFIG = {"color": BLUE} def __init__(self, *vertices, **kwargs): VMobject.__init__(self, **kwargs) @@ -820,9 +872,7 @@ def round_corners(self, radius=0.5): class RegularPolygon(Polygon): - CONFIG = { - "start_angle": None, - } + CONFIG = {"start_angle": None} def __init__(self, n=6, **kwargs): digest_config(self, kwargs, locals()) @@ -836,6 +886,244 @@ def __init__(self, n=6, **kwargs): Polygon.__init__(self, *vertices, **kwargs) +class ArcPolygon(VMobject): + """A generalized polygon allowing for points to be connected with arcs. + + This version tries to stick close to the way :class:`Polygon` is used. Points + can be passed to it directly which are used to generate the according arcs + (using :class:`ArcBetweenPoints`). An angle or radius can be passed to it to + use across all arcs, but to configure arcs individually an ``arc_config`` list + has to be passed with the syntax explained below. + + .. tip:: + + Two instances of :class:`ArcPolygon` can be transformed properly into one + another as well. Be advised that any arc initialized with ``angle=0`` + will actually be a straight line, so if a straight section should seamlessly + transform into an arced section or vice versa, initialize the straight section + with a negligible angle instead (such as ``angle=0.0001``). + + There is an alternative version (:class:`ArcPolygonFromArcs`) that is instantiated + with pre-defined arcs. + + See Also + -------- + :class:`ArcPolygonFromArcs` + + Parameters + ---------- + vertices : Union[:class:`list`, :class:`np.array`] + A list of vertices, start and end points for the arc segments. + angle : :class:`float` + The angle used for constructing the arcs. If no other parameters + are set, this angle is used to construct all arcs. + radius : Optional[:class:`float`] + The circle radius used to construct the arcs. If specified, + overrides the specified ``angle``. + arc_config : Optional[Union[List[:class:`dict`]], :class:`dict`] + When passing a ``dict``, its content will be passed as keyword + arguments to :class:`~.ArcBetweenPoints`. Otherwise, a list + of dictionaries containing values that are passed as keyword + arguments for every individual arc can be passed. + kwargs + Further keyword arguments that are passed to the constructor of + :class:`~.VMobject`. + + Attributes + ---------- + arcs : :class:`list` + The arcs created from the input parameters:: + + >>> from manim import ArcPolygon + >>> ap = ArcPolygon([0, 0, 0], [2, 0, 0], [0, 2, 0]) + >>> ap.arcs + [ArcBetweenPoints, ArcBetweenPoints, ArcBetweenPoints] + + Examples + -------- + + .. manim:: SeveralArcPolygons + + class SeveralArcPolygons(Scene): + def construct(self): + a = [0, 0, 0] + b = [2, 0, 0] + c = [0, 2, 0] + ap1 = ArcPolygon(a, b, c, radius=2) + ap2 = ArcPolygon(a, b, c, angle=45*DEGREES) + ap3 = ArcPolygon(a, b, c, arc_config={'radius': 1.7, 'color': RED}) + ap4 = ArcPolygon(a, b, c, color=RED, fill_opacity=1, + arc_config=[{'radius': 1.7, 'color': RED}, + {'angle': 20*DEGREES, 'color': BLUE}, + {'radius': 1}]) + ap_group = VGroup(ap1, ap2, ap3, ap4).arrange() + self.play(*[ShowCreation(ap) for ap in [ap1, ap2, ap3, ap4]]) + self.wait() + + For further examples see :class:`ArcPolygonFromArcs`. + """ + + def __init__(self, *vertices, angle=PI / 4, radius=None, arc_config=None, **kwargs): + n = len(vertices) + point_pairs = [(vertices[k], vertices[(k + 1) % n]) for k in range(n)] + + if not arc_config: + if radius: + all_arc_configs = [{"radius": radius} for pair in point_pairs] + else: + all_arc_configs = [{"angle": angle} for pair in point_pairs] + elif isinstance(arc_config, dict): + all_arc_configs = [arc_config for pair in point_pairs] + else: + assert len(arc_config) == n + all_arc_configs = arc_config + + arcs = [ + ArcBetweenPoints(*pair, **conf) + for (pair, conf) in zip(point_pairs, all_arc_configs) + ] + + super().__init__(**kwargs) + # Adding the arcs like this makes ArcPolygon double as a VGroup. + # Also makes changes to the ArcPolygon, such as scaling, affect + # the arcs, so that their new values are usable. + self.add(*arcs) + for arc in arcs: + self.append_points(arc.points) + + # This enables the use of ArcPolygon.arcs as a convenience + # because ArcPolygon[0] returns itself, not the first Arc. + self.arcs = arcs + + +class ArcPolygonFromArcs(VMobject): + """A generalized polygon allowing for points to be connected with arcs. + + This version takes in pre-defined arcs to generate the arcpolygon and introduces + little new syntax. However unlike :class:`Polygon` it can't be created with points + directly. + + For proper appearance the passed arcs should connect seamlessly: + ``[a,b][b,c][c,a]`` + + If there are any gaps between the arcs, those will be filled in + with straight lines, which can be used deliberately for any straight + sections. Arcs can also be passed as straight lines such as an arc + initialized with ``angle=0``. + + .. tip:: + + Two instances of :class:`ArcPolygon` can be transformed properly into + one another as well. Be advised that any arc initialized with ``angle=0`` + will actually be a straight line, so if a straight section should seamlessly + transform into an arced section or vice versa, initialize the straight + section with a negligible angle instead (such as ``angle=0.0001``). + + There is an alternative version (:class:`ArcPolygon`) that can be instantiated + with points. + + See Also + -------- + :class:`ArcPolygon` + + Parameters + ---------- + arcs : Union[:class:`Arc`, :class:`ArcBetweenPoints`] + These are the arcs from which the arcpolygon is assembled. + kwargs + Keyword arguments that are passed to the constructor of + :class:`~.VMobject`. Affects how the ArcPolygon itself is drawn, + but doesn't affect passed arcs. + + Attributes + ---------- + arcs : :class:`list` + The arcs used to initialize the ArcPolygonFromArcs:: + + >>> from manim import ArcPolygonFromArcs, Arc, ArcBetweenPoints + >>> ap = ArcPolygonFromArcs(Arc(), ArcBetweenPoints([1,0,0], [0,1,0]), Arc()) + >>> ap.arcs + [Arc, ArcBetweenPoints, Arc] + + Examples + -------- + One example of an arcpolygon is the Reuleaux triangle. + Instead of 3 straight lines connecting the outer points, + a Reuleaux triangle has 3 arcs connecting those points, + making a shape with constant width. + + Passed arcs are stored as submobjects in the arcpolygon. + This means that the arcs are changed along with the arcpolygon, + for example when it's shifted, and these arcs can be manipulated + after the arcpolygon has been initialized. + + Also both the arcs contained in an :class:`~.ArcPolygonFromArcs`, as well as the + arcpolygon itself are drawn, which affects draw time in :class:`~.ShowCreation` + for example. In most cases the arcs themselves don't + need to be drawn, in which case they can be passed as invisible. + + .. manim:: ArcPolygonExample + + class ArcPolygonExample(Scene): + def construct(self): + arc_conf = {"stroke_width": 0} + poly_conf = {"stroke_width": 10, "stroke_color": BLUE, + "fill_opacity": 1, "color": PURPLE} + a = [-1, 0, 0] + b = [1, 0, 0] + c = [0, np.sqrt(3), 0] + arc0 = ArcBetweenPoints(a, b, radius=2, **arc_conf) + arc1 = ArcBetweenPoints(b, c, radius=2, **arc_conf) + arc2 = ArcBetweenPoints(c, a, radius=2, **arc_conf) + reuleaux_tri = ArcPolygonFromArcs(arc0, arc1, arc2, **poly_conf) + self.play(FadeIn(reuleaux_tri)) + self.wait(2) + + The arcpolygon itself can also be hidden so that instead only the contained + arcs are drawn. This can be used to easily debug arcs or to highlight them. + + .. manim:: ArcPolygonExample2 + + class ArcPolygonExample2(Scene): + def construct(self): + arc_conf = {"stroke_width": 3, "stroke_color": BLUE, + "fill_opacity": 0.5, "color": GREEN} + poly_conf = {"color": None} + a = [-1, 0, 0] + b = [1, 0, 0] + c = [0, np.sqrt(3), 0] + arc0 = ArcBetweenPoints(a, b, radius=2, **arc_conf) + arc1 = ArcBetweenPoints(b, c, radius=2, **arc_conf) + arc2 = ArcBetweenPoints(c, a, radius=2, stroke_color=RED) + reuleaux_tri = ArcPolygonFromArcs(arc0, arc1, arc2, **poly_conf) + self.play(FadeIn(reuleaux_tri)) + self.wait(2) + + """ + + def __init__(self, *arcs, **kwargs): + if not all(isinstance(m, (Arc, ArcBetweenPoints)) for m in arcs): + raise ValueError( + "All ArcPolygon submobjects must be of type Arc/ArcBetweenPoints" + ) + super().__init__(**kwargs) + # Adding the arcs like this makes ArcPolygonFromArcs double as a VGroup. + # Also makes changes to the ArcPolygonFromArcs, such as scaling, affect + # the arcs, so that their new values are usable. + self.add(*arcs) + # This enables the use of ArcPolygonFromArcs.arcs as a convenience + # because ArcPolygonFromArcs[0] returns itself, not the first Arc. + self.arcs = [*arcs] + for arc1, arc2 in adjacent_pairs(arcs): + self.append_points(arc1.points) + line = Line(arc1.get_end(), arc2.get_start()) + len_ratio = line.get_length() / arc1.get_arc_length() + if math.isnan(len_ratio) or math.isinf(len_ratio): + continue + line.insert_n_curves(int(arc1.get_num_curves() * len_ratio)) + self.append_points(line.get_points()) + + class Triangle(RegularPolygon): def __init__(self, **kwargs): RegularPolygon.__init__(self, n=3, **kwargs) @@ -857,9 +1145,7 @@ def __init__(self, **kwargs): class Square(Rectangle): - CONFIG = { - "side_length": 2.0, - } + CONFIG = {"side_length": 2.0} def __init__(self, **kwargs): digest_config(self, kwargs) @@ -869,9 +1155,7 @@ def __init__(self, **kwargs): class RoundedRectangle(Rectangle): - CONFIG = { - "corner_radius": 0.5, - } + CONFIG = {"corner_radius": 0.5} def __init__(self, **kwargs): Rectangle.__init__(self, **kwargs) @@ -924,6 +1208,27 @@ class ArrowTip(VMobject): >>> arrow = Arrow(np.array([0, 0, 0]), np.array([1, 1, 0]), ... tip_style={'fill_opacity': 0, 'stroke_width': 3}) + The following example illustrates the usage of all of the predefined + arrow tips. + + .. manim:: ArrowTipsShowcase + :save_last_frame: + + from manim.mobject.geometry import ArrowTriangleTip, ArrowSquareTip, ArrowSquareFilledTip,\ + ArrowCircleTip, ArrowCircleFilledTip + class ArrowTipsShowcase(Scene): + def construct(self): + a00 = Arrow(start=[-2, 3, 0], end=[2, 3, 0], color=YELLOW) + a11 = Arrow(start=[-2, 2, 0], end=[2, 2, 0], tip_shape=ArrowTriangleTip) + a12 = Arrow(start=[-2, 1, 0], end=[2, 1, 0]) + a21 = Arrow(start=[-2, 0, 0], end=[2, 0, 0], tip_shape=ArrowSquareTip) + a22 = Arrow([-2, -1, 0], [2, -1, 0], tip_shape=ArrowSquareFilledTip) + a31 = Arrow([-2, -2, 0], [2, -2, 0], tip_shape=ArrowCircleTip) + a32 = Arrow([-2, -3, 0], [2, -3, 0], tip_shape=ArrowCircleFilledTip) + b11 = a11.copy().scale(0.5, scale_tips=True).next_to(a11, RIGHT) + b12 = a12.copy().scale(0.5, scale_tips=True).next_to(a12, RIGHT) + b21 = a21.copy().scale(0.5, scale_tips=True).next_to(a21, RIGHT) + self.add(a00, a11, a12, a21, a22, a31, a32, b11, b12, b21) """ CONFIG = { @@ -1086,3 +1391,49 @@ def __init__(self, **kwargs): class ArrowSquareFilledTip(ArrowFilledTip, ArrowSquareTip): r"""Square arrow tip with filled tip.""" pass + + +class Cutout(VMobject): + """A shape with smaller cutouts. + + .. warning:: + + Technically, this class behaves similar to a symmetric difference: if + parts of the ``mobjects`` are not located within the ``main_shape``, + these parts will be added to the resulting :class:`~.VMobject`. + + Parameters + ---------- + main_shape : :class:`~.VMobject` + The primary shape from which cutouts are made. + mobjects : :class:`~.VMobject` + The smaller shapes which are to be cut out of the ``main_shape``. + kwargs + Further keyword arguments that are passed to the constructor of + :class:`~.VMobject`. + + Examples + -------- + .. manim:: CutoutExample + + class CutoutExample(Scene): + def construct(self): + s1 = Square().scale(2.5) + s2 = Triangle().shift(DOWN + RIGHT).scale(0.5) + s3 = Square().shift(UP + RIGHT).scale(0.5) + s4 = RegularPolygon(5).shift(DOWN + LEFT).scale(0.5) + s5 = RegularPolygon(6).shift(UP + LEFT).scale(0.5) + c = Cutout(s1, s2, s3, s4, s5, fill_opacity=1, color=BLUE, stroke_color=RED) + self.play(Write(c), run_time=4) + self.wait() + """ + + def __init__(self, main_shape, *mobjects, **kwargs): + VMobject.__init__(self, **kwargs) + self.append_points(main_shape.get_points()) + if main_shape.get_direction() == "CW": + sub_direction = "CCW" + else: + sub_direction = "CW" + for mobject in mobjects: + self.append_points(mobject.force_direction(sub_direction).get_points()) diff --git a/manim/mobject/logo.py b/manim/mobject/logo.py new file mode 100644 index 0000000000..344797b81c --- /dev/null +++ b/manim/mobject/logo.py @@ -0,0 +1,155 @@ +"""Utilities for Manim's logo and banner.""" + +__all__ = ["ManimBanner"] + +from ..constants import LEFT, UP, RIGHT, DOWN, ORIGIN +from ..animation.composition import AnimationGroup, Succession +from ..animation.fading import FadeIn +from ..animation.transform import ApplyMethod +from ..mobject.geometry import Circle, Square, Triangle +from ..mobject.svg.tex_mobject import Tex, MathTex +from ..mobject.types.vectorized_mobject import VGroup +from ..utils.tex_templates import TexFontTemplates + + +class ManimBanner(VGroup): + r"""Convenience class representing Manim's banner. + + Can be animated using custom methods. + + Parameters + ---------- + dark_theme + If ``True`` (the default), the dark theme version of the logo + (with light text font) will be rendered. Otherwise, if ``False``, + the light theme version (with dark text font) is used. + + Examples + -------- + + .. manim:: BannerDarkBackground + + class BannerDarkBackground(Scene): + def construct(self): + banner = ManimBanner().scale(0.5).to_corner(DR) + self.play(FadeIn(banner)) + self.play(banner.expand()) + self.play(FadeOut(banner)) + + .. manim:: BannerLightBackground + + class BannerLightBackground(Scene): + def construct(self): + self.camera.background_color = "#ece6e2" + banner = ManimBanner(dark_theme=False).scale(0.5).to_corner(UR) + self.play(FadeIn(banner)) + self.play(banner.expand()) + self.play(FadeOut(banner)) + + """ + + def __init__(self, dark_theme: bool = True): + VGroup.__init__(self) + + logo_green = "#81b29a" + logo_blue = "#454866" + logo_red = "#e07a5f" + m_height_over_anim_height = 0.75748 + + self.font_color = "#ece6e2" if dark_theme else "#343434" + self.scale_factor = 1 + + self.M = MathTex(r"\mathbb{M}").scale(7).set_color(self.font_color) + self.M.shift(2.25 * LEFT + 1.5 * UP) + + self.circle = Circle(color=logo_green, fill_opacity=1).shift(LEFT) + self.square = Square(color=logo_blue, fill_opacity=1).shift(UP) + self.triangle = Triangle(color=logo_red, fill_opacity=1).shift(RIGHT) + self.add(self.triangle, self.square, self.circle, self.M) + self.move_to(ORIGIN) + + anim = VGroup() + for i, ch in enumerate("anim"): + tex = Tex( + "\\textbf{" + ch + "}", + tex_template=TexFontTemplates.gnu_freeserif_freesans, + ) + if i != 0: + tex.next_to(anim, buff=0.01) + tex.align_to(self.M, DOWN) + anim.add(tex) + anim.set_color(self.font_color).set_height( + m_height_over_anim_height * self.M.get_height() + ) + + # Note: "anim" is only shown in the expanded state + # and thus not yet added to the submobjects of self. + self.anim = anim + self.anim.set_opacity(0) + + def scale(self, scale_factor: float, **kwargs) -> "ManimBanner": + """Scale the banner by the specified scale factor. + + Parameters + ---------- + scale_factor + The factor used for scaling the banner. + + Returns + ------- + :class:`~.ManimBanner` + The scaled banner. + """ + self.scale_factor *= scale_factor + # Note: self.anim is only added to self after expand() + if self.anim not in self.submobjects: + self.anim.scale(scale_factor, **kwargs) + return super().scale(scale_factor, **kwargs) + + def expand(self) -> Succession: + """An animation that expands Manim's logo into its banner. + + The returned animation transforms the banner from its initial + state (representing Manim's logo with just the icons) to its + expanded state (showing the full name together with the icons). + + See the class documentation for how to use this. + + .. note:: + + Before calling this method, the text "anim" is not a + submobject of the banner object. After the expansion, + it is added as a submobject so subsequent animations + to the banner object apply to the text "anim" as well. + + Returns + ------- + :class:`~.Succession` + An animation to be used in a :meth:`.Scene.play` call. + + """ + m_shape_offset = 6.25 * self.scale_factor + m_anim_buff = 0.06 + self.add(self.anim) + self.anim.next_to(self.M, buff=m_anim_buff).shift( + m_shape_offset * LEFT + ).align_to(self.M, DOWN) + move_left = AnimationGroup( + ApplyMethod(self.triangle.shift, m_shape_offset * LEFT), + ApplyMethod(self.square.shift, m_shape_offset * LEFT), + ApplyMethod(self.circle.shift, m_shape_offset * LEFT), + ApplyMethod(self.M.shift, m_shape_offset * LEFT), + ) + move_right = AnimationGroup( + ApplyMethod(self.triangle.shift, m_shape_offset * RIGHT), + ApplyMethod(self.square.shift, m_shape_offset * RIGHT), + ApplyMethod(self.circle.shift, m_shape_offset * RIGHT), + ApplyMethod(self.M.shift, 0 * LEFT), + AnimationGroup( + *[ApplyMethod(obj.set_opacity, 1) for obj in self.anim], lag_ratio=0.15 + ), + # It would be nice to have the last AnimationGroup replaced by + # FadeIn(self.anim, lag_ratio=1) + # Currently not working though. + ) + return Succession(move_left, move_right) diff --git a/manim/mobject/matrix.py b/manim/mobject/matrix.py index d6e4a7ed1a..becff30aad 100644 --- a/manim/mobject/matrix.py +++ b/manim/mobject/matrix.py @@ -144,7 +144,7 @@ def get_rows(self): Each VGroup contains a row of the matrix. """ return VGroup( - *[VGroup(*self.mob_matrix[i, :]) for i in range(self.mob_matrix.shape[1])] + *[VGroup(*self.mob_matrix[i, :]) for i in range(self.mob_matrix.shape[0])] ) def set_row_colors(self, *colors): diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py index 8f58c5d0d8..d0249fd989 100644 --- a/manim/mobject/mobject.py +++ b/manim/mobject/mobject.py @@ -15,7 +15,7 @@ from colour import Color import numpy as np -from .. import config, file_writer_config +from .. import config from ..constants import * from ..container import Container from ..utils.color import color_gradient, WHITE, BLACK, YELLOW_C @@ -96,6 +96,9 @@ def add(self, *mobjects): ------ :class:`ValueError` When a mobject tries to add itself. + :class:`TypeError` + When trying to add an object that is not an instance of :class:`Mobject`. + Notes ----- @@ -130,8 +133,11 @@ def add(self, *mobjects): ValueError: Mobject cannot contain self """ - if self in mobjects: - raise ValueError("Mobject cannot contain self") + for m in mobjects: + if not isinstance(m, Mobject): + raise TypeError("All submobjects must be of type Mobject") + if m is self: + raise ValueError("Mobject cannot contain self") self.submobjects = list_update(self.submobjects, mobjects) return self @@ -199,7 +205,7 @@ def show(self, camera=None): def save_image(self, name=None): self.get_image().save( - Path(file_writer_config["video_dir"]).joinpath((name or str(self)) + ".png") + Path(config.get_dir("video_dir")).joinpath((name or str(self)) + ".png") ) def copy(self): @@ -244,13 +250,38 @@ def get_updaters(self): def get_family_updaters(self): return list(it.chain(*[sm.get_updaters() for sm in self.get_family()])) - def add_updater(self, update_function, index=None, call_updater=True): + def add_updater(self, update_function, index=None, call_updater=False): + """Add an update function to this mobject. + + Examples + -------- + + .. manim:: RotationUpdater + + class RotationUpdater(Scene): + def construct(self): + def updater_forth(mobj, dt): + mobj.rotate_about_origin(dt) + def updater_back(mobj, dt): + mobj.rotate_about_origin(-dt) + line_reference = Line(ORIGIN, LEFT).set_color(WHITE) + line_moving = Line(ORIGIN, LEFT).set_color(YELLOW) + line_moving.add_updater(updater_forth) + self.add(line_reference, line_moving) + self.wait(2) + line_moving.remove_updater(updater_forth) + line_moving.add_updater(updater_back) + self.wait(2) + line_moving.remove_updater(updater_back) + self.wait(0.5) + + """ if index is None: self.updaters.append(update_function) else: self.updaters.insert(index, update_function) if call_updater: - self.update(0) + update_function(self, 0) return self def remove_updater(self, update_function): @@ -473,6 +504,26 @@ def next_to( index_of_submobject_to_align=None, coor_mask=np.array([1, 1, 1]), ): + """Move this mobject next to another mobject or coordinate. + + Examples + -------- + + .. manim:: GeometricShapes + :save_last_frame: + + class GeometricShapes(Scene): + def construct(self): + d = Dot() + c = Circle() + s = Square() + t = Triangle() + d.next_to(c, RIGHT) + s.next_to(c, LEFT) + t.next_to(c, DOWN) + self.add(d, c, s, t) + + """ if isinstance(mobject_or_point, Mobject): mob = mobject_or_point if index_of_submobject_to_align is not None: @@ -653,7 +704,7 @@ def set_color(self, color=YELLOW_C, family=True): if family: for submob in self.submobjects: submob.set_color(color, family=family) - self.color = color + self.color = Color(color) return self def set_color_by_gradient(self, *colors): @@ -1115,9 +1166,25 @@ def repeat_submobject(self, submob): return submob.copy() def interpolate(self, mobject1, mobject2, alpha, path_func=straight_path): - """ - Turns self into an interpolation between mobject1 - and mobject2. + """Turns this mobject into an interpolation between ``mobject1`` + and ``mobject2``. + + Examples + -------- + + .. manim:: DotInterpolation + :save_last_frame: + + class DotInterpolation(Scene): + def construct(self): + dotL = Dot(color=DARK_GREY) + dotL.shift(2 * RIGHT) + dotR = Dot(color=WHITE) + dotR.shift(2 * LEFT) + + dotMiddle = VMobject().interpolate(dotL, dotR, alpha=0.3) + + self.add(dotL, dotR, dotMiddle) """ self.points = path_func(mobject1.points, mobject2.points, alpha) self.interpolate_color(mobject1, mobject2, alpha) @@ -1181,7 +1248,5 @@ class Group(Mobject): """Groups together multiple Mobjects.""" def __init__(self, *mobjects, **kwargs): - if not all([isinstance(m, Mobject) for m in mobjects]): - raise TypeError("All submobjects must be of type Mobject") Mobject.__init__(self, **kwargs) self.add(*mobjects) diff --git a/manim/mobject/number_line.py b/manim/mobject/number_line.py index 7a8994c5df..4571f76c50 100644 --- a/manim/mobject/number_line.py +++ b/manim/mobject/number_line.py @@ -23,6 +23,7 @@ class NumberLine(Line): CONFIG = { "color": LIGHT_GREY, "unit_size": 1, + "width": None, # overrides unit_size if a value is passed "include_ticks": True, "tick_size": 0.1, "tick_frequency": 1, @@ -58,6 +59,9 @@ def __init__(self, **kwargs): Line.__init__( self, start - self.add_start * RIGHT, end + self.add_end * RIGHT, **kwargs ) + if self.width is not None: + self.set_width(self.width) + self.unit_size = self.get_unit_size() self.shift(-self.number_to_point(self.number_at_center)) self.init_leftmost_tick() @@ -142,7 +146,7 @@ def p2n(self, point): return self.point_to_number(point) def get_unit_size(self): - return (self.x_max - self.x_min) / self.get_length() + return self.get_length() / (self.x_max - self.x_min) def default_numbers_to_display(self): if self.numbers_to_show is not None: diff --git a/manim/mobject/numbers.py b/manim/mobject/numbers.py index 03c67dd3e1..6bfb1a1d5b 100644 --- a/manim/mobject/numbers.py +++ b/manim/mobject/numbers.py @@ -10,6 +10,36 @@ class DecimalNumber(VMobject): + """An mobject representing a decimal number. + + Examples + -------- + + .. manim:: MovingSquareWithUpdaters + + class MovingSquareWithUpdaters(Scene): + def construct(self): + decimal = DecimalNumber( + 0, + show_ellipsis=True, + num_decimal_places=3, + include_sign=True, + ) + square = Square().to_edge(UP) + + decimal.add_updater(lambda d: d.next_to(square, RIGHT)) + decimal.add_updater(lambda d: d.set_value(square.get_center()[1])) + self.add(square, decimal) + self.play( + square.to_edge, + DOWN, + rate_func=there_and_back, + run_time=5, + ) + self.wait() + + """ + CONFIG = { "num_decimal_places": 2, "include_sign": False, @@ -190,6 +220,51 @@ class Variable(VMobject): # Using math mode for the label on_screen_int_var = Variable(int_var, "{a}_{i}", var_type=Integer) + .. manim:: VariablesWithValueTracker + + class VariablesWithValueTracker(Scene): + def construct(self): + var = 0.5 + on_screen_var = Variable(var, Text("var"), num_decimal_places=3) + + # You can also change the colours for the label and value + on_screen_var.label.set_color(RED) + on_screen_var.value.set_color(GREEN) + + self.play(Write(on_screen_var)) + # The above line will just display the variable with + # its initial value on the screen. If you also wish to + # update it, you can do so by accessing the `tracker` attribute + self.wait() + var_tracker = on_screen_var.tracker + var = 10.5 + self.play(var_tracker.set_value, var) + self.wait() + + int_var = 0 + on_screen_int_var = Variable( + int_var, Text("int_var"), var_type=Integer + ).next_to(on_screen_var, DOWN) + on_screen_int_var.label.set_color(RED) + on_screen_int_var.value.set_color(GREEN) + + self.play(Write(on_screen_int_var)) + self.wait() + var_tracker = on_screen_int_var.tracker + var = 10.5 + self.play(var_tracker.set_value, var) + self.wait() + + # If you wish to have a somewhat more complicated label for your + # variable with subscripts, superscripts, etc. the default class + # for the label is MathTex + subscript_label_var = 10 + on_screen_subscript_var = Variable(subscript_label_var, "{a}_{i}").next_to( + on_screen_int_var, DOWN + ) + self.play(Write(on_screen_subscript_var)) + self.wait() + """ def __init__( diff --git a/manim/mobject/shape_matchers.py b/manim/mobject/shape_matchers.py index a9bf1e0cb8..24a761e6be 100644 --- a/manim/mobject/shape_matchers.py +++ b/manim/mobject/shape_matchers.py @@ -43,7 +43,7 @@ def pointwise_become_partial(self, mobject, a, b): self.set_fill(opacity=b * self.original_fill_opacity) return self - def set_style_data( + def set_style( self, stroke_color=None, stroke_width=None, @@ -52,7 +52,7 @@ def set_style_data( family=True, ): # Unchangable style, except for fill_opacity - VMobject.set_style_data( + VMobject.set_style( self, stroke_color=BLACK, stroke_width=0, diff --git a/manim/mobject/svg/brace.py b/manim/mobject/svg/brace.py index 379d878a22..997e0911d5 100644 --- a/manim/mobject/svg/brace.py +++ b/manim/mobject/svg/brace.py @@ -1,6 +1,6 @@ """Mobject representing curly braces.""" -__all__ = ["Brace", "BraceLabel", "BraceText"] +__all__ = ["Brace", "BraceLabel", "BraceText", "BraceBetweenPoints"] import numpy as np @@ -12,11 +12,42 @@ from ...mobject.svg.tex_mobject import MathTex from ...mobject.svg.tex_mobject import Tex from ...mobject.types.vectorized_mobject import VMobject +from ...mobject.geometry import Line from ...utils.config_ops import digest_config from ...utils.space_ops import get_norm class Brace(MathTex): + """Takes a mobject and draws a brace adjacent to it. + + Passing a direction vector determines the direction from which the + brace is drawn. By default it is drawn from below. + + Parameters + ---------- + mobject : :class:`~.Mobject` + The mobject adjacent to which the brace is placed. + direction : Optional[Union[:class:`list`, :class:`numpy.array`]] + The direction from which the brace faces the mobject. + + See Also + -------- + :class:`BraceBetweenPoints` + + Examples + -------- + .. manim:: BraceExample + + class BraceExample(Scene): + def construct(self): + circle = Circle() + brace = Brace(circle, direction=RIGHT) + self.play(ShowCreation(circle)) + self.play(ShowCreation(brace)) + self.wait(2) + + """ + CONFIG = { "buff": 0.2, "width_multiplier": 2, @@ -129,3 +160,42 @@ def change_brace_label(self, obj, *text): class BraceText(BraceLabel): CONFIG = {"label_constructor": Tex} + + +class BraceBetweenPoints(Brace): + """Similar to Brace, but instead of taking a mobject it uses 2 + points to place the brace. + + A fitting direction for the brace is + computed, but it still can be manually overridden. + If the points go from left to right, the brace is drawn from below. + Swapping the points places the brace on the opposite side. + + Parameters + ---------- + point_1 : Union[:class:`list`, :class:`numpy.array`] + The first point. + point_2 : Union[:class:`list`, :class:`numpy.array`] + The second point. + direction : Optional[Union[:class:`list`, :class:`numpy.array`]] + The direction from which the brace faces towards the points. + + Examples + -------- + .. manim:: BraceBPExample + + class BraceBPExample(Scene): + def construct(self): + p1 = [0,0,0] + p2 = [1,2,0] + brace = BraceBetweenPoints(p1,p2) + self.play(ShowCreation(NumberPlane())) + self.play(ShowCreation(brace)) + self.wait(2) + """ + + def __init__(self, point_1, point_2, direction=ORIGIN, **kwargs): + if all(direction == ORIGIN): + line_vector = np.array(point_2) - np.array(point_1) + direction = np.array([line_vector[1], -line_vector[0], 0]) + Brace.__init__(self, Line(point_1, point_2), direction=direction, **kwargs) diff --git a/manim/mobject/svg/code_mobject.py b/manim/mobject/svg/code_mobject.py index 86a98e6a2b..a4a1f8efe1 100644 --- a/manim/mobject/svg/code_mobject.py +++ b/manim/mobject/svg/code_mobject.py @@ -240,9 +240,10 @@ def ensure_valid_file(self): if os.path.exists(path): self.file_path = path return - raise IOError( - "File %s not found. Please specify a correct file path" % self.file_name + error = "From: {}, could not find {} at either of these locations: {}".format( + os.getcwd(), self.file_name, possible_paths ) + raise IOError(error) def gen_line_numbers(self): """Function to generate line_numbers. @@ -369,8 +370,6 @@ def gen_code_json(self): self.tab_spaces = [] code_json_line_index = -1 for line_index in range(0, lines.__len__()): - if lines[line_index].__len__() == 0: - continue # print(lines[line_index]) self.code_json.append([]) code_json_line_index = code_json_line_index + 1 @@ -397,8 +396,9 @@ def gen_code_json(self): "\t" * indentation_chars_count + lines[line_index][start_point:] ) indentation_chars_count = 0 - while lines[line_index][indentation_chars_count] == "\t": - indentation_chars_count = indentation_chars_count + 1 + if lines[line_index]: + while lines[line_index][indentation_chars_count] == "\t": + indentation_chars_count = indentation_chars_count + 1 self.tab_spaces.append(indentation_chars_count) # print(lines[line_index]) lines[line_index] = self.correct_non_span(lines[line_index]) diff --git a/manim/mobject/svg/drawings.py b/manim/mobject/svg/drawings.py deleted file mode 100644 index 31084394db..0000000000 --- a/manim/mobject/svg/drawings.py +++ /dev/null @@ -1,1010 +0,0 @@ -"""Mobjects representing predefined SVG drawings.""" - -__all__ = [ - "Lightbulb", - "BitcoinLogo", - "Guitar", - "Speedometer", - "AoPSLogo", - "PartyHat", - "Laptop", - "PatreonLogo", - "VideoIcon", - "VideoSeries", - "Headphones", - "Clock", - "ClockPassesTime", - "Bubble", - "SpeechBubble", - "DoubleSpeechBubble", - "ThoughtBubble", - "Car", - "VectorizedEarth", - "Logo", - "DeckOfCards", - "PlayingCard", - "SuitSymbol", -] - - -import itertools as it -import string - -from ... import config -from ...animation.animation import Animation -from ...animation.rotation import Rotating -from ...constants import * -from ...mobject.geometry import AnnularSector -from ...mobject.geometry import Arc -from ...mobject.geometry import Circle -from ...mobject.geometry import Line -from ...mobject.geometry import Polygon -from ...mobject.geometry import Rectangle -from ...mobject.geometry import Square -from ...mobject.mobject import Mobject -from ...mobject.svg.svg_mobject import SVGMobject -from ...mobject.svg.tex_mobject import MathTex -from ...mobject.svg.tex_mobject import Tex -from ...mobject.three_dimensions import Cube -from ...mobject.types.vectorized_mobject import VGroup -from ...mobject.types.vectorized_mobject import VMobject -from ...mobject.types.vectorized_mobject import VectorizedPoint -from ...utils.bezier import interpolate -from ...utils.config_ops import digest_config -from ...utils.rate_functions import linear -from ...utils.space_ops import angle_of_vector -from ...utils.space_ops import complex_to_R3 -from ...utils.space_ops import rotate_vector -from ...utils.color import ( - YELLOW, - WHITE, - DARK_GREY, - MAROON_B, - PURPLE, - GREEN, - BLACK, - LIGHT_GREY, - GREY, - BLUE_B, - BLUE_D, -) - - -class Lightbulb(SVGMobject): - CONFIG = { - "file_name": "lightbulb", - "height": 1, - "stroke_color": YELLOW, - "stroke_width": 3, - "fill_color": YELLOW, - "fill_opacity": 0, - } - - -class BitcoinLogo(SVGMobject): - CONFIG = { - "file_name": "Bitcoin_logo", - "height": 1, - "fill_color": "#f7931a", - "inner_color": WHITE, - "fill_opacity": 1, - "stroke_width": 0, - } - - def __init__(self, **kwargs): - SVGMobject.__init__(self, **kwargs) - self[0].set_fill(self.fill_color, self.fill_opacity) - self[1].set_fill(self.inner_color, 1) - - -class Guitar(SVGMobject): - CONFIG = { - "file_name": "guitar", - "height": 2.5, - "fill_color": DARK_GREY, - "fill_opacity": 1, - "stroke_color": WHITE, - "stroke_width": 0.5, - } - - -class Speedometer(VMobject): - CONFIG = { - "arc_angle": 4 * np.pi / 3, - "num_ticks": 8, - "tick_length": 0.2, - "needle_width": 0.1, - "needle_height": 0.8, - "needle_color": YELLOW, - } - - def generate_points(self): - start_angle = np.pi / 2 + self.arc_angle / 2 - end_angle = np.pi / 2 - self.arc_angle / 2 - self.add(Arc(start_angle=start_angle, angle=-self.arc_angle)) - tick_angle_range = np.linspace(start_angle, end_angle, self.num_ticks) - for index, angle in enumerate(tick_angle_range): - vect = rotate_vector(RIGHT, angle) - tick = Line((1 - self.tick_length) * vect, vect) - label = MathTex(str(10 * index)) - label.set_height(self.tick_length) - label.shift((1 + self.tick_length) * vect) - self.add(tick, label) - - needle = Polygon( - LEFT, - UP, - RIGHT, - stroke_width=0, - fill_opacity=1, - fill_color=self.needle_color, - ) - needle.stretch_to_fit_width(self.needle_width) - needle.stretch_to_fit_height(self.needle_height) - needle.rotate(start_angle - np.pi / 2, about_point=ORIGIN) - self.add(needle) - self.needle = needle - - self.center_offset = self.get_center() - - def get_center(self): - result = VMobject.get_center(self) - if hasattr(self, "center_offset"): - result -= self.center_offset - return result - - def get_needle_tip(self): - return self.needle.get_anchors()[1] - - def get_needle_angle(self): - return angle_of_vector(self.get_needle_tip() - self.get_center()) - - def rotate_needle(self, angle): - self.needle.rotate(angle, about_point=self.get_center()) - return self - - def move_needle_to_velocity(self, velocity): - max_velocity = 10 * (self.num_ticks - 1) - proportion = float(velocity) / max_velocity - start_angle = np.pi / 2 + self.arc_angle / 2 - target_angle = start_angle - self.arc_angle * proportion - self.rotate_needle(target_angle - self.get_needle_angle()) - return self - - -class AoPSLogo(SVGMobject): - CONFIG = { - "file_name": "aops_logo", - "height": 1.5, - } - - def __init__(self, **kwargs): - SVGMobject.__init__(self, **kwargs) - self.set_stroke(WHITE, width=0) - colors = [BLUE_E, "#008445", GREEN_B] - index_lists = [ - (10, 11, 12, 13, 14, 21, 22, 23, 24, 27, 28, 29, 30), - (0, 1, 2, 3, 4, 15, 16, 17, 26), - (5, 6, 7, 8, 9, 18, 19, 20, 25), - ] - for color, index_list in zip(colors, index_lists): - for i in index_list: - self.submobjects[i].set_fill(color, opacity=1) - - self.set_height(self.height) - self.center() - - -class PartyHat(SVGMobject): - CONFIG = { - "file_name": "party_hat", - "height": 1.5, - "stroke_width": 0, - "fill_opacity": 1, - "frills_colors": [MAROON_B, PURPLE], - "cone_color": GREEN, - "dots_colors": [YELLOW], - } - NUM_FRILLS = 7 - NUM_DOTS = 6 - - def __init__(self, **kwargs): - SVGMobject.__init__(self, **kwargs) - self.set_height(self.height) - - self.frills = VGroup(*self[: self.NUM_FRILLS]) - self.cone = self[self.NUM_FRILLS] - self.dots = VGroup(*self[self.NUM_FRILLS + 1 :]) - - self.frills.set_color_by_gradient(*self.frills_colors) - self.cone.set_color(self.cone_color) - self.dots.set_color_by_gradient(*self.dots_colors) - - -class Laptop(VGroup): - CONFIG = { - "width": 3, - "body_dimensions": [4, 3, 0.05], - "screen_thickness": 0.01, - "keyboard_width_to_body_width": 0.9, - "keyboard_height_to_body_height": 0.5, - "screen_width_to_screen_plate_width": 0.9, - "key_color_kwargs": { - "stroke_width": 0, - "fill_color": BLACK, - "fill_opacity": 1, - }, - "fill_opacity": 1, - "stroke_width": 0, - "body_color": LIGHT_GREY, - "shaded_body_color": GREY, - "open_angle": np.pi / 4, - } - - def __init__(self, **kwargs): - super().__init__(**kwargs) - body = Cube(side_length=1) - for dim, scale_factor in enumerate(self.body_dimensions): - body.stretch(scale_factor, dim=dim) - body.set_width(self.width) - body.set_fill(self.shaded_body_color, opacity=1) - body.sort(lambda p: p[2]) - body[-1].set_fill(self.body_color) - screen_plate = body.copy() - keyboard = VGroup( - *[ - VGroup( - *[Square(**self.key_color_kwargs) for x in range(12 - y % 2)] - ).arrange(RIGHT, buff=SMALL_BUFF) - for y in range(4) - ] - ).arrange(DOWN, buff=MED_SMALL_BUFF) - keyboard.stretch_to_fit_width( - self.keyboard_width_to_body_width * body.get_width(), - ) - keyboard.stretch_to_fit_height( - self.keyboard_height_to_body_height * body.get_height(), - ) - keyboard.next_to(body, OUT, buff=0.1 * SMALL_BUFF) - keyboard.shift(MED_SMALL_BUFF * UP) - body.add(keyboard) - - screen_plate.stretch(self.screen_thickness / self.body_dimensions[2], dim=2) - screen = Rectangle( - stroke_width=0, - fill_color=BLACK, - fill_opacity=1, - ) - screen.replace(screen_plate, stretch=True) - screen.scale_in_place(self.screen_width_to_screen_plate_width) - screen.next_to(screen_plate, OUT, buff=0.1 * SMALL_BUFF) - screen_plate.add(screen) - screen_plate.next_to(body, UP, buff=0) - screen_plate.rotate( - self.open_angle, RIGHT, about_point=screen_plate.get_bottom() - ) - self.screen_plate = screen_plate - self.screen = screen - - axis = Line( - body.get_corner(UP + LEFT + OUT), - body.get_corner(UP + RIGHT + OUT), - color=BLACK, - stroke_width=2, - ) - self.axis = axis - - self.add(body, screen_plate, axis) - self.rotate(5 * np.pi / 12, LEFT, about_point=ORIGIN) - self.rotate(np.pi / 6, DOWN, about_point=ORIGIN) - - -class PatreonLogo(SVGMobject): - CONFIG = { - "file_name": "patreon_logo", - "fill_color": "#F96854", - # "fill_color" : WHITE, - "fill_opacity": 1, - "stroke_width": 0, - "width": 4, - } - - def __init__(self, **kwargs): - SVGMobject.__init__(self, **kwargs) - self.set_width(self.width) - self.center() - - -class VideoIcon(SVGMobject): - CONFIG = { - "file_name": "video_icon", - } - - def __init__(self, **kwargs): - SVGMobject.__init__(self, **kwargs) - self.width = config["frame_width"] / 12.0 - self.center() - self.set_width(self.width) - self.set_stroke(color=WHITE, width=0) - self.set_fill(color=WHITE, opacity=1) - - -class VideoSeries(VGroup): - CONFIG = { - "num_videos": 11, - "gradient_colors": [BLUE_B, BLUE_D], - } - - def __init__(self, **kwargs): - digest_config(self, kwargs) - videos = [VideoIcon() for x in range(self.num_videos)] - VGroup.__init__(self, *videos, **kwargs) - self.arrange() - self.set_width(config["frame_width"] - config["med_large_buff"]) - self.set_color_by_gradient(*self.gradient_colors) - - -class Headphones(SVGMobject): - CONFIG = { - "file_name": "headphones", - "height": 2, - "y_stretch_factor": 0.5, - "color": GREY, - } - - def __init__(self, **kwargs): - digest_config(self, kwargs) - SVGMobject.__init__(self, file_name=self.file_name, **kwargs) - self.stretch(self.y_stretch_factor, 1) - self.set_height(self.height) - self.set_stroke(width=0) - self.set_fill(color=self.color) - - -class Clock(VGroup): - CONFIG = {} - - def __init__(self, **kwargs): - circle = Circle(color=WHITE) - ticks = [] - for x in range(12): - alpha = x / 12.0 - point = complex_to_R3(np.exp(2 * np.pi * alpha * complex(0, 1))) - length = 0.2 if x % 3 == 0 else 0.1 - ticks.append(Line(point, (1 - length) * point)) - self.hour_hand = Line(ORIGIN, 0.3 * UP) - self.minute_hand = Line(ORIGIN, 0.6 * UP) - # for hand in self.hour_hand, self.minute_hand: - # #Balance out where the center is - # hand.add(VectorizedPoint(-hand.get_end())) - - VGroup.__init__(self, circle, self.hour_hand, self.minute_hand, *ticks) - - -class ClockPassesTime(Animation): - CONFIG = { - "run_time": 5, - "hours_passed": 12, - "rate_func": linear, - } - - def __init__(self, clock, **kwargs): - digest_config(self, kwargs) - assert isinstance(clock, Clock) - rot_kwargs = {"axis": OUT, "about_point": clock.get_center()} - hour_radians = -self.hours_passed * 2 * np.pi / 12 - self.hour_rotation = Rotating( - clock.hour_hand, radians=hour_radians, **rot_kwargs - ) - self.hour_rotation.begin() - self.minute_rotation = Rotating( - clock.minute_hand, radians=12 * hour_radians, **rot_kwargs - ) - self.minute_rotation.begin() - Animation.__init__(self, clock, **kwargs) - - def interpolate_mobject(self, alpha): - for rotation in self.hour_rotation, self.minute_rotation: - rotation.interpolate_mobject(alpha) - - -class Bubble(SVGMobject): - CONFIG = { - "direction": LEFT, - "center_point": ORIGIN, - "content_scale_factor": 0.75, - "height": 5, - "width": 8, - "bubble_center_adjustment_factor": 1.0 / 8, - "file_name": None, - "fill_color": BLACK, - "fill_opacity": 0.8, - "stroke_color": WHITE, - "stroke_width": 3, - } - - def __init__(self, **kwargs): - digest_config(self, kwargs, locals()) - if self.file_name is None: - raise Exception("Must invoke Bubble subclass") - try: - SVGMobject.__init__(self, **kwargs) - except IOError as err: - self.file_name = os.path.join(FILE_DIR, self.file_name) - SVGMobject.__init__(self, **kwargs) - self.center() - self.stretch_to_fit_height(self.height) - self.stretch_to_fit_width(self.width) - if self.direction[0] > 0: - self.flip() - self.direction_was_specified = "direction" in kwargs - self.content = Mobject() - - def get_tip(self): - # TODO, find a better way - return self.get_corner(DOWN + self.direction) - 0.6 * self.direction - - def get_bubble_center(self): - factor = self.bubble_center_adjustment_factor - return self.get_center() + factor * self.get_height() * UP - - def move_tip_to(self, point): - mover = VGroup(self) - if self.content is not None: - mover.add(self.content) - mover.shift(point - self.get_tip()) - return self - - def flip(self, axis=UP): - Mobject.flip(self, axis=axis) - if abs(axis[1]) > 0: - self.direction = -np.array(self.direction) - return self - - def pin_to(self, mobject): - mob_center = mobject.get_center() - want_to_flip = np.sign(mob_center[0]) != np.sign(self.direction[0]) - can_flip = not self.direction_was_specified - if want_to_flip and can_flip: - self.flip() - boundary_point = mobject.get_critical_point(UP - self.direction) - vector_from_center = 1.0 * (boundary_point - mob_center) - self.move_tip_to(mob_center + vector_from_center) - return self - - def position_mobject_inside(self, mobject): - scaled_width = self.content_scale_factor * self.get_width() - if mobject.get_width() > scaled_width: - mobject.set_width(scaled_width) - mobject.shift(self.get_bubble_center() - mobject.get_center()) - return mobject - - def add_content(self, mobject): - self.position_mobject_inside(mobject) - self.content = mobject - return self.content - - def write(self, *text): - self.add_content(Tex(*text)) - return self - - def resize_to_content(self): - target_width = self.content.get_width() - target_width += max(MED_LARGE_BUFF, 2) - target_height = self.content.get_height() - target_height += 2.5 * LARGE_BUFF - tip_point = self.get_tip() - self.stretch_to_fit_width(target_width) - self.stretch_to_fit_height(target_height) - self.move_tip_to(tip_point) - self.position_mobject_inside(self.content) - - def clear(self): - self.add_content(VMobject()) - return self - - -class SpeechBubble(Bubble): - CONFIG = {"file_name": "Bubbles_speech.svg", "height": 4} - - -class DoubleSpeechBubble(Bubble): - CONFIG = {"file_name": "Bubbles_double_speech.svg", "height": 4} - - -class ThoughtBubble(Bubble): - CONFIG = { - "file_name": "Bubbles_thought.svg", - } - - def __init__(self, **kwargs): - Bubble.__init__(self, **kwargs) - self.submobjects.sort(key=lambda m: m.get_bottom()[1]) - - def make_green_screen(self): - self.submobjects[-1].set_fill(GREEN_SCREEN, opacity=1) - return self - - -class Car(SVGMobject): - CONFIG = { - "file_name": "Car", - "height": 1, - "color": LIGHT_GREY, - "light_colors": [BLACK, BLACK], - } - - def __init__(self, **kwargs): - SVGMobject.__init__(self, **kwargs) - - path = self.submobjects[0] - subpaths = path.get_subpaths() - path.clear_points() - for indices in [(0, 1), (2, 3), (4, 6, 7), (5,), (8,)]: - part = VMobject() - for index in indices: - part.append_points(subpaths[index]) - path.add(part) - - self.set_height(self.height) - self.set_stroke(color=WHITE, width=0) - self.set_fill(self.color, opacity=1) - - orientation_line = Line(self.get_left(), self.get_right()) - orientation_line.set_stroke(width=0) - self.add(orientation_line) - self.orientation_line = orientation_line - - for light, color in zip(self.get_lights(), self.light_colors): - light.set_fill(color, 1) - light.is_subpath = False - - self.add_treds_to_tires() - - def move_to(self, point_or_mobject): - vect = rotate_vector(UP + LEFT, self.orientation_line.get_angle()) - self.next_to(point_or_mobject, vect, buff=0) - return self - - def get_front_line(self): - return DashedLine( - self.get_corner(UP + RIGHT), - self.get_corner(DOWN + RIGHT), - color=DISTANCE_COLOR, - dash_length=0.05, - ) - - def add_treds_to_tires(self): - for tire in self.get_tires(): - radius = tire.get_width() / 2 - center = tire.get_center() - tred = Line( - 0.7 * radius * RIGHT, 1.1 * radius * RIGHT, stroke_width=2, color=BLACK - ) - tred.rotate(PI / 5, about_point=tred.get_end()) - for theta in np.arange(0, 2 * np.pi, np.pi / 4): - new_tred = tred.copy() - new_tred.rotate(theta, about_point=ORIGIN) - new_tred.shift(center) - tire.add(new_tred) - return self - - def get_tires(self): - return VGroup(self[1][0], self[1][1]) - - def get_lights(self): - return VGroup(self.get_front_light(), self.get_rear_light()) - - def get_front_light(self): - return self[1][3] - - def get_rear_light(self): - return self[1][4] - - -class VectorizedEarth(SVGMobject): - CONFIG = { - "file_name": "earth", - "height": 1.5, - "fill_color": BLACK, - } - - def __init__(self, **kwargs): - SVGMobject.__init__(self, **kwargs) - circle = Circle( - stroke_width=3, - stroke_color=GREEN, - fill_opacity=1, - fill_color=BLUE_C, - ) - circle.replace(self) - self.add_to_back(circle) - - -class Logo(VMobject): - CONFIG = { - "pupil_radius": 1.0, - "outer_radius": 2.0, - "iris_background_blue": "#74C0E3", - "iris_background_brown": "#8C6239", - "blue_spike_colors": [ - "#528EA3", - "#3E6576", - "#224C5B", - BLACK, - ], - "brown_spike_colors": [ - "#754C24", - "#603813", - "#42210b", - BLACK, - ], - "n_spike_layers": 4, - "n_spikes": 28, - "spike_angle": TAU / 28, - } - - def __init__(self, **kwargs): - VMobject.__init__(self, **kwargs) - self.add_iris_back() - self.add_spikes() - self.add_pupil() - - def add_iris_back(self): - blue_iris_back = AnnularSector( - inner_radius=self.pupil_radius, - outer_radius=self.outer_radius, - angle=270 * DEGREES, - start_angle=180 * DEGREES, - fill_color=self.iris_background_blue, - fill_opacity=1, - stroke_width=0, - ) - brown_iris_back = AnnularSector( - inner_radius=self.pupil_radius, - outer_radius=self.outer_radius, - angle=90 * DEGREES, - start_angle=90 * DEGREES, - fill_color=self.iris_background_brown, - fill_opacity=1, - stroke_width=0, - ) - self.iris_background = VGroup( - blue_iris_back, - brown_iris_back, - ) - self.add(self.iris_background) - - def add_spikes(self): - layers = VGroup() - radii = np.linspace( - self.outer_radius, - self.pupil_radius, - self.n_spike_layers, - endpoint=False, - ) - radii[:2] = radii[1::-1] # Swap first two - if self.n_spike_layers > 2: - radii[-1] = interpolate(radii[-1], self.pupil_radius, 0.25) - - for radius in radii: - tip_angle = self.spike_angle - half_base = radius * np.tan(tip_angle) - triangle, right_half_triangle = [ - Polygon( - radius * UP, - half_base * RIGHT, - vertex3, - fill_opacity=1, - stroke_width=0, - ) - for vertex3 in ( - half_base * LEFT, - ORIGIN, - ) - ] - left_half_triangle = right_half_triangle.copy() - left_half_triangle.flip(UP, about_point=ORIGIN) - - n_spikes = self.n_spikes - full_spikes = [ - triangle.copy().rotate(-angle, about_point=ORIGIN) - for angle in np.linspace(0, TAU, n_spikes, endpoint=False) - ] - index = (3 * n_spikes) // 4 - if radius == radii[0]: - layer = VGroup(*full_spikes) - layer.rotate(-TAU / n_spikes / 2, about_point=ORIGIN) - layer.brown_index = index - else: - half_spikes = [ - right_half_triangle.copy(), - left_half_triangle.copy().rotate( - 90 * DEGREES, - about_point=ORIGIN, - ), - right_half_triangle.copy().rotate( - 90 * DEGREES, - about_point=ORIGIN, - ), - left_half_triangle.copy(), - ] - layer = VGroup( - *it.chain( - half_spikes[:1], - full_spikes[1:index], - half_spikes[1:3], - full_spikes[index + 1 :], - half_spikes[3:], - ) - ) - layer.brown_index = index + 1 - - layers.add(layer) - - # Color spikes - blues = self.blue_spike_colors - browns = self.brown_spike_colors - for layer, blue, brown in zip(layers, blues, browns): - index = layer.brown_index - layer[:index].set_color(blue) - layer[index:].set_color(brown) - - self.spike_layers = layers - self.add(layers) - - def add_pupil(self): - self.pupil = Circle( - radius=self.pupil_radius, - fill_color=BLACK, - fill_opacity=1, - stroke_width=0, - sheen=0.0, - ) - self.pupil.rotate(90 * DEGREES) - self.add(self.pupil) - - def cut_pupil(self): - pupil = self.pupil - center = pupil.get_center() - new_pupil = VGroup( - *[ - pupil.copy().pointwise_become_partial(pupil, a, b) - for (a, b) in [(0.25, 1), (0, 0.25)] - ] - ) - for sector in new_pupil: - sector.add_cubic_bezier_curve_to( - [sector.points[-1], *[center] * 3, *[sector.points[0]] * 2] - ) - self.remove(pupil) - self.add(new_pupil) - self.pupil = new_pupil - - def get_blue_part_and_brown_part(self): - if len(self.pupil) == 1: - self.cut_pupil() - # circle = Circle() - # circle.set_stroke(width=0) - # circle.set_fill(BLACK, opacity=1) - # circle.match_width(self) - # circle.move_to(self) - blue_part = VGroup( - self.iris_background[0], - *[layer[: layer.brown_index] for layer in self.spike_layers], - self.pupil[0], - ) - brown_part = VGroup( - self.iris_background[1], - *[layer[layer.brown_index :] for layer in self.spike_layers], - self.pupil[1], - ) - return blue_part, brown_part - - -# Cards -class DeckOfCards(VGroup): - def __init__(self, **kwargs): - possible_values = list(map(str, list(range(1, 11)))) + ["J", "Q", "K"] - possible_suits = ["hearts", "diamonds", "spades", "clubs"] - VGroup.__init__( - self, - *[ - PlayingCard(value=value, suit=suit, **kwargs) - for value in possible_values - for suit in possible_suits - ], - ) - - -class PlayingCard(VGroup): - CONFIG = { - "value": None, - "suit": None, - "key": None, # String like "8H" or "KS" - "height": 2, - "height_to_width": 3.5 / 2.5, - "card_height_to_symbol_height": 7, - "card_width_to_corner_num_width": 10, - "card_height_to_corner_num_height": 10, - "color": LIGHT_GREY, - "turned_over": False, - "possible_suits": ["hearts", "diamonds", "spades", "clubs"], - "possible_values": list(map(str, list(range(2, 11)))) + ["J", "Q", "K", "A"], - } - - def __init__(self, key=None, **kwargs): - VGroup.__init__(self, key=key, **kwargs) - - def generate_points(self): - self.add( - Rectangle( - height=self.height, - width=self.height / self.height_to_width, - stroke_color=WHITE, - stroke_width=2, - fill_color=self.color, - fill_opacity=1, - ) - ) - if self.turned_over: - self.set_fill(DARK_GREY) - self.set_stroke(LIGHT_GREY) - contents = VectorizedPoint(self.get_center()) - else: - value = self.get_value() - symbol = self.get_symbol() - design = self.get_design(value, symbol) - corner_numbers = self.get_corner_numbers(value, symbol) - contents = VGroup(design, corner_numbers) - self.design = design - self.corner_numbers = corner_numbers - self.add(contents) - - def get_value(self): - value = self.value - if value is None: - if self.key is not None: - value = self.key[:-1] - else: - value = random.choice(self.possible_values) - value = string.upper(str(value)) - if value == "1": - value = "A" - if value not in self.possible_values: - raise Exception("Invalid card value") - - face_card_to_value = { - "J": 11, - "Q": 12, - "K": 13, - "A": 14, - } - try: - self.numerical_value = int(value) - except: - self.numerical_value = face_card_to_value[value] - return value - - def get_symbol(self): - suit = self.suit - if suit is None: - if self.key is not None: - suit = dict([(string.upper(s[0]), s) for s in self.possible_suits])[ - string.upper(self.key[-1]) - ] - else: - suit = random.choice(self.possible_suits) - if suit not in self.possible_suits: - raise Exception("Invalud suit value") - self.suit = suit - symbol_height = float(self.height) / self.card_height_to_symbol_height - symbol = SuitSymbol(suit, height=symbol_height) - return symbol - - def get_design(self, value, symbol): - if value == "A": - return self.get_ace_design(symbol) - if value in list(map(str, list(range(2, 11)))): - return self.get_number_design(value, symbol) - else: - return self.get_face_card_design(value, symbol) - - def get_ace_design(self, symbol): - design = symbol.copy().scale(1.5) - design.move_to(self) - return design - - def get_number_design(self, value, symbol): - num = int(value) - n_rows = { - 2: 2, - 3: 3, - 4: 2, - 5: 2, - 6: 3, - 7: 3, - 8: 3, - 9: 4, - 10: 4, - }[num] - n_cols = 1 if num in [2, 3] else 2 - insertion_indices = { - 5: [0], - 7: [0], - 8: [0, 1], - 9: [1], - 10: [0, 2], - }.get(num, []) - - top = self.get_top() + symbol.get_height() * DOWN - bottom = self.get_bottom() + symbol.get_height() * UP - column_points = [ - interpolate(top, bottom, alpha) for alpha in np.linspace(0, 1, n_rows) - ] - - design = VGroup(*[symbol.copy().move_to(point) for point in column_points]) - if n_cols == 2: - space = 0.2 * self.get_width() - column_copy = design.copy().shift(space * RIGHT) - design.shift(space * LEFT) - design.add(*column_copy) - design.add( - *[ - symbol.copy().move_to(center_of_mass(column_points[i : i + 2])) - for i in insertion_indices - ] - ) - for symbol in design: - if symbol.get_center()[1] < self.get_center()[1]: - symbol.rotate_in_place(np.pi) - return design - - def get_face_card_design(self, value, symbol): - return VGroup() - - def get_corner_numbers(self, value, symbol): - value_mob = Tex(value) - width = self.get_width() / self.card_width_to_corner_num_width - height = self.get_height() / self.card_height_to_corner_num_height - value_mob.set_width(width) - value_mob.stretch_to_fit_height(height) - value_mob.next_to( - self.get_corner(UP + LEFT), DOWN + RIGHT, buff=MED_LARGE_BUFF * width - ) - value_mob.set_color(symbol.get_color()) - corner_symbol = symbol.copy() - corner_symbol.set_width(width) - corner_symbol.next_to(value_mob, DOWN, buff=MED_SMALL_BUFF * width) - corner_group = VGroup(value_mob, corner_symbol) - opposite_corner_group = corner_group.copy() - opposite_corner_group.rotate(np.pi, about_point=self.get_center()) - - return VGroup(corner_group, opposite_corner_group) - - -class SuitSymbol(SVGMobject): - CONFIG = { - "height": 0.5, - "fill_opacity": 1, - "stroke_width": 0, - "red": "#D02028", - "black": BLACK, - } - - def __init__(self, suit_name, **kwargs): - digest_config(self, kwargs) - suits_to_colors = { - "hearts": self.red, - "diamonds": self.red, - "spades": self.black, - "clubs": self.black, - } - if suit_name not in suits_to_colors: - raise ValueError("Invalid suit name") - SVGMobject.__init__(self, file_name=suit_name, **kwargs) - - color = suits_to_colors[suit_name] - self.set_stroke(width=0) - self.set_fill(color, 1) - self.set_height(self.height) diff --git a/manim/mobject/svg/svg_mobject.py b/manim/mobject/svg/svg_mobject.py index 9fe65d26d2..dde7d59404 100644 --- a/manim/mobject/svg/svg_mobject.py +++ b/manim/mobject/svg/svg_mobject.py @@ -12,6 +12,7 @@ from xml.dom import minidom +from ... import config from ...constants import * from ...mobject.geometry import Circle from ...mobject.geometry import Rectangle @@ -30,6 +31,47 @@ def string_to_numbers(num_string): class SVGMobject(VMobject): + """A SVGMobject is a Vector Mobject constructed from an SVG (or XDV) file. + + SVGMobjects are constructed from the XML data within the SVG file + structure. As such, subcomponents from the XML data can be accessed via + the submobjects attribute. There is varying amounts of support for SVG + elements, experiment with SVG files at your own peril. + + Examples + -------- + + .. code-block:: python + + class Sample(Scene): + def construct(self): + self.play( + FadeIn(SVGMobject("manim-logo-sidebar.svg")) + ) + + Parameters + -------- + file_name : :class:`str` + The file's path name. When possible, the full path is preferred but a + relative path may be used as well. Relative paths are relative to the + directory specified by the `--assets_dir` command line argument. + + Other Parameters + -------- + should_center : :class:`bool` + Whether the SVGMobject should be centered to the origin. Defaults to `True`. + height : :class:`float` + Specify the final height of the SVG file. Defaults to 2 units. + width : :class:`float` + Specify the width the SVG file should occupy. Defaults to `None`. + unpack_groups : :class:`bool` + Whether the hierarchies of VGroups generated should be flattened. Defaults to `True`. + stroke_width : :class:`float` + The stroke width of the outer edge of an SVG path element. Defaults to `4`. + fill_opacity : :class:`float` + Specifies the opacity of the image. `1` is opaque, `0` is transparent. Defaults to `1`. + """ + CONFIG = { "should_center": True, "height": 2, @@ -50,21 +92,43 @@ def __init__(self, file_name=None, **kwargs): self.move_into_position() def ensure_valid_file(self): + """Reads self.file_name and determines whether the given input file_name + is valid. + """ if self.file_name is None: raise Exception("Must specify file for SVGMobject") + + if os.path.exists(self.file_name): + self.file_path = self.file_name + return + + relative = os.path.join(os.getcwd(), self.file_name) + if os.path.exists(relative): + self.file_path = relative + return + possible_paths = [ - os.path.join(os.path.join("assets", "svg_images"), self.file_name), - os.path.join(os.path.join("assets", "svg_images"), self.file_name + ".svg"), - os.path.join(os.path.join("assets", "svg_images"), self.file_name + ".xdv"), + os.path.join(config.get_dir("assets_dir"), self.file_name), + os.path.join(config.get_dir("assets_dir"), self.file_name + ".svg"), + os.path.join(config.get_dir("assets_dir"), self.file_name + ".xdv"), self.file_name, + self.file_name + ".svg", + self.file_name + ".xdv", ] for path in possible_paths: if os.path.exists(path): self.file_path = path return - raise IOError("No file matching %s in image directory" % self.file_name) + error = "From: {}, could not find {} at either of these locations: {}".format( + os.getcwd(), self.file_name, possible_paths + ) + raise IOError(error) def generate_points(self): + """Called by the Mobject abstract base class. Responsible for generating + the SVGMobject's points from XML tags, populating self.mobjects, and + any submobjects within self.mobjects. + """ doc = minidom.parse(self.file_path) self.ref_to_element = {} for svg in doc.getElementsByTagName("svg"): @@ -76,6 +140,18 @@ def generate_points(self): doc.unlink() def get_mobjects_from(self, element): + """Parses a given SVG element into a Mobject. + + Parameters + ---------- + element : :class:`str` + The SVG data in the XML to be parsed. + + Returns + ------- + VMobject + A VMobject representing the associated SVG element. + """ result = [] if not isinstance(element, minidom.Element): return result @@ -112,14 +188,51 @@ def get_mobjects_from(self, element): return result def g_to_mobjects(self, g_element): + """Converts the ``g`` SVG element into VMobjects. + + Parameters + ---------- + g_element : :class:`str` + A ``g`` element is a group of other SVG elements. As such a ``g`` element is equivalent to a VGroup. + + Returns + ------- + List[VMobject] + A list of VMobject reprsented by the group. + """ mob = VGroup(*self.get_mobjects_from(g_element)) self.handle_transforms(g_element, mob) return mob.submobjects def path_string_to_mobject(self, path_string): + """Converts a SVG path element's ``d`` attribute to a mobject. + + Parameters + ---------- + path_string : str + A path with potentially multiple path commands to create a shape. + + Returns + ------- + VMobjectFromSVGPathstring + A VMobject from the given path string, or d attribute. + """ return VMobjectFromSVGPathstring(path_string) def use_to_mobjects(self, use_element): + """Converts a SVG element to VMobject. + + Parameters + ---------- + use_element : str + An SVG element which represents nodes that should be + duplicated elsewhere. + + Returns + ------- + VMobject + A VMobject + """ # Remove initial "#" character ref = use_element.getAttribute("xlink:href")[1:] if ref not in self.ref_to_element: @@ -128,13 +241,37 @@ def use_to_mobjects(self, use_element): return self.get_mobjects_from(self.ref_to_element[ref]) def attribute_to_float(self, attr): + """A helper method which converts the attribute to float. + + Parameters + ---------- + attr : str + An SVG path attribute. + + Returns + ------- + float + A float representing the attribute string value. + """ stripped_attr = "".join( [char for char in attr if char in string.digits + "." + "-"] ) return float(stripped_attr) def polygon_to_mobject(self, polygon_element): - # TODO, This seems hacky... + """Constructs a VMobject from a SVG element. + + Parameters + ---------- + polygon_element : str + An SVG polygon element. + + Returns + ------- + VMobjectFromSVGPathstring + A VMobject representing the polygon. + """ + # TODO, This seems hacky... yes it is. path_string = polygon_element.getAttribute("points") for digit in string.digits: path_string = path_string.replace(" " + digit, " L" + digit) @@ -144,6 +281,18 @@ def polygon_to_mobject(self, polygon_element): # def circle_to_mobject(self, circle_element): + """Creates a Circle VMobject from a SVG command. + + Parameters + ---------- + circle_element : str + A SVG circle path command. + + Returns + ------- + Circle + A Circle VMobject + """ x, y, r = [ self.attribute_to_float(circle_element.getAttribute(key)) if circle_element.hasAttribute(key) @@ -153,6 +302,19 @@ def circle_to_mobject(self, circle_element): return Circle(radius=r).shift(x * RIGHT + y * DOWN) def ellipse_to_mobject(self, circle_element): + """Creates a stretched Circle VMobject from a SVG path + command. + + Parameters + ---------- + circle_element : str + A SVG circle path command. + + Returns + ------- + Circle + A Circle VMobject + """ x, y, rx, ry = [ self.attribute_to_float(circle_element.getAttribute(key)) if circle_element.hasAttribute(key) @@ -162,6 +324,19 @@ def ellipse_to_mobject(self, circle_element): return Circle().scale(rx * RIGHT + ry * UP).shift(x * RIGHT + y * DOWN) def rect_to_mobject(self, rect_element): + """Converts a SVG command to a VMobject. + + Parameters + ---------- + rect_element : str + A SVG rect path command. + + Returns + ------- + Rectangle + Creates either a Rectangle, or RoundRectangle, VMobject from a + rect element. + """ fill_color = rect_element.getAttribute("fill") stroke_color = rect_element.getAttribute("stroke") stroke_width = rect_element.getAttribute("stroke-width") @@ -214,6 +389,17 @@ def rect_to_mobject(self, rect_element): return mob def handle_transforms(self, element, mobject): + """Applies the SVG transform to the specified mobject. Tranforms include: + ``rotate``, ``translate``, ``scale``, and ``skew``. + + Parameters + ---------- + element : str + The transform command to perform + + mobject : Mobject + The Mobject to transform. + """ x, y = 0, 0 try: x = self.attribute_to_float(element.getAttribute("x")) @@ -275,6 +461,7 @@ def handle_transforms(self, element, mobject): # TODO, ... def flatten(self, input_list): + """A helper method to flatten the ``input_list`` into an 1D array.""" output_list = [] for i in input_list: if isinstance(i, list): @@ -284,6 +471,19 @@ def flatten(self, input_list): return output_list def get_all_childNodes_have_id(self, element): + """Gets all child nodes containing the `id` attribute and returns + them in a flattened list. + + Parameters + -------- + element : :class:`str` + An element from SVG XML data. Elements use a unique `id`. + + Returns + ------- + List[DOM element] + A flattened list of DOM elements containing the `id` attribute. + """ all_childNodes_have_id = [] if not isinstance(element, minidom.Element): return @@ -294,12 +494,22 @@ def get_all_childNodes_have_id(self, element): return self.flatten([e for e in all_childNodes_have_id if e]) def update_ref_to_element(self, defs): + """Updates the ``ref_to_element`` dictionary. + Parameters + -------- + defs : :class:`defs` + The new defs + """ new_refs = dict( [(e.getAttribute("id"), e) for e in self.get_all_childNodes_have_id(defs)] ) self.ref_to_element.update(new_refs) def move_into_position(self): + """Uses the SVGMobject's config dictionary to set the Mobject's + width, height, and/or center it. Use ``width``, ``height``, and + ``should_center`` respectively to modify this. + """ if self.should_center: self.center() if self.height is not None: @@ -314,6 +524,17 @@ def __init__(self, path_string, **kwargs): VMobject.__init__(self, **kwargs) def get_path_commands(self): + """Returns a list of possible path commands used within an SVG ``d`` + attribute. + + See: https://svgwg.org/svg2-draft/paths.html#DProperty for further + details on what each path command does. + + Returns + ------- + List[:class:`str`] + The various upper and lower cased path commands. + """ result = [ "M", # moveto "L", # lineto @@ -330,6 +551,7 @@ def get_path_commands(self): return result def generate_points(self): + """Generates points from a given an SVG ``d`` attribute.""" pattern = "[%s]" % ("".join(self.get_path_commands())) pairs = list( zip( @@ -345,6 +567,7 @@ def generate_points(self): self.rotate(np.pi, RIGHT, about_point=ORIGIN) def handle_command(self, command, coord_string): + """Core logic for handling each of the various path commands.""" isLower = command.islower() command = command.upper() # new_points are the points that will be added to the curr_points @@ -413,6 +636,9 @@ def handle_command(self, command, coord_string): self.add_cubic_bezier_curve_to(*new_points[i : i + 3]) def string_to_points(self, coord_string): + """Since the SVG file's path command is provided as a string, this + converts the coordinates into numbers. + """ numbers = string_to_numbers(coord_string) if len(numbers) % 2 == 1: numbers.append(0) @@ -422,4 +648,5 @@ def string_to_points(self, coord_string): return result def get_original_path_string(self): + """A simple getter for the path's ``d`` attribute.""" return self.path_string diff --git a/manim/mobject/svg/tex_mobject.py b/manim/mobject/svg/tex_mobject.py index da5a07528a..ae73b84d8a 100644 --- a/manim/mobject/svg/tex_mobject.py +++ b/manim/mobject/svg/tex_mobject.py @@ -1,4 +1,160 @@ -"""Mobjects representing text rendered using LaTeX.""" +r"""Mobjects representing text rendered using LaTeX. + + +The Tex mobject ++++++++++++++++ +Just as you can use :class:`~.Text` to add text to your videos, you can use :class:`~.Tex` to insert LaTeX. + +.. manim:: HelloLaTeX + :save_last_frame: + + class HelloLaTeX(Scene): + def construct(self): + tex = Tex(r'\LaTeX').scale(3) + self.add(tex) + +Note that we are using a raw string (``r'---'``) instead of a regular string (``'---'``). +This is because TeX code uses a lot of special characters - like ``\`` for example - +that have special meaning within a regular python string. An alternative would have +been to write ``\\`` as in ``Tex('\\LaTeX')``. + +The MathTex mobject ++++++++++++++++++++ +Anything enclosed in ``$`` signs is interpreted as maths-mode: + +.. manim:: HelloTex + :save_last_frame: + + class HelloTex(Scene): + def construct(self): + tex = Tex(r'$\xrightarrow{x^2y^3}$ \LaTeX').scale(3) + self.add(tex) + +Whereas in a :class:`~.MathTex` mobject everything is math-mode by default. + +.. manim:: MovingBraces + + class MovingBraces(Scene): + def construct(self): + text=MathTex( + "\\frac{d}{dx}f(x)g(x)=", #0 + "f(x)\\frac{d}{dx}g(x)", #1 + "+", #2 + "g(x)\\frac{d}{dx}f(x)" #3 + ) + self.play(Write(text)) + brace1 = Brace(text[1], UP, buff=SMALL_BUFF) + brace2 = Brace(text[3], UP, buff=SMALL_BUFF) + t1 = brace1.get_text("$g'f$") + t2 = brace2.get_text("$f'g$") + self.play( + GrowFromCenter(brace1), + FadeIn(t1), + ) + self.wait() + self.play( + ReplacementTransform(brace1,brace2), + ReplacementTransform(t1,t2) + ) + self.wait() + + +LaTeX commands and keyword arguments +++++++++++++++++++++++++++++++++++++ +We can use any standard LaTeX commands in the AMS maths packages. For example the ``mathtt`` math-text type, or the ``looparrowright`` arrow. + +.. manim:: AMSLaTeX + :save_last_frame: + + class AMSLaTeX(Scene): + def construct(self): + tex = Tex(r'$\mathtt{H} \looparrowright$ \LaTeX').scale(3) + self.add(tex) + +On the manim side, the :class:`~.Tex` class also accepts attributes to change the appearance of the output. +This is very similar to the :class:`~.Text` class. For example, the ``color`` keyword changes the color of the TeX mobject: + +.. manim:: LaTeXAttributes + :save_last_frame: + + class LaTeXAttributes(Scene): + def construct(self): + tex = Tex(r'Hello \LaTeX', color=BLUE).scale(3) + self.add(tex) + +Extra LaTeX Packages +++++++++++++++++++++ +Some commands require special packages to be loaded into the TeX template. For example, +to use the ``mathscr`` script, we need to add the ``mathrsfs`` package. Since this package isn't loaded +into manim's tex template by default, we add it manually: + +.. manim:: AddPackageLatex + :save_last_frame: + + class AddPackageLatex(Scene): + def construct(self): + myTemplate = TexTemplate() + myTemplate.add_to_preamble(r"\usepackage{mathrsfs}") + tex = Tex(r'$\mathscr{H} \rightarrow \mathbb{H}$}', tex_template=myTemplate).scale(3) + self.add(tex) + +Substrings and parts +++++++++++++++++++++ +The TeX mobject can accept multiple strings as arguments. Afterwards you can refer to the individual +parts either by their index (like ``tex[1]``), or you can look them up by (parts of) the tex code like +in this example where we set the color of the ``\bigstar`` using :func:`~.set_color_by_tex`: + +.. manim:: LaTeXSubstrings + :save_last_frame: + + class LaTeXSubstrings(Scene): + def construct(self): + tex = Tex('Hello', r'$\bigstar$', r'\LaTeX').scale(3) + tex.set_color_by_tex('igsta', RED) + self.add(tex) + +LaTeX Maths Fonts - The Template Library +++++++++++++++++++++++++++++++++++++++++ +Changing fonts in LaTeX when typesetting mathematical formulae is a little bit more tricky than +with regular text. It requires changing the template that is used to compile the tex code. +Manim comes with a collection of :class:`~.TexFontTemplates` ready for you to use. These templates will all work +in maths mode: + +.. manim:: LaTeXMathFonts + :save_last_frame: + + class LaTeXMathFonts(Scene): + def construct(self): + tex = Tex(r'$x^2 + y^2 = z^2$', tex_template=TexFontTemplates.french_cursive).scale(3) + self.add(tex) + +Manim also has a :class:`~.TexTemplateLibrary` containing the TeX templates used by 3Blue1Brown. One example +is the ctex template, used for typesetting Chinese. For this to work, the ctex LaTeX package +must be installed on your system. Furthermore, if you are only typesetting Text, you probably do not +need :class:`~.Tex` at all, and should use :class:`~.Text` or :class:`~.PangoText` instead. + +.. manim:: LaTeXTemplateLibrary + :save_last_frame: + + class LaTeXTemplateLibrary(Scene): + def construct(self): + tex = Tex('Hello 你好 \\LaTeX', tex_template=TexTemplateLibrary.ctex).scale(3) + self.add(tex) + + +Aligning formulae ++++++++++++++++++ +A :class:`~.MathTex` mobject is typeset in the LaTeX ``align*`` environment. This means you can use the ``&`` alignment +character when typesetting multiline formulae: + +.. manim:: LaTeXAlignEnvironment + :save_last_frame: + + class LaTeXAlignEnvironment(Scene): + def construct(self): + tex = MathTex(r'f(x) &= 3 + 2 + 1\\ &= 5 + 1 \\ &= 6').scale(2) + self.add(tex) +""" __all__ = [ "TexSymbol", @@ -52,12 +208,11 @@ class SingleStringMathTex(SVGMobject): CONFIG = { "stroke_width": 0, "fill_opacity": 1.0, - "background_stroke_width": 1, + "background_stroke_width": 0, "background_stroke_color": BLACK, "should_center": True, "height": None, "organize_left_to_right": False, - "alignment": "", "tex_environment": "align*", "tex_template": None, } @@ -84,7 +239,7 @@ def __repr__(self): return f"{type(self).__name__}({repr(self.tex_string)})" def get_modified_expression(self, tex_string): - result = self.alignment + " " + tex_string + result = tex_string result = result.strip() result = self.modify_special_strings(result) return result @@ -311,9 +466,8 @@ class Tex(MathTex): """ CONFIG = { - "alignment": "\\centering", "arg_separator": "", - "tex_environment": None, + "tex_environment": "center", } @@ -322,7 +476,7 @@ class BulletedList(Tex): "buff": MED_LARGE_BUFF, "dot_scale_factor": 2, # Have to include because of handle_multiple_args implementation - "alignment": "", + "tex_environment": None, } def __init__(self, *items, **kwargs): diff --git a/manim/mobject/svg/text_mobject.py b/manim/mobject/svg/text_mobject.py index 906be3d176..1755e16a99 100644 --- a/manim/mobject/svg/text_mobject.py +++ b/manim/mobject/svg/text_mobject.py @@ -1,6 +1,48 @@ -"""Mobjects used for displaying (non-LaTeX) text.""" +"""Mobjects used for displaying (non-LaTeX) text. -__all__ = ["Text", "Paragraph", "PangoText", "CairoText"] +The simplest way to add text to your animations is to use the :class:`~.Text` class. It uses the Pango library to render text. +With Pango, you are also able to render non-English alphabets like `你好` or `こんにちは` or `안녕하세요` or `مرحبا بالعالم`. + +Examples +-------- + +.. manim:: HelloWorld + :save_last_frame: + + class HelloWorld(Scene): + def construct(self): + text = Text('Hello world').scale(3) + self.add(text) + +.. manim:: TextAlignement + :save_last_frame: + + class TextAlignement(Scene): + def construct(self): + title = Text("K-means clustering and Logistic Regression", color=WHITE) + title.scale_in_place(0.75) + self.add(title.to_edge(UP)) + + t1 = Text("1. Measuring").set_color(WHITE) + t1.next_to(ORIGIN, direction=RIGHT, aligned_edge=UP) + + t2 = Text("2. Clustering").set_color(WHITE) + t2.next_to(t1, direction=DOWN, aligned_edge=LEFT) + + t3 = Text("3. Regression").set_color(WHITE) + t3.next_to(t2, direction=DOWN, aligned_edge=LEFT) + + t4 = Text("4. Prediction").set_color(WHITE) + t4.next_to(t3, direction=DOWN, aligned_edge=LEFT) + + x = VGroup(t1, t2, t3, t4).scale_in_place(0.7) + x.set_opacity(0.5) + x.submobjects[1].set_opacity(1) + self.add(x) + +""" + +__all__ = ["Text", "Paragraph", "CairoText"] import copy @@ -13,14 +55,13 @@ import pangocairocffi import pangocffi -from ... import config, file_writer_config, logger +from ... import config, logger from ...constants import * -from ...container import Container -from ...mobject.geometry import Dot, Rectangle +from ...mobject.geometry import Dot from ...mobject.svg.svg_mobject import SVGMobject from ...mobject.types.vectorized_mobject import VGroup from ...utils.config_ops import digest_config -from ...utils.color import WHITE, BLACK +from ...utils.color import WHITE TEXT_MOB_SCALE_FACTOR = 0.05 @@ -310,7 +351,7 @@ def text2svg(self): if NOT_SETTING_FONT_MSG: logger.warning(NOT_SETTING_FONT_MSG) - dir_name = file_writer_config["text_dir"] + dir_name = config.get_dir("text_dir") if not os.path.exists(dir_name): os.makedirs(dir_name) @@ -358,6 +399,11 @@ class Paragraph(VGroup): `weird `_. Consider using :meth:`remove_invisible_chars` to resolve this issue. + .. note:: + + Due to issues with the Pango-powered :class:`.Text`, this class uses + :class:`.CairoText`. + Parameters ---------- line_spacing : :class:`int`, optional @@ -390,7 +436,7 @@ def __init__(self, *text, **config): VGroup.__init__(self, **config) lines_str = "\n".join(list(text)) - self.lines_text = Text(lines_str, **config) + self.lines_text = CairoText(lines_str, **config) lines_str_list = lines_str.split("\n") self.chars = self.gen_chars(lines_str_list) @@ -533,10 +579,10 @@ def change_alignment_for_a_line(self, alignment, line_no): ) -class PangoText(SVGMobject): +class Text(SVGMobject): r"""Display (non-LaTeX) text rendered using `Pango `_. - PangoText objects behave like a :class:`.VGroup`-like iterable of all characters + Text objects behave like a :class:`.VGroup`-like iterable of all characters in the given text. In particular, slicing is possible. Parameters @@ -546,27 +592,74 @@ class PangoText(SVGMobject): Returns ------- - :class:`PangoText` + :class:`Text` The mobject like :class:`.VGroup`. Examples --------- + + .. manim:: Example1Text + :save_last_frame: + + class Example1Text(Scene): + def construct(self): + text = Text('Hello world').scale(3) + self.add(text) + + .. manim:: TextColorExample + :save_last_frame: + + class TextColorExample(Scene): + def construct(self): + text1 = Text('Hello world', color=BLUE).scale(3) + text2 = Text('Hello world', gradient=(BLUE, GREEN)).scale(3).next_to(text1, DOWN) + self.add(text1, text2) + + .. manim:: TextItalicAndBoldExample + :save_last_frame: + + class TextItalicAndBoldExample(Scene): + def construct(self): + text0 = Text('Hello world', slant=ITALIC) + text1 = Text('Hello world', t2s={'world':ITALIC}) + text2 = Text('Hello world', weight=BOLD) + text3 = Text('Hello world', t2w={'world':BOLD}) + self.add(text0,text1, text2,text3) + for i,mobj in enumerate(self.mobjects): + mobj.shift(DOWN*(i-1)) + + + .. manim:: TextMoreCustomization + :save_last_frame: + + class TextMoreCustomization(Scene): + def construct(self): + text1 = Text( + 'Google', + t2c={'[:1]': '#3174f0', '[1:2]': '#e53125', + '[2:3]': '#fbb003', '[3:4]': '#3174f0', + '[4:5]': '#269a43', '[5:]': '#e53125'}, size=1.2).scale(3) + self.add(text1) + + As :class:`Text` uses Pango to render text, rendering non-English + characters is easily possible: + .. manim:: MultipleFonts :save_last_frame: class MultipleFonts(Scene): def construct(self): - morning = PangoText("வணக்கம்", font="sans-serif") - chin = PangoText( + morning = Text("வணக்கம்", font="sans-serif") + chin = Text( "見 角 言 谷 辛 辰 辵 邑 酉 釆 里!", t2c={"見 角 言": BLUE} ) # works same as ``Text``. - mess = PangoText("Multi-Language", style=BOLD) - russ = PangoText("Здравствуйте मस नम म ", font="sans-serif") - hin = PangoText("नमस्ते", font="sans-serif") - arb = PangoText( + mess = Text("Multi-Language", style=BOLD) + russ = Text("Здравствуйте मस नम म ", font="sans-serif") + hin = Text("नमस्ते", font="sans-serif") + arb = Text( "صباح الخير \n تشرفت بمقابلتك", font="sans-serif" ) # don't mix RTL and LTR languages nothing shows up then ;-) - japanese = PangoText("臂猿「黛比」帶著孩子", font="sans-serif") + japanese = Text("臂猿「黛比」帶著孩子", font="sans-serif") self.add(morning,chin,mess,russ,hin,arb,japanese) for i,mobj in enumerate(self.mobjects): mobj.shift(DOWN*(i-3)) @@ -577,16 +670,16 @@ def construct(self): class PangoRender(Scene): def construct(self): - morning = PangoText("வணக்கம்", font="sans-serif") + morning = Text("வணக்கம்", font="sans-serif") self.play(Write(morning)) self.wait(2) Tests ----- - Check that the creation of :class:`~.PangoText` works:: + Check that the creation of :class:`~.Text` works:: - >>> PangoText('The horse does not eat cucumber salad.') + >>> Text('The horse does not eat cucumber salad.') Text('The horse does not eat cucumber salad.') .. WARNING:: @@ -622,6 +715,10 @@ def construct(self): } def __init__(self, text: str, **config): # pylint: disable=redefined-outer-name + logger.info( + "Text now uses Pango for rendering. " + "In case of problems, the old implementation is available as CairoText." + ) self.full2short(config) digest_config(self, config) self.original_text = text @@ -774,7 +871,7 @@ def text2hash(self): """ settings = ( "PANGO" + self.font + self.slant + self.weight - ) # to differentiate Text and PangoText + ) # to differentiate Text and CairoText settings += str(self.t2f) + str(self.t2s) + str(self.t2w) settings += str(self.line_spacing) + str(self.size) id_str = self.text + settings @@ -833,7 +930,7 @@ def text2svg(self): """ size = self.size * 10 line_spacing = self.line_spacing * 10 - dir_name = file_writer_config["text_dir"] + dir_name = config.get_dir("text_dir") if not os.path.exists(dir_name): os.makedirs(dir_name) hash_name = self.text2hash() @@ -846,13 +943,13 @@ def text2svg(self): settings = self.text2settings() offset_x = 0 last_line_num = 0 + layout = pangocairocffi.create_layout(context) + layout.set_width(pangocffi.units_from_double(600)) for setting in settings: family = setting.font style = self.str2style(setting.slant) weight = self.str2weight(setting.weight) text = self.text[setting.start : setting.end].replace("\n", " ") - layout = pangocairocffi.create_layout(context) - layout.set_width(pangocffi.units_from_double(600)) fontdesc = pangocffi.FontDescription() fontdesc.set_size(pangocffi.units_from_double(size)) if family: @@ -866,76 +963,10 @@ def text2svg(self): context.move_to( START_X + offset_x, START_Y + line_spacing * setting.line_num ) + pangocairocffi.update_layout(context, layout) layout.set_text(text) logger.debug(f"Setting Text {text}") pangocairocffi.show_layout(context, layout) - offset_x += layout.get_extents()[0].x + offset_x += pangocffi.units_to_double(layout.get_size()[0]) surface.finish() return file_name - - -class Text(CairoText): - """Display (non-LaTeX) text. - - Text objects behave like a :class:`.VGroup`-like iterable of all characters - in the given text. In particular, slicing is possible. - - Examples - -------- - .. manim:: Example1Text - :save_last_frame: - - class Example1Text(Scene): - def construct(self): - text = Text('Hello world').scale(3) - self.add(text) - - .. manim:: TextColorExample - :save_last_frame: - - class TextColorExample(Scene): - def construct(self): - text1 = Text('Hello world', color=BLUE).scale(3) - text2 = Text('Hello world', gradient=(BLUE, GREEN)).scale(3).next_to(text1, DOWN) - self.add(text1, text2) - - .. manim:: TextItalicAndBoldExample - :save_last_frame: - - class TextItalicAndBoldExample(Scene): - def construct(self): - text0 = Text('Hello world', slant=ITALIC) - text1 = Text('Hello world', t2s={'world':ITALIC}) - text2 = Text('Hello world', weight=BOLD) - text3 = Text('Hello world', t2w={'world':BOLD}) - self.add(text0,text1, text2,text3) - for i,mobj in enumerate(self.mobjects): - mobj.shift(DOWN*(i-1)) - - - .. manim:: TextMoreCustomization - :save_last_frame: - - class TextMoreCustomization(Scene): - def construct(self): - text1 = Text( - 'Google', - t2c={'[:1]': '#3174f0', '[1:2]': '#e53125', - '[2:3]': '#fbb003', '[3:4]': '#3174f0', - '[4:5]': '#269a43', '[5:]': '#e53125'}, size=1.2).scale(3) - self.add(text1) - - .. WARNING:: - - Using a :class:`.Transform` on text with leading whitespace can look - `weird `_. Consider using - :meth:`remove_invisible_chars` to resolve this issue. - - """ - - def __init__(self, text, **config): - logger.warning( - "Using Text uses Cairo Toy API to Render Text." - "Using PangoText is recommended and soon Text would point to PangoText" - ) - CairoText.__init__(self, text, **config) diff --git a/manim/mobject/three_d_shading_utils.py b/manim/mobject/three_d_shading_utils.py deleted file mode 100644 index 25dc41f32e..0000000000 --- a/manim/mobject/three_d_shading_utils.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Utility functions for shading of three-dimensional mobjects.""" - -import numpy as np - -from ..constants import ORIGIN -from ..utils.space_ops import get_unit_normal - - -def get_3d_vmob_gradient_start_and_end_points(vmob): - return ( - get_3d_vmob_start_corner(vmob), - get_3d_vmob_end_corner(vmob), - ) - - -def get_3d_vmob_start_corner_index(vmob): - return 0 - - -def get_3d_vmob_end_corner_index(vmob): - return ((len(vmob.points) - 1) // 6) * 3 - - -def get_3d_vmob_start_corner(vmob): - if vmob.get_num_points() == 0: - return np.array(ORIGIN) - return vmob.points[get_3d_vmob_start_corner_index(vmob)] - - -def get_3d_vmob_end_corner(vmob): - if vmob.get_num_points() == 0: - return np.array(ORIGIN) - return vmob.points[get_3d_vmob_end_corner_index(vmob)] - - -def get_3d_vmob_unit_normal(vmob, point_index): - n_points = vmob.get_num_points() - if vmob.get_num_points() == 0: - return np.array(ORIGIN) - i = point_index - im1 = i - 1 if i > 0 else (n_points - 2) - ip1 = i + 1 if i < (n_points - 1) else 1 - return get_unit_normal( - vmob.points[ip1] - vmob.points[i], - vmob.points[im1] - vmob.points[i], - ) - - -def get_3d_vmob_start_corner_unit_normal(vmob): - return get_3d_vmob_unit_normal(vmob, get_3d_vmob_start_corner_index(vmob)) - - -def get_3d_vmob_end_corner_unit_normal(vmob): - return get_3d_vmob_unit_normal(vmob, get_3d_vmob_end_corner_index(vmob)) diff --git a/manim/mobject/types/image_mobject.py b/manim/mobject/types/image_mobject.py index e0bb9c94fe..7125c4d5e0 100644 --- a/manim/mobject/types/image_mobject.py +++ b/manim/mobject/types/image_mobject.py @@ -2,11 +2,13 @@ __all__ = ["AbstractImageMobject", "ImageMobject", "ImageMobjectFromCamera"] +import pathlib import numpy as np from PIL import Image +from ... import config from ...constants import * from ...mobject.mobject import Mobject from ...mobject.shape_matchers import SurroundingRectangle @@ -14,18 +16,30 @@ from ...utils.color import color_to_int_rgb, WHITE from ...utils.config_ops import digest_config from ...utils.images import get_full_raster_image_path +from manim.constants import QUALITIES, DEFAULT_QUALITY class AbstractImageMobject(Mobject): """ Automatically filters out black pixels + + Parameters + ---------- + scale_to_resolution : :class:`int` + At this resolution the image is placed pixel by pixel onto the screen, so it will look the sharpest and best. + This is a custom parameter of ImageMobject so that rendering a scene with e.g. the ``--quality low`` or ``--quality medium`` flag for faster rendering won't effect the position of the image on the screen. """ CONFIG = { - "height": 2.0, "pixel_array_dtype": "uint8", } + def __init__(self, scale_to_resolution, **kwargs): + digest_config(self, kwargs) + self.scale_to_resolution = scale_to_resolution + + Mobject.__init__(self, **kwargs) + def get_pixel_array(self): raise NotImplementedError() @@ -44,19 +58,52 @@ def reset_points(self): ) self.center() h, w = self.get_pixel_array().shape[:2] + if self.scale_to_resolution: + self.height = h / self.scale_to_resolution * config["frame_height"] + else: + self.height = 3 ## this is the case for ImageMobjectFromCamera self.stretch_to_fit_height(self.height) self.stretch_to_fit_width(self.height * w / h) class ImageMobject(AbstractImageMobject): + """Displays an Image from a numpy array or a file. + + Parameters + ---------- + scale_to_resolution : :class:`int` + At this resolution the image is placed pixel by pixel onto the screen, so it will look the sharpest and best. + This is a custom parameter of ImageMobject so that rendering a scene with e.g. the ``--quality low`` or ``--quality medium`` flag for faster rendering won't effect the position of the image on the screen. + + + + Example + ------- + .. manim:: ImageFromArray + :save_last_frame: + + class ImageFromArray(Scene): + def construct(self): + image = ImageMobject(np.uint8([[0, 100, 30, 200], + [255, 0, 5, 33]])) + image.set_height(7) + self.add(image) + + """ + CONFIG = { "invert": False, "image_mode": "RGBA", } - def __init__(self, filename_or_array, **kwargs): + def __init__( + self, + filename_or_array, + scale_to_resolution=QUALITIES[DEFAULT_QUALITY]["pixel_height"], + **kwargs, + ): digest_config(self, kwargs) - if isinstance(filename_or_array, str): + if isinstance(filename_or_array, (str, pathlib.PurePath)): path = get_full_raster_image_path(filename_or_array) image = Image.open(path).convert(self.image_mode) self.pixel_array = np.array(image) @@ -65,9 +112,10 @@ def __init__(self, filename_or_array, **kwargs): self.change_to_rgba_array() if self.invert: self.pixel_array[:, :, :3] = 255 - self.pixel_array[:, :, :3] - AbstractImageMobject.__init__(self, **kwargs) + AbstractImageMobject.__init__(self, scale_to_resolution, **kwargs) def change_to_rgba_array(self): + """Converts an RGB array into RGBA with the alpha value opacity maxed.""" pa = self.pixel_array if len(pa.shape) == 2: pa = pa.reshape(list(pa.shape) + [1]) @@ -81,6 +129,7 @@ def change_to_rgba_array(self): self.pixel_array = pa def get_pixel_array(self): + """A simple getter method.""" return self.pixel_array def set_color(self, color, alpha=None, family=True): @@ -94,19 +143,51 @@ def set_color(self, color, alpha=None, family=True): return self def set_opacity(self, alpha): + """Sets the image's opacity. + + Parameters + ---------- + alpha : float + The alpha value of the object, 1 being opaque and 0 being + transparent. + """ self.pixel_array[:, :, 3] = int(255 * alpha) return self def fade(self, darkness=0.5, family=True): + """Sets the image's opacity using a 1 - alpha relationship. + + Parameters + ---------- + darkness : float + The alpha value of the object, 1 being transparent and 0 being + opaque. + family : Boolean + Whether the submobjects of the ImageMobject should be affected. + """ self.set_opacity(1 - darkness) super().fade(darkness, family) return self def interpolate_color(self, mobject1, mobject2, alpha): + """Interpolates an array of pixel color values into another array of + equal size. + + Parameters + ---------- + mobject1 : ImageMobject + The ImageMobject to tranform from. + + mobject1 : ImageMobject + + The ImageMobject to tranform into. + alpha : float + Used to track the lerp relationship. Not opacity related. + """ assert mobject1.pixel_array.shape == mobject2.pixel_array.shape, ( f"Mobject pixel array shapes incompatible for interpolation.\n" f"Mobject 1 ({mobject1}) : {mobject1.pixel_array.shape}\n" - f"Mobject 2 ({mobject2}) : {mobject1.pixel_array.shape}" + f"Mobject 2 ({mobject2}) : {mobject2.pixel_array.shape}" ) self.pixel_array = interpolate( mobject1.pixel_array, mobject2.pixel_array, alpha @@ -130,7 +211,7 @@ class ImageMobjectFromCamera(AbstractImageMobject): def __init__(self, camera, **kwargs): self.camera = camera self.pixel_array = self.camera.pixel_array - AbstractImageMobject.__init__(self, **kwargs) + AbstractImageMobject.__init__(self, scale_to_resolution=False, **kwargs) # TODO: Get rid of this. def get_pixel_array(self): @@ -148,7 +229,7 @@ def interpolate_color(self, mobject1, mobject2, alpha): assert mobject1.pixel_array.shape == mobject2.pixel_array.shape, ( f"Mobject pixel array shapes incompatible for interpolation.\n" f"Mobject 1 ({mobject1}) : {mobject1.pixel_array.shape}\n" - f"Mobject 2 ({mobject2}) : {mobject1.pixel_array.shape}" + f"Mobject 2 ({mobject2}) : {mobject2.pixel_array.shape}" ) self.pixel_array = interpolate( mobject1.pixel_array, mobject2.pixel_array, alpha diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index 8f7546cfe7..7c2b00af36 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -30,6 +30,7 @@ from ...utils.simple_functions import clip_in_place from ...utils.space_ops import rotate_vector from ...utils.space_ops import get_norm +from ...utils.space_ops import shoelace_direction # TODO # - Change cubic curve groups to have 4 points instead of 3 @@ -251,6 +252,7 @@ def match_style(self, vmobject, family=True): def set_color(self, color, family=True): self.set_fill(color, family=family) self.set_stroke(color, family=family) + self.color = colour.Color(color) return self def set_opacity(self, opacity, family=True): @@ -879,8 +881,95 @@ def get_subcurve(self, a, b): vmob.pointwise_become_partial(self, a, b) return vmob + def get_direction(self): + """Uses :func:`~.space_ops.shoelace_direction` to calculate the direction. + The direction of points determines in which direction the + object is drawn, clockwise or counterclockwise. + + Examples + -------- + The default direction of a :class:`~.Circle` is counterclockwise:: + + >>> from manim import Circle + >>> Circle().get_direction() + 'CCW' + + Returns + ------- + :class:`str` + Either ``"CW"`` or ``"CCW"``. + """ + return shoelace_direction(self.get_start_anchors()) + + def reverse_direction(self): + """Reverts the point direction by inverting the point order. + + Returns + ------- + :class:`VMobject` + Returns self. + + Examples + -------- + .. manim:: ChangeOfDirection + + class ChangeOfDirection(Scene): + def construct(self): + ccw = RegularPolygon(5) + ccw.shift(LEFT).rotate + cw = RegularPolygon(5) + cw.shift(RIGHT).reverse_direction() + + self.play(ShowCreation(ccw), ShowCreation(cw), + run_time=4) + """ + self.points = self.points[::-1] + return self + + def force_direction(self, target_direction): + """Makes sure that points are either directed clockwise or + counterclockwise. + + Parameters + ---------- + target_direction : :class:`str` + Either ``"CW"`` or ``"CCW"``. + """ + if target_direction not in ("CW", "CCW"): + raise ValueError('Invalid input for force_direction. Use "CW" or "CCW"') + if self.get_direction() != target_direction: + # Since we already assured the input is CW or CCW, + # and the directions don't match, we just reverse + self.reverse_direction() + return self + class VGroup(VMobject): + """A group of vectorized mobjects. + + This can be used to group multiple :class:`~.VMobject` instances together + in order to scale, move, ... them together. + + Examples + -------- + + .. manim:: ArcShapeIris + :save_last_frame: + + class ArcShapeIris(Scene): + def construct(self): + colors = [DARK_BLUE, DARK_BROWN, BLUE_E, BLUE_D, BLUE_A, TEAL_B, GREEN_B, YELLOW_E] + radius = [1 + rad * 0.1 for rad in range(len(colors))] + + circles_group = VGroup() + + # zip(radius, color) makes the iterator [(radius[i], color[i]) for i in range(radius)] + circles_group.add(*[Circle(radius=rad, stroke_width=10, color=col) + for rad, col in zip(radius, colors)]) + self.add(circles_group) + + """ + def __init__(self, *vmobjects, **kwargs): VMobject.__init__(self, **kwargs) self.add(*vmobjects) @@ -943,6 +1032,74 @@ class VDict(VMobject): submob_dict : :class:`dict` Is the actual python dictionary that is used to bind the keys to the mobjects. + + Examples + -------- + + .. manim:: ShapesWithVDict + + class ShapesWithVDict(Scene): + def construct(self): + square = Square().set_color(RED) + circle = Circle().set_color(YELLOW).next_to(square, UP) + + # create dict from list of tuples each having key-mobject pair + pairs = [("s", square), ("c", circle)] + my_dict = VDict(pairs, show_keys=True) + + # display it just like a VGroup + self.play(ShowCreation(my_dict)) + self.wait() + + text = Tex("Some text").set_color(GREEN).next_to(square, DOWN) + + # add a key-value pair by wrapping it in a single-element list of tuple + # after attrs branch is merged, it will be easier like `.add(t=text)` + my_dict.add([("t", text)]) + self.wait() + + rect = Rectangle().next_to(text, DOWN) + # can also do key assignment like a python dict + my_dict["r"] = rect + + # access submobjects like a python dict + my_dict["t"].set_color(PURPLE) + self.play(my_dict["t"].scale, 3) + self.wait() + + # also supports python dict styled reassignment + my_dict["t"] = Tex("Some other text").set_color(BLUE) + self.wait() + + # remove submoject by key + my_dict.remove("t") + self.wait() + + self.play(Uncreate(my_dict["s"])) + self.wait() + + self.play(FadeOut(my_dict["c"])) + self.wait() + + self.play(FadeOutAndShift(my_dict["r"], DOWN)) + self.wait() + + # you can also make a VDict from an existing dict of mobjects + plain_dict = { + 1: Integer(1).shift(DOWN), + 2: Integer(2).shift(2 * DOWN), + 3: Integer(3).shift(3 * DOWN), + } + + vdict_from_plain_dict = VDict(plain_dict) + vdict_from_plain_dict.shift(1.5 * (UP + LEFT)) + self.play(ShowCreation(vdict_from_plain_dict)) + + # you can even use zip + vdict_using_zip = VDict(zip(["s", "c", "r"], [Square(), Circle(), Rectangle()])) + vdict_using_zip.shift(1.5 * RIGHT) + self.play(ShowCreation(vdict_using_zip)) + self.wait() """ def __init__(self, mapping_or_iterable={}, show_keys=False, **kwargs): @@ -990,7 +1147,7 @@ def remove(self, key): Parameters ---------- - key : Hashable + key : :class:`typing.Hashable` The key of the submoject to be removed. Returns @@ -1011,11 +1168,11 @@ def remove(self, key): return self def __getitem__(self, key): - """Overriding the [] operator for getting submobject by key + """Override the [] operator for item retrieval. Parameters ---------- - key : Hashable + key : :class:`typing.Hashable` The key of the submoject to be accessed Returns @@ -1033,11 +1190,11 @@ def __getitem__(self, key): return submob def __setitem__(self, key, value): - """Overriding the [] operator for assigning submobject like a python dict + """Override the [] operator for item assignment. Parameters ---------- - key : Hashable + key : :class:`typing.Hashable` The key of the submoject to be assigned value : :class:`VMobject` The submobject to bind the key to @@ -1057,6 +1214,62 @@ def __setitem__(self, key, value): self.remove(key) self.add([(key, value)]) + def __delitem__(self, key): + """Override the del operator for deleting an item. + + Parameters + ---------- + key : :class:`typing.Hashable` + The key of the submoject to be deleted + + Returns + ------- + None + + Examples + -------- + :: + + >>> from manim import * + >>> my_dict = VDict({'sq': Square()}) + >>> 'sq' in my_dict + True + >>> del my_dict['sq'] + >>> 'sq' in my_dict + False + + Notes + ----- + Removing an item from a VDict does not remove that item from any Scene + that the VDict is part of. + + """ + del self.submob_dict[key] + + def __contains__(self, key): + """Override the in operator. + + Parameters + ---------- + key : :class:`typing.Hashable` + The key to check membership of. + + Returns + ------- + :class:`bool` + + Examples + -------- + :: + + >>> from manim import * + >>> my_dict = VDict({'sq': Square()}) + >>> 'sq' in my_dict + True + + """ + return key in self.submob_dict + def get_all_submobjects(self): """To get all the submobjects associated with a particular :class:`VDict` object @@ -1081,7 +1294,7 @@ def add_key_value_pair(self, key, value): Parameters ---------- - key : Hashable + key : :class:`typing.Hashable` The key of the submobject to be added. value : :class:`~.VMobject` The mobject associated with the key diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index dc97effbb7..98d0e881fb 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -1,10 +1,9 @@ import numpy as np -from .. import config, camera_config, file_writer_config +from .. import config from ..utils.iterables import list_update from ..utils.exceptions import EndSceneEarlyException -from ..constants import DEFAULT_WAIT_TIME from ..scene.scene_file_writer import SceneFileWriter -from ..utils.caching import handle_caching_play, handle_caching_wait +from ..utils.caching import handle_caching_play from ..camera.camera import Camera @@ -37,10 +36,9 @@ def handle_play_like_call(func): """ def wrapper(self, scene, *args, **kwargs): - allow_write = not file_writer_config["skip_animations"] - self.file_writer.begin_animation(allow_write) + self.file_writer.begin_animation(not self.skip_animations) func(self, scene, *args, **kwargs) - self.file_writer.end_animation(allow_write) + self.file_writer.end_animation(not self.skip_animations) self.num_plays += 1 return wrapper @@ -53,7 +51,7 @@ class CairoRenderer: time: time elapsed since initialisation of scene. """ - def __init__(self, camera_class=None, **kwargs): + def __init__(self, camera_class=None, skip_animations=False, **kwargs): # All of the following are set to EITHER the value passed via kwargs, # OR the value stored in the global config dict at the time of # _instance construction_. Before, they were in the CONFIG dict, which @@ -71,8 +69,9 @@ def __init__(self, camera_class=None, **kwargs): ]: self.video_quality_config[attr] = kwargs.get(attr, config[attr]) camera_cls = camera_class if camera_class is not None else Camera - self.camera = camera_cls(self.video_quality_config, **camera_config) - self.original_skipping_status = file_writer_config["skip_animations"] + self.camera = camera_cls(self.video_quality_config) + self.original_skipping_status = skip_animations + self.skip_animations = skip_animations self.animations_hashes = [] self.num_plays = 0 self.time = 0 @@ -82,7 +81,6 @@ def init(self, scene): self, self.video_quality_config, scene.__class__.__name__, - **file_writer_config, ) @pass_scene_reference @@ -91,12 +89,6 @@ def init(self, scene): def play(self, scene, *args, **kwargs): scene.play_internal(*args, **kwargs) - @pass_scene_reference - @handle_caching_wait - @handle_play_like_call - def wait(self, scene, duration=DEFAULT_WAIT_TIME, stop_condition=None): - scene.wait_internal(duration=duration, stop_condition=stop_condition) - def update_frame( # TODO Description in Docstring self, scene, @@ -123,7 +115,7 @@ def update_frame( # TODO Description in Docstring **kwargs """ - if file_writer_config["skip_animations"] and not ignore_skipping: + if self.skip_animations and not ignore_skipping: return if mobjects is None: mobjects = list_update( @@ -163,7 +155,7 @@ def add_frame(self, frame, num_frames=1): """ dt = 1 / self.camera.frame_rate self.time += num_frames * dt - if file_writer_config["skip_animations"]: + if self.skip_animations: return for _ in range(num_frames): self.file_writer.write_frame(frame) @@ -184,32 +176,17 @@ def update_skipping_status(self): the number of animations that need to be played, and raises an EndSceneEarlyException if they don't correspond. """ - if file_writer_config["from_animation_number"]: - if self.num_plays < file_writer_config["from_animation_number"]: - file_writer_config["skip_animations"] = True - if file_writer_config["upto_animation_number"]: - if self.num_plays > file_writer_config["upto_animation_number"]: - file_writer_config["skip_animations"] = True + if config["from_animation_number"]: + if self.num_plays < config["from_animation_number"]: + self.skip_animations = True + if config["upto_animation_number"]: + if self.num_plays > config["upto_animation_number"]: + self.skip_animations = True raise EndSceneEarlyException() - def revert_to_original_skipping_status(self): - """ - Forces the scene to go back to its original skipping status, - by setting skip_animations to whatever it reads - from original_skipping_status. - - Returns - ------- - Scene - The Scene, with the original skipping status. - """ - if hasattr(self, "original_skipping_status"): - file_writer_config["skip_animations"] = self.original_skipping_status - return self - def finish(self, scene): - file_writer_config["skip_animations"] = False + self.skip_animations = self.original_skipping_status self.file_writer.finish() - if file_writer_config["save_last_frame"]: - self.update_frame(scene, ignore_skipping=True) + if config["save_last_frame"]: + self.update_frame(scene, ignore_skipping=False) self.file_writer.save_final_image(self.camera.get_image()) diff --git a/manim/scene/graph_scene.py b/manim/scene/graph_scene.py index 2e5c3a629f..0ae87a7f82 100644 --- a/manim/scene/graph_scene.py +++ b/manim/scene/graph_scene.py @@ -1,4 +1,44 @@ -"""A scene for plotting / graphing functions.""" +"""A scene for plotting / graphing functions. + +Examples +-------- + +.. manim:: FunctionPlotWithLabbeledYAxis + :save_last_frame: + + class FunctionPlotWithLabbeledYAxis(GraphScene): + CONFIG = { + "y_min": 0, + "y_max": 100, + "y_axis_config": {"tick_frequency": 10}, + "y_labeled_nums": np.arange(0, 100, 10) + } + + def construct(self): + self.setup_axes() + dot = Dot().move_to(self.coords_to_point(PI / 2, 20)) + func_graph = self.get_graph(lambda x: 20 * np.sin(x)) + self.add(dot,func_graph) + + +.. manim:: GaussianFunctionPlot + :save_last_frame: + + amp = 5 + mu = 3 + sig = 1 + + def gaussian(x): + return amp * np.exp((-1 / 2 * ((x - mu) / sig) ** 2)) + + class GaussianFunctionPlot(GraphScene): + def construct(self): + self.setup_axes() + graph = self.get_graph(gaussian, x_min=-1, x_max=10) + graph.set_stroke(width=5) + self.add(graph) + +""" __all__ = ["GraphScene"] @@ -109,7 +149,10 @@ def setup_axes(self, animate=False): ) x_axis = NumberLine(**self.x_axis_config) - x_axis.shift(self.graph_origin - x_axis.number_to_point(0)) + x_shift = x_axis.number_to_point( + 0 if self.x_min <= 0 <= self.x_max else self.x_min + ) + x_axis.shift(self.graph_origin - x_shift) if len(self.x_labeled_nums) > 0: if self.exclude_zero_label: self.x_labeled_nums = [x for x in self.x_labeled_nums if x != 0] @@ -117,11 +160,10 @@ def setup_axes(self, animate=False): if self.x_axis_label: x_label = Tex(self.x_axis_label) x_label.next_to( - x_axis.get_tips() if self.include_tip else x_axis.get_tick_marks(), + x_axis.get_corner(self.x_label_position), self.x_label_position, buff=SMALL_BUFF, ) - x_label.shift_onto_screen() x_axis.add(x_label) self.x_axis_label_mob = x_label @@ -151,8 +193,11 @@ def setup_axes(self, animate=False): ) y_axis = NumberLine(**self.y_axis_config) - y_axis.shift(self.graph_origin - y_axis.number_to_point(0)) - y_axis.rotate(np.pi / 2, about_point=y_axis.number_to_point(0)) + y_shift = y_axis.number_to_point( + 0 if self.y_min <= 0 <= self.y_max else self.y_min + ) + y_axis.shift(self.graph_origin - y_shift) + y_axis.rotate(np.pi / 2, about_point=self.graph_origin) if len(self.y_labeled_nums) > 0: if self.exclude_zero_label: self.y_labeled_nums = [y for y in self.y_labeled_nums if y != 0] @@ -164,7 +209,6 @@ def setup_axes(self, animate=False): self.y_label_position, buff=SMALL_BUFF, ) - y_label.shift_onto_screen() y_axis.add(y_label) self.y_axis_label_mob = y_label @@ -201,6 +245,26 @@ def coords_to_point(self, x, y): ------- np.ndarray The array of the coordinates. + + Examples + -------- + + .. manim:: SequencePlot + :save_last_frame: + + class SequencePlot(GraphScene): + CONFIG = { + "y_axis_label": r"Concentration [\%]", + "x_axis_label": "Time [s]", + } + + def construct(self): + data = [1, 2, 2, 4, 4, 1, 3] + self.setup_axes() + for time, dat in enumerate(data): + dot = Dot().move_to(self.coords_to_point(time, dat)) + self.add(dot) + """ assert hasattr(self, "x_axis") and hasattr(self, "y_axis") result = self.x_axis.number_to_point(x)[0] * RIGHT diff --git a/manim/scene/js_scene.py b/manim/scene/js_scene.py index 8a3bf89c78..8acd9df0e4 100644 --- a/manim/scene/js_scene.py +++ b/manim/scene/js_scene.py @@ -1,7 +1,7 @@ from . import scene from ..camera.camera import Camera from ..camera.js_camera import JsCamera -from ..config import config +from .. import config from ..grpc.gen import renderserver_pb2 from ..grpc.gen import renderserver_pb2_grpc from ..grpc.impl.frame_server_impl import FrameServer @@ -14,7 +14,7 @@ import random import string import types -from ..config.logger import logger +from .. import logger def get_random_name(name_map): diff --git a/manim/scene/moving_camera_scene.py b/manim/scene/moving_camera_scene.py index 231df40c82..9d85e01788 100644 --- a/manim/scene/moving_camera_scene.py +++ b/manim/scene/moving_camera_scene.py @@ -4,6 +4,73 @@ :mod:`.moving_camera` + +Examples +-------- + +.. manim:: ChangingCameraWidthAndRestore + + class ChangingCameraWidthAndRestore(MovingCameraScene): + def construct(self): + text = Text("Hello World").set_color(BLUE) + self.add(text) + self.camera_frame.save_state() + self.play(self.camera_frame.set_width, text.get_width() * 1.2) + self.wait(0.3) + self.play(Restore(self.camera_frame)) + + +.. manim:: MovingCameraCenter + + class MovingCameraCenter(MovingCameraScene): + def construct(self): + s = Square(color=RED, fill_opacity=0.5).move_to(2 * LEFT) + t = Triangle(color=GREEN, fill_opacity=0.5).move_to(2 * RIGHT) + self.wait(0.3) + self.add(s, t) + self.play(self.camera_frame.move_to, s) + self.wait(0.3) + self.play(self.camera_frame.move_to, t) + + +.. manim:: MovingAndZoomingCamera + + class MovingAndZoomingCamera(MovingCameraScene): + def construct(self): + s = Square(color=BLUE, fill_opacity=0.5).move_to(2 * LEFT) + t = Triangle(color=YELLOW, fill_opacity=0.5).move_to(2 * RIGHT) + self.add(s, t) + self.play(self.camera_frame.move_to, s, + self.camera_frame.set_width,s.get_width()*2) + self.wait(0.3) + self.play(self.camera_frame.move_to, t, + self.camera_frame.set_width,t.get_width()*2) + + self.play(self.camera_frame.move_to, ORIGIN, + self.camera_frame.set_width,14) + +.. manim:: MovingCameraOnGraph + + class MovingCameraOnGraph(GraphScene, MovingCameraScene): + def setup(self): + GraphScene.setup(self) + MovingCameraScene.setup(self) + def construct(self): + self.camera_frame.save_state() + self.setup_axes(animate=False) + graph = self.get_graph(lambda x: np.sin(x), + color=WHITE, + x_min=0, + x_max=3 * PI + ) + dot_at_start_graph = Dot().move_to(graph.points[0]) + dot_at_end_grap = Dot().move_to(graph.points[-1]) + self.add(graph, dot_at_end_grap, dot_at_start_graph) + self.play(self.camera_frame.scale, 0.5, self.camera_frame.move_to, dot_at_start_graph) + self.play(self.camera_frame.move_to, dot_at_end_grap) + self.play(Restore(self.camera_frame)) + self.wait() + """ __all__ = ["MovingCameraScene"] diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 108725cf61..23b2d3c21a 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -8,21 +8,18 @@ import random import warnings import platform -import copy from tqdm import tqdm as ProgressDisplay import numpy as np -from .. import camera_config, file_writer_config, logger -from ..animation.animation import Animation -from ..animation.transform import MoveToTarget, ApplyMethod +from .. import config, logger +from ..animation.animation import Animation, Wait +from ..animation.transform import MoveToTarget from ..camera.camera import Camera from ..constants import * from ..container import Container from ..mobject.mobject import Mobject -from ..scene.scene_file_writer import SceneFileWriter -from ..utils.iterables import list_update -from ..utils.hashing import get_hash_from_play_call, get_hash_from_wait_call +from ..utils.iterables import list_update, list_difference_update from ..utils.family import extract_mobject_family_members from ..renderer.cairo_renderer import CairoRenderer from ..utils.exceptions import EndSceneEarlyException @@ -58,7 +55,6 @@ def construct(self): CONFIG = { "camera_class": Camera, - "skip_animations": False, "always_update_mobjects": False, "random_seed": 0, } @@ -66,7 +62,10 @@ def construct(self): def __init__(self, renderer=None, **kwargs): Container.__init__(self, **kwargs) if renderer is None: - self.renderer = CairoRenderer(camera_class=self.camera_class) + self.renderer = CairoRenderer( + camera_class=self.camera_class, + skip_animations=kwargs.get("skip_animations", False), + ) else: self.renderer = renderer self.renderer.init(self) @@ -78,12 +77,15 @@ def __init__(self, renderer=None, **kwargs): random.seed(self.random_seed) np.random.seed(self.random_seed) - self.setup() + @property + def camera(self): + return self.renderer.camera def render(self): """ Render this Scene. """ + self.setup() try: self.construct() except EndSceneEarlyException: @@ -120,21 +122,6 @@ def construct(self): def __str__(self): return self.__class__.__name__ - def set_variables_as_attrs(self, *objects, **newly_named_objects): - """ - This method is slightly hacky, making it a little easier - for certain methods (typically subroutines of construct) - to share local variables. - """ - caller_locals = inspect.currentframe().f_back.f_locals - for key, value in list(caller_locals.items()): - for o in objects: - if value is o: - setattr(self, key, value) - for key, value in list(newly_named_objects.items()): - setattr(self, key, value) - return self - def get_attrs(self, *keys): """ Gets attributes of a scene given the attribute's identifier/name. @@ -151,8 +138,6 @@ def get_attrs(self, *keys): """ return [getattr(self, key) for key in keys] - ### - def update_mobjects(self, dt): """ Begins updating all mobjects in the Scene. @@ -178,10 +163,6 @@ def should_update_mobjects(self): [mob.has_time_based_updater() for mob in self.get_mobject_family_members()] ) - ### - - ### - def get_top_level_mobjects(self): """ Returns all mobjects which are not submobjects. @@ -193,14 +174,13 @@ def get_top_level_mobjects(self): """ # Return only those which are not in the family # of another mobject from the scene - mobjects = self.get_mobjects() - families = [m.get_family() for m in mobjects] + families = [m.get_family() for m in self.mobjects] def is_top_level(mobject): num_families = sum([(mobject in family) for family in families]) return num_families == 1 - return list(filter(is_top_level, mobjects)) + return list(filter(is_top_level, self.mobjects)) def get_mobject_family_members(self): """ @@ -239,15 +219,6 @@ def add(self, *mobjects): self.mobjects += mobjects return self - def add_mobjects_among(self, values): - """ - This is meant mostly for quick prototyping, - e.g. to add all mobjects defined up to a point, - call self.add_mobjects_among(locals().values()) - """ - self.add(*filter(lambda m: isinstance(m, Mobject), values)) - return self - def add_mobjects_from_animations(self, animations): curr_mobjects = self.get_mobject_family_members() @@ -255,7 +226,7 @@ def add_mobjects_from_animations(self, animations): # Anything animated that's not already in the # scene gets added to the scene mob = animation.mobject - if mob not in curr_mobjects: + if mob is not None and mob not in curr_mobjects: self.add(mob) curr_mobjects += mob.get_family() @@ -475,30 +446,6 @@ def clear(self): self.foreground_mobjects = [] return self - def get_mobjects(self): - """ - Returns all the mobjects in self.mobjects - - Returns - ------ - list - The list of self.mobjects . - """ - return list(self.mobjects) - - def get_mobject_copies(self): - """ - Returns a copy of all mobjects present in - self.mobjects . - - Returns - ------ - list - A list of the copies of all the mobjects - in self.mobjects - """ - return [m.copy() for m in self.mobjects] - def get_moving_mobjects(self, *animations): """ Gets all moving mobjects in the passed animation(s). @@ -530,98 +477,23 @@ def get_moving_mobjects(self, *animations): return mobjects[i:] return [] - def get_time_progression( - self, run_time, n_iterations=None, override_skip_animations=False - ): - """ - You will hardly use this when making your own animations. - This method is for Manim's internal use. - - Returns a CommandLine ProgressBar whose fill_time - is dependent on the run_time of an animation, - the iterations to perform in that animation - and a bool saying whether or not to consider - the skipped animations. - - Parameters - ---------- - run_time: float - The run_time of the animation. - - n_iterations: int, optional - The number of iterations in the animation. - - override_skip_animations: bool, optional - Whether or not to show skipped animations in the progress bar. - - Returns - ------ - ProgressDisplay - The CommandLine Progress Bar. - """ - if file_writer_config["skip_animations"] and not override_skip_animations: - times = [run_time] - else: - step = 1 / self.renderer.camera.frame_rate - times = np.arange(0, run_time, step) - time_progression = ProgressDisplay( - times, - total=n_iterations, - leave=file_writer_config["leave_progress_bars"], - ascii=True if platform.system() == "Windows" else None, - disable=not file_writer_config["progress_bar"], + def get_moving_and_stationary_mobjects(self, animations): + moving_mobjects = self.get_moving_mobjects(*animations) + all_mobjects = list_update(self.mobjects, self.foreground_mobjects) + all_mobject_families = extract_mobject_family_members( + all_mobjects, + use_z_index=self.renderer.camera.use_z_index, + only_those_with_points=True, ) - return time_progression - - def get_run_time(self, animations): - """ - Gets the total run time for a list of animations. - - Parameters - ---------- - animations: list of Animation - A list of the animations whose total - run_time is to be calculated. - - Returns - ------ - float - The total run_time of all of the animations in the list. - """ - - return np.max([animation.run_time for animation in animations]) - - def get_animation_time_progression(self, animations): - """ - You will hardly use this when making your own animations. - This method is for Manim's internal use. - - Uses get_time_progression to obtaina - CommandLine ProgressBar whose fill_time is - dependent on the qualities of the passed Animation, - - Parameters - ---------- - animations : list of Animation - The list of animations to get - the time progression for. - - Returns - ------ - ProgressDisplay - The CommandLine Progress Bar. - """ - time_progression = self.get_time_progression(self.run_time) - time_progression.set_description( - "".join( - [ - "Animation {}: ".format(self.renderer.num_plays), - str(animations[0]), - (", etc." if len(animations) > 1 else ""), - ] - ) + moving_mobjects = self.get_moving_mobjects(*animations) + all_moving_mobject_families = extract_mobject_family_members( + moving_mobjects, + use_z_index=self.renderer.camera.use_z_index, ) - return time_progression + stationary_mobjects = list_difference_update( + all_mobject_families, all_moving_mobject_families + ) + return all_moving_mobject_families, stationary_mobjects def compile_play_args_to_animation_list(self, *args, **kwargs): """ @@ -630,7 +502,7 @@ def compile_play_args_to_animation_list(self, *args, **kwargs): by a dict of kwargs for that method). This animation list is built by going through the args list, and each animation is simply added, but when a mobject method - s hit, a MoveToTarget animation is built using the args that + is hit, a MoveToTarget animation is built using the args that follow up until either another animation is hit, another method is hit, or the args list runs out. @@ -702,164 +574,90 @@ def compile_method(state): return animations - def begin_animations(self, animations): - """ - This method begins the list of animations that is passed, - and adds any mobjects involved (if not already present) - to the scene again. - - Parameters - ---------- - animations : list - List of involved animations. - - """ - for animation in animations: - # Begin animation - animation.begin() - - def progress_through_animations(self): - """ - This method progresses through each animation - in the list passed and and updates the frames as required. - """ - for t in self.get_animation_time_progression(self.animations): - self.update_animation_to_time(t) - self.renderer.update_frame(self, self.moving_mobjects, self.static_image) - self.renderer.add_frame(self.renderer.get_frame()) - - def update_animation_to_time(self, t): - """ - Updates the current animation to the specified time. - - Parameters - ---------- - t : int - Offset from the start of the animation to which to update the current - animation. + def get_time_progression( + self, run_time, n_iterations=None, override_skip_animations=False + ): """ - dt = t - self.last_t - self.last_t = t - for animation in self.animations: - animation.update_mobjects(dt) - alpha = t / animation.run_time - animation.interpolate(alpha) - self.update_mobjects(dt) + You will hardly use this when making your own animations. + This method is for Manim's internal use. - def finish_animations(self, animations): - """ - This function cleans up after the end - of each animation in the passed list. + Returns a CommandLine ProgressBar whose ``fill_time`` + is dependent on the ``run_time`` of an animation, + the iterations to perform in that animation + and a bool saying whether or not to consider + the skipped animations. Parameters ---------- - animations : list - list of animations to finish. - """ - for animation in animations: - animation.finish() - animation.clean_up_from_scene(self) - # TODO: This is only used in one place and should probably be removed. - self.mobjects_from_last_animation = [anim.mobject for anim in animations] - if file_writer_config["skip_animations"]: - # TODO, run this call in for each animation? - self.update_mobjects(self.get_run_time(animations)) - else: - self.update_mobjects(0) - - def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): - self.renderer.wait(self, duration=duration, stop_condition=stop_condition) + run_time : float + The ``run_time`` of the animation. - def play(self, *args, **kwargs): - self.renderer.play(self, *args, **kwargs) + n_iterations : int, optional + The number of iterations in the animation. - def play_internal(self, *args, **kwargs): - """ - This method is used to prep the animations for rendering, - apply the arguments and parameters required to them, - render them, and write them to the video file. + override_skip_animations : bool, optional + Whether or not to show skipped animations in the progress bar. - Parameters - ---------- - *args : Animation or mobject with mobject method and params - **kwargs : named parameters affecting what was passed in *args e.g run_time, lag_ratio etc. + Returns + ------- + ProgressDisplay + The CommandLine Progress Bar. """ - if len(args) == 0: - warnings.warn("Called Scene.play with no animations") - return - self.animations = self.compile_play_args_to_animation_list(*args, **kwargs) - self.begin_animations(self.animations) - - # Paint all non-moving objects onto the screen, so they don't - # have to be rendered every frame - self.moving_mobjects = self.get_moving_mobjects(*self.animations) - self.renderer.update_frame(self, excluded_mobjects=self.moving_mobjects) - self.static_image = self.renderer.get_frame() - self.last_t = 0 - self.run_time = self.get_run_time(self.animations) - - self.progress_through_animations() - - self.finish_animations(self.animations) - - def wait_internal(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): - self.update_mobjects(dt=0) # Any problems with this? - self.animations = [] - self.duration = duration - self.stop_condition = stop_condition - self.last_t = 0 - - if self.should_update_mobjects(): - time_progression = self.get_wait_time_progression(duration, stop_condition) - # TODO, be smart about setting a static image - # the same way Scene.play does - for t in time_progression: - self.update_animation_to_time(t) - self.renderer.update_frame(self) - self.renderer.add_frame(self.renderer.get_frame()) - if stop_condition is not None and stop_condition(): - time_progression.close() - break - elif self.skip_animations: - # Do nothing - return self + if self.renderer.skip_animations and not override_skip_animations: + times = [run_time] else: - self.renderer.update_frame(self) - dt = 1 / self.renderer.camera.frame_rate - self.renderer.add_frame( - self.renderer.get_frame(), num_frames=int(duration / dt) - ) - return self + step = 1 / self.renderer.camera.frame_rate + times = np.arange(0, run_time, step) + time_progression = ProgressDisplay( + times, + total=n_iterations, + leave=config["leave_progress_bars"], + ascii=True if platform.system() == "Windows" else None, + disable=not config["progress_bar"], + ) + return time_progression - def clean_up_animations(self, *animations): + def _get_animation_time_progression(self, animations): """ - This method cleans up and removes from the - scene all the animations that were passed + You will hardly use this when making your own animations. + This method is for Manim's internal use. + + Uses :func:`~.get_time_progression` to obtain a + CommandLine ProgressBar whose ``fill_time`` is + dependent on the qualities of the passed Animation, Parameters ---------- - *animations : Animation - Animation to clean up. + animations : List[:class:`~.Animation`, ...] + The list of animations to get + the time progression for. Returns ------- - Scene - The scene with the animations - cleaned up. - + ProgressDisplay + The CommandLine Progress Bar. """ - for animation in animations: - animation.clean_up_from_scene(self) - return self + run_time = self.get_run_time(animations) + time_progression = self.get_time_progression(run_time) + time_progression.set_description( + "".join( + [ + "Animation {}: ".format(self.renderer.num_plays), + str(animations[0]), + (", etc." if len(animations) > 1 else ""), + ] + ) + ) + return time_progression - def get_wait_time_progression(self, duration, stop_condition): + def _get_wait_time_progression(self, duration, stop_condition): """ This method is used internally to obtain the CommandLine Progressbar for when self.wait() is called in a scene. Parameters ---------- - duration: int or float + duration : int or float duration of wait time stop_condition : function @@ -887,6 +685,30 @@ def get_wait_time_progression(self, duration, stop_condition): ) return time_progression + def get_run_time(self, animations): + """ + Gets the total run time for a list of animations. + + Parameters + ---------- + animations : List[:class:`Animation`, ...] + A list of the animations whose total + ``run_time`` is to be calculated. + + Returns + ------- + float + The total ``run_time`` of all of the animations in the list. + """ + + return np.max([animation.run_time for animation in animations]) + + def play(self, *args, **kwargs): + self.renderer.play(self, *args, **kwargs) + + def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): + self.play(Wait(duration=duration, stop_condition=stop_condition)) + def wait_until(self, stop_condition, max_time=60): """ Like a wrapper for wait(). @@ -903,6 +725,86 @@ def wait_until(self, stop_condition, max_time=60): """ self.wait(max_time, stop_condition=stop_condition) + def play_internal(self, *args, **kwargs): + """ + This method is used to prep the animations for rendering, + apply the arguments and parameters required to them, + render them, and write them to the video file. + + Parameters + ---------- + args + Animation or mobject with mobject method and params + kwargs + named parameters affecting what was passed in ``args``, + e.g. ``run_time``, ``lag_ratio`` and so on. + """ + if len(args) == 0: + warnings.warn("Called Scene.play with no animations") + return + + animations = self.compile_play_args_to_animation_list(*args, **kwargs) + if ( + len(animations) == 1 + and isinstance(animations[0], Wait) + and not self.should_update_mobjects() + ): + self.add_static_frames(animations[0].duration) + return + + for animation in animations: + animation.begin() + + moving_mobjects = None + static_mobjects = None + duration = None + stop_condition = None + time_progression = None + if len(animations) == 1 and isinstance(animations[0], Wait): + # TODO, be smart about setting a static image + # the same way Scene.play does + duration = animations[0].duration + stop_condition = animations[0].stop_condition + self.static_image = None + time_progression = self._get_wait_time_progression(duration, stop_condition) + else: + # Paint all non-moving objects onto the screen, so they don't + # have to be rendered every frame + ( + moving_mobjects, + stationary_mobjects, + ) = self.get_moving_and_stationary_mobjects(animations) + self.renderer.update_frame(self, mobjects=stationary_mobjects) + self.static_image = self.renderer.get_frame() + time_progression = self._get_animation_time_progression(animations) + + last_t = 0 + for t in time_progression: + dt = t - last_t + last_t = t + for animation in animations: + animation.update_mobjects(dt) + alpha = t / animation.run_time + animation.interpolate(alpha) + self.update_mobjects(dt) + self.renderer.update_frame(self, moving_mobjects, self.static_image) + self.renderer.add_frame(self.renderer.get_frame()) + if stop_condition is not None and stop_condition(): + time_progression.close() + break + + for animation in animations: + animation.finish() + animation.clean_up_from_scene(self) + + def add_static_frames(self, duration): + self.renderer.update_frame(self) + dt = 1 / self.renderer.camera.frame_rate + self.renderer.add_frame( + self.renderer.get_frame(), + num_frames=int(duration / dt), + ) + def add_sound(self, sound_file, time_offset=0, gain=None, **kwargs): """ This method is used to add a sound to the animation. @@ -919,7 +821,7 @@ def add_sound(self, sound_file, time_offset=0, gain=None, **kwargs): gain : """ - if file_writer_config["skip_animations"]: + if self.renderer.skip_animations: return time = self.time + time_offset self.renderer.file_writer.add_sound(sound_file, time, gain, **kwargs) diff --git a/manim/scene/scene_file_writer.py b/manim/scene/scene_file_writer.py index e116477153..0804b86d74 100644 --- a/manim/scene/scene_file_writer.py +++ b/manim/scene/scene_file_writer.py @@ -8,12 +8,12 @@ import shutil import subprocess import os -import _thread as thread from time import sleep import datetime from PIL import Image +from pathlib import Path -from .. import file_writer_config, logger, console +from .. import config, logger from ..constants import FFMPEG_BIN, GIF_FILE_EXTENSION from ..utils.config_ops import digest_config from ..utils.file_ops import guarantee_existence @@ -52,71 +52,62 @@ def __init__(self, renderer, video_quality_config, scene_name, **kwargs): self.frame_count = 0 self.partial_movie_files = [] - # Output directories and files def init_output_directories(self, scene_name): + """Initialise output directories. + + Notes + ----- + The directories are read from ``config``, for example + ``config['media_dir']``. If the target directories don't already + exist, they will be created. + """ - This method initialises the directories to which video - files will be written to and read from (within MEDIA_DIR). - If they don't already exist, they will be created. - """ - module_directory = self.get_default_module_directory() - default_name = self.get_default_scene_name(scene_name) - if file_writer_config["save_last_frame"] or file_writer_config["save_pngs"]: - if file_writer_config["media_dir"] != "": - if not file_writer_config["custom_folders"]: - image_dir = guarantee_existence( - os.path.join( - file_writer_config["images_dir"], - module_directory, - ) - ) - else: - image_dir = guarantee_existence(file_writer_config["images_dir"]) + if config["dry_run"]: # in dry-run mode there is no output + return + + if config["input_file"]: + module_name = config.get_dir("input_file").stem + else: + module_name = "" + + if config["output_file"]: + default_name = config.get_dir("output_file") + else: + default_name = Path(scene_name) + + if config["save_last_frame"] or config["save_pngs"]: + if config["media_dir"]: + image_dir = guarantee_existence( + config.get_dir("images_dir", module_name=module_name) + ) self.image_file_path = os.path.join( image_dir, add_extension_if_not_present(default_name, ".png") ) - if file_writer_config["write_to_movie"]: - if file_writer_config["video_dir"]: - if not file_writer_config["custom_folders"]: - movie_dir = guarantee_existence( - os.path.join( - file_writer_config["video_dir"], - module_directory, - self.get_resolution_directory(), - ) - ) - else: - movie_dir = guarantee_existence( - os.path.join(file_writer_config["video_dir"]) - ) + if config["write_to_movie"]: + movie_dir = guarantee_existence( + config.get_dir("video_dir", module_name=module_name) + ) + self.movie_file_path = os.path.join( movie_dir, add_extension_if_not_present( - default_name, file_writer_config["movie_file_extension"] + default_name, config["movie_file_extension"] ), ) - self.gif_file_path = os.path.join( - movie_dir, - add_extension_if_not_present(default_name, GIF_FILE_EXTENSION), - ) - if not file_writer_config["custom_folders"]: - self.partial_movie_directory = guarantee_existence( - os.path.join( - movie_dir, - "partial_movie_files", - default_name, - ) + if config["save_as_gif"]: + self.gif_file_path = os.path.join( + movie_dir, + add_extension_if_not_present(default_name, GIF_FILE_EXTENSION), ) - else: - self.partial_movie_directory = guarantee_existence( - os.path.join( - file_writer_config["media_dir"], - "temp_files", - "partial_movie_files", - default_name, - ) + + self.partial_movie_directory = guarantee_existence( + config.get_dir( + "partial_movie_dir", + scene_name=default_name, + module_name=module_name, ) + ) def add_partial_movie_file(self, hash_animation): """Adds a new partial movie file path to scene.partial_movie_files from an hash. This method will compute the path from the hash. @@ -126,6 +117,8 @@ def add_partial_movie_file(self, hash_animation): hash_animation : str Hash of the animation. """ + if not hasattr(self, "partial_movie_directory"): + return # None has to be added to partial_movie_files to keep the right index with scene.num_plays. # i.e if an animation is skipped, scene.num_plays is still incremented and we add an element to partial_movie_file be even with num_plays. @@ -136,40 +129,11 @@ def add_partial_movie_file(self, hash_animation): self.partial_movie_directory, "{}{}".format( hash_animation, - file_writer_config["movie_file_extension"], + config["movie_file_extension"], ), ) self.partial_movie_files.append(new_partial_movie_file) - def get_default_module_directory(self): - """ - This method gets the name of the directory containing - the file that has the Scene that is being rendered. - - Returns - ------- - str - The name of the directory. - """ - filename = os.path.basename(file_writer_config["input_file"]) - root, _ = os.path.splitext(filename) - return root - - def get_default_scene_name(self, scene_name): - """ - This method returns the default scene name - which is the value of "output_file", if it exists and - the actual name of the class that inherited from - Scene in your animation script, if "output_file" is None. - - Returns - ------- - str - The default scene name. - """ - fn = file_writer_config["output_file"] - return fn if fn else scene_name - def get_resolution_directory(self): """Get the name of the resolution directory directly containing the video file. @@ -195,36 +159,10 @@ def get_resolution_directory(self): :class:`str` The name of the directory. """ - pixel_height = self.video_quality_config["pixel_height"] - frame_rate = self.video_quality_config["frame_rate"] + pixel_height = config["pixel_height"] + frame_rate = config["frame_rate"] return "{}p{}".format(pixel_height, frame_rate) - # Directory getters - def get_image_file_path(self): - """ - This returns the directory path to which any images will be - written to. - It is usually named "images", but can be changed by changing - "image_file_path". - - Returns - ------- - str - The path of the directory. - """ - return self.image_file_path - - def get_movie_file_path(self): - """ - Returns the final path of the written video file. - - Returns - ------- - str - The path of the movie file. - """ - return self.movie_file_path - # Sound def init_audio(self): """ @@ -315,7 +253,7 @@ def begin_animation(self, allow_write=False): allow_write : bool, optional Whether or not to write to a video file. """ - if file_writer_config["write_to_movie"] and allow_write: + if config["write_to_movie"] and allow_write: self.open_movie_pipe() def end_animation(self, allow_write=False): @@ -328,7 +266,7 @@ def end_animation(self, allow_write=False): allow_write : bool, optional Whether or not to write to a video file. """ - if file_writer_config["write_to_movie"] and allow_write: + if config["write_to_movie"] and allow_write: self.close_movie_pipe() def write_frame(self, frame): @@ -341,9 +279,9 @@ def write_frame(self, frame): frame : np.array Pixel array of the frame. """ - if file_writer_config["write_to_movie"]: + if config["write_to_movie"]: self.writing_process.stdin.write(frame.tostring()) - if file_writer_config["save_pngs"]: + if config["save_pngs"]: path, extension = os.path.splitext(self.image_file_path) Image.fromarray(frame).save(f"{path}{self.frame_count}{extension}") self.frame_count += 1 @@ -358,7 +296,7 @@ def save_final_image(self, image): image : np.array The pixel array of the image to save. """ - file_path = self.get_image_file_path() + file_path = self.image_file_path image.save(file_path) self.print_file_ready_message(file_path) @@ -374,7 +312,7 @@ def idle_stream(self): self.add_frame(*[frame] * n_frames) b = datetime.datetime.now() time_diff = (b - a).total_seconds() - frame_duration = 1 / self.video_quality_config["frame_rate"] + frame_duration = 1 / config["frame_rate"] if time_diff < frame_duration: sleep(frame_duration - time_diff) @@ -386,11 +324,11 @@ def finish(self): If save_last_frame is True, saves the last frame in the default image directory. """ - if file_writer_config["write_to_movie"]: + if config["write_to_movie"]: if hasattr(self, "writing_process"): self.writing_process.terminate() self.combine_movie_files() - if file_writer_config["flush_cache"]: + if config["flush_cache"]: self.flush_cache_directory() else: self.clean_cache() @@ -405,16 +343,14 @@ def open_movie_pipe(self): # TODO #486 Why does ffmpeg need temp files ? temp_file_path = ( - os.path.splitext(file_path)[0] - + "_temp" - + file_writer_config["movie_file_extension"] + os.path.splitext(file_path)[0] + "_temp" + config["movie_file_extension"] ) self.partial_movie_file_path = file_path self.temp_partial_movie_file_path = temp_file_path - fps = self.video_quality_config["frame_rate"] - height = self.video_quality_config["pixel_height"] - width = self.video_quality_config["pixel_width"] + fps = config["frame_rate"] + height = config["pixel_height"] + width = config["pixel_width"] command = [ FFMPEG_BIN, @@ -431,24 +367,12 @@ def open_movie_pipe(self): "-", # The imput comes from a pipe "-an", # Tells FFMPEG not to expect any audio "-loglevel", - file_writer_config["ffmpeg_loglevel"], + config["ffmpeg_loglevel"].lower(), ] - # TODO, the test for a transparent background should not be based on - # the file extension. - if file_writer_config["movie_file_extension"] == ".mov": - # This is if the background of the exported - # video should be transparent. - command += [ - "-vcodec", - "qtrle", - ] + if config["transparent"]: + command += ["-vcodec", "qtrle"] else: - command += [ - "-vcodec", - "libx264", - "-pix_fmt", - "yuv420p", - ] + command += ["-vcodec", "libx264", "-pix_fmt", "yuv420p"] command += [temp_file_path] self.writing_process = subprocess.Popen(command, stdin=subprocess.PIPE) @@ -482,9 +406,11 @@ def is_already_cached(self, hash_invocation): :class:`bool` Whether the file exists. """ + if not hasattr(self, "partial_movie_directory"): + return False path = os.path.join( self.partial_movie_directory, - "{}{}".format(hash_invocation, self.movie_file_extension), + "{}{}".format(hash_invocation, config["movie_file_extension"]), ) return os.path.exists(path) @@ -494,20 +420,20 @@ def combine_movie_files(self): partial movie files that make up a Scene into a single video file for that Scene. """ - # Manim renders the scene as many smaller movie files - # which are then concatenated to a larger one. The reason - # for this is that sometimes video-editing is made easier when - # one works with the broken up scene, which effectively has - # cuts at all the places you might want. But for viewing - # the scene as a whole, one of course wants to see it as a + # Manim renders the scene as many smaller movie files which are then + # concatenated to a larger one. The reason for this is that sometimes + # video-editing is made easier when one works with the broken up scene, + # which effectively has cuts at all the places you might want. But for + # viewing the scene as a whole, one of course wants to see it as a # single piece. partial_movie_files = [el for el in self.partial_movie_files if el is not None] - # NOTE : Here we should do a check and raise an exeption if partial movie file is empty. - # We can't, as a lot of stuff (in particular, in tests) use scene initialization, and this error would be raised as it's just - # an empty scene initialized. + # NOTE : Here we should do a check and raise an exeption if partial + # movie file is empty. We can't, as a lot of stuff (in particular, in + # tests) use scene initialization, and this error would be raised as + # it's just an empty scene initialized. - # Write a file partial_file_list.txt containing all - # partial movie files. This is used by FFMPEG. + # Write a file partial_file_list.txt containing all partial movie + # files. This is used by FFMPEG. file_list = os.path.join( self.partial_movie_directory, "partial_movie_file_list.txt" ) @@ -521,7 +447,7 @@ def combine_movie_files(self): if os.name == "nt": pf_path = pf_path.replace("\\", "/") fp.write("file 'file:{}'\n".format(pf_path)) - movie_file_path = self.get_movie_file_path() + movie_file_path = self.movie_file_path commands = [ FFMPEG_BIN, "-y", # overwrite output file if it exists @@ -532,13 +458,13 @@ def combine_movie_files(self): "-i", file_list, "-loglevel", - file_writer_config["ffmpeg_loglevel"], + config["ffmpeg_loglevel"].lower(), ] - if self.write_to_movie and not self.save_as_gif: + if config["write_to_movie"] and not config["save_as_gif"]: commands += ["-c", "copy", movie_file_path] - if self.save_as_gif: + if config["save_as_gif"]: commands += [self.gif_file_path] if not self.includes_sound: @@ -549,7 +475,7 @@ def combine_movie_files(self): if self.includes_sound: sound_file_path = movie_file_path.replace( - file_writer_config["movie_file_extension"], ".wav" + config["movie_file_extension"], ".wav" ) # Makes sure sound file length will match video file self.add_audio_segment(AudioSegment.silent(0)) @@ -578,7 +504,7 @@ def combine_movie_files(self): "-map", "1:a:0", "-loglevel", - file_writer_config["ffmpeg_loglevel"], + config["ffmpeg_loglevel"].lower(), # "-shortest", temp_file_path, ] @@ -587,9 +513,9 @@ def combine_movie_files(self): os.remove(sound_file_path) self.print_file_ready_message( - self.gif_file_path if self.save_as_gif else movie_file_path + self.gif_file_path if config["save_as_gif"] else movie_file_path ) - if file_writer_config["write_to_movie"]: + if config["write_to_movie"]: for file_path in partial_movie_files: # We have to modify the accessed time so if we have to clean the cache we remove the one used the longest. modify_atime(file_path) @@ -601,9 +527,9 @@ def clean_cache(self): for file_name in os.listdir(self.partial_movie_directory) if file_name != "partial_movie_file_list.txt" ] - if len(cached_partial_movies) > file_writer_config["max_files_cached"]: + if len(cached_partial_movies) > config["max_files_cached"]: number_files_to_delete = ( - len(cached_partial_movies) - file_writer_config["max_files_cached"] + len(cached_partial_movies) - config["max_files_cached"] ) oldest_files_to_delete = sorted( [partial_movie_file for partial_movie_file in cached_partial_movies], @@ -613,7 +539,7 @@ def clean_cache(self): for file_to_delete in oldest_files_to_delete: os.remove(file_to_delete) logger.info( - f"The partial movie directory is full (> {file_writer_config['max_files_cached']} files). Therefore, manim has removed {number_files_to_delete} file(s) used by it the longest ago." + f"The partial movie directory is full (> {config['max_files_cached']} files). Therefore, manim has removed {number_files_to_delete} file(s) used by it the longest ago." + "You can change this behaviour by changing max_files_cached in config." ) @@ -632,7 +558,5 @@ def flush_cache_directory(self): ) def print_file_ready_message(self, file_path): - """ - Prints the "File Ready" message to STDOUT. - """ + """Prints the "File Ready" message to STDOUT.""" logger.info("\nFile ready at %(file_path)s\n", {"file_path": file_path}) diff --git a/manim/scene/three_d_scene.py b/manim/scene/three_d_scene.py index 5065899f5d..b7a3119a0b 100644 --- a/manim/scene/three_d_scene.py +++ b/manim/scene/three_d_scene.py @@ -165,7 +165,7 @@ def move_camera( anims.append(ApplyMethod(tracker.set_value, value, **kwargs)) if frame_center is not None: anims.append( - ApplyMethod(self.renderer.camera.frame_center.move_to, frame_center) + ApplyMethod(self.renderer.camera._frame_center.move_to, frame_center) ) self.play(*anims + added_anims) @@ -308,11 +308,11 @@ class SpecialThreeDScene(ThreeDScene): def __init__(self, **kwargs): digest_config(self, kwargs) if self.renderer.camera_config["pixel_width"] == config["pixel_width"]: - config = {} + _config = {} else: - config = self.low_quality_config - config = merge_dicts_recursively(config, kwargs) - ThreeDScene.__init__(self, **config) + _config = self.low_quality_config + _config = merge_dicts_recursively(_config, kwargs) + ThreeDScene.__init__(self, **_config) def get_axes(self): """Return a set of 3D axes. diff --git a/manim/scene/vector_space_scene.py b/manim/scene/vector_space_scene.py index 105655759d..60fc34e4ad 100644 --- a/manim/scene/vector_space_scene.py +++ b/manim/scene/vector_space_scene.py @@ -401,13 +401,14 @@ def coords_to_vector(self, vector, coords_start=2 * RIGHT + 2 * UP, clean_up=Tru ) ) self.play(ShowCreation(x_line)) - self.play( + animations = [ ApplyFunction( lambda y: self.position_y_coordinate(y, y_line, vector), y_coord ), FadeOut(array.get_brackets()), - ) - y_coord, brackets = self.mobjects_from_last_animation + ] + self.play(*animations) + y_coord, _ = [anim.mobject for anim in animations] self.play(ShowCreation(y_line)) self.play(ShowCreation(arrow)) self.wait() @@ -545,7 +546,7 @@ def __init__(self, **kwargs): "x_min": -config["frame_width"] / 2, "y_max": config["frame_width"] / 2, "y_min": -config["frame_width"] / 2, - "faded_line_ratio": 0, + "faded_line_ratio": 1, } def setup(self): diff --git a/manim/scene/zoomed_scene.py b/manim/scene/zoomed_scene.py index e88bfa5042..36fd37e87f 100644 --- a/manim/scene/zoomed_scene.py +++ b/manim/scene/zoomed_scene.py @@ -1,4 +1,45 @@ -"""A scene supporting zooming in on a specified section.""" +"""A scene supporting zooming in on a specified section. + + +Examples +-------- + +.. manim:: UseZoomedScene + + class UseZoomedScene(ZoomedScene): + def construct(self): + dot = Dot().set_color(GREEN) + self.add(dot) + self.wait(1) + self.activate_zooming(animate=False) + self.wait(1) + self.play(dot.shift, LEFT) + +.. manim:: ChangingZoomScale + + class ChangingZoomScale(ZoomedScene): + CONFIG = { + "zoom_factor": 0.3, + "zoomed_display_height": 1, + "zoomed_display_width": 3, + "image_frame_stroke_width": 20, + "zoomed_camera_config": { + "default_frame_stroke_width": 3, + }, + } + def construct(self): + dot = Dot().set_color(GREEN) + sq = Circle(fill_opacity=1, radius=0.2).next_to(dot, RIGHT) + self.add(dot, sq) + self.wait(1) + self.activate_zooming(animate=False) + self.wait(1) + self.play(dot.shift, LEFT * 0.3) + + self.play(self.zoomed_camera.frame.scale, 4) + self.play(self.zoomed_camera.frame.shift, 0.5 * DOWN) + +""" __all__ = ["ZoomedScene"] diff --git a/manim/utils/caching.py b/manim/utils/caching.py index 6b611b96c3..72a3b7ac51 100644 --- a/manim/utils/caching.py +++ b/manim/utils/caching.py @@ -1,6 +1,5 @@ -from .. import file_writer_config, logger -from ..utils.hashing import get_hash_from_play_call, get_hash_from_wait_call -from ..constants import DEFAULT_WAIT_TIME +from .. import config, logger +from ..utils.hashing import get_hash_from_play_call def handle_caching_play(func): @@ -19,11 +18,11 @@ def handle_caching_play(func): """ def wrapper(self, scene, *args, **kwargs): - self.revert_to_original_skipping_status() + self.skip_animations = self.original_skipping_status self.update_skipping_status() animations = scene.compile_play_args_to_animation_list(*args, **kwargs) scene.add_mobjects_from_animations(animations) - if file_writer_config["skip_animations"]: + if self.skip_animations: logger.debug(f"Skipping animation {self.num_plays}") func(self, scene, *args, **kwargs) # If the animation is skipped, we mark its hash as None. @@ -31,8 +30,8 @@ def wrapper(self, scene, *args, **kwargs): self.animations_hashes.append(None) self.file_writer.add_partial_movie_file(None) return - if not file_writer_config["disable_caching"]: - mobjects_on_scene = scene.get_mobjects() + if not config["disable_caching"]: + mobjects_on_scene = scene.mobjects hash_play = get_hash_from_play_call( self, self.camera, animations, mobjects_on_scene ) @@ -41,7 +40,7 @@ def wrapper(self, scene, *args, **kwargs): f"Animation {self.num_plays} : Using cached data (hash : %(hash_play)s)", {"hash_play": hash_play}, ) - file_writer_config["skip_animations"] = True + self.skip_animations = True else: hash_play = "uncached_{:05}".format(self.num_plays) self.animations_hashes.append(hash_play) @@ -53,50 +52,3 @@ def wrapper(self, scene, *args, **kwargs): func(self, scene, *args, **kwargs) return wrapper - - -def handle_caching_wait(func): - """Decorator that returns a wrapped version of func that will compute the hash of - the wait invocation. - - The returned function will act according to the computed hash: either skip the - animation because it's already cached, or let the invoked function play normally. - - Parameters - ---------- - func : Callable[[...], None] - The wait like function that has to be written to the video file stream. - Take the same parameters as `scene.wait`. - """ - - def wrapper(self, scene, duration=DEFAULT_WAIT_TIME, stop_condition=None): - self.revert_to_original_skipping_status() - self.update_skipping_status() - if file_writer_config["skip_animations"]: - logger.debug(f"Skipping wait {self.num_plays}") - func(self, scene, duration, stop_condition) - # If the animation is skipped, we mark its hash as None. - # When sceneFileWriter will start combining partial movie files, it won't take into account None hashes. - self.animations_hashes.append(None) - self.file_writer.add_partial_movie_file(None) - return - if not file_writer_config["disable_caching"]: - hash_wait = get_hash_from_wait_call( - self, self.camera, duration, stop_condition, scene.get_mobjects() - ) - if self.file_writer.is_already_cached(hash_wait): - logger.info( - f"Wait {self.num_plays} : Using cached data (hash : {hash_wait})" - ) - file_writer_config["skip_animations"] = True - else: - hash_wait = "uncached_{:05}".format(self.num_plays) - self.animations_hashes.append(hash_wait) - self.file_writer.add_partial_movie_file(hash_wait) - logger.debug( - "List of the first few animation hashes of the scene: %(h)s", - {"h": str(self.animations_hashes[:5])}, - ) - func(self, scene, duration, stop_condition) - - return wrapper diff --git a/manim/utils/file_ops.py b/manim/utils/file_ops.py index 856c59941d..2296a6bf0f 100644 --- a/manim/utils/file_ops.py +++ b/manim/utils/file_ops.py @@ -11,16 +11,14 @@ import os import platform -import numpy as np import time -import re import subprocess as sp +from pathlib import Path def add_extension_if_not_present(file_name, extension): - # This could conceivably be smarter about handling existing differing extensions - if file_name[-len(extension) :] != extension: - return file_name + extension + if file_name.suffix != extension: + return file_name.with_suffix(extension) else: return file_name @@ -34,13 +32,15 @@ def guarantee_existence(path): def seek_full_path_from_defaults(file_name, default_dir, extensions): possible_paths = [file_name] possible_paths += [ - os.path.join(default_dir, file_name + extension) - for extension in ["", *extensions] + Path(default_dir) / f"{file_name}{extension}" for extension in ["", *extensions] ] for path in possible_paths: if os.path.exists(path): return path - raise IOError("File {} not Found".format(file_name)) + error = "From: {}, could not find {} at either of these locations: {}".format( + os.getcwd(), file_name, possible_paths + ) + raise IOError(error) def modify_atime(file_path): diff --git a/manim/utils/hashing.py b/manim/utils/hashing.py index 05f1f97738..b7eb231dff 100644 --- a/manim/utils/hashing.py +++ b/manim/utils/hashing.py @@ -177,7 +177,7 @@ def encode(self, obj): def get_json(obj): """Recursively serialize `object` to JSON using the :class:`CustomEncoder` class. - Paramaters + Parameters ---------- dict_config : :class:`dict` The dict to flatten diff --git a/manim/utils/images.py b/manim/utils/images.py index ad9a5dc633..3a36b442a4 100644 --- a/manim/utils/images.py +++ b/manim/utils/images.py @@ -4,8 +4,8 @@ import numpy as np -import os +from .. import config from PIL import Image from ..utils.file_ops import seek_full_path_from_defaults @@ -14,8 +14,8 @@ def get_full_raster_image_path(image_file_name): return seek_full_path_from_defaults( image_file_name, - default_dir=os.path.join("assets", "raster_images"), - extensions=[".jpg", ".png", ".gif"], + default_dir=config.get_dir("assets_dir"), + extensions=[".jpg", ".png", ".gif", ".ico"], ) diff --git a/manim/utils/module_ops.py b/manim/utils/module_ops.py index 4d901eb915..99faea529c 100644 --- a/manim/utils/module_ops.py +++ b/manim/utils/module_ops.py @@ -1,16 +1,15 @@ -from .. import constants -from ..config import file_writer_config -from ..config.logger import logger, console +from .. import constants, logger, console, config import importlib.util import inspect import os +from pathlib import Path import sys import types import re def get_module(file_name): - if file_name == "-": + if str(file_name) == "-": module = types.ModuleType("input_scenes") logger.info( "Enter the animation's code & end with an EOF (CTRL+D on Linux/Unix, CTRL+Z on Windows):" @@ -29,13 +28,15 @@ def get_module(file_name): logger.error(f"Failed to render scene: {str(e)}") sys.exit(2) else: - if os.path.exists(file_name): - if file_name[-3:] != ".py": + if Path(file_name).exists(): + ext = file_name.suffix + if ext != ".py": raise ValueError(f"{file_name} is not a valid Manim python script.") - module_name = file_name[:-3].replace(os.sep, ".").split(".")[-1] + module_name = ext.replace(os.sep, ".").split(".")[-1] spec = importlib.util.spec_from_file_location(module_name, file_name) module = importlib.util.module_from_spec(spec) sys.modules[module_name] = module + sys.path.insert(0, str(file_name.parent.absolute())) spec.loader.exec_module(module) return module else: @@ -63,10 +64,10 @@ def get_scenes_to_render(scene_classes): if not scene_classes: logger.error(constants.NO_SCENE_MESSAGE) return [] - if file_writer_config["write_all"]: + if config["write_all"]: return scene_classes result = [] - for scene_name in file_writer_config["scene_names"]: + for scene_name in config["scene_names"]: found = False for scene_class in scene_classes: if scene_class.__name__ == scene_name: diff --git a/manim/utils/sounds.py b/manim/utils/sounds.py index 84d587b57b..f63c32a263 100644 --- a/manim/utils/sounds.py +++ b/manim/utils/sounds.py @@ -5,7 +5,7 @@ ] -import os +from pathlib import Path from ..utils.file_ops import seek_full_path_from_defaults @@ -13,6 +13,6 @@ def get_full_sound_file_path(sound_file_name): return seek_full_path_from_defaults( sound_file_name, - default_dir=os.path.join("assets", "sounds"), + default_dir=Path("assets") / "sounds", extensions=[".wav", ".mp3"], ) diff --git a/manim/utils/space_ops.py b/manim/utils/space_ops.py index 97b6017057..5962ebf9c6 100644 --- a/manim/utils/space_ops.py +++ b/manim/utils/space_ops.py @@ -252,3 +252,31 @@ def get_winding_number(points): d_angle = ((d_angle + PI) % TAU) - PI total_angle += d_angle return total_angle / TAU + + +def shoelace(x_y): + """2D implementation of the shoelace formula. + + Returns + ------- + :class:`float` + Returns signed area. + """ + x = x_y[:, 0] + y = x_y[:, 1] + area = 0.5 * np.array(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1))) + return area + + +def shoelace_direction(x_y): + """ + Uses the area determined by the shoelace method to determine whether + the input set of points is directed clockwise or counterclockwise. + + Returns + ------- + :class:`str` + Either ``"CW"`` or ``"CCW"``. + """ + area = shoelace(x_y) + return "CW" if area > 0 else "CCW" diff --git a/manim/utils/tex.py b/manim/utils/tex.py index 840c555210..6f4218143c 100644 --- a/manim/utils/tex.py +++ b/manim/utils/tex.py @@ -6,7 +6,7 @@ ] -import os +import re class TexTemplate: @@ -130,6 +130,7 @@ def _rebuild(self): def add_to_preamble(self, txt, prepend=False): """Adds stuff to the TeX template's preamble (e.g. definitions, packages). Text can be inserted at the beginning or at the end of the preamble. + Parameters ---------- txt : :class:`string` @@ -169,6 +170,44 @@ def get_texcode_for_expression(self, expression): """ return self.body.replace(self.placeholder_text, expression) + def _texcode_for_environment(self, environment): + """Processes the tex_environment string to return the correct ``\\begin{environment}[extra]{extra}`` and + ``\\end{environment}`` strings + + Parameters + ---------- + environment : :class:`str` + The tex_environment as a string. Acceptable formats include: + ``{align*}``, ``align*``, ``{tabular}[t]{cccl}``, ``tabular}{cccl``, ``\\begin{tabular}[t]{cccl}``. + + Returns + ------- + Tuple[:class:`str`, :class:`str`] + A pair of strings representing the opening and closing of the tex environment, e.g. + ``\\begin{tabular}{cccl}`` and ``\\end{tabular}`` + """ + + # If the environment starts with \begin, remove it + if environment[0:6] == r"\begin": + environment = environment[6:] + + # If environment begins with { strip it + if environment[0] == r"{": + environment = environment[1:] + + # The \begin command takes everything and closes with a brace + begin = r"\begin{" + environment + if ( + begin[-1] != r"}" and begin[-1] != r"]" + ): # If it doesn't end on } or ], assume missing } + begin += r"}" + + # While the \end command terminates at the first closing brace + split_at_brace = re.split(r"}", environment, 1) + end = r"\end{" + split_at_brace[0] + r"}" + + return begin, end + def get_texcode_for_expression_in_env(self, expression, environment): """Inserts expression into TeX template wrapped in \begin{environemnt} and \end{environment} @@ -184,8 +223,7 @@ def get_texcode_for_expression_in_env(self, expression, environment): :class:`str` LaTeX code based on template, containing the given expression inside its environment, ready for typesetting """ - begin = r"\begin{" + environment + "}" - end = r"\end{" + environment + "}" + begin, end = self._texcode_for_environment(environment) return self.body.replace( self.placeholder_text, "{0}\n{1}\n{2}".format(begin, expression, end) ) @@ -236,7 +274,7 @@ def _rebuild(self): with open(self.template_file, "r") as infile: self.body = infile.read() - def file_not_mutable(): + def file_not_mutable(self): raise Exception("Cannot modify TexTemplate when using a template file.") def add_to_preamble(self, txt, prepend=False): diff --git a/manim/utils/tex_file_writing.py b/manim/utils/tex_file_writing.py index c47e65ab0c..de98d70e37 100644 --- a/manim/utils/tex_file_writing.py +++ b/manim/utils/tex_file_writing.py @@ -10,7 +10,7 @@ import hashlib from pathlib import Path -from .. import file_writer_config, config, logger +from .. import config, logger def tex_hash(expression): @@ -72,7 +72,7 @@ def generate_tex_file(expression, environment=None, tex_template=None): else: output = tex_template.get_texcode_for_expression(expression) - tex_dir = file_writer_config["tex_dir"] + tex_dir = config.get_dir("tex_dir") if not os.path.exists(tex_dir): os.makedirs(tex_dir) @@ -156,7 +156,7 @@ def compile_tex(tex_file, tex_compiler, output_format): result = tex_file.replace(".tex", output_format) result = Path(result).as_posix() tex_file = Path(tex_file).as_posix() - tex_dir = Path(file_writer_config["tex_dir"]).as_posix() + tex_dir = Path(config.get_dir("tex_dir")).as_posix() if not os.path.exists(result): command = tex_compilation_command( tex_compiler, output_format, tex_file, tex_dir diff --git a/manim/utils/tex_templates.py b/manim/utils/tex_templates.py index a824ebff1f..14071db06e 100644 --- a/manim/utils/tex_templates.py +++ b/manim/utils/tex_templates.py @@ -549,7 +549,7 @@ class TexTemplateLibrary(object): """ ) gnufsfs.tex_compiler = "xelatex" -gnufsfs.output_format = ".pdf" +gnufsfs.output_format = ".xdv" # GFS NeoHellenic gfsneohellenic = _new_ams_template() diff --git a/poetry.lock b/poetry.lock index bfa0fcc0f6..e0b33d9ed4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -38,21 +38,21 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "20.2.0" +version = "20.3.0" description = "Classes Without Boilerplate" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] -docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] [[package]] name = "babel" -version = "2.8.0" +version = "2.9.0" description = "Internationalization utilities" category = "dev" optional = false @@ -86,11 +86,11 @@ d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] [[package]] name = "cairocffi" -version = "1.1.0" +version = "1.2.0" description = "cffi-based cairo bindings for Python" category = "main" optional = false -python-versions = ">= 3.5" +python-versions = ">=3.6" [package.dependencies] cffi = ">=1.1.0" @@ -102,7 +102,7 @@ xcb = ["xcffib (>=0.3.2)"] [[package]] name = "certifi" -version = "2020.6.20" +version = "2020.11.8" description = "Python package for providing Mozilla's CA Bundle." category = "dev" optional = false @@ -137,7 +137,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "colorama" -version = "0.4.3" +version = "0.4.4" description = "Cross-platform colored terminal text." category = "main" optional = false @@ -165,6 +165,17 @@ python-versions = "*" [package.extras] test = ["flake8 (3.7.8)", "hypothesis (3.55.3)"] +[[package]] +name = "cycler" +version = "0.10.0" +description = "Composable style cycles" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +six = "*" + [[package]] name = "dataclasses" version = "0.7" @@ -183,28 +194,28 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "grpcio" -version = "1.32.0" +version = "1.33.2" description = "HTTP/2-based RPC framework" category = "main" -optional = false +optional = true python-versions = "*" [package.dependencies] six = ">=1.5.2" [package.extras] -protobuf = ["grpcio-tools (>=1.32.0)"] +protobuf = ["grpcio-tools (>=1.33.2)"] [[package]] name = "grpcio-tools" -version = "1.32.0" +version = "1.33.2" description = "Protobuf code generator for gRPC" category = "main" -optional = false +optional = true python-versions = "*" [package.dependencies] -grpcio = ">=1.32.0" +grpcio = ">=1.33.2" protobuf = ">=3.5.0.post1,<4.0dev" [[package]] @@ -251,7 +262,7 @@ testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] [[package]] name = "iniconfig" -version = "1.0.1" +version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" category = "dev" optional = false @@ -259,7 +270,7 @@ python-versions = "*" [[package]] name = "isort" -version = "5.5.4" +version = "5.6.4" description = "A Python utility / library to sort Python imports." category = "dev" optional = false @@ -284,6 +295,14 @@ MarkupSafe = ">=0.23" [package.extras] i18n = ["Babel (>=0.8)"] +[[package]] +name = "kiwisolver" +version = "1.3.1" +description = "A fast implementation of the Cassowary constraint solver" +category = "dev" +optional = false +python-versions = ">=3.6" + [[package]] name = "lazy-object-proxy" version = "1.4.3" @@ -300,6 +319,22 @@ category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +[[package]] +name = "matplotlib" +version = "3.3.3" +description = "Python plotting package" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cycler = ">=0.10" +kiwisolver = ">=1.0.1" +numpy = ">=1.15" +pillow = ">=6.2.0" +pyparsing = ">=2.0.3,<2.0.4 || >2.0.4,<2.1.2 || >2.1.2,<2.1.6 || >2.1.6" +python-dateutil = ">=2.1" + [[package]] name = "mccabe" version = "0.6.1" @@ -318,7 +353,7 @@ python-versions = "*" [[package]] name = "numpy" -version = "1.19.2" +version = "1.19.4" description = "NumPy is the fundamental package for array computing with Python." category = "main" optional = false @@ -338,31 +373,31 @@ six = "*" [[package]] name = "pangocairocffi" -version = "0.3.2" +version = "0.4.0" description = "CFFI-based pango-cairo bindings for Python" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] cairocffi = ">=1.0.2" cffi = ">=1.1.0" -pangocffi = ">=0.4.0" +pangocffi = ">=0.8.0" [[package]] name = "pangocffi" -version = "0.6.0" +version = "0.8.0" description = "CFFI-based pango bindings for Python" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] cffi = ">=1.1.0" [[package]] name = "pathspec" -version = "0.8.0" +version = "0.8.1" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false @@ -373,16 +408,16 @@ name = "pathtools" version = "0.1.2" description = "File system general utilities" category = "main" -optional = false +optional = true python-versions = "*" [[package]] name = "pillow" -version = "7.2.0" +version = "8.0.1" description = "Python Imaging Library (Fork)" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [[package]] name = "pluggy" @@ -408,10 +443,10 @@ python-versions = "*" [[package]] name = "protobuf" -version = "3.13.0" +version = "3.14.0" description = "Protocol Buffers" category = "main" -optional = false +optional = true python-versions = "*" [package.dependencies] @@ -451,7 +486,7 @@ python-versions = "*" [[package]] name = "pygments" -version = "2.7.1" +version = "2.7.2" description = "Pygments is a syntax highlighting package written in Python." category = "main" optional = false @@ -482,7 +517,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "pytest" -version = "6.1.1" +version = "6.1.2" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -503,9 +538,20 @@ toml = "*" checkqa_mypy = ["mypy (0.780)"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +[[package]] +name = "python-dateutil" +version = "2.8.1" +description = "Extensions to the standard Python datetime module" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + [[package]] name = "pytz" -version = "2020.1" +version = "2020.4" description = "World timezone definitions, modern and historical" category = "dev" optional = false @@ -526,7 +572,7 @@ sphinx = ">=1.3.1" [[package]] name = "regex" -version = "2020.9.27" +version = "2020.11.13" description = "Alternative regular expression module, to replace re." category = "dev" optional = false @@ -534,7 +580,7 @@ python-versions = "*" [[package]] name = "requests" -version = "2.24.0" +version = "2.25.0" description = "Python HTTP for Humans." category = "dev" optional = false @@ -544,7 +590,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" certifi = ">=2017.4.17" chardet = ">=3.0.2,<4" idna = ">=2.5,<3" -urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" +urllib3 = ">=1.21.1,<1.27" [package.extras] security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] @@ -552,7 +598,7 @@ socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] [[package]] name = "rich" -version = "8.0.0" +version = "6.2.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" category = "main" optional = false @@ -570,7 +616,7 @@ jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] [[package]] name = "scipy" -version = "1.5.2" +version = "1.5.4" description = "SciPy: Scientific Library for Python" category = "main" optional = false @@ -597,7 +643,7 @@ python-versions = "*" [[package]] name = "sphinx" -version = "3.2.1" +version = "3.3.1" description = "Python documentation generator" category = "dev" optional = false @@ -623,7 +669,7 @@ sphinxcontrib-serializinghtml = "*" [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.780)", "docutils-stubs"] +lint = ["flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.790)", "docutils-stubs"] test = ["pytest", "pytest-cov", "html5lib", "typed-ast", "cython"] [[package]] @@ -699,15 +745,15 @@ test = ["pytest"] [[package]] name = "toml" -version = "0.10.1" +version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" category = "dev" optional = false -python-versions = "*" +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tqdm" -version = "4.50.1" +version = "4.51.0" description = "Fast, Extensible Progress Meter" category = "main" optional = false @@ -734,7 +780,7 @@ python-versions = "*" [[package]] name = "urllib3" -version = "1.25.10" +version = "1.26.2" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false @@ -742,7 +788,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] brotli = ["brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] [[package]] @@ -750,7 +796,7 @@ name = "watchdog" version = "0.10.3" description = "Filesystem events monitoring" category = "main" -optional = false +optional = true python-versions = "*" [package.dependencies] @@ -769,7 +815,7 @@ python-versions = "*" [[package]] name = "zipp" -version = "3.3.0" +version = "3.4.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false @@ -779,10 +825,13 @@ python-versions = ">=3.6" docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +[extras] +js_renderer = ["grpcio", "grpcio-tools", "watchdog"] + [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "19fad3a14d174f8365b3941c9d1fc44037855d5eaabff297313d65bbea223c38" +content-hash = "f66610f54761f5f832bba54c4b8fb33ca584624493965927935763b24df6e08f" [metadata.files] alabaster = [ @@ -802,22 +851,22 @@ atomicwrites = [ {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ - {file = "attrs-20.2.0-py2.py3-none-any.whl", hash = "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"}, - {file = "attrs-20.2.0.tar.gz", hash = "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594"}, + {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, + {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, ] babel = [ - {file = "Babel-2.8.0-py2.py3-none-any.whl", hash = "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"}, - {file = "Babel-2.8.0.tar.gz", hash = "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38"}, + {file = "Babel-2.9.0-py2.py3-none-any.whl", hash = "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5"}, + {file = "Babel-2.9.0.tar.gz", hash = "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05"}, ] black = [ {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, ] cairocffi = [ - {file = "cairocffi-1.1.0.tar.gz", hash = "sha256:f1c0c5878f74ac9ccb5d48b2601fcc75390c881ce476e79f4cfedd288b1b05db"}, + {file = "cairocffi-1.2.0.tar.gz", hash = "sha256:9a979b500c64c8179fec286f337e8fe644eca2f2cd05860ce0b62d25f22ea140"}, ] certifi = [ - {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, - {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, + {file = "certifi-2020.11.8-py2.py3-none-any.whl", hash = "sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd"}, + {file = "certifi-2020.11.8.tar.gz", hash = "sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4"}, ] cffi = [ {file = "cffi-1.14.3-2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc"}, @@ -866,8 +915,8 @@ click = [ {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, ] colorama = [ - {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, - {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] colour = [ {file = "colour-0.1.5-py2.py3-none-any.whl", hash = "sha256:33f6db9d564fadc16e59921a56999b79571160ce09916303d35346dddc17978c"}, @@ -877,6 +926,10 @@ commonmark = [ {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, ] +cycler = [ + {file = "cycler-0.10.0-py2.py3-none-any.whl", hash = "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d"}, + {file = "cycler-0.10.0.tar.gz", hash = "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8"}, +] dataclasses = [ {file = "dataclasses-0.7-py3-none-any.whl", hash = "sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836"}, {file = "dataclasses-0.7.tar.gz", hash = "sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6"}, @@ -886,86 +939,100 @@ docutils = [ {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, ] grpcio = [ - {file = "grpcio-1.32.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3afb058b6929eba07dba9ae6c5b555aa1d88cb140187d78cc510bd72d0329f28"}, - {file = "grpcio-1.32.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a8004b34f600a8a51785e46859cd88f3386ef67cccd1cfc7598e3d317608c643"}, - {file = "grpcio-1.32.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:e6786f6f7be0937614577edcab886ddce91b7c1ea972a07ef9972e9f9ecbbb78"}, - {file = "grpcio-1.32.0-cp27-cp27m-win32.whl", hash = "sha256:e467af6bb8f5843f5a441e124b43474715cfb3981264e7cd227343e826dcc3ce"}, - {file = "grpcio-1.32.0-cp27-cp27m-win_amd64.whl", hash = "sha256:1376a60f9bfce781b39973f100b5f67e657b5be479f2fd8a7d2a408fc61c085c"}, - {file = "grpcio-1.32.0-cp27-cp27mu-linux_armv7l.whl", hash = "sha256:ce617e1c4a39131f8527964ac9e700eb199484937d7a0b3e52655a3ba50d5fb9"}, - {file = "grpcio-1.32.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:99bac0e2c820bf446662365df65841f0c2a55b0e2c419db86eaf5d162ddae73e"}, - {file = "grpcio-1.32.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6d869a3e8e62562b48214de95e9231c97c53caa7172802236cd5d60140d7cddd"}, - {file = "grpcio-1.32.0-cp35-cp35m-linux_armv7l.whl", hash = "sha256:182c64ade34c341398bf71ec0975613970feb175090760ab4f51d1e9a5424f05"}, - {file = "grpcio-1.32.0-cp35-cp35m-macosx_10_7_intel.whl", hash = "sha256:9c0d8f2346c842088b8cbe3e14985b36e5191a34bf79279ba321a4bf69bd88b7"}, - {file = "grpcio-1.32.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:4775bc35af9cd3b5033700388deac2e1d611fa45f4a8dcb93667d94cb25f0444"}, - {file = "grpcio-1.32.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:be98e3198ec765d0a1e27f69d760f69374ded8a33b953dcfe790127731f7e690"}, - {file = "grpcio-1.32.0-cp35-cp35m-manylinux2014_i686.whl", hash = "sha256:378fe80ec5d9353548eb2a8a43ea03747a80f2e387c4f177f2b3ff6c7d898753"}, - {file = "grpcio-1.32.0-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:f7d508691301027033215d3662dab7e178f54d5cca2329f26a71ae175d94b83f"}, - {file = "grpcio-1.32.0-cp35-cp35m-win32.whl", hash = "sha256:25959a651420dd4a6fd7d3e8dee53f4f5fd8c56336a64963428e78b276389a59"}, - {file = "grpcio-1.32.0-cp35-cp35m-win_amd64.whl", hash = "sha256:ac7028d363d2395f3d755166d0161556a3f99500a5b44890421ccfaaf2aaeb08"}, - {file = "grpcio-1.32.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:c31e8a219650ddae1cd02f5a169e1bffe66a429a8255d3ab29e9363c73003b62"}, - {file = "grpcio-1.32.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e28e4c0d4231beda5dee94808e3a224d85cbaba3cfad05f2192e6f4ec5318053"}, - {file = "grpcio-1.32.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f03dfefa9075dd1c6c5cc27b1285c521434643b09338d8b29e1d6a27b386aa82"}, - {file = "grpcio-1.32.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:c4966d746dccb639ef93f13560acbe9630681c07f2b320b7ec03fe2c8f0a1f15"}, - {file = "grpcio-1.32.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:ec10d5f680b8e95a06f1367d73c5ddcc0ed04a3f38d6e4c9346988fb0cea2ffa"}, - {file = "grpcio-1.32.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:28677f057e2ef11501860a7bc15de12091d40b95dd0fddab3c37ff1542e6b216"}, - {file = "grpcio-1.32.0-cp36-cp36m-win32.whl", hash = "sha256:0f3f09269ffd3fded430cd89ba2397eabbf7e47be93983b25c187cdfebb302a7"}, - {file = "grpcio-1.32.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4396b1d0f388ae875eaf6dc05cdcb612c950fd9355bc34d38b90aaa0665a0d4b"}, - {file = "grpcio-1.32.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1ada89326a364a299527c7962e5c362dbae58c67b283fe8383c4d952b26565d5"}, - {file = "grpcio-1.32.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:1d384a61f96a1fc6d5d3e0b62b0a859abc8d4c3f6d16daba51ebf253a3e7df5d"}, - {file = "grpcio-1.32.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e811ce5c387256609d56559d944a974cc6934a8eea8c76e7c86ec388dc06192d"}, - {file = "grpcio-1.32.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:07b430fa68e5eecd78e2ad529ab80f6a234b55fc1b675fe47335ccbf64c6c6c8"}, - {file = "grpcio-1.32.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:0e3edd8cdb71809d2455b9dbff66b4dd3d36c321e64bfa047da5afdfb0db332b"}, - {file = "grpcio-1.32.0-cp37-cp37m-win32.whl", hash = "sha256:6f7947dad606c509d067e5b91a92b250aa0530162ab99e4737090f6b17eb12c4"}, - {file = "grpcio-1.32.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7cda998b7b551503beefc38db9be18c878cfb1596e1418647687575cdefa9273"}, - {file = "grpcio-1.32.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c58825a3d8634cd634d8f869afddd4d5742bdb59d594aea4cea17b8f39269a55"}, - {file = "grpcio-1.32.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:ef9bd7fdfc0a063b4ed0efcab7906df5cae9bbcf79d05c583daa2eba56752b00"}, - {file = "grpcio-1.32.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1ce6f5ff4f4a548c502d5237a071fa617115df58ea4b7bd41dac77c1ab126e9c"}, - {file = "grpcio-1.32.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:f12900be4c3fd2145ba94ab0d80b7c3d71c9e6414cfee2f31b1c20188b5c281f"}, - {file = "grpcio-1.32.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:f53f2dfc8ff9a58a993e414a016c8b21af333955ae83960454ad91798d467c7b"}, - {file = "grpcio-1.32.0-cp38-cp38-win32.whl", hash = "sha256:5bddf9d53c8df70061916c3bfd2f468ccf26c348bb0fb6211531d895ed5e4c72"}, - {file = "grpcio-1.32.0-cp38-cp38-win_amd64.whl", hash = "sha256:14c0f017bfebbc18139551111ac58ecbde11f4bc375b73a53af38927d60308b6"}, - {file = "grpcio-1.32.0.tar.gz", hash = "sha256:01d3046fe980be25796d368f8fc5ff34b7cf5e1444f3789a017a7fe794465639"}, + {file = "grpcio-1.33.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c5030be8a60fb18de1fc8d93d130d57e4296c02f229200df814f6578da00429e"}, + {file = "grpcio-1.33.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:5b21d3de520a699cb631cfd3a773a57debeb36b131be366bf832153405cc5404"}, + {file = "grpcio-1.33.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:b412f43c99ca72769306293ba83811b241d41b62ca8f358e47e0fdaf7b6fbbd7"}, + {file = "grpcio-1.33.2-cp27-cp27m-win32.whl", hash = "sha256:703da25278ee7318acb766be1c6d3b67d392920d002b2d0304e7f3431b74f6c1"}, + {file = "grpcio-1.33.2-cp27-cp27m-win_amd64.whl", hash = "sha256:2f2eabfd514af8945ee415083a0f849eea6cb3af444999453bb6666fadc10f54"}, + {file = "grpcio-1.33.2-cp27-cp27mu-linux_armv7l.whl", hash = "sha256:d51ddfb3d481a6a3439db09d4b08447fb9f6b60d862ab301238f37bea8f60a6d"}, + {file = "grpcio-1.33.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:407b4d869ce5c6a20af5b96bb885e3ecaf383e3fb008375919eb26cf8f10d9cd"}, + {file = "grpcio-1.33.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:abaf30d18874310d4439a23a0afb6e4b5709c4266966401de7c4ae345cc810ee"}, + {file = "grpcio-1.33.2-cp35-cp35m-linux_armv7l.whl", hash = "sha256:f2673c51e8535401c68806d331faba614bcff3ee16373481158a2e74f510b7f6"}, + {file = "grpcio-1.33.2-cp35-cp35m-macosx_10_7_intel.whl", hash = "sha256:65b06fa2db2edd1b779f9b256e270f7a58d60e40121660d8b5fd6e8b88f122ed"}, + {file = "grpcio-1.33.2-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:514b4a6790d6597fc95608f49f2f13fe38329b2058538095f0502b734b98ffd2"}, + {file = "grpcio-1.33.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:4cef3eb2df338abd9b6164427ede961d351c6bf39b4a01448a65f9e795f56575"}, + {file = "grpcio-1.33.2-cp35-cp35m-manylinux2014_i686.whl", hash = "sha256:3ac453387add933b6cfbc67cc8635f91ff9895299130fc612c3c4b904e91d82a"}, + {file = "grpcio-1.33.2-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:7d292dabf7ded9c062357f8207e20e94095a397d487ffd25aa213a2c3dff0ab4"}, + {file = "grpcio-1.33.2-cp35-cp35m-win32.whl", hash = "sha256:0aeed3558a0eec0b31700af6072f1c90e8fd5701427849e76bc469554a14b4f5"}, + {file = "grpcio-1.33.2-cp35-cp35m-win_amd64.whl", hash = "sha256:88f2a102cbc67e91f42b4323cec13348bf6255b25f80426088079872bd4f3c5c"}, + {file = "grpcio-1.33.2-cp36-cp36m-linux_armv7l.whl", hash = "sha256:affbb739fde390710190e3540acc9f3e65df25bd192cc0aa554f368288ee0ea2"}, + {file = "grpcio-1.33.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ffec0b854d2ed6ee98776c7168c778cdd18503642a68d36c00ba0f96d4ccff7c"}, + {file = "grpcio-1.33.2-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:7744468ee48be3265db798f27e66e118c324d7831a34fd39d5775bcd5a70a2c4"}, + {file = "grpcio-1.33.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:6a1b5b7e47600edcaeaa42983b1c19e7a5892c6b98bcde32ae2aa509a99e0436"}, + {file = "grpcio-1.33.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:289671cfe441069f617bf23c41b1fa07053a31ff64de918d1016ac73adda2f73"}, + {file = "grpcio-1.33.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:a8c84db387907e8d800c383e4c92f39996343adedf635ae5206a684f94df8311"}, + {file = "grpcio-1.33.2-cp36-cp36m-win32.whl", hash = "sha256:4bb771c4c2411196b778871b519c7e12e87f3fa72b0517b22f952c64ead07958"}, + {file = "grpcio-1.33.2-cp36-cp36m-win_amd64.whl", hash = "sha256:b581ddb8df619402c377c81f186ad7f5e2726ad9f8d57047144b352f83f37522"}, + {file = "grpcio-1.33.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:02a4a637a774382d6ac8e65c0a7af4f7f4b9704c980a0a9f4f7bbc1e97c5b733"}, + {file = "grpcio-1.33.2-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:592656b10528aa327058d2007f7ab175dc9eb3754b289e24cac36e09129a2f6b"}, + {file = "grpcio-1.33.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:c89510381cbf8c8317e14e747a8b53988ad226f0ed240824064a9297b65f921d"}, + {file = "grpcio-1.33.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:7fda62846ef8d86caf06bd1ecfddcae2c7e59479a4ee28808120e170064d36cc"}, + {file = "grpcio-1.33.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:d386630af995fd4de225d550b6806507ca09f5a650f227fddb29299335cda55e"}, + {file = "grpcio-1.33.2-cp37-cp37m-win32.whl", hash = "sha256:bf7de9e847d2d14a0efcd48b290ee181fdbffb2ae54dfa2ec2a935a093730bac"}, + {file = "grpcio-1.33.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7c1ea6ea6daa82031af6eb5b7d1ab56b1193840389ea7cf46d80e98636f8aff5"}, + {file = "grpcio-1.33.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:85e56ab125b35b1373205b3802f58119e70ffedfe0d7e2821999126058f7c44f"}, + {file = "grpcio-1.33.2-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:0cebba3907441d5c620f7b491a780ed155140fbd590da0886ecfb1df6ad947b9"}, + {file = "grpcio-1.33.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:52143467237bfa77331ed1979dc3e203a1c12511ee37b3ddd9ff41b05804fb10"}, + {file = "grpcio-1.33.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:8cf67b8493bff50fa12b4bc30ab40ce1f1f216eb54145962b525852959b0ab3d"}, + {file = "grpcio-1.33.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:fa78bd55ec652d4a88ba254c8dae623c9992e2ce647bd17ba1a37ca2b7b42222"}, + {file = "grpcio-1.33.2-cp38-cp38-win32.whl", hash = "sha256:143b4fe72c01000fc0667bf62ace402a6518939b3511b3c2bec04d44b1d7591c"}, + {file = "grpcio-1.33.2-cp38-cp38-win_amd64.whl", hash = "sha256:08b6a58c8a83e71af5650f8f879fe14b7b84dce0c4969f3817b42c72989dacf0"}, + {file = "grpcio-1.33.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:56e2a985efdba8e2282e856470b684e83a3cadd920f04fcd360b4b826ced0dd3"}, + {file = "grpcio-1.33.2-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:62ce7e86f11e8c4ff772e63c282fb5a7904274258be0034adf37aa679cf96ba0"}, + {file = "grpcio-1.33.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:7f727b8b6d9f92fcab19dbc62ec956d8352c6767b97b8ab18754b2dfa84d784f"}, + {file = "grpcio-1.33.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:2d5124284f9d29e4f06f674a12ebeb23fc16ce0f96f78a80a6036930642ae5ab"}, + {file = "grpcio-1.33.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:eff55d318a114742ed2a06972f5daacfe3d5ad0c0c0d9146bcaf10acb427e6be"}, + {file = "grpcio-1.33.2-cp39-cp39-win32.whl", hash = "sha256:dd47fac2878f6102efa211461eb6fa0a6dd7b4899cd1ade6cdcb9fa9748363eb"}, + {file = "grpcio-1.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:89add4f4cda9546f61cb8a6988bc5b22101dd8ca4af610dff6f28105d1f78695"}, + {file = "grpcio-1.33.2.tar.gz", hash = "sha256:21265511880056d19ce4f809ce3fbe2a3fa98ec1fc7167dbdf30a80d3276202e"}, ] grpcio-tools = [ - {file = "grpcio-tools-1.32.0.tar.gz", hash = "sha256:28547272c51e1d2d343685b9f531e85bb90ad7bd93e726ba646b5627173cbc47"}, - {file = "grpcio_tools-1.32.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6165dc7d424c3c58a54e9e47eacc7cc1513cd09c7c71ff5323e74ead5bb863f"}, - {file = "grpcio_tools-1.32.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a7432b84d6f2f6260d5461eb2a8904db8cf24b663e0a1236375098c8e15c289c"}, - {file = "grpcio_tools-1.32.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:4f61edfb0c07689a2835f15f4a25a781f058866cb4fea0bea391ae6deb74325f"}, - {file = "grpcio_tools-1.32.0-cp27-cp27m-win32.whl", hash = "sha256:a3524be59d4e6f8b089f7eaa128bc83e2375aac973f1bf0b568cd1c04c4df56e"}, - {file = "grpcio_tools-1.32.0-cp27-cp27m-win_amd64.whl", hash = "sha256:b31e7e909ba9efd8a08eb45665bf2f8326726da288d9e33555473e6b20596dbd"}, - {file = "grpcio_tools-1.32.0-cp27-cp27mu-linux_armv7l.whl", hash = "sha256:4e04d6a7c48adbdca64e9b67cc75e8294b3b37b1284dd2819183e38a4207aa39"}, - {file = "grpcio_tools-1.32.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:37acc75ec1dc836772496ef77170fab585e2517abdf1330c29e682eb50a6ce86"}, - {file = "grpcio_tools-1.32.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:7d5be0d06bf830efbf1867db7b01720e54a136454410270e896441ec56baba00"}, - {file = "grpcio_tools-1.32.0-cp35-cp35m-linux_armv7l.whl", hash = "sha256:6e26e8d0ef73c04dc1118513c06ff56bce36672c8e28410ae4f938c22002ba00"}, - {file = "grpcio_tools-1.32.0-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:3971dee0cf57dc3813f6f40724161341ec3b31137b026ae8d4db30c83afeb2a1"}, - {file = "grpcio_tools-1.32.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:83414dd919b692d92876db787b6fda709c226243c9bdb71b5025297a127f3be4"}, - {file = "grpcio_tools-1.32.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:9b5beb49002bb1f1c0641b55ddc2d1d92c7844fb42348e874146bf7667b6ca20"}, - {file = "grpcio_tools-1.32.0-cp35-cp35m-manylinux2014_i686.whl", hash = "sha256:7a18d6375efe075cc274fdfe004bee4530319a2dbb044eb7eb157c313fe88c97"}, - {file = "grpcio_tools-1.32.0-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:8147085f0a044ddc27c870feb8e82a25685f3fdf09184dba0f63fed720f12e93"}, - {file = "grpcio_tools-1.32.0-cp35-cp35m-win32.whl", hash = "sha256:e2a37e716ef6b5e81c44648648aae258b67b9ef19e0a472ec4080f5e384be386"}, - {file = "grpcio_tools-1.32.0-cp35-cp35m-win_amd64.whl", hash = "sha256:130c248d0d94473f3eb80d86bdae35a39eb20ab98fde6d227e7f7e053ccbba88"}, - {file = "grpcio_tools-1.32.0-cp36-cp36m-linux_armv7l.whl", hash = "sha256:11228fb5343c197e1f4376a966f6845ea270c794ec925260b8a27f6df5d90d04"}, - {file = "grpcio_tools-1.32.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:246caf8cdea97ff3710a810c55c9400e3aa7af1a5464a667d62184e38a58a031"}, - {file = "grpcio_tools-1.32.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:6aa6dd1d7e746c41803a209565d23e6027b0a5dd9b59596da37f99257cc58e65"}, - {file = "grpcio_tools-1.32.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:541a6b992aa417a6305c965bb6896aa1a1ca37d00a82d5438074b18db6a37aad"}, - {file = "grpcio_tools-1.32.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:e83146ef8f17e3a35fe77a438794f0a4a50ea11085194bfea1b419c1b342f7b1"}, - {file = "grpcio_tools-1.32.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:71c451240e66245125e504abee5acc7ab30da099d5c17596d43ecc66e6034e20"}, - {file = "grpcio_tools-1.32.0-cp36-cp36m-win32.whl", hash = "sha256:6155ed6fed3c9a41fd03156c31adb5012c2399992c929987d3fa8ff1cd3c7cd8"}, - {file = "grpcio_tools-1.32.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a137b6079c96f11f0854a4793910f76aa4a62283947311b6e5131369fa226b48"}, - {file = "grpcio_tools-1.32.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f5f381943081792d82fe34c5a649d98a6b91741c6d62cbca8914943b8d1a4e8b"}, - {file = "grpcio_tools-1.32.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:adeae62f3bd1c6839e3822620f7650d30adb7398170e3a0b45a0059f9fe631c8"}, - {file = "grpcio_tools-1.32.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bcc62cb4a3c9a39fb9e349124018e7d7edf0f627592561410e28b590767b831f"}, - {file = "grpcio_tools-1.32.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:d11f432ed6fde059b33c514b64fcbf4527f56e03ff94f52f95121547c6945825"}, - {file = "grpcio_tools-1.32.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:708077380f458ef831e7da67f574abfb2fc6b6a24225c5976d92809b8930254c"}, - {file = "grpcio_tools-1.32.0-cp37-cp37m-win32.whl", hash = "sha256:de8ca90742bd41a19c1067fba6ffa13befd3ddb505d67eb297d6a418a5937a25"}, - {file = "grpcio_tools-1.32.0-cp37-cp37m-win_amd64.whl", hash = "sha256:632bba5853e955072392aac42fbca16daf65adfc0ec094fa840afbb83c78bee8"}, - {file = "grpcio_tools-1.32.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2da2a4b2209156d0f88f91bd5d4650a9ed830acb6f685881a26d67d3f671361"}, - {file = "grpcio_tools-1.32.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d3d01ebc1526cc9cdc5e29d2196bae43d56d8ec545dd30fead8b8b3e0b126808"}, - {file = "grpcio_tools-1.32.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:fd059d37d9537fa1a89b1139f8cbed7530a5f81c8577560d3f7710fcec95efde"}, - {file = "grpcio_tools-1.32.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:524f0460a49a3248d1cb462d0904e783a75bb3cecdcaea520c3688c8bccd9f2f"}, - {file = "grpcio_tools-1.32.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:6327f2c6acca4eac1d5a8e1ee92282682b83069d53199ff8ce18906e912086ed"}, - {file = "grpcio_tools-1.32.0-cp38-cp38-win32.whl", hash = "sha256:07c1da5f1dbd4db664d416f68db6a92d5c88b4073ec6be41fcc7aa4d632f60a9"}, - {file = "grpcio_tools-1.32.0-cp38-cp38-win_amd64.whl", hash = "sha256:9b92f998ed1d01925160e47e9546c742aa0de49009f8fa3bb79420252d8a888d"}, + {file = "grpcio-tools-1.33.2.tar.gz", hash = "sha256:af40774c0275f5465f49fd92bfcd9831b19b013de4cc77b8fb38aea76fa6dce3"}, + {file = "grpcio_tools-1.33.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:f80943d6ff6fb0125e68a7af7591a5373d177c8e97a9fcfaeb873cb0a11efbff"}, + {file = "grpcio_tools-1.33.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:74762069fb0336535518097cb765250fe2800c19dfb9a62bd3ebf7c1d31f568c"}, + {file = "grpcio_tools-1.33.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:895f6926310e6c1bd4424af2c9b1c777481c246a87c9943478e9d12631fa5331"}, + {file = "grpcio_tools-1.33.2-cp27-cp27m-win32.whl", hash = "sha256:76d51b8625f49a52ffd207b586e459e9f04f2451226c1602e9eb1e67c530d830"}, + {file = "grpcio_tools-1.33.2-cp27-cp27m-win_amd64.whl", hash = "sha256:6b3e10a2b1409e4bbc744d8f966b2291bf042fea612d3c144797d8e845335ee4"}, + {file = "grpcio_tools-1.33.2-cp27-cp27mu-linux_armv7l.whl", hash = "sha256:584888e7b679e329c18ec1cc93b8d43514d7311a83080382bdde36edaa2fc3f8"}, + {file = "grpcio_tools-1.33.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7d7c98448835df0d64c34c205b53a088c631efa2623f180ae08f13d3427c6905"}, + {file = "grpcio_tools-1.33.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d0b1651a66c0254e84207b108cb5f76a0076141dda35d522de1f4a4a33e2db82"}, + {file = "grpcio_tools-1.33.2-cp35-cp35m-linux_armv7l.whl", hash = "sha256:7a4b1d0399259449b9ead9a1e43611da281c6cce6fab2629ad4f9b16887679a9"}, + {file = "grpcio_tools-1.33.2-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:aeae23f77c668b9d4bce43007eeeb395d8e87c72f1281d2329872bc43ea66a83"}, + {file = "grpcio_tools-1.33.2-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:40aae8f6c75864b9063aed405158b6926dc71d1a6b9a3f89c4b4ce5915f7b154"}, + {file = "grpcio_tools-1.33.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:2db16055baed821fd1e12d3154cab26bec6a947ff47eaad7aedc2f86b4dead02"}, + {file = "grpcio_tools-1.33.2-cp35-cp35m-manylinux2014_i686.whl", hash = "sha256:4346774a7edf07d29fa1bc96ec69a53c3bb222e1e07d1046ed19fe3cff1c96bc"}, + {file = "grpcio_tools-1.33.2-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:ba2971017ed3e1d429abeef7e3d98a7aff5191d76e63ebf5b0a700d6b505d342"}, + {file = "grpcio_tools-1.33.2-cp35-cp35m-win32.whl", hash = "sha256:7a9877611bbd9587dbf8cc2d80edb99fce371faaa504960cf27ca4bb43b3eeb3"}, + {file = "grpcio_tools-1.33.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9777378bfb59e5bfe9f972d01c27e502b577f8c5d539967ae17409e562b3b347"}, + {file = "grpcio_tools-1.33.2-cp36-cp36m-linux_armv7l.whl", hash = "sha256:5a90192f5b198a5d3b2d8015f9088f4fcfdf8c1020931afb48f8bb52db02ef92"}, + {file = "grpcio_tools-1.33.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ed34b6316c6a932d8586a3567d341599aff5a61a34f9d6640501f4af5537b95e"}, + {file = "grpcio_tools-1.33.2-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:eb2a452ecf9b8c0beee6a49ea3e42bd5344e07dab9692b0c60ef7caf9dbf0e7e"}, + {file = "grpcio_tools-1.33.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:be6f34a188806dbec3dba9e6c6b41253507f3b7e4beeb344a02903609d708659"}, + {file = "grpcio_tools-1.33.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:c2a3a69871047dccd49507cb989b6f84e24f5427902930d92e10cf1e366e578a"}, + {file = "grpcio_tools-1.33.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:09f25ad6d47566b5ca3eefae7d499d1c431646d20b6543a00b70fe7fe61b1a02"}, + {file = "grpcio_tools-1.33.2-cp36-cp36m-win32.whl", hash = "sha256:36b71fddb8876d619b6f00e49d47b917b716ca5761b9037631f1256851ed74dc"}, + {file = "grpcio_tools-1.33.2-cp36-cp36m-win_amd64.whl", hash = "sha256:2e6f9b93b19ad5d680beeccd937357e8bd8403f1090a6ce4369c9fc162d5c3f1"}, + {file = "grpcio_tools-1.33.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b57fcd12c210916f8addbb983ff2264a675acdd0665be0df87f30efccec50119"}, + {file = "grpcio_tools-1.33.2-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:55f4e125b5d4af409ec5783009794262fb28fc9d01cc1cd0a123c59ea7d9a03b"}, + {file = "grpcio_tools-1.33.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:fb0651c7ba378b18865dac9f10414c0e15ddc190ea6fd0b5c96bfe235276304e"}, + {file = "grpcio_tools-1.33.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:bc33d340c981b5f2ecdc11888e4c9f462fe25811da0bd98080a0f239a5c71cc0"}, + {file = "grpcio_tools-1.33.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:ba1bd7e60872727b0123b80658a723ba39357f3baa85a53d3a4a877fdc68e218"}, + {file = "grpcio_tools-1.33.2-cp37-cp37m-win32.whl", hash = "sha256:278092221bcbf3e9710e64e0f64b3ffd91879da87cc901151f52f50ab2c1e364"}, + {file = "grpcio_tools-1.33.2-cp37-cp37m-win_amd64.whl", hash = "sha256:3a407b9e2ca0a15b4d337120e8a5ad35fedaaf658c40c445d63d41abb8f8f5e3"}, + {file = "grpcio_tools-1.33.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9bfae79bd3c2bd6f49d72243408c269a4cb9f6e2231aae06c5df5106d60ebf5d"}, + {file = "grpcio_tools-1.33.2-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:a35f0663154f5083b4e84661c33bf193797de7d68138a1e94ddd9bec4b7d0d39"}, + {file = "grpcio_tools-1.33.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:5979937daddc1cf624a11ee5e45baa6e7f1c03948f8534529e08c0baef963b2d"}, + {file = "grpcio_tools-1.33.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:6400950ebc37cc74c8b1d8a25ef4c624f3daf08b5f61c6ba6175845908c0e9d7"}, + {file = "grpcio_tools-1.33.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:cb0dbc3203975efaf541887fa641742287c3473298a9a45b9619506b27ce7028"}, + {file = "grpcio_tools-1.33.2-cp38-cp38-win32.whl", hash = "sha256:686462d1abbd4d9c13f96a8b3442e62ce4ed412272996012cc561a6dcc7e65b2"}, + {file = "grpcio_tools-1.33.2-cp38-cp38-win_amd64.whl", hash = "sha256:947f9d96271c7ab0470b58438bd788ce1d4308c73696eea64eab58e7e68c1d39"}, + {file = "grpcio_tools-1.33.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:853f632fcb354c4c48c4abb151a3f7d9ecf1ab662fbc3483eb4f941f61573b88"}, + {file = "grpcio_tools-1.33.2-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:792e4ed3ab2063d330067253cf76d9b6d4b957fb723cb191adf025486df69f3f"}, + {file = "grpcio_tools-1.33.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:081cad29621dadec6b5cedb818c3261ac25ba5a78c09dffe18d51637debfd750"}, + {file = "grpcio_tools-1.33.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:0d44a17cac96005cec38e8197ec2b76a61fea6f3861c38765a3b51a4787fdbef"}, + {file = "grpcio_tools-1.33.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:ed36ca81cdbe08f2976323930f8430ec97a68ec7d690606d59a7303ae6b61726"}, + {file = "grpcio_tools-1.33.2-cp39-cp39-win32.whl", hash = "sha256:da31aeebb9c3a901bf1f0773fac20cb9e9d662cdf4f19eabc6dc55c52409d09c"}, + {file = "grpcio_tools-1.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:b68ce929acb8c392b83c49257e18f03d73ca3af408bfd5f7e78fe351c384c008"}, ] guzzle-sphinx-theme = [ {file = "guzzle_sphinx_theme-0.7.11.tar.gz", hash = "sha256:9b8c1639c343c02c3f3db7df660ddf6f533b5454ee92a5f7b02edaa573fed3e6"}, @@ -983,17 +1050,51 @@ importlib-metadata = [ {file = "importlib_metadata-2.0.0.tar.gz", hash = "sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da"}, ] iniconfig = [ - {file = "iniconfig-1.0.1-py3-none-any.whl", hash = "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437"}, - {file = "iniconfig-1.0.1.tar.gz", hash = "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69"}, + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] isort = [ - {file = "isort-5.5.4-py3-none-any.whl", hash = "sha256:36f0c6659b9000597e92618d05b72d4181104cf59472b1c6a039e3783f930c95"}, - {file = "isort-5.5.4.tar.gz", hash = "sha256:ba040c24d20aa302f78f4747df549573ae1eaf8e1084269199154da9c483f07f"}, + {file = "isort-5.6.4-py3-none-any.whl", hash = "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7"}, + {file = "isort-5.6.4.tar.gz", hash = "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58"}, ] jinja2 = [ {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, ] +kiwisolver = [ + {file = "kiwisolver-1.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fd34fbbfbc40628200730bc1febe30631347103fc8d3d4fa012c21ab9c11eca9"}, + {file = "kiwisolver-1.3.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:d3155d828dec1d43283bd24d3d3e0d9c7c350cdfcc0bd06c0ad1209c1bbc36d0"}, + {file = "kiwisolver-1.3.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5a7a7dbff17e66fac9142ae2ecafb719393aaee6a3768c9de2fd425c63b53e21"}, + {file = "kiwisolver-1.3.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:f8d6f8db88049a699817fd9178782867bf22283e3813064302ac59f61d95be05"}, + {file = "kiwisolver-1.3.1-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:5f6ccd3dd0b9739edcf407514016108e2280769c73a85b9e59aa390046dbf08b"}, + {file = "kiwisolver-1.3.1-cp36-cp36m-win32.whl", hash = "sha256:225e2e18f271e0ed8157d7f4518ffbf99b9450fca398d561eb5c4a87d0986dd9"}, + {file = "kiwisolver-1.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cf8b574c7b9aa060c62116d4181f3a1a4e821b2ec5cbfe3775809474113748d4"}, + {file = "kiwisolver-1.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:232c9e11fd7ac3a470d65cd67e4359eee155ec57e822e5220322d7b2ac84fbf0"}, + {file = "kiwisolver-1.3.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b38694dcdac990a743aa654037ff1188c7a9801ac3ccc548d3341014bc5ca278"}, + {file = "kiwisolver-1.3.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ca3820eb7f7faf7f0aa88de0e54681bddcb46e485beb844fcecbcd1c8bd01689"}, + {file = "kiwisolver-1.3.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:c8fd0f1ae9d92b42854b2979024d7597685ce4ada367172ed7c09edf2cef9cb8"}, + {file = "kiwisolver-1.3.1-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:1e1bc12fb773a7b2ffdeb8380609f4f8064777877b2225dec3da711b421fda31"}, + {file = "kiwisolver-1.3.1-cp37-cp37m-win32.whl", hash = "sha256:72c99e39d005b793fb7d3d4e660aed6b6281b502e8c1eaf8ee8346023c8e03bc"}, + {file = "kiwisolver-1.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:8be8d84b7d4f2ba4ffff3665bcd0211318aa632395a1a41553250484a871d454"}, + {file = "kiwisolver-1.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:31dfd2ac56edc0ff9ac295193eeaea1c0c923c0355bf948fbd99ed6018010b72"}, + {file = "kiwisolver-1.3.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:563c649cfdef27d081c84e72a03b48ea9408c16657500c312575ae9d9f7bc1c3"}, + {file = "kiwisolver-1.3.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:78751b33595f7f9511952e7e60ce858c6d64db2e062afb325985ddbd34b5c131"}, + {file = "kiwisolver-1.3.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a357fd4f15ee49b4a98b44ec23a34a95f1e00292a139d6015c11f55774ef10de"}, + {file = "kiwisolver-1.3.1-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:5989db3b3b34b76c09253deeaf7fbc2707616f130e166996606c284395da3f18"}, + {file = "kiwisolver-1.3.1-cp38-cp38-win32.whl", hash = "sha256:c08e95114951dc2090c4a630c2385bef681cacf12636fb0241accdc6b303fd81"}, + {file = "kiwisolver-1.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:44a62e24d9b01ba94ae7a4a6c3fb215dc4af1dde817e7498d901e229aaf50e4e"}, + {file = "kiwisolver-1.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:50af681a36b2a1dee1d3c169ade9fdc59207d3c31e522519181e12f1b3ba7000"}, + {file = "kiwisolver-1.3.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:a53d27d0c2a0ebd07e395e56a1fbdf75ffedc4a05943daf472af163413ce9598"}, + {file = "kiwisolver-1.3.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:834ee27348c4aefc20b479335fd422a2c69db55f7d9ab61721ac8cd83eb78882"}, + {file = "kiwisolver-1.3.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5c3e6455341008a054cccee8c5d24481bcfe1acdbc9add30aa95798e95c65621"}, + {file = "kiwisolver-1.3.1-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:acef3d59d47dd85ecf909c359d0fd2c81ed33bdff70216d3956b463e12c38a54"}, + {file = "kiwisolver-1.3.1-cp39-cp39-win32.whl", hash = "sha256:c5518d51a0735b1e6cee1fdce66359f8d2b59c3ca85dc2b0813a8aa86818a030"}, + {file = "kiwisolver-1.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:b9edd0110a77fc321ab090aaa1cfcaba1d8499850a12848b81be2222eab648f6"}, + {file = "kiwisolver-1.3.1-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0cd53f403202159b44528498de18f9285b04482bab2a6fc3f5dd8dbb9352e30d"}, + {file = "kiwisolver-1.3.1-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:33449715e0101e4d34f64990352bce4095c8bf13bed1b390773fc0a7295967b3"}, + {file = "kiwisolver-1.3.1-pp36-pypy36_pp73-win32.whl", hash = "sha256:401a2e9afa8588589775fe34fc22d918ae839aaaf0c0e96441c0fdbce6d8ebe6"}, + {file = "kiwisolver-1.3.1.tar.gz", hash = "sha256:950a199911a8d94683a6b10321f9345d5a3a8433ec58b217ace979e18f16e248"}, +] lazy-object-proxy = [ {file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"}, {file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"}, @@ -1052,6 +1153,33 @@ markupsafe = [ {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] +matplotlib = [ + {file = "matplotlib-3.3.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b2a5e1f637a92bb6f3526cc54cc8af0401112e81ce5cba6368a1b7908f9e18bc"}, + {file = "matplotlib-3.3.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c586ac1d64432f92857c3cf4478cfb0ece1ae18b740593f8a39f2f0b27c7fda5"}, + {file = "matplotlib-3.3.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:9b03722c89a43a61d4d148acfc89ec5bb54cd0fd1539df25b10eb9c5fa6c393a"}, + {file = "matplotlib-3.3.3-cp36-cp36m-win32.whl", hash = "sha256:2c2c5041608cb75c39cbd0ed05256f8a563e144234a524c59d091abbfa7a868f"}, + {file = "matplotlib-3.3.3-cp36-cp36m-win_amd64.whl", hash = "sha256:c092fc4673260b1446b8578015321081d5db73b94533fe4bf9b69f44e948d174"}, + {file = "matplotlib-3.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:27c9393fada62bd0ad7c730562a0fecbd3d5aaa8d9ed80ba7d3ebb8abc4f0453"}, + {file = "matplotlib-3.3.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b8ba2a1dbb4660cb469fe8e1febb5119506059e675180c51396e1723ff9b79d9"}, + {file = "matplotlib-3.3.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0caa687fce6174fef9b27d45f8cc57cbc572e04e98c81db8e628b12b563d59a2"}, + {file = "matplotlib-3.3.3-cp37-cp37m-win32.whl", hash = "sha256:b7b09c61a91b742cb5460b72efd1fe26ef83c1c704f666e0af0df156b046aada"}, + {file = "matplotlib-3.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:6ffd2d80d76df2e5f9f0c0140b5af97e3b87dd29852dcdb103ec177d853ec06b"}, + {file = "matplotlib-3.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5111d6d47a0f5b8f3e10af7a79d5e7eb7e73a22825391834734274c4f312a8a0"}, + {file = "matplotlib-3.3.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a4fe54eab2c7129add75154823e6543b10261f9b65b2abe692d68743a4999f8c"}, + {file = "matplotlib-3.3.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:83e6c895d93fdf93eeff1a21ee96778ba65ef258e5d284160f7c628fee40c38f"}, + {file = "matplotlib-3.3.3-cp38-cp38-win32.whl", hash = "sha256:b26c472847911f5a7eb49e1c888c31c77c4ddf8023c1545e0e8e0367ba74fb15"}, + {file = "matplotlib-3.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:09225edca87a79815822eb7d3be63a83ebd4d9d98d5aa3a15a94f4eee2435954"}, + {file = "matplotlib-3.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eb6b6700ea454bb88333d98601e74928e06f9669c1ea231b4c4c666c1d7701b4"}, + {file = "matplotlib-3.3.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2d31aff0c8184b05006ad756b9a4dc2a0805e94d28f3abc3187e881b6673b302"}, + {file = "matplotlib-3.3.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d082f77b4ed876ae94a9373f0db96bf8768a7cca6c58fc3038f94e30ffde1880"}, + {file = "matplotlib-3.3.3-cp39-cp39-win32.whl", hash = "sha256:e71cdd402047e657c1662073e9361106c6981e9621ab8c249388dfc3ec1de07b"}, + {file = "matplotlib-3.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:756ee498b9ba35460e4cbbd73f09018e906daa8537fff61da5b5bf8d5e9de5c7"}, + {file = "matplotlib-3.3.3-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ad44f2c74c50567c694ee91c6fa16d67e7c8af6f22c656b80469ad927688457"}, + {file = "matplotlib-3.3.3-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:3a4c3e9be63adf8e9b305aa58fb3ec40ecc61fd0f8fd3328ce55bc30e7a2aeb0"}, + {file = "matplotlib-3.3.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:746897fbd72bd462b888c74ed35d812ca76006b04f717cd44698cdfc99aca70d"}, + {file = "matplotlib-3.3.3-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:5ed3d3342698c2b1f3651f8ea6c099b0f196d16ee00e33dc3a6fee8cb01d530a"}, + {file = "matplotlib-3.3.3.tar.gz", hash = "sha256:b1b60c6476c4cfe9e5cf8ab0d3127476fd3d5f05de0f343a452badaad0e4bdec"}, +] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, @@ -1061,79 +1189,87 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] numpy = [ - {file = "numpy-1.19.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b594f76771bc7fc8a044c5ba303427ee67c17a09b36e1fa32bde82f5c419d17a"}, - {file = "numpy-1.19.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:e6ddbdc5113628f15de7e4911c02aed74a4ccff531842c583e5032f6e5a179bd"}, - {file = "numpy-1.19.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3733640466733441295b0d6d3dcbf8e1ffa7e897d4d82903169529fd3386919a"}, - {file = "numpy-1.19.2-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:4339741994c775396e1a274dba3609c69ab0f16056c1077f18979bec2a2c2e6e"}, - {file = "numpy-1.19.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7c6646314291d8f5ea900a7ea9c4261f834b5b62159ba2abe3836f4fa6705526"}, - {file = "numpy-1.19.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:7118f0a9f2f617f921ec7d278d981244ba83c85eea197be7c5a4f84af80a9c3c"}, - {file = "numpy-1.19.2-cp36-cp36m-win32.whl", hash = "sha256:9a3001248b9231ed73894c773142658bab914645261275f675d86c290c37f66d"}, - {file = "numpy-1.19.2-cp36-cp36m-win_amd64.whl", hash = "sha256:967c92435f0b3ba37a4257c48b8715b76741410467e2bdb1097e8391fccfae15"}, - {file = "numpy-1.19.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d526fa58ae4aead839161535d59ea9565863bb0b0bdb3cc63214613fb16aced4"}, - {file = "numpy-1.19.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:eb25c381d168daf351147713f49c626030dcff7a393d5caa62515d415a6071d8"}, - {file = "numpy-1.19.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:62139af94728d22350a571b7c82795b9d59be77fc162414ada6c8b6a10ef5d02"}, - {file = "numpy-1.19.2-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:0c66da1d202c52051625e55a249da35b31f65a81cb56e4c69af0dfb8fb0125bf"}, - {file = "numpy-1.19.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:2117536e968abb7357d34d754e3733b0d7113d4c9f1d921f21a3d96dec5ff716"}, - {file = "numpy-1.19.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:54045b198aebf41bf6bf4088012777c1d11703bf74461d70cd350c0af2182e45"}, - {file = "numpy-1.19.2-cp37-cp37m-win32.whl", hash = "sha256:aba1d5daf1144b956bc87ffb87966791f5e9f3e1f6fab3d7f581db1f5b598f7a"}, - {file = "numpy-1.19.2-cp37-cp37m-win_amd64.whl", hash = "sha256:addaa551b298052c16885fc70408d3848d4e2e7352de4e7a1e13e691abc734c1"}, - {file = "numpy-1.19.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:58d66a6b3b55178a1f8a5fe98df26ace76260a70de694d99577ddeab7eaa9a9d"}, - {file = "numpy-1.19.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:59f3d687faea7a4f7f93bd9665e5b102f32f3fa28514f15b126f099b7997203d"}, - {file = "numpy-1.19.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cebd4f4e64cfe87f2039e4725781f6326a61f095bc77b3716502bed812b385a9"}, - {file = "numpy-1.19.2-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c35a01777f81e7333bcf276b605f39c872e28295441c265cd0c860f4b40148c1"}, - {file = "numpy-1.19.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d7ac33585e1f09e7345aa902c281bd777fdb792432d27fca857f39b70e5dd31c"}, - {file = "numpy-1.19.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:04c7d4ebc5ff93d9822075ddb1751ff392a4375e5885299445fcebf877f179d5"}, - {file = "numpy-1.19.2-cp38-cp38-win32.whl", hash = "sha256:51ee93e1fac3fe08ef54ff1c7f329db64d8a9c5557e6c8e908be9497ac76374b"}, - {file = "numpy-1.19.2-cp38-cp38-win_amd64.whl", hash = "sha256:1669ec8e42f169ff715a904c9b2105b6640f3f2a4c4c2cb4920ae8b2785dac65"}, - {file = "numpy-1.19.2-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:0bfd85053d1e9f60234f28f63d4a5147ada7f432943c113a11afcf3e65d9d4c8"}, - {file = "numpy-1.19.2.zip", hash = "sha256:0d310730e1e793527065ad7dde736197b705d0e4c9999775f212b03c44a8484c"}, + {file = "numpy-1.19.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e9b30d4bd69498fc0c3fe9db5f62fffbb06b8eb9321f92cc970f2969be5e3949"}, + {file = "numpy-1.19.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:fedbd128668ead37f33917820b704784aff695e0019309ad446a6d0b065b57e4"}, + {file = "numpy-1.19.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8ece138c3a16db8c1ad38f52eb32be6086cc72f403150a79336eb2045723a1ad"}, + {file = "numpy-1.19.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:64324f64f90a9e4ef732be0928be853eee378fd6a01be21a0a8469c4f2682c83"}, + {file = "numpy-1.19.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:ad6f2ff5b1989a4899bf89800a671d71b1612e5ff40866d1f4d8bcf48d4e5764"}, + {file = "numpy-1.19.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d6c7bb82883680e168b55b49c70af29b84b84abb161cbac2800e8fcb6f2109b6"}, + {file = "numpy-1.19.4-cp36-cp36m-win32.whl", hash = "sha256:13d166f77d6dc02c0a73c1101dd87fdf01339febec1030bd810dcd53fff3b0f1"}, + {file = "numpy-1.19.4-cp36-cp36m-win_amd64.whl", hash = "sha256:448ebb1b3bf64c0267d6b09a7cba26b5ae61b6d2dbabff7c91b660c7eccf2bdb"}, + {file = "numpy-1.19.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:27d3f3b9e3406579a8af3a9f262f5339005dd25e0ecf3cf1559ff8a49ed5cbf2"}, + {file = "numpy-1.19.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:16c1b388cc31a9baa06d91a19366fb99ddbe1c7b205293ed072211ee5bac1ed2"}, + {file = "numpy-1.19.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e5b6ed0f0b42317050c88022349d994fe72bfe35f5908617512cd8c8ef9da2a9"}, + {file = "numpy-1.19.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:18bed2bcb39e3f758296584337966e68d2d5ba6aab7e038688ad53c8f889f757"}, + {file = "numpy-1.19.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:fe45becb4c2f72a0907c1d0246ea6449fe7a9e2293bb0e11c4e9a32bb0930a15"}, + {file = "numpy-1.19.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:6d7593a705d662be5bfe24111af14763016765f43cb6923ed86223f965f52387"}, + {file = "numpy-1.19.4-cp37-cp37m-win32.whl", hash = "sha256:6ae6c680f3ebf1cf7ad1d7748868b39d9f900836df774c453c11c5440bc15b36"}, + {file = "numpy-1.19.4-cp37-cp37m-win_amd64.whl", hash = "sha256:9eeb7d1d04b117ac0d38719915ae169aa6b61fca227b0b7d198d43728f0c879c"}, + {file = "numpy-1.19.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cb1017eec5257e9ac6209ac172058c430e834d5d2bc21961dceeb79d111e5909"}, + {file = "numpy-1.19.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:edb01671b3caae1ca00881686003d16c2209e07b7ef8b7639f1867852b948f7c"}, + {file = "numpy-1.19.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f29454410db6ef8126c83bd3c968d143304633d45dc57b51252afbd79d700893"}, + {file = "numpy-1.19.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:ec149b90019852266fec2341ce1db513b843e496d5a8e8cdb5ced1923a92faab"}, + {file = "numpy-1.19.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1aeef46a13e51931c0b1cf8ae1168b4a55ecd282e6688fdb0a948cc5a1d5afb9"}, + {file = "numpy-1.19.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:08308c38e44cc926bdfce99498b21eec1f848d24c302519e64203a8da99a97db"}, + {file = "numpy-1.19.4-cp38-cp38-win32.whl", hash = "sha256:5734bdc0342aba9dfc6f04920988140fb41234db42381cf7ccba64169f9fe7ac"}, + {file = "numpy-1.19.4-cp38-cp38-win_amd64.whl", hash = "sha256:09c12096d843b90eafd01ea1b3307e78ddd47a55855ad402b157b6c4862197ce"}, + {file = "numpy-1.19.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e452dc66e08a4ce642a961f134814258a082832c78c90351b75c41ad16f79f63"}, + {file = "numpy-1.19.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:a5d897c14513590a85774180be713f692df6fa8ecf6483e561a6d47309566f37"}, + {file = "numpy-1.19.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:a09f98011236a419ee3f49cedc9ef27d7a1651df07810ae430a6b06576e0b414"}, + {file = "numpy-1.19.4-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:50e86c076611212ca62e5a59f518edafe0c0730f7d9195fec718da1a5c2bb1fc"}, + {file = "numpy-1.19.4-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f0d3929fe88ee1c155129ecd82f981b8856c5d97bcb0d5f23e9b4242e79d1de3"}, + {file = "numpy-1.19.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:c42c4b73121caf0ed6cd795512c9c09c52a7287b04d105d112068c1736d7c753"}, + {file = "numpy-1.19.4-cp39-cp39-win32.whl", hash = "sha256:8cac8790a6b1ddf88640a9267ee67b1aee7a57dfa2d2dd33999d080bc8ee3a0f"}, + {file = "numpy-1.19.4-cp39-cp39-win_amd64.whl", hash = "sha256:4377e10b874e653fe96985c05feed2225c912e328c8a26541f7fc600fb9c637b"}, + {file = "numpy-1.19.4-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:2a2740aa9733d2e5b2dfb33639d98a64c3b0f24765fed86b0fd2aec07f6a0a08"}, + {file = "numpy-1.19.4.zip", hash = "sha256:141ec3a3300ab89c7f2b0775289954d193cc8edb621ea05f99db9cb181530512"}, ] packaging = [ {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, ] pangocairocffi = [ - {file = "pangocairocffi-0.3.2.tar.gz", hash = "sha256:ab0364646032533a2fe78c50727da436ee3aeb533f6da0524605c346c739d85c"}, + {file = "pangocairocffi-0.4.0.tar.gz", hash = "sha256:ebebffb16861aca36823d39dfbcabe963eb4af03035ed9e0a701bd97a0e16af0"}, ] pangocffi = [ - {file = "pangocffi-0.6.0.tar.gz", hash = "sha256:bce26a098d0ce77bc5e9b6ce0cd0aa256e0abdaef22b2e667d80ad57ed4c4c1c"}, + {file = "pangocffi-0.8.0.tar.gz", hash = "sha256:f8b03bc7dcc85e70f8cc58ca2b56b2077511a44c3d5c20bb0abdcbc66132288d"}, ] pathspec = [ - {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, - {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, + {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, + {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, ] pathtools = [ {file = "pathtools-0.1.2.tar.gz", hash = "sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0"}, ] pillow = [ - {file = "Pillow-7.2.0-cp35-cp35m-macosx_10_10_intel.whl", hash = "sha256:1ca594126d3c4def54babee699c055a913efb01e106c309fa6b04405d474d5ae"}, - {file = "Pillow-7.2.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c92302a33138409e8f1ad16731568c55c9053eee71bb05b6b744067e1b62380f"}, - {file = "Pillow-7.2.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:8dad18b69f710bf3a001d2bf3afab7c432785d94fcf819c16b5207b1cfd17d38"}, - {file = "Pillow-7.2.0-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:431b15cffbf949e89df2f7b48528be18b78bfa5177cb3036284a5508159492b5"}, - {file = "Pillow-7.2.0-cp35-cp35m-win32.whl", hash = "sha256:09d7f9e64289cb40c2c8d7ad674b2ed6105f55dc3b09aa8e4918e20a0311e7ad"}, - {file = "Pillow-7.2.0-cp35-cp35m-win_amd64.whl", hash = "sha256:0295442429645fa16d05bd567ef5cff178482439c9aad0411d3f0ce9b88b3a6f"}, - {file = "Pillow-7.2.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:ec29604081f10f16a7aea809ad42e27764188fc258b02259a03a8ff7ded3808d"}, - {file = "Pillow-7.2.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:612cfda94e9c8346f239bf1a4b082fdd5c8143cf82d685ba2dba76e7adeeb233"}, - {file = "Pillow-7.2.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0a80dd307a5d8440b0a08bd7b81617e04d870e40a3e46a32d9c246e54705e86f"}, - {file = "Pillow-7.2.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:06aba4169e78c439d528fdeb34762c3b61a70813527a2c57f0540541e9f433a8"}, - {file = "Pillow-7.2.0-cp36-cp36m-win32.whl", hash = "sha256:f7e30c27477dffc3e85c2463b3e649f751789e0f6c8456099eea7ddd53be4a8a"}, - {file = "Pillow-7.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:ffe538682dc19cc542ae7c3e504fdf54ca7f86fb8a135e59dd6bc8627eae6cce"}, - {file = "Pillow-7.2.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:94cf49723928eb6070a892cb39d6c156f7b5a2db4e8971cb958f7b6b104fb4c4"}, - {file = "Pillow-7.2.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6edb5446f44d901e8683ffb25ebdfc26988ee813da3bf91e12252b57ac163727"}, - {file = "Pillow-7.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:52125833b070791fcb5710fabc640fc1df07d087fc0c0f02d3661f76c23c5b8b"}, - {file = "Pillow-7.2.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:9ad7f865eebde135d526bb3163d0b23ffff365cf87e767c649550964ad72785d"}, - {file = "Pillow-7.2.0-cp37-cp37m-win32.whl", hash = "sha256:c79f9c5fb846285f943aafeafda3358992d64f0ef58566e23484132ecd8d7d63"}, - {file = "Pillow-7.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d350f0f2c2421e65fbc62690f26b59b0bcda1b614beb318c81e38647e0f673a1"}, - {file = "Pillow-7.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:6d7741e65835716ceea0fd13a7d0192961212fd59e741a46bbed7a473c634ed6"}, - {file = "Pillow-7.2.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:edf31f1150778abd4322444c393ab9c7bd2af271dd4dafb4208fb613b1f3cdc9"}, - {file = "Pillow-7.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:d08b23fdb388c0715990cbc06866db554e1822c4bdcf6d4166cf30ac82df8c41"}, - {file = "Pillow-7.2.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:5e51ee2b8114def244384eda1c82b10e307ad9778dac5c83fb0943775a653cd8"}, - {file = "Pillow-7.2.0-cp38-cp38-win32.whl", hash = "sha256:725aa6cfc66ce2857d585f06e9519a1cc0ef6d13f186ff3447ab6dff0a09bc7f"}, - {file = "Pillow-7.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:a060cf8aa332052df2158e5a119303965be92c3da6f2d93b6878f0ebca80b2f6"}, - {file = "Pillow-7.2.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:9c87ef410a58dd54b92424ffd7e28fd2ec65d2f7fc02b76f5e9b2067e355ebf6"}, - {file = "Pillow-7.2.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:e901964262a56d9ea3c2693df68bc9860b8bdda2b04768821e4c44ae797de117"}, - {file = "Pillow-7.2.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:25930fadde8019f374400f7986e8404c8b781ce519da27792cbe46eabec00c4d"}, - {file = "Pillow-7.2.0.tar.gz", hash = "sha256:97f9e7953a77d5a70f49b9a48da7776dc51e9b738151b22dacf101641594a626"}, + {file = "Pillow-8.0.1-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:b63d4ff734263ae4ce6593798bcfee6dbfb00523c82753a3a03cbc05555a9cc3"}, + {file = "Pillow-8.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5f9403af9c790cc18411ea398a6950ee2def2a830ad0cfe6dc9122e6d528b302"}, + {file = "Pillow-8.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6b4a8fd632b4ebee28282a9fef4c341835a1aa8671e2770b6f89adc8e8c2703c"}, + {file = "Pillow-8.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:cc3ea6b23954da84dbee8025c616040d9aa5eaf34ea6895a0a762ee9d3e12e11"}, + {file = "Pillow-8.0.1-cp36-cp36m-win32.whl", hash = "sha256:d8a96747df78cda35980905bf26e72960cba6d355ace4780d4bdde3b217cdf1e"}, + {file = "Pillow-8.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:7ba0ba61252ab23052e642abdb17fd08fdcfdbbf3b74c969a30c58ac1ade7cd3"}, + {file = "Pillow-8.0.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:795e91a60f291e75de2e20e6bdd67770f793c8605b553cb6e4387ce0cb302e09"}, + {file = "Pillow-8.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0a2e8d03787ec7ad71dc18aec9367c946ef8ef50e1e78c71f743bc3a770f9fae"}, + {file = "Pillow-8.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:006de60d7580d81f4a1a7e9f0173dc90a932e3905cc4d47ea909bc946302311a"}, + {file = "Pillow-8.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:bd7bf289e05470b1bc74889d1466d9ad4a56d201f24397557b6f65c24a6844b8"}, + {file = "Pillow-8.0.1-cp37-cp37m-win32.whl", hash = "sha256:95edb1ed513e68bddc2aee3de66ceaf743590bf16c023fb9977adc4be15bd3f0"}, + {file = "Pillow-8.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:e38d58d9138ef972fceb7aeec4be02e3f01d383723965bfcef14d174c8ccd039"}, + {file = "Pillow-8.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:d3d07c86d4efa1facdf32aa878bd508c0dc4f87c48125cc16b937baa4e5b5e11"}, + {file = "Pillow-8.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:fbd922f702582cb0d71ef94442bfca57624352622d75e3be7a1e7e9360b07e72"}, + {file = "Pillow-8.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:92c882b70a40c79de9f5294dc99390671e07fc0b0113d472cbea3fde15db1792"}, + {file = "Pillow-8.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7c9401e68730d6c4245b8e361d3d13e1035cbc94db86b49dc7da8bec235d0015"}, + {file = "Pillow-8.0.1-cp38-cp38-win32.whl", hash = "sha256:6c1aca8231625115104a06e4389fcd9ec88f0c9befbabd80dc206c35561be271"}, + {file = "Pillow-8.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:cc9ec588c6ef3a1325fa032ec14d97b7309db493782ea8c304666fb10c3bd9a7"}, + {file = "Pillow-8.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:eb472586374dc66b31e36e14720747595c2b265ae962987261f044e5cce644b5"}, + {file = "Pillow-8.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:0eeeae397e5a79dc088d8297a4c2c6f901f8fb30db47795113a4a605d0f1e5ce"}, + {file = "Pillow-8.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:81f812d8f5e8a09b246515fac141e9d10113229bc33ea073fec11403b016bcf3"}, + {file = "Pillow-8.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:895d54c0ddc78a478c80f9c438579ac15f3e27bf442c2a9aa74d41d0e4d12544"}, + {file = "Pillow-8.0.1-cp39-cp39-win32.whl", hash = "sha256:2fb113757a369a6cdb189f8df3226e995acfed0a8919a72416626af1a0a71140"}, + {file = "Pillow-8.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:59e903ca800c8cfd1ebe482349ec7c35687b95e98cefae213e271c8c7fffa021"}, + {file = "Pillow-8.0.1-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:5abd653a23c35d980b332bc0431d39663b1709d64142e3652890df4c9b6970f6"}, + {file = "Pillow-8.0.1-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:4b0ef2470c4979e345e4e0cc1bbac65fda11d0d7b789dbac035e4c6ce3f98adb"}, + {file = "Pillow-8.0.1-pp37-pypy37_pp73-win32.whl", hash = "sha256:8de332053707c80963b589b22f8e0229f1be1f3ca862a932c1bcd48dafb18dd8"}, + {file = "Pillow-8.0.1.tar.gz", hash = "sha256:11c5c6e9b02c9dac08af04f093eb5a2f84857df70a7d4a6a6ad461aca803fb9e"}, ] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, @@ -1143,24 +1279,24 @@ progressbar = [ {file = "progressbar-2.5.tar.gz", hash = "sha256:5d81cb529da2e223b53962afd6c8ca0f05c6670e40309a7219eacc36af9b6c63"}, ] protobuf = [ - {file = "protobuf-3.13.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9c2e63c1743cba12737169c447374fab3dfeb18111a460a8c1a000e35836b18c"}, - {file = "protobuf-3.13.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1e834076dfef9e585815757a2c7e4560c7ccc5962b9d09f831214c693a91b463"}, - {file = "protobuf-3.13.0-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:df3932e1834a64b46ebc262e951cd82c3cf0fa936a154f0a42231140d8237060"}, - {file = "protobuf-3.13.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:8c35bcbed1c0d29b127c886790e9d37e845ffc2725cc1db4bd06d70f4e8359f4"}, - {file = "protobuf-3.13.0-cp35-cp35m-win32.whl", hash = "sha256:339c3a003e3c797bc84499fa32e0aac83c768e67b3de4a5d7a5a9aa3b0da634c"}, - {file = "protobuf-3.13.0-cp35-cp35m-win_amd64.whl", hash = "sha256:361acd76f0ad38c6e38f14d08775514fbd241316cce08deb2ce914c7dfa1184a"}, - {file = "protobuf-3.13.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9edfdc679a3669988ec55a989ff62449f670dfa7018df6ad7f04e8dbacb10630"}, - {file = "protobuf-3.13.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5db9d3e12b6ede5e601b8d8684a7f9d90581882925c96acf8495957b4f1b204b"}, - {file = "protobuf-3.13.0-cp36-cp36m-win32.whl", hash = "sha256:c8abd7605185836f6f11f97b21200f8a864f9cb078a193fe3c9e235711d3ff1e"}, - {file = "protobuf-3.13.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4d1174c9ed303070ad59553f435846a2f877598f59f9afc1b89757bdf846f2a7"}, - {file = "protobuf-3.13.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0bba42f439bf45c0f600c3c5993666fcb88e8441d011fad80a11df6f324eef33"}, - {file = "protobuf-3.13.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c0c5ab9c4b1eac0a9b838f1e46038c3175a95b0f2d944385884af72876bd6bc7"}, - {file = "protobuf-3.13.0-cp37-cp37m-win32.whl", hash = "sha256:f68eb9d03c7d84bd01c790948320b768de8559761897763731294e3bc316decb"}, - {file = "protobuf-3.13.0-cp37-cp37m-win_amd64.whl", hash = "sha256:91c2d897da84c62816e2f473ece60ebfeab024a16c1751aaf31100127ccd93ec"}, - {file = "protobuf-3.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3dee442884a18c16d023e52e32dd34a8930a889e511af493f6dc7d4d9bf12e4f"}, - {file = "protobuf-3.13.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:e7662437ca1e0c51b93cadb988f9b353fa6b8013c0385d63a70c8a77d84da5f9"}, - {file = "protobuf-3.13.0-py2.py3-none-any.whl", hash = "sha256:d69697acac76d9f250ab745b46c725edf3e98ac24763990b24d58c16c642947a"}, - {file = "protobuf-3.13.0.tar.gz", hash = "sha256:6a82e0c8bb2bf58f606040cc5814e07715b2094caeba281e2e7d0b0e2e397db5"}, + {file = "protobuf-3.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:629b03fd3caae7f815b0c66b41273f6b1900a579e2ccb41ef4493a4f5fb84f3a"}, + {file = "protobuf-3.14.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:5b7a637212cc9b2bcf85dd828b1178d19efdf74dbfe1ddf8cd1b8e01fdaaa7f5"}, + {file = "protobuf-3.14.0-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:43b554b9e73a07ba84ed6cf25db0ff88b1e06be610b37656e292e3cbb5437472"}, + {file = "protobuf-3.14.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:5e9806a43232a1fa0c9cf5da8dc06f6910d53e4390be1fa06f06454d888a9142"}, + {file = "protobuf-3.14.0-cp35-cp35m-win32.whl", hash = "sha256:1c51fda1bbc9634246e7be6016d860be01747354ed7015ebe38acf4452f470d2"}, + {file = "protobuf-3.14.0-cp35-cp35m-win_amd64.whl", hash = "sha256:4b74301b30513b1a7494d3055d95c714b560fbb630d8fb9956b6f27992c9f980"}, + {file = "protobuf-3.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:86a75477addde4918e9a1904e5c6af8d7b691f2a3f65587d73b16100fbe4c3b2"}, + {file = "protobuf-3.14.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ecc33531a213eee22ad60e0e2aaea6c8ba0021f0cce35dbf0ab03dee6e2a23a1"}, + {file = "protobuf-3.14.0-cp36-cp36m-win32.whl", hash = "sha256:72230ed56f026dd664c21d73c5db73ebba50d924d7ba6b7c0d81a121e390406e"}, + {file = "protobuf-3.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:0fc96785262042e4863b3f3b5c429d4636f10d90061e1840fce1baaf59b1a836"}, + {file = "protobuf-3.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4e75105c9dfe13719b7293f75bd53033108f4ba03d44e71db0ec2a0e8401eafd"}, + {file = "protobuf-3.14.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2a7e2fe101a7ace75e9327b9c946d247749e564a267b0515cf41dfe450b69bac"}, + {file = "protobuf-3.14.0-cp37-cp37m-win32.whl", hash = "sha256:b0d5d35faeb07e22a1ddf8dce620860c8fe145426c02d1a0ae2688c6e8ede36d"}, + {file = "protobuf-3.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8971c421dbd7aad930c9bd2694122f332350b6ccb5202a8b7b06f3f1a5c41ed5"}, + {file = "protobuf-3.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9616f0b65a30851e62f1713336c931fcd32c057202b7ff2cfbfca0fc7d5e3043"}, + {file = "protobuf-3.14.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:22bcd2e284b3b1d969c12e84dc9b9a71701ec82d8ce975fdda19712e1cfd4e00"}, + {file = "protobuf-3.14.0-py2.py3-none-any.whl", hash = "sha256:0e247612fadda953047f53301a7b0407cb0c3cb4ae25a6fde661597a04039b3c"}, + {file = "protobuf-3.14.0.tar.gz", hash = "sha256:1d63eb389347293d8915fb47bee0951c7b5dab522a4a60118b9a18f33e21f8ce"}, ] py = [ {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, @@ -1173,6 +1309,8 @@ pycairo = [ {file = "pycairo-1.20.0-cp37-cp37m-win_amd64.whl", hash = "sha256:273a33c56aba724ec42fe1d8f94c86c2e2660c1277470be9b04e5113d7c5b72d"}, {file = "pycairo-1.20.0-cp38-cp38-win32.whl", hash = "sha256:2088100a099c09c5e90bf247409ce6c98f51766b53bd13f96d6aac7addaa3e66"}, {file = "pycairo-1.20.0-cp38-cp38-win_amd64.whl", hash = "sha256:ceb1edcbeb48dabd5fbbdff2e4b429aa88ddc493d6ebafe78d94b050ac0749e2"}, + {file = "pycairo-1.20.0-cp39-cp39-win32.whl", hash = "sha256:57a768f4edc8a9890d98070dd473a812ac3d046cef4bc1c817d68024dab9a9b4"}, + {file = "pycairo-1.20.0-cp39-cp39-win_amd64.whl", hash = "sha256:57166119e424d71eccdba6b318bd731bdabd17188e2ba10d4f315f7bf16ace3f"}, {file = "pycairo-1.20.0.tar.gz", hash = "sha256:5695a10cb7f9ae0d01f665b56602a845b0a8cb17e2123bfece10c2e58552468c"}, ] pycparser = [ @@ -1184,8 +1322,8 @@ pydub = [ {file = "pydub-0.24.1.tar.gz", hash = "sha256:630c68bfff9bb27cbc5e1f02923f717c3bc5f4d73fd685fda08b6ce90f76dc69"}, ] pygments = [ - {file = "Pygments-2.7.1-py3-none-any.whl", hash = "sha256:307543fe65c0947b126e83dd5a61bd8acbd84abec11f43caebaf5534cbc17998"}, - {file = "Pygments-2.7.1.tar.gz", hash = "sha256:926c3f319eda178d1bd90851e4317e6d8cdb5e292a3386aac9bd75eca29cf9c7"}, + {file = "Pygments-2.7.2-py3-none-any.whl", hash = "sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773"}, + {file = "Pygments-2.7.2.tar.gz", hash = "sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0"}, ] pylint = [ {file = "pylint-2.6.0-py3-none-any.whl", hash = "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f"}, @@ -1196,71 +1334,98 @@ pyparsing = [ {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] pytest = [ - {file = "pytest-6.1.1-py3-none-any.whl", hash = "sha256:7a8190790c17d79a11f847fba0b004ee9a8122582ebff4729a082c109e81a4c9"}, - {file = "pytest-6.1.1.tar.gz", hash = "sha256:8f593023c1a0f916110285b6efd7f99db07d59546e3d8c36fc60e2ab05d3be92"}, + {file = "pytest-6.1.2-py3-none-any.whl", hash = "sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe"}, + {file = "pytest-6.1.2.tar.gz", hash = "sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, + {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, ] pytz = [ - {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"}, - {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, + {file = "pytz-2020.4-py2.py3-none-any.whl", hash = "sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd"}, + {file = "pytz-2020.4.tar.gz", hash = "sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268"}, ] recommonmark = [ {file = "recommonmark-0.6.0-py2.py3-none-any.whl", hash = "sha256:2ec4207a574289355d5b6ae4ae4abb29043346ca12cdd5f07d374dc5987d2852"}, {file = "recommonmark-0.6.0.tar.gz", hash = "sha256:29cd4faeb6c5268c633634f2d69aef9431e0f4d347f90659fd0aab20e541efeb"}, ] regex = [ - {file = "regex-2020.9.27-cp27-cp27m-win32.whl", hash = "sha256:d23a18037313714fb3bb5a94434d3151ee4300bae631894b1ac08111abeaa4a3"}, - {file = "regex-2020.9.27-cp27-cp27m-win_amd64.whl", hash = "sha256:84e9407db1b2eb368b7ecc283121b5e592c9aaedbe8c78b1a2f1102eb2e21d19"}, - {file = "regex-2020.9.27-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5f18875ac23d9aa2f060838e8b79093e8bb2313dbaaa9f54c6d8e52a5df097be"}, - {file = "regex-2020.9.27-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ae91972f8ac958039920ef6e8769277c084971a142ce2b660691793ae44aae6b"}, - {file = "regex-2020.9.27-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9a02d0ae31d35e1ec12a4ea4d4cca990800f66a917d0fb997b20fbc13f5321fc"}, - {file = "regex-2020.9.27-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:ebbe29186a3d9b0c591e71b7393f1ae08c83cb2d8e517d2a822b8f7ec99dfd8b"}, - {file = "regex-2020.9.27-cp36-cp36m-win32.whl", hash = "sha256:4707f3695b34335afdfb09be3802c87fa0bc27030471dbc082f815f23688bc63"}, - {file = "regex-2020.9.27-cp36-cp36m-win_amd64.whl", hash = "sha256:9bc13e0d20b97ffb07821aa3e113f9998e84994fe4d159ffa3d3a9d1b805043b"}, - {file = "regex-2020.9.27-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f1b3afc574a3db3b25c89161059d857bd4909a1269b0b3cb3c904677c8c4a3f7"}, - {file = "regex-2020.9.27-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5533a959a1748a5c042a6da71fe9267a908e21eded7a4f373efd23a2cbdb0ecc"}, - {file = "regex-2020.9.27-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:1fe0a41437bbd06063aa184c34804efa886bcc128222e9916310c92cd54c3b4c"}, - {file = "regex-2020.9.27-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:c570f6fa14b9c4c8a4924aaad354652366577b4f98213cf76305067144f7b100"}, - {file = "regex-2020.9.27-cp37-cp37m-win32.whl", hash = "sha256:eda4771e0ace7f67f58bc5b560e27fb20f32a148cbc993b0c3835970935c2707"}, - {file = "regex-2020.9.27-cp37-cp37m-win_amd64.whl", hash = "sha256:60b0e9e6dc45683e569ec37c55ac20c582973841927a85f2d8a7d20ee80216ab"}, - {file = "regex-2020.9.27-cp38-cp38-manylinux1_i686.whl", hash = "sha256:088afc8c63e7bd187a3c70a94b9e50ab3f17e1d3f52a32750b5b77dbe99ef5ef"}, - {file = "regex-2020.9.27-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:eaf548d117b6737df379fdd53bdde4f08870e66d7ea653e230477f071f861121"}, - {file = "regex-2020.9.27-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:41bb65f54bba392643557e617316d0d899ed5b4946dccee1cb6696152b29844b"}, - {file = "regex-2020.9.27-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:8d69cef61fa50c8133382e61fd97439de1ae623fe943578e477e76a9d9471637"}, - {file = "regex-2020.9.27-cp38-cp38-win32.whl", hash = "sha256:f2388013e68e750eaa16ccbea62d4130180c26abb1d8e5d584b9baf69672b30f"}, - {file = "regex-2020.9.27-cp38-cp38-win_amd64.whl", hash = "sha256:4318d56bccfe7d43e5addb272406ade7a2274da4b70eb15922a071c58ab0108c"}, - {file = "regex-2020.9.27-cp39-cp39-manylinux1_i686.whl", hash = "sha256:84cada8effefe9a9f53f9b0d2ba9b7b6f5edf8d2155f9fdbe34616e06ececf81"}, - {file = "regex-2020.9.27-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:816064fc915796ea1f26966163f6845de5af78923dfcecf6551e095f00983650"}, - {file = "regex-2020.9.27-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:5d892a4f1c999834eaa3c32bc9e8b976c5825116cde553928c4c8e7e48ebda67"}, - {file = "regex-2020.9.27-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c9443124c67b1515e4fe0bb0aa18df640965e1030f468a2a5dc2589b26d130ad"}, - {file = "regex-2020.9.27-cp39-cp39-win32.whl", hash = "sha256:49f23ebd5ac073765ecbcf046edc10d63dcab2f4ae2bce160982cb30df0c0302"}, - {file = "regex-2020.9.27-cp39-cp39-win_amd64.whl", hash = "sha256:3d20024a70b97b4f9546696cbf2fd30bae5f42229fbddf8661261b1eaff0deb7"}, - {file = "regex-2020.9.27.tar.gz", hash = "sha256:a6f32aea4260dfe0e55dc9733ea162ea38f0ea86aa7d0f77b15beac5bf7b369d"}, + {file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6"}, + {file = "regex-2020.11.13-cp36-cp36m-win32.whl", hash = "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e"}, + {file = "regex-2020.11.13-cp36-cp36m-win_amd64.whl", hash = "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884"}, + {file = "regex-2020.11.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538"}, + {file = "regex-2020.11.13-cp37-cp37m-win32.whl", hash = "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4"}, + {file = "regex-2020.11.13-cp37-cp37m-win_amd64.whl", hash = "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444"}, + {file = "regex-2020.11.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b"}, + {file = "regex-2020.11.13-cp38-cp38-win32.whl", hash = "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c"}, + {file = "regex-2020.11.13-cp38-cp38-win_amd64.whl", hash = "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683"}, + {file = "regex-2020.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c"}, + {file = "regex-2020.11.13-cp39-cp39-win32.whl", hash = "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"}, + {file = "regex-2020.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d"}, + {file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"}, ] requests = [ - {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, - {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, + {file = "requests-2.25.0-py2.py3-none-any.whl", hash = "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"}, + {file = "requests-2.25.0.tar.gz", hash = "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8"}, ] rich = [ - {file = "rich-8.0.0-py3-none-any.whl", hash = "sha256:3c5e4bb1e48c647bc75bc4ae7c125d399bec5b6ed2a319f0d447361635f02a9a"}, - {file = "rich-8.0.0.tar.gz", hash = "sha256:1b5023d2241e6552a24ddfe830a853fc8e53da4e6a6ed6c7105bb262593edf97"}, + {file = "rich-6.2.0-py3-none-any.whl", hash = "sha256:fa55d5d6ba9a0df1f1c95518891b57b13f1d45548a9a198a87b093fceee513ec"}, + {file = "rich-6.2.0.tar.gz", hash = "sha256:f99c877277906e1ff83b135c564920590ba31188f424dcdb5d1cae652a519b4b"}, ] scipy = [ - {file = "scipy-1.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cca9fce15109a36a0a9f9cfc64f870f1c140cb235ddf27fe0328e6afb44dfed0"}, - {file = "scipy-1.5.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:1c7564a4810c1cd77fcdee7fa726d7d39d4e2695ad252d7c86c3ea9d85b7fb8f"}, - {file = "scipy-1.5.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:07e52b316b40a4f001667d1ad4eb5f2318738de34597bd91537851365b6c61f1"}, - {file = "scipy-1.5.2-cp36-cp36m-win32.whl", hash = "sha256:d56b10d8ed72ec1be76bf10508446df60954f08a41c2d40778bc29a3a9ad9bce"}, - {file = "scipy-1.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:8e28e74b97fc8d6aa0454989db3b5d36fc27e69cef39a7ee5eaf8174ca1123cb"}, - {file = "scipy-1.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6e86c873fe1335d88b7a4bfa09d021f27a9e753758fd75f3f92d714aa4093768"}, - {file = "scipy-1.5.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:a0afbb967fd2c98efad5f4c24439a640d39463282040a88e8e928db647d8ac3d"}, - {file = "scipy-1.5.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:eecf40fa87eeda53e8e11d265ff2254729d04000cd40bae648e76ff268885d66"}, - {file = "scipy-1.5.2-cp37-cp37m-win32.whl", hash = "sha256:315aa2165aca31375f4e26c230188db192ed901761390be908c9b21d8b07df62"}, - {file = "scipy-1.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:ec5fe57e46828d034775b00cd625c4a7b5c7d2e354c3b258d820c6c72212a6ec"}, - {file = "scipy-1.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fc98f3eac993b9bfdd392e675dfe19850cc8c7246a8fd2b42443e506344be7d9"}, - {file = "scipy-1.5.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a785409c0fa51764766840185a34f96a0a93527a0ff0230484d33a8ed085c8f8"}, - {file = "scipy-1.5.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0a0e9a4e58a4734c2eba917f834b25b7e3b6dc333901ce7784fd31aefbd37b2f"}, - {file = "scipy-1.5.2-cp38-cp38-win32.whl", hash = "sha256:dac09281a0eacd59974e24525a3bc90fa39b4e95177e638a31b14db60d3fa806"}, - {file = "scipy-1.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:92eb04041d371fea828858e4fff182453c25ae3eaa8782d9b6c32b25857d23bc"}, - {file = "scipy-1.5.2.tar.gz", hash = "sha256:066c513d90eb3fd7567a9e150828d39111ebd88d3e924cdfc9f8ce19ab6f90c9"}, + {file = "scipy-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4f12d13ffbc16e988fa40809cbbd7a8b45bc05ff6ea0ba8e3e41f6f4db3a9e47"}, + {file = "scipy-1.5.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a254b98dbcc744c723a838c03b74a8a34c0558c9ac5c86d5561703362231107d"}, + {file = "scipy-1.5.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:368c0f69f93186309e1b4beb8e26d51dd6f5010b79264c0f1e9ca00cd92ea8c9"}, + {file = "scipy-1.5.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:4598cf03136067000855d6b44d7a1f4f46994164bcd450fb2c3d481afc25dd06"}, + {file = "scipy-1.5.4-cp36-cp36m-win32.whl", hash = "sha256:e98d49a5717369d8241d6cf33ecb0ca72deee392414118198a8e5b4c35c56340"}, + {file = "scipy-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:65923bc3809524e46fb7eb4d6346552cbb6a1ffc41be748535aa502a2e3d3389"}, + {file = "scipy-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9ad4fcddcbf5dc67619379782e6aeef41218a79e17979aaed01ed099876c0e62"}, + {file = "scipy-1.5.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f87b39f4d69cf7d7529d7b1098cb712033b17ea7714aed831b95628f483fd012"}, + {file = "scipy-1.5.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:25b241034215247481f53355e05f9e25462682b13bd9191359075682adcd9554"}, + {file = "scipy-1.5.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:fa789583fc94a7689b45834453fec095245c7e69c58561dc159b5d5277057e4c"}, + {file = "scipy-1.5.4-cp37-cp37m-win32.whl", hash = "sha256:d6d25c41a009e3c6b7e757338948d0076ee1dd1770d1c09ec131f11946883c54"}, + {file = "scipy-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:2c872de0c69ed20fb1a9b9cf6f77298b04a26f0b8720a5457be08be254366c6e"}, + {file = "scipy-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e360cb2299028d0b0d0f65a5c5e51fc16a335f1603aa2357c25766c8dab56938"}, + {file = "scipy-1.5.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3397c129b479846d7eaa18f999369a24322d008fac0782e7828fa567358c36ce"}, + {file = "scipy-1.5.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:168c45c0c32e23f613db7c9e4e780bc61982d71dcd406ead746c7c7c2f2004ce"}, + {file = "scipy-1.5.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:213bc59191da2f479984ad4ec39406bf949a99aba70e9237b916ce7547b6ef42"}, + {file = "scipy-1.5.4-cp38-cp38-win32.whl", hash = "sha256:634568a3018bc16a83cda28d4f7aed0d803dd5618facb36e977e53b2df868443"}, + {file = "scipy-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:b03c4338d6d3d299e8ca494194c0ae4f611548da59e3c038813f1a43976cb437"}, + {file = "scipy-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3d5db5d815370c28d938cf9b0809dade4acf7aba57eaf7ef733bfedc9b2474c4"}, + {file = "scipy-1.5.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:6b0ceb23560f46dd236a8ad4378fc40bad1783e997604ba845e131d6c680963e"}, + {file = "scipy-1.5.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:ed572470af2438b526ea574ff8f05e7f39b44ac37f712105e57fc4d53a6fb660"}, + {file = "scipy-1.5.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:8c8d6ca19c8497344b810b0b0344f8375af5f6bb9c98bd42e33f747417ab3f57"}, + {file = "scipy-1.5.4-cp39-cp39-win32.whl", hash = "sha256:d84cadd7d7998433334c99fa55bcba0d8b4aeff0edb123b2a1dfcface538e474"}, + {file = "scipy-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:cc1f78ebc982cd0602c9a7615d878396bec94908db67d4ecddca864d049112f2"}, + {file = "scipy-1.5.4.tar.gz", hash = "sha256:4a453d5e5689de62e5d38edf40af3f17560bfd63c9c5bd228c18c1f99afa155b"}, ] six = [ {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, @@ -1271,8 +1436,8 @@ snowballstemmer = [ {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, ] sphinx = [ - {file = "Sphinx-3.2.1-py3-none-any.whl", hash = "sha256:ce6fd7ff5b215af39e2fcd44d4a321f6694b4530b6f2b2109b64d120773faea0"}, - {file = "Sphinx-3.2.1.tar.gz", hash = "sha256:321d6d9b16fa381a5306e5a0b76cd48ffbc588e6340059a729c6fdd66087e0e8"}, + {file = "Sphinx-3.3.1-py3-none-any.whl", hash = "sha256:d4e59ad4ea55efbb3c05cde3bfc83bfc14f0c95aa95c3d75346fcce186a47960"}, + {file = "Sphinx-3.3.1.tar.gz", hash = "sha256:1e8d592225447104d1172be415bc2972bd1357e3e12fdc76edf2261105db4300"}, ] sphinxcontrib-applehelp = [ {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, @@ -1299,12 +1464,12 @@ sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib_serializinghtml-1.1.4-py2.py3-none-any.whl", hash = "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"}, ] toml = [ - {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, - {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tqdm = [ - {file = "tqdm-4.50.1-py2.py3-none-any.whl", hash = "sha256:5313148c57fcca7df562187903cf9cfa30fe1df2fe0641ea6ddb8ef9e841a137"}, - {file = "tqdm-4.50.1.tar.gz", hash = "sha256:b04bbbc52a7f1e3665eaa310f34c6ebbdf058bd3f6251fd64c6ab831817121ea"}, + {file = "tqdm-4.51.0-py2.py3-none-any.whl", hash = "sha256:9ad44aaf0fc3697c06f6e05c7cf025dd66bc7bcb7613c66d85f4464c47ac8fad"}, + {file = "tqdm-4.51.0.tar.gz", hash = "sha256:ef54779f1c09f346b2b5a8e5c61f96fbcb639929e640e59f8cf810794f406432"}, ] typed-ast = [ {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, @@ -1314,19 +1479,28 @@ typed-ast = [ {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f"}, {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298"}, {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d"}, {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, + {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c"}, + {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072"}, + {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91"}, + {file = "typed_ast-1.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d"}, + {file = "typed_ast-1.4.1-cp39-cp39-win32.whl", hash = "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395"}, + {file = "typed_ast-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c"}, {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, ] typing-extensions = [ @@ -1335,8 +1509,8 @@ typing-extensions = [ {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, ] urllib3 = [ - {file = "urllib3-1.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"}, - {file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"}, + {file = "urllib3-1.26.2-py2.py3-none-any.whl", hash = "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"}, + {file = "urllib3-1.26.2.tar.gz", hash = "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08"}, ] watchdog = [ {file = "watchdog-0.10.3.tar.gz", hash = "sha256:4214e1379d128b0588021880ccaf40317ee156d4603ac388b9adcf29165e0c04"}, @@ -1345,6 +1519,6 @@ wrapt = [ {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, ] zipp = [ - {file = "zipp-3.3.0-py3-none-any.whl", hash = "sha256:eed8ec0b8d1416b2ca33516a37a08892442f3954dee131e92cfd92d8fe3e7066"}, - {file = "zipp-3.3.0.tar.gz", hash = "sha256:64ad89efee774d1897a58607895d80789c59778ea02185dd846ac38394a8642b"}, + {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"}, + {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"}, ] diff --git a/pyproject.toml b/pyproject.toml index 24e1112a04..9856f4f1b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "manimce" -version = "0.1.0" +version = "0.1.1" description = "Animation engine for explanatory math videos." authors = ["The Manim Community Developers","3b1b "] license="MIT" @@ -33,20 +33,24 @@ scipy = "*" tqdm = "*" pydub = "*" pygments = "*" -rich = ">=4.2.1" -pycairo = "^1.20" -grpcio = "*" -grpcio-tools = "*" -watchdog = "*" -pangocffi = "^0.6.0" -pangocairocffi = "^0.3.0" +rich = "^6.0" +pycairo = "^1.19" +pangocffi = "^0.8.0" +pangocairocffi = "^0.4.0" cairocffi = "^1.1.0" +grpcio = { version = "*", optional = true } +grpcio-tools = { version = "*", optional = true } +watchdog = { version = "*", optional = true } + +[tool.poetry.extras] +js_renderer = ["grpcio","grpcio-tools","watchdog"] [tool.poetry.dev-dependencies] pytest = "^6.0" pylint = "*" guzzle_sphinx_theme = "*" recommonmark = "*" +matplotlib = "^3.3.2" [tool.poetry.dev-dependencies.black] version = "^20.8b1" @@ -60,8 +64,9 @@ markers = "slow: Mark the test as slow. Can be skipped with --skip_slow" [tool.poetry.plugins] [tool.poetry.plugins."console_scripts"] "manim" = "manim.__main__:main" -"manimcm" = "manim.__main__:main" +"manimce" = "manim.__main__:main" [build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" +requires = ["setuptools","poetry_core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + diff --git a/readme-assets/command.png b/readme-assets/command.png deleted file mode 100644 index 1fc7004ddd..0000000000 Binary files a/readme-assets/command.png and /dev/null differ diff --git a/readme-assets/pull-requests.PNG b/readme-assets/pull-requests.PNG deleted file mode 100644 index 1f77fc2576..0000000000 Binary files a/readme-assets/pull-requests.PNG and /dev/null differ diff --git a/readme-assets/windows_cairo.png b/readme-assets/windows_cairo.png deleted file mode 100644 index e9691edda9..0000000000 Binary files a/readme-assets/windows_cairo.png and /dev/null differ diff --git a/readme-assets/windows_miktex.png b/readme-assets/windows_miktex.png deleted file mode 100644 index 9189428a17..0000000000 Binary files a/readme-assets/windows_miktex.png and /dev/null differ diff --git a/readme-assets/windows_sox.png b/readme-assets/windows_sox.png deleted file mode 100644 index 27fae80d32..0000000000 Binary files a/readme-assets/windows_sox.png and /dev/null differ diff --git a/scripts/twitter_post_template.py b/scripts/twitter_post_template.py new file mode 100644 index 0000000000..fd1e4f0182 --- /dev/null +++ b/scripts/twitter_post_template.py @@ -0,0 +1,17 @@ +from manim import * +import pkg_resources + +version_num = pkg_resources.get_distribution("manimce").version + + +class TwitterScene(Scene): + def construct(self): + self.camera.background_color = "#ece6e2" + version = Tex(f"v{version_num}").to_corner(UR).set_color(BLACK) + self.add(version) + ## add twitter scene content here + + banner = ManimBanner(dark_theme=False).scale(0.3).to_corner(DR) + self.play(FadeIn(banner)) + self.play(banner.expand()) + self.play(FadeOut(banner)) diff --git a/tests/control_data/graphical_units_data/creation/DrawBorderThenFillTest.npy b/tests/control_data/graphical_units_data/creation/DrawBorderThenFillTest.npy deleted file mode 100644 index ccfc7f8aa1..0000000000 Binary files a/tests/control_data/graphical_units_data/creation/DrawBorderThenFillTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/creation/DrawBorderThenFillTest.npz b/tests/control_data/graphical_units_data/creation/DrawBorderThenFillTest.npz new file mode 100644 index 0000000000..3cb5150efe Binary files /dev/null and b/tests/control_data/graphical_units_data/creation/DrawBorderThenFillTest.npz differ diff --git a/tests/control_data/graphical_units_data/creation/FadeInFromDownTest.npy b/tests/control_data/graphical_units_data/creation/FadeInFromDownTest.npy deleted file mode 100644 index 2d2fb7cc2a..0000000000 Binary files a/tests/control_data/graphical_units_data/creation/FadeInFromDownTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/creation/FadeInFromDownTest.npz b/tests/control_data/graphical_units_data/creation/FadeInFromDownTest.npz new file mode 100644 index 0000000000..85ee318614 Binary files /dev/null and b/tests/control_data/graphical_units_data/creation/FadeInFromDownTest.npz differ diff --git a/tests/control_data/graphical_units_data/creation/FadeInFromLargeTest.npy b/tests/control_data/graphical_units_data/creation/FadeInFromLargeTest.npy deleted file mode 100644 index 2d2fb7cc2a..0000000000 Binary files a/tests/control_data/graphical_units_data/creation/FadeInFromLargeTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/creation/FadeInFromLargeTest.npz b/tests/control_data/graphical_units_data/creation/FadeInFromLargeTest.npz new file mode 100644 index 0000000000..85ee318614 Binary files /dev/null and b/tests/control_data/graphical_units_data/creation/FadeInFromLargeTest.npz differ diff --git a/tests/control_data/graphical_units_data/creation/FadeInFromTest.npy b/tests/control_data/graphical_units_data/creation/FadeInFromTest.npy deleted file mode 100644 index 2d2fb7cc2a..0000000000 Binary files a/tests/control_data/graphical_units_data/creation/FadeInFromTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/creation/FadeInFromTest.npz b/tests/control_data/graphical_units_data/creation/FadeInFromTest.npz new file mode 100644 index 0000000000..85ee318614 Binary files /dev/null and b/tests/control_data/graphical_units_data/creation/FadeInFromTest.npz differ diff --git a/tests/control_data/graphical_units_data/creation/FadeInTest.npy b/tests/control_data/graphical_units_data/creation/FadeInTest.npy deleted file mode 100644 index 2d2fb7cc2a..0000000000 Binary files a/tests/control_data/graphical_units_data/creation/FadeInTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/creation/FadeInTest.npz b/tests/control_data/graphical_units_data/creation/FadeInTest.npz new file mode 100644 index 0000000000..85ee318614 Binary files /dev/null and b/tests/control_data/graphical_units_data/creation/FadeInTest.npz differ diff --git a/tests/control_data/graphical_units_data/creation/FadeOutAndShiftTest.npy b/tests/control_data/graphical_units_data/creation/FadeOutAndShiftTest.npy deleted file mode 100644 index de3abcab7b..0000000000 Binary files a/tests/control_data/graphical_units_data/creation/FadeOutAndShiftTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/creation/FadeOutAndShiftTest.npz b/tests/control_data/graphical_units_data/creation/FadeOutAndShiftTest.npz new file mode 100644 index 0000000000..f8eb0c53bf Binary files /dev/null and b/tests/control_data/graphical_units_data/creation/FadeOutAndShiftTest.npz differ diff --git a/tests/control_data/graphical_units_data/creation/FadeOutTest.npy b/tests/control_data/graphical_units_data/creation/FadeOutTest.npy deleted file mode 100644 index de3abcab7b..0000000000 Binary files a/tests/control_data/graphical_units_data/creation/FadeOutTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/creation/FadeOutTest.npz b/tests/control_data/graphical_units_data/creation/FadeOutTest.npz new file mode 100644 index 0000000000..f8eb0c53bf Binary files /dev/null and b/tests/control_data/graphical_units_data/creation/FadeOutTest.npz differ diff --git a/tests/control_data/graphical_units_data/creation/GrowFromCenterTest.npy b/tests/control_data/graphical_units_data/creation/GrowFromCenterTest.npy deleted file mode 100644 index 2d2fb7cc2a..0000000000 Binary files a/tests/control_data/graphical_units_data/creation/GrowFromCenterTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/creation/GrowFromCenterTest.npz b/tests/control_data/graphical_units_data/creation/GrowFromCenterTest.npz new file mode 100644 index 0000000000..85ee318614 Binary files /dev/null and b/tests/control_data/graphical_units_data/creation/GrowFromCenterTest.npz differ diff --git a/tests/control_data/graphical_units_data/creation/GrowFromEdgeTest.npy b/tests/control_data/graphical_units_data/creation/GrowFromEdgeTest.npy deleted file mode 100644 index 2d2fb7cc2a..0000000000 Binary files a/tests/control_data/graphical_units_data/creation/GrowFromEdgeTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/creation/GrowFromEdgeTest.npz b/tests/control_data/graphical_units_data/creation/GrowFromEdgeTest.npz new file mode 100644 index 0000000000..85ee318614 Binary files /dev/null and b/tests/control_data/graphical_units_data/creation/GrowFromEdgeTest.npz differ diff --git a/tests/control_data/graphical_units_data/creation/GrowFromPointTest.npy b/tests/control_data/graphical_units_data/creation/GrowFromPointTest.npy deleted file mode 100644 index 2d2fb7cc2a..0000000000 Binary files a/tests/control_data/graphical_units_data/creation/GrowFromPointTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/creation/GrowFromPointTest.npz b/tests/control_data/graphical_units_data/creation/GrowFromPointTest.npz new file mode 100644 index 0000000000..85ee318614 Binary files /dev/null and b/tests/control_data/graphical_units_data/creation/GrowFromPointTest.npz differ diff --git a/tests/control_data/graphical_units_data/creation/ShowCreationTest.npy b/tests/control_data/graphical_units_data/creation/ShowCreationTest.npy deleted file mode 100644 index 2d2fb7cc2a..0000000000 Binary files a/tests/control_data/graphical_units_data/creation/ShowCreationTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/creation/ShowCreationTest.npz b/tests/control_data/graphical_units_data/creation/ShowCreationTest.npz new file mode 100644 index 0000000000..85ee318614 Binary files /dev/null and b/tests/control_data/graphical_units_data/creation/ShowCreationTest.npz differ diff --git a/tests/control_data/graphical_units_data/creation/ShrinkToCenterTest.npy b/tests/control_data/graphical_units_data/creation/ShrinkToCenterTest.npy deleted file mode 100644 index de3abcab7b..0000000000 Binary files a/tests/control_data/graphical_units_data/creation/ShrinkToCenterTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/creation/ShrinkToCenterTest.npz b/tests/control_data/graphical_units_data/creation/ShrinkToCenterTest.npz new file mode 100644 index 0000000000..f8eb0c53bf Binary files /dev/null and b/tests/control_data/graphical_units_data/creation/ShrinkToCenterTest.npz differ diff --git a/tests/control_data/graphical_units_data/creation/SpinInFromNothingTest.npy b/tests/control_data/graphical_units_data/creation/SpinInFromNothingTest.npy deleted file mode 100644 index 2d2fb7cc2a..0000000000 Binary files a/tests/control_data/graphical_units_data/creation/SpinInFromNothingTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/creation/SpinInFromNothingTest.npz b/tests/control_data/graphical_units_data/creation/SpinInFromNothingTest.npz new file mode 100644 index 0000000000..85ee318614 Binary files /dev/null and b/tests/control_data/graphical_units_data/creation/SpinInFromNothingTest.npz differ diff --git a/tests/control_data/graphical_units_data/creation/UncreateTest.npy b/tests/control_data/graphical_units_data/creation/UncreateTest.npy deleted file mode 100644 index de3abcab7b..0000000000 Binary files a/tests/control_data/graphical_units_data/creation/UncreateTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/creation/UncreateTest.npz b/tests/control_data/graphical_units_data/creation/UncreateTest.npz new file mode 100644 index 0000000000..f8eb0c53bf Binary files /dev/null and b/tests/control_data/graphical_units_data/creation/UncreateTest.npz differ diff --git a/tests/control_data/graphical_units_data/creation/WriteTest.npy b/tests/control_data/graphical_units_data/creation/WriteTest.npy deleted file mode 100644 index e835795e1f..0000000000 Binary files a/tests/control_data/graphical_units_data/creation/WriteTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/creation/WriteTest.npz b/tests/control_data/graphical_units_data/creation/WriteTest.npz new file mode 100644 index 0000000000..6e81913d02 Binary files /dev/null and b/tests/control_data/graphical_units_data/creation/WriteTest.npz differ diff --git a/tests/control_data/graphical_units_data/geometry/AnnotationDotTest.npz b/tests/control_data/graphical_units_data/geometry/AnnotationDotTest.npz new file mode 100644 index 0000000000..7db83a38f0 Binary files /dev/null and b/tests/control_data/graphical_units_data/geometry/AnnotationDotTest.npz differ diff --git a/tests/control_data/graphical_units_data/geometry/AnnularSectorTest.npy b/tests/control_data/graphical_units_data/geometry/AnnularSectorTest.npy deleted file mode 100644 index a15aa12443..0000000000 Binary files a/tests/control_data/graphical_units_data/geometry/AnnularSectorTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/geometry/AnnularSectorTest.npz b/tests/control_data/graphical_units_data/geometry/AnnularSectorTest.npz new file mode 100644 index 0000000000..cb6ef9cab2 Binary files /dev/null and b/tests/control_data/graphical_units_data/geometry/AnnularSectorTest.npz differ diff --git a/tests/control_data/graphical_units_data/geometry/AnnulusTest.npy b/tests/control_data/graphical_units_data/geometry/AnnulusTest.npy deleted file mode 100644 index 7cb120b272..0000000000 Binary files a/tests/control_data/graphical_units_data/geometry/AnnulusTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/geometry/AnnulusTest.npz b/tests/control_data/graphical_units_data/geometry/AnnulusTest.npz new file mode 100644 index 0000000000..4221bcde1e Binary files /dev/null and b/tests/control_data/graphical_units_data/geometry/AnnulusTest.npz differ diff --git a/tests/control_data/graphical_units_data/geometry/ArcBetweenPointsTest.npy b/tests/control_data/graphical_units_data/geometry/ArcBetweenPointsTest.npy deleted file mode 100644 index cc3ae52178..0000000000 Binary files a/tests/control_data/graphical_units_data/geometry/ArcBetweenPointsTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/geometry/ArcBetweenPointsTest.npz b/tests/control_data/graphical_units_data/geometry/ArcBetweenPointsTest.npz new file mode 100644 index 0000000000..383926737d Binary files /dev/null and b/tests/control_data/graphical_units_data/geometry/ArcBetweenPointsTest.npz differ diff --git a/tests/control_data/graphical_units_data/geometry/ArcTest.npy b/tests/control_data/graphical_units_data/geometry/ArcTest.npy deleted file mode 100644 index 7541b40f30..0000000000 Binary files a/tests/control_data/graphical_units_data/geometry/ArcTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/geometry/ArcTest.npz b/tests/control_data/graphical_units_data/geometry/ArcTest.npz new file mode 100644 index 0000000000..6e2b86e434 Binary files /dev/null and b/tests/control_data/graphical_units_data/geometry/ArcTest.npz differ diff --git a/tests/control_data/graphical_units_data/geometry/CircleTest.npy b/tests/control_data/graphical_units_data/geometry/CircleTest.npy deleted file mode 100644 index 78f0f70f98..0000000000 Binary files a/tests/control_data/graphical_units_data/geometry/CircleTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/geometry/CircleTest.npz b/tests/control_data/graphical_units_data/geometry/CircleTest.npz new file mode 100644 index 0000000000..5622807c53 Binary files /dev/null and b/tests/control_data/graphical_units_data/geometry/CircleTest.npz differ diff --git a/tests/control_data/graphical_units_data/geometry/CoordinatesTest.npy b/tests/control_data/graphical_units_data/geometry/CoordinatesTest.npy deleted file mode 100644 index b4632579e7..0000000000 Binary files a/tests/control_data/graphical_units_data/geometry/CoordinatesTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/geometry/CoordinatesTest.npz b/tests/control_data/graphical_units_data/geometry/CoordinatesTest.npz new file mode 100644 index 0000000000..94b122d26a Binary files /dev/null and b/tests/control_data/graphical_units_data/geometry/CoordinatesTest.npz differ diff --git a/tests/control_data/graphical_units_data/geometry/CurvedArrowTest.npy b/tests/control_data/graphical_units_data/geometry/CurvedArrowTest.npy deleted file mode 100644 index 4ce7413722..0000000000 Binary files a/tests/control_data/graphical_units_data/geometry/CurvedArrowTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/geometry/CurvedArrowTest.npz b/tests/control_data/graphical_units_data/geometry/CurvedArrowTest.npz new file mode 100644 index 0000000000..75ce061e1b Binary files /dev/null and b/tests/control_data/graphical_units_data/geometry/CurvedArrowTest.npz differ diff --git a/tests/control_data/graphical_units_data/geometry/CustomDoubleArrowTest.npy b/tests/control_data/graphical_units_data/geometry/CustomDoubleArrowTest.npy deleted file mode 100644 index e6f24e4236..0000000000 Binary files a/tests/control_data/graphical_units_data/geometry/CustomDoubleArrowTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/geometry/CustomDoubleArrowTest.npz b/tests/control_data/graphical_units_data/geometry/CustomDoubleArrowTest.npz new file mode 100644 index 0000000000..c1771dee92 Binary files /dev/null and b/tests/control_data/graphical_units_data/geometry/CustomDoubleArrowTest.npz differ diff --git a/tests/control_data/graphical_units_data/geometry/DotTest.npy b/tests/control_data/graphical_units_data/geometry/DotTest.npy deleted file mode 100644 index 5151b856c2..0000000000 Binary files a/tests/control_data/graphical_units_data/geometry/DotTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/geometry/DotTest.npz b/tests/control_data/graphical_units_data/geometry/DotTest.npz new file mode 100644 index 0000000000..5a02949626 Binary files /dev/null and b/tests/control_data/graphical_units_data/geometry/DotTest.npz differ diff --git a/tests/control_data/graphical_units_data/geometry/DoubleArrowTest.npy b/tests/control_data/graphical_units_data/geometry/DoubleArrowTest.npy deleted file mode 100644 index 68ddf55913..0000000000 Binary files a/tests/control_data/graphical_units_data/geometry/DoubleArrowTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/geometry/DoubleArrowTest.npz b/tests/control_data/graphical_units_data/geometry/DoubleArrowTest.npz new file mode 100644 index 0000000000..1a1c711ca3 Binary files /dev/null and b/tests/control_data/graphical_units_data/geometry/DoubleArrowTest.npz differ diff --git a/tests/control_data/graphical_units_data/geometry/Elbowtest.npy b/tests/control_data/graphical_units_data/geometry/Elbowtest.npy deleted file mode 100644 index 2fb455aa49..0000000000 Binary files a/tests/control_data/graphical_units_data/geometry/Elbowtest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/geometry/Elbowtest.npz b/tests/control_data/graphical_units_data/geometry/Elbowtest.npz new file mode 100644 index 0000000000..fb9aa2d245 Binary files /dev/null and b/tests/control_data/graphical_units_data/geometry/Elbowtest.npz differ diff --git a/tests/control_data/graphical_units_data/geometry/EllipseTest.npy b/tests/control_data/graphical_units_data/geometry/EllipseTest.npy deleted file mode 100644 index ad08e2d8b9..0000000000 Binary files a/tests/control_data/graphical_units_data/geometry/EllipseTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/geometry/EllipseTest.npz b/tests/control_data/graphical_units_data/geometry/EllipseTest.npz new file mode 100644 index 0000000000..e03a18dcd5 Binary files /dev/null and b/tests/control_data/graphical_units_data/geometry/EllipseTest.npz differ diff --git a/tests/control_data/graphical_units_data/geometry/LineTest.npy b/tests/control_data/graphical_units_data/geometry/LineTest.npy deleted file mode 100644 index 2dcd9b595d..0000000000 Binary files a/tests/control_data/graphical_units_data/geometry/LineTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/geometry/LineTest.npz b/tests/control_data/graphical_units_data/geometry/LineTest.npz new file mode 100644 index 0000000000..c00ad5f58c Binary files /dev/null and b/tests/control_data/graphical_units_data/geometry/LineTest.npz differ diff --git a/tests/control_data/graphical_units_data/geometry/PolygonTest.npy b/tests/control_data/graphical_units_data/geometry/PolygonTest.npy deleted file mode 100644 index a7bb8e92c7..0000000000 Binary files a/tests/control_data/graphical_units_data/geometry/PolygonTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/geometry/PolygonTest.npz b/tests/control_data/graphical_units_data/geometry/PolygonTest.npz new file mode 100644 index 0000000000..ed0f2ec9c4 Binary files /dev/null and b/tests/control_data/graphical_units_data/geometry/PolygonTest.npz differ diff --git a/tests/control_data/graphical_units_data/geometry/RectangleTest.npy b/tests/control_data/graphical_units_data/geometry/RectangleTest.npy deleted file mode 100644 index 650779206a..0000000000 Binary files a/tests/control_data/graphical_units_data/geometry/RectangleTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/geometry/RectangleTest.npz b/tests/control_data/graphical_units_data/geometry/RectangleTest.npz new file mode 100644 index 0000000000..735e00cf45 Binary files /dev/null and b/tests/control_data/graphical_units_data/geometry/RectangleTest.npz differ diff --git a/tests/control_data/graphical_units_data/geometry/RoundedRectangleTest.npy b/tests/control_data/graphical_units_data/geometry/RoundedRectangleTest.npy deleted file mode 100644 index 4c73433bf8..0000000000 Binary files a/tests/control_data/graphical_units_data/geometry/RoundedRectangleTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/geometry/RoundedRectangleTest.npz b/tests/control_data/graphical_units_data/geometry/RoundedRectangleTest.npz new file mode 100644 index 0000000000..c61254c240 Binary files /dev/null and b/tests/control_data/graphical_units_data/geometry/RoundedRectangleTest.npz differ diff --git a/tests/control_data/graphical_units_data/geometry/SectorTest.npy b/tests/control_data/graphical_units_data/geometry/SectorTest.npy deleted file mode 100644 index c5ea54e633..0000000000 Binary files a/tests/control_data/graphical_units_data/geometry/SectorTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/geometry/SectorTest.npz b/tests/control_data/graphical_units_data/geometry/SectorTest.npz new file mode 100644 index 0000000000..3e44cfb750 Binary files /dev/null and b/tests/control_data/graphical_units_data/geometry/SectorTest.npz differ diff --git a/tests/control_data/graphical_units_data/geometry/VectorTest.npy b/tests/control_data/graphical_units_data/geometry/VectorTest.npy deleted file mode 100644 index 70c3ab7eba..0000000000 Binary files a/tests/control_data/graphical_units_data/geometry/VectorTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/geometry/VectorTest.npz b/tests/control_data/graphical_units_data/geometry/VectorTest.npz new file mode 100644 index 0000000000..c8b465dbf3 Binary files /dev/null and b/tests/control_data/graphical_units_data/geometry/VectorTest.npz differ diff --git a/tests/control_data/graphical_units_data/graph/AxesTest.npz b/tests/control_data/graphical_units_data/graph/AxesTest.npz new file mode 100644 index 0000000000..51d28b7d64 Binary files /dev/null and b/tests/control_data/graphical_units_data/graph/AxesTest.npz differ diff --git a/tests/control_data/graphical_units_data/graph/PlotFunctions.npy b/tests/control_data/graphical_units_data/graph/PlotFunctions.npy deleted file mode 100644 index 9b1079088a..0000000000 Binary files a/tests/control_data/graphical_units_data/graph/PlotFunctions.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/graph/PlotFunctions.npz b/tests/control_data/graphical_units_data/graph/PlotFunctions.npz new file mode 100644 index 0000000000..dfddd05f28 Binary files /dev/null and b/tests/control_data/graphical_units_data/graph/PlotFunctions.npz differ diff --git a/tests/control_data/graphical_units_data/img_and_svg/ImageMobjectTest.npz b/tests/control_data/graphical_units_data/img_and_svg/ImageMobjectTest.npz new file mode 100644 index 0000000000..db7f87c61b Binary files /dev/null and b/tests/control_data/graphical_units_data/img_and_svg/ImageMobjectTest.npz differ diff --git a/tests/control_data/graphical_units_data/img_and_svg/SVGMobjectTest.npz b/tests/control_data/graphical_units_data/img_and_svg/SVGMobjectTest.npz new file mode 100644 index 0000000000..e18a32c5f9 Binary files /dev/null and b/tests/control_data/graphical_units_data/img_and_svg/SVGMobjectTest.npz differ diff --git a/tests/control_data/graphical_units_data/indication/ApplyWaveTest.npy b/tests/control_data/graphical_units_data/indication/ApplyWaveTest.npy deleted file mode 100644 index 2d2fb7cc2a..0000000000 Binary files a/tests/control_data/graphical_units_data/indication/ApplyWaveTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/indication/ApplyWaveTest.npz b/tests/control_data/graphical_units_data/indication/ApplyWaveTest.npz new file mode 100644 index 0000000000..85ee318614 Binary files /dev/null and b/tests/control_data/graphical_units_data/indication/ApplyWaveTest.npz differ diff --git a/tests/control_data/graphical_units_data/indication/CircleIndicateTest.npy b/tests/control_data/graphical_units_data/indication/CircleIndicateTest.npy deleted file mode 100644 index 2d2fb7cc2a..0000000000 Binary files a/tests/control_data/graphical_units_data/indication/CircleIndicateTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/indication/CircleIndicateTest.npz b/tests/control_data/graphical_units_data/indication/CircleIndicateTest.npz new file mode 100644 index 0000000000..85ee318614 Binary files /dev/null and b/tests/control_data/graphical_units_data/indication/CircleIndicateTest.npz differ diff --git a/tests/control_data/graphical_units_data/indication/FlashTest.npy b/tests/control_data/graphical_units_data/indication/FlashTest.npy deleted file mode 100644 index 2d2fb7cc2a..0000000000 Binary files a/tests/control_data/graphical_units_data/indication/FlashTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/indication/FlashTest.npz b/tests/control_data/graphical_units_data/indication/FlashTest.npz new file mode 100644 index 0000000000..85ee318614 Binary files /dev/null and b/tests/control_data/graphical_units_data/indication/FlashTest.npz differ diff --git a/tests/control_data/graphical_units_data/indication/FocusOnTest.npy b/tests/control_data/graphical_units_data/indication/FocusOnTest.npy deleted file mode 100644 index 2d2fb7cc2a..0000000000 Binary files a/tests/control_data/graphical_units_data/indication/FocusOnTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/indication/FocusOnTest.npz b/tests/control_data/graphical_units_data/indication/FocusOnTest.npz new file mode 100644 index 0000000000..85ee318614 Binary files /dev/null and b/tests/control_data/graphical_units_data/indication/FocusOnTest.npz differ diff --git a/tests/control_data/graphical_units_data/indication/IndicateTest.npy b/tests/control_data/graphical_units_data/indication/IndicateTest.npy deleted file mode 100644 index 2d2fb7cc2a..0000000000 Binary files a/tests/control_data/graphical_units_data/indication/IndicateTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/indication/IndicateTest.npz b/tests/control_data/graphical_units_data/indication/IndicateTest.npz new file mode 100644 index 0000000000..85ee318614 Binary files /dev/null and b/tests/control_data/graphical_units_data/indication/IndicateTest.npz differ diff --git a/tests/control_data/graphical_units_data/indication/ShowCreationThenDestructionTest.npy b/tests/control_data/graphical_units_data/indication/ShowCreationThenDestructionTest.npy deleted file mode 100644 index de3abcab7b..0000000000 Binary files a/tests/control_data/graphical_units_data/indication/ShowCreationThenDestructionTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/indication/ShowCreationThenDestructionTest.npz b/tests/control_data/graphical_units_data/indication/ShowCreationThenDestructionTest.npz new file mode 100644 index 0000000000..f8eb0c53bf Binary files /dev/null and b/tests/control_data/graphical_units_data/indication/ShowCreationThenDestructionTest.npz differ diff --git a/tests/control_data/graphical_units_data/indication/ShowCreationThenFadeOutTest.npy b/tests/control_data/graphical_units_data/indication/ShowCreationThenFadeOutTest.npy deleted file mode 100644 index de3abcab7b..0000000000 Binary files a/tests/control_data/graphical_units_data/indication/ShowCreationThenFadeOutTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/indication/ShowCreationThenFadeOutTest.npz b/tests/control_data/graphical_units_data/indication/ShowCreationThenFadeOutTest.npz new file mode 100644 index 0000000000..f8eb0c53bf Binary files /dev/null and b/tests/control_data/graphical_units_data/indication/ShowCreationThenFadeOutTest.npz differ diff --git a/tests/control_data/graphical_units_data/indication/ShowPassingFlashAroundTest.npy b/tests/control_data/graphical_units_data/indication/ShowPassingFlashAroundTest.npy deleted file mode 100644 index de3abcab7b..0000000000 Binary files a/tests/control_data/graphical_units_data/indication/ShowPassingFlashAroundTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/indication/ShowPassingFlashAroundTest.npz b/tests/control_data/graphical_units_data/indication/ShowPassingFlashAroundTest.npz new file mode 100644 index 0000000000..f8eb0c53bf Binary files /dev/null and b/tests/control_data/graphical_units_data/indication/ShowPassingFlashAroundTest.npz differ diff --git a/tests/control_data/graphical_units_data/indication/ShowPassingFlashTest.npy b/tests/control_data/graphical_units_data/indication/ShowPassingFlashTest.npy deleted file mode 100644 index de3abcab7b..0000000000 Binary files a/tests/control_data/graphical_units_data/indication/ShowPassingFlashTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/indication/ShowPassingFlashTest.npz b/tests/control_data/graphical_units_data/indication/ShowPassingFlashTest.npz new file mode 100644 index 0000000000..f8eb0c53bf Binary files /dev/null and b/tests/control_data/graphical_units_data/indication/ShowPassingFlashTest.npz differ diff --git a/tests/control_data/graphical_units_data/indication/TurnInsideOutTest.npy b/tests/control_data/graphical_units_data/indication/TurnInsideOutTest.npy deleted file mode 100644 index 2d2fb7cc2a..0000000000 Binary files a/tests/control_data/graphical_units_data/indication/TurnInsideOutTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/indication/TurnInsideOutTest.npz b/tests/control_data/graphical_units_data/indication/TurnInsideOutTest.npz new file mode 100644 index 0000000000..85ee318614 Binary files /dev/null and b/tests/control_data/graphical_units_data/indication/TurnInsideOutTest.npz differ diff --git a/tests/control_data/graphical_units_data/indication/WiggleOutThenInTest.npy b/tests/control_data/graphical_units_data/indication/WiggleOutThenInTest.npy deleted file mode 100644 index 2d2fb7cc2a..0000000000 Binary files a/tests/control_data/graphical_units_data/indication/WiggleOutThenInTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/indication/WiggleOutThenInTest.npz b/tests/control_data/graphical_units_data/indication/WiggleOutThenInTest.npz new file mode 100644 index 0000000000..85ee318614 Binary files /dev/null and b/tests/control_data/graphical_units_data/indication/WiggleOutThenInTest.npz differ diff --git a/tests/control_data/graphical_units_data/movements/HomotopyTest.npy b/tests/control_data/graphical_units_data/movements/HomotopyTest.npy deleted file mode 100644 index 60af6c7b6b..0000000000 Binary files a/tests/control_data/graphical_units_data/movements/HomotopyTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/movements/HomotopyTest.npz b/tests/control_data/graphical_units_data/movements/HomotopyTest.npz new file mode 100644 index 0000000000..81a202818d Binary files /dev/null and b/tests/control_data/graphical_units_data/movements/HomotopyTest.npz differ diff --git a/tests/control_data/graphical_units_data/movements/MoveAlongPathTest.npy b/tests/control_data/graphical_units_data/movements/MoveAlongPathTest.npy deleted file mode 100644 index a531aa13ba..0000000000 Binary files a/tests/control_data/graphical_units_data/movements/MoveAlongPathTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/movements/MoveAlongPathTest.npz b/tests/control_data/graphical_units_data/movements/MoveAlongPathTest.npz new file mode 100644 index 0000000000..f326c7640c Binary files /dev/null and b/tests/control_data/graphical_units_data/movements/MoveAlongPathTest.npz differ diff --git a/tests/control_data/graphical_units_data/movements/MoveToTest.npy b/tests/control_data/graphical_units_data/movements/MoveToTest.npy deleted file mode 100644 index 4f22b1523e..0000000000 Binary files a/tests/control_data/graphical_units_data/movements/MoveToTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/movements/MoveToTest.npz b/tests/control_data/graphical_units_data/movements/MoveToTest.npz new file mode 100644 index 0000000000..50f3af6872 Binary files /dev/null and b/tests/control_data/graphical_units_data/movements/MoveToTest.npz differ diff --git a/tests/control_data/graphical_units_data/movements/PhaseFlowTest.npy b/tests/control_data/graphical_units_data/movements/PhaseFlowTest.npy deleted file mode 100644 index cbc3eb5974..0000000000 Binary files a/tests/control_data/graphical_units_data/movements/PhaseFlowTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/movements/PhaseFlowTest.npz b/tests/control_data/graphical_units_data/movements/PhaseFlowTest.npz new file mode 100644 index 0000000000..2d84c60a78 Binary files /dev/null and b/tests/control_data/graphical_units_data/movements/PhaseFlowTest.npz differ diff --git a/tests/control_data/graphical_units_data/movements/RotateTest.npy b/tests/control_data/graphical_units_data/movements/RotateTest.npy deleted file mode 100644 index 2d2fb7cc2a..0000000000 Binary files a/tests/control_data/graphical_units_data/movements/RotateTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/movements/RotateTest.npz b/tests/control_data/graphical_units_data/movements/RotateTest.npz new file mode 100644 index 0000000000..85ee318614 Binary files /dev/null and b/tests/control_data/graphical_units_data/movements/RotateTest.npz differ diff --git a/tests/control_data/graphical_units_data/movements/ShiftTest.npy b/tests/control_data/graphical_units_data/movements/ShiftTest.npy deleted file mode 100644 index 67bb448cb9..0000000000 Binary files a/tests/control_data/graphical_units_data/movements/ShiftTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/movements/ShiftTest.npz b/tests/control_data/graphical_units_data/movements/ShiftTest.npz new file mode 100644 index 0000000000..235cba9c68 Binary files /dev/null and b/tests/control_data/graphical_units_data/movements/ShiftTest.npz differ diff --git a/tests/control_data/graphical_units_data/sample_scenes/BasicScene.npy b/tests/control_data/graphical_units_data/sample_scenes/BasicScene.npy deleted file mode 100644 index 2d2fb7cc2a..0000000000 Binary files a/tests/control_data/graphical_units_data/sample_scenes/BasicScene.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/sample_scenes/BasicScene.npz b/tests/control_data/graphical_units_data/sample_scenes/BasicScene.npz new file mode 100644 index 0000000000..85ee318614 Binary files /dev/null and b/tests/control_data/graphical_units_data/sample_scenes/BasicScene.npz differ diff --git a/tests/control_data/graphical_units_data/sample_scenes/BasicTex.npy b/tests/control_data/graphical_units_data/sample_scenes/BasicTex.npy deleted file mode 100644 index 188dd5e2d4..0000000000 Binary files a/tests/control_data/graphical_units_data/sample_scenes/BasicTex.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/sample_scenes/BasicTex.npz b/tests/control_data/graphical_units_data/sample_scenes/BasicTex.npz new file mode 100644 index 0000000000..978084e593 Binary files /dev/null and b/tests/control_data/graphical_units_data/sample_scenes/BasicTex.npz differ diff --git a/tests/control_data/graphical_units_data/sample_scenes/GeometryScene.npy b/tests/control_data/graphical_units_data/sample_scenes/GeometryScene.npy deleted file mode 100644 index 1f0353683a..0000000000 Binary files a/tests/control_data/graphical_units_data/sample_scenes/GeometryScene.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/sample_scenes/GeometryScene.npz b/tests/control_data/graphical_units_data/sample_scenes/GeometryScene.npz new file mode 100644 index 0000000000..6b2241107e Binary files /dev/null and b/tests/control_data/graphical_units_data/sample_scenes/GeometryScene.npz differ diff --git a/tests/control_data/graphical_units_data/threed/AmbientCameraMoveTest.npy b/tests/control_data/graphical_units_data/threed/AmbientCameraMoveTest.npy deleted file mode 100644 index 001d7c56fa..0000000000 Binary files a/tests/control_data/graphical_units_data/threed/AmbientCameraMoveTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/threed/AmbientCameraMoveTest.npz b/tests/control_data/graphical_units_data/threed/AmbientCameraMoveTest.npz new file mode 100644 index 0000000000..c174c8ec9b Binary files /dev/null and b/tests/control_data/graphical_units_data/threed/AmbientCameraMoveTest.npz differ diff --git a/tests/control_data/graphical_units_data/threed/AxesTest.npy b/tests/control_data/graphical_units_data/threed/AxesTest.npy deleted file mode 100644 index 0cb1f4e3e8..0000000000 Binary files a/tests/control_data/graphical_units_data/threed/AxesTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/threed/AxesTest.npz b/tests/control_data/graphical_units_data/threed/AxesTest.npz new file mode 100644 index 0000000000..49df37b47d Binary files /dev/null and b/tests/control_data/graphical_units_data/threed/AxesTest.npz differ diff --git a/tests/control_data/graphical_units_data/threed/CameraMoveTest.npy b/tests/control_data/graphical_units_data/threed/CameraMoveTest.npy deleted file mode 100644 index 515e608e07..0000000000 Binary files a/tests/control_data/graphical_units_data/threed/CameraMoveTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/threed/CameraMoveTest.npz b/tests/control_data/graphical_units_data/threed/CameraMoveTest.npz new file mode 100644 index 0000000000..4d7054c930 Binary files /dev/null and b/tests/control_data/graphical_units_data/threed/CameraMoveTest.npz differ diff --git a/tests/control_data/graphical_units_data/threed/CubeTest.npy b/tests/control_data/graphical_units_data/threed/CubeTest.npy deleted file mode 100644 index 31df75ae63..0000000000 Binary files a/tests/control_data/graphical_units_data/threed/CubeTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/threed/CubeTest.npz b/tests/control_data/graphical_units_data/threed/CubeTest.npz new file mode 100644 index 0000000000..859acd741b Binary files /dev/null and b/tests/control_data/graphical_units_data/threed/CubeTest.npz differ diff --git a/tests/control_data/graphical_units_data/threed/FixedInFrameMObjectTest.npz b/tests/control_data/graphical_units_data/threed/FixedInFrameMObjectTest.npz new file mode 100644 index 0000000000..c14f3c6101 Binary files /dev/null and b/tests/control_data/graphical_units_data/threed/FixedInFrameMObjectTest.npz differ diff --git a/tests/control_data/graphical_units_data/threed/SphereTest.npy b/tests/control_data/graphical_units_data/threed/SphereTest.npy deleted file mode 100644 index afb6a1d984..0000000000 Binary files a/tests/control_data/graphical_units_data/threed/SphereTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/threed/SphereTest.npz b/tests/control_data/graphical_units_data/threed/SphereTest.npz new file mode 100644 index 0000000000..85c81c82a9 Binary files /dev/null and b/tests/control_data/graphical_units_data/threed/SphereTest.npz differ diff --git a/tests/control_data/graphical_units_data/transform/ApplyComplexFunctionTest.npy b/tests/control_data/graphical_units_data/transform/ApplyComplexFunctionTest.npy deleted file mode 100644 index ebd7959608..0000000000 Binary files a/tests/control_data/graphical_units_data/transform/ApplyComplexFunctionTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/transform/ApplyComplexFunctionTest.npz b/tests/control_data/graphical_units_data/transform/ApplyComplexFunctionTest.npz new file mode 100644 index 0000000000..1706657f20 Binary files /dev/null and b/tests/control_data/graphical_units_data/transform/ApplyComplexFunctionTest.npz differ diff --git a/tests/control_data/graphical_units_data/transform/ApplyFunctionTest.npy b/tests/control_data/graphical_units_data/transform/ApplyFunctionTest.npy deleted file mode 100644 index a22156c7dc..0000000000 Binary files a/tests/control_data/graphical_units_data/transform/ApplyFunctionTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/transform/ApplyFunctionTest.npz b/tests/control_data/graphical_units_data/transform/ApplyFunctionTest.npz new file mode 100644 index 0000000000..9ddcb533d4 Binary files /dev/null and b/tests/control_data/graphical_units_data/transform/ApplyFunctionTest.npz differ diff --git a/tests/control_data/graphical_units_data/transform/ApplyMatrixTest.npy b/tests/control_data/graphical_units_data/transform/ApplyMatrixTest.npy deleted file mode 100644 index e3f12518a8..0000000000 Binary files a/tests/control_data/graphical_units_data/transform/ApplyMatrixTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/transform/ApplyMatrixTest.npz b/tests/control_data/graphical_units_data/transform/ApplyMatrixTest.npz new file mode 100644 index 0000000000..8a50b9dc6c Binary files /dev/null and b/tests/control_data/graphical_units_data/transform/ApplyMatrixTest.npz differ diff --git a/tests/control_data/graphical_units_data/transform/ApplyPointwiseFunctionTest.npy b/tests/control_data/graphical_units_data/transform/ApplyPointwiseFunctionTest.npy deleted file mode 100644 index de3abcab7b..0000000000 Binary files a/tests/control_data/graphical_units_data/transform/ApplyPointwiseFunctionTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/transform/ApplyPointwiseFunctionTest.npz b/tests/control_data/graphical_units_data/transform/ApplyPointwiseFunctionTest.npz new file mode 100644 index 0000000000..f8eb0c53bf Binary files /dev/null and b/tests/control_data/graphical_units_data/transform/ApplyPointwiseFunctionTest.npz differ diff --git a/tests/control_data/graphical_units_data/transform/ClockwiseTransformTest.npy b/tests/control_data/graphical_units_data/transform/ClockwiseTransformTest.npy deleted file mode 100644 index 78f0f70f98..0000000000 Binary files a/tests/control_data/graphical_units_data/transform/ClockwiseTransformTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/transform/ClockwiseTransformTest.npz b/tests/control_data/graphical_units_data/transform/ClockwiseTransformTest.npz new file mode 100644 index 0000000000..5622807c53 Binary files /dev/null and b/tests/control_data/graphical_units_data/transform/ClockwiseTransformTest.npz differ diff --git a/tests/control_data/graphical_units_data/transform/CounterclockwiseTransformTest.npy b/tests/control_data/graphical_units_data/transform/CounterclockwiseTransformTest.npy deleted file mode 100644 index 78f0f70f98..0000000000 Binary files a/tests/control_data/graphical_units_data/transform/CounterclockwiseTransformTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/transform/CounterclockwiseTransformTest.npz b/tests/control_data/graphical_units_data/transform/CounterclockwiseTransformTest.npz new file mode 100644 index 0000000000..5622807c53 Binary files /dev/null and b/tests/control_data/graphical_units_data/transform/CounterclockwiseTransformTest.npz differ diff --git a/tests/control_data/graphical_units_data/transform/CyclicReplaceTest.npy b/tests/control_data/graphical_units_data/transform/CyclicReplaceTest.npy deleted file mode 100644 index 0cab724b1b..0000000000 Binary files a/tests/control_data/graphical_units_data/transform/CyclicReplaceTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/transform/CyclicReplaceTest.npz b/tests/control_data/graphical_units_data/transform/CyclicReplaceTest.npz new file mode 100644 index 0000000000..0b953aeb82 Binary files /dev/null and b/tests/control_data/graphical_units_data/transform/CyclicReplaceTest.npz differ diff --git a/tests/control_data/graphical_units_data/transform/FadeInAndOutTest.npz b/tests/control_data/graphical_units_data/transform/FadeInAndOutTest.npz new file mode 100644 index 0000000000..881f1fbcdb Binary files /dev/null and b/tests/control_data/graphical_units_data/transform/FadeInAndOutTest.npz differ diff --git a/tests/control_data/graphical_units_data/transform/FadeToColortTest.npy b/tests/control_data/graphical_units_data/transform/FadeToColortTest.npy deleted file mode 100644 index f303647c02..0000000000 Binary files a/tests/control_data/graphical_units_data/transform/FadeToColortTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/transform/FadeToColortTest.npz b/tests/control_data/graphical_units_data/transform/FadeToColortTest.npz new file mode 100644 index 0000000000..5fd21fb753 Binary files /dev/null and b/tests/control_data/graphical_units_data/transform/FadeToColortTest.npz differ diff --git a/tests/control_data/graphical_units_data/transform/MoveToTargetTest.npy b/tests/control_data/graphical_units_data/transform/MoveToTargetTest.npy deleted file mode 100644 index e16b35b0a5..0000000000 Binary files a/tests/control_data/graphical_units_data/transform/MoveToTargetTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/transform/MoveToTargetTest.npz b/tests/control_data/graphical_units_data/transform/MoveToTargetTest.npz new file mode 100644 index 0000000000..f0b46559e1 Binary files /dev/null and b/tests/control_data/graphical_units_data/transform/MoveToTargetTest.npz differ diff --git a/tests/control_data/graphical_units_data/transform/RestoreTest.npy b/tests/control_data/graphical_units_data/transform/RestoreTest.npy deleted file mode 100644 index 78f0f70f98..0000000000 Binary files a/tests/control_data/graphical_units_data/transform/RestoreTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/transform/RestoreTest.npz b/tests/control_data/graphical_units_data/transform/RestoreTest.npz new file mode 100644 index 0000000000..5622807c53 Binary files /dev/null and b/tests/control_data/graphical_units_data/transform/RestoreTest.npz differ diff --git a/tests/control_data/graphical_units_data/transform/ScaleInPlaceTest.npy b/tests/control_data/graphical_units_data/transform/ScaleInPlaceTest.npy deleted file mode 100644 index 50da222f5c..0000000000 Binary files a/tests/control_data/graphical_units_data/transform/ScaleInPlaceTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/transform/ScaleInPlaceTest.npz b/tests/control_data/graphical_units_data/transform/ScaleInPlaceTest.npz new file mode 100644 index 0000000000..c981716fc1 Binary files /dev/null and b/tests/control_data/graphical_units_data/transform/ScaleInPlaceTest.npz differ diff --git a/tests/control_data/graphical_units_data/transform/ShrinkToCenterTest.npy b/tests/control_data/graphical_units_data/transform/ShrinkToCenterTest.npy deleted file mode 100644 index de3abcab7b..0000000000 Binary files a/tests/control_data/graphical_units_data/transform/ShrinkToCenterTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/transform/ShrinkToCenterTest.npz b/tests/control_data/graphical_units_data/transform/ShrinkToCenterTest.npz new file mode 100644 index 0000000000..f8eb0c53bf Binary files /dev/null and b/tests/control_data/graphical_units_data/transform/ShrinkToCenterTest.npz differ diff --git a/tests/control_data/graphical_units_data/transform/TransformFromCopyTest.npy b/tests/control_data/graphical_units_data/transform/TransformFromCopyTest.npy deleted file mode 100644 index 78f0f70f98..0000000000 Binary files a/tests/control_data/graphical_units_data/transform/TransformFromCopyTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/transform/TransformFromCopyTest.npz b/tests/control_data/graphical_units_data/transform/TransformFromCopyTest.npz new file mode 100644 index 0000000000..5622807c53 Binary files /dev/null and b/tests/control_data/graphical_units_data/transform/TransformFromCopyTest.npz differ diff --git a/tests/control_data/graphical_units_data/transform/TransformTest.npy b/tests/control_data/graphical_units_data/transform/TransformTest.npy deleted file mode 100644 index 78f0f70f98..0000000000 Binary files a/tests/control_data/graphical_units_data/transform/TransformTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/transform/TransformTest.npz b/tests/control_data/graphical_units_data/transform/TransformTest.npz new file mode 100644 index 0000000000..5622807c53 Binary files /dev/null and b/tests/control_data/graphical_units_data/transform/TransformTest.npz differ diff --git a/tests/control_data/graphical_units_data/updaters/UpdaterTest.npy b/tests/control_data/graphical_units_data/updaters/UpdaterTest.npy deleted file mode 100644 index b0b48565c5..0000000000 Binary files a/tests/control_data/graphical_units_data/updaters/UpdaterTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/updaters/UpdaterTest.npz b/tests/control_data/graphical_units_data/updaters/UpdaterTest.npz new file mode 100644 index 0000000000..f0eba4e757 Binary files /dev/null and b/tests/control_data/graphical_units_data/updaters/UpdaterTest.npz differ diff --git a/tests/control_data/graphical_units_data/updaters/ValueTrackerTest.npy b/tests/control_data/graphical_units_data/updaters/ValueTrackerTest.npy deleted file mode 100644 index de3abcab7b..0000000000 Binary files a/tests/control_data/graphical_units_data/updaters/ValueTrackerTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/updaters/ValueTrackerTest.npz b/tests/control_data/graphical_units_data/updaters/ValueTrackerTest.npz new file mode 100644 index 0000000000..f8eb0c53bf Binary files /dev/null and b/tests/control_data/graphical_units_data/updaters/ValueTrackerTest.npz differ diff --git a/tests/control_data/graphical_units_data/writing/DarwinTexMobjectTest.npy b/tests/control_data/graphical_units_data/writing/DarwinTexMobjectTest.npy deleted file mode 100644 index 83cde8237d..0000000000 Binary files a/tests/control_data/graphical_units_data/writing/DarwinTexMobjectTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/writing/DarwinTexMobjectTest.npz b/tests/control_data/graphical_units_data/writing/DarwinTexMobjectTest.npz new file mode 100644 index 0000000000..131df64cc9 Binary files /dev/null and b/tests/control_data/graphical_units_data/writing/DarwinTexMobjectTest.npz differ diff --git a/tests/control_data/graphical_units_data/writing/DarwinTextMobjectTest.npy b/tests/control_data/graphical_units_data/writing/DarwinTextMobjectTest.npy deleted file mode 100644 index f9215108c1..0000000000 Binary files a/tests/control_data/graphical_units_data/writing/DarwinTextMobjectTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/writing/DarwinTextMobjectTest.npz b/tests/control_data/graphical_units_data/writing/DarwinTextMobjectTest.npz new file mode 100644 index 0000000000..0a3587e836 Binary files /dev/null and b/tests/control_data/graphical_units_data/writing/DarwinTextMobjectTest.npz differ diff --git a/tests/control_data/graphical_units_data/writing/DarwinTextTest.npy b/tests/control_data/graphical_units_data/writing/DarwinTextTest.npy deleted file mode 100644 index b8104af031..0000000000 Binary files a/tests/control_data/graphical_units_data/writing/DarwinTextTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/writing/DarwinTextTest.npz b/tests/control_data/graphical_units_data/writing/DarwinTextTest.npz new file mode 100644 index 0000000000..05c861d9ec Binary files /dev/null and b/tests/control_data/graphical_units_data/writing/DarwinTextTest.npz differ diff --git a/tests/control_data/graphical_units_data/writing/LinuxTexMobjectTest.npy b/tests/control_data/graphical_units_data/writing/LinuxTexMobjectTest.npy deleted file mode 100644 index 804bc21353..0000000000 Binary files a/tests/control_data/graphical_units_data/writing/LinuxTexMobjectTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/writing/LinuxTexMobjectTest.npz b/tests/control_data/graphical_units_data/writing/LinuxTexMobjectTest.npz new file mode 100644 index 0000000000..a5d33772ff Binary files /dev/null and b/tests/control_data/graphical_units_data/writing/LinuxTexMobjectTest.npz differ diff --git a/tests/control_data/graphical_units_data/writing/LinuxTextMobjectTest.npy b/tests/control_data/graphical_units_data/writing/LinuxTextMobjectTest.npy deleted file mode 100644 index 6ef740e76d..0000000000 Binary files a/tests/control_data/graphical_units_data/writing/LinuxTextMobjectTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/writing/LinuxTextMobjectTest.npz b/tests/control_data/graphical_units_data/writing/LinuxTextMobjectTest.npz new file mode 100644 index 0000000000..d5dc8b1fa8 Binary files /dev/null and b/tests/control_data/graphical_units_data/writing/LinuxTextMobjectTest.npz differ diff --git a/tests/control_data/graphical_units_data/writing/LinuxTextTest.npy b/tests/control_data/graphical_units_data/writing/LinuxTextTest.npy deleted file mode 100644 index ef3a4015c9..0000000000 Binary files a/tests/control_data/graphical_units_data/writing/LinuxTextTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/writing/LinuxTextTest.npz b/tests/control_data/graphical_units_data/writing/LinuxTextTest.npz new file mode 100644 index 0000000000..8d84013b69 Binary files /dev/null and b/tests/control_data/graphical_units_data/writing/LinuxTextTest.npz differ diff --git a/tests/control_data/graphical_units_data/writing/WindowsTexMobjectTest.npy b/tests/control_data/graphical_units_data/writing/WindowsTexMobjectTest.npy deleted file mode 100644 index 83cde8237d..0000000000 Binary files a/tests/control_data/graphical_units_data/writing/WindowsTexMobjectTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/writing/WindowsTexMobjectTest.npz b/tests/control_data/graphical_units_data/writing/WindowsTexMobjectTest.npz new file mode 100644 index 0000000000..131df64cc9 Binary files /dev/null and b/tests/control_data/graphical_units_data/writing/WindowsTexMobjectTest.npz differ diff --git a/tests/control_data/graphical_units_data/writing/WindowsTextMobjectTest.npy b/tests/control_data/graphical_units_data/writing/WindowsTextMobjectTest.npy deleted file mode 100644 index f9215108c1..0000000000 Binary files a/tests/control_data/graphical_units_data/writing/WindowsTextMobjectTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/writing/WindowsTextMobjectTest.npz b/tests/control_data/graphical_units_data/writing/WindowsTextMobjectTest.npz new file mode 100644 index 0000000000..0a3587e836 Binary files /dev/null and b/tests/control_data/graphical_units_data/writing/WindowsTextMobjectTest.npz differ diff --git a/tests/control_data/graphical_units_data/writing/WindowsTextTest.npy b/tests/control_data/graphical_units_data/writing/WindowsTextTest.npy deleted file mode 100644 index b3e2559e23..0000000000 Binary files a/tests/control_data/graphical_units_data/writing/WindowsTextTest.npy and /dev/null differ diff --git a/tests/control_data/graphical_units_data/writing/WindowsTextTest.npz b/tests/control_data/graphical_units_data/writing/WindowsTextTest.npz new file mode 100644 index 0000000000..087fc8b76c Binary files /dev/null and b/tests/control_data/graphical_units_data/writing/WindowsTextTest.npz differ diff --git a/tests/control_data/logs_data/BasicSceneLoggingTest.txt b/tests/control_data/logs_data/BasicSceneLoggingTest.txt index edee71e9ed..ff9db9c0ae 100644 --- a/tests/control_data/logs_data/BasicSceneLoggingTest.txt +++ b/tests/control_data/logs_data/BasicSceneLoggingTest.txt @@ -1,4 +1,4 @@ -{"levelname": "INFO", "module": "config", "message": "Log file will be saved in <>"} +{"levelname": "INFO", "module": "logger_utils", "message": "Log file will be saved in <>"} {"levelname": "DEBUG", "module": "hashing", "message": "Hashing ..."} {"levelname": "DEBUG", "module": "hashing", "message": "Hashing done in <> s."} {"levelname": "DEBUG", "module": "hashing", "message": "Hash generated : <>"} diff --git a/tests/helpers/graphical_units.py b/tests/helpers/graphical_units.py index 94880e92e9..47e7c68594 100644 --- a/tests/helpers/graphical_units.py +++ b/tests/helpers/graphical_units.py @@ -5,8 +5,7 @@ import tempfile import numpy as np -import manim -from manim import config, file_writer_config, logger +from manim import config, logger def set_test_scene(scene_object, module_name): @@ -16,39 +15,42 @@ def set_test_scene(scene_object, module_name): Parameters ---------- scene_object : :class:`~.Scene` - The scene with wich we want to set up a new test. + The scene with which we want to set up a new test. module_name : :class:`str` - The name of the module in which the functionnality tested is contained. For example, 'Write' is contained in the module 'creation'. This will be used in the folder architecture - of '/tests_data'. + The name of the module in which the functionality tested is contained. For example, ``Write`` is contained in the module ``creation``. This will be used in the folder architecture + of ``/tests_data``. Examples -------- Normal usage:: - set_test_scene(DotTest, "geometry") - """ - file_writer_config["skip_animations"] = True - file_writer_config["write_to_movie"] = False - file_writer_config["disable_caching"] = True + """ + config["write_to_movie"] = False + config["disable_caching"] = True + config["save_last_frame"] = True config["pixel_height"] = 480 config["pixel_width"] = 854 config["frame_rate"] = 15 with tempfile.TemporaryDirectory() as tmpdir: os.makedirs(os.path.join(tmpdir, "tex")) - file_writer_config["text_dir"] = os.path.join(tmpdir, "text") - file_writer_config["tex_dir"] = os.path.join(tmpdir, "tex") - scene = scene_object() + config["text_dir"] = os.path.join(tmpdir, "text") + config["tex_dir"] = os.path.join(tmpdir, "tex") + scene = scene_object(skip_animations=True) + scene.render() data = scene.renderer.get_frame() + assert not np.all( + data == np.array([0, 0, 0, 255]) + ), f"Control data generated for {str(scene)} only contains empty pixels." + assert data.shape == (480, 854, 4) tests_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) path_control_data = os.path.join( tests_directory, "control_data", "graphical_units_data" ) - print(path_control_data) path = os.path.join(path_control_data, module_name) if not os.path.isdir(path): os.makedirs(path) - np.save(os.path.join(path, str(scene)), data) + np.savez_compressed(os.path.join(path, str(scene)), frame_data=data) logger.info(f"Test data for {str(scene)} saved in {path}\n") diff --git a/tests/helpers/path_utils.py b/tests/helpers/path_utils.py new file mode 100644 index 0000000000..ded7079ef6 --- /dev/null +++ b/tests/helpers/path_utils.py @@ -0,0 +1,5 @@ +from pathlib import Path + + +def get_project_root() -> Path: + return Path(__file__).parent.parent.parent diff --git a/tests/helpers/video_utils.py b/tests/helpers/video_utils.py index 5b7aa2791f..c051c0604a 100644 --- a/tests/helpers/video_utils.py +++ b/tests/helpers/video_utils.py @@ -4,7 +4,7 @@ import subprocess import json -from manim.config.logger import logger +from manim import logger def capture(command): diff --git a/tests/template_generate_graphical_units_data.py b/tests/template_generate_graphical_units_data.py new file mode 100644 index 0000000000..ee91f4525c --- /dev/null +++ b/tests/template_generate_graphical_units_data.py @@ -0,0 +1,18 @@ +from manim import * +from tests.helpers.graphical_units import set_test_scene + +# Note: DO NOT COMMIT THIS FILE. The purpose of this template is to produce control data for graphical_units_data. As +# soon as the test data is produced, please revert all changes you made to this file, so this template file will be +# still available for others :) +# More about graphical unit tests: https://github.com/ManimCommunity/manim/wiki/Testing#graphical-unit-test + + +class YourClassHere(Scene): + def construct(self): + circle = Circle() + self.play(Animation(circle)) + + +set_test_scene( + YourClassHere, "" +) # can be e.g. "geometry" or "movements" diff --git a/tests/test_axes_shift.py b/tests/test_axes_shift.py new file mode 100644 index 0000000000..194e92df1f --- /dev/null +++ b/tests/test_axes_shift.py @@ -0,0 +1,44 @@ +import pytest +import numpy as np + +from manim.scene.graph_scene import GraphScene + + +def test_axes_without_shift(): + """Test whether axes are not shifted when origin is in plot range.""" + G = GraphScene( + x_min=-1, x_max=2, x_axis_label="", y_min=-2, y_max=5, y_axis_label="" + ) + G.setup_axes() + assert all(np.isclose(G.graph_origin, G.x_axis.n2p(0))) + assert all(np.isclose(G.graph_origin, G.y_axis.n2p(0))) + + +def test_axes_with_x_shift(): + """Test whether x-axis is shifted when 0 is not in plot range of x-axis.""" + G = GraphScene( + x_min=2, x_max=8, x_axis_label="", y_min=-2, y_max=5, y_axis_label="" + ) + G.setup_axes() + assert all(np.isclose(G.graph_origin, G.x_axis.n2p(2))) + assert all(np.isclose(G.graph_origin, G.y_axis.n2p(0))) + + +def test_axes_with_y_shift(): + """Test whether y-axis is shifted when 0 is not in plot range of y-axis.""" + G = GraphScene( + x_min=-1, x_max=2, x_axis_label="", y_min=1, y_max=5, y_axis_label="" + ) + G.setup_axes() + assert all(np.isclose(G.graph_origin, G.x_axis.n2p(0))) + assert all(np.isclose(G.graph_origin, G.y_axis.n2p(1))) + + +def test_axes_with_xy_shift(): + """Test whether both axes are shifted when origin is not in plot range.""" + G = GraphScene( + x_min=1, x_max=5, x_axis_label="", y_min=-5, y_max=-1, y_axis_label="" + ) + G.setup_axes() + assert all(np.isclose(G.graph_origin, G.x_axis.n2p(1))) + assert all(np.isclose(G.graph_origin, G.y_axis.n2p(-5))) diff --git a/tests/test_color.py b/tests/test_color.py index 259d83fcfb..d64ab4c733 100644 --- a/tests/test_color.py +++ b/tests/test_color.py @@ -1,8 +1,38 @@ import pytest -from manim import Camera, tempconfig, config +import numpy as np + +from manim import Mobject, VMobject, Camera, Scene, tempconfig, config, WHITE, BLACK def test_import_color(): import manim.utils.color as C C.WHITE + + +def test_background_color(): + S = Scene() + S.camera.background_color = "#ff0000" + S.renderer.update_frame(S) + assert np.all(S.renderer.get_frame()[0, 0] == np.array([255, 0, 0, 255])) + + S.camera.background_color = "#436f80" + S.renderer.update_frame(S) + assert np.all(S.renderer.get_frame()[0, 0] == np.array([67, 111, 128, 255])) + + S.camera.background_color = "#bbffbb" + S.camera.background_opacity = 0.5 + S.renderer.update_frame(S) + assert np.all(S.renderer.get_frame()[0, 0] == np.array([187, 255, 187, 127])) + + +def test_set_color(): + m = Mobject() + assert m.color.hex == "#fff" + m.set_color(BLACK) + assert m.color.hex == "#000" + + m = VMobject() + assert m.color.hex == "#fff" + m.set_color(BLACK) + assert m.color.hex == "#000" diff --git a/tests/test_composition.py b/tests/test_composition.py new file mode 100644 index 0000000000..6b3de0907f --- /dev/null +++ b/tests/test_composition.py @@ -0,0 +1,78 @@ +import pytest +from manim.animation.composition import Succession +from manim.animation.fading import FadeInFrom, FadeOutAndShift +from manim.constants import DOWN +from manim.mobject.geometry import Line + + +def test_succession_timing(): + """Test timing of animations in a succession.""" + line = Line() + animation_1s = FadeInFrom(line, direction=DOWN, run_time=1.0) + animation_4s = FadeOutAndShift(line, direction=DOWN, run_time=4.0) + succession = Succession(animation_1s, animation_4s) + assert succession.get_run_time() == 5.0 + succession.begin() + assert succession.active_index == 0 + # The first animation takes 20% of the total run time. + succession.interpolate(0.199) + assert succession.active_index == 0 + succession.interpolate(0.2) + assert succession.active_index == 1 + succession.interpolate(0.8) + assert succession.active_index == 1 + # At 100% and more, no animation must be active anymore. + succession.interpolate(1.0) + assert succession.active_index == 2 + assert succession.active_animation is None + succession.interpolate(1.2) + assert succession.active_index == 2 + assert succession.active_animation is None + + +def test_succession_in_succession_timing(): + """Test timing of nested successions.""" + line = Line() + animation_1s = FadeInFrom(line, direction=DOWN, run_time=1.0) + animation_4s = FadeOutAndShift(line, direction=DOWN, run_time=4.0) + nested_succession = Succession(animation_1s, animation_4s) + succession = Succession( + FadeInFrom(line, direction=DOWN, run_time=4.0), + nested_succession, + FadeInFrom(line, direction=DOWN, run_time=1.0), + ) + assert nested_succession.get_run_time() == 5.0 + assert succession.get_run_time() == 10.0 + succession.begin() + succession.interpolate(0.1) + assert succession.active_index == 0 + # The nested succession must not be active yet, and as a result hasn't set active_animation yet. + assert not hasattr(nested_succession, "active_animation") + succession.interpolate(0.39) + assert succession.active_index == 0 + assert not hasattr(nested_succession, "active_animation") + # The nested succession starts at 40% of total run time + succession.interpolate(0.4) + assert succession.active_index == 1 + assert nested_succession.active_index == 0 + # The nested succession second animation starts at 50% of total run time. + succession.interpolate(0.49) + assert succession.active_index == 1 + assert nested_succession.active_index == 0 + succession.interpolate(0.5) + assert succession.active_index == 1 + assert nested_succession.active_index == 1 + # The last animation starts at 90% of total run time. The nested succession must be finished at that time. + succession.interpolate(0.89) + assert succession.active_index == 1 + assert nested_succession.active_index == 1 + succession.interpolate(0.9) + assert succession.active_index == 2 + assert nested_succession.active_index == 2 + assert nested_succession.active_animation is None + # After 100%, nothing must be playing anymore. + succession.interpolate(1.0) + assert succession.active_index == 3 + assert succession.active_animation is None + assert nested_succession.active_index == 2 + assert nested_succession.active_animation is None diff --git a/tests/test_config.py b/tests/test_config.py index 36c60243bd..f18880d095 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,20 +1,19 @@ -import pytest +import tempfile +from pathlib import Path import numpy as np -from manim import config, tempconfig + +from manim import config, tempconfig, Scene, Square, WHITE def test_tempconfig(): """Test the tempconfig context manager.""" original = config.copy() - with tempconfig({"frame_width": 100, "frame_height": 42, "foo": -1}): + with tempconfig({"frame_width": 100, "frame_height": 42}): # check that config was modified correctly assert config["frame_width"] == 100 assert config["frame_height"] == 42 - # 'foo' is not a key in the original dict so it shouldn't be added - assert "foo" not in config - # check that no keys are missing and no new keys were added assert set(original.keys()) == set(config.keys()) @@ -27,3 +26,69 @@ def test_tempconfig(): assert np.allclose(config[k], v) else: assert config[k] == v + + +class MyScene(Scene): + def construct(self): + self.add(Square()) + self.wait(1) + + +def test_transparent(): + """Test the 'transparent' config option.""" + orig_verbosity = config["verbosity"] + config["verbosity"] = "ERROR" + + with tempconfig({"dry_run": True}): + scene = MyScene() + scene.render() + frame = scene.renderer.get_frame() + assert np.allclose(frame[0, 0], [0, 0, 0, 255]) + + with tempconfig({"transparent": True, "dry_run": True}): + scene = MyScene() + scene.render() + frame = scene.renderer.get_frame() + assert np.allclose(frame[0, 0], [0, 0, 0, 0]) + + config["verbosity"] = orig_verbosity + + +def test_background_color(): + """Test the 'background_color' config option.""" + with tempconfig({"background_color": WHITE, "verbosity": "ERROR", "dry_run": True}): + scene = MyScene() + scene.render() + frame = scene.renderer.get_frame() + assert np.allclose(frame[0, 0], [255, 255, 255, 255]) + + +def test_digest_file(tmp_path): + """Test that a config file can be digested programatically.""" + with tempconfig({}): + tmp_cfg = tempfile.NamedTemporaryFile("w", dir=tmp_path, delete=False) + tmp_cfg.write( + """ + [CLI] + media_dir = this_is_my_favorite_path + video_dir = {media_dir}/videos + """ + ) + tmp_cfg.close() + config.digest_file(tmp_cfg.name) + + assert config.get_dir("media_dir") == Path("this_is_my_favorite_path") + assert config.get_dir("video_dir") == Path("this_is_my_favorite_path/videos") + + +def test_temporary_dry_run(): + """Test that tempconfig correctly restores after setting dry_run.""" + assert config["write_to_movie"] + assert not config["save_last_frame"] + + with tempconfig({"dry_run": True}): + assert not config["write_to_movie"] + assert not config["save_last_frame"] + + assert config["write_to_movie"] + assert not config["save_last_frame"] diff --git a/tests/test_container.py b/tests/test_container.py index 5a26b3ba56..579ccf32f0 100644 --- a/tests/test_container.py +++ b/tests/test_container.py @@ -1,5 +1,5 @@ import pytest -from manim import Container, Mobject, Scene +from manim import Container, Mobject, Scene, tempconfig def test_ABC(): @@ -9,7 +9,9 @@ def test_ABC(): # The following should work without raising exceptions Mobject() - Scene() + + with tempconfig({"dry_run": True}): + Scene() def container_add(obj, get_submobjects): @@ -56,6 +58,10 @@ def test_mobject_add(): with pytest.raises(ValueError): obj.add(obj) + # can only add Mobjects + with pytest.raises(TypeError): + obj.add("foo") + def test_mobject_remove(): """Test Mobject.remove().""" @@ -63,13 +69,10 @@ def test_mobject_remove(): container_remove(obj, lambda: obj.submobjects) -def test_scene_add(): - """Test Scene.add().""" - scene = Scene() - container_add(scene, lambda: scene.mobjects) - - -def test_scene_remove(): - """Test Scene.remove().""" - scene = Scene() - container_remove(scene, lambda: scene.mobjects) +def test_scene_add_remove(): + """Test Scene.add() and Scene.remove().""" + with tempconfig({"dry_run": True}): + scene = Scene() + container_add(scene, lambda: scene.mobjects) + scene = Scene() + container_remove(scene, lambda: scene.mobjects) diff --git a/tests/test_copy.py b/tests/test_copy.py index a24fb1d1da..4edd5e00a3 100644 --- a/tests/test_copy.py +++ b/tests/test_copy.py @@ -1,5 +1,5 @@ from pathlib import Path -from manim import Mobject, BraceLabel, file_writer_config +from manim import Mobject, BraceLabel, config def test_mobject_copy(): @@ -18,13 +18,13 @@ def test_mobject_copy(): def test_bracelabel_copy(tmp_path): """Test that a copy is a deepcopy.""" # For this test to work, we need to tweak some folders temporarily - original_text_dir = file_writer_config["text_dir"] - original_tex_dir = file_writer_config["tex_dir"] + original_text_dir = config["text_dir"] + original_tex_dir = config["tex_dir"] mediadir = Path(tmp_path) / "deepcopy" - file_writer_config["text_dir"] = str(mediadir.joinpath("Text")) - file_writer_config["tex_dir"] = str(mediadir.joinpath("Tex")) + config["text_dir"] = str(mediadir.joinpath("Text")) + config["tex_dir"] = str(mediadir.joinpath("Tex")) for el in ["text_dir", "tex_dir"]: - Path(file_writer_config[el]).mkdir(parents=True) + Path(config[el]).mkdir(parents=True) # Before the refactoring of Mobject.copy(), the class BraceLabel was the # only one to have a non-trivial definition of copy. Here we test that it @@ -43,5 +43,5 @@ def test_bracelabel_copy(tmp_path): assert copy.submobjects[0] is not orig.brace # Restore the original folders - file_writer_config["text_dir"] = original_text_dir - file_writer_config["tex_dir"] = original_tex_dir + config["text_dir"] = original_text_dir + config["tex_dir"] = original_tex_dir diff --git a/tests/test_graphical_units/img_svg_resources/tree_img_640x351.png b/tests/test_graphical_units/img_svg_resources/tree_img_640x351.png new file mode 100644 index 0000000000..960d48b78b Binary files /dev/null and b/tests/test_graphical_units/img_svg_resources/tree_img_640x351.png differ diff --git a/tests/test_graphical_units/img_svg_resources/weight.svg b/tests/test_graphical_units/img_svg_resources/weight.svg new file mode 100644 index 0000000000..2ac6620933 --- /dev/null +++ b/tests/test_graphical_units/img_svg_resources/weight.svg @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/tests/test_graphical_units/test_axes.py b/tests/test_graphical_units/test_axes.py new file mode 100644 index 0000000000..45a23e1626 --- /dev/null +++ b/tests/test_graphical_units/test_axes.py @@ -0,0 +1,42 @@ +import pytest + +from manim import * +from ..utils.testing_utils import get_scenes_to_test +from ..utils.GraphicalUnitTester import GraphicalUnitTester + + +class AxesTest(GraphScene): + CONFIG = { + "x_min": -5, + "x_max": 5, + "y_min": -3, + "y_max": 3, + "x_axis_config": { + "add_start": 0.5, + "add_end": 0.5, + "include_tip": True, + }, + "y_axis_config": { + "add_start": 0.25, + "add_end": 0.5, + "include_tip": True, + }, + "x_axis_visibility": True, + "y_axis_visibility": True, + "y_label_position": UP, + "x_label_position": RIGHT, + "graph_origin": ORIGIN, + "axes_color": WHITE, + } + + def construct(self): + self.setup_axes(animate=True) + + +MODULE_NAME = "graph" + + +@pytest.mark.slow +@pytest.mark.parametrize("scene_to_test", get_scenes_to_test(__name__), indirect=False) +def test_scene(scene_to_test, tmpdir, show_diff): + GraphicalUnitTester(scene_to_test[1], MODULE_NAME, tmpdir).test(show_diff=show_diff) diff --git a/tests/test_graphical_units/test_geometry.py b/tests/test_graphical_units/test_geometry.py index 84366097a7..86720b603a 100644 --- a/tests/test_graphical_units/test_geometry.py +++ b/tests/test_graphical_units/test_geometry.py @@ -54,6 +54,12 @@ def construct(self): self.play(Animation(dot)) +class AnnotationDotTest(Scene): + def construct(self): + adot = AnnotationDot() + self.play(Animation(adot)) + + class EllipseTest(Scene): def construct(self): e = Ellipse() diff --git a/tests/test_graphical_units/test_img_and_svg.py b/tests/test_graphical_units/test_img_and_svg.py new file mode 100644 index 0000000000..a6b2fa9822 --- /dev/null +++ b/tests/test_graphical_units/test_img_and_svg.py @@ -0,0 +1,44 @@ +import sys +from pathlib import Path + +import pytest +from manim import * +from ..helpers.path_utils import get_project_root +from ..utils.testing_utils import get_scenes_to_test +from ..utils.GraphicalUnitTester import GraphicalUnitTester + +# Tests for the modules image_mobject and svg_mobject + + +class SVGMobjectTest(Scene): + def construct(self): + path = ( + get_project_root() + / "tests/test_graphical_units/img_svg_resources/weight.svg" + ) + svg_obj = SVGMobject(str(path)) + self.add(svg_obj) + self.wait() + + +class ImageMobjectTest(Scene): + def construct(self): + file_path = ( + get_project_root() + / "tests/test_graphical_units/img_svg_resources/tree_img_640x351.png" + ) + im1 = ImageMobject(file_path).shift(4 * LEFT + UP) + im2 = ImageMobject(file_path, scale_to_resolution=1080).shift( + 4 * LEFT + 2 * DOWN + ) + im3 = ImageMobject(file_path, scale_to_resolution=540).shift(4 * RIGHT) + self.add(im1, im2, im3) + self.wait(1) + + +MODULE_NAME = "img_and_svg" + + +@pytest.mark.parametrize("scene_to_test", get_scenes_to_test(__name__), indirect=False) +def test_scene(scene_to_test, tmpdir, show_diff): + GraphicalUnitTester(scene_to_test[1], MODULE_NAME, tmpdir).test(show_diff=show_diff) diff --git a/tests/test_graphical_units/test_threed.py b/tests/test_graphical_units/test_threed.py index ce7cc8ae3f..8068a7323a 100644 --- a/tests/test_graphical_units/test_threed.py +++ b/tests/test_graphical_units/test_threed.py @@ -24,7 +24,7 @@ class CameraMoveTest(ThreeDScene): def construct(self): cube = Cube() self.play(Animation(cube)) - self.move_camera(phi=PI / 4, theta=PI / 4) + self.move_camera(phi=PI / 4, theta=PI / 4, frame_center=[0, 0, -1]) class AmbientCameraMoveTest(ThreeDScene): @@ -34,6 +34,17 @@ def construct(self): self.play(Animation(cube)) +class FixedInFrameMObjectTest(ThreeDScene): + def construct(self): + axes = ThreeDAxes() + self.set_camera_orientation(phi=75 * DEGREES, theta=-45 * DEGREES) + circ = Circle() + self.add_fixed_in_frame_mobjects(circ) + circ.to_corner(UL) + self.add(axes) + self.wait() + + MODULE_NAME = "threed" diff --git a/tests/test_graphical_units/test_transform.py b/tests/test_graphical_units/test_transform.py index 98e7aae18c..84a6c4285b 100644 --- a/tests/test_graphical_units/test_transform.py +++ b/tests/test_graphical_units/test_transform.py @@ -119,6 +119,18 @@ def construct(self): self.play(CyclicReplace(square, circle)) +class FadeInAndOutTest(Scene): + def construct(self): + square = Square(color=BLUE).shift(2 * UP) + annotation = Square(color=BLUE) + self.add(annotation) + self.play(FadeIn(square)) + + annotation.become(Square(color=RED).rotate(PI / 4)) + self.add(annotation) + self.play(FadeOut(square)) + + MODULE_NAME = "transform" diff --git a/tests/test_logging/test_logging.py b/tests/test_logging/test_logging.py index 4ac424265a..504a76ecce 100644 --- a/tests/test_logging/test_logging.py +++ b/tests/test_logging/test_logging.py @@ -1,7 +1,4 @@ -import subprocess import os -import sys -import pytest import re from ..utils.commands import capture @@ -46,7 +43,7 @@ def test_logging_when_scene_is_not_specified(tmp_path, python_version): "-m", "manim", path_basic_scene, - "-l", + "-ql", "--log_to_file", "-v", "DEBUG", diff --git a/tests/test_pango.py b/tests/test_pango.py index c1ff76b36c..3c393673c5 100644 --- a/tests/test_pango.py +++ b/tests/test_pango.py @@ -1,4 +1,4 @@ -"""Tests :class:`PangoText` by comparing SVG files created. +"""Tests :class:`Text` by comparing SVG files created. """ import os import re @@ -6,7 +6,7 @@ import cairocffi import pangocairocffi import pangocffi -from manim import START_X, START_Y, PangoText, SVGMobject +from manim import START_X, START_Y, Text, SVGMobject, ITALIC RTL_TEXT: str = """صباح الخير مرحبا جميعا""" @@ -27,9 +27,9 @@ def remove_last_M(file_path: str) -> None: # pylint: disable=invalid-name def compare_SVGObject_with_PangoText( # pylint: disable=invalid-name - text: PangoText, svg_path: str + text: Text, svg_path: str ) -> bool: - """Checks for the path_string formed by PangoText and Formed SVG file. + """Checks for the path_string formed by Text and Formed SVG file. Uses SVGMobject as it parses the SVG and returns the path_string """ remove_last_M(svg_path) # to prevent issue displaying @@ -53,7 +53,7 @@ def test_general_text_svgobject() -> None: """ text = "hello" size = 1 - temp_pango_text = PangoText(text, size=size) + temp_pango_text = Text(text, size=size) surface = cairocffi.SVGSurface(filename, WIDTH, HEIGTH) context = cairocffi.Context(surface) context.move_to(START_X, START_Y) @@ -73,7 +73,7 @@ def test_rtl_text_to_svgobject() -> None: called using ``SVGMobject``""" size = 1 text = RTL_TEXT.replace("\n", "") - temp_pango_text = PangoText(text, size=1) + temp_pango_text = Text(text, size=1) surface = cairocffi.SVGSurface(filename, WIDTH, HEIGTH) context = cairocffi.Context(surface) context.move_to(START_X, START_Y) @@ -93,7 +93,7 @@ def test_font_face() -> None: size = 1 text = RTL_TEXT.replace("\n", "") font_face = "sans" - temp_pango_text = PangoText(text, size=1, font=font_face) + temp_pango_text = Text(text, size=1, font=font_face) surface = cairocffi.SVGSurface(filename, WIDTH, HEIGTH) context = cairocffi.Context(surface) context.move_to(START_X, START_Y) @@ -111,7 +111,7 @@ def test_font_face() -> None: def test_whether_svg_file_created() -> None: """Checks Whether SVG file is created in desired location""" - temp_pango_text = PangoText("hello", size=1) + temp_pango_text = Text("hello", size=1) theo_path = os.path.abspath( os.path.join(folder, temp_pango_text.text2hash() + ".svg") ) @@ -123,7 +123,7 @@ def test_tabs_replace() -> None: """Checks whether are there in end svg image. Pango should handle tabs and line breaks.""" size = 1 - temp_pango_text = PangoText("hello\thi\nf") + temp_pango_text = Text("hello\thi\nf") assert temp_pango_text.text == "hellohif" surface = cairocffi.SVGSurface(filename, WIDTH, HEIGTH) context = cairocffi.Context(surface) @@ -137,3 +137,20 @@ def test_tabs_replace() -> None: pangocairocffi.show_layout(context, layout) surface.finish() assert compare_SVGObject_with_PangoText(temp_pango_text, filename) + + +def test_t2s() -> None: + size = 1 + temp_pango_text = Text("Helloworld", t2s={"world": ITALIC}) + surface = cairocffi.SVGSurface(filename, WIDTH, HEIGTH) + context = cairocffi.Context(surface) + context.move_to(START_X, START_Y) + layout = pangocairocffi.create_layout(context) + layout.set_width(pangocffi.units_from_double(WIDTH)) + fontdesc = pangocffi.FontDescription() + fontdesc.set_size(pangocffi.units_from_double(size * 10)) + layout.set_font_description(fontdesc) + layout.set_markup('Helloworld') # yay, pango markup + pangocairocffi.show_layout(context, layout) + surface.finish() + assert compare_SVGObject_with_PangoText(temp_pango_text, filename) diff --git a/tests/test_cli_flags.py b/tests/test_quality_flags.py similarity index 53% rename from tests/test_cli_flags.py rename to tests/test_quality_flags.py index 1ed0b17f98..e1f1f081bc 100644 --- a/tests/test_cli_flags.py +++ b/tests/test_quality_flags.py @@ -1,45 +1,50 @@ from manim import constants -from manim.config.config_utils import _determine_quality, _parse_cli +from manim._config.main_utils import parse_args +from manim._config.utils import _determine_quality def test_quality_flags(): # Assert that quality is the default when not specifying it - parsed = _parse_cli([], False) + parsed = parse_args("manim dummy_filename".split()) assert parsed.quality == constants.DEFAULT_QUALITY_SHORT assert _determine_quality(parsed) == constants.DEFAULT_QUALITY for quality in constants.QUALITIES.keys(): + if not constants.QUALITIES[quality]["flag"]: + continue + + flag = constants.QUALITIES[quality]["flag"] # Assert that quality is properly set when using -q* - arguments = f"-q{constants.QUALITIES[quality]}".split() - parsed = _parse_cli(arguments, False) + arguments = f"manim -q{flag} dummy_filename".split() + parsed = parse_args(arguments) - assert parsed.quality == constants.QUALITIES[quality] + assert parsed.quality == flag assert quality == _determine_quality(parsed) # Assert that quality is properly set when using -q * - arguments = f"-q {constants.QUALITIES[quality]}".split() - parsed = _parse_cli(arguments, False) + arguments = f"manim -q {flag} dummy_filename".split() + parsed = parse_args(arguments) - assert parsed.quality == constants.QUALITIES[quality] + assert parsed.quality == flag assert quality == _determine_quality(parsed) # Assert that quality is properly set when using --quality * - arguments = f"--quality {constants.QUALITIES[quality]}".split() - parsed = _parse_cli(arguments, False) + arguments = f"manim --quality {flag} dummy_filename".split() + parsed = parse_args(arguments) - assert parsed.quality == constants.QUALITIES[quality] + assert parsed.quality == flag assert quality == _determine_quality(parsed) # Assert that quality is properly set when using -*_quality - arguments = f"--{quality}".split() - parsed = _parse_cli(arguments, False) + arguments = f"manim --{quality} dummy_filename".split() + parsed = parse_args(arguments) assert getattr(parsed, quality) assert quality == _determine_quality(parsed) # Assert that *_quality is False when not specifying it - parsed = _parse_cli([], False) + parsed = parse_args("manim dummy_filename".split()) assert not getattr(parsed, quality) assert _determine_quality(parsed) == constants.DEFAULT_QUALITY diff --git a/tests/test_scene_rendering/conftest.py b/tests/test_scene_rendering/conftest.py index 0dc51dce18..b5b6f1d052 100644 --- a/tests/test_scene_rendering/conftest.py +++ b/tests/test_scene_rendering/conftest.py @@ -2,8 +2,6 @@ from pathlib import Path -from manim import file_writer_config - @pytest.fixture def manim_cfg_file(): diff --git a/tests/test_scene_rendering/simple_scenes.py b/tests/test_scene_rendering/simple_scenes.py index b05c27575f..6522d4cf60 100644 --- a/tests/test_scene_rendering/simple_scenes.py +++ b/tests/test_scene_rendering/simple_scenes.py @@ -27,3 +27,10 @@ def construct(self): self.wait(1) self.play(ShowCreation(Square().shift(3 * DOWN))) self.wait(1) + + +class NoAnimations(Scene): + def construct(self): + dot = Dot().set_color(GREEN) + self.add(dot) + self.wait(1) diff --git a/tests/test_scene_rendering/test_caching_related.py b/tests/test_scene_rendering/test_caching_related.py index 3f6d4ec973..2d7ddc7084 100644 --- a/tests/test_scene_rendering/test_caching_related.py +++ b/tests/test_scene_rendering/test_caching_related.py @@ -1,12 +1,12 @@ import os import pytest import subprocess -from manim import file_writer_config from ..utils.commands import capture from ..utils.video_tester import * +@pytest.mark.slow @video_comparison( "SceneWithMultipleWaitCallsWithNFlag.json", "videos/simple_scenes/480p15/SceneWithMultipleWaitCalls.mp4", @@ -30,6 +30,7 @@ def test_wait_skip(tmp_path, manim_cfg_file, simple_scenes_path): assert exit_code == 0, err +@pytest.mark.slow @video_comparison( "SceneWithMultiplePlayCallsWithNFlag.json", "videos/simple_scenes/480p15/SceneWithMultipleCalls.mp4", diff --git a/tests/test_scene_rendering/test_cli_flags.py b/tests/test_scene_rendering/test_cli_flags.py index aa815223ac..e6347f4961 100644 --- a/tests/test_scene_rendering/test_cli_flags.py +++ b/tests/test_scene_rendering/test_cli_flags.py @@ -1,4 +1,7 @@ import pytest +import numpy as np +from PIL import Image +from pathlib import Path from ..utils.video_tester import * @@ -23,6 +26,7 @@ def test_basic_scene_with_default_values(tmp_path, manim_cfg_file, simple_scenes assert exit_code == 0, err +@pytest.mark.slow @video_comparison( "SquareToCircleWithlFlag.json", "videos/simple_scenes/480p15/SquareToCircle.mp4" ) @@ -61,3 +65,102 @@ def test_n_flag(tmp_path, simple_scenes_path): ] _, err, exit_code = capture(command) assert exit_code == 0, err + + +@pytest.mark.slow +def test_s_flag_no_animations(tmp_path, manim_cfg_file, simple_scenes_path): + scene_name = "NoAnimations" + command = [ + "python", + "-m", + "manim", + simple_scenes_path, + scene_name, + "-ql", + "-s", + "--media_dir", + str(tmp_path), + ] + out, err, exit_code = capture(command) + assert exit_code == 0, err + + exists = (tmp_path / "videos").exists() + assert not exists, "running manim with -s flag rendered a video" + + is_empty = not any((tmp_path / "images" / "simple_scenes").iterdir()) + assert not is_empty, "running manim with -s flag did not render an image" + + +@pytest.mark.slow +def test_s_flag(tmp_path, manim_cfg_file, simple_scenes_path): + scene_name = "SquareToCircle" + command = [ + "python", + "-m", + "manim", + simple_scenes_path, + scene_name, + "-ql", + "-s", + "--media_dir", + str(tmp_path), + ] + out, err, exit_code = capture(command) + assert exit_code == 0, err + + exists = (tmp_path / "videos").exists() + assert not exists, "running manim with -s flag rendered a video" + + is_empty = not any((tmp_path / "images" / "simple_scenes").iterdir()) + assert not is_empty, "running manim with -s flag did not render an image" + + +@pytest.mark.slow +def test_r_flag(tmp_path, manim_cfg_file, simple_scenes_path): + scene_name = "SquareToCircle" + command = [ + "python", + "-m", + "manim", + simple_scenes_path, + scene_name, + "-ql", + "-s", + "--media_dir", + str(tmp_path), + "-r", + "100, 200", + ] + out, err, exit_code = capture(command) + assert exit_code == 0, err + + is_not_empty = any((tmp_path / "images").iterdir()) + assert is_not_empty, "running manim with -s, -r flag did not render a file" + + filename = tmp_path / "images" / "simple_scenes" / "SquareToCircle.png" + assert np.asarray(Image.open(filename)).shape == (100, 200, 4) + + +@pytest.mark.slow +def test_custom_folders(tmp_path, manim_cfg_file, simple_scenes_path): + scene_name = "SquareToCircle" + command = [ + "python", + "-m", + "manim", + simple_scenes_path, + scene_name, + "-ql", + "-s", + "--media_dir", + str(tmp_path), + "--custom_folders", + ] + out, err, exit_code = capture(command) + assert exit_code == 0, err + + exists = (tmp_path / "videos").exists() + assert not exists, "--custom_folders produced a 'videos/' dir" + + exists = (tmp_path / "SquareToCircle.png").exists() + assert exists, "--custom_folders did not produce the output file" diff --git a/tests/utils/GraphicalUnitTester.py b/tests/utils/GraphicalUnitTester.py index b4352b7080..e5680b9b1f 100644 --- a/tests/utils/GraphicalUnitTester.py +++ b/tests/utils/GraphicalUnitTester.py @@ -2,7 +2,7 @@ import logging import numpy as np -from manim import config, file_writer_config +from manim import config, tempconfig class GraphicalUnitTester: @@ -47,31 +47,25 @@ def __init__( tests_directory, "control_data", "graphical_units_data", module_tested ) - # IMPORTANT NOTE : The graphical units tests don't use for now any custom manim.cfg, - # since it is impossible to manually select a manim.cfg from a python file. (see issue #293) - file_writer_config["text_dir"] = os.path.join( - self.path_tests_medias_cache, "Text" - ) - file_writer_config["tex_dir"] = os.path.join( - self.path_tests_medias_cache, "Tex" - ) + # IMPORTANT NOTE : The graphical units tests don't use for now any + # custom manim.cfg, since it is impossible to manually select a + # manim.cfg from a python file. (see issue #293) + config["text_dir"] = os.path.join(self.path_tests_medias_cache, "Text") + config["tex_dir"] = os.path.join(self.path_tests_medias_cache, "Tex") - file_writer_config["skip_animations"] = True - file_writer_config["write_to_movie"] = False - file_writer_config["disable_caching"] = True - config["pixel_height"] = 480 - config["pixel_width"] = 854 - config["frame_rate"] = 15 + config["disable_caching"] = True + config["quality"] = "low_quality" for dir_temp in [ self.path_tests_medias_cache, - file_writer_config["text_dir"], - file_writer_config["tex_dir"], + config["text_dir"], + config["tex_dir"], ]: os.makedirs(dir_temp) - self.scene = scene_object() - self.scene.render() + with tempconfig({"dry_run": True}): + self.scene = scene_object(skip_animations=True) + self.scene.render() def _load_data(self): """Load the np.array of the last frame of a pre-rendered scene. If not found, throw FileNotFoundError. @@ -82,9 +76,9 @@ def _load_data(self): The pre-rendered frame. """ frame_data_path = os.path.join( - os.path.join(self.path_control_data, "{}.npy".format(str(self.scene))) + os.path.join(self.path_control_data, "{}.npz".format(str(self.scene))) ) - return np.load(frame_data_path) + return np.load(frame_data_path)["frame_data"] def _show_diff_helper(self, frame_data, expected_frame_data): """Will visually display with matplotlib differences between frame generared and the one expected."""