mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-22 10:01:47 +00:00
Merge branch 'main' into patch-1
This commit is contained in:
commit
0555a69012
103 changed files with 3132 additions and 1411 deletions
|
|
@ -1 +1,20 @@
|
|||
.git
|
||||
|
||||
# Development / test artifacts
|
||||
__pycache__
|
||||
**/__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.egg-info
|
||||
dist/
|
||||
build/
|
||||
coverage.xml
|
||||
|
||||
# Not needed to install the package
|
||||
docs/
|
||||
tests/
|
||||
example_scenes/
|
||||
media/
|
||||
logo/
|
||||
scripts/
|
||||
|
|
|
|||
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -1,10 +1,6 @@
|
|||
<!-- Thank you for contributing to Manim! Learn more about the process in our contributing guidelines: https://docs.manim.community/en/latest/contributing.html -->
|
||||
|
||||
## Overview: What does this pull request change?
|
||||
<!-- If there is more information than the PR title that should be added to our release changelog, add it in the following changelog section. This is optional, but recommended for larger pull requests. -->
|
||||
<!--changelog-start-->
|
||||
|
||||
<!--changelog-end-->
|
||||
|
||||
## Motivation and Explanation: Why and how do your changes improve the library?
|
||||
<!-- Optional for bugfixes, small enhancements, and documentation-related PRs. Otherwise, please give a short reasoning for your changes. -->
|
||||
|
|
|
|||
38
.github/workflows/python-publish.yml
vendored
38
.github/workflows/python-publish.yml
vendored
|
|
@ -11,12 +11,13 @@ jobs:
|
|||
environment: release
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install dependencies
|
||||
run: apt-get update && apt-get install -y build-essential python3-dev libcairo2-dev libpango1.0-dev
|
||||
run: sudo apt-get update && sudo apt-get install -y build-essential python3-dev libcairo2-dev libpango1.0-dev
|
||||
|
||||
- name: Set up Python 3.13
|
||||
uses: actions/setup-python@v6
|
||||
|
|
@ -28,7 +29,6 @@ jobs:
|
|||
|
||||
- name: Build and push release to PyPI
|
||||
run: |
|
||||
uv sync
|
||||
uv build
|
||||
uv publish
|
||||
|
||||
|
|
@ -37,37 +37,11 @@ jobs:
|
|||
with:
|
||||
path: dist/*.tar.gz
|
||||
name: manim.tar.gz
|
||||
- name: Install Dependency
|
||||
run: pip install requests
|
||||
- name: Get Upload URL
|
||||
id: create_release
|
||||
shell: python
|
||||
env:
|
||||
access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag_act: ${{ github.ref }}
|
||||
run: |
|
||||
import requests
|
||||
import os
|
||||
ref_tag = os.getenv('tag_act').split('/')[-1]
|
||||
access_token = os.getenv('access_token')
|
||||
headers = {
|
||||
"Accept":"application/vnd.github.v3+json",
|
||||
"Authorization": f"token {access_token}"
|
||||
}
|
||||
url = f"https://api.github.com/repos/ManimCommunity/manim/releases/tags/{ref_tag}"
|
||||
c = requests.get(url,headers=headers)
|
||||
upload_url=c.json()['upload_url']
|
||||
with open(os.getenv('GITHUB_OUTPUT'), 'w') as f:
|
||||
print(f"upload_url={upload_url}", file=f)
|
||||
print(f"tag_name={ref_tag[1:]}", file=f)
|
||||
|
||||
- name: Upload Release Asset
|
||||
id: upload-release
|
||||
uses: actions/upload-release-asset@v1
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: dist/manim-${{ steps.create_release.outputs.tag_name }}.tar.gz
|
||||
asset_name: manim-${{ steps.create_release.outputs.tag_name }}.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
run: |
|
||||
TAG=${{ github.event.release.tag_name }}
|
||||
gh release upload "$TAG" "dist/manim-${TAG#v}.tar.gz"
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ authors:
|
|||
-
|
||||
name: "The Manim Community Developers"
|
||||
cff-version: "1.2.0"
|
||||
date-released: 2026-01-17
|
||||
date-released: 2026-02-20
|
||||
license: MIT
|
||||
message: "We acknowledge the importance of good software to support research, and we note that research becomes more valuable when it is communicated effectively. To demonstrate the value of Manim, we ask that you cite Manim in your work."
|
||||
title: Manim – Mathematical Animation Framework
|
||||
url: "https://www.manim.community/"
|
||||
version: "v0.19.2"
|
||||
version: "v0.20.0"
|
||||
...
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<p align="center">
|
||||
<a href="https://www.manim.community/"><img src="https://raw.githubusercontent.com/ManimCommunity/manim/main/logo/cropped.png"></a>
|
||||
<a href="https://www.manim.community/"><img src="https://raw.githubusercontent.com/ManimCommunity/manim/main/logo/cropped.png" alt="Manim Community logo"></a>
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://pypi.org/project/manim/"><img src="https://img.shields.io/pypi/v/manim.svg?style=flat&logo=pypi" alt="PyPI Latest Release"></a>
|
||||
<a href="https://hub.docker.com/r/manimcommunity/manim"><img src="https://img.shields.io/docker/v/manimcommunity/manim?color=%23099cec&label=docker%20image&logo=docker" alt="Docker image"> </a>
|
||||
<a href="https://mybinder.org/v2/gh/ManimCommunity/jupyter_examples/HEAD?filepath=basic_example_scenes.ipynb"><img src="https://mybinder.org/badge_logo.svg"></a>
|
||||
<a href="https://mybinder.org/v2/gh/ManimCommunity/jupyter_examples/HEAD?filepath=basic_example_scenes.ipynb"><img src="https://mybinder.org/badge_logo.svg" alt="Launch Binder"></a>
|
||||
<a href="http://choosealicense.com/licenses/mit/"><img src="https://img.shields.io/badge/license-MIT-red.svg?style=flat" alt="MIT License"></a>
|
||||
<a href="https://www.reddit.com/r/manim/"><img src="https://img.shields.io/reddit/subreddit-subscribers/manim.svg?color=orange&label=reddit&logo=reddit" alt="Reddit" href=></a>
|
||||
<a href="https://twitter.com/manimcommunity/"><img src="https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=Follow%20%40manimcommunity" alt="Twitter">
|
||||
|
|
|
|||
|
|
@ -1,42 +1,74 @@
|
|||
FROM python:3.11-slim
|
||||
# ── Stage 1: builder ─────────────────────────────────────────────────────────
|
||||
FROM python:3.14-slim AS builder
|
||||
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install --no-install-recommends -y \
|
||||
build-essential \
|
||||
gcc \
|
||||
cmake \
|
||||
make \
|
||||
pkg-config \
|
||||
wget \
|
||||
libcairo2-dev \
|
||||
libffi-dev \
|
||||
libpango1.0-dev \
|
||||
freeglut3-dev \
|
||||
ffmpeg \
|
||||
pkg-config \
|
||||
make \
|
||||
wget \
|
||||
libegl-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Setup a minimal TeX Live installation (no ctex: drops ~100 MB of CJK fonts/packages)
|
||||
COPY docker/texlive-profile.txt /tmp/
|
||||
ENV PATH=/usr/local/texlive/bin/armhf-linux:/usr/local/texlive/bin/aarch64-linux:/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 cbfonts-fd cm-super count1to doublestroke dvisvgm everysel \
|
||||
fontspec frcursive fundus-calligra gnu-freefont jknapltx latex-bin \
|
||||
mathastext microtype multitoc physics prelim2e preview ragged2e relsize rsfs \
|
||||
setspace standalone tipa wasy wasysym xcolor xetex xkeyval \
|
||||
&& rm -rf /tmp/install-tl /tmp/install-tl-unx.tar.gz
|
||||
|
||||
# Install manim into an isolated virtualenv
|
||||
ENV VIRTUAL_ENV=/opt/venv
|
||||
RUN python -m venv $VIRTUAL_ENV
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
COPY . /opt/manim
|
||||
WORKDIR /opt/manim
|
||||
RUN pip install --no-cache-dir .[jupyterlab]
|
||||
|
||||
# ── Stage 2: runtime ─────────────────────────────────────────────────────────
|
||||
FROM python:3.14-slim
|
||||
|
||||
# Runtime libs only:
|
||||
# - no ffmpeg: PyAV (av package) bundles its own ffmpeg libraries in av.libs/
|
||||
# - OpenGL: keep EGL for headless rendering and libGL as required by moderngl/glcontext
|
||||
# - fonts-noto-core instead of fonts-noto (drops CJK noto fonts)
|
||||
RUN apt-get update -qq \
|
||||
&& apt-get install --no-install-recommends -y \
|
||||
libcairo2 \
|
||||
libpango-1.0-0 \
|
||||
libpangocairo-1.0-0 \
|
||||
libpangoft2-1.0-0 \
|
||||
libffi8 \
|
||||
libegl1 \
|
||||
libgl1 \
|
||||
ghostscript \
|
||||
fonts-noto
|
||||
fonts-noto-core \
|
||||
fontconfig \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN fc-cache -fv
|
||||
|
||||
# setup a minimal texlive installation
|
||||
COPY docker/texlive-profile.txt /tmp/
|
||||
# Copy TeX Live from builder
|
||||
ENV PATH=/usr/local/texlive/bin/armhf-linux:/usr/local/texlive/bin/aarch64-linux:/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 cbfonts-fd cm-super count1to ctex doublestroke dvisvgm everysel \
|
||||
fontspec frcursive fundus-calligra gnu-freefont jknapltx latex-bin \
|
||||
mathastext microtype multitoc physics prelim2e preview ragged2e relsize rsfs \
|
||||
setspace standalone tipa wasy wasysym xcolor xetex xkeyval
|
||||
COPY --from=builder /usr/local/texlive /usr/local/texlive
|
||||
|
||||
# clone and build manim
|
||||
COPY . /opt/manim
|
||||
WORKDIR /opt/manim
|
||||
RUN pip install --no-cache .[jupyterlab]
|
||||
|
||||
RUN pip install -r docs/requirements.txt
|
||||
# Copy the pre-built virtualenv from builder
|
||||
ENV VIRTUAL_ENV=/opt/venv
|
||||
COPY --from=builder /opt/venv /opt/venv
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
ARG NB_USER=manimuser
|
||||
ARG NB_UID=1000
|
||||
|
|
@ -49,11 +81,8 @@ RUN adduser --disabled-password \
|
|||
--uid ${NB_UID} \
|
||||
${NB_USER}
|
||||
|
||||
# create working directory for user to mount local directory into
|
||||
WORKDIR ${HOME}
|
||||
USER root
|
||||
RUN chown -R ${NB_USER}:${NB_USER} ${HOME}
|
||||
RUN chmod 777 ${HOME}
|
||||
RUN chown -R ${NB_USER}:${NB_USER} ${HOME} && chmod 777 ${HOME}
|
||||
USER ${NB_USER}
|
||||
|
||||
CMD [ "/bin/bash" ]
|
||||
CMD ["/bin/bash"]
|
||||
|
|
|
|||
|
|
@ -13,3 +13,11 @@ Multi-platform builds are possible by running
|
|||
docker buildx build --push --platform linux/arm64/v8,linux/amd64 --tag manimcommunity/manim:TAG -f docker/Dockerfile .
|
||||
```
|
||||
from the root directory of the repository.
|
||||
|
||||
# Runtime notes
|
||||
- The image is built via a multi-stage Dockerfile (build dependencies are not
|
||||
carried into the runtime stage).
|
||||
- The image does not include the `ffmpeg` CLI binary.
|
||||
- The default TeX installation is minimal and does not include `ctex`.
|
||||
- Headless OpenGL rendering relies on EGL/GL runtime libraries available in the
|
||||
image.
|
||||
|
|
|
|||
|
|
@ -2,14 +2,17 @@
|
|||
Changelog
|
||||
#########
|
||||
|
||||
This page contains a list of changes made between releases. Changes
|
||||
from versions that are not listed below (in particular patch-level
|
||||
releases since v0.18.0) are documented on our
|
||||
`GitHub release page <https://github.com/ManimCommunity/manim/releases/>`__.
|
||||
This page contains a list of changes made between releases.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
changelog/0.20.0-changelog
|
||||
changelog/0.19.2-changelog
|
||||
changelog/0.19.1-changelog
|
||||
changelog/0.19.0-changelog
|
||||
changelog/0.18.1-changelog
|
||||
changelog/0.18.0.post0-changelog
|
||||
changelog/0.18.0-changelog
|
||||
changelog/0.17.3-changelog
|
||||
changelog/0.17.2-changelog
|
||||
|
|
|
|||
9
docs/source/changelog/0.18.0.post0-changelog.rst
Normal file
9
docs/source/changelog/0.18.0.post0-changelog.rst
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
*************
|
||||
v0.18.0.post0
|
||||
*************
|
||||
|
||||
:Date: April 08, 2024
|
||||
|
||||
This release is a post-release fixing `#3676
|
||||
<https://github.com/ManimCommunity/manim/issues/3676>`_, a bug caused by a recent
|
||||
change introduced to the way how SVG files of text are generated by Pango.
|
||||
160
docs/source/changelog/0.18.1-changelog.md
Normal file
160
docs/source/changelog/0.18.1-changelog.md
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
---
|
||||
short-title: v0.18.1
|
||||
description: Changelog for Manim v0.18.1
|
||||
---
|
||||
|
||||
# v0.18.1
|
||||
|
||||
Date
|
||||
: April 28, 2024
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Breaking Changes and Deprecations
|
||||
|
||||
* Removed deprecated `manim new` command by {user}`chopan050` in {pr}`3512`
|
||||
* Removed support for dynamic plugin imports by {user}`Viicos` in {pr}`3524`
|
||||
* Remove meth:``.Mobject.wag`` by {user}`JasonGrace2282` in {pr}`3539`
|
||||
* Remove deprecated parameters and animations by {user}`JasonGrace2282` in {pr}`3688`
|
||||
|
||||
|
||||
### New Features
|
||||
|
||||
* Added `cap_style` feature to `VMobject` by {user}`MathItYT` in {pr}`3516`
|
||||
* Allow hiding version splash by {user}`jeertmans` in {pr}`3329`
|
||||
* Added the ability to pass lists and generators to `Scene.play()` by {user}`MrDiver` in {pr}`3365`
|
||||
* Added ``--preview_command`` cli flag by {user}`JasonGrace2282` in {pr}`3615`
|
||||
|
||||
### Fixed Bugs and Enhancements
|
||||
|
||||
* Allow accessing ghost vectors in :class:`.LinearTransformationScene` by {user}`JasonGrace2282` in {pr}`3435`
|
||||
* Optimized `get_unit_normal()` and replaced `np.cross()` with custom `cross()` in `manim.utils.space_ops` by {user}`chopan050` in {pr}`3494`
|
||||
* Implement caching of fonts list to improve runtime performance by {user}`MrDiver` in {pr}`3316`
|
||||
* Reformatting the `--save_sections` output to have the format `<Scene>_<SecNum>_<SecName><extension>` by {user}`doaamuham` in {pr}`3499`
|
||||
* Account for dtype in the pixel array so the maximum value stays correct in the invert function by {user}`jeertmans` in {pr}`3493`
|
||||
* Added `grid_lines` attribute to `Rectangle` to add individual styling to the grid lines by {user}`RobinPH` in {pr}`3428`
|
||||
* Fixed rectangle grid properties (#3082) by {user}`pauluhlenbruck` in {pr}`3513`
|
||||
* Fixed animations with zero runtime length to give a useful error instead of a broken pipe by {user}`MrDiver` in {pr}`3491`
|
||||
* Fixed stroke width being ignored by `StreamLines` with a single color by {user}`yashm277` in {pr}`3436`
|
||||
* Fixed formatting of ``MoveAlongPath`` docs by {user}`JasonGrace2282` in {pr}`3541`
|
||||
* Added helpful hints to `VGroup.add()` error message by {user}`vvolhejn` in {pr}`3561`
|
||||
* Made `earclip_triangulation` more robust by {user}`hydromelvictor` in {pr}`3574`
|
||||
* Refactored `TexTemplate` by {user}`Viicos` in {pr}`3520`
|
||||
* Fixed `write_subcaption_file` error when using OpenGL renderer by {user}`yuan-xy` in {pr}`3546`
|
||||
* Fixed `get_arc_center()` returning reference of point by {user}`sparshg` in {pr}`3599`
|
||||
* Improved handling of specified font name by {user}`staghado` in {pr}`3429`
|
||||
* Fixing the behavior of `.become` to not modify target mobject via side effects fix color linking by {user}`MrDiver` in {pr}`3508`
|
||||
* Fixed bug in :class:`.VMobjectFromSVGPath` by {user}`abul4fia` in {pr}`3677`
|
||||
* Fix for windows cp1252 encoding failure (fix test pipeline) by {user}`JasonGrace2282` in {pr}`3687`
|
||||
* Fix NameError in try... except by {user}`JasonGrace2282` in {pr}`3694`
|
||||
* Fix successive calls of :meth:`.LinearTransformationScene.apply_matrix` by {user}`SirJamesClarkMaxwell` in {pr}`3675`
|
||||
* Fixed `Mobject.put_start_and_end_on` with same start and end point by {user}`MontroyJosh` in {pr}`3718`
|
||||
* Fixed issue where `SpiralIn` doesn't show elements by {user}`Gixtox` in {pr}`3589`
|
||||
* Cleaned `Graph` layouts and increase flexibility by {user}`Nikhil-42` in {pr}`3434`
|
||||
* `AnimationGroup`: optimized `interpolate()` and fixed alpha bug on `finish()` by {user}`chopan050` in {pr}`3542`
|
||||
* Fixed warning about missing plugin `""` by {user}`behackl` in {pr}`3734`
|
||||
|
||||
### Documentation
|
||||
|
||||
* Typo in `indication` documentation by {user}`jcep` in {pr}`3477`
|
||||
* Fixed typo: 360° to 180° in quickstart tutorial by {user}`szchixy` in {pr}`3498`
|
||||
* Fixed typo in mobject docstring: `line` -> `square` by {user}`yuan-xy` in {pr}`3509`
|
||||
* Explain ``.Transform`` vs ``.ReplacementTransform`` in quickstart examples by {user}`JasonGrace2282` in {pr}`3500`
|
||||
* Fixed formatting in building blocks tutorial by {user}`MrDiver` in {pr}`3515`
|
||||
* Fixed `Indicate` docstring typo by {user}`Lawqup` in {pr}`3461`
|
||||
* Added Documentation to `.to_edge` and `to_corner` by {user}`TheMathematicFanatic` in {pr}`3408`
|
||||
* Added some words about Cairo 1.18 by {user}`jeertmans` in {pr}`3530`
|
||||
* Fixed typo of `get_y_axis_label` parameter documentation by {user}`yuan-xy` in {pr}`3547`
|
||||
* Added note in docstring of `ManimColor` about class constructors by {user}`JasonGrace2282` in {pr}`3554`
|
||||
* Improve documentation section about contributing to docs by {user}`chopan050` in {pr}`3555`
|
||||
* Removed duplicated documentation for -s / --save_last_frame CLI flag by {user}`Gixtox` in {pr}`3528`
|
||||
* Updated Docker instructions to use bash from the PATH by {user}`NotWearingPants` in {pr}`3582`
|
||||
* Fixed typo in `value_tracker.py` by {user}`yuan-xy` in {pr}`3594`
|
||||
* Added `ref_class` for `BooleanOperations` in Example Gallery by {user}`JasonGrace2282` in {pr}`3598`
|
||||
* Changed `Vector3` to `Vector3D` in contributing docs by {user}`JasonGrace2282` in {pr}`3639`
|
||||
* Added some examples for `Mobject`/`VMobject` methods by {user}`JasonGrace2282` in {pr}`3641`
|
||||
* Fixed broken link to Poetry's installation guide in the documentation by {user}`biinnnggggg` in {pr}`3692`
|
||||
* Fixed minor grammatical errors found in the index page of the documentation by {user}`biinnnggggg` in {pr}`3690`
|
||||
* Fixed typo on page about translations by {user}`biinnnggggg` in {pr}`3696`
|
||||
* Fixed outdated description of CLI option in Manim's Output Settings by {user}`HairlessVillager` in {pr}`3674`
|
||||
* Mention pixi in installation guide by {user}`pavelzw` in {pr}`3678`
|
||||
* Updated typing guidelines by {user}`JasonGrace2282` in {pr}`3704`
|
||||
* Updated documentation and typings for `ParametricFunction` by {user}`danielzsh` in {pr}`3703`
|
||||
* Fixed docstring markup in `Rotate` by {user}`TheCrowned` in {pr}`3721`
|
||||
* Improve consistency in axis label example by {user}`amrear` in {pr}`3730`
|
||||
|
||||
### Maintenance and Testing
|
||||
|
||||
* Fixed wrong path in action building downloadable docs by {user}`behackl` in {pr}`3450`
|
||||
* Add type hints to `_config` by {user}`Viicos` in {pr}`3440`
|
||||
* Update dependency constraints, fix deprecation warnings by {user}`Viicos` in {pr}`3376`
|
||||
* Update Docker base image to python3.12-slim (#3458) by {user}`PikaBlue107` in {pr}`3459`
|
||||
* Fixed `line_join` to `joint_type` in example_scenes/basic.py by {user}`szchixy` in {pr}`3510`
|
||||
* Fixed :attr:`.Mobject.animate` type-hint to allow LSP autocomplete by {user}`JasonGrace2282` in {pr}`3543`
|
||||
* Finish TODO's in ``contributing/typings.rst`` by {user}`JasonGrace2282` in {pr}`3545`
|
||||
* Fixed use of `Mobject`'s deprecated `get_*()` and `set_*()` methods in Cairo tests by {user}`JasonGrace2282` in {pr}`3549`
|
||||
* Added support for Manim type aliases in Sphinx docs and added new TypeAliases by {user}`chopan050` in {pr}`3484`
|
||||
* Fixed typing of `Animation` by {user}`dandavison` in {pr}`3568`
|
||||
* Added some TODOs for future use of `ManimFrame` by {user}`chopan050` in {pr}`3553`
|
||||
* Fixed typehint of :attr:`InternalPoint2D_Array` by {user}`JasonGrace2282` in {pr}`3592`
|
||||
* Fixed error in Windows CI pipeline by {user}`behackl` in {pr}`3611`
|
||||
* Fixed type hint of indication.py by {user}`yuan-xy` in {pr}`3613`
|
||||
* Revert vector type aliases to NumPy ndarrays by {user}`chopan050` in {pr}`3595`
|
||||
* Run `poetry lock --no-update` by {user}`JasonGrace2282` in {pr}`3621`
|
||||
* Code Cleanup: removing unused imports and global variables by {user}`JasonGrace2282` in {pr}`3620`
|
||||
* Fixed type hint of `Vector` direction parameter by {user}`JasonGrace2282` in {pr}`3640`
|
||||
* Flake8 rule C901 is about McCabe code complexity by {user}`cclauss` in {pr}`3673`
|
||||
* Updated year in license by {user}`JasonGrace2282` in {pr}`3689`
|
||||
* Automated copyright updating for docs by {user}`JasonGrace2282` in {pr}`3708`
|
||||
* Fixed some typehints in `mobject.py` by {user}`JasonGrace2282` in {pr}`3668`
|
||||
* Search for type aliases if TYPE_CHECKING by {user}`JasonGrace2282` in {pr}`3671`
|
||||
* Follow-up to graph layout cleanup: improvements for tests and typing by {user}`behackl` in {pr}`3728`
|
||||
* GH Actions: Changed from macos-latest to macos-13 by {user}`JasonGrace2282` in {pr}`3729`
|
||||
* Fixed return type inconsistency for `get_anchors()` by {user}`JinchuLi2002` in {pr}`3214`
|
||||
* Prepared new release: `v0.18.1` by {user}`behackl` in {pr}`3719`
|
||||
|
||||
#### Dependency Version Changes
|
||||
|
||||
* Bump jupyter-server from 2.9.1 to 2.11.2 by {user}`dependabot` in {pr}`3497`
|
||||
* Bump github/codeql-action from 2 to 3 by {user}`dependabot` in {pr}`3567`
|
||||
* Bump actions/upload-artifact from 3 to 4 by {user}`dependabot` in {pr}`3566`
|
||||
* Bump actions/setup-python from 4 to 5 by {user}`dependabot` in {pr}`3565`
|
||||
* updated several packages (pillow, jupyterlab, notebook, jupyterlab-lsp, jinja2, gitpython) by {user}`behackl` in {pr}`3593`
|
||||
* Update jupyter.rst by {user}`abul4fia` in {pr}`3630`
|
||||
* Bump black from 23.12.1 to 24.3.0 by {user}`dependabot` in {pr}`3649`
|
||||
* Bump cryptography from 42.0.0 to 42.0.4 by {user}`dependabot` in {pr}`3629`
|
||||
* Bump actions/cache from 3 to 4 by {user}`dependabot` in {pr}`3607`
|
||||
* Bump FedericoCarboni/setup-ffmpeg from 2 to 3 by {user}`dependabot` in {pr}`3608`
|
||||
* Bump ssciwr/setup-mesa-dist-win from 1 to 2 by {user}`dependabot` in {pr}`3609`
|
||||
* Bump idna from 3.6 to 3.7 by {user}`dependabot` in {pr}`3693`
|
||||
* Bump pillow from 10.2.0 to 10.3.0 by {user}`dependabot` in {pr}`3672`
|
||||
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci` in {pr}`3332`
|
||||
* Updated sphinx deps by {user}`JasonGrace2282` in {pr}`3720`
|
||||
|
||||
|
||||
## New Contributors
|
||||
* {user}`Lawqup` made their first contribution in {pr}`3461`
|
||||
* {user}`jcep` made their first contribution in {pr}`3477`
|
||||
* {user}`szchixy` made their first contribution in {pr}`3498`
|
||||
* {user}`PikaBlue107` made their first contribution in {pr}`3459`
|
||||
* {user}`yuan-xy` made their first contribution in {pr}`3509`
|
||||
* {user}`MathItYT` made their first contribution in {pr}`3516`
|
||||
* {user}`doaamuham` made their first contribution in {pr}`3499`
|
||||
* {user}`RobinPH` made their first contribution in {pr}`3428`
|
||||
* {user}`pauluhlenbruck` made their first contribution in {pr}`3513`
|
||||
* {user}`yashm277` made their first contribution in {pr}`3436`
|
||||
* {user}`TheMathematicFanatic` made their first contribution in {pr}`3408`
|
||||
* {user}`vvolhejn` made their first contribution in {pr}`3561`
|
||||
* {user}`hydromelvictor` made their first contribution in {pr}`3574`
|
||||
* {user}`dandavison` made their first contribution in {pr}`3568`
|
||||
* {user}`Gixtox` made their first contribution in {pr}`3528`
|
||||
* {user}`staghado` made their first contribution in {pr}`3429`
|
||||
* {user}`biinnnggggg` made their first contribution in {pr}`3692`
|
||||
* {user}`HairlessVillager` made their first contribution in {pr}`3674`
|
||||
* {user}`SirJamesClarkMaxwell` made their first contribution in {pr}`3675`
|
||||
* {user}`danielzsh` made their first contribution in {pr}`3703`
|
||||
* {user}`TheCrowned` made their first contribution in {pr}`3721`
|
||||
* {user}`MontroyJosh` made their first contribution in {pr}`3718`
|
||||
* {user}`amrear` made their first contribution in {pr}`3730`
|
||||
|
||||
**Full Changelog**: https://github.com/ManimCommunity/manim/compare/v0.18.0.post0...v0.18.1
|
||||
197
docs/source/changelog/0.19.1-changelog.md
Normal file
197
docs/source/changelog/0.19.1-changelog.md
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
---
|
||||
short-title: v0.19.1
|
||||
description: Changelog for Manim v0.19.1
|
||||
---
|
||||
|
||||
# v0.19.1
|
||||
|
||||
Date
|
||||
: December 01, 2025
|
||||
|
||||
## What's Changed
|
||||
|
||||
### New Features
|
||||
|
||||
* Introduce seed in `random_color` method to produce colors deterministically by {user}`ishu9bansal` in {pr}`4265`
|
||||
* Add support for arithmetic operators `//`, `%`, `*`, `**` and `/` on `ValueTracker` by {user}`fmuenkel` in {pr}`4351`
|
||||
* Add `TangentialArc` mobject by {user}`Brainsucker92` in {pr}`4469`
|
||||
|
||||
|
||||
### Fixed Bugs and Enhancements
|
||||
|
||||
* Fix environment formatting for Tex() mobject by {user}`fmuenkel` in {pr}`4159`
|
||||
* Improved consistency of rate_function implementations by {user}`BenKirkels` in {pr}`4144`
|
||||
* Make new `Code` mobject compatible with OpenGL renderer by {user}`behackl` in {pr}`4164`
|
||||
* Fix HSL color ordering in ManimColor by {user}`thehugwizard` in {pr}`4202`
|
||||
* Fix return type of `Polygram.get_vertex_groups()` and rename variables in `.round_corners()` by {user}`chopan050` in {pr}`4063`
|
||||
* Improve `Mobject.align_data` docstring by {user}`irvanalhaq9` in {pr}`4152`
|
||||
* Fix :meth:`VMobject.pointwise_become_partial` failing when `vmobject` is `self` by {user}`irvanalhaq9` in {pr}`4193`
|
||||
* Fix `add_points_as_corners` not connecting single point to existing path by {user}`irvanalhaq9` in {pr}`4219`
|
||||
* Complete typing for logger_utils.py by {user}`fmuenkel` in {pr}`4134`
|
||||
* Fix(graph): Allow any Line subclass as edge_type in Graph/DiGraph by {user}`Akshat-Mishra-py` in {pr}`4251`
|
||||
* Replace exceptions, remove unused parameters, and fix type hints in `Animation`, `ShowPartial`, `Create`, `ShowPassingFlash`, and `DrawBorderThenFill` by {user}`irvanalhaq9` in {pr}`4214`
|
||||
* Fix: `Axes` submobject colors are not being set properly by {user}`ishu9bansal` in {pr}`4291`
|
||||
* Refactor `Rotating` and add docstrings to `Mobject.rotate()` and `Rotating` by {user}`irvanalhaq9` in {pr}`4147`
|
||||
* Fix default config of `manim init project` to use correct `pixel_height` and `pixel_width` by {user}`StevenH34` in {pr}`4213`
|
||||
* Handle opacity and transparent images by {user}`henrikmidtiby` in {pr}`4313`
|
||||
* Gracefully fall back when version metadata is missing by {user}`mohiuddin-khan-shiam` in {pr}`4324`
|
||||
* Fix for issue 4255 - Do not clear points when the number of curves is zero by {user}`henrikmidtiby` in {pr}`4320`
|
||||
* Use utf-8 encoding to read generated .tex files. by {user}`OliverStrait` in {pr}`4334`
|
||||
* Add zero to vmobject points to remove negative zeros in `get_mobject_key` by {user}`elshorbagyx` in {pr}`4332`
|
||||
* Ensure `stroke_width` attribute of `SVGMobject` is not set to `None` by {user}`henrikmidtiby` in {pr}`4319`
|
||||
* Fix `Prism` incorrectly rendering with `dimensions=[2, 2, 2]` in OpenGL by {user}`ra1u` in {pr}`4003`
|
||||
* Fix `BraceLabel.change_label()` and document `BraceText` by {user}`henrikmidtiby` in {pr}`4347`
|
||||
* Include `Text.gradient` in hash to properly regenerate `Text` when its gradient changes by {user}`AbhilashaTandon` in {pr}`4099`
|
||||
* Fixed surface animations in OpenGL by {user}`nubDotDev` in {pr}`4286`
|
||||
* Add type hints and support for arithmetic operators `+` and `-` on `ValueTracker` by {user}`fmuenkel` in {pr}`4129`
|
||||
* Fix duplicate references in `Scene.mobjects` after `ReplacementTransform` with existing target mobject by {user}`irvanalhaq9` in {pr}`4242`
|
||||
* Optimize `always_redraw()` by reducing `Mobject` copying in `Mobject.become()` by {user}`chopan050` in {pr}`4357`
|
||||
* Enhance `manim cfg show` output and add info-level logging for config files read by {user}`xnov18` in {pr}`4375`
|
||||
* Let `Cube` use Bevel type line joints by {user}`nubDotDev` in {pr}`4361`
|
||||
* Properly define `init_points` methods for use in OpenGL instead of defining `init_points = generate_points` by {user}`chopan050` in {pr}`4360`
|
||||
* Allow passing a tuple to `buff` in `SurroundingRectangle` to specify buffer in x and y direction independently by {user}`nubDotDev` in {pr}`4390`
|
||||
* Rewrite `color_gradient` to always return a list of ManimColors by {user}`henrikmidtiby` in {pr}`4380`
|
||||
* Ensure leading whitespace does not change line height for lines in CodeMobject by {user}`behackl` in {pr}`4392`
|
||||
* Simplify the function `remove_invisible_chars` in `text_mobject.py` by {user}`henrikmidtiby` in {pr}`4394`
|
||||
* Fix some config options specified via `--config_file` not being respected properly by {user}`behackl` in {pr}`4401`
|
||||
* Fix: Correct resolution tuple order to (height, width) by {user}`Nikhil172913832` in {pr}`4440`
|
||||
* Ensure that start and end points are stored as float values in Line3D by {user}`SirJamesClarkMaxwell` in {pr}`4080`
|
||||
* OpenGL: Fix iterated nesting in `DecimalNumber.set_value` by {user}`henrikmidtiby` in {pr}`4373`
|
||||
* Update default resolution in CLI to match Manim’s 1920x1080 default settings by {user}`SASHAKT1290` in {pr}`4452`
|
||||
* Better parsing of color styles in CodeMobject by {user}`SirJamesClarkMaxwell` in {pr}`4454`
|
||||
* Allow selection of all scenes to render using '*' by {user}`NightyStudios` in {pr}`4470`
|
||||
* Prevent mutation of `about_point` in `apply_points_function_about_point` by {user}`Morkunas` in {pr}`4478`
|
||||
* Fix behavior of `Mobject.suspend_updating`: when only suspending parent mobject, let children continue updating by {user}`behackl` in {pr}`4402`
|
||||
* Allow passing a `buff` to `LabeledDot` by {user}`nubDotDev` in {pr}`4403`
|
||||
* Pass ndarrays to `mapbox_earcut.triangulate_float32()` to fix `TypeError` in `mapbox_earcut==2.0.0` by {user}`GuiCT` in {pr}`4479`
|
||||
* Fix duplicated arrow tips in DashedVMobject (issue #3220) by {user}`jakekinchen` in {pr}`4484`
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* Add docstring to :meth:`.Mobject.get_family` by {user}`irvanalhaq9` in {pr}`4127`
|
||||
* Fix link formatting and clarify the distinction between Manim versions in index.rst by {user}`irvanalhaq9` in {pr}`4131`
|
||||
* Add instructions for installing system utilities `cairo` and `pkg-config` via Homebrew on MacOS by {user}`behackl` in {pr}`4146`
|
||||
* Add missing line break in Code of Conduct's conflict of interest policy by {user}`Hasan-Mesbaul-Ali-Taher` in {pr}`4185`
|
||||
* Fix links to Pango website by {user}`ragibson` in {pr}`4217`
|
||||
* Replace poetry with uv in the README by {user}`xinoehp512` in {pr}`4226`
|
||||
* Improve docstring for `interpolate` method in `Mobject` class by {user}`irvanalhaq9` in {pr}`4149`
|
||||
* Add docstrings to `Line` and remove `None` handling for `path_arc` parameter by {user}`irvanalhaq9` in {pr}`4223`
|
||||
* Add docstring to :meth:`Mobject.family_members_with_points` by {user}`irvanalhaq9` in {pr}`4128`
|
||||
* Update incorrect docstring for :attr:`ManimConfig.gui_location` property by {user}`SAYAN02-DEV` in {pr}`4254`
|
||||
* Fix formatting of color space documentation by {user}`behackl` in {pr}`4274`
|
||||
* Enhance and Paraphrase Description of ManimCE in README.md by {user}`irvanalhaq9` in {pr}`4141`
|
||||
* docs: add explanation about the rate_func in the custom animation by {user}`pedropxoto` in {pr}`4278`
|
||||
* Fixed artifact in docstring of Animation by {user}`barollet` in {pr}`4283`
|
||||
* Rename update function `dot_position` to `update_label` in `.add_updater` example by {user}`irvanalhaq9` in {pr}`4196`
|
||||
* Fix Microsoft typo in `TexFontTemplateLibrary` scene in `example_scenes/advanced_tex_fonts.py` by {user}`alterdim` in {pr}`4305`
|
||||
* Improved readability, grammar, as well as added docstrings for consistency by {user}`NASAnerd05` in {pr}`4267`
|
||||
* Add docstrings for `ChangingDecimal` and `ChangeDecimalToValue` by {user}`haveheartt` in {pr}`4346`
|
||||
* Fix Sphinx exceptions when trying to build documentation via latex / as pdf by {user}`behackl` in {pr}`4370`
|
||||
* Added license information to documentation landing page by {user}`Nikil-D-Gr8` in {pr}`3986`
|
||||
* Set the default Python version to 3.13 in the uv installation guide by {user}`henrikmidtiby` in {pr}`4480`
|
||||
|
||||
|
||||
### Maintenance and Testing
|
||||
|
||||
* Change project management tool from poetry to uv by {user}`behackl` in {pr}`4138`
|
||||
* Re-add ffmpeg as dependency within Docker image by {user}`behackl` in {pr}`4150`
|
||||
* Add tests for Matrix, DecimalMatrix, IntegerMatrix by {user}`pdrzan` in {pr}`4279`
|
||||
* Add tests for polylabel utility by {user}`giolucasd` in {pr}`4269`
|
||||
* Add support for `pycodestyle W` rule in Ruff by {user}`KaiqueDultra` in {pr}`4276`
|
||||
* Fix files with few MyPy typing errors by {user}`henrikmidtiby` in {pr}`4263`
|
||||
* Explicitly mention all files that mypy should ignore in the `mypy.ini` configuration file by {user}`henrikmidtiby` in {pr}`4306`
|
||||
* Remove dead code from `scene.py` and `vector_space_scene.py` by {user}`henrikmidtiby` in {pr}`4310`
|
||||
* Add type annotations to `scene.py` and `vector_space_scene.py` by {user}`henrikmidtiby` in {pr}`4260`
|
||||
* Replace setup-texlive-action in CI workflow by {user}`behackl` in {pr}`4326`
|
||||
* Adding type annotations to polyhedra.py and matrix.py by {user}`henrikmidtiby` in {pr}`4322`
|
||||
* Handling typing errors in text/numbers.py by {user}`henrikmidtiby` in {pr}`4317`
|
||||
* Move `configure_pygui` into a `Scene` method and remove `manim.gui` by {user}`chopan050` in {pr}`4314`
|
||||
* Add typing annotations to svg_mobject.py by {user}`henrikmidtiby` in {pr}`4318`
|
||||
* Add type annotations to `mobject/svg/brace.py` and default to `label_constructor=Text` in `BraceText` by {user}`henrikmidtiby` in {pr}`4309`
|
||||
* Add classes `MethodWithArgs`, `SceneInteractContinue` and `SceneInteractRerun` inside new module `manim.data_structures` by {user}`chopan050` in {pr}`4315`
|
||||
* Fix typo in import of OpenGLCamera in `utils/hashing.py` by {user}`fmuenkel` in {pr}`4352`
|
||||
* Add type annotations to `manim/renderer/shader.py` by {user}`henrikmidtiby` in {pr}`4350`
|
||||
* Add type annotations to `tex_mobject.py` by {user}`henrikmidtiby` in {pr}`4355`
|
||||
* Add type annotations to `three_d_camera.py` by {user}`henrikmidtiby` in {pr}`4356`
|
||||
* Revert change of default value for tex_environment by {user}`henrikmidtiby` in {pr}`4358`
|
||||
* Add type hints to `scene_file_writer.py`, `section.py`, and `zoomed_scene.py` by {user}`fmuenkel` in {pr}`4133`
|
||||
* Add type annotations for most of `camera` and `mobject.graphing` by {user}`henrikmidtiby` in {pr}`4125`
|
||||
* Add `VectorNDLike` type aliases by {user}`chopan050` in {pr}`4068`
|
||||
* Add type annotations to `dot_cloud.py`, `vectorized_mobject_rendering.py` and `opengl_three_dimensions.py` by {user}`henrikmidtiby` in {pr}`4359`
|
||||
* Add type annotations to `indication.py` by {user}`henrikmidtiby` in {pr}`4367`
|
||||
* Add type annotations to `composition.py` by {user}`henrikmidtiby` in {pr}`4366`
|
||||
* Add type annotations to `growing.py` by {user}`henrikmidtiby` in {pr}`4368`
|
||||
* Add type annotations to `movement.py` by {user}`henrikmidtiby` in {pr}`4371`
|
||||
* Exclude check for cyclic imports by CodeQL by {user}`behackl` in {pr}`4384`
|
||||
* Refactor imports from `collections.abc`, `typing` and `typing_extensions` for Python 3.9 by {user}`chopan050` in {pr}`4353`
|
||||
* Add type annotations to `opengl_renderer_window.py` by {user}`fmuenkel` in {pr}`4363`
|
||||
* Rename `SceneFileWriter.save_final_image()` to `save_image()` by {user}`fmuenkel` in {pr}`4378`
|
||||
* Add type annotations to `text_mobject.py` by {user}`henrikmidtiby` in {pr}`4381`
|
||||
* Rename types like `RGBA_Array_Float` to `FloatRGBA` and add types like `FloatRGBA_Array` by {user}`chopan050` in {pr}`4386`
|
||||
* Add type annotations to `opengl_geometry.py` by {user}`henrikmidtiby` in {pr}`4396`
|
||||
* Add type annotations to `moving_camera.py` by {user}`henrikmidtiby` in {pr}`4397`
|
||||
* Add type annotations to `opengl_mobject.py` by {user}`RBerga06` in {pr}`4398`
|
||||
* Fix failing pre-commit tests by {user}`cclauss` in {pr}`4434`
|
||||
* Add type annotations to `cairo_renderer.py` by {user}`fmuenkel` in {pr}`4393`
|
||||
* Fix type errors and add typings for `Mobject.apply_function()`, its derivatives, and other utility functions by {user}`godalming123` in {pr}`4228`
|
||||
* Bump macOS image from deprecated macos-13 to macos-15-intel by {user}`chopan050` in {pr}`4481`
|
||||
* Prepare new release `v0.19.1` and bump minimum required Python version to 3.10 by {user}`behackl` in {pr}`4490`
|
||||
|
||||
|
||||
### Dependency Version Changes
|
||||
|
||||
* Bump typing extensions minimum version by {user}`JasonGrace2282` in {pr}`4121`
|
||||
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4122`
|
||||
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4140`
|
||||
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4148`
|
||||
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4181`
|
||||
* Bump astral-sh/setup-uv from 5 to 6 by {user}`dependabot`[bot] in {pr}`4234`
|
||||
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4204`
|
||||
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4391`
|
||||
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4405`
|
||||
* Bump actions/setup-python from 5 to 6 by {user}`dependabot`[bot] in {pr}`4433`
|
||||
* Bump actions/checkout from 4 to 5 by {user}`dependabot`[bot] in {pr}`4418`
|
||||
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4409`
|
||||
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4460`
|
||||
* [pre-commit.ci] pre-commit autoupdate by {user}`pre-commit-ci`[bot] in {pr}`4467`
|
||||
* Bump github/codeql-action from 3 to 4 by {user}`dependabot`[bot] in {pr}`4466`
|
||||
* Bump astral-sh/setup-uv from 6 to 7 by {user}`dependabot`[bot] in {pr}`4465`
|
||||
* Bump actions/upload-artifact from 4 to 5 by {user}`dependabot`[bot] in {pr}`4464`
|
||||
|
||||
|
||||
## New Contributors
|
||||
* {user}`BenKirkels` made their first contribution in {pr}`4144`
|
||||
* {user}`Hasan-Mesbaul-Ali-Taher` made their first contribution in {pr}`4185`
|
||||
* {user}`ragibson` made their first contribution in {pr}`4217`
|
||||
* {user}`thehugwizard` made their first contribution in {pr}`4202`
|
||||
* {user}`xinoehp512` made their first contribution in {pr}`4226`
|
||||
* {user}`SAYAN02-DEV` made their first contribution in {pr}`4254`
|
||||
* {user}`Akshat-Mishra-py` made their first contribution in {pr}`4251`
|
||||
* {user}`pdrzan` made their first contribution in {pr}`4279`
|
||||
* {user}`pedropxoto` made their first contribution in {pr}`4278`
|
||||
* {user}`giolucasd` made their first contribution in {pr}`4269`
|
||||
* {user}`KaiqueDultra` made their first contribution in {pr}`4276`
|
||||
* {user}`ishu9bansal` made their first contribution in {pr}`4291`
|
||||
* {user}`StevenH34` made their first contribution in {pr}`4213`
|
||||
* {user}`alterdim` made their first contribution in {pr}`4305`
|
||||
* {user}`mohiuddin-khan-shiam` made their first contribution in {pr}`4324`
|
||||
* {user}`elshorbagyx` made their first contribution in {pr}`4332`
|
||||
* {user}`NASAnerd05` made their first contribution in {pr}`4267`
|
||||
* {user}`ra1u` made their first contribution in {pr}`4003`
|
||||
* {user}`AbhilashaTandon` made their first contribution in {pr}`4099`
|
||||
* {user}`nubDotDev` made their first contribution in {pr}`4286`
|
||||
* {user}`haveheartt` made their first contribution in {pr}`4346`
|
||||
* {user}`xnov18` made their first contribution in {pr}`4375`
|
||||
* {user}`Nikil-D-Gr8` made their first contribution in {pr}`3986`
|
||||
* {user}`RBerga06` made their first contribution in {pr}`4398`
|
||||
* {user}`Nikhil172913832` made their first contribution in {pr}`4440`
|
||||
* {user}`SASHAKT1290` made their first contribution in {pr}`4452`
|
||||
* {user}`Brainsucker92` made their first contribution in {pr}`4469`
|
||||
* {user}`NightyStudios` made their first contribution in {pr}`4470`
|
||||
* {user}`Morkunas` made their first contribution in {pr}`4478`
|
||||
* {user}`GuiCT` made their first contribution in {pr}`4479`
|
||||
* {user}`godalming123` made their first contribution in {pr}`4228`
|
||||
* {user}`jakekinchen` made their first contribution in {pr}`4484`
|
||||
|
||||
**Full Changelog**: https://github.com/ManimCommunity/manim/compare/v0.19.0...v0.19.1
|
||||
41
docs/source/changelog/0.19.2-changelog.md
Normal file
41
docs/source/changelog/0.19.2-changelog.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
short-title: v0.19.2
|
||||
description: Changelog for Manim v0.19.2
|
||||
---
|
||||
|
||||
# v0.19.2
|
||||
|
||||
Date
|
||||
: January 17, 2026
|
||||
|
||||
|
||||
## What's Changed
|
||||
### Highlights 🌟
|
||||
* Add support for Python 3.14, bump minimum Python to 3.11 and av to 14.0.1 by {user}`behackl` in {pr}`4385`
|
||||
|
||||
### Bug Fixes 🐛
|
||||
* Fix argument passed to `get_hash_from_play_call` in hashing by {user}`judenimo` in {pr}`4524`
|
||||
* Fix incorrect `Circle.point_at_angle` calculation by {user}`Swarnlataaa` in {pr}`4438`
|
||||
|
||||
### Testing 🧪
|
||||
* Test on Apple Silicon ARM64 by {user}`cclauss` in {pr}`4496`
|
||||
|
||||
### Code Quality & Refactoring 🧹
|
||||
* Add ruff rules PERF for performance by {user}`cclauss` in {pr}`4492`
|
||||
* Remove deprecation warning from pytest "np.trapz" -> "np.trapezoid" by {user}`henrikmidtiby` in {pr}`4513`
|
||||
* Bump Python target versions of both mypy and ruff by {user}`behackl` in {pr}`4520`
|
||||
* Replace legacy numpy usage -- ruff rule NPY002 by {user}`cclauss` in {pr}`4516`
|
||||
* Add `.github/release.yml` for improved classifications in automatically generated changelogs by {user}`behackl` in {pr}`4526`
|
||||
* Check and bump lower version requirements for dependencies by {user}`henrikmidtiby` in {pr}`4529`
|
||||
|
||||
### Type Hints 📝
|
||||
* Add type annotations to `three_dimensions.py` by {user}`henrikmidtiby` in {pr}`4497`
|
||||
|
||||
### Other Changes
|
||||
* Prepare new release `v0.19.2` by {user}`behackl` in {pr}`4528`
|
||||
|
||||
## New Contributors
|
||||
* {user}`judenimo` made their first contribution in {pr}`4524`
|
||||
* {user}`Swarnlataaa` made their first contribution in {pr}`4438`
|
||||
|
||||
**Full Changelog**: https://github.com/ManimCommunity/manim/compare/v0.19.1...v0.19.2
|
||||
86
docs/source/changelog/0.20.0-changelog.md
Normal file
86
docs/source/changelog/0.20.0-changelog.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
---
|
||||
short-title: v0.20.0
|
||||
description: Changelog for v0.20.0
|
||||
---
|
||||
|
||||
# v0.20.0
|
||||
|
||||
Date
|
||||
: February 20, 2026
|
||||
|
||||
|
||||
## What's Changed
|
||||
### Breaking Changes 🚨
|
||||
* Fix `ImageMobject` 3D rotation/flipping and remove resampling algorithms `lanczos` (`antialias`), `box` and `hamming` by {user}`chopan050` in {pr}`4266`
|
||||
* Fix `YELLOW_C` and add `PURE_CYAN`, `PURE_MAGENTA` and `PURE_YELLOW` by {user}`chopan050` in {pr}`4562`
|
||||
|
||||
### Highlights 🌟
|
||||
* Rewrite MathTex to make it more robust regarding splitting by {user}`henrikmidtiby` in {pr}`4515`
|
||||
|
||||
The MathTex implementation has been updated to make it more robust and fix a number of issues.
|
||||
A beneficial side effect is that named groups in svg files can now be accessed through SVGMobject.
|
||||
|
||||
* Add new Animation Builder `Mobject.always` by {user}`JasonGrace2282` in {pr}`4594`
|
||||
|
||||
This new feature is a convenience wrapper around `add_updater` that allows adding
|
||||
updaters to a mobject in an intuitive and easy-to-read way. Example usage in a scene:
|
||||
```python
|
||||
d = Dot()
|
||||
s = Square()
|
||||
d.always.next_to(s, UP)
|
||||
self.add(s, d)
|
||||
self.play(s.animate.to_edge(LEFT))
|
||||
```
|
||||
|
||||
|
||||
### New Features ✨
|
||||
* Add a `seed` config option + `--seed` CLI option for reproducible randomness in rendered scenes by {user}`arnaud-ma` in {pr}`4532`
|
||||
|
||||
### Enhancements 🚀
|
||||
* Enable `strict=True` for `zip()` where safe by {user}`Oll-iver` in {pr}`4547`
|
||||
|
||||
### Bug Fixes 🐛
|
||||
* using `color` instead of `fill_color` with MathTeX for node labels by {user}`Schefflera-Arboricola` in {pr}`4501`
|
||||
* fix: infinite recursion caused by accessing color of a highlighted Ta… by {user}`BHearron` in {pr}`4435`
|
||||
* Prevent potential `UnboundLocalError` in `PolarPlane` by {user}`RinZ27` in {pr}`4557`
|
||||
* Fixed division by 0 in `turn_animation_into_updater` by {user}`SoldierSacha` in {pr}`4567`
|
||||
* Fix TOCTOU Race Conditions when creating directories by {user}`SoldierSacha` in {pr}`4587`
|
||||
* Resolve more race conditions potentially happening during directory creation by {user}`SoldierSacha` in {pr}`4589`
|
||||
* Fix `c2p`/`coords_to_point` method call with single flat list or 1D array input by {user}`danielalanbates` in {pr}`4596`
|
||||
|
||||
### Documentation 📚
|
||||
* Enable rendered documentation of `RandomColorGenerator` by {user}`arnaud-ma` in {pr}`4533`
|
||||
* Remove pin to Python 3.13 in installation docs by {user}`chopan050` in {pr}`4534`
|
||||
* Fix broken aquabeam OpenGL link using Wayback Machine by {user}`behackl` in {pr}`4545`
|
||||
* Add type annotations and docstrings in `opengl_renderer.py` by {user}`arnaud-ma` in {pr}`4537`
|
||||
* docs: improve `TransformFromCopy` docstring by {user}`GoThrones` in {pr}`4597`
|
||||
|
||||
### Infrastructure & Build 🔨
|
||||
* Install missing dependencies in release pipeline by {user}`behackl` in {pr}`4531`
|
||||
|
||||
### Code Quality & Refactoring 🧹
|
||||
* Rework and consolidate release changelog script, add previously skipped changelog entries by {user}`behackl` in {pr}`4568`
|
||||
* Remove `__future__.annotations` from required imports by {user}`JasonGrace2282` in {pr}`4571`
|
||||
* Cleaned up `mypy.ini` by {user}`henrikmidtiby` in {pr}`4584`
|
||||
* Add `py.typed` to declare manim as having type hints by {user}`Timmmm` in {pr}`4553`
|
||||
* Fix assertion in `ImageMobjectFromCamera.interpolate_color()` by {user}`chopan050` in {pr}`4593`
|
||||
* Reduce dependency on scipy - replace `scipy.special.comb` with `math.comb` by {user}`fmuenkel` in {pr}`4598`
|
||||
|
||||
### Type Hints 📝
|
||||
* Add type annotations to `rotation.py` by {user}`fmuenkel` in {pr}`4535`
|
||||
* Add type annotations to `opengl_compatibility.py` by {user}`fmuenkel` in {pr}`4585`
|
||||
* Add type annotations to `image_mobject.py` by {user}`henrikmidtiby` in {pr}`4458`
|
||||
* Add type annotations to `opengl_image_mobject.py` by {user}`fmuenkel` in {pr}`4536`
|
||||
* Add type annotations to `point_cloud_mobject.py` by {user}`fmuenkel` in {pr}`4586`
|
||||
|
||||
## New Contributors
|
||||
* {user}`arnaud-ma` made their first contribution in {pr}`4533`
|
||||
* {user}`Schefflera-Arboricola` made their first contribution in {pr}`4501`
|
||||
* {user}`BHearron` made their first contribution in {pr}`4435`
|
||||
* {user}`RinZ27` made their first contribution in {pr}`4557`
|
||||
* {user}`SoldierSacha` made their first contribution in {pr}`4567`
|
||||
* {user}`Oll-iver` made their first contribution in {pr}`4547`
|
||||
* {user}`GoThrones` made their first contribution in {pr}`4597`
|
||||
* {user}`danielalanbates` made their first contribution in {pr}`4596`
|
||||
|
||||
**Full Changelog**: [Compare view](https://github.com/ManimCommunity/manim/compare/v0.19.2...v0.20.0)
|
||||
|
|
@ -58,7 +58,7 @@ extensions = [
|
|||
# Automatically generate stub pages when using the .. autosummary directive
|
||||
autosummary_generate = True
|
||||
|
||||
myst_enable_extensions = ["colon_fence", "amsmath"]
|
||||
myst_enable_extensions = ["colon_fence", "amsmath", "deflist"]
|
||||
|
||||
# redirects (for moved / deleted pages)
|
||||
redirects = {
|
||||
|
|
@ -161,7 +161,8 @@ latex_engine = "lualatex"
|
|||
# external links
|
||||
extlinks = {
|
||||
"issue": ("https://github.com/ManimCommunity/manim/issues/%s", "#%s"),
|
||||
"pr": ("https://github.com/ManimCommunity/manim/pull/%s", "#%s"),
|
||||
"pr": ("https://github.com/ManimCommunity/manim/pull/%s", "PR #%s"),
|
||||
"user": ("https://github.com/%s", "@%s"),
|
||||
}
|
||||
|
||||
# opengraph settings
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ or specific OpenGL classes like `OpenGLSurface`, but documentation for some of
|
|||
them is available in form of docstrings
|
||||
[in the source code](https://github.com/ManimCommunity/manim/tree/main/manim/mobject/opengl).
|
||||
|
||||
Furthermore, [this user guide by *aquabeam*](https://www.aquabeam.me/manim/opengl_guide/)
|
||||
Furthermore, [this user guide by *aquabeam*](https://web.archive.org/web/20250708135737/https://www.aquabeam.me/manim/opengl_guide/)
|
||||
can be helpful to get started using the OpenGL renderer.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -389,8 +389,9 @@ 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 by
|
||||
selecting parts of the tex code. In this example, we set the color
|
||||
of the ``\bigstar`` using :func:`~.set_color_by_tex`:
|
||||
using :func:`~.set_color_by_tex`, which matches the argument exactly against
|
||||
the strings passed to the constructor. In this example, we color the
|
||||
``\bigstar`` part:
|
||||
|
||||
.. manim:: LaTeXSubstrings
|
||||
:save_last_frame:
|
||||
|
|
@ -398,25 +399,13 @@ of the ``\bigstar`` using :func:`~.set_color_by_tex`:
|
|||
class LaTeXSubstrings(Scene):
|
||||
def construct(self):
|
||||
tex = Tex('Hello', r'$\bigstar$', r'\LaTeX', font_size=144)
|
||||
tex.set_color_by_tex('igsta', RED)
|
||||
tex.set_color_by_tex(r'$\bigstar$', RED)
|
||||
self.add(tex)
|
||||
|
||||
Note that :func:`~.set_color_by_tex` colors the entire substring containing
|
||||
the Tex, not just the specific symbol or Tex expression. Consider the following example:
|
||||
|
||||
.. manim:: IncorrectLaTeXSubstringColoring
|
||||
:save_last_frame:
|
||||
|
||||
class IncorrectLaTeXSubstringColoring(Scene):
|
||||
def construct(self):
|
||||
equation = MathTex(
|
||||
r"e^x = x^0 + x^1 + \frac{1}{2} x^2 + \frac{1}{6} x^3 + \cdots + \frac{1}{n!} x^n + \cdots"
|
||||
)
|
||||
equation.set_color_by_tex("x", YELLOW)
|
||||
self.add(equation)
|
||||
|
||||
As you can see, this colors the entire equation yellow, contrary to what
|
||||
may be expected. To color only ``x`` yellow, we have to do the following:
|
||||
Because :func:`~.set_color_by_tex` requires an exact match, it cannot directly
|
||||
target a token inside a string that was passed as a single argument. To color
|
||||
every ``x`` in a formula, use ``substrings_to_isolate`` to split the string at
|
||||
each occurrence first:
|
||||
|
||||
.. manim:: CorrectLaTeXSubstringColoring
|
||||
:save_last_frame:
|
||||
|
|
@ -424,25 +413,33 @@ may be expected. To color only ``x`` yellow, we have to do the following:
|
|||
class CorrectLaTeXSubstringColoring(Scene):
|
||||
def construct(self):
|
||||
equation = MathTex(
|
||||
r"e^x = x^0 + x^1 + \frac{1}{2} x^2 + \frac{1}{6} x^3 + \cdots + \frac{1}{n!} x^n + \cdots",
|
||||
r"e^{x} = x^0 + x^1 + \frac{1}{2} x^2 + \frac{1}{6} x^3 + \cdots + \frac{1}{n!} x^n + \cdots",
|
||||
substrings_to_isolate="x"
|
||||
)
|
||||
equation.set_color_by_tex("x", YELLOW)
|
||||
self.add(equation)
|
||||
|
||||
By setting ``substrings_to_isolate`` to ``x``, we split up the
|
||||
:class:`~.MathTex` into substrings automatically and isolate the ``x`` components
|
||||
into individual substrings. Only then can :meth:`~.set_color_by_tex` be used
|
||||
to achieve the desired result.
|
||||
Each isolated occurrence of ``x`` becomes its own sub-mobject that
|
||||
:meth:`~.set_color_by_tex` can match exactly.
|
||||
If one of the ``substrings_to_isolate`` is in a sub or superscript, it needs
|
||||
to be enclosed by curly brackets.
|
||||
|
||||
Note that Manim also supports a custom syntax that allows splitting
|
||||
a TeX string into substrings easily: simply enclose parts of your formula
|
||||
that you want to isolate with double braces. In the string
|
||||
``MathTex(r"{{ a^2 }} + {{ b^2 }} = {{ c^2 }}")``, the rendered mobject
|
||||
``MathTex(r"{{ a^2 }} + {{ b^2 }} = {{ c^2 }}")``, the rendered mobject
|
||||
will consist of the substrings ``a^2``, ``+``, ``b^2``, ``=``, and ``c^2``.
|
||||
This makes transformations between similar text fragments easy
|
||||
to write using :class:`~.TransformMatchingTex`.
|
||||
|
||||
For Manim to recognise a ``{{`` as a group opener, it must appear either
|
||||
at the very start of the string or be immediately preceded by a whitespace
|
||||
character. This means that ``{{`` embedded directly after non-whitespace
|
||||
LaTeX — such as ``\frac{{{n}}}{k}`` or ``a^{{2}}`` — is left untouched,
|
||||
which prevents accidental splitting of ordinary nested-brace expressions.
|
||||
To stop a leading ``{{`` from being treated as a group opener, insert a
|
||||
space between the two braces: ``{{ ... }}`` → ``{ { ... } }``.
|
||||
|
||||
Using ``index_labels`` to work with complicated strings
|
||||
=======================================================
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,13 @@ For our image ``manimcommunity/manim``, there are the following tags:
|
|||
``-p`` (preview file) and ``-f`` (show output file in the file browser)
|
||||
are not supported.
|
||||
|
||||
.. note::
|
||||
|
||||
The Docker image ships with a minimal TeX Live installation. In particular,
|
||||
``ctex`` is not installed by default. If your scenes rely on
|
||||
``TexTemplateLibrary.ctex``, install it in the container via
|
||||
``tlmgr install ctex``.
|
||||
|
||||
|
||||
Basic usage of the Docker container
|
||||
-----------------------------------
|
||||
|
|
|
|||
|
|
@ -329,3 +329,13 @@ version satisfies the requirement. Change the line to, for example
|
|||
to pin the python version to `3.12`. Finally, run `uv sync`, and your
|
||||
environment is updated!
|
||||
:::
|
||||
|
||||
:::{dropdown} Installing the latest development version
|
||||
If you want to install the latest (potentially unstable!)
|
||||
development version of Manim from our source repository
|
||||
[on GitHub](https://github.com/ManimCommunity/manim), then
|
||||
simply run
|
||||
```bash
|
||||
uv add git+https://github.com/ManimCommunity/manim.git@main
|
||||
```
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import logging
|
|||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import Iterable, Iterator, Mapping, MutableMapping
|
||||
from collections.abc import Iterator, Mapping, MutableMapping
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, NoReturn
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ def make_config_parser(
|
|||
return parser
|
||||
|
||||
|
||||
def _determine_quality(qual: str) -> str:
|
||||
def _determine_quality(qual: str | None) -> str:
|
||||
for quality, values in constants.QUALITIES.items():
|
||||
if values["flag"] is not None and values["flag"] == qual:
|
||||
return quality
|
||||
|
|
@ -338,6 +338,7 @@ class ManimConfig(MutableMapping):
|
|||
|
||||
def __contains__(self, key: object) -> bool:
|
||||
try:
|
||||
assert isinstance(key, str)
|
||||
self.__getitem__(key)
|
||||
return True
|
||||
except AttributeError:
|
||||
|
|
@ -428,7 +429,7 @@ class ManimConfig(MutableMapping):
|
|||
# Deepcopying the underlying dict is enough because all properties
|
||||
# either read directly from it or compute their value on the fly from
|
||||
# values read directly from it.
|
||||
c._d = copy.deepcopy(self._d, memo)
|
||||
c._d = copy.deepcopy(self._d, memo) # type: ignore[arg-type]
|
||||
return c
|
||||
|
||||
# helper type-checking methods
|
||||
|
|
@ -655,13 +656,15 @@ class ManimConfig(MutableMapping):
|
|||
"window_size"
|
||||
] # if not "default", get a tuple of the position
|
||||
if window_size != "default":
|
||||
window_size = tuple(map(int, re.split(r"[;,\-]", window_size)))
|
||||
self.window_size = window_size
|
||||
window_size_numbers = tuple(map(int, re.split(r"[;,\-]", window_size)))
|
||||
self.window_size = window_size_numbers
|
||||
else:
|
||||
self.window_size = window_size
|
||||
|
||||
# plugins
|
||||
plugins = parser["CLI"].get("plugins", fallback="", raw=True)
|
||||
plugins = [] if plugins == "" else plugins.split(",")
|
||||
self.plugins = plugins
|
||||
plugin_list = [] if plugins is None or plugins == "" else plugins.split(",")
|
||||
self.plugins = plugin_list
|
||||
# the next two must be set AFTER digesting pixel_width and pixel_height
|
||||
self["frame_height"] = parser["CLI"].getfloat("frame_height", 8.0)
|
||||
width = parser["CLI"].getfloat("frame_width", None)
|
||||
|
|
@ -671,31 +674,31 @@ class ManimConfig(MutableMapping):
|
|||
self["frame_width"] = width
|
||||
|
||||
# other logic
|
||||
val = parser["CLI"].get("tex_template_file")
|
||||
if val:
|
||||
self.tex_template_file = val
|
||||
tex_template_file = parser["CLI"].get("tex_template_file")
|
||||
if tex_template_file:
|
||||
self.tex_template_file = Path(tex_template_file)
|
||||
|
||||
val = parser["CLI"].get("progress_bar")
|
||||
if val:
|
||||
self.progress_bar = val
|
||||
progress_bar = parser["CLI"].get("progress_bar")
|
||||
if progress_bar:
|
||||
self.progress_bar = progress_bar
|
||||
|
||||
val = parser["ffmpeg"].get("loglevel")
|
||||
if val:
|
||||
self.ffmpeg_loglevel = val
|
||||
ffmpeg_loglevel = parser["ffmpeg"].get("loglevel")
|
||||
if ffmpeg_loglevel:
|
||||
self.ffmpeg_loglevel = ffmpeg_loglevel
|
||||
|
||||
try:
|
||||
val = parser["jupyter"].getboolean("media_embed")
|
||||
media_embed = parser["jupyter"].getboolean("media_embed")
|
||||
except ValueError:
|
||||
val = None
|
||||
self.media_embed = val
|
||||
media_embed = None
|
||||
self.media_embed = media_embed
|
||||
|
||||
val = parser["jupyter"].get("media_width")
|
||||
if val:
|
||||
self.media_width = val
|
||||
media_width = parser["jupyter"].get("media_width")
|
||||
if media_width:
|
||||
self.media_width = media_width
|
||||
|
||||
val = parser["CLI"].get("quality", fallback="", raw=True)
|
||||
if val:
|
||||
self.quality = _determine_quality(val)
|
||||
quality = parser["CLI"].get("quality", fallback="", raw=True)
|
||||
if quality:
|
||||
self.quality = _determine_quality(quality)
|
||||
|
||||
return self
|
||||
|
||||
|
|
@ -1044,7 +1047,7 @@ class ManimConfig(MutableMapping):
|
|||
logger.setLevel(val)
|
||||
|
||||
@property
|
||||
def format(self) -> str:
|
||||
def format(self) -> str | None:
|
||||
"""File format; "png", "gif", "mp4", "webm" or "mov"."""
|
||||
return self._d["format"]
|
||||
|
||||
|
|
@ -1076,7 +1079,7 @@ class ManimConfig(MutableMapping):
|
|||
logging.getLogger("libav").setLevel(self.ffmpeg_loglevel)
|
||||
|
||||
@property
|
||||
def media_embed(self) -> bool:
|
||||
def media_embed(self) -> bool | None:
|
||||
"""Whether to embed videos in Jupyter notebook."""
|
||||
return self._d["media_embed"]
|
||||
|
||||
|
|
@ -1112,8 +1115,10 @@ class ManimConfig(MutableMapping):
|
|||
self._set_pos_number("pixel_height", value, False)
|
||||
|
||||
@property
|
||||
def aspect_ratio(self) -> int:
|
||||
def aspect_ratio(self) -> float:
|
||||
"""Aspect ratio (width / height) in pixels (--resolution, -r)."""
|
||||
assert isinstance(self._d["pixel_width"], int)
|
||||
assert isinstance(self._d["pixel_height"], int)
|
||||
return self._d["pixel_width"] / self._d["pixel_height"]
|
||||
|
||||
@property
|
||||
|
|
@ -1137,22 +1142,22 @@ class ManimConfig(MutableMapping):
|
|||
@property
|
||||
def frame_y_radius(self) -> float:
|
||||
"""Half the frame height (no flag)."""
|
||||
return self._d["frame_height"] / 2
|
||||
return self._d["frame_height"] / 2 # type: ignore[operator]
|
||||
|
||||
@frame_y_radius.setter
|
||||
def frame_y_radius(self, value: float) -> None:
|
||||
self._d.__setitem__("frame_y_radius", value) or self._d.__setitem__(
|
||||
self._d.__setitem__("frame_y_radius", value) or self._d.__setitem__( # type: ignore[func-returns-value]
|
||||
"frame_height", 2 * value
|
||||
)
|
||||
|
||||
@property
|
||||
def frame_x_radius(self) -> float:
|
||||
"""Half the frame width (no flag)."""
|
||||
return self._d["frame_width"] / 2
|
||||
return self._d["frame_width"] / 2 # type: ignore[operator]
|
||||
|
||||
@frame_x_radius.setter
|
||||
def frame_x_radius(self, value: float) -> None:
|
||||
self._d.__setitem__("frame_x_radius", value) or self._d.__setitem__(
|
||||
self._d.__setitem__("frame_x_radius", value) or self._d.__setitem__( # type: ignore[func-returns-value]
|
||||
"frame_width", 2 * value
|
||||
)
|
||||
|
||||
|
|
@ -1285,7 +1290,7 @@ class ManimConfig(MutableMapping):
|
|||
|
||||
@frame_size.setter
|
||||
def frame_size(self, value: tuple[int, int]) -> None:
|
||||
self._d.__setitem__("pixel_width", value[0]) or self._d.__setitem__(
|
||||
self._d.__setitem__("pixel_width", value[0]) or self._d.__setitem__( # type: ignore[func-returns-value]
|
||||
"pixel_height", value[1]
|
||||
)
|
||||
|
||||
|
|
@ -1295,7 +1300,7 @@ class ManimConfig(MutableMapping):
|
|||
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):
|
||||
if all(q[k] == constants.QUALITIES[qual][k] for k in keys): # type: ignore[literal-required]
|
||||
return qual
|
||||
return None
|
||||
|
||||
|
|
@ -1312,6 +1317,7 @@ class ManimConfig(MutableMapping):
|
|||
@property
|
||||
def transparent(self) -> bool:
|
||||
"""Whether the background opacity is less than 1.0 (-t)."""
|
||||
assert isinstance(self._d["background_opacity"], float)
|
||||
return self._d["background_opacity"] < 1.0
|
||||
|
||||
@transparent.setter
|
||||
|
|
@ -1421,12 +1427,12 @@ class ManimConfig(MutableMapping):
|
|||
self._d.__setitem__("window_position", value)
|
||||
|
||||
@property
|
||||
def window_size(self) -> str:
|
||||
"""The size of the opengl window as 'width,height' or 'default' to automatically scale the window based on the display monitor."""
|
||||
def window_size(self) -> str | tuple[int, ...]:
|
||||
"""The size of the opengl window. 'default' to automatically scale the window based on the display monitor."""
|
||||
return self._d["window_size"]
|
||||
|
||||
@window_size.setter
|
||||
def window_size(self, value: str) -> None:
|
||||
def window_size(self, value: str | tuple[int, ...]) -> None:
|
||||
self._d.__setitem__("window_size", value)
|
||||
|
||||
def resolve_movie_file_extension(self, is_transparent: bool) -> None:
|
||||
|
|
@ -1455,7 +1461,7 @@ class ManimConfig(MutableMapping):
|
|||
self._set_boolean("enable_gui", value)
|
||||
|
||||
@property
|
||||
def gui_location(self) -> tuple[Any]:
|
||||
def gui_location(self) -> tuple[int, ...]:
|
||||
"""Location parameters for the GUI window (e.g., screen coordinates or layout settings)."""
|
||||
return self._d["gui_location"]
|
||||
|
||||
|
|
@ -1639,6 +1645,7 @@ class ManimConfig(MutableMapping):
|
|||
all_args["quality"] = f"{self.pixel_height}p{self.frame_rate:g}"
|
||||
|
||||
path = self._d[key]
|
||||
assert isinstance(path, str)
|
||||
while "{" in path:
|
||||
try:
|
||||
path = path.format(**all_args)
|
||||
|
|
@ -1738,7 +1745,7 @@ class ManimConfig(MutableMapping):
|
|||
self._set_dir("custom_folders", value)
|
||||
|
||||
@property
|
||||
def input_file(self) -> str:
|
||||
def input_file(self) -> str | Path:
|
||||
"""Input file name."""
|
||||
return self._d["input_file"]
|
||||
|
||||
|
|
@ -1767,7 +1774,7 @@ class ManimConfig(MutableMapping):
|
|||
@property
|
||||
def tex_template(self) -> TexTemplate:
|
||||
"""Template used when rendering Tex. See :class:`.TexTemplate`."""
|
||||
if not hasattr(self, "_tex_template") or not self._tex_template:
|
||||
if not hasattr(self, "_tex_template") or not self._tex_template: # type: ignore[has-type]
|
||||
fn = self._d["tex_template_file"]
|
||||
if fn:
|
||||
self._tex_template = TexTemplate.from_file(fn)
|
||||
|
|
@ -1803,7 +1810,7 @@ class ManimConfig(MutableMapping):
|
|||
return self._d["plugins"]
|
||||
|
||||
@plugins.setter
|
||||
def plugins(self, value: list[str]):
|
||||
def plugins(self, value: list[str]) -> None:
|
||||
self._d["plugins"] = value
|
||||
|
||||
@property
|
||||
|
|
@ -1861,7 +1868,7 @@ class ManimFrame(Mapping):
|
|||
self.__dict__["_c"] = c
|
||||
|
||||
# there are required by parent class Mapping to behave like a dict
|
||||
def __getitem__(self, key: str | int) -> Any:
|
||||
def __getitem__(self, key: str) -> Any:
|
||||
if key in self._OPTS:
|
||||
return self._c[key]
|
||||
elif key in self._CONSTANTS:
|
||||
|
|
@ -1869,7 +1876,7 @@ class ManimFrame(Mapping):
|
|||
else:
|
||||
raise KeyError(key)
|
||||
|
||||
def __iter__(self) -> Iterable[str]:
|
||||
def __iter__(self) -> Iterator[Any]:
|
||||
return iter(list(self._OPTS) + list(self._CONSTANTS))
|
||||
|
||||
def __len__(self) -> int:
|
||||
|
|
@ -1887,4 +1894,4 @@ class ManimFrame(Mapping):
|
|||
|
||||
|
||||
for opt in list(ManimFrame._OPTS) + list(ManimFrame._CONSTANTS):
|
||||
setattr(ManimFrame, opt, property(lambda self, o=opt: self[o]))
|
||||
setattr(ManimFrame, opt, property(lambda self, o=opt: self[o])) # type: ignore[misc]
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ class AnimationGroup(Animation):
|
|||
sub_alphas[(sub_alphas > 1) | with_zero_run_time] = 1
|
||||
|
||||
for anim_to_update, sub_alpha in zip(
|
||||
to_update["anim"], sub_alphas, strict=False
|
||||
to_update["anim"], sub_alphas, strict=True
|
||||
):
|
||||
anim_to_update.interpolate(sub_alpha)
|
||||
|
||||
|
|
|
|||
|
|
@ -472,7 +472,7 @@ class SpiralIn(Animation):
|
|||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
alpha = self.rate_func(alpha)
|
||||
for original_shape, shape in zip(self.shapes, self.mobject, strict=False):
|
||||
for original_shape, shape in zip(self.shapes, self.mobject, strict=True):
|
||||
shape.restore()
|
||||
fill_opacity = original_shape.get_fill_opacity()
|
||||
stroke_opacity = original_shape.get_stroke_opacity()
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ from ..mobject.mobject import Mobject
|
|||
from ..mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from ..typing import Point3D, Point3DLike, Vector3DLike
|
||||
from ..utils.bezier import interpolate, inverse_interpolate
|
||||
from ..utils.color import GREY, YELLOW, ParsableManimColor
|
||||
from ..utils.color import GREY, PURE_YELLOW, ParsableManimColor
|
||||
from ..utils.rate_functions import RateFunction, smooth, there_and_back, wiggle
|
||||
from ..utils.space_ops import normalize
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ class FocusOn(Transform):
|
|||
|
||||
class UsingFocusOn(Scene):
|
||||
def construct(self):
|
||||
dot = Dot(color=YELLOW).shift(DOWN)
|
||||
dot = Dot(color=PURE_YELLOW).shift(DOWN)
|
||||
self.add(Tex("Focusing on the dot below:"), dot)
|
||||
self.play(FocusOn(dot))
|
||||
self.wait()
|
||||
|
|
@ -153,7 +153,7 @@ class Indicate(Transform):
|
|||
self,
|
||||
mobject: Mobject,
|
||||
scale_factor: float = 1.2,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
color: ParsableManimColor = PURE_YELLOW,
|
||||
rate_func: RateFunction = there_and_back,
|
||||
**kwargs: Any,
|
||||
):
|
||||
|
|
@ -198,7 +198,7 @@ class Flash(AnimationGroup):
|
|||
|
||||
class UsingFlash(Scene):
|
||||
def construct(self):
|
||||
dot = Dot(color=YELLOW).shift(DOWN)
|
||||
dot = Dot(color=PURE_YELLOW).shift(DOWN)
|
||||
self.add(Tex("Flash the dot below:"), dot)
|
||||
self.play(Flash(dot))
|
||||
self.wait()
|
||||
|
|
@ -226,7 +226,7 @@ class Flash(AnimationGroup):
|
|||
num_lines: int = 12,
|
||||
flash_radius: float = 0.1,
|
||||
line_stroke_width: int = 3,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
color: ParsableManimColor = PURE_YELLOW,
|
||||
time_width: float = 1,
|
||||
run_time: float = 1.0,
|
||||
**kwargs: Any,
|
||||
|
|
@ -349,7 +349,7 @@ class ShowPassingFlashWithThinningStrokeWidth(AnimationGroup):
|
|||
for stroke_width, time_width in zip(
|
||||
np.linspace(0, max_stroke_width, self.n_segments),
|
||||
np.linspace(max_time_width, 0, self.n_segments),
|
||||
strict=False,
|
||||
strict=True,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
|
@ -618,7 +618,7 @@ class Circumscribe(Succession):
|
|||
fade_out: bool = False,
|
||||
time_width: float = 0.3,
|
||||
buff: float = SMALL_BUFF,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
color: ParsableManimColor = PURE_YELLOW,
|
||||
run_time: float = 1,
|
||||
stroke_width: float = DEFAULT_STROKE_WIDTH,
|
||||
**kwargs: Any,
|
||||
|
|
|
|||
|
|
@ -235,8 +235,8 @@ class Transform(Animation):
|
|||
self.target_copy,
|
||||
]
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
return zip(*(mob.get_family() for mob in mobs), strict=False)
|
||||
return zip(*(mob.family_members_with_points() for mob in mobs), strict=False)
|
||||
return zip(*(mob.get_family() for mob in mobs), strict=True)
|
||||
return zip(*(mob.family_members_with_points() for mob in mobs), strict=True)
|
||||
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
|
|
@ -304,7 +304,7 @@ class ReplacementTransform(Transform):
|
|||
|
||||
|
||||
class TransformFromCopy(Transform):
|
||||
"""Performs a reversed Transform"""
|
||||
"""Preserves a copy of the original VMobject and transforms only it's copy to the target VMobject"""
|
||||
|
||||
def __init__(self, mobject: Mobject, target_mobject: Mobject, **kwargs) -> None:
|
||||
super().__init__(target_mobject, mobject, **kwargs)
|
||||
|
|
@ -741,7 +741,7 @@ class CyclicReplace(Transform):
|
|||
def create_target(self) -> Group:
|
||||
target = self.group.copy()
|
||||
cycled_targets = [target[-1], *target[:-1]]
|
||||
for m1, m2 in zip(cycled_targets, self.group, strict=False):
|
||||
for m1, m2 in zip(cycled_targets, self.group, strict=True):
|
||||
m1.move_to(m2)
|
||||
return target
|
||||
|
||||
|
|
@ -929,5 +929,5 @@ class FadeTransformPieces(FadeTransform):
|
|||
"""Replaces the source submobjects by the target submobjects and sets
|
||||
the opacity to 0.
|
||||
"""
|
||||
for sm0, sm1 in zip(source.get_family(), target.get_family(), strict=False):
|
||||
for sm0, sm1 in zip(source.get_family(), target.get_family(), strict=True):
|
||||
super().ghost_to(sm0, sm1)
|
||||
|
|
|
|||
|
|
@ -214,6 +214,16 @@ def turn_animation_into_updater(
|
|||
def update(m: Mobject, dt: float):
|
||||
if animation.total_time >= 0:
|
||||
run_time = animation.get_run_time()
|
||||
|
||||
# handle zero/negative runtime safely
|
||||
if run_time <= 0:
|
||||
# instantly snap to final state once, then remove updater
|
||||
animation.interpolate(1)
|
||||
animation.update_mobjects(dt)
|
||||
animation.finish()
|
||||
m.remove_updater(update)
|
||||
return
|
||||
|
||||
time_ratio = animation.total_time / run_time
|
||||
if cycle:
|
||||
alpha = time_ratio % 1
|
||||
|
|
|
|||
|
|
@ -14,32 +14,32 @@ from typing import TYPE_CHECKING, Any, Self
|
|||
|
||||
import cairo
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
from PIL import Image
|
||||
from scipy.spatial.distance import pdist
|
||||
|
||||
from manim.typing import (
|
||||
FloatRGBA_Array,
|
||||
FloatRGBALike_Array,
|
||||
ManimInt,
|
||||
PixelArray,
|
||||
Point3D,
|
||||
Point3D_Array,
|
||||
)
|
||||
|
||||
from .. import config, logger
|
||||
from ..constants import *
|
||||
from ..mobject.mobject import Mobject
|
||||
from ..mobject.types.point_cloud_mobject import PMobject
|
||||
from ..mobject.types.vectorized_mobject import VMobject
|
||||
from ..utils.color import ManimColor, ParsableManimColor, color_to_int_rgba
|
||||
from ..utils.family import extract_mobject_family_members
|
||||
from ..utils.images import get_full_raster_image_path
|
||||
from ..utils.iterables import list_difference_update
|
||||
from ..utils.space_ops import angle_of_vector
|
||||
from manim._config import config, logger
|
||||
from manim.constants import *
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.types.point_cloud_mobject import PMobject
|
||||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
from manim.utils.color import ManimColor, ParsableManimColor, color_to_int_rgba
|
||||
from manim.utils.family import extract_mobject_family_members
|
||||
from manim.utils.images import get_full_raster_image_path
|
||||
from manim.utils.iterables import list_difference_update
|
||||
from manim.utils.space_ops import cross2d
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..mobject.types.image_mobject import AbstractImageMobject
|
||||
import numpy.typing as npt
|
||||
|
||||
from manim.mobject.types.image_mobject import AbstractImageMobject
|
||||
from manim.typing import (
|
||||
FloatRGBA_Array,
|
||||
FloatRGBALike_Array,
|
||||
ManimFloat,
|
||||
ManimInt,
|
||||
PixelArray,
|
||||
Point3D,
|
||||
Point3D_Array,
|
||||
)
|
||||
|
||||
|
||||
LINE_JOIN_MAP = {
|
||||
|
|
@ -756,9 +756,8 @@ class Camera:
|
|||
points = vmobject.get_gradient_start_and_end_points()
|
||||
points = self.transform_points_pre_display(vmobject, points)
|
||||
pat = cairo.LinearGradient(*it.chain(*(point[:2] for point in points)))
|
||||
step = 1.0 / (len(rgbas) - 1)
|
||||
offsets = np.arange(0, 1 + step, step)
|
||||
for rgba, offset in zip(rgbas, offsets, strict=False):
|
||||
offsets = np.linspace(0, 1, len(rgbas))
|
||||
for rgba, offset in zip(rgbas, offsets, strict=True):
|
||||
pat.add_color_stop_rgba(offset, *rgba[2::-1], rgba[3])
|
||||
ctx.set_source(pat)
|
||||
return self
|
||||
|
|
@ -998,60 +997,113 @@ class Camera:
|
|||
def display_image_mobject(
|
||||
self, image_mobject: AbstractImageMobject, pixel_array: np.ndarray
|
||||
) -> None:
|
||||
"""Displays an ImageMobject by changing the pixel_array suitably.
|
||||
"""Display an :class:`~.ImageMobject` by changing the ``pixel_array`` suitably.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
image_mobject
|
||||
The imageMobject to display
|
||||
The :class:`~.ImageMobject` to display.
|
||||
pixel_array
|
||||
The Pixel array to put the imagemobject in.
|
||||
The pixel array to put the :class:`~.ImageMobject` in.
|
||||
"""
|
||||
corner_coords = self.points_to_pixel_coords(image_mobject, image_mobject.points)
|
||||
ul_coords, ur_coords, dl_coords, _ = corner_coords
|
||||
right_vect = ur_coords - ul_coords
|
||||
down_vect = dl_coords - ul_coords
|
||||
center_coords = ul_coords + (right_vect + down_vect) / 2
|
||||
|
||||
sub_image = Image.fromarray(image_mobject.get_pixel_array(), mode="RGBA")
|
||||
original_coords = np.array(
|
||||
[
|
||||
[0, 0],
|
||||
[sub_image.width, 0],
|
||||
[0, sub_image.height],
|
||||
[sub_image.width, sub_image.height],
|
||||
]
|
||||
)
|
||||
target_coords = self.points_to_subpixel_coords(
|
||||
image_mobject, image_mobject.points
|
||||
)
|
||||
int_target_coords = target_coords.astype(np.int64)
|
||||
|
||||
# Reshape
|
||||
pixel_width = max(int(pdist([ul_coords, ur_coords]).item()), 1)
|
||||
pixel_height = max(int(pdist([ul_coords, dl_coords]).item()), 1)
|
||||
sub_image = sub_image.resize(
|
||||
(pixel_width, pixel_height),
|
||||
# Temporarily translate target coords to upper left corner to calculate the
|
||||
# smallest possible size for the target image.
|
||||
shift_vector = np.array(
|
||||
[
|
||||
min(*[x for x, y in int_target_coords]),
|
||||
min(*[y for x, y in int_target_coords]),
|
||||
]
|
||||
)
|
||||
target_coords -= shift_vector
|
||||
int_target_coords -= shift_vector
|
||||
target_size = (
|
||||
max(*[x for x, y in int_target_coords]),
|
||||
max(*[y for x, y in int_target_coords]),
|
||||
)
|
||||
|
||||
# Check that the quadrilateral of the transformed image can actually contain any
|
||||
# pixels by checking that its height from the longest side is longer than 0.5 pixels.
|
||||
# If it's not, do not render the image. Otherwise, the perspective transform
|
||||
# coefficients below might have broken values due to the extreme distortion (for
|
||||
# example, when the image is perpendicular to the camera).
|
||||
ordered_vertices = [target_coords[i] for i in (0, 1, 3, 2)]
|
||||
sides = [ordered_vertices[(i + 1) % 4] - ordered_vertices[i] for i in range(4)]
|
||||
side_lengths_in_pixels = np.linalg.norm(sides, axis=1)
|
||||
|
||||
longest_side_index = np.argmax(side_lengths_in_pixels)
|
||||
longest_side = sides[longest_side_index]
|
||||
longest_side_length_in_pixels = side_lengths_in_pixels[longest_side_index]
|
||||
if longest_side_length_in_pixels == 0:
|
||||
return
|
||||
|
||||
previous_side = sides[(longest_side_index - 1) % 4]
|
||||
next_side = sides[(longest_side_index - 1) % 4]
|
||||
|
||||
# height = area / base
|
||||
h1 = abs(cross2d(longest_side, previous_side)) / longest_side_length_in_pixels
|
||||
h2 = abs(cross2d(longest_side, next_side)) / longest_side_length_in_pixels
|
||||
height_from_longest_side_in_pixels = max(h1, h2)
|
||||
|
||||
if height_from_longest_side_in_pixels < 0.5:
|
||||
return
|
||||
|
||||
# Use PIL.Image.Image.transform() to apply a perspective transform to the image.
|
||||
# The transform coefficients must be calculated. The following is adapted from:
|
||||
# https://pc-pillow.readthedocs.io/en/latest/Image_class/Image_transform.html#transform-perspective-coefficients
|
||||
# https://stackoverflow.com/questions/14177744/how-does-perspective-transformation-work-in-pil
|
||||
# The derivation can be found here:
|
||||
# https://web.archive.org/web/20150222120106/xenia.media.mit.edu/~cwren/interpolator/
|
||||
homography_matrix = []
|
||||
for (x, y), (X, Y) in zip(target_coords, original_coords, strict=True):
|
||||
homography_matrix.append([x, y, 1, 0, 0, 0, -X * x, -X * y])
|
||||
homography_matrix.append([0, 0, 0, x, y, 1, -Y * x, -Y * y])
|
||||
|
||||
A = np.array(homography_matrix, dtype=np.float64)
|
||||
b = original_coords.reshape(8).astype(np.float64)
|
||||
|
||||
try:
|
||||
transform_coefficients = np.linalg.solve(A, b)
|
||||
except np.linalg.LinAlgError:
|
||||
# The matrix A might be singular if three points are collinear.
|
||||
# In this case, do nothing and return.
|
||||
return
|
||||
|
||||
sub_image = sub_image.transform(
|
||||
size=target_size, # Use the smallest possible size for speed.
|
||||
method=Image.Transform.PERSPECTIVE,
|
||||
data=transform_coefficients,
|
||||
resample=image_mobject.resampling_algorithm,
|
||||
)
|
||||
|
||||
# Rotate
|
||||
angle = angle_of_vector(right_vect)
|
||||
adjusted_angle = -int(360 * angle / TAU)
|
||||
if adjusted_angle != 0:
|
||||
sub_image = sub_image.rotate(
|
||||
adjusted_angle,
|
||||
resample=image_mobject.resampling_algorithm,
|
||||
expand=1,
|
||||
)
|
||||
|
||||
# TODO, there is no accounting for a shear...
|
||||
|
||||
# Paste into an image as large as the camera's pixel array
|
||||
# Paste into an image as large as the camera's pixel array.
|
||||
full_image = Image.fromarray(
|
||||
np.zeros((self.pixel_height, self.pixel_width)),
|
||||
mode="RGBA",
|
||||
)
|
||||
new_ul_coords = center_coords - np.array(sub_image.size) / 2
|
||||
new_ul_coords = new_ul_coords.astype(int)
|
||||
full_image.paste(
|
||||
sub_image,
|
||||
box=(
|
||||
new_ul_coords[0],
|
||||
new_ul_coords[1],
|
||||
new_ul_coords[0] + sub_image.size[0],
|
||||
new_ul_coords[1] + sub_image.size[1],
|
||||
shift_vector[0],
|
||||
shift_vector[1],
|
||||
shift_vector[0] + target_size[0],
|
||||
shift_vector[1] + target_size[1],
|
||||
),
|
||||
)
|
||||
# Paint on top of existing pixel array
|
||||
# Paint on top of existing pixel array.
|
||||
self.overlay_PIL_image(pixel_array, full_image)
|
||||
|
||||
def overlay_rgba_array(
|
||||
|
|
@ -1127,11 +1179,13 @@ class Camera:
|
|||
points = np.zeros((1, 3))
|
||||
return points
|
||||
|
||||
def points_to_pixel_coords(
|
||||
def points_to_subpixel_coords(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
points: Point3D_Array,
|
||||
) -> npt.NDArray[ManimInt]: # TODO: Write more detailed docstrings for this method.
|
||||
) -> npt.NDArray[
|
||||
ManimFloat
|
||||
]: # TODO: Write more detailed docstrings for this method.
|
||||
points = self.transform_points_pre_display(mobject, points)
|
||||
shifted_points = points - self.frame_center
|
||||
|
||||
|
|
@ -1149,7 +1203,14 @@ class Camera:
|
|||
|
||||
result[:, 0] = shifted_points[:, 0] * width_mult + width_add
|
||||
result[:, 1] = shifted_points[:, 1] * height_mult + height_add
|
||||
return result.astype("int")
|
||||
return result
|
||||
|
||||
def points_to_pixel_coords(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
points: Point3D_Array,
|
||||
) -> npt.NDArray[ManimInt]: # TODO: Write more detailed docstrings for this method.
|
||||
return self.points_to_subpixel_coords(mobject, points).astype(np.int64)
|
||||
|
||||
def on_screen_pixels(self, pixel_coords: np.ndarray) -> PixelArray:
|
||||
"""Returns array of pixels that are on the screen from a given
|
||||
|
|
|
|||
|
|
@ -308,7 +308,7 @@ Are you sure you want to continue? (y/n)""",
|
|||
if proceed:
|
||||
if not directory_path.is_dir():
|
||||
console.print(f"Creating folder: {directory}.", style="red bold")
|
||||
directory_path.mkdir(parents=True)
|
||||
directory_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
ctx.invoke(write)
|
||||
from_path = Path.cwd() / "manim.cfg"
|
||||
|
|
|
|||
|
|
@ -111,14 +111,10 @@ ULTRAHEAVY = "ULTRAHEAVY"
|
|||
RESAMPLING_ALGORITHMS = {
|
||||
"nearest": Resampling.NEAREST,
|
||||
"none": Resampling.NEAREST,
|
||||
"lanczos": Resampling.LANCZOS,
|
||||
"antialias": Resampling.LANCZOS,
|
||||
"bilinear": Resampling.BILINEAR,
|
||||
"linear": Resampling.BILINEAR,
|
||||
"bicubic": Resampling.BICUBIC,
|
||||
"cubic": Resampling.BICUBIC,
|
||||
"box": Resampling.BOX,
|
||||
"hamming": Resampling.HAMMING,
|
||||
}
|
||||
|
||||
# Geometry: directions
|
||||
|
|
|
|||
|
|
@ -101,12 +101,12 @@ class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
self,
|
||||
tip_length: float = DEFAULT_ARROW_TIP_LENGTH,
|
||||
normal_vector: Vector3DLike = OUT,
|
||||
tip_style: dict = {},
|
||||
tip_style: dict | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.tip_length: float = tip_length
|
||||
self.normal_vector = normal_vector
|
||||
self.tip_style: dict = tip_style
|
||||
self.tip_style: dict = tip_style if tip_style is not None else {}
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Adding, Creating, Modifying tips
|
||||
|
|
@ -128,7 +128,7 @@ class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
else:
|
||||
self.position_tip(tip, at_start)
|
||||
self.reset_endpoints_based_on_tip(tip, at_start)
|
||||
self.asign_tip_attr(tip, at_start)
|
||||
self.assign_tip_attr(tip, at_start)
|
||||
self.add(tip)
|
||||
return self
|
||||
|
||||
|
|
@ -201,6 +201,7 @@ class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
axis=axis,
|
||||
) # Rotates the tip along the vertical wrt the axis
|
||||
self._init_positioning_axis = axis
|
||||
|
||||
tip.shift(anchor - tip.tip_point)
|
||||
return tip
|
||||
|
||||
|
|
@ -215,7 +216,7 @@ class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
self.put_start_and_end_on(self.get_start(), tip.base)
|
||||
return self
|
||||
|
||||
def asign_tip_attr(self, tip: tips.ArrowTip, at_start: bool) -> Self:
|
||||
def assign_tip_attr(self, tip: tips.ArrowTip, at_start: bool) -> Self:
|
||||
if at_start:
|
||||
self.start_tip = tip
|
||||
else:
|
||||
|
|
@ -241,7 +242,8 @@ class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
if self.has_start_tip():
|
||||
result.add(self.start_tip)
|
||||
self.remove(self.start_tip)
|
||||
self.put_start_and_end_on(start, end)
|
||||
if result.submobjects:
|
||||
self.put_start_and_end_on(start, end)
|
||||
return result
|
||||
|
||||
def get_tips(self) -> VGroup:
|
||||
|
|
@ -1231,7 +1233,7 @@ class ArcPolygon(VMobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
arcs = [
|
||||
ArcBetweenPoints(*pair, **conf)
|
||||
for (pair, conf) in zip(point_pairs, all_arc_configs, strict=False)
|
||||
for (pair, conf) in zip(point_pairs, all_arc_configs, strict=True)
|
||||
]
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ class Polygram(VMobject, metaclass=ConvertToOpenGL):
|
|||
# times, then .get_vertex_groups() splits it into N vertex groups.
|
||||
group = []
|
||||
for start, end in zip(
|
||||
self.get_start_anchors(), self.get_end_anchors(), strict=False
|
||||
self.get_start_anchors(), self.get_end_anchors(), strict=True
|
||||
):
|
||||
group.append(start)
|
||||
|
||||
|
|
@ -240,7 +240,7 @@ class Polygram(VMobject, metaclass=ConvertToOpenGL):
|
|||
radius_list = radius * ceil(len(vertex_group) / len(radius))
|
||||
|
||||
for current_radius, (v1, v2, v3) in zip(
|
||||
radius_list, adjacent_n_tuples(vertex_group, 3), strict=False
|
||||
radius_list, adjacent_n_tuples(vertex_group, 3), strict=True
|
||||
):
|
||||
vect1 = v2 - v1
|
||||
vect2 = v3 - v2
|
||||
|
|
@ -552,7 +552,7 @@ class Star(Polygon):
|
|||
)
|
||||
|
||||
vertices: list[npt.NDArray] = []
|
||||
for pair in zip(outer_vertices, inner_vertices, strict=False):
|
||||
for pair in zip(outer_vertices, inner_vertices, strict=True):
|
||||
vertices.extend(pair)
|
||||
|
||||
super().__init__(*vertices, **kwargs)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from manim.mobject.geometry.polygram import RoundedRectangle
|
|||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.mobject.types.vectorized_mobject import VGroup
|
||||
from manim.utils.color import BLACK, RED, YELLOW, ManimColor, ParsableManimColor
|
||||
from manim.utils.color import BLACK, PURE_YELLOW, RED, ParsableManimColor
|
||||
|
||||
|
||||
class SurroundingRectangle(RoundedRectangle):
|
||||
|
|
@ -50,7 +50,7 @@ class SurroundingRectangle(RoundedRectangle):
|
|||
def __init__(
|
||||
self,
|
||||
*mobjects: Mobject,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
color: ParsableManimColor = PURE_YELLOW,
|
||||
buff: float | tuple[float, float] = SMALL_BUFF,
|
||||
corner_radius: float = 0.0,
|
||||
**kwargs: Any,
|
||||
|
|
@ -149,12 +149,6 @@ class BackgroundRectangle(SurroundingRectangle):
|
|||
)
|
||||
return self
|
||||
|
||||
def get_fill_color(self) -> ManimColor:
|
||||
# The type of the color property is set to Any using the property decorator
|
||||
# vectorized_mobject.py#L571
|
||||
temp_color: ManimColor = self.color
|
||||
return temp_color
|
||||
|
||||
|
||||
class Cross(VGroup):
|
||||
"""Creates a cross.
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ from manim.utils.color import (
|
|||
BLUE,
|
||||
BLUE_D,
|
||||
GREEN,
|
||||
PURE_YELLOW,
|
||||
WHITE,
|
||||
YELLOW,
|
||||
ManimColor,
|
||||
ParsableManimColor,
|
||||
color_gradient,
|
||||
|
|
@ -448,7 +448,7 @@ class CoordinateSystem:
|
|||
zip(
|
||||
tick_range,
|
||||
axis.scaling.get_custom_labels(tick_range),
|
||||
strict=False,
|
||||
strict=True,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -1300,7 +1300,7 @@ class CoordinateSystem:
|
|||
|
||||
colors = color_gradient(color, len(x_range_array))
|
||||
|
||||
for x, color in zip(x_range_array, colors, strict=False):
|
||||
for x, color in zip(x_range_array, colors, strict=True):
|
||||
if input_sample_type == "left":
|
||||
sample_input = x
|
||||
elif input_sample_type == "right":
|
||||
|
|
@ -1614,7 +1614,7 @@ class CoordinateSystem:
|
|||
x: float,
|
||||
graph: ParametricFunction,
|
||||
dx: float | None = None,
|
||||
dx_line_color: ParsableManimColor = YELLOW,
|
||||
dx_line_color: ParsableManimColor = PURE_YELLOW,
|
||||
dy_line_color: ParsableManimColor | None = None,
|
||||
dx_label: float | str | None = None,
|
||||
dy_label: float | str | None = None,
|
||||
|
|
@ -1796,7 +1796,7 @@ class CoordinateSystem:
|
|||
triangle_size: float = MED_SMALL_BUFF,
|
||||
triangle_color: ParsableManimColor | None = WHITE,
|
||||
line_func: type[Line] = Line,
|
||||
line_color: ParsableManimColor = YELLOW,
|
||||
line_color: ParsableManimColor = PURE_YELLOW,
|
||||
) -> VGroup:
|
||||
"""Creates a labelled triangle marker with a vertical line from the x-axis
|
||||
to a curve at a given x-value.
|
||||
|
|
@ -2089,6 +2089,10 @@ class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
|
|||
|
||||
``ax.coords_to_point( [[x_0, y_0, z_0], [x_1, y_1, z_1]] )``
|
||||
|
||||
A single coordinate can also be passed as a flat list or 1D array:
|
||||
|
||||
``ax.coords_to_point( [x, y, z] )``
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.ndarray
|
||||
|
|
@ -2117,6 +2121,10 @@ class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
|
|||
array([[0. , 0.86, 0.86],
|
||||
[0.75, 0.75, 0. ],
|
||||
[0. , 0. , 0. ]])
|
||||
>>> np.around(ax.coords_to_point([1, 0, 0]), 2)
|
||||
array([0.86, 0. , 0. ])
|
||||
>>> np.around(ax.coords_to_point(np.array([1, 0])), 2)
|
||||
array([0.86, 0. , 0. ])
|
||||
|
||||
.. manim:: CoordsToPointExample
|
||||
:save_last_frame:
|
||||
|
|
@ -2159,6 +2167,10 @@ class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
|
|||
else:
|
||||
coords = coords.T
|
||||
are_coordinates_transposed = True
|
||||
# If coords is in the format ([x, y, z]) -- a single flat list/array passed as one argument:
|
||||
elif coords.ndim == 2 and coords.shape[0] == 1:
|
||||
# Extract the single list so [x, y, z] is treated like c2p(x, y, z).
|
||||
coords = coords[0]
|
||||
# Otherwise, coords already looked like (x, y, z) or ([x1 x2 ...], [y1 y2 ...], [z1 z2 ...]),
|
||||
# so no further processing is needed.
|
||||
|
||||
|
|
@ -2293,7 +2305,7 @@ class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
|
|||
x_values: Iterable[float],
|
||||
y_values: Iterable[float],
|
||||
z_values: Iterable[float] | None = None,
|
||||
line_color: ParsableManimColor = YELLOW,
|
||||
line_color: ParsableManimColor = PURE_YELLOW,
|
||||
add_vertex_dots: bool = True,
|
||||
vertex_dot_radius: float = DEFAULT_DOT_RADIUS,
|
||||
vertex_dot_style: dict[str, Any] | None = None,
|
||||
|
|
@ -2362,7 +2374,7 @@ class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
|
|||
|
||||
vertices = [
|
||||
self.coords_to_point(x, y, z)
|
||||
for x, y, z in zip(x_values, y_values, z_values, strict=False)
|
||||
for x, y, z in zip(x_values, y_values, z_values, strict=True)
|
||||
]
|
||||
graph.set_points_as_corners(vertices)
|
||||
line_graph["line_graph"] = graph
|
||||
|
|
@ -3245,6 +3257,7 @@ class PolarPlane(Axes):
|
|||
}
|
||||
for i in a_values
|
||||
]
|
||||
a_tex = []
|
||||
if self.azimuth_units == "PI radians" or self.azimuth_units == "TAU radians":
|
||||
a_tex = [
|
||||
self.get_radian_label(
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ if TYPE_CHECKING:
|
|||
from manim.typing import Point3D, Point3DLike
|
||||
from manim.utils.color import ParsableManimColor
|
||||
|
||||
from manim.utils.color import YELLOW
|
||||
from manim.utils.color import PURE_YELLOW
|
||||
|
||||
|
||||
class ParametricFunction(VMobject, metaclass=ConvertToOpenGL):
|
||||
|
|
@ -157,7 +157,7 @@ class ParametricFunction(VMobject, metaclass=ConvertToOpenGL):
|
|||
else:
|
||||
boundary_times = [self.t_min, self.t_max]
|
||||
|
||||
for t1, t2 in zip(boundary_times[0::2], boundary_times[1::2], strict=False):
|
||||
for t1, t2 in zip(boundary_times[0::2], boundary_times[1::2], strict=True):
|
||||
t_range = np.array(
|
||||
[
|
||||
*self.scaling.function(np.arange(t1, t2, self.t_step)),
|
||||
|
|
@ -217,7 +217,7 @@ class FunctionGraph(ParametricFunction):
|
|||
self,
|
||||
function: Callable[[float], Any],
|
||||
x_range: tuple[float, float] | tuple[float, float, float] | None = None,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
color: ParsableManimColor = PURE_YELLOW,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
if x_range is None:
|
||||
|
|
|
|||
|
|
@ -262,7 +262,7 @@ class NumberLine(Line):
|
|||
zip(
|
||||
tick_range,
|
||||
custom_labels,
|
||||
strict=False,
|
||||
strict=True,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ class SampleSpace(Rectangle):
|
|||
|
||||
last_point = self.get_edge_center(-vect)
|
||||
parts = VGroup()
|
||||
for factor, color in zip(p_list_complete, colors_in_gradient, strict=False):
|
||||
for factor, color in zip(p_list_complete, colors_in_gradient, strict=True):
|
||||
part = SampleSpace()
|
||||
part.set_fill(color, 1)
|
||||
part.replace(self, stretch=True)
|
||||
|
|
@ -368,7 +368,7 @@ class BarChart(Axes):
|
|||
labels = VGroup()
|
||||
|
||||
for i, (value, bar_name) in enumerate(
|
||||
zip(val_range, self.bar_names, strict=False)
|
||||
zip(val_range, self.bar_names, strict=True)
|
||||
):
|
||||
# to accommodate negative bars, the label may need to be
|
||||
# below or above the x_axis depending on the value of the bar
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import warnings
|
|||
from collections.abc import Callable, Iterable
|
||||
from functools import partialmethod, reduce
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -28,8 +28,8 @@ from .. import config, logger
|
|||
from ..constants import *
|
||||
from ..utils.color import (
|
||||
BLACK,
|
||||
PURE_YELLOW,
|
||||
WHITE,
|
||||
YELLOW_C,
|
||||
ManimColor,
|
||||
ParsableManimColor,
|
||||
color_gradient,
|
||||
|
|
@ -394,6 +394,42 @@ class Mobject:
|
|||
"""
|
||||
return _AnimationBuilder(self)
|
||||
|
||||
@property
|
||||
def always(self) -> Self:
|
||||
"""Call a method on a mobject every frame.
|
||||
|
||||
This is syntactic sugar for ``mob.add_updater(lambda m: m.method(*args, **kwargs), call_updater=True)``.
|
||||
Note that this will call the method immediately. If this behavior is not
|
||||
desired, you should use :meth:`add_updater` directly.
|
||||
|
||||
.. warning::
|
||||
|
||||
Chaining of methods is allowed, but each method will be added
|
||||
as its own updater. If you are chaining methods, make sure they
|
||||
do not interfere with each other or you may get unexpected results.
|
||||
|
||||
.. warning::
|
||||
|
||||
:attr:`always` is not compatible with :meth:`.ValueTracker.get_value`, because
|
||||
the value will be computed once and then never updated again. Use :meth:`add_updater`
|
||||
if you would like to use a :class:`~.ValueTracker` to update the value.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
.. manim:: AlwaysExample
|
||||
|
||||
class AlwaysExample(Scene):
|
||||
def construct(self):
|
||||
sq = Square().to_edge(LEFT)
|
||||
t = Text("Hello World!")
|
||||
t.always.next_to(sq, UP)
|
||||
self.add(sq, t)
|
||||
self.play(sq.animate.to_edge(RIGHT))
|
||||
"""
|
||||
# can't use typing.cast because Self is under TYPE_CHECKING
|
||||
return _UpdaterBuilder(self) # type: ignore[return-value]
|
||||
|
||||
def __deepcopy__(self, clone_from_id) -> Self:
|
||||
cls = self.__class__
|
||||
result = cls.__new__(cls)
|
||||
|
|
@ -406,9 +442,10 @@ class Mobject:
|
|||
def __repr__(self) -> str:
|
||||
return str(self.name)
|
||||
|
||||
def reset_points(self) -> None:
|
||||
def reset_points(self) -> Self:
|
||||
"""Sets :attr:`points` to be an empty array."""
|
||||
self.points = np.zeros((0, self.dim))
|
||||
return self
|
||||
|
||||
def init_colors(self) -> object:
|
||||
"""Initializes the colors.
|
||||
|
|
@ -815,7 +852,7 @@ class Mobject:
|
|||
self.scale_to_fit_depth(value)
|
||||
|
||||
# Can't be staticmethod because of point_cloud_mobject.py
|
||||
def get_array_attrs(self) -> list[Literal["points"]]:
|
||||
def get_array_attrs(self) -> list[str]:
|
||||
return ["points"]
|
||||
|
||||
def apply_over_attr_arrays(self, func: MultiMappingFunction) -> Self:
|
||||
|
|
@ -1965,7 +2002,7 @@ class Mobject:
|
|||
# Color functions
|
||||
|
||||
def set_color(
|
||||
self, color: ParsableManimColor = YELLOW_C, family: bool = True
|
||||
self, color: ParsableManimColor = PURE_YELLOW, family: bool = True
|
||||
) -> Self:
|
||||
"""Condition is function which takes in one arguments, (x, y, z).
|
||||
Here it just recurses to submobjects, but in subclasses this
|
||||
|
|
@ -2016,7 +2053,7 @@ class Mobject:
|
|||
mobs = self.family_members_with_points()
|
||||
new_colors = color_gradient(colors, len(mobs))
|
||||
|
||||
for mob, color in zip(mobs, new_colors, strict=False):
|
||||
for mob, color in zip(mobs, new_colors, strict=True):
|
||||
mob.set_color(color, family=False)
|
||||
return self
|
||||
|
||||
|
|
@ -2309,7 +2346,7 @@ class Mobject:
|
|||
return Group(
|
||||
*(
|
||||
template.copy().pointwise_become_partial(self, a1, a2)
|
||||
for a1, a2 in zip(alphas[:-1], alphas[1:], strict=False)
|
||||
for a1, a2 in zip(alphas[:-1], alphas[1:], strict=True)
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -2502,7 +2539,7 @@ class Mobject:
|
|||
x = VGroup(s1, s2, s3, s4).set_x(0).arrange(buff=1.0)
|
||||
self.add(x)
|
||||
"""
|
||||
for m1, m2 in zip(self.submobjects, self.submobjects[1:], strict=False):
|
||||
for m1, m2 in zip(self.submobjects[:-1], self.submobjects[1:], strict=True):
|
||||
m2.next_to(m1, direction, buff, **kwargs)
|
||||
if center:
|
||||
self.center()
|
||||
|
|
@ -2887,7 +2924,7 @@ class Mobject:
|
|||
if not skip_point_alignment:
|
||||
self.align_points(mobject)
|
||||
# Recurse
|
||||
for m1, m2 in zip(self.submobjects, mobject.submobjects, strict=False):
|
||||
for m1, m2 in zip(self.submobjects, mobject.submobjects, strict=True):
|
||||
m1.align_data(m2)
|
||||
|
||||
def get_point_mobject(self, center=None):
|
||||
|
|
@ -2957,7 +2994,7 @@ class Mobject:
|
|||
repeat_indices = (np.arange(target) * curr) // target
|
||||
split_factors = [sum(repeat_indices == i) for i in range(curr)]
|
||||
new_submobs = []
|
||||
for submob, sf in zip(self.submobjects, split_factors, strict=False):
|
||||
for submob, sf in zip(self.submobjects, split_factors, strict=True):
|
||||
new_submobs.append(submob)
|
||||
new_submobs.extend(submob.copy().fade(1) for _ in range(1, sf))
|
||||
self.submobjects = new_submobs
|
||||
|
|
@ -3169,7 +3206,7 @@ class Mobject:
|
|||
mobject.move_to(self.get_center())
|
||||
|
||||
self.align_data(mobject, skip_point_alignment=True)
|
||||
for sm1, sm2 in zip(self.get_family(), mobject.get_family(), strict=False):
|
||||
for sm1, sm2 in zip(self.get_family(), mobject.get_family(), strict=True):
|
||||
sm1.points = np.array(sm2.points)
|
||||
sm1.interpolate_color(sm1, sm2, 1)
|
||||
return self
|
||||
|
|
@ -3339,6 +3376,24 @@ class _AnimationBuilder:
|
|||
return anim
|
||||
|
||||
|
||||
class _UpdaterBuilder:
|
||||
"""Syntactic sugar for adding updaters to mobjects."""
|
||||
|
||||
def __init__(self, mobject: Mobject):
|
||||
self._mobject = mobject
|
||||
|
||||
def __getattr__(self, name: str, /) -> Callable[..., Self]:
|
||||
# just return a function that will add the updater
|
||||
def add_updater(*method_args, **method_kwargs) -> Self:
|
||||
self._mobject.add_updater(
|
||||
lambda m: getattr(m, name)(*method_args, **method_kwargs),
|
||||
call_updater=True,
|
||||
)
|
||||
return self
|
||||
|
||||
return add_updater
|
||||
|
||||
|
||||
def override_animate(method) -> types.FunctionType:
|
||||
r"""Decorator for overriding method animations.
|
||||
|
||||
|
|
|
|||
|
|
@ -9,13 +9,13 @@ import numpy as np
|
|||
from manim.constants import ORIGIN, RIGHT, UP
|
||||
from manim.mobject.opengl.opengl_point_cloud_mobject import OpenGLPMobject
|
||||
from manim.typing import Point3DLike
|
||||
from manim.utils.color import YELLOW, ParsableManimColor
|
||||
from manim.utils.color import PURE_YELLOW, ParsableManimColor
|
||||
|
||||
|
||||
class DotCloud(OpenGLPMobject):
|
||||
def __init__(
|
||||
self,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
color: ParsableManimColor = PURE_YELLOW,
|
||||
stroke_width: float = 2.0,
|
||||
radius: float = 2.0,
|
||||
density: float = 10,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from abc import ABCMeta
|
||||
from typing import Any
|
||||
|
||||
from manim import config
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
|
|
@ -19,13 +20,15 @@ class ConvertToOpenGL(ABCMeta):
|
|||
on the lowest order inheritance classes such as Mobject and VMobject.
|
||||
"""
|
||||
|
||||
_converted_classes = []
|
||||
_converted_classes: list[type] = []
|
||||
|
||||
def __new__(mcls, name, bases, namespace):
|
||||
def __new__(
|
||||
mcls, name: str, bases: tuple[type, ...], namespace: dict[str, Any]
|
||||
) -> type:
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
# Must check class names to prevent
|
||||
# cyclic importing.
|
||||
base_names_to_opengl = {
|
||||
base_names_to_opengl: dict[str, type] = {
|
||||
"Mobject": OpenGLMobject,
|
||||
"VMobject": OpenGLVMobject,
|
||||
"PMobject": OpenGLPMobject,
|
||||
|
|
@ -40,6 +43,6 @@ class ConvertToOpenGL(ABCMeta):
|
|||
|
||||
return super().__new__(mcls, name, bases, namespace)
|
||||
|
||||
def __init__(cls, name, bases, namespace):
|
||||
def __init__(cls, name: str, bases: tuple[type, ...], namespace: dict[str, Any]):
|
||||
super().__init__(name, bases, namespace)
|
||||
cls._converted_classes.append(cls)
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ class OpenGLTipableVMobject(OpenGLVMobject):
|
|||
"""
|
||||
tip = self.create_tip(at_start, **kwargs)
|
||||
self.reset_endpoints_based_on_tip(tip, at_start)
|
||||
self.asign_tip_attr(tip, at_start)
|
||||
self.assign_tip_attr(tip, at_start)
|
||||
self.add(tip)
|
||||
return self
|
||||
|
||||
|
|
@ -160,7 +160,7 @@ class OpenGLTipableVMobject(OpenGLVMobject):
|
|||
self.put_start_and_end_on(start, end)
|
||||
return self
|
||||
|
||||
def asign_tip_attr(self, tip: OpenGLArrowTip, at_start: bool) -> Self:
|
||||
def assign_tip_attr(self, tip: OpenGLArrowTip, at_start: bool) -> Self:
|
||||
if at_start:
|
||||
self.start_tip = tip
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ __all__ = [
|
|||
]
|
||||
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
|
@ -13,26 +14,29 @@ from PIL.Image import Resampling
|
|||
from manim.mobject.opengl.opengl_surface import OpenGLSurface, OpenGLTexturedSurface
|
||||
from manim.utils.images import get_full_raster_image_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy.typing as npt
|
||||
|
||||
__all__ = ["OpenGLImageMobject"]
|
||||
|
||||
|
||||
class OpenGLImageMobject(OpenGLTexturedSurface):
|
||||
def __init__(
|
||||
self,
|
||||
filename_or_array: str | Path | np.ndarray,
|
||||
width: float = None,
|
||||
height: float = None,
|
||||
filename_or_array: str | Path | npt.NDArray,
|
||||
width: float | None = None,
|
||||
height: float | None = None,
|
||||
image_mode: str = "RGBA",
|
||||
resampling_algorithm: int = Resampling.BICUBIC,
|
||||
resampling_algorithm: Resampling = Resampling.BICUBIC,
|
||||
opacity: float = 1,
|
||||
gloss: float = 0,
|
||||
shadow: float = 0,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.image = filename_or_array
|
||||
self.resampling_algorithm = resampling_algorithm
|
||||
if isinstance(filename_or_array, np.ndarray):
|
||||
self.size = self.image.shape[1::-1]
|
||||
self.size = filename_or_array.shape[1::-1]
|
||||
elif isinstance(filename_or_array, (str, Path)):
|
||||
path = get_full_raster_image_path(filename_or_array)
|
||||
self.size = Image.open(path).size
|
||||
|
|
@ -68,7 +72,7 @@ class OpenGLImageMobject(OpenGLTexturedSurface):
|
|||
self,
|
||||
image_file: str | Path | np.ndarray,
|
||||
image_mode: str,
|
||||
):
|
||||
) -> Image.Image:
|
||||
if isinstance(image_file, (str, Path)):
|
||||
return super().get_image_from_file(image_file, image_mode)
|
||||
else:
|
||||
|
|
@ -76,7 +80,7 @@ class OpenGLImageMobject(OpenGLTexturedSurface):
|
|||
Image.fromarray(image_file.astype("uint8"))
|
||||
.convert(image_mode)
|
||||
.resize(
|
||||
np.array(image_file.shape[:2])
|
||||
image_file.shape[:2]
|
||||
* 200, # assumption of 200 ppmu (pixels per manim unit) would suffice
|
||||
resample=self.resampling_algorithm,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1055,7 +1055,7 @@ class OpenGLMobject:
|
|||
x = OpenGLVGroup(s1, s2, s3, s4).set_x(0).arrange(buff=1.0)
|
||||
self.add(x)
|
||||
"""
|
||||
for m1, m2 in zip(self.submobjects, self.submobjects[1:], strict=False):
|
||||
for m1, m2 in zip(self.submobjects[:-1], self.submobjects[1:], strict=True):
|
||||
m2.next_to(m1, direction, **kwargs)
|
||||
if center:
|
||||
self.center()
|
||||
|
|
@ -2190,7 +2190,7 @@ class OpenGLMobject:
|
|||
# Color and opacity
|
||||
if color is not None and opacity is not None:
|
||||
rgbas: FloatRGBA_Array = np.array(
|
||||
[[*rgb, o] for rgb, o in zip(*make_even(rgbs, opacities), strict=False)]
|
||||
[[*rgb, o] for rgb, o in zip(*make_even(rgbs, opacities), strict=True)]
|
||||
)
|
||||
for mob in self.get_family(recurse):
|
||||
mob.data[name] = rgbas.copy()
|
||||
|
|
@ -2266,7 +2266,7 @@ class OpenGLMobject:
|
|||
mobs = self.submobjects
|
||||
new_colors = color_gradient(colors, len(mobs))
|
||||
|
||||
for mob, color in zip(mobs, new_colors, strict=False):
|
||||
for mob, color in zip(mobs, new_colors, strict=True):
|
||||
mob.set_color(color)
|
||||
return self
|
||||
|
||||
|
|
@ -2481,7 +2481,7 @@ class OpenGLMobject:
|
|||
return OpenGLGroup(
|
||||
*(
|
||||
template.copy().pointwise_become_partial(self, a1, a2)
|
||||
for a1, a2 in zip(alphas[:-1], alphas[1:], strict=False)
|
||||
for a1, a2 in zip(alphas[:-1], alphas[1:], strict=True)
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -2612,7 +2612,7 @@ class OpenGLMobject:
|
|||
mob1.add_n_more_submobjects(max(0, n2 - n1))
|
||||
mob2.add_n_more_submobjects(max(0, n1 - n2))
|
||||
# Recurse
|
||||
for sm1, sm2 in zip(mob1.submobjects, mob2.submobjects, strict=False):
|
||||
for sm1, sm2 in zip(mob1.submobjects, mob2.submobjects, strict=True):
|
||||
sm1.align_family(sm2)
|
||||
return self
|
||||
|
||||
|
|
@ -2638,7 +2638,7 @@ class OpenGLMobject:
|
|||
repeat_indices = (np.arange(target) * curr) // target
|
||||
split_factors = [(repeat_indices == i).sum() for i in range(curr)]
|
||||
new_submobs = []
|
||||
for submob, sf in zip(self.submobjects, split_factors, strict=False):
|
||||
for submob, sf in zip(self.submobjects, split_factors, strict=True):
|
||||
new_submobs.append(submob)
|
||||
for _ in range(1, sf):
|
||||
new_submob = submob.copy()
|
||||
|
|
@ -2780,7 +2780,7 @@ class OpenGLMobject:
|
|||
mobject.move_to(self.get_center())
|
||||
|
||||
self.align_family(mobject)
|
||||
for sm1, sm2 in zip(self.get_family(), mobject.get_family(), strict=False):
|
||||
for sm1, sm2 in zip(self.get_family(), mobject.get_family(), strict=True):
|
||||
sm1.set_data(sm2.data)
|
||||
sm1.set_uniforms(sm2.uniforms)
|
||||
self.refresh_bounding_box(recurse_down=True)
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
|||
from manim.utils.bezier import interpolate
|
||||
from manim.utils.color import (
|
||||
BLACK,
|
||||
PURE_YELLOW,
|
||||
WHITE,
|
||||
YELLOW,
|
||||
ParsableManimColor,
|
||||
color_gradient,
|
||||
color_to_rgba,
|
||||
|
|
@ -48,7 +48,7 @@ class OpenGLPMobject(OpenGLMobject):
|
|||
def __init__(
|
||||
self,
|
||||
stroke_width: float = 2.0,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
color: ParsableManimColor = PURE_YELLOW,
|
||||
render_primitive: int = moderngl.POINTS,
|
||||
**kwargs,
|
||||
):
|
||||
|
|
@ -79,7 +79,7 @@ class OpenGLPMobject(OpenGLMobject):
|
|||
Rgbas must be a Nx4 numpy array if it is not None.
|
||||
"""
|
||||
if rgbas is None and color is None:
|
||||
color = YELLOW
|
||||
color = PURE_YELLOW
|
||||
self.append_points(points)
|
||||
# rgbas array will have been resized with points
|
||||
if color is not None:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
|||
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import moderngl
|
||||
import numpy as np
|
||||
|
|
@ -9,7 +10,6 @@ from PIL import Image
|
|||
|
||||
from manim.constants import *
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.typing import Point3D_Array, Vector3D_Array
|
||||
from manim.utils.bezier import integer_interpolate, interpolate
|
||||
from manim.utils.color import *
|
||||
from manim.utils.config_ops import _Data, _Uniforms
|
||||
|
|
@ -17,6 +17,11 @@ from manim.utils.images import change_to_rgba_array, get_full_raster_image_path
|
|||
from manim.utils.iterables import listify
|
||||
from manim.utils.space_ops import normalize_along_axis
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy.typing as npt
|
||||
|
||||
from manim.typing import Point3D_Array, Vector3D_Array
|
||||
|
||||
__all__ = ["OpenGLSurface", "OpenGLTexturedSurface"]
|
||||
|
||||
|
||||
|
|
@ -83,7 +88,7 @@ class OpenGLSurface(OpenGLMobject):
|
|||
render_primitive=moderngl.TRIANGLES,
|
||||
depth_test=True,
|
||||
shader_folder=None,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.passed_uv_func = uv_func
|
||||
self.u_range = u_range if u_range is not None else (0, 1)
|
||||
|
|
@ -374,7 +379,7 @@ class OpenGLTexturedSurface(OpenGLSurface):
|
|||
def __init__(
|
||||
self,
|
||||
uv_surface: OpenGLSurface,
|
||||
image_file: str | Path,
|
||||
image_file: str | Path | npt.NDArray,
|
||||
dark_image_file: str | Path = None,
|
||||
image_mode: str | Iterable[str] = "RGBA",
|
||||
shader_folder: str | Path = None,
|
||||
|
|
@ -416,7 +421,7 @@ class OpenGLTexturedSurface(OpenGLSurface):
|
|||
self,
|
||||
image_file: str | Path,
|
||||
image_mode: str,
|
||||
):
|
||||
) -> Image.Image:
|
||||
image_file = get_full_raster_image_path(image_file)
|
||||
return Image.open(image_file).convert(image_mode)
|
||||
|
||||
|
|
|
|||
|
|
@ -350,7 +350,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
return self
|
||||
elif len(submobs2) == 0:
|
||||
submobs2 = [vmobject]
|
||||
for sm1, sm2 in zip(*make_even(submobs1, submobs2), strict=False):
|
||||
for sm1, sm2 in zip(*make_even(submobs1, submobs2), strict=True):
|
||||
sm1.match_style(sm2)
|
||||
return self
|
||||
|
||||
|
|
@ -580,7 +580,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
new_points.extend(
|
||||
[
|
||||
partial_bezier_points(tup, a1, a2)
|
||||
for a1, a2 in zip(alphas, alphas[1:], strict=False)
|
||||
for a1, a2 in zip(alphas[:-1], alphas[1:], strict=True)
|
||||
],
|
||||
)
|
||||
else:
|
||||
|
|
@ -769,7 +769,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
split_indices = [0, *split_indices, len(points)]
|
||||
return [
|
||||
points[i1:i2]
|
||||
for i1, i2 in zip(split_indices, split_indices[1:], strict=False)
|
||||
for i1, i2 in zip(split_indices[:-1], split_indices[1:], strict=True)
|
||||
if (i2 - i1) >= nppc
|
||||
]
|
||||
|
||||
|
|
@ -1093,7 +1093,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
|
||||
s = self.get_start_anchors()
|
||||
e = self.get_end_anchors()
|
||||
return list(it.chain.from_iterable(zip(s, e, strict=False)))
|
||||
return list(it.chain.from_iterable(zip(s, e, strict=True)))
|
||||
|
||||
def get_points_without_null_curves(self, atol=1e-9):
|
||||
nppc = self.n_points_per_curve
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@ from ..geometry.arc import Circle
|
|||
from ..geometry.line import Line
|
||||
from ..geometry.polygram import Polygon, Rectangle, RoundedRectangle
|
||||
from ..opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from ..types.vectorized_mobject import VMobject
|
||||
from ..types.vectorized_mobject import VGroup, VMobject
|
||||
|
||||
__all__ = ["SVGMobject", "VMobjectFromSVGPath"]
|
||||
|
||||
|
||||
SVG_HASH_TO_MOB_MAP: dict[int, VMobject] = {}
|
||||
SVG_HASH_TO_MOB_MAP: dict[int, SVGMobject] = {}
|
||||
|
||||
|
||||
def _convert_point_to_3d(x: float, y: float) -> np.ndarray:
|
||||
|
|
@ -127,6 +127,7 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
self.stroke_color = stroke_color
|
||||
self.stroke_opacity = stroke_opacity # type: ignore[assignment]
|
||||
self.stroke_width = stroke_width # type: ignore[assignment]
|
||||
self.id_to_vgroup_dict: dict[str, VGroup] = {}
|
||||
if self.stroke_width is None:
|
||||
self.stroke_width = 0
|
||||
|
||||
|
|
@ -170,6 +171,7 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
if hash_val in SVG_HASH_TO_MOB_MAP:
|
||||
mob = SVG_HASH_TO_MOB_MAP[hash_val].copy()
|
||||
self.add(*mob)
|
||||
self.id_to_vgroup_dict = mob.id_to_vgroup_dict
|
||||
return
|
||||
|
||||
self.generate_mobject()
|
||||
|
|
@ -203,8 +205,9 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
svg = se.SVG.parse(modified_file_path)
|
||||
modified_file_path.unlink()
|
||||
|
||||
mobjects = self.get_mobjects_from(svg)
|
||||
mobjects, mobject_dict = self.get_mobjects_from(svg)
|
||||
self.add(*mobjects)
|
||||
self.id_to_vgroup_dict = mobject_dict
|
||||
self.flip(RIGHT) # Flip y
|
||||
|
||||
def get_file_path(self) -> Path:
|
||||
|
|
@ -258,7 +261,9 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
result[svg_key] = str(svg_default_dict[style_key])
|
||||
return result
|
||||
|
||||
def get_mobjects_from(self, svg: se.SVG) -> list[VMobject]:
|
||||
def get_mobjects_from(
|
||||
self, svg: se.SVG
|
||||
) -> tuple[list[VMobject], dict[str, VGroup]]:
|
||||
"""Convert the elements of the SVG to a list of mobjects.
|
||||
|
||||
Parameters
|
||||
|
|
@ -267,36 +272,77 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
The parsed SVG file.
|
||||
"""
|
||||
result: list[VMobject] = []
|
||||
for shape in svg.elements():
|
||||
# can we combine the two continue cases into one?
|
||||
if isinstance(shape, se.Group): # noqa: SIM114
|
||||
continue
|
||||
elif isinstance(shape, se.Path):
|
||||
mob: VMobject = self.path_to_mobject(shape)
|
||||
elif isinstance(shape, se.SimpleLine):
|
||||
mob = self.line_to_mobject(shape)
|
||||
elif isinstance(shape, se.Rect):
|
||||
mob = self.rect_to_mobject(shape)
|
||||
elif isinstance(shape, (se.Circle, se.Ellipse)):
|
||||
mob = self.ellipse_to_mobject(shape)
|
||||
elif isinstance(shape, se.Polygon):
|
||||
mob = self.polygon_to_mobject(shape)
|
||||
elif isinstance(shape, se.Polyline):
|
||||
mob = self.polyline_to_mobject(shape)
|
||||
elif isinstance(shape, se.Text):
|
||||
mob = self.text_to_mobject(shape)
|
||||
elif isinstance(shape, se.Use) or type(shape) is se.SVGElement:
|
||||
continue
|
||||
else:
|
||||
logger.warning(f"Unsupported element type: {type(shape)}")
|
||||
continue
|
||||
if mob is None or not mob.has_points():
|
||||
continue
|
||||
self.apply_style_to_mobject(mob, shape)
|
||||
if isinstance(shape, se.Transformable) and shape.apply:
|
||||
self.handle_transform(mob, shape.transform)
|
||||
result.append(mob)
|
||||
return result
|
||||
stack: list[tuple[se.SVGElement, int]] = []
|
||||
stack.append((svg, 1))
|
||||
group_id_number = 0
|
||||
vgroup_stack: list[str] = ["root"]
|
||||
vgroup_names: list[str] = ["root"]
|
||||
vgroups: dict[str, VGroup] = {"root": VGroup()}
|
||||
while len(stack) > 0:
|
||||
element, depth = stack.pop()
|
||||
# Reduce stack heights
|
||||
vgroup_stack = vgroup_stack[0:(depth)]
|
||||
try:
|
||||
group_name = str(element.values["id"])
|
||||
except Exception:
|
||||
group_name = f"numbered_group_{group_id_number}"
|
||||
group_id_number += 1
|
||||
vg = VGroup()
|
||||
vgroup_names.append(group_name)
|
||||
vgroup_stack.append(group_name)
|
||||
vgroups[group_name] = vg
|
||||
|
||||
if isinstance(element, (se.Group, se.Use)):
|
||||
stack.extend((subelement, depth + 1) for subelement in element[::-1])
|
||||
# Add element to the parent vgroup
|
||||
try:
|
||||
if isinstance(
|
||||
element,
|
||||
(
|
||||
se.Path,
|
||||
se.SimpleLine,
|
||||
se.Rect,
|
||||
se.Circle,
|
||||
se.Ellipse,
|
||||
se.Polygon,
|
||||
se.Polyline,
|
||||
se.Text,
|
||||
),
|
||||
):
|
||||
mob = self.get_mob_from_shape_element(element)
|
||||
if mob is not None:
|
||||
result.append(mob)
|
||||
for parent_name in vgroup_stack[:-1]:
|
||||
vgroups[parent_name].add(mob)
|
||||
except Exception as e:
|
||||
logger.error(f"Exception occurred in 'get_mobjects_from'. Details: {e}")
|
||||
|
||||
return result, vgroups
|
||||
|
||||
def get_mob_from_shape_element(self, shape: se.SVGElement) -> VMobject | None:
|
||||
if isinstance(shape, se.Path):
|
||||
mob: VMobject | None = self.path_to_mobject(shape)
|
||||
elif isinstance(shape, se.SimpleLine):
|
||||
mob = self.line_to_mobject(shape)
|
||||
elif isinstance(shape, se.Rect):
|
||||
mob = self.rect_to_mobject(shape)
|
||||
elif isinstance(shape, (se.Circle, se.Ellipse)):
|
||||
mob = self.ellipse_to_mobject(shape)
|
||||
elif isinstance(shape, se.Polygon):
|
||||
mob = self.polygon_to_mobject(shape)
|
||||
elif isinstance(shape, se.Polyline):
|
||||
mob = self.polyline_to_mobject(shape)
|
||||
elif isinstance(shape, se.Text):
|
||||
mob = self.text_to_mobject(shape)
|
||||
else:
|
||||
logger.warning(f"Unsupported element type: {type(shape)}")
|
||||
mob = None
|
||||
if mob is None or not mob.has_points():
|
||||
return mob
|
||||
self.apply_style_to_mobject(mob, shape)
|
||||
if isinstance(shape, se.Transformable) and shape.apply:
|
||||
self.handle_transform(mob, shape.transform)
|
||||
return mob
|
||||
|
||||
@staticmethod
|
||||
def handle_transform(mob: VMobject, matrix: se.Matrix) -> VMobject:
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ from ..animation.composition import AnimationGroup
|
|||
from ..animation.creation import Create, Write
|
||||
from ..animation.fading import FadeIn
|
||||
from ..mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from ..utils.color import BLACK, YELLOW, ManimColor, ParsableManimColor
|
||||
from ..utils.color import BLACK, PURE_YELLOW, ManimColor, ParsableManimColor
|
||||
from .utils import get_vectorized_mobject_class
|
||||
|
||||
|
||||
|
|
@ -811,7 +811,10 @@ class Table(VGroup):
|
|||
return rec
|
||||
|
||||
def get_highlighted_cell(
|
||||
self, pos: Sequence[int] = (1, 1), color: ParsableManimColor = YELLOW, **kwargs
|
||||
self,
|
||||
pos: Sequence[int] = (1, 1),
|
||||
color: ParsableManimColor = PURE_YELLOW,
|
||||
**kwargs,
|
||||
) -> BackgroundRectangle:
|
||||
"""Returns a :class:`~.BackgroundRectangle` of the cell at the given position.
|
||||
|
||||
|
|
@ -847,7 +850,10 @@ class Table(VGroup):
|
|||
return bg_cell
|
||||
|
||||
def add_highlighted_cell(
|
||||
self, pos: Sequence[int] = (1, 1), color: ParsableManimColor = YELLOW, **kwargs
|
||||
self,
|
||||
pos: Sequence[int] = (1, 1),
|
||||
color: ParsableManimColor = PURE_YELLOW,
|
||||
**kwargs,
|
||||
) -> Table:
|
||||
"""Highlights one cell at a specific position on the table by adding a :class:`~.BackgroundRectangle`.
|
||||
|
||||
|
|
@ -1078,11 +1084,11 @@ class IntegerTable(Table):
|
|||
[[0,30,45,60,90],
|
||||
[90,60,45,30,0]],
|
||||
col_labels=[
|
||||
MathTex(r"\frac{\sqrt{0}}{2}"),
|
||||
MathTex(r"\frac{\sqrt{1}}{2}"),
|
||||
MathTex(r"\frac{\sqrt{2}}{2}"),
|
||||
MathTex(r"\frac{\sqrt{3}}{2}"),
|
||||
MathTex(r"\frac{\sqrt{4}}{2}")],
|
||||
MathTex(r"\frac{ \sqrt{0} }{2}"),
|
||||
MathTex(r"\frac{ \sqrt{1} }{2}"),
|
||||
MathTex(r"\frac{ \sqrt{2} }{2}"),
|
||||
MathTex(r"\frac{ \sqrt{3} }{2}"),
|
||||
MathTex(r"\frac{ \sqrt{4} }{2}")],
|
||||
row_labels=[MathTex(r"\sin"), MathTex(r"\cos")],
|
||||
h_buff=1,
|
||||
element_to_mobject_config={"unit": r"^{\circ}"})
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ r"""Mobjects representing text rendered using LaTeX.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from manim.utils.color import BLACK, ManimColor, ParsableManimColor
|
||||
from manim.utils.color import BLACK, ParsableManimColor
|
||||
|
||||
__all__ = [
|
||||
"SingleStringMathTex",
|
||||
|
|
@ -23,10 +23,9 @@ __all__ = [
|
|||
]
|
||||
|
||||
|
||||
import itertools as it
|
||||
import operator as op
|
||||
import re
|
||||
from collections.abc import Iterable, Sequence
|
||||
from collections.abc import Iterable
|
||||
from functools import reduce
|
||||
from textwrap import dedent
|
||||
from typing import Any, Self
|
||||
|
|
@ -39,6 +38,10 @@ from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
|||
from manim.utils.tex import TexTemplate
|
||||
from manim.utils.tex_file_writing import tex_to_svg_file
|
||||
|
||||
from ..opengl.opengl_compatibility import ConvertToOpenGL
|
||||
|
||||
MATHTEX_SUBSTRING = "substring"
|
||||
|
||||
|
||||
class SingleStringMathTex(SVGMobject):
|
||||
"""Elementary building block for rendering text with LaTeX.
|
||||
|
|
@ -234,6 +237,26 @@ class MathTex(SingleStringMathTex):
|
|||
t = MathTex(r"\int_a^b f'(x) dx = f(b)- f(a)")
|
||||
self.add(t)
|
||||
|
||||
Notes
|
||||
-----
|
||||
Double-brace notation ``{{ ... }}`` can be used to split a single
|
||||
string argument into multiple submobjects without having to pass
|
||||
separate strings::
|
||||
|
||||
MathTex(r"{{ a^2 }} + {{ b^2 }} = {{ c^2 }}")
|
||||
|
||||
Each ``{{ ... }}`` group and every piece of text between groups
|
||||
becomes its own submobject, which is useful for
|
||||
:class:`~.TransformMatchingTex` animations.
|
||||
|
||||
For ``{{`` to be recognised as a group opener it must appear either
|
||||
at the very start of the string or be immediately preceded by a
|
||||
whitespace character. ``{{`` that follows non-whitespace — such as
|
||||
in ``\frac{{{n}}}{k}`` or ``a^{{2}}`` — is left untouched, so
|
||||
ordinary nested-brace LaTeX is not accidentally split. To prevent
|
||||
an unintentional split, insert a space between the two braces:
|
||||
``{{ ... }}`` → ``{ { ... } }``.
|
||||
|
||||
Tests
|
||||
-----
|
||||
Check that creating a :class:`~.MathTex` works::
|
||||
|
|
@ -264,22 +287,30 @@ class MathTex(SingleStringMathTex):
|
|||
self.tex_template = kwargs.pop("tex_template", config["tex_template"])
|
||||
self.arg_separator = arg_separator
|
||||
self.substrings_to_isolate = (
|
||||
[] if substrings_to_isolate is None else substrings_to_isolate
|
||||
[] if substrings_to_isolate is None else list(substrings_to_isolate)
|
||||
)
|
||||
if tex_to_color_map is None:
|
||||
self.tex_to_color_map: dict[str, ParsableManimColor] = {}
|
||||
else:
|
||||
self.tex_to_color_map = tex_to_color_map
|
||||
self.substrings_to_isolate.extend(self.tex_to_color_map.keys())
|
||||
self.tex_environment = tex_environment
|
||||
self.brace_notation_split_occurred = False
|
||||
self.tex_strings = self._break_up_tex_strings(tex_strings)
|
||||
self.tex_strings = self._prepare_tex_strings(tex_strings)
|
||||
self.matched_strings_and_ids: list[tuple[str, str]] = []
|
||||
|
||||
try:
|
||||
joined_string = self._join_tex_strings_with_unique_deliminters(
|
||||
self.tex_strings, self.substrings_to_isolate
|
||||
)
|
||||
super().__init__(
|
||||
self.arg_separator.join(self.tex_strings),
|
||||
joined_string,
|
||||
tex_environment=self.tex_environment,
|
||||
tex_template=self.tex_template,
|
||||
**kwargs,
|
||||
)
|
||||
# Save the original tex_string
|
||||
self.tex_string = self.arg_separator.join(self.tex_strings)
|
||||
self._break_up_by_substrings()
|
||||
except ValueError as compilation_error:
|
||||
if self.brace_notation_split_occurred:
|
||||
|
|
@ -301,36 +332,188 @@ class MathTex(SingleStringMathTex):
|
|||
if self.organize_left_to_right:
|
||||
self._organize_submobjects_left_to_right()
|
||||
|
||||
def _break_up_tex_strings(self, tex_strings: Sequence[str]) -> list[str]:
|
||||
# Separate out anything surrounded in double braces
|
||||
pre_split_length = len(tex_strings)
|
||||
tex_strings_brace_splitted = [
|
||||
re.split("{{(.*?)}}", str(t)) for t in tex_strings
|
||||
def _prepare_tex_strings(self, tex_strings: Iterable[str]) -> list[str]:
|
||||
# Deal with the case where tex_strings contains integers instead
|
||||
# of strings.
|
||||
tex_strings_validated = [
|
||||
string if isinstance(string, str) else str(string) for string in tex_strings
|
||||
]
|
||||
tex_strings_combined = sum(tex_strings_brace_splitted, [])
|
||||
if len(tex_strings_combined) > pre_split_length:
|
||||
# Locate double curly bracers and split on them.
|
||||
tex_strings_validated_two = []
|
||||
for tex_string in tex_strings_validated:
|
||||
split = self._split_double_braces(tex_string)
|
||||
tex_strings_validated_two.extend(split)
|
||||
if len(tex_strings_validated_two) > len(tex_strings_validated):
|
||||
self.brace_notation_split_occurred = True
|
||||
return [string for string in tex_strings_validated_two if len(string) > 0]
|
||||
|
||||
# Separate out any strings specified in the isolate
|
||||
# or tex_to_color_map lists.
|
||||
patterns = []
|
||||
patterns.extend(
|
||||
[
|
||||
f"({re.escape(ss)})"
|
||||
for ss in it.chain(
|
||||
self.substrings_to_isolate,
|
||||
self.tex_to_color_map.keys(),
|
||||
@staticmethod
|
||||
def _split_double_braces(tex_string: str) -> list[str]:
|
||||
r"""Split *tex_string* on Manim's ``{{ ... }}`` double-brace notation.
|
||||
|
||||
Rules that avoid false positives on ordinary LaTeX source:
|
||||
|
||||
* ``{{`` is only treated as a group opener when it appears at the very
|
||||
start of the string or is immediately preceded by a whitespace
|
||||
character. Naturally-occurring ``{{`` in LaTeX is usually preceded
|
||||
by non-whitespace (e.g. ``\frac{{{n}}}{k}`` or ``a^{{2}}``), so
|
||||
the whitespace guard eliminates the most common false positives
|
||||
without any brace-depth bookkeeping on the outer string.
|
||||
|
||||
* Inside an open group the depth of *real* LaTeX braces is tracked.
|
||||
``}}`` only closes the Manim group when the inner depth is zero,
|
||||
so ``{{ a^{b^{c}} }}`` is handled correctly.
|
||||
|
||||
* Escape sequences are consumed as two-character units in priority
|
||||
order: ``\\`` first (escaped backslash), then ``\{`` / ``\}``
|
||||
(escaped braces). This ensures e.g. ``\\}}`` is read as an
|
||||
escaped backslash followed by a real ``}}`` rather than as
|
||||
``\`` + ``\}`` + lone ``}``.
|
||||
"""
|
||||
segments: list[str] = []
|
||||
current = ""
|
||||
i = 0
|
||||
inside_manim = False
|
||||
inner_depth = 0
|
||||
|
||||
while i < len(tex_string):
|
||||
# --- consume escape sequences as atomic units ---
|
||||
if tex_string[i] == "\\" and i + 1 < len(tex_string):
|
||||
next_ch = tex_string[i + 1]
|
||||
if next_ch == "\\" or next_ch in "{}":
|
||||
# \\ (escaped backslash) checked before \{ / \} so that
|
||||
# the second \ in \\ is never mistaken for an escape prefix.
|
||||
current += tex_string[i : i + 2]
|
||||
i += 2
|
||||
continue
|
||||
|
||||
if not inside_manim:
|
||||
# {{ opens a Manim group only at start-of-string or after whitespace.
|
||||
if tex_string[i : i + 2] == "{{" and (
|
||||
i == 0 or tex_string[i - 1].isspace()
|
||||
):
|
||||
segments.append(current)
|
||||
current = ""
|
||||
inside_manim = True
|
||||
inner_depth = 0
|
||||
i += 2
|
||||
else:
|
||||
current += tex_string[i]
|
||||
i += 1
|
||||
else:
|
||||
if tex_string[i] == "{":
|
||||
inner_depth += 1
|
||||
current += tex_string[i]
|
||||
i += 1
|
||||
elif (
|
||||
tex_string[i] == "}"
|
||||
and inner_depth == 0
|
||||
and tex_string[i : i + 2] == "}}"
|
||||
):
|
||||
# }} at inner depth 0 closes the Manim group.
|
||||
segments.append(current)
|
||||
current = ""
|
||||
inside_manim = False
|
||||
i += 2
|
||||
elif tex_string[i] == "}":
|
||||
inner_depth -= 1
|
||||
current += tex_string[i]
|
||||
i += 1
|
||||
else:
|
||||
current += tex_string[i]
|
||||
i += 1
|
||||
|
||||
segments.append(current)
|
||||
return segments
|
||||
|
||||
def _join_tex_strings_with_unique_deliminters(
|
||||
self, tex_strings: list[str], substrings_to_isolate: Iterable[str]
|
||||
) -> str:
|
||||
joined_string = ""
|
||||
ssIdx = 0
|
||||
for idx, tex_string in enumerate(tex_strings):
|
||||
string_part = rf"\special{{dvisvgm:raw <g id='unique{idx:03d}'>}}"
|
||||
self.matched_strings_and_ids.append((tex_string, f"unique{idx:03d}"))
|
||||
|
||||
# Try to match with all substrings_to_isolate and apply the first match
|
||||
# then match again (on the rest of the string) and continue until no
|
||||
# characters are left in the string
|
||||
unprocessed_string = str(tex_string)
|
||||
processed_string = ""
|
||||
while len(unprocessed_string) > 0:
|
||||
first_match = self._locate_first_match(
|
||||
substrings_to_isolate, unprocessed_string
|
||||
)
|
||||
],
|
||||
|
||||
if first_match:
|
||||
processed, unprocessed_string = self._handle_match(
|
||||
ssIdx, first_match
|
||||
)
|
||||
processed_string = processed_string + processed
|
||||
ssIdx += 1
|
||||
else:
|
||||
processed_string = processed_string + unprocessed_string
|
||||
unprocessed_string = ""
|
||||
|
||||
string_part += processed_string
|
||||
if idx < len(tex_strings) - 1:
|
||||
string_part += self.arg_separator
|
||||
string_part += r"\special{dvisvgm:raw </g>}"
|
||||
joined_string = joined_string + string_part
|
||||
return joined_string
|
||||
|
||||
def _locate_first_match(
|
||||
self, substrings_to_isolate: Iterable[str], unprocessed_string: str
|
||||
) -> re.Match | None:
|
||||
first_match_start = len(unprocessed_string)
|
||||
first_match_length = 0
|
||||
first_match = None
|
||||
for substring in substrings_to_isolate:
|
||||
match = re.match(f"(.*?)({re.escape(substring)})(.*)", unprocessed_string)
|
||||
if match and len(match.group(1)) < first_match_start:
|
||||
first_match = match
|
||||
first_match_start = len(match.group(1))
|
||||
first_match_length = len(match.group(2))
|
||||
elif match and len(match.group(1)) == first_match_start:
|
||||
# Break ties by looking at length of matches.
|
||||
if first_match_length < len(match.group(2)):
|
||||
first_match = match
|
||||
first_match_start = len(match.group(1))
|
||||
first_match_length = len(match.group(2))
|
||||
return first_match
|
||||
|
||||
def _handle_match(self, ssIdx: int, first_match: re.Match) -> tuple[str, str]:
|
||||
pre_match = first_match.group(1)
|
||||
matched_string = first_match.group(2)
|
||||
post_match = first_match.group(3)
|
||||
pre_string = (
|
||||
rf"\special{{dvisvgm:raw <g id='unique{ssIdx:03d}{MATHTEX_SUBSTRING}'>}}"
|
||||
)
|
||||
pattern = "|".join(patterns)
|
||||
if pattern:
|
||||
pieces = []
|
||||
for s in tex_strings_combined:
|
||||
pieces.extend(re.split(pattern, s))
|
||||
else:
|
||||
pieces = tex_strings_combined
|
||||
return [p for p in pieces if p]
|
||||
post_string = r"\special{dvisvgm:raw </g>}"
|
||||
self.matched_strings_and_ids.append(
|
||||
(matched_string, f"unique{ssIdx:03d}{MATHTEX_SUBSTRING}")
|
||||
)
|
||||
processed_string = pre_match + pre_string + matched_string + post_string
|
||||
unprocessed_string = post_match
|
||||
return processed_string, unprocessed_string
|
||||
|
||||
@property
|
||||
def _substring_matches(self) -> list[tuple[str, str]]:
|
||||
"""Return only the 'ss' (substring_to_isolate) matches."""
|
||||
return [
|
||||
(tex, id_)
|
||||
for tex, id_ in self.matched_strings_and_ids
|
||||
if id_.endswith(MATHTEX_SUBSTRING)
|
||||
]
|
||||
|
||||
@property
|
||||
def _main_matches(self) -> list[tuple[str, str]]:
|
||||
"""Return only the main tex_string matches."""
|
||||
return [
|
||||
(tex, id_)
|
||||
for tex, id_ in self.matched_strings_and_ids
|
||||
if not id_.endswith(MATHTEX_SUBSTRING)
|
||||
]
|
||||
|
||||
def _break_up_by_substrings(self) -> Self:
|
||||
"""
|
||||
|
|
@ -339,51 +522,32 @@ class MathTex(SingleStringMathTex):
|
|||
of tex_strings)
|
||||
"""
|
||||
new_submobjects: list[VMobject] = []
|
||||
curr_index = 0
|
||||
for tex_string in self.tex_strings:
|
||||
sub_tex_mob = SingleStringMathTex(
|
||||
tex_string,
|
||||
tex_environment=self.tex_environment,
|
||||
tex_template=self.tex_template,
|
||||
try:
|
||||
for tex_string, tex_string_id in self._main_matches:
|
||||
mtp = MathTexPart()
|
||||
mtp.tex_string = tex_string
|
||||
mtp.add(*self.id_to_vgroup_dict[tex_string_id].submobjects)
|
||||
new_submobjects.append(mtp)
|
||||
except KeyError:
|
||||
logger.error(
|
||||
f"MathTex: Could not find SVG group for tex part '{tex_string}' (id: {tex_string_id}). Using fallback to root group."
|
||||
)
|
||||
num_submobs = len(sub_tex_mob.submobjects)
|
||||
new_index = (
|
||||
curr_index + num_submobs + len("".join(self.arg_separator.split()))
|
||||
)
|
||||
if num_submobs == 0:
|
||||
last_submob_index = min(curr_index, len(self.submobjects) - 1)
|
||||
sub_tex_mob.move_to(self.submobjects[last_submob_index], RIGHT)
|
||||
else:
|
||||
sub_tex_mob.submobjects = self.submobjects[curr_index:new_index]
|
||||
new_submobjects.append(sub_tex_mob)
|
||||
curr_index = new_index
|
||||
new_submobjects.append(self.id_to_vgroup_dict["root"])
|
||||
self.submobjects = new_submobjects
|
||||
return self
|
||||
|
||||
def get_parts_by_tex(
|
||||
self, tex: str, substring: bool = True, case_sensitive: bool = True
|
||||
) -> VGroup:
|
||||
def test(tex1: str, tex2: str) -> bool:
|
||||
if not case_sensitive:
|
||||
tex1 = tex1.lower()
|
||||
tex2 = tex2.lower()
|
||||
if substring:
|
||||
return tex1 in tex2
|
||||
else:
|
||||
return tex1 == tex2
|
||||
|
||||
return VGroup(*(m for m in self.submobjects if test(tex, m.get_tex_string())))
|
||||
|
||||
def get_part_by_tex(self, tex: str, **kwargs: Any) -> MathTex | None:
|
||||
all_parts = self.get_parts_by_tex(tex, **kwargs)
|
||||
return all_parts[0] if all_parts else None
|
||||
def get_part_by_tex(self, tex: str, **kwargs: Any) -> VGroup | None:
|
||||
for tex_str, match_id in self.matched_strings_and_ids:
|
||||
if tex_str == tex:
|
||||
return self.id_to_vgroup_dict[match_id]
|
||||
return None
|
||||
|
||||
def set_color_by_tex(
|
||||
self, tex: str, color: ParsableManimColor, **kwargs: Any
|
||||
) -> Self:
|
||||
parts_to_color = self.get_parts_by_tex(tex, **kwargs)
|
||||
for part in parts_to_color:
|
||||
part.set_color(color)
|
||||
for tex_str, match_id in self.matched_strings_and_ids:
|
||||
if tex_str == tex:
|
||||
self.id_to_vgroup_dict[match_id].set_color(color)
|
||||
return self
|
||||
|
||||
def set_opacity_by_tex(
|
||||
|
|
@ -409,22 +573,18 @@ class MathTex(SingleStringMathTex):
|
|||
"""
|
||||
if remaining_opacity is not None:
|
||||
self.set_opacity(opacity=remaining_opacity)
|
||||
for part in self.get_parts_by_tex(tex):
|
||||
part.set_opacity(opacity)
|
||||
for tex_str, match_id in self.matched_strings_and_ids:
|
||||
if tex_str == tex:
|
||||
self.id_to_vgroup_dict[match_id].set_opacity(opacity)
|
||||
return self
|
||||
|
||||
def set_color_by_tex_to_color_map(
|
||||
self, texs_to_color_map: dict[str, ParsableManimColor], **kwargs: Any
|
||||
) -> Self:
|
||||
for texs, color in list(texs_to_color_map.items()):
|
||||
try:
|
||||
# If the given key behaves like tex_strings
|
||||
texs + ""
|
||||
self.set_color_by_tex(texs, ManimColor(color), **kwargs)
|
||||
except TypeError:
|
||||
# If the given key is a tuple
|
||||
for tex in texs:
|
||||
self.set_color_by_tex(tex, ManimColor(color), **kwargs)
|
||||
for match in self.matched_strings_and_ids:
|
||||
if match[0] == texs:
|
||||
self.id_to_vgroup_dict[match[1]].set_color(color)
|
||||
return self
|
||||
|
||||
def index_of_part(self, part: MathTex) -> int:
|
||||
|
|
@ -433,16 +593,17 @@ class MathTex(SingleStringMathTex):
|
|||
raise ValueError("Trying to get index of part not in MathTex")
|
||||
return split_self.index(part)
|
||||
|
||||
def index_of_part_by_tex(self, tex: str, **kwargs: Any) -> int:
|
||||
part = self.get_part_by_tex(tex, **kwargs)
|
||||
if part is None:
|
||||
return -1
|
||||
return self.index_of_part(part)
|
||||
|
||||
def sort_alphabetically(self) -> None:
|
||||
self.submobjects.sort(key=lambda m: m.get_tex_string())
|
||||
|
||||
|
||||
class MathTexPart(VMobject, metaclass=ConvertToOpenGL):
|
||||
tex_string: str
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{type(self).__name__}({repr(self.tex_string)})"
|
||||
|
||||
|
||||
class Tex(MathTex):
|
||||
r"""A string compiled with LaTeX in normal mode.
|
||||
|
||||
|
|
|
|||
|
|
@ -801,8 +801,7 @@ class Text(SVGMobject):
|
|||
line_spacing /= TEXT2SVG_ADJUSTMENT_FACTOR
|
||||
|
||||
dir_name = config.get_dir("text_dir")
|
||||
if not dir_name.is_dir():
|
||||
dir_name.mkdir(parents=True)
|
||||
dir_name.mkdir(parents=True, exist_ok=True)
|
||||
hash_name = self._text2hash(color)
|
||||
file_name = dir_name / (hash_name + ".svg")
|
||||
|
||||
|
|
@ -1349,8 +1348,7 @@ class MarkupText(SVGMobject):
|
|||
line_spacing /= TEXT2SVG_ADJUSTMENT_FACTOR
|
||||
|
||||
dir_name = config.get_dir("text_dir")
|
||||
if not dir_name.is_dir():
|
||||
dir_name.mkdir(parents=True)
|
||||
dir_name.mkdir(parents=True, exist_ok=True)
|
||||
hash_name = self._text2hash(color)
|
||||
file_name = dir_name / (hash_name + ".svg")
|
||||
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ class Polyhedron(VGroup):
|
|||
"""Creates list of cyclic pairwise tuples."""
|
||||
edges: list[tuple[int, int]] = []
|
||||
for face in faces_list:
|
||||
edges += zip(face, face[1:] + face[:1], strict=False)
|
||||
edges += zip(face, face[1:] + face[:1], strict=True)
|
||||
return edges
|
||||
|
||||
def create_faces(
|
||||
|
|
|
|||
|
|
@ -18,7 +18,13 @@ from ...camera.moving_camera import MovingCamera
|
|||
from ...constants import *
|
||||
from ...mobject.mobject import Mobject
|
||||
from ...utils.bezier import interpolate
|
||||
from ...utils.color import WHITE, ManimColor, color_to_int_rgb
|
||||
from ...utils.color import (
|
||||
WHITE,
|
||||
YELLOW_C,
|
||||
ManimColor,
|
||||
ParsableManimColor,
|
||||
color_to_int_rgb,
|
||||
)
|
||||
from ...utils.images import change_to_rgba_array, get_full_raster_image_path
|
||||
|
||||
__all__ = ["ImageMobject", "ImageMobjectFromCamera"]
|
||||
|
|
@ -62,9 +68,14 @@ class AbstractImageMobject(Mobject):
|
|||
def get_pixel_array(self) -> PixelArray:
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_color(self, color, alpha=None, family=True):
|
||||
def set_color( # type: ignore[override]
|
||||
self,
|
||||
color: ParsableManimColor = YELLOW_C,
|
||||
alpha: Any = None,
|
||||
family: bool = True,
|
||||
) -> AbstractImageMobject:
|
||||
# Likely to be implemented in subclasses, but no obligation
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_resampling_algorithm(self, resampling_algorithm: int) -> Self:
|
||||
"""
|
||||
|
|
@ -87,18 +98,18 @@ class AbstractImageMobject(Mobject):
|
|||
* 'hamming'
|
||||
* 'lanczos' or 'antialias'
|
||||
"""
|
||||
if isinstance(resampling_algorithm, int):
|
||||
self.resampling_algorithm = resampling_algorithm
|
||||
else:
|
||||
if resampling_algorithm not in RESAMPLING_ALGORITHMS.values():
|
||||
raise ValueError(
|
||||
"resampling_algorithm has to be an int, one of the values defined in "
|
||||
"RESAMPLING_ALGORITHMS or a Pillow resampling filter constant. "
|
||||
"Available algorithms: 'bicubic', 'nearest', 'box', 'bilinear', "
|
||||
"'hamming', 'lanczos'.",
|
||||
"Available algorithms: 'bicubic' (or 'cubic'), 'nearest' (or 'none'), "
|
||||
"'bilinear' (or 'linear').",
|
||||
)
|
||||
|
||||
self.resampling_algorithm = resampling_algorithm
|
||||
return self
|
||||
|
||||
def reset_points(self) -> None:
|
||||
def reset_points(self) -> Self:
|
||||
"""Sets :attr:`points` to be the four image corners."""
|
||||
self.points = np.array(
|
||||
[
|
||||
|
|
@ -116,6 +127,7 @@ class AbstractImageMobject(Mobject):
|
|||
height = 3 # this is the case for ImageMobjectFromCamera
|
||||
self.stretch_to_fit_height(height)
|
||||
self.stretch_to_fit_width(height * w / h)
|
||||
return self
|
||||
|
||||
|
||||
class ImageMobject(AbstractImageMobject):
|
||||
|
|
@ -157,27 +169,18 @@ class ImageMobject(AbstractImageMobject):
|
|||
[0, 0, 0, 255]
|
||||
]))
|
||||
|
||||
img.height = 2
|
||||
img1 = img.copy()
|
||||
img2 = img.copy()
|
||||
img3 = img.copy()
|
||||
img4 = img.copy()
|
||||
img5 = img.copy()
|
||||
img.height = 3
|
||||
|
||||
img1.set_resampling_algorithm(RESAMPLING_ALGORITHMS["nearest"])
|
||||
img2.set_resampling_algorithm(RESAMPLING_ALGORITHMS["lanczos"])
|
||||
img3.set_resampling_algorithm(RESAMPLING_ALGORITHMS["linear"])
|
||||
img4.set_resampling_algorithm(RESAMPLING_ALGORITHMS["cubic"])
|
||||
img5.set_resampling_algorithm(RESAMPLING_ALGORITHMS["box"])
|
||||
img1.add(Text("nearest").scale(0.5).next_to(img1,UP))
|
||||
img2.add(Text("lanczos").scale(0.5).next_to(img2,UP))
|
||||
img3.add(Text("linear").scale(0.5).next_to(img3,UP))
|
||||
img4.add(Text("cubic").scale(0.5).next_to(img4,UP))
|
||||
img5.add(Text("box").scale(0.5).next_to(img5,UP))
|
||||
group = Group()
|
||||
algorithm_texts = ["nearest", "linear", "cubic"]
|
||||
for algorithm_text in algorithm_texts:
|
||||
algorithm = RESAMPLING_ALGORITHMS[algorithm_text]
|
||||
img_copy = img.copy().set_resampling_algorithm(algorithm)
|
||||
img_copy.add(Text(algorithm_text).scale(0.5).next_to(img_copy, UP))
|
||||
group.add(img_copy)
|
||||
|
||||
x= Group(img1,img2,img3,img4,img5)
|
||||
x.arrange()
|
||||
self.add(x)
|
||||
group.arrange()
|
||||
self.add(group)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -210,18 +213,23 @@ class ImageMobject(AbstractImageMobject):
|
|||
self.orig_alpha_pixel_array = self.pixel_array[:, :, 3].copy()
|
||||
super().__init__(scale_to_resolution, **kwargs)
|
||||
|
||||
def get_pixel_array(self):
|
||||
def get_pixel_array(self) -> PixelArray:
|
||||
"""A simple getter method."""
|
||||
return self.pixel_array
|
||||
|
||||
def set_color(self, color, alpha=None, family=True):
|
||||
def set_color( # type: ignore[override]
|
||||
self,
|
||||
color: ParsableManimColor = YELLOW_C,
|
||||
alpha: Any = None,
|
||||
family: bool = True,
|
||||
) -> Self:
|
||||
rgb = color_to_int_rgb(color)
|
||||
self.pixel_array[:, :, :3] = rgb
|
||||
if alpha is not None:
|
||||
self.pixel_array[:, :, 3] = int(255 * alpha)
|
||||
for submob in self.submobjects:
|
||||
submob.set_color(color, alpha, family)
|
||||
self.color = color
|
||||
self.color = ManimColor(color)
|
||||
return self
|
||||
|
||||
def set_opacity(self, alpha: float) -> Self:
|
||||
|
|
@ -253,7 +261,7 @@ class ImageMobject(AbstractImageMobject):
|
|||
return self
|
||||
|
||||
def interpolate_color(
|
||||
self, mobject1: ImageMobject, mobject2: ImageMobject, alpha: float
|
||||
self, mobject1: Mobject, mobject2: Mobject, alpha: float
|
||||
) -> None:
|
||||
"""Interpolates the array of pixel color values from one ImageMobject
|
||||
into an array of equal size in the target ImageMobject.
|
||||
|
|
@ -269,6 +277,8 @@ class ImageMobject(AbstractImageMobject):
|
|||
alpha
|
||||
Used to track the lerp relationship. Not opacity related.
|
||||
"""
|
||||
assert isinstance(mobject1, ImageMobject)
|
||||
assert isinstance(mobject2, ImageMobject)
|
||||
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"
|
||||
|
|
@ -292,7 +302,7 @@ class ImageMobject(AbstractImageMobject):
|
|||
|
||||
def get_style(self) -> dict[str, Any]:
|
||||
return {
|
||||
"fill_color": ManimColor(self.color.get_rgb()).to_hex(),
|
||||
"fill_color": ManimColor(self.color.to_rgb()).to_hex(),
|
||||
"fill_opacity": self.fill_opacity,
|
||||
}
|
||||
|
||||
|
|
@ -321,7 +331,7 @@ class ImageMobjectFromCamera(AbstractImageMobject):
|
|||
super().__init__(scale_to_resolution=False, **kwargs)
|
||||
|
||||
# TODO: Get rid of this.
|
||||
def get_pixel_array(self):
|
||||
def get_pixel_array(self) -> PixelArray:
|
||||
self.pixel_array = self.camera.pixel_array
|
||||
return self.pixel_array
|
||||
|
||||
|
|
@ -332,7 +342,11 @@ class ImageMobjectFromCamera(AbstractImageMobject):
|
|||
self.add(self.display_frame)
|
||||
return self
|
||||
|
||||
def interpolate_color(self, mobject1, mobject2, alpha) -> None:
|
||||
def interpolate_color(
|
||||
self, mobject1: Mobject, mobject2: Mobject, alpha: float
|
||||
) -> None:
|
||||
assert isinstance(mobject1, ImageMobjectFromCamera)
|
||||
assert isinstance(mobject2, ImageMobjectFromCamera)
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ from ...mobject.mobject import Mobject
|
|||
from ...utils.bezier import interpolate
|
||||
from ...utils.color import (
|
||||
BLACK,
|
||||
PURE_YELLOW,
|
||||
WHITE,
|
||||
YELLOW,
|
||||
ManimColor,
|
||||
ParsableManimColor,
|
||||
color_gradient,
|
||||
|
|
@ -110,7 +110,7 @@ class PMobject(Mobject, metaclass=ConvertToOpenGL):
|
|||
return self
|
||||
|
||||
def set_color(
|
||||
self, color: ParsableManimColor = YELLOW, family: bool = True
|
||||
self, color: ParsableManimColor = PURE_YELLOW, family: bool = True
|
||||
) -> Self:
|
||||
rgba = color_to_rgba(color)
|
||||
mobs = self.family_members_with_points() if family else [self]
|
||||
|
|
@ -130,7 +130,7 @@ class PMobject(Mobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
def set_color_by_gradient(self, *colors: ParsableManimColor) -> Self:
|
||||
self.rgbas = np.array(
|
||||
list(map(color_to_rgba, color_gradient(*colors, len(self.points)))),
|
||||
list(map(color_to_rgba, color_gradient(colors, len(self.points)))),
|
||||
)
|
||||
return self
|
||||
|
||||
|
|
@ -172,7 +172,7 @@ class PMobject(Mobject, metaclass=ConvertToOpenGL):
|
|||
for mob in self.family_members_with_points():
|
||||
num_points = self.get_num_points()
|
||||
mob.apply_over_attr_arrays(
|
||||
lambda arr, n=num_points: arr[np.arange(0, n, factor)],
|
||||
lambda arr, n=num_points: arr[np.arange(0, n, factor)], # type: ignore[misc]
|
||||
)
|
||||
return self
|
||||
|
||||
|
|
@ -182,7 +182,7 @@ class PMobject(Mobject, metaclass=ConvertToOpenGL):
|
|||
"""Function is any map from R^3 to R"""
|
||||
for mob in self.family_members_with_points():
|
||||
indices = np.argsort(np.apply_along_axis(function, 1, mob.points))
|
||||
mob.apply_over_attr_arrays(lambda arr, idx=indices: arr[idx])
|
||||
mob.apply_over_attr_arrays(lambda arr, idx=indices: arr[idx]) # type: ignore[misc]
|
||||
return self
|
||||
|
||||
def fade_to(
|
||||
|
|
@ -199,7 +199,7 @@ class PMobject(Mobject, metaclass=ConvertToOpenGL):
|
|||
def ingest_submobjects(self) -> Self:
|
||||
attrs = self.get_array_attrs()
|
||||
arrays = list(map(self.get_merged_array, attrs))
|
||||
for attr, array in zip(attrs, arrays, strict=False):
|
||||
for attr, array in zip(attrs, arrays, strict=True):
|
||||
setattr(self, attr, array)
|
||||
self.submobjects = []
|
||||
return self
|
||||
|
|
@ -359,7 +359,7 @@ class PointCloudDot(Mobject1D):
|
|||
radius: float = 2.0,
|
||||
stroke_width: int = 2,
|
||||
density: int = DEFAULT_POINT_DENSITY_1D,
|
||||
color: ManimColor = YELLOW,
|
||||
color: ManimColor = PURE_YELLOW,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.radius = radius
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ class VMobject(Mobject):
|
|||
rgbas: FloatRGBA_Array = np.array(
|
||||
[
|
||||
c.to_rgba_with_alpha(o)
|
||||
for c, o in zip(*make_even(colors, opacities), strict=False)
|
||||
for c, o in zip(*make_even(colors, opacities), strict=True)
|
||||
],
|
||||
)
|
||||
|
||||
|
|
@ -461,7 +461,7 @@ class VMobject(Mobject):
|
|||
return self
|
||||
elif len(submobs2) == 0:
|
||||
submobs2 = [vmobject]
|
||||
for sm1, sm2 in zip(*make_even(submobs1, submobs2), strict=False):
|
||||
for sm1, sm2 in zip(*make_even(submobs1, submobs2), strict=True):
|
||||
sm1.match_style(sm2)
|
||||
return self
|
||||
|
||||
|
|
@ -1337,7 +1337,7 @@ class VMobject(Mobject):
|
|||
split_indices = [0] + list(filtered) + [len(points)]
|
||||
return (
|
||||
points[i1:i2]
|
||||
for i1, i2 in zip(split_indices, split_indices[1:], strict=False)
|
||||
for i1, i2 in zip(split_indices[:-1], split_indices[1:], strict=True)
|
||||
if (i2 - i1) >= nppcc
|
||||
)
|
||||
|
||||
|
|
@ -1691,7 +1691,7 @@ class VMobject(Mobject):
|
|||
|
||||
s = self.get_start_anchors()
|
||||
e = self.get_end_anchors()
|
||||
return list(it.chain.from_iterable(zip(s, e, strict=False)))
|
||||
return list(it.chain.from_iterable(zip(s, e, strict=True)))
|
||||
|
||||
def get_points_defining_boundary(self) -> Point3D_Array:
|
||||
# Probably returns all anchors, but this is weird regarding the name of the method.
|
||||
|
|
|
|||
0
manim/py.typed
Normal file
0
manim/py.typed
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -25,14 +25,23 @@ class Window(PygletWindow):
|
|||
def __init__(
|
||||
self,
|
||||
renderer: OpenGLRenderer,
|
||||
window_size: str = config.window_size,
|
||||
window_size: str | tuple[int, ...] = config.window_size,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
monitors = get_monitors()
|
||||
mon_index = config.window_monitor
|
||||
monitor = monitors[min(mon_index, len(monitors) - 1)]
|
||||
|
||||
if window_size == "default":
|
||||
invalid_window_size_error_message = (
|
||||
"window_size must be specified either as 'default', a string of the form "
|
||||
"'width,height', or a tuple of 2 ints of the form (width, height)."
|
||||
)
|
||||
|
||||
if isinstance(window_size, tuple):
|
||||
if len(window_size) != 2:
|
||||
raise ValueError(invalid_window_size_error_message)
|
||||
size = window_size
|
||||
elif window_size == "default":
|
||||
# make window_width half the width of the monitor
|
||||
# but make it full screen if --fullscreen
|
||||
window_width = monitor.width
|
||||
|
|
@ -48,9 +57,7 @@ class Window(PygletWindow):
|
|||
(window_width, window_height) = tuple(map(int, window_size.split(",")))
|
||||
size = (window_width, window_height)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Window_size must be specified as 'width,height' or 'default'.",
|
||||
)
|
||||
raise ValueError(invalid_window_size_error_message)
|
||||
|
||||
super().__init__(size=size)
|
||||
|
||||
|
|
|
|||
|
|
@ -903,7 +903,24 @@ class Scene:
|
|||
# as soon as there's one that needs updating of
|
||||
# some kind per frame, return the list from that
|
||||
# point forward.
|
||||
animation_mobjects = [anim.mobject for anim in animations]
|
||||
# Imported inside the method to avoid cyclic import.
|
||||
from ..animation.composition import AnimationGroup
|
||||
|
||||
def _collect_animation_mobjects(
|
||||
nested_animations: Iterable[Animation],
|
||||
) -> list[Mobject | OpenGLMobject]:
|
||||
animation_mobjects: list[Mobject | OpenGLMobject] = []
|
||||
for anim in nested_animations:
|
||||
if isinstance(anim, AnimationGroup):
|
||||
animation_mobjects.extend(
|
||||
_collect_animation_mobjects(anim.animations),
|
||||
)
|
||||
else:
|
||||
animation_mobjects.extend(anim.mobject.get_family())
|
||||
return animation_mobjects
|
||||
|
||||
animation_mobjects = _collect_animation_mobjects(animations)
|
||||
|
||||
mobjects = self.get_mobject_family_members()
|
||||
for i, mob in enumerate(mobjects):
|
||||
update_possibilities = [
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ __all__ = ["SceneFileWriter"]
|
|||
|
||||
import json
|
||||
import shutil
|
||||
import warnings
|
||||
from fractions import Fraction
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
|
|
@ -17,7 +18,17 @@ import av
|
|||
import numpy as np
|
||||
import srt
|
||||
from PIL import Image
|
||||
from pydub import AudioSegment
|
||||
|
||||
# Manim handles audio conversion through PyAV directly. Importing pydub emits a
|
||||
# RuntimeWarning if ffmpeg/avconv is not on PATH, even when only WAV code paths
|
||||
# are used (which do not need ffmpeg). Silence this specific warning.
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings(
|
||||
"ignore",
|
||||
message=r".*ffmpeg or avconv.*",
|
||||
category=RuntimeWarning,
|
||||
)
|
||||
from pydub import AudioSegment
|
||||
|
||||
from manim import __version__
|
||||
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@ from ..utils.color import (
|
|||
BLUE_D,
|
||||
GREEN_C,
|
||||
GREY,
|
||||
PURE_YELLOW,
|
||||
RED_C,
|
||||
WHITE,
|
||||
YELLOW,
|
||||
ManimColor,
|
||||
ParsableManimColor,
|
||||
)
|
||||
|
|
@ -172,7 +172,7 @@ class VectorScene(Scene):
|
|||
def add_vector(
|
||||
self,
|
||||
vector: Arrow | Vector3DLike,
|
||||
color: ParsableManimColor | Iterable[ParsableManimColor] = YELLOW,
|
||||
color: ParsableManimColor | Iterable[ParsableManimColor] = PURE_YELLOW,
|
||||
animate: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> Arrow:
|
||||
|
|
@ -808,7 +808,7 @@ class LinearTransformationScene(VectorScene):
|
|||
|
||||
def get_unit_square(
|
||||
self,
|
||||
color: ParsableManimColor | Iterable[ParsableManimColor] = YELLOW,
|
||||
color: ParsableManimColor | Iterable[ParsableManimColor] = PURE_YELLOW,
|
||||
opacity: float = 0.3,
|
||||
stroke_width: float = 3,
|
||||
) -> Rectangle:
|
||||
|
|
@ -875,7 +875,7 @@ class LinearTransformationScene(VectorScene):
|
|||
def add_vector(
|
||||
self,
|
||||
vector: Arrow | list | tuple | np.ndarray,
|
||||
color: ParsableManimColor = YELLOW,
|
||||
color: ParsableManimColor = PURE_YELLOW,
|
||||
animate: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> Arrow:
|
||||
|
|
|
|||
|
|
@ -999,7 +999,7 @@ def bezier_remap(
|
|||
|
||||
new_tuples = np.empty((new_number_of_curves, nppc, dim))
|
||||
index = 0
|
||||
for curve, sf in zip(bezier_tuples, split_factors, strict=False):
|
||||
for curve, sf in zip(bezier_tuples, split_factors, strict=True):
|
||||
new_tuples[index : index + sf] = subdivide_bezier(curve, sf).reshape(
|
||||
sf, nppc, dim
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1418,7 +1418,7 @@ def color_gradient(
|
|||
floors[-1] = num_colors - 2
|
||||
return [
|
||||
rgb_to_color((rgbs[i] * (1 - alpha)) + (rgbs[i + 1] * alpha))
|
||||
for i, alpha in zip(floors, alphas_mod1, strict=False)
|
||||
for i, alpha in zip(floors, alphas_mod1, strict=True)
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -1520,7 +1520,7 @@ class RandomColorGenerator:
|
|||
|
||||
>>> rnd = RandomColorGenerator(42)
|
||||
>>> rnd.next()
|
||||
ManimColor('#ECE7E2')
|
||||
ManimColor('#8B4513')
|
||||
>>> rnd.next()
|
||||
ManimColor('#BBBBBB')
|
||||
>>> rnd.next()
|
||||
|
|
@ -1530,7 +1530,7 @@ class RandomColorGenerator:
|
|||
|
||||
>>> rnd2 = RandomColorGenerator(42)
|
||||
>>> rnd2.next()
|
||||
ManimColor('#ECE7E2')
|
||||
ManimColor('#8B4513')
|
||||
>>> rnd2.next()
|
||||
ManimColor('#BBBBBB')
|
||||
>>> rnd2.next()
|
||||
|
|
|
|||
|
|
@ -102,6 +102,9 @@ These colors form Manim's default color space.
|
|||
"pure_red",
|
||||
"pure_green",
|
||||
"pure_blue",
|
||||
"pure_cyan",
|
||||
"pure_magenta",
|
||||
"pure_yellow",
|
||||
)
|
||||
|
||||
pure_lines = named_lines_group(
|
||||
|
|
@ -145,12 +148,17 @@ DARK_GRAY = ManimColor("#444444")
|
|||
DARK_GREY = ManimColor("#444444")
|
||||
DARKER_GRAY = ManimColor("#222222")
|
||||
DARKER_GREY = ManimColor("#222222")
|
||||
PURE_RED = ManimColor("#FF0000")
|
||||
PURE_GREEN = ManimColor("#00FF00")
|
||||
PURE_BLUE = ManimColor("#0000FF")
|
||||
PURE_CYAN = ManimColor("#00FFFF")
|
||||
PURE_MAGENTA = ManimColor("#FF00FF")
|
||||
PURE_YELLOW = ManimColor("#FFFF00")
|
||||
BLUE_A = ManimColor("#C7E9F1")
|
||||
BLUE_B = ManimColor("#9CDCEB")
|
||||
BLUE_C = ManimColor("#58C4DD")
|
||||
BLUE_D = ManimColor("#29ABCA")
|
||||
BLUE_E = ManimColor("#236B8E")
|
||||
PURE_BLUE = ManimColor("#0000FF")
|
||||
BLUE = ManimColor("#58C4DD")
|
||||
DARK_BLUE = ManimColor("#236B8E")
|
||||
TEAL_A = ManimColor("#ACEAD7")
|
||||
|
|
@ -164,14 +172,13 @@ GREEN_B = ManimColor("#A6CF8C")
|
|||
GREEN_C = ManimColor("#83C167")
|
||||
GREEN_D = ManimColor("#77B05D")
|
||||
GREEN_E = ManimColor("#699C52")
|
||||
PURE_GREEN = ManimColor("#00FF00")
|
||||
GREEN = ManimColor("#83C167")
|
||||
YELLOW_A = ManimColor("#FFF1B6")
|
||||
YELLOW_B = ManimColor("#FFEA94")
|
||||
YELLOW_C = ManimColor("#FFFF00")
|
||||
YELLOW_C = ManimColor("#F7D96F")
|
||||
YELLOW_D = ManimColor("#F4D345")
|
||||
YELLOW_E = ManimColor("#E8C11C")
|
||||
YELLOW = ManimColor("#FFFF00")
|
||||
YELLOW = ManimColor("#F7D96F")
|
||||
GOLD_A = ManimColor("#F7C797")
|
||||
GOLD_B = ManimColor("#F9B775")
|
||||
GOLD_C = ManimColor("#F0AC5F")
|
||||
|
|
@ -183,7 +190,6 @@ RED_B = ManimColor("#FF8080")
|
|||
RED_C = ManimColor("#FC6255")
|
||||
RED_D = ManimColor("#E65A4C")
|
||||
RED_E = ManimColor("#CF5044")
|
||||
PURE_RED = ManimColor("#FF0000")
|
||||
RED = ManimColor("#FC6255")
|
||||
MAROON_A = ManimColor("#ECABC1")
|
||||
MAROON_B = ManimColor("#EC92AB")
|
||||
|
|
|
|||
|
|
@ -153,15 +153,14 @@ def add_version_before_extension(file_name: Path) -> Path:
|
|||
|
||||
|
||||
def guarantee_existence(path: Path) -> Path:
|
||||
if not path.exists():
|
||||
path.mkdir(parents=True)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path.resolve(strict=True)
|
||||
|
||||
|
||||
def guarantee_empty_existence(path: Path) -> Path:
|
||||
if path.exists():
|
||||
shutil.rmtree(str(path))
|
||||
path.mkdir(parents=True)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path.resolve(strict=True)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ def adjacent_n_tuples(objects: Sequence[T], n: int) -> zip[tuple[T, ...]]:
|
|||
>>> list(adjacent_n_tuples([1, 2, 3, 4], 3))
|
||||
[(1, 2, 3), (2, 3, 4), (3, 4, 1), (4, 1, 2)]
|
||||
"""
|
||||
return zip(*([*objects[k:], *objects[:k]] for k in range(n)), strict=False)
|
||||
return zip(*([*objects[k:], *objects[:k]] for k in range(n)), strict=True)
|
||||
|
||||
|
||||
def adjacent_pairs(objects: Sequence[T]) -> zip[tuple[T, ...]]:
|
||||
|
|
|
|||
|
|
@ -9,13 +9,12 @@ __all__ = [
|
|||
"sigmoid",
|
||||
]
|
||||
|
||||
|
||||
import math
|
||||
from collections.abc import Callable
|
||||
from functools import lru_cache
|
||||
from typing import Any, Protocol, TypeVar
|
||||
|
||||
import numpy as np
|
||||
from scipy import special
|
||||
|
||||
|
||||
def binary_search(
|
||||
|
|
@ -87,9 +86,9 @@ def choose(n: int, k: int) -> int:
|
|||
References
|
||||
----------
|
||||
- https://en.wikipedia.org/wiki/Combination
|
||||
- https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.comb.html
|
||||
- https://docs.python.org/3/library/math.html#math.comb
|
||||
"""
|
||||
value: int = special.comb(n, k, exact=True)
|
||||
value: int = math.comb(n, k)
|
||||
return value
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -609,7 +609,7 @@ def find_intersection(
|
|||
# algorithm from https://en.wikipedia.org/wiki/Skew_lines#Nearest_points
|
||||
result = []
|
||||
|
||||
for p0, v0, p1, v1 in zip(p0s, v0s, p1s, v1s, strict=False):
|
||||
for p0, v0, p1, v1 in zip(p0s, v0s, p1s, v1s, strict=True):
|
||||
normal = cross(v1, cross(v0, v1))
|
||||
denom = max(np.dot(v0, normal), threshold)
|
||||
result += [p0 + np.dot(p1 - p0, normal) / denom * v0]
|
||||
|
|
|
|||
|
|
@ -103,8 +103,7 @@ def generate_tex_file(
|
|||
output = tex_template.get_texcode_for_expression(expression)
|
||||
|
||||
tex_dir = config.get_dir("tex_dir")
|
||||
if not tex_dir.exists():
|
||||
tex_dir.mkdir()
|
||||
tex_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
result = tex_dir / (tex_hash(output) + ".tex")
|
||||
if not result.exists():
|
||||
|
|
|
|||
123
mypy.ini
123
mypy.ini
|
|
@ -52,27 +52,15 @@ warn_return_any = True
|
|||
#
|
||||
# disable_recursive_aliases = True
|
||||
|
||||
[mypy-manim.__main__]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim._config.utils]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim._config.cli_colors]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.animation.animation]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.animation.creation]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.animation.indication]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.animation.specialized]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.animation.speedmodifier]
|
||||
ignore_errors = True
|
||||
|
||||
|
|
@ -85,21 +73,9 @@ ignore_errors = True
|
|||
[mypy-manim.animation.updaters.mobject_update_utils]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.camera.camera]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.camera.mapping_camera]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.cli.default_group]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.geometry.boolean_ops]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.geometry.polygram]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.graphing.coordinate_systems]
|
||||
ignore_errors = True
|
||||
|
||||
|
|
@ -112,12 +88,6 @@ ignore_errors = True
|
|||
[mypy-manim.mobject.mobject]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.opengl.opengl_compatibility]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.opengl.opengl_image_mobject]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.opengl.opengl_point_cloud_mobject]
|
||||
ignore_errors = True
|
||||
|
||||
|
|
@ -130,21 +100,12 @@ ignore_errors = True
|
|||
[mypy-manim.mobject.table]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.types.image_mobject]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.types.point_cloud_mobject]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.types.vectorized_mobject]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.vector_field]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.renderer.opengl_renderer]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.renderer.shader_wrapper]
|
||||
ignore_errors = True
|
||||
|
||||
|
|
@ -154,90 +115,6 @@ ignore_errors = True
|
|||
[mypy-manim.utils.hashing]
|
||||
ignore_errors = True
|
||||
|
||||
# Added temporarily due to current mypy failures
|
||||
[mypy-manim.camera.three_d_camera]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.graphing.functions]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.graphing.number_line]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.graphing.probability]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.graphing.scale]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.matrix]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.opengl.opengl_geometry]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.opengl.opengl_mobject]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.svg.brace]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.svg.svg_mobject]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.text.code_mobject]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.three_d.polyhedra]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.three_d.three_d_utils]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.mobject.three_d.three_dimensions]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.renderer.shader]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.renderer.vectorized_mobject_rendering]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.scene.scene]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.scene.scene_file_writer]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.scene.vector_space_scene]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.utils.bezier]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.utils.color.core]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.utils.commands]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.utils.images]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.utils.iterables]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.utils.opengl]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.utils.paths]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.utils.space_ops]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-manim.utils.testing._test_class_makers]
|
||||
ignore_errors = True
|
||||
|
||||
|
||||
# ---------------- Stubless imported Modules --------------------------
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "manim"
|
||||
version = "0.19.2"
|
||||
version = "0.20.0"
|
||||
description = "Animation engine for explanatory math videos."
|
||||
authors = [
|
||||
{name = "The Manim Community Developers", email = "contact@manim.community"},
|
||||
|
|
@ -78,11 +78,9 @@ jupyterlab = [
|
|||
[dependency-groups]
|
||||
dev = [
|
||||
"furo>=2024.8.6",
|
||||
"gitpython>=3.1.44",
|
||||
"matplotlib>=3.9.4",
|
||||
"myst-parser>=3.0.1",
|
||||
"pre-commit>=4.1.0",
|
||||
"pygithub>=2.5.0",
|
||||
"pytest>=8.3.4",
|
||||
"pytest-cov>=6.0.0",
|
||||
"pytest-xdist>=2.2,<3.0",
|
||||
|
|
@ -212,9 +210,6 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
|||
"F403",
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
required-imports = ["from __future__ import annotations"]
|
||||
|
||||
[tool.ruff.lint.flake8-pytest-style]
|
||||
fixture-parentheses = false
|
||||
mark-parentheses = false
|
||||
|
|
|
|||
|
|
@ -1,316 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
"""Script to generate contributor and pull request lists.
|
||||
|
||||
This script generates contributor and pull request lists for release
|
||||
changelogs using Github v3 protocol. Use requires an authentication token in
|
||||
order to have sufficient bandwidth, you can get one following the directions at
|
||||
`<https://help.github.com/articles/creating-an-access-token-for-command-line-use/>_
|
||||
Don't add any scope, as the default is read access to public information. The
|
||||
token may be stored in an environment variable as you only get one chance to
|
||||
see it.
|
||||
|
||||
Usage::
|
||||
|
||||
$ ./scripts/dev_changelog.py [OPTIONS] TOKEN PRIOR TAG [ADDITIONAL]...
|
||||
|
||||
The output is utf8 rst.
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
- gitpython
|
||||
- pygithub
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
From a bash command line with $GITHUB environment variable as the GitHub token::
|
||||
|
||||
$ ./scripts/dev_changelog.py $GITHUB v0.3.0 v0.4.0
|
||||
|
||||
This would generate 0.4.0-changelog.rst file and place it automatically under
|
||||
docs/source/changelog/.
|
||||
|
||||
As another example, you may also run include PRs that have been excluded by
|
||||
providing a space separated list of ticket numbers after TAG::
|
||||
|
||||
$ ./scripts/dev_changelog.py $GITHUB v0.3.0 v0.4.0 1911 1234 1492 ...
|
||||
|
||||
|
||||
Note
|
||||
----
|
||||
|
||||
This script was taken from Numpy under the terms of BSD-3-Clause license.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import concurrent.futures
|
||||
import datetime
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from textwrap import dedent, indent
|
||||
|
||||
import cloup
|
||||
from git import Repo
|
||||
from github import Github
|
||||
from tqdm import tqdm
|
||||
|
||||
from manim.constants import CONTEXT_SETTINGS, EPILOG
|
||||
|
||||
this_repo = Repo(str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
PR_LABELS = {
|
||||
"breaking changes": "Breaking changes",
|
||||
"highlight": "Highlights",
|
||||
"pr:deprecation": "Deprecated classes and functions",
|
||||
"new feature": "New features",
|
||||
"enhancement": "Enhancements",
|
||||
"pr:bugfix": "Fixed bugs",
|
||||
"documentation": "Documentation-related changes",
|
||||
"testing": "Changes concerning the testing system",
|
||||
"infrastructure": "Changes to our development infrastructure",
|
||||
"maintenance": "Code quality improvements and similar refactors",
|
||||
"revert": "Changes that needed to be reverted again",
|
||||
"release": "New releases",
|
||||
"unlabeled": "Unclassified changes",
|
||||
}
|
||||
|
||||
SILENT_CONTRIBUTORS = [
|
||||
"dependabot[bot]",
|
||||
]
|
||||
|
||||
|
||||
def update_citation(version, date):
|
||||
current_directory = Path(__file__).parent
|
||||
parent_directory = current_directory.parent
|
||||
contents = (current_directory / "TEMPLATE.cff").read_text()
|
||||
contents = contents.replace("<version>", version)
|
||||
contents = contents.replace("<date_released>", date)
|
||||
with (parent_directory / "CITATION.cff").open("w", newline="\n") as f:
|
||||
f.write(contents)
|
||||
|
||||
|
||||
def process_pullrequests(lst, cur, github_repo, pr_nums):
|
||||
lst_commit = github_repo.get_commit(sha=this_repo.git.rev_list("-1", lst))
|
||||
lst_date = lst_commit.commit.author.date
|
||||
|
||||
authors = set()
|
||||
reviewers = set()
|
||||
pr_by_labels = defaultdict(list)
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
future_to_num = {
|
||||
executor.submit(github_repo.get_pull, num): num for num in pr_nums
|
||||
}
|
||||
for future in tqdm(
|
||||
concurrent.futures.as_completed(future_to_num), "Processing PRs"
|
||||
):
|
||||
pr = future.result()
|
||||
authors.add(pr.user)
|
||||
reviewers = reviewers.union(rev.user for rev in pr.get_reviews())
|
||||
pr_labels = [label.name for label in pr.labels]
|
||||
for label in PR_LABELS:
|
||||
if label in pr_labels:
|
||||
pr_by_labels[label].append(pr)
|
||||
break # ensure that PR is only added in one category
|
||||
else:
|
||||
pr_by_labels["unlabeled"].append(pr)
|
||||
|
||||
# identify first-time contributors:
|
||||
author_names = []
|
||||
for author in authors:
|
||||
name = author.name if author.name is not None else author.login
|
||||
if name in SILENT_CONTRIBUTORS:
|
||||
continue
|
||||
if github_repo.get_commits(author=author, until=lst_date).totalCount == 0:
|
||||
name += " +"
|
||||
author_names.append(name)
|
||||
|
||||
reviewer_names = []
|
||||
for reviewer in reviewers:
|
||||
name = reviewer.name if reviewer.name is not None else reviewer.login
|
||||
if name in SILENT_CONTRIBUTORS:
|
||||
continue
|
||||
reviewer_names.append(name)
|
||||
|
||||
# Sort items in pr_by_labels
|
||||
for i in pr_by_labels:
|
||||
pr_by_labels[i] = sorted(pr_by_labels[i], key=lambda pr: pr.number)
|
||||
|
||||
return {
|
||||
"authors": sorted(author_names),
|
||||
"reviewers": sorted(reviewer_names),
|
||||
"PRs": pr_by_labels,
|
||||
}
|
||||
|
||||
|
||||
def get_pr_nums(lst, cur):
|
||||
print("Getting PR Numbers:")
|
||||
prnums = []
|
||||
|
||||
# From regular merges
|
||||
merges = this_repo.git.log("--oneline", "--merges", f"{lst}..{cur}")
|
||||
issues = re.findall(r".*\(\#(\d+)\)", merges)
|
||||
prnums.extend(int(s) for s in issues)
|
||||
|
||||
# From fast forward squash-merges
|
||||
commits = this_repo.git.log(
|
||||
"--oneline",
|
||||
"--no-merges",
|
||||
"--first-parent",
|
||||
f"{lst}..{cur}",
|
||||
)
|
||||
split_commits = list(
|
||||
filter(
|
||||
lambda x: not any(
|
||||
["pre-commit autoupdate" in x, "New Crowdin updates" in x]
|
||||
),
|
||||
commits.split("\n"),
|
||||
),
|
||||
)
|
||||
commits = "\n".join(split_commits)
|
||||
issues = re.findall(r"^.*\(\#(\d+)\)$", commits, re.M)
|
||||
prnums.extend(int(s) for s in issues)
|
||||
|
||||
print(prnums)
|
||||
return prnums
|
||||
|
||||
|
||||
def get_summary(body):
|
||||
pattern = '<!--changelog-start-->([^"]*)<!--changelog-end-->'
|
||||
try:
|
||||
has_changelog_pattern = re.search(pattern, body)
|
||||
if has_changelog_pattern:
|
||||
return has_changelog_pattern.group()[22:-21].strip()
|
||||
except Exception:
|
||||
print(f"Error parsing body for changelog: {body}")
|
||||
|
||||
|
||||
@cloup.command(
|
||||
context_settings=CONTEXT_SETTINGS,
|
||||
epilog=EPILOG,
|
||||
)
|
||||
@cloup.argument("token")
|
||||
@cloup.argument("prior")
|
||||
@cloup.argument("tag")
|
||||
@cloup.argument(
|
||||
"additional",
|
||||
nargs=-1,
|
||||
required=False,
|
||||
type=int,
|
||||
)
|
||||
@cloup.option(
|
||||
"-o",
|
||||
"--outfile",
|
||||
type=str,
|
||||
help="Path and file name of the changelog output.",
|
||||
)
|
||||
def main(token, prior, tag, additional, outfile):
|
||||
"""Generate Changelog/List of contributors/PRs for release.
|
||||
|
||||
TOKEN is your GitHub Personal Access Token.
|
||||
|
||||
PRIOR is the tag/commit SHA of the previous release.
|
||||
|
||||
TAG is the tag of the new release.
|
||||
|
||||
ADDITIONAL includes additional PR(s) that have not been recognized automatically.
|
||||
"""
|
||||
lst_release, cur_release = prior, tag
|
||||
|
||||
github = Github(token)
|
||||
github_repo = github.get_repo("ManimCommunity/manim")
|
||||
|
||||
pr_nums = get_pr_nums(lst_release, cur_release)
|
||||
if additional:
|
||||
print(f"Adding {additional} to the mix!")
|
||||
pr_nums = pr_nums + list(additional)
|
||||
|
||||
# document authors
|
||||
contributions = process_pullrequests(lst_release, cur_release, github_repo, pr_nums)
|
||||
authors = contributions["authors"]
|
||||
reviewers = contributions["reviewers"]
|
||||
|
||||
# update citation file
|
||||
today = datetime.date.today()
|
||||
update_citation(tag, str(today))
|
||||
|
||||
if not outfile:
|
||||
outfile = (
|
||||
Path(__file__).resolve().parent.parent / "docs" / "source" / "changelog"
|
||||
)
|
||||
outfile = outfile / f"{tag[1:] if tag.startswith('v') else tag}-changelog.rst"
|
||||
else:
|
||||
outfile = Path(outfile).resolve()
|
||||
|
||||
with outfile.open("w", encoding="utf8", newline="\n") as f:
|
||||
f.write("*" * len(tag) + "\n")
|
||||
f.write(f"{tag}\n")
|
||||
f.write("*" * len(tag) + "\n\n")
|
||||
|
||||
f.write(f":Date: {today.strftime('%B %d, %Y')}\n\n")
|
||||
|
||||
heading = "Contributors"
|
||||
f.write(f"{heading}\n")
|
||||
f.write("=" * len(heading) + "\n\n")
|
||||
f.write(
|
||||
dedent(
|
||||
f"""\
|
||||
A total of {len(set(authors).union(set(reviewers)))} people contributed to this
|
||||
release. People with a '+' by their names authored a patch for the first
|
||||
time.\n
|
||||
""",
|
||||
),
|
||||
)
|
||||
|
||||
for author in authors:
|
||||
f.write(f"* {author}\n")
|
||||
|
||||
f.write("\n")
|
||||
f.write(
|
||||
dedent(
|
||||
"""
|
||||
The patches included in this release have been reviewed by
|
||||
the following contributors.\n
|
||||
""",
|
||||
),
|
||||
)
|
||||
|
||||
for reviewer in reviewers:
|
||||
f.write(f"* {reviewer}\n")
|
||||
|
||||
# document pull requests
|
||||
heading = "Pull requests merged"
|
||||
f.write("\n")
|
||||
f.write(heading + "\n")
|
||||
f.write("=" * len(heading) + "\n\n")
|
||||
f.write(
|
||||
f"A total of {len(pr_nums)} pull requests were merged for this release.\n\n",
|
||||
)
|
||||
|
||||
pr_by_labels = contributions["PRs"]
|
||||
for label in PR_LABELS:
|
||||
pr_of_label = pr_by_labels[label]
|
||||
|
||||
if pr_of_label:
|
||||
heading = PR_LABELS[label]
|
||||
f.write(f"{heading}\n")
|
||||
f.write("-" * len(heading) + "\n\n")
|
||||
|
||||
for PR in pr_by_labels[label]:
|
||||
num = PR.number
|
||||
title = PR.title
|
||||
label = PR.labels
|
||||
f.write(f"* :pr:`{num}`: {title}\n")
|
||||
overview = get_summary(PR.body)
|
||||
if overview:
|
||||
f.write(indent(f"{overview}\n\n", " "))
|
||||
else:
|
||||
f.write("\n\n")
|
||||
|
||||
print(f"Wrote changelog to: {outfile}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -13,8 +13,7 @@ def main():
|
|||
sys.exit(1)
|
||||
npz_file = sys.argv[1]
|
||||
output_folder = pathlib.Path(sys.argv[2])
|
||||
if not output_folder.exists():
|
||||
output_folder.mkdir(parents=True)
|
||||
output_folder.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
data = np.load(npz_file)
|
||||
if "frame_data" not in data:
|
||||
|
|
|
|||
545
scripts/release.py
Normal file
545
scripts/release.py
Normal file
|
|
@ -0,0 +1,545 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Release management tools for Manim.
|
||||
|
||||
This script provides commands for preparing and managing Manim releases:
|
||||
- Generate changelogs from GitHub's release notes API
|
||||
- Update CITATION.cff with new version information
|
||||
- Fetch existing release notes for documentation
|
||||
|
||||
Usage:
|
||||
# Generate changelog for an upcoming release
|
||||
uv run python scripts/release.py changelog --base v0.19.0 --version 0.20.0
|
||||
|
||||
# Also update CITATION.cff at the same time
|
||||
uv run python scripts/release.py changelog --base v0.19.0 --version 0.20.0 --update-citation
|
||||
|
||||
# Update only CITATION.cff
|
||||
uv run python scripts/release.py citation --version 0.20.0
|
||||
|
||||
# Fetch existing release changelogs for documentation
|
||||
uv run python scripts/release.py fetch-releases
|
||||
|
||||
Requirements:
|
||||
- gh CLI installed and authenticated
|
||||
- Python 3.11+
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
# =============================================================================
|
||||
# Constants
|
||||
# =============================================================================
|
||||
|
||||
REPO = "manimcommunity/manim"
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
REPO_ROOT = SCRIPT_DIR.parent
|
||||
CHANGELOG_DIR = REPO_ROOT / "docs" / "source" / "changelog"
|
||||
CITATION_TEMPLATE = SCRIPT_DIR / "TEMPLATE.cff"
|
||||
CITATION_FILE = REPO_ROOT / "CITATION.cff"
|
||||
|
||||
# Minimum version for fetching historical releases
|
||||
DEFAULT_MIN_VERSION = "0.18.0"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GitHub CLI Helpers
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def run_gh(
|
||||
args: Sequence[str],
|
||||
*,
|
||||
check: bool = True,
|
||||
suppress_errors: bool = False,
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
"""
|
||||
Run a gh CLI command.
|
||||
|
||||
Args:
|
||||
args: Arguments to pass to gh
|
||||
check: If True, raise on non-zero exit
|
||||
suppress_errors: If True, don't print errors to stderr
|
||||
|
||||
Returns:
|
||||
CompletedProcess with stdout/stderr
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["gh", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if (
|
||||
result.returncode != 0
|
||||
and not suppress_errors
|
||||
and "not found" not in result.stderr.lower()
|
||||
):
|
||||
click.echo(f"gh error: {result.stderr}", err=True)
|
||||
if check and result.returncode != 0:
|
||||
raise click.ClickException(f"gh command failed: gh {' '.join(args)}")
|
||||
return result
|
||||
|
||||
|
||||
def get_release_tags() -> list[str]:
|
||||
"""Get all published release tags from GitHub, newest first."""
|
||||
result = run_gh(
|
||||
["release", "list", "--repo", REPO, "--limit", "100", "--json", "tagName"],
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0 or not result.stdout.strip():
|
||||
return []
|
||||
|
||||
import json
|
||||
|
||||
try:
|
||||
data = json.loads(result.stdout)
|
||||
return [item["tagName"] for item in data]
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
return []
|
||||
|
||||
|
||||
def get_release_body(tag: str) -> str | None:
|
||||
"""Get the release body for a published release."""
|
||||
result = run_gh(
|
||||
["release", "view", tag, "--repo", REPO, "--json", "body", "--jq", ".body"],
|
||||
check=False,
|
||||
suppress_errors=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
return result.stdout.strip() or None
|
||||
|
||||
|
||||
def get_release_date(tag: str) -> str | None:
|
||||
"""Get the release date formatted as 'Month DD, YYYY'."""
|
||||
result = run_gh(
|
||||
[
|
||||
"release",
|
||||
"view",
|
||||
tag,
|
||||
"--repo",
|
||||
REPO,
|
||||
"--json",
|
||||
"createdAt",
|
||||
"--jq",
|
||||
".createdAt",
|
||||
],
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
|
||||
date_str = result.stdout.strip().strip('"')
|
||||
try:
|
||||
dt = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
|
||||
return dt.strftime("%B %d, %Y")
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def generate_release_notes(head_tag: str, base_tag: str) -> str:
|
||||
"""
|
||||
Generate release notes using GitHub's API.
|
||||
|
||||
This respects .github/release.yml for PR categorization.
|
||||
"""
|
||||
result = run_gh(
|
||||
[
|
||||
"api",
|
||||
f"repos/{REPO}/releases/generate-notes",
|
||||
"--field",
|
||||
f"tag_name={head_tag}",
|
||||
"--field",
|
||||
f"previous_tag_name={base_tag}",
|
||||
"--jq",
|
||||
".body",
|
||||
]
|
||||
)
|
||||
body = result.stdout.strip()
|
||||
if not body:
|
||||
raise click.ClickException("GitHub API returned empty release notes")
|
||||
return body
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Version Utilities
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def normalize_tag(tag: str) -> str:
|
||||
"""Ensure tag has 'v' prefix."""
|
||||
return tag if tag.startswith("v") else f"v{tag}"
|
||||
|
||||
|
||||
def version_from_tag(tag: str) -> str:
|
||||
"""Extract version from tag (e.g., 'v0.18.0' -> '0.18.0')."""
|
||||
return tag[1:] if tag.startswith("v") else tag
|
||||
|
||||
|
||||
def parse_version(version: str) -> tuple[int, ...]:
|
||||
"""Parse version string into comparable tuple."""
|
||||
# Handle post-releases like '0.18.0.post0'
|
||||
version = version.replace(".post", "-post")
|
||||
parts = []
|
||||
for part in version.replace("-", ".").split("."):
|
||||
try:
|
||||
parts.append(int(part))
|
||||
except ValueError:
|
||||
continue
|
||||
# Pad to at least 3 components
|
||||
while len(parts) < 3:
|
||||
parts.append(0)
|
||||
return tuple(parts)
|
||||
|
||||
|
||||
def version_gte(version: str, min_version: str) -> bool:
|
||||
"""Check if version >= min_version."""
|
||||
return parse_version(version) >= parse_version(min_version)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Markdown Conversion
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def convert_to_myst(body: str) -> str:
|
||||
"""
|
||||
Convert GitHub markdown to MyST format.
|
||||
|
||||
- PR URLs -> {pr}`NUMBER`
|
||||
- Issue URLs -> {issue}`NUMBER`
|
||||
- @mentions -> {user}`USERNAME`
|
||||
- Strips HTML comments
|
||||
- Ensures proper list spacing
|
||||
"""
|
||||
lines = body.split("\n")
|
||||
result = []
|
||||
prev_bullet = False
|
||||
|
||||
for line in lines:
|
||||
# Skip HTML comments
|
||||
if line.strip().startswith("<!--") and line.strip().endswith("-->"):
|
||||
continue
|
||||
|
||||
# Convert URLs to extlinks
|
||||
line = re.sub(
|
||||
r"https://github\.com/ManimCommunity/manim/pull/(\d+)",
|
||||
r"{pr}`\1`",
|
||||
line,
|
||||
)
|
||||
line = re.sub(
|
||||
r"https://github\.com/ManimCommunity/manim/issues/(\d+)",
|
||||
r"{issue}`\1`",
|
||||
line,
|
||||
)
|
||||
line = re.sub(r"@([a-zA-Z0-9_-]+)", r"{user}`\1`", line)
|
||||
|
||||
if line.startswith("**Full Changelog**:"):
|
||||
_, url = line.split(":", 1)
|
||||
url = url.strip().replace("vmain", "main")
|
||||
line = f"**Full Changelog**: [Compare view]({url})"
|
||||
|
||||
# Handle list spacing
|
||||
is_bullet = line.strip().startswith("*") and not line.strip().startswith("**")
|
||||
if prev_bullet and not is_bullet and line.strip():
|
||||
result.append("")
|
||||
|
||||
result.append(line)
|
||||
prev_bullet = is_bullet
|
||||
|
||||
return "\n".join(result)
|
||||
|
||||
|
||||
def format_changelog(
|
||||
version: str,
|
||||
body: str,
|
||||
date: str | None = None,
|
||||
title: str | None = None,
|
||||
) -> str:
|
||||
"""Create changelog file content with MyST frontmatter."""
|
||||
title = title or f"v{version}"
|
||||
body = convert_to_myst(body)
|
||||
date_section = f"Date\n: {date}\n" if date else ""
|
||||
|
||||
return f"""---
|
||||
short-title: {title}
|
||||
description: Changelog for {title}
|
||||
---
|
||||
|
||||
# {title}
|
||||
|
||||
{date_section}
|
||||
{body}
|
||||
"""
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# File Operations
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def get_existing_versions() -> set[str]:
|
||||
"""Get versions that already have changelog files."""
|
||||
if not CHANGELOG_DIR.exists():
|
||||
return set()
|
||||
return {
|
||||
f.stem.replace("-changelog", "") for f in CHANGELOG_DIR.glob("*-changelog.*")
|
||||
}
|
||||
|
||||
|
||||
def save_changelog(version: str, content: str) -> Path:
|
||||
"""Save changelog and return filepath."""
|
||||
filepath = CHANGELOG_DIR / f"{version}-changelog.md"
|
||||
filepath.write_text(content)
|
||||
return filepath
|
||||
|
||||
|
||||
def update_citation(version: str, date: str | None = None) -> Path:
|
||||
"""Update CITATION.cff from template."""
|
||||
if not CITATION_TEMPLATE.exists():
|
||||
raise click.ClickException(f"Citation template not found: {CITATION_TEMPLATE}")
|
||||
|
||||
date = date or datetime.now().strftime("%Y-%m-%d")
|
||||
version_tag = normalize_tag(version)
|
||||
|
||||
content = CITATION_TEMPLATE.read_text()
|
||||
content = content.replace("<version>", version_tag)
|
||||
content = content.replace("<date_released>", date)
|
||||
|
||||
CITATION_FILE.write_text(content)
|
||||
return CITATION_FILE
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CLI Commands
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.option(
|
||||
"--dry-run", is_flag=True, help="Show what would be done without making changes"
|
||||
)
|
||||
@click.pass_context
|
||||
def cli(ctx: click.Context, dry_run: bool) -> None:
|
||||
"""Release management tools for Manim."""
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj["dry_run"] = dry_run
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--base", required=True, help="Base tag for comparison (e.g., v0.19.0)")
|
||||
@click.option(
|
||||
"--version", "version", required=True, help="New version number (e.g., 0.20.0)"
|
||||
)
|
||||
@click.option("--head", default="main", help="Head ref for comparison (default: main)")
|
||||
@click.option("--title", help="Custom changelog title (default: vX.Y.Z)")
|
||||
@click.option(
|
||||
"--update-citation",
|
||||
"also_update_citation",
|
||||
is_flag=True,
|
||||
help="Also update CITATION.cff",
|
||||
)
|
||||
@click.pass_context
|
||||
def changelog(
|
||||
ctx: click.Context,
|
||||
base: str,
|
||||
version: str,
|
||||
head: str,
|
||||
title: str | None,
|
||||
also_update_citation: bool,
|
||||
) -> None:
|
||||
"""Generate changelog for an upcoming release.
|
||||
|
||||
Uses GitHub's release notes API with .github/release.yml categorization.
|
||||
"""
|
||||
dry_run = ctx.obj["dry_run"]
|
||||
base_tag = normalize_tag(base)
|
||||
head_tag = normalize_tag(head) if head != "main" else normalize_tag(version)
|
||||
|
||||
click.echo(f"Generating changelog for v{version}...")
|
||||
click.echo(f" Comparing: {base_tag} → {head}")
|
||||
|
||||
body = generate_release_notes(head_tag, base_tag)
|
||||
date = datetime.now().strftime("%B %d, %Y")
|
||||
content = format_changelog(version, body, date=date, title=title)
|
||||
|
||||
if dry_run:
|
||||
click.echo()
|
||||
click.secho("[DRY RUN]", fg="yellow", bold=True)
|
||||
click.echo(f" Would save: {CHANGELOG_DIR / f'{version}-changelog.md'}")
|
||||
if also_update_citation:
|
||||
click.echo(f" Would update: {CITATION_FILE}")
|
||||
click.echo()
|
||||
click.echo("--- Generated changelog ---")
|
||||
click.echo(content)
|
||||
return
|
||||
|
||||
filepath = save_changelog(version, content)
|
||||
click.secho(f" ✓ Saved: {filepath}", fg="green")
|
||||
|
||||
if also_update_citation:
|
||||
citation_path = update_citation(version)
|
||||
click.secho(f" ✓ Updated: {citation_path}", fg="green")
|
||||
|
||||
click.echo()
|
||||
click.echo("Next steps:")
|
||||
click.echo(" • Review and edit the changelog as needed")
|
||||
click.echo(" • Update docs/source/changelog.rst to include the new file")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option(
|
||||
"--version", "version", required=True, help="Version number (e.g., 0.20.0)"
|
||||
)
|
||||
@click.option("--date", help="Release date as YYYY-MM-DD (default: today)")
|
||||
@click.pass_context
|
||||
def citation(ctx: click.Context, version: str, date: str | None) -> None:
|
||||
"""Update CITATION.cff for a release."""
|
||||
dry_run = ctx.obj["dry_run"]
|
||||
display_date = date or datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
if dry_run:
|
||||
click.secho("[DRY RUN]", fg="yellow", bold=True)
|
||||
click.echo(f" Would update: {CITATION_FILE}")
|
||||
click.echo(f" Version: v{version}")
|
||||
click.echo(f" Date: {display_date}")
|
||||
return
|
||||
|
||||
filepath = update_citation(version, date)
|
||||
click.secho(f"✓ Updated: {filepath}", fg="green")
|
||||
click.echo(f" Version: v{version}")
|
||||
click.echo(f" Date: {display_date}")
|
||||
|
||||
|
||||
@cli.command("fetch-releases")
|
||||
@click.option("--tag", help="Fetch a specific release tag")
|
||||
@click.option(
|
||||
"--min-version",
|
||||
default=DEFAULT_MIN_VERSION,
|
||||
help=f"Minimum version to fetch (default: {DEFAULT_MIN_VERSION})",
|
||||
)
|
||||
@click.option("--force", is_flag=True, help="Overwrite existing changelog files")
|
||||
@click.pass_context
|
||||
def fetch_releases(
|
||||
ctx: click.Context,
|
||||
tag: str | None,
|
||||
min_version: str,
|
||||
force: bool,
|
||||
) -> None:
|
||||
"""Fetch existing release changelogs from GitHub.
|
||||
|
||||
Converts GitHub release notes to documentation-ready MyST markdown.
|
||||
"""
|
||||
dry_run = ctx.obj["dry_run"]
|
||||
existing = get_existing_versions()
|
||||
|
||||
# Single tag mode
|
||||
if tag:
|
||||
tag = normalize_tag(tag)
|
||||
version = version_from_tag(tag)
|
||||
|
||||
if version in existing and not force:
|
||||
click.echo(
|
||||
f"Changelog for {version} already exists. Use --force to overwrite."
|
||||
)
|
||||
return
|
||||
|
||||
if dry_run:
|
||||
click.secho("[DRY RUN]", fg="yellow", bold=True)
|
||||
click.echo(f" Would fetch: {version}")
|
||||
return
|
||||
|
||||
_fetch_single_release(tag, version)
|
||||
return
|
||||
|
||||
# Batch mode
|
||||
click.echo(f"Existing versions: {', '.join(sorted(existing)) or '(none)'}")
|
||||
click.echo("Fetching release list...")
|
||||
|
||||
tags = get_release_tags()
|
||||
click.echo(f"Found {len(tags)} releases")
|
||||
|
||||
fetched = 0
|
||||
prev_tag = None
|
||||
|
||||
for tag in reversed(tags):
|
||||
version = version_from_tag(tag)
|
||||
|
||||
if not version_gte(version, min_version):
|
||||
prev_tag = tag
|
||||
continue
|
||||
|
||||
if version in existing and not force:
|
||||
click.echo(f" Skipping {version} (exists)")
|
||||
prev_tag = tag
|
||||
continue
|
||||
|
||||
if dry_run:
|
||||
click.echo(f" [DRY RUN] Would fetch {version}")
|
||||
fetched += 1
|
||||
else:
|
||||
if _fetch_single_release(tag, version, prev_tag):
|
||||
existing.add(version)
|
||||
fetched += 1
|
||||
|
||||
prev_tag = tag
|
||||
|
||||
click.echo()
|
||||
click.echo(f"Processed {fetched} changelog(s)")
|
||||
|
||||
if fetched > 0 and not dry_run:
|
||||
click.echo()
|
||||
click.echo("Next steps:")
|
||||
click.echo(" • Update docs/source/changelog.rst to include new files")
|
||||
|
||||
|
||||
def _fetch_single_release(tag: str, version: str, prev_tag: str | None = None) -> bool:
|
||||
"""Fetch and save a single release changelog."""
|
||||
click.echo(f" Fetching {version}...")
|
||||
|
||||
body = get_release_body(tag)
|
||||
|
||||
if not body and prev_tag:
|
||||
click.echo(f" No body, generating from {prev_tag}...")
|
||||
try:
|
||||
body = generate_release_notes(tag, prev_tag)
|
||||
except click.ClickException:
|
||||
body = None
|
||||
|
||||
if not body:
|
||||
click.secho(f" ✗ Could not get release notes for {tag}", fg="red", err=True)
|
||||
return False
|
||||
|
||||
date = get_release_date(tag)
|
||||
content = format_changelog(version, body, date=date)
|
||||
|
||||
filepath = save_changelog(version, content)
|
||||
click.secho(f" ✓ Saved: {filepath}", fg="green")
|
||||
return True
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Entry Point
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Entry point."""
|
||||
cli()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main() or 0)
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
{"levelname": "INFO", "module": "logger_utils", "message": "Log file will be saved in <>"}
|
||||
{"levelname": "INFO", "module": "tex_file_writing", "message": "Writing <> to <>"}
|
||||
{"levelname": "ERROR", "module": "tex_file_writing", "message": "LaTeX compilation error: LaTeX Error: File `notapackage.sty' not found.\n"}
|
||||
{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\\begin{center}\n\\frac{1}{0}\n"}
|
||||
{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\\begin{center}\n\\special{dvisvgm:raw <g id='unique000'>}\\frac{1}{0}\\special{dvisvgm:raw </g>}\n"}
|
||||
{"levelname": "INFO", "module": "tex_file_writing", "message": "You do not have package notapackage.sty installed."}
|
||||
{"levelname": "INFO", "module": "tex_file_writing", "message": "Install notapackage.sty it using your LaTeX package manager, or check for typos."}
|
||||
{"levelname": "ERROR", "module": "tex_file_writing", "message": "LaTeX compilation error: Emergency stop.\n"}
|
||||
{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\\begin{center}\n\\frac{1}{0}\n"}
|
||||
{"levelname": "ERROR", "module": "tex_file_writing", "message": "Context of error: \n\\documentclass[preview]{standalone}\n-> \\usepackage{notapackage}\n\\begin{document}\n\\begin{center}\n\\special{dvisvgm:raw <g id='unique000'>}\\frac{1}{0}\\special{dvisvgm:raw </g>}\n"}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ def set_test_scene(scene_object: type[Scene], module_name: str, config):
|
|||
tests_directory = Path(__file__).absolute().parent.parent
|
||||
path_control_data = Path(tests_directory) / "control_data" / "graphical_units_data"
|
||||
path = Path(path_control_data) / module_name
|
||||
if not path.is_dir():
|
||||
path.mkdir(parents=True)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
np.savez_compressed(path / str(scene), frame_data=data)
|
||||
logger.info(f"Test data for {str(scene)} saved in {path}\n")
|
||||
|
|
|
|||
67
tests/module/animation/test_updaters.py
Normal file
67
tests/module/animation/test_updaters.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from manim import UP, Circle, Dot, FadeIn
|
||||
from manim.animation.updaters.mobject_update_utils import turn_animation_into_updater
|
||||
|
||||
|
||||
def test_turn_animation_into_updater_zero_run_time():
|
||||
"""Test that turn_animation_into_updater handles zero run_time correctly."""
|
||||
# Create a simple mobject and animation
|
||||
mobject = Circle()
|
||||
animation = FadeIn(mobject, run_time=0)
|
||||
|
||||
# Track updater calls
|
||||
update_calls = []
|
||||
original_updaters = mobject.updaters.copy()
|
||||
|
||||
# Call turn_animation_into_updater
|
||||
result = turn_animation_into_updater(animation)
|
||||
|
||||
# Verify mobject is returned
|
||||
assert result is mobject
|
||||
|
||||
# Get the updater that was added
|
||||
assert len(mobject.updaters) == len(original_updaters) + 1
|
||||
updater = mobject.updaters[-1]
|
||||
|
||||
# Simulate calling the updater
|
||||
updater(mobject, dt=0.1)
|
||||
|
||||
# The updater should have finished and removed itself
|
||||
assert len(mobject.updaters) == len(original_updaters)
|
||||
assert updater not in mobject.updaters
|
||||
|
||||
# Animation should be in finished state
|
||||
assert animation.total_time >= 0
|
||||
|
||||
|
||||
def test_turn_animation_into_updater_positive_run_time_persists():
|
||||
"""Test that updater persists with positive run_time."""
|
||||
mobject = Circle()
|
||||
animation = FadeIn(mobject, run_time=1.0)
|
||||
|
||||
original_updaters = mobject.updaters.copy()
|
||||
|
||||
# Call turn_animation_into_updater
|
||||
result = turn_animation_into_updater(animation)
|
||||
|
||||
# Get the updater that was added
|
||||
updater = mobject.updaters[-1]
|
||||
|
||||
# Simulate calling the updater (partial progress)
|
||||
updater(mobject, dt=0.1)
|
||||
|
||||
# The updater should still be present (not finished)
|
||||
assert len(mobject.updaters) == len(original_updaters) + 1
|
||||
assert updater in mobject.updaters
|
||||
|
||||
|
||||
def test_always():
|
||||
d = Dot()
|
||||
circ = Circle()
|
||||
d.always.next_to(circ, UP)
|
||||
assert len(d.updaters) == 1
|
||||
# we should be able to chain updaters
|
||||
d2 = Dot()
|
||||
d.always.next_to(d2, UP).next_to(circ, UP)
|
||||
assert len(d.updaters) == 3
|
||||
|
|
@ -7,6 +7,7 @@ import numpy as np
|
|||
from manim import (
|
||||
DEGREES,
|
||||
DOWN,
|
||||
GREEN,
|
||||
LEFT,
|
||||
ORIGIN,
|
||||
RIGHT,
|
||||
|
|
@ -89,7 +90,7 @@ def test_Polygram_get_vertex_groups():
|
|||
for vertex_groups in vertex_groups_arr:
|
||||
polygram = Polygram(*vertex_groups)
|
||||
poly_vertex_groups = polygram.get_vertex_groups()
|
||||
for poly_group, group in zip(poly_vertex_groups, vertex_groups, strict=False):
|
||||
for poly_group, group in zip(poly_vertex_groups, vertex_groups, strict=True):
|
||||
np.testing.assert_array_equal(poly_group, group)
|
||||
|
||||
# If polygram is a Polygram of a vertex group containing the start vertex N times,
|
||||
|
|
@ -152,6 +153,18 @@ def test_BackgroundRectangle(manim_caplog):
|
|||
)
|
||||
|
||||
|
||||
def test_BackgroundRectangle_color_access():
|
||||
"""Test that BackgroundRectangle color access works correctly.
|
||||
|
||||
Regression test for https://github.com/ManimCommunity/manim/issues/4419
|
||||
"""
|
||||
square = Square()
|
||||
bg_rect = BackgroundRectangle(square, color=GREEN)
|
||||
|
||||
# Should not cause infinite recursion
|
||||
assert bg_rect.color == GREEN
|
||||
|
||||
|
||||
def test_Square_side_length_reflets_correct_width_and_height():
|
||||
sq = Square(side_length=1).scale(3)
|
||||
assert sq.side_length == 3
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ def test_add_labels():
|
|||
expected_label_length = 6
|
||||
num_line = NumberLine(x_range=[-4, 4])
|
||||
num_line.add_labels(
|
||||
dict(zip(list(range(-3, 3)), [Integer(m) for m in range(-1, 5)], strict=False)),
|
||||
dict(zip(list(range(-3, 3)), [Integer(m) for m in range(-1, 5)], strict=True)),
|
||||
)
|
||||
actual_label_length = len(num_line.labels)
|
||||
assert actual_label_length == expected_label_length, (
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ def test_bracelabel_copy(tmp_path, config):
|
|||
config["text_dir"] = str(mediadir.joinpath("Text"))
|
||||
config["tex_dir"] = str(mediadir.joinpath("Tex"))
|
||||
for el in ["text_dir", "tex_dir"]:
|
||||
Path(config[el]).mkdir(parents=True)
|
||||
Path(config[el]).mkdir(parents=True, exist_ok=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
|
||||
|
|
|
|||
19
tests/module/mobject/test_table.py
Normal file
19
tests/module/mobject/test_table.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
"""Tests for Table and related mobjects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from manim import Table
|
||||
from manim.utils.color import GREEN
|
||||
|
||||
|
||||
def test_highlighted_cell_color_access():
|
||||
"""Test that accessing the color of a highlighted cell doesn't cause infinite recursion.
|
||||
|
||||
Regression test for https://github.com/ManimCommunity/manim/issues/4419
|
||||
"""
|
||||
table = Table([["This", "is a"], ["simple", "table"]])
|
||||
rect = table.get_highlighted_cell((1, 1), color=GREEN)
|
||||
|
||||
# Should not raise RecursionError
|
||||
color = rect.color
|
||||
assert color == GREEN
|
||||
|
|
@ -10,7 +10,7 @@ from manim import MathTex, SingleStringMathTex, Tex, TexTemplate, tempconfig
|
|||
|
||||
def test_MathTex(config):
|
||||
MathTex("a^2 + b^2 = c^2")
|
||||
assert Path(config.media_dir, "Tex", "e4be163a00cf424f.svg").exists()
|
||||
assert Path(config.media_dir, "Tex", "05bb0a41ed575f00.svg").exists()
|
||||
|
||||
|
||||
def test_SingleStringMathTex(config):
|
||||
|
|
@ -20,16 +20,125 @@ def test_SingleStringMathTex(config):
|
|||
|
||||
@pytest.mark.parametrize( # : PT006
|
||||
("text_input", "length_sub"),
|
||||
[("{{ a }} + {{ b }} = {{ c }}", 5), (r"\frac{1}{a+b\sqrt{2}}", 1)],
|
||||
[
|
||||
("{{ a }} + {{ b }} = {{ c }}", 5),
|
||||
(r"\frac{1}{a+b\sqrt{2}}", 1),
|
||||
# Regression test for https://github.com/ManimCommunity/manim/issues/4601:
|
||||
# a string whose only }} comes from closing two nested LaTeX brace groups
|
||||
# (not from the {{ }} notation) must not be split.
|
||||
(r"\\+\int_{0}^{\frac{Mq}{M+m}}", 1),
|
||||
],
|
||||
)
|
||||
def test_double_braces_testing(text_input, length_sub):
|
||||
t1 = MathTex(text_input)
|
||||
assert len(t1.submobjects) == length_sub
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Unit tests for MathTex._split_double_braces — no LaTeX compilation needed.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("tex_string", "expected_segments"),
|
||||
[
|
||||
# ---- intended notation ----
|
||||
# Basic split: each {{ }} group and the text between become segments.
|
||||
(
|
||||
"{{ a }} + {{ b }}",
|
||||
["", " a ", " + ", " b ", ""],
|
||||
),
|
||||
# {{ }} at the very start of the string (no preceding character).
|
||||
(
|
||||
"{{x}}",
|
||||
["", "x", ""],
|
||||
),
|
||||
# Content with arbitrarily nested LaTeX braces: inner }} must NOT close
|
||||
# the Manim group early.
|
||||
(
|
||||
r"{{ a^{b^{c}} }}",
|
||||
["", r" a^{b^{c}} ", ""],
|
||||
),
|
||||
# \frac inside a Manim group — the }} from {1}{a} are at inner_depth > 0.
|
||||
(
|
||||
r"{{ \frac{1}{a} }}",
|
||||
["", r" \frac{1}{a} ", ""],
|
||||
),
|
||||
# ---- false-positive guards: {{ not preceded by whitespace ----
|
||||
# \text{{word}}: {{ preceded by {, must not split.
|
||||
(
|
||||
r"\text{{word}}",
|
||||
[r"\text{{word}}"],
|
||||
),
|
||||
# ^{{\alpha}}: {{ preceded by {, must not split.
|
||||
(
|
||||
r"^{{\alpha}}",
|
||||
[r"^{{\alpha}}"],
|
||||
),
|
||||
# +{{a}}: {{ preceded by non-whitespace, must not split.
|
||||
(
|
||||
r"+{{a}}",
|
||||
[r"+{{a}}"],
|
||||
),
|
||||
# ---- bug case: }} without any {{ must not split ----
|
||||
(
|
||||
r"\\+\int_{0}^{\frac{Mq}{M+m}}",
|
||||
[r"\\+\int_{0}^{\frac{Mq}{M+m}}"],
|
||||
),
|
||||
# ---- backslash escape handling ----
|
||||
# \}} — \} consumed as unit, remaining } is a lone close, not }}.
|
||||
(
|
||||
r"\}}",
|
||||
[r"\}}"],
|
||||
),
|
||||
# \\}} — \\ consumed as unit, leaving real }} which is not inside any
|
||||
# Manim group so it passes through unchanged.
|
||||
(
|
||||
r"\\}}",
|
||||
[r"\\}}"],
|
||||
),
|
||||
# \\\}} — \\ consumed, then \} consumed; lone } passes through.
|
||||
(
|
||||
r"\\\}}",
|
||||
[r"\\\}}"],
|
||||
),
|
||||
# \\\\}} — two \\ consumed; lone }} passes through (no Manim group open).
|
||||
(
|
||||
r"\\\\}}",
|
||||
[r"\\\\}}"],
|
||||
),
|
||||
# Same backslash cases *inside* a Manim group.
|
||||
# The escape sequence is placed right before the Manim }} close.
|
||||
#
|
||||
# {{ a \}}} — \} consumed as escaped brace (content), }} closes the group.
|
||||
(
|
||||
r"{{ a \}}}",
|
||||
["", r" a \}", ""],
|
||||
),
|
||||
# {{ a \\}} — \\ consumed as escaped backslash (content), }} closes.
|
||||
(
|
||||
r"{{ a \\}}",
|
||||
["", r" a \\", ""],
|
||||
),
|
||||
# {{ a \\\}}} — \\ then \} consumed (content), }} closes.
|
||||
(
|
||||
r"{{ a \\\}}}",
|
||||
["", r" a \\\}", ""],
|
||||
),
|
||||
# {{ a \\\\}} — \\ then \\ consumed (content), }} closes.
|
||||
(
|
||||
r"{{ a \\\\}}",
|
||||
["", r" a \\\\", ""],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_split_double_braces(tex_string, expected_segments):
|
||||
assert MathTex._split_double_braces(tex_string) == expected_segments
|
||||
|
||||
|
||||
def test_tex(config):
|
||||
Tex("The horse does not eat cucumber salad.")
|
||||
assert Path(config.media_dir, "Tex", "c3945e23e546c95a.svg").exists()
|
||||
assert Path(config.media_dir, "Tex", "5384b41741a246bd.svg").exists()
|
||||
|
||||
|
||||
def test_tex_temp_directory(tmpdir, monkeypatch):
|
||||
|
|
@ -38,16 +147,16 @@ def test_tex_temp_directory(tmpdir, monkeypatch):
|
|||
# tempconfig to change media directory to temporary directory by default
|
||||
# we partially, revert that change here.
|
||||
monkeypatch.chdir(tmpdir)
|
||||
Path(tmpdir, "media").mkdir()
|
||||
Path(tmpdir, "media").mkdir(exist_ok=True)
|
||||
with tempconfig({"media_dir": "media"}):
|
||||
Tex("The horse does not eat cucumber salad.")
|
||||
assert Path("media", "Tex").exists()
|
||||
assert Path("media", "Tex", "c3945e23e546c95a.svg").exists()
|
||||
assert Path("media", "Tex", "5384b41741a246bd.svg").exists()
|
||||
|
||||
|
||||
def test_percent_char_rendering(config):
|
||||
Tex(r"\%")
|
||||
assert Path(config.media_dir, "Tex", "4a583af4d19a3adf.tex").exists()
|
||||
assert Path(config.media_dir, "Tex", "32509dd0ea993961.tex").exists()
|
||||
|
||||
|
||||
def test_tex_whitespace_arg():
|
||||
|
|
@ -108,7 +217,7 @@ def test_multi_part_tex_with_empty_parts():
|
|||
for one_part_glyph, multi_part_glyph in zip(
|
||||
one_part_fomula.family_members_with_points(),
|
||||
multi_part_formula.family_members_with_points(),
|
||||
strict=False,
|
||||
strict=True,
|
||||
):
|
||||
np.testing.assert_allclose(one_part_glyph.points, multi_part_glyph.points)
|
||||
|
||||
|
|
@ -215,14 +324,14 @@ def test_tempconfig_resetting_tex_template(config):
|
|||
|
||||
def test_tex_garbage_collection(tmpdir, monkeypatch, config):
|
||||
monkeypatch.chdir(tmpdir)
|
||||
Path(tmpdir, "media").mkdir()
|
||||
Path(tmpdir, "media").mkdir(exist_ok=True)
|
||||
config.media_dir = "media"
|
||||
|
||||
tex_without_log = Tex("Hello World!") # d771330b76d29ffb.tex
|
||||
assert Path("media", "Tex", "d771330b76d29ffb.tex").exists()
|
||||
assert not Path("media", "Tex", "d771330b76d29ffb.log").exists()
|
||||
tex_without_log = Tex("Hello World!") # 058a4e242c57db6d.tex
|
||||
assert Path("media", "Tex", "058a4e242c57db6d.tex").exists()
|
||||
assert not Path("media", "Tex", "058a4e242c57db6d.log").exists()
|
||||
|
||||
config.no_latex_cleanup = True
|
||||
|
||||
tex_with_log = Tex("Hello World, again!") # da27670a37b08799.tex
|
||||
assert Path("media", "Tex", "da27670a37b08799.log").exists()
|
||||
tex_with_log = Tex("Hello World, again!") # 45b4e7819cc20cb1.tex
|
||||
assert Path("media", "Tex", "45b4e7819cc20cb1.log").exists()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from manim import ORIGIN, UR, Arrow, DashedVMobject, VGroup
|
||||
from manim import ORIGIN, UR, Arrow, DashedLine, DashedVMobject, VGroup
|
||||
from manim.mobject.geometry.tips import ArrowTip, StealthTip
|
||||
|
||||
|
||||
|
|
@ -41,3 +41,19 @@ def test_dashed_arrow_with_start_tip_has_two_tips():
|
|||
tips = _collect_tips(dashed)
|
||||
|
||||
assert len(tips) == 2
|
||||
|
||||
|
||||
def test_zero_length_dashed_line_submobjects_have_2d_points():
|
||||
"""Submobjects of a zero-length DashedLine must have 2-D point arrays."""
|
||||
line = DashedLine(ORIGIN, ORIGIN)
|
||||
for sub in line.submobjects:
|
||||
assert sub.points.ndim == 2, (
|
||||
f"Expected 2-D points array, got shape {sub.points.shape}"
|
||||
)
|
||||
|
||||
|
||||
def test_become_nonzero_to_zero_dashed_line_does_not_crash():
|
||||
"""become() from a normal DashedLine to a zero-length one should not crash."""
|
||||
normal = DashedLine(ORIGIN, 2 * UR)
|
||||
zero = DashedLine(ORIGIN, ORIGIN)
|
||||
normal.become(zero)
|
||||
|
|
|
|||
|
|
@ -369,7 +369,7 @@ def test_vdict_init():
|
|||
# Test VDict made from a python dict
|
||||
VDict({"a": VMobject(), "b": VMobject(), "c": VMobject()})
|
||||
# Test VDict made using zip
|
||||
VDict(zip(["a", "b", "c"], [VMobject(), VMobject(), VMobject()], strict=False))
|
||||
VDict(zip(["a", "b", "c"], [VMobject(), VMobject(), VMobject()], strict=True))
|
||||
# If the value is of type Mobject, must raise a TypeError
|
||||
with pytest.raises(TypeError):
|
||||
VDict({"a": Mobject()})
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from manim import (
|
|||
tempconfig,
|
||||
)
|
||||
from manim import CoordinateSystem as CS
|
||||
from manim.utils.color import BLUE, GREEN, ORANGE, RED, YELLOW
|
||||
from manim.utils.color import BLUE, GREEN, ORANGE, PURE_YELLOW, RED
|
||||
from manim.utils.testing.frames_comparison import frames_comparison
|
||||
|
||||
__module_test__ = "coordinate_system_opengl"
|
||||
|
|
@ -152,7 +152,7 @@ def test_gradient_line_graph_x_axis(scene, using_opengl_renderer):
|
|||
curve = axes.plot(
|
||||
lambda x: 0.1 * x**3,
|
||||
x_range=(-3, 3, 0.001),
|
||||
colorscale=[BLUE, GREEN, YELLOW, ORANGE, RED],
|
||||
colorscale=[BLUE, GREEN, PURE_YELLOW, ORANGE, RED],
|
||||
colorscale_axis=0,
|
||||
)
|
||||
|
||||
|
|
@ -167,7 +167,7 @@ def test_gradient_line_graph_y_axis(scene, using_opengl_renderer):
|
|||
curve = axes.plot(
|
||||
lambda x: 0.1 * x**3,
|
||||
x_range=(-3, 3, 0.001),
|
||||
colorscale=[BLUE, GREEN, YELLOW, ORANGE, RED],
|
||||
colorscale=[BLUE, GREEN, PURE_YELLOW, ORANGE, RED],
|
||||
colorscale_axis=1,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ def test_bracelabel_copy(config, using_opengl_renderer, tmp_path):
|
|||
config["text_dir"] = str(mediadir.joinpath("Text"))
|
||||
config["tex_dir"] = str(mediadir.joinpath("Tex"))
|
||||
for el in ["text_dir", "tex_dir"]:
|
||||
Path(config[el]).mkdir(parents=True)
|
||||
Path(config[el]).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Before the refactoring of OpenGLMobject.copy(), the class BraceLabel was the
|
||||
# only one to have a non-trivial definition of copy. Here we test that it
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ def test_add_labels():
|
|||
expected_label_length = 6
|
||||
num_line = NumberLine(x_range=[-4, 4])
|
||||
num_line.add_labels(
|
||||
dict(zip(list(range(-3, 3)), [Integer(m) for m in range(-1, 5)], strict=False)),
|
||||
dict(zip(list(range(-3, 3)), [Integer(m) for m in range(-1, 5)], strict=True)),
|
||||
)
|
||||
actual_label_length = len(num_line.labels)
|
||||
assert actual_label_length == expected_label_length, (
|
||||
|
|
|
|||
|
|
@ -312,7 +312,7 @@ def test_vdict_init(using_opengl_renderer):
|
|||
zip(
|
||||
["a", "b", "c"],
|
||||
[OpenGLVMobject(), OpenGLVMobject(), OpenGLVMobject()],
|
||||
strict=False,
|
||||
strict=True,
|
||||
)
|
||||
)
|
||||
# If the value is of type OpenGLMobject, must raise a TypeError
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from manim import MathTex, SingleStringMathTex, Tex
|
|||
|
||||
def test_MathTex(config, using_opengl_renderer):
|
||||
MathTex("a^2 + b^2 = c^2")
|
||||
assert Path(config.media_dir, "Tex", "e4be163a00cf424f.svg").exists()
|
||||
assert Path(config.media_dir, "Tex", "05bb0a41ed575f00.svg").exists()
|
||||
|
||||
|
||||
def test_SingleStringMathTex(config, using_opengl_renderer):
|
||||
|
|
@ -28,7 +28,7 @@ def test_double_braces_testing(using_opengl_renderer, text_input, length_sub):
|
|||
|
||||
def test_tex(config, using_opengl_renderer):
|
||||
Tex("The horse does not eat cucumber salad.")
|
||||
assert Path(config.media_dir, "Tex", "c3945e23e546c95a.svg").exists()
|
||||
assert Path(config.media_dir, "Tex", "5384b41741a246bd.svg").exists()
|
||||
|
||||
|
||||
def test_tex_whitespace_arg(using_opengl_renderer):
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -32,7 +32,7 @@ def test_custom_coordinates(scene):
|
|||
ax = Axes(x_range=[0, 10])
|
||||
|
||||
ax.add_coordinates(
|
||||
dict(zip(list(range(1, 10)), [Tex("str") for _ in range(1, 10)], strict=False)),
|
||||
dict(zip(list(range(1, 10)), [Tex("str") for _ in range(1, 10)], strict=True)),
|
||||
)
|
||||
scene.add(ax)
|
||||
|
||||
|
|
@ -226,7 +226,7 @@ def test_get_area(scene):
|
|||
area2 = ax.get_area(
|
||||
curve1,
|
||||
x_range=(-4.5, -2),
|
||||
color=(RED, YELLOW),
|
||||
color=(RED, PURE_YELLOW),
|
||||
opacity=0.2,
|
||||
bounded_graph=curve2,
|
||||
)
|
||||
|
|
@ -266,7 +266,7 @@ def test_get_riemann_rectangles(scene, use_vectorized):
|
|||
quadratic,
|
||||
x_range=[-1.5, 1.5],
|
||||
dx=0.15,
|
||||
color=YELLOW,
|
||||
color=PURE_YELLOW,
|
||||
)
|
||||
|
||||
bounding_line = ax.plot(lambda x: 1.5 * x, color=BLUE_B, x_range=[3.3, 6])
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ def test_line_graph(scene):
|
|||
first_line = plane.plot_line_graph(
|
||||
x_values=[-3, 1],
|
||||
y_values=[-2, 2],
|
||||
line_color=YELLOW,
|
||||
line_color=PURE_YELLOW,
|
||||
)
|
||||
second_line = plane.plot_line_graph(
|
||||
x_values=[0, 2, 2, 4],
|
||||
|
|
@ -71,7 +71,7 @@ def test_plot_surface_colorscale(scene):
|
|||
param_trig,
|
||||
u_range=(-3, 3),
|
||||
v_range=(-3, 3),
|
||||
colorscale=[BLUE, GREEN, YELLOW, ORANGE, RED],
|
||||
colorscale=[BLUE, GREEN, PURE_YELLOW, ORANGE, RED],
|
||||
)
|
||||
|
||||
scene.add(axes, trig_plane)
|
||||
|
|
@ -151,7 +151,7 @@ def test_gradient_line_graph_x_axis(scene):
|
|||
curve = axes.plot(
|
||||
lambda x: 0.1 * x**3,
|
||||
x_range=(-3, 3, 0.001),
|
||||
colorscale=[BLUE, GREEN, YELLOW, ORANGE, RED],
|
||||
colorscale=[BLUE, GREEN, PURE_YELLOW, ORANGE, RED],
|
||||
colorscale_axis=0,
|
||||
)
|
||||
|
||||
|
|
@ -166,7 +166,7 @@ def test_gradient_line_graph_y_axis(scene):
|
|||
curve = axes.plot(
|
||||
lambda x: 0.1 * x**3,
|
||||
x_range=(-3, 3, 0.001),
|
||||
colorscale=[BLUE, GREEN, YELLOW, ORANGE, RED],
|
||||
colorscale=[BLUE, GREEN, PURE_YELLOW, ORANGE, RED],
|
||||
colorscale_axis=1,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -174,6 +174,27 @@ def test_ZIndex(scene):
|
|||
scene.play(ApplyMethod(triangle.shift, 2 * UP))
|
||||
|
||||
|
||||
@frames_comparison(last_frame=False)
|
||||
def test_negative_z_index_AnimationGroup(scene):
|
||||
# https://github.com/ManimCommunity/manim/issues/3334
|
||||
s = Square().set_z_index(-1)
|
||||
scene.play(AnimationGroup(GrowFromCenter(s)))
|
||||
|
||||
|
||||
@frames_comparison(last_frame=False)
|
||||
def test_negative_z_index_LaggedStart(scene):
|
||||
# https://github.com/ManimCommunity/manim/issues/3914
|
||||
line_1 = Line(LEFT, RIGHT, color=BLUE)
|
||||
line_2 = Line(UP + LEFT, UP + RIGHT, color=RED).set_z_index(-1)
|
||||
scene.play(LaggedStart(FadeIn(line_1), FadeIn(line_2), lag_ratio=0.5))
|
||||
|
||||
|
||||
@frames_comparison(last_frame=False)
|
||||
def test_nested_animation_groups_with_negative_z_index(scene):
|
||||
line = Line(LEFT, RIGHT, color=BLUE).set_z_index(-1)
|
||||
scene.play(AnimationGroup(AnimationGroup(AnimationGroup(FadeIn(line)))))
|
||||
|
||||
|
||||
@frames_comparison
|
||||
def test_Angle(scene):
|
||||
l1 = Line(ORIGIN, RIGHT)
|
||||
|
|
|
|||
|
|
@ -268,21 +268,16 @@ def test_ImageInterpolation(scene):
|
|||
img = ImageMobject(
|
||||
np.uint8([[63, 0, 0, 0], [0, 127, 0, 0], [0, 0, 191, 0], [0, 0, 0, 255]]),
|
||||
)
|
||||
img.height = 2
|
||||
img1 = img.copy()
|
||||
img2 = img.copy()
|
||||
img3 = img.copy()
|
||||
img4 = img.copy()
|
||||
img5 = img.copy()
|
||||
img.height = 3
|
||||
|
||||
img1.set_resampling_algorithm(RESAMPLING_ALGORITHMS["nearest"])
|
||||
img2.set_resampling_algorithm(RESAMPLING_ALGORITHMS["lanczos"])
|
||||
img3.set_resampling_algorithm(RESAMPLING_ALGORITHMS["linear"])
|
||||
img4.set_resampling_algorithm(RESAMPLING_ALGORITHMS["cubic"])
|
||||
img5.set_resampling_algorithm(RESAMPLING_ALGORITHMS["box"])
|
||||
algorithm_texts = ["nearest", "linear", "cubic"]
|
||||
for i, algorithm_text in enumerate(algorithm_texts):
|
||||
algorithm = RESAMPLING_ALGORITHMS[algorithm_text]
|
||||
img_copy = img.copy().set_resampling_algorithm(algorithm)
|
||||
position = img.height * (i - (len(algorithm_texts) - 1) / 2) * RIGHT
|
||||
img_copy.move_to(position)
|
||||
scene.add(img_copy)
|
||||
|
||||
scene.add(img1, img2, img3, img4, img5)
|
||||
[s.shift(4 * LEFT + pos * 2 * RIGHT) for pos, s in enumerate(scene.mobjects)]
|
||||
scene.wait()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ def test_become(scene):
|
|||
.set_opacity(0.25)
|
||||
.set_color(GREEN)
|
||||
)
|
||||
s3 = s.copy().become(d, stretch=True).set_opacity(0.25).set_color(YELLOW)
|
||||
s3 = s.copy().become(d, stretch=True).set_opacity(0.25).set_color(PURE_YELLOW)
|
||||
|
||||
scene.add(s, d, s1, s2, s3)
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ def test_become_no_color_linking(scene):
|
|||
scene.add(b)
|
||||
b.become(a)
|
||||
b.shift(1 * RIGHT)
|
||||
b.set_stroke(YELLOW, opacity=1)
|
||||
b.set_stroke(PURE_YELLOW, opacity=1)
|
||||
|
||||
|
||||
@frames_comparison
|
||||
|
|
@ -59,7 +59,7 @@ def test_vmobject_joint_types(scene):
|
|||
]
|
||||
)
|
||||
lines = VGroup(*[angled_line.copy() for _ in range(len(LineJointType))])
|
||||
for line, joint_type in zip(lines, LineJointType, strict=False):
|
||||
for line, joint_type in zip(lines, LineJointType, strict=True):
|
||||
line.joint_type = joint_type
|
||||
|
||||
lines.arrange(RIGHT, buff=1)
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ __module_test__ = "modifier_methods"
|
|||
|
||||
@frames_comparison
|
||||
def test_Gradient(scene):
|
||||
c = Circle(fill_opacity=1).set_color(color=[YELLOW, GREEN])
|
||||
c = Circle(fill_opacity=1).set_color(color=[PURE_YELLOW, GREEN])
|
||||
scene.add(c)
|
||||
|
||||
|
||||
@frames_comparison
|
||||
def test_GradientRotation(scene):
|
||||
c = Circle(fill_opacity=1).set_color(color=[YELLOW, GREEN]).rotate(PI)
|
||||
c = Circle(fill_opacity=1).set_color(color=[PURE_YELLOW, GREEN]).rotate(PI)
|
||||
scene.add(c)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from manim.constants import LEFT
|
||||
from manim.mobject.graphing.probability import BarChart
|
||||
from manim.mobject.text.tex_mobject import MathTex
|
||||
from manim.utils.color import BLUE, GREEN, RED, WHITE, YELLOW
|
||||
from manim.utils.color import BLUE, GREEN, PURE_YELLOW, RED, WHITE
|
||||
from manim.utils.testing.frames_comparison import frames_comparison
|
||||
|
||||
__module_test__ = "probability"
|
||||
|
|
@ -69,13 +69,13 @@ def test_advanced_customization(scene):
|
|||
chart = BarChart(values=[10, 40, 10, 20], bar_names=["one", "two", "three", "four"])
|
||||
|
||||
c_x_lbls = chart.x_axis.labels
|
||||
c_x_lbls.set_color_by_gradient(GREEN, RED, YELLOW)
|
||||
c_x_lbls.set_color_by_gradient(GREEN, RED, PURE_YELLOW)
|
||||
|
||||
c_y_nums = chart.y_axis.numbers
|
||||
c_y_nums.set_color_by_gradient(BLUE, WHITE).shift(LEFT)
|
||||
|
||||
c_y_axis = chart.y_axis
|
||||
c_y_axis.ticks.set_color(YELLOW)
|
||||
c_y_axis.ticks.set_color(PURE_YELLOW)
|
||||
|
||||
c_bar_lbls = chart.get_bar_labels()
|
||||
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ def test_SurfaceColorscale(scene):
|
|||
u_range=[-3, 3],
|
||||
)
|
||||
trig_plane.set_fill_by_value(
|
||||
axes=axes, colorscale=[BLUE, GREEN, YELLOW, ORANGE, RED]
|
||||
axes=axes, colorscale=[BLUE, GREEN, PURE_YELLOW, ORANGE, RED]
|
||||
)
|
||||
scene.add(axes, trig_plane)
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ def test_Y_Direction(scene):
|
|||
)
|
||||
surface_plane.set_style(fill_opacity=1)
|
||||
surface_plane.set_fill_by_value(
|
||||
axes=axes, colorscale=[(RED, -0.4), (YELLOW, 0), (GREEN, 0.4)], axis=1
|
||||
axes=axes, colorscale=[(RED, -0.4), (PURE_YELLOW, 0), (GREEN, 0.4)], axis=1
|
||||
)
|
||||
scene.add(axes, surface_plane)
|
||||
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ def test_AnimationBuilder(scene):
|
|||
|
||||
@frames_comparison(last_frame=False)
|
||||
def test_ReplacementTransform(scene):
|
||||
yellow = Square(fill_opacity=1.0, fill_color=YELLOW)
|
||||
yellow = Square(fill_opacity=1.0, fill_color=PURE_YELLOW)
|
||||
yellow.move_to([0, 0.75, 0])
|
||||
|
||||
green = Square(fill_opacity=1.0, fill_color=GREEN)
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ def create_plugin(tmp_path, python_version, random_string):
|
|||
def _create_plugin(entry_point, class_name, function_name, all_dec=""):
|
||||
entry_point = entry_point.format(plugin_name=plugin_name)
|
||||
module_dir = plugin_dir / plugin_name
|
||||
module_dir.mkdir(parents=True)
|
||||
module_dir.mkdir(parents=True, exist_ok=True)
|
||||
(module_dir / "__init__.py").write_text(
|
||||
plugin_init_template.format(
|
||||
class_name=class_name,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue