Compare commits

..

112 commits

Author SHA1 Message Date
dependabot[bot]
0e83f4b09a
chore(deps): bump jupyterlab from 4.5.7 to 4.5.9 (#4822)
---
updated-dependencies:
- dependency-name: jupyterlab
  dependency-version: 4.5.9
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-20 13:14:07 -04:00
dependabot[bot]
f7fd708276
chore(deps): bump jupyter-server from 2.18.0 to 2.20.0 (#4821)
Bumps [jupyter-server](https://github.com/jupyter-server/jupyter_server) from 2.18.0 to 2.20.0.
- [Release notes](https://github.com/jupyter-server/jupyter_server/releases)
- [Changelog](https://github.com/jupyter-server/jupyter_server/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jupyter-server/jupyter_server/compare/v2.18.0...v2.20.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-20 11:22:50 -04:00
Caffein3
4c4622df54
feat: support multiple TeX compilers in TexTemplate (#4810)
* feat: support multiple compilations

Allow `tex_compiler` to be a `str | list[str]` instead of just
`str`. When a list is given, document will be compiled sequentially.

* fix: suppress compilation progress log when only one compiler is used

* fix: updated docstrings and variable naming
2026-06-16 22:35:14 -04:00
dependabot[bot]
66d5a4937a
chore(deps): bump bleach from 6.3.0 to 6.4.0 (#4814)
Bumps [bleach](https://github.com/mozilla/bleach) from 6.3.0 to 6.4.0.
- [Changelog](https://github.com/mozilla/bleach/blob/main/CHANGES)
- [Commits](https://github.com/mozilla/bleach/compare/v6.3.0...v6.4.0)

---
updated-dependencies:
- dependency-name: bleach
  dependency-version: 6.4.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-17 01:49:15 +00:00
dependabot[bot]
c07137dbed
chore(deps): bump tornado from 6.5.5 to 6.5.7 (#4813)
Bumps [tornado](https://github.com/tornadoweb/tornado) from 6.5.5 to 6.5.7.
- [Changelog](https://github.com/tornadoweb/tornado/blob/master/docs/releases.rst)
- [Commits](https://github.com/tornadoweb/tornado/compare/v6.5.5...v6.5.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-17 01:33:08 +00:00
dependabot[bot]
71ab85f960
chore(deps): bump idna from 3.11 to 3.15 (#4736)
Bumps [idna](https://github.com/kjd/idna) from 3.11 to 3.15.
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md)
- [Commits](https://github.com/kjd/idna/compare/v3.11...v3.15)

---
updated-dependencies:
- dependency-name: idna
  dependency-version: '3.15'
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-06-17 01:09:58 +00:00
dependabot[bot]
16f0a3de3e
chore(deps): bump urllib3 from 2.6.3 to 2.7.0 (#4729)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.3 to 2.7.0.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.6.3...2.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-06-17 00:58:29 +00:00
dependabot[bot]
537a134360
chore(deps): bump mistune from 3.2.0 to 3.2.1 (#4725)
Bumps [mistune](https://github.com/lepture/mistune) from 3.2.0 to 3.2.1.
- [Release notes](https://github.com/lepture/mistune/releases)
- [Changelog](https://github.com/lepture/mistune/blob/main/docs/changes.rst)
- [Commits](https://github.com/lepture/mistune/compare/v3.2.0...v3.2.1)

---
updated-dependencies:
- dependency-name: mistune
  dependency-version: 3.2.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 23:49:46 +00:00
dependabot[bot]
b7bf2ea90f
chore(deps): bump docker/setup-buildx-action from 3 to 4 (#4716)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-06-16 22:42:37 +00:00
dependabot[bot]
bdaf4497b7
chore(deps): bump jupyterlab from 4.5.2 to 4.5.7 (#4713)
Bumps [jupyterlab](https://github.com/jupyterlab/jupyterlab) from 4.5.2 to 4.5.7.
- [Release notes](https://github.com/jupyterlab/jupyterlab/releases)
- [Changelog](https://github.com/jupyterlab/jupyterlab/blob/main/RELEASE.md)
- [Commits](https://github.com/jupyterlab/jupyterlab/compare/@jupyterlab/lsp@4.5.2...@jupyterlab/lsp@4.5.7)

---
updated-dependencies:
- dependency-name: jupyterlab
  dependency-version: 4.5.7
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 22:05:50 +00:00
dependabot[bot]
037b376ec2
chore(deps): bump notebook from 7.5.2 to 7.5.6 (#4712)
Bumps [notebook](https://github.com/jupyter/notebook) from 7.5.2 to 7.5.6.
- [Release notes](https://github.com/jupyter/notebook/releases)
- [Changelog](https://github.com/jupyter/notebook/blob/@jupyter-notebook/tree@7.5.6/CHANGELOG.md)
- [Commits](https://github.com/jupyter/notebook/compare/@jupyter-notebook/tree@7.5.2...@jupyter-notebook/tree@7.5.6)

---
updated-dependencies:
- dependency-name: notebook
  dependency-version: 7.5.6
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-06-16 17:46:31 -04:00
dependabot[bot]
e9639c2697
chore(deps): bump nbconvert from 7.17.0 to 7.17.1 (#4702)
Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 7.17.0 to 7.17.1.
- [Release notes](https://github.com/jupyter/nbconvert/releases)
- [Changelog](https://github.com/jupyter/nbconvert/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jupyter/nbconvert/compare/v7.17.0...v7.17.1)

---
updated-dependencies:
- dependency-name: nbconvert
  dependency-version: 7.17.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-06-16 21:13:42 +00:00
dependabot[bot]
1b2d5ce72b
chore(deps-dev): bump pytest from 9.0.2 to 9.0.3 (#4688)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 9.0.2 to 9.0.3.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/9.0.2...9.0.3)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 9.0.3
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-06-16 20:38:28 +00:00
dependabot[bot]
c94a7ea9fc
chore(deps): bump pillow from 12.1.1 to 12.2.0 (#4687)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 12.1.1 to 12.2.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/12.1.1...12.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-06-16 20:10:01 +00:00
dependabot[bot]
bb1be6ef8a
chore(deps): bump actions/upload-artifact from 6 to 7 (#4620)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-06-16 19:48:22 +00:00
rsn
516c8c8ba7
Fix ThreeDScene.set_to_default_angled_camera_orientation() (#4704)
Co-authored-by: neeh <rapetisiddhu@gmail.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-06-16 15:20:18 -04:00
Mayank Basena
ccee37a614
Add docstring to GenericGraph.__getitem__() (#4761)
Allows accessing edges via tuple keys like g[(1, 2)] in addition to
vertex lookups like g[1]. Previously only vertex lookups were supported.

Fixes ManimCommunity/manim#3798

Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-06-16 18:52:42 +00:00
dependabot[bot]
31db147222
chore(deps): bump jupyter-server from 2.17.0 to 2.18.0 (#4720)
Bumps [jupyter-server](https://github.com/jupyter-server/jupyter_server) from 2.17.0 to 2.18.0.
- [Release notes](https://github.com/jupyter-server/jupyter_server/releases)
- [Changelog](https://github.com/jupyter-server/jupyter_server/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jupyter-server/jupyter_server/compare/v2.17.0...v2.18.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-06-16 17:57:48 +00:00
GniLudio
34124c3f60
Fix wrong type annotation of Animation._on_finish (#4812) 2026-06-16 13:11:45 -04:00
Benjamin Hackl
d999d422c9
Introduce first-class support for rendering text and markup via Typst (optional dependency) (#4681)
* feat: add TypstMobject and TypstMathMobject for first-class Typst support

Implement the initial TypstMobject proposal (see agents/typst.md):

- TypstMobject: renders arbitrary Typst markup to SVG via the 'typst'
  Python package (self-contained Rust binary, no system install needed),
  then imports through SVGMobject.
- TypstMathMobject: convenience subclass that wraps input in Typst math
  delimiters ($ ... $).

Pipeline: user string → wrap in minimal Typst document (auto-sized page,
transparent background) → write .typ file → compile to SVG via
typst.compile() → import via SVGMobject → scale/center/recolor.

Key details:
- Compilation helper in manim/utils/typst_file_writing.py with SHA-256
  content-hash caching (same scheme as the LaTeX pipeline).
- font_size property mirrors SingleStringMathTex: compile at fixed 11pt,
  scale after import using initial_height / SCALE_FACTOR_PER_FONT_POINT.
- init_colors() recolors black submobjects to self.color (Typst default
  fill is black), preserving any explicit Typst colors.
- path_string_config enables curve subdivision for smooth animation.
- 'typst' added as optional dependency group in pyproject.toml.
- 10 tests covering creation, font_size, caching, preamble, and repr.

* feat(typst): add sub-expression selection via {{ }} groups and .select()

- Override modify_xml_tree in Typst to convert data-typst-label
  attributes to id attributes before svgelements parsing (avoids
  attribute inheritance issue with data-* attributes)
- Add Typst.select(key) method accepting str labels or int indices
  to retrieve VGroup sub-expressions from id_to_vgroup_dict
- Implement {{ }} double-brace preprocessor on TypstMath:
  - {{ content }} → manimgrp("_grp-N", content) (auto-numbered)
  - {{ content : label }} → manimgrp("label", content) (named)
  - Skips {{ }} inside string literals and [...] content blocks
  - Uses math-mode call convention (no # prefix) to keep args in
    math mode
- Auto-inject manimgrp preamble when groups are detected
- Add 12 new tests covering label mapping, nesting, select(),
  error handling, and preprocessor edge cases

* Remove unneeded assert line

* Generalize some more critical hard-coded LaTeX assumptions

* Fix syntax error

* Support Typst labels in existing APIs

* Add ManimTextLabel typing alias

* Preserve Typst SVG stroke widths by default

* Calibrate Typst font sizing against TeX

* Add Typst mobject documentation

* Track Typst baseline frames

* Document Typst baseline frame tracking

* Add Typst section to text guide

* Polish Typst docs and label handling

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

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

* Improve Typst stroke scaling and docs examples

* Tune Typst SVG stroke scaling

* Restore Typst math example wording

* Fix pre-commit issues for Typst branch

* Fix number line runtime typing import

* Exclude Typst autogenerated .rst files

* Use Manim directive in docstrings and fix wrong key in Typst.select() docstring

* Fix GroupedMath comments

---------

Co-authored-by: Toon Verstraelen <Toon.Verstraelen@UGent.be>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <francisco.manriquezn@usm.cl>
2026-06-11 03:54:21 +00:00
GniLudio
561de9d72a
Use language parameter to format Code even when passing code_file (#4706)
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-06-10 21:49:59 +00:00
Sai Sridhar Tarra
05b3042ab0
Add tests for edge lookup in GenericGraph.__getitem__() (#4753)
GenericGraph.__getitem__ only handled vertex lookup. Passing a 2-tuple
(u, v) to retrieve an edge mobject raised a KeyError against the
vertices dict instead of looking up self.edges.

Route tuple keys of length 2 to self.edges so that g[(u, v)] returns
the edge mobject, consistent with how edges are stored internally.

Adds a test covering vertex lookup, edge lookup (Graph), and edge
lookup (DiGraph).

Fixes #3798

Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-06-10 17:34:39 -04:00
HairlessVillager
2ece488b2c
Support edge lookup via tuple in GenericGraph.__getitem__() (#3799)
* feat: add tuple key support in GenericGraph.__getitem__()

* feat: add an error message for missing vertice or edge

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

* docs: add an example

* fix: tuple vertex return edge

---------

Co-authored-by: adeshpande <110117391+JasonGrace2282@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-06-10 17:19:24 -04:00
Guillaume Vauvert
852ebd1c60
Fix empty submobjects distorting width/height of parent Mobject (#4088)
* Issue-4087 Add bug test

* Issue-4087 Fix bug

* Issue-4087 Fix comment and add type hint

* Issue-4087 Fix comment

* Issue-4087 Improve previous test instead of adding a new one

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

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

* Update tests/module/mobject/mobject/test_mobject.py

* Enhance docstring for reduce_across_dimension method

Updated docstring for reduce_across_dimension method to clarify its purpose and parameters.

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

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

* Remove float typehints for coords in length_over_dim

* Modify test for empty VMobject dimensions

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

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

---------

Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-06-10 16:21:52 -04:00
Irvanal Haq
82522795f1
Allow CyclicReplace and Swap to accept single Group or VGroup and add example (#4211)
* Allow Swap to accept Group and VGroup and add example

* Modify create_target return type to Group | VGroup

Updated create_target method to allow returning either Group or VGroup.

---------

Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-06-09 17:33:12 -04:00
Henrik Skov Midtiby
12c5640a32
Add type annotations to transform_matching_parts.py (#4400)
* Add type annotations to `transform_matching_parts.py`

* Make two type errors quiet

* Make the pytests pass

---------

Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-06-09 17:11:01 -04:00
Laszlo
33424fe43d
More specific Callable type annotations in mobject_update_utils (#4728)
* More specific Callable type annotations in mobject_update_utils

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-06-03 17:02:00 +00:00
Leonardo Cariaggi
1b3390073c
Allow excluding inner lines of Table (#4731)
* Add option to hide/show inner lines in Table class

* Add tests for Table class inner lines visibility

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

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

* Correct indentation

* Fix os.startfile usage to check for availability on Windows

* Add ConversationFlowScene to animate user-chatbot interactions and metadata display

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

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

* Revert "Merge remote-tracking branch 'origin/conversation' into hide_table_inner_lines"

This reverts commit c0ba5b8511, reversing
changes made to 1f71f4b0e8.

* Revert change

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Leonardo Cariaggi <leonardo.cariaggi@kbc.be>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-06-03 09:06:03 -04:00
Rafael Brusiquesi Martins
56f7eb2a1f
Add regression tests for four recent fixes in vectorized_mobject.py (#4750)
* Add regression tests for four recent fixes in vectorized_mobject.py

Adds four regression tests for fixes that landed without tests in the same PR:

  * 21cf9998 (PR #4630, fixes #3569 + #4629) -- IndexError in
    `get_nth_subpath` when `path_list` is empty; ensure it always returns
    a NumPy array.

  * f6cdb547 (PR #4219) -- `add_points_as_corners` silently dropped a
    single new point when called on a VMobject whose last subpath was
    complete.

  * 3d029c12 (PR #4320, fixes #4255) -- `pointwise_become_partial` cleared
    the target's points when the source had no cubic curves, surfacing as
    `Arrow3D.get_start()` / `get_end()` returning the origin after a
    `Create` animation.

  * 429f25328 (PR #4694) -- `scale(scale_stroke=True)` on a compound
    VMobject propagated the parent's scaled stroke width to every
    submobject, overwriting submobjects with their own (e.g. zero) stroke.

Each test reproduces the original failing condition at the unit level
and asserts the post-fix behavior. Validation: every test was confirmed
to fail when the corresponding fix is reverted on the source file, and
pass when the fix is restored.

4 tests, ~0.2s runtime. Tests go in the existing files
`tests/module/mobject/types/vectorized_mobject/test_vectorized_mobject.py`
(three tests, adjacent to their topically-related siblings) and
`tests/module/mobject/types/vectorized_mobject/test_stroke.py` (one test,
next to the existing `test_stroke_scale`).

Co-authored-by: LetMarq <LetMarq@users.noreply.github.com>

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

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

---------

Co-authored-by: THE-RAF <THE-RAF@users.noreply.github.com>
Co-authored-by: LetMarq <LetMarq@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-06-02 21:53:49 +00:00
Rafael Brusiquesi Martins
cfb5c684b7
Add unit tests for module-level color helpers in manim/utils/color/core.py (#4749)
* Add unit tests for module-level color helpers in manim/utils/color/core.py

Adds tests/module/utils/test_color_helpers.py covering the standalone
helper functions exported from manim.utils.color.core (color_to_rgb,
color_to_rgba, color_to_int_rgb, color_to_int_rgba, rgb_to_color,
rgba_to_color, rgb_to_hex, hex_to_rgb, invert_color, color_gradient,
interpolate_color, average_color, random_bright_color, random_color)
and the RandomColorGenerator class.

Before this PR, only the ManimColor class itself was tested
(via tests/module/utils/test_manim_color.py); the standalone helpers
had zero direct test coverage. Coverage on manim/utils/color/core.py
goes from 71% to 80% (line+branch), and 17 of the 18 testable symbols
in the file's __all__ are now exercised. The only deferred symbol is
get_shaded_rgb, which concerns lighting math rather than color
conversion and is better suited to a follow-up.

42 tests, ~0.3s runtime, no new dependencies.

Co-authored-by: LetMarq <LetMarq@users.noreply.github.com>

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

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

* Update tests/module/utils/test_color_helpers.py

Increased assertion scheme to cover all colors.

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

---------

Co-authored-by: THE-RAF <THE-RAF@users.noreply.github.com>
Co-authored-by: LetMarq <LetMarq@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-05-29 17:23:28 -04:00
Vihaan Dutta
429f25328d
Fixed inconsistent stroke width scaling for text in compound objects (#4694)
* fixed zero-stroke mobject scaling

* updated vectorized_mobject scale docstring text
2026-04-19 16:10:14 -04:00
GoThrones
c45724989d
Refactor`Mobject.put_start_and_end_on() to shift Mobject to start when it's a closed curve (#4658)
* fix: preserve geometry in put_start_and_end_on for zero-vector mobjects

* fix: preserve geometry in put_start_and_end_on for zero-vector mobjects

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

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

* fixed asser_array_equal line

* removed commets in mobject.py and opengl_mobject.py as suggested

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

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

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

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

* added stacklevel=2 suggestion

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-04-14 19:57:06 +00:00
dependabot[bot]
af70b6fef2
chore(deps-dev): bump requests from 2.32.5 to 2.33.0 (#4659)
Bumps [requests](https://github.com/psf/requests) from 2.32.5 to 2.33.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.32.5...v2.33.0)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.33.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 01:57:29 +00:00
dependabot[bot]
4b32312dd1
chore(deps): bump tornado from 6.5.4 to 6.5.5 (#4635)
Bumps [tornado](https://github.com/tornadoweb/tornado) from 6.5.4 to 6.5.5.
- [Changelog](https://github.com/tornadoweb/tornado/blob/master/docs/releases.rst)
- [Commits](https://github.com/tornadoweb/tornado/compare/v6.5.4...v6.5.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-04-07 01:43:47 +00:00
GoThrones
90141df105
Change VMobject._bezier_t_values typehint to ndarray of np.float64 (#4675)
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-04-06 17:02:25 +00:00
Benjamin Hackl
82f93b6c3c
chore: combine dependabot updates for CI retest (#4677)
Includes the changes from PRs #4666, #4668, #4669, #4670, and #4671.

Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-04-06 12:19:30 -04:00
Francisco Manríquez Novoa
752b46a003
Update TinyTeX Windows/macOS installation in ci.yml to fix failing pipelines (#4679)
* Update TinyTeX installation method in CI workflow

* Fix path execution for TinyTex Windows installer
2026-04-06 11:59:03 -04:00
Mingqi Geng
21cf9998cc
Fix IndexError in get_nth_subpath() and ensure it always returns a NumPy array (#4630)
* fix #3569 and #4629

* Add changes to opengl_vectorized_mobject.py

---------

Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-03-18 11:15:45 -03:00
Henrik Skov Midtiby
ebb230f6f1
Update link in pyproject.toml to X / Twitter (#4642) 2026-03-18 13:40:16 +00:00
Xiuyuan (Jack) Yuan
46177d247e
Fix wrong angle ranges in Sphere documentation and add more examples (#3973)
* Made the document changes

* added additional example

* Apply some suggestions from code review

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

* Shorten sphere examples

* Fix line breaks and remove trailing whitespace

* Add example for showing portions of spheres

* Undo accidental overwrite of ExampleSphereOverlap

* Change name of manim code block

---------

Co-authored-by: Xiuyuan <u7678992@anu.edu.au>
Co-authored-by: TahitiX <136950383+TahitiX@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <francisco.manriquezn@usm.cl>
2026-03-18 00:28:52 -03:00
Henrik Skov Midtiby
468929889b
Add type annotations to mobject.py (#4388)
* Add type annotations to `mobject.py`

* More work on type annotations for mobject.py

* ...

* Almost handled all mypy errors

* Add the mypy error messages to the lines that trigger them

* Use typing.cast to avoid some mypy errors, as suggested by JasonGrace2282

* Used the ruff linter

* Fixed one typing issue and added the error descriptions to the source code for the remaning 10 errors

* Set the type of the elements in a VGroup to VMobject

* Use typing.cast to handle some specific cases.

* Code cleaning

* Updates

* When started to use typing.cast it is needed to import / define certain elements during runtime and not only during type checking

* Fix bug introduced with the type annotations.

* Made it work again

* ..

* Fixed more issues.

* Code cleanup

* Code cleanup.

Replace self.lines with self.lines_chars and self.lines_alignment.

* Handle slicing when accessing elements inside a VGroup

* Fix missing issues.

* Silence the last mypy error

* Make _Updater, _NonTimeBasedUpdater and _TimeBasedUpdater private.

* Replace | with Union[...] in one location

* Move import of Union

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

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

* Remove comment that is no longer relevant

* Overload the auto_zoom method in MovingCamera to provide better type hinting

* Codecleanup

* Code cleanup

* Renamed lines_alignment to lines_alignments and added a TODO about a future cleanup task

* More code cleanup

* Update manim/mobject/matrix.py

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

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

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

* Addressing multiple comments from Chopan50

* Implementing more suggestions from Chopan50

* More suggestions by Chopan50

* ...

* More renaming and code cleanup, as suggested by Chopan50

* Restructure code

* anim_args

* Update typing in vector_space_scene

* Implemented a number of suggestions from chopan50

* Make a list of faces with the type list[ThreeDVMobject]

* Ensure to return a VGroup if slicing is used.

* Revert back to the original code in text_mobject.py

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

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

* Add the propagate_colors option to Mobject as it is used by VMobject.

Replace Self with _UpdateBuilder

* Simplify code

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-03-11 20:33:21 -03:00
xuruidong
98c458b6b2
docs(quickstart): fix incorrect terminology for SquareToCircle (#4631) 2026-03-08 09:47:57 +01:00
Colin Belhomme
d4af5b2baa
fix: fix cast crash in OpenGLMobject.arrange_in_grid() and OpenGLPoint.get_location() (#4622)
Added quotes to Vector3D and to Point3D to match other calls to `cast`, such has Chopan suggested

Issue #4550: https://github.com/ManimCommunity/manim/issues/4550

Co-authored-by: Colin Belhomme <colin.belhomme@telecom-paris.fr>
2026-03-03 23:48:31 -03:00
Benjamin Hackl
1157b746c3
Prepare new release: v0.20.1 (#4615)
* bump version, regenerate lockfile, changelog 1st pass

* fix two docbuild warnings

* update changelog, include newly merged PR + review suggestion
2026-02-27 08:09:59 +01:00
Irvanal Haq
6f825e8513
Fix unintended kwargs propagation in LaggedStartMap (#4613)
* Fix unintended kwargs propagation in LaggedStartMap

Ensure  intended for  are not forwarded to the superclass, and make  explicit.

* add test for keyword propagation in LaggedStartMap

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

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

* Update test_composition.py

update the test

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

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

* Update test_composition.py

add there_and_back

---------

Co-authored-by: Benjamin Hackl <mail@behackl.dev>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-02-27 07:14:28 +01:00
Henrik Skov Midtiby
a0414dccec
Add type annotations to manim/_config/utils.py (#4230)
* Stop ignoring  manim._config erros in mypy.ini

* Aspect ratio should be a float.

* Handled more mypy issues in _config/utils.py

* Handled more mypy issues in _config/utils.py

* Removed two assert statements that triggered errors in the unittests.

* Fix last mypy issue in utils.py and activate mypy checking

* Fix type of window_size in opengl_renderer_window.py

* ...

---------

Co-authored-by: F. Muenkel <25496279+fmuenkel@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
2026-02-22 22:15:16 +00:00
Benjamin Hackl
33a0e56d73
enhancement: optimize Docker image build and runtime footprint (#4604)
* docker: add libgl1 runtime dependency for OpenGL

* docker: tighten .dockerignore for smaller build context

* docs(docker): add runtime notes for latest image

* docs(docker): note ctex is not included by default

---------

Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-02-22 17:29:18 -03:00
Benjamin Hackl
80fd11efbc
Suppress pydub ffmpeg warning on import (#4603)
Co-authored-by: Henrik Skov Midtiby <hemi@mmmi.sdu.dk>
2026-02-22 19:30:13 +00:00
Piyush Acharya
498f0b9c89
docs: add alt text to all images for accessibility (#4064)
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
2026-02-22 12:51:39 +01:00
Matvey Merzlikin
87cd63549c
Add support for negative z-index in AnimationGroup (#4389)
* Add support for negative z-index in AnimationGroup

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

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

* Refactor animation unpacking logic for clarity

* Fix unpacking logic to handle Mobject instances correctly

* Fix mypy check

* Fix tests

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

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

* fix: simplify AnimationGroup unpacking for moving mobjects

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
2026-02-22 11:24:32 +00:00
u7920349
cd370610c5
Documentation: manual installation of manim as a local package (#4456)
* Documentation: additonal instalation method

* Typo fix

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

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

* docs: simplify dev-install section and restore PR template checklist

---------

Co-authored-by: CA3000 <93896083+CA3000@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
2026-02-22 11:59:40 +01:00
Josie Thompson
a6af7f3d76
Cleanup TipableVMobject: avoid mutable default and fix assign_tip_attr typo (#4503)
* Small touch-ups before fixing tip bugs
- fixed mutable default argument in Tipable VMobject constructor
- fixed typo in method name assign_tip_attr
- added two todo's which outline unexpected behavior in tip placement and how to solve

* refactor: align tip attr naming and remove TODO comments

---------

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
2026-02-22 11:36:29 +01:00
Abdelsalam
e34e707858
Fix creation or animation of a zero-length DashedLine (#4606)
* fixing #4591

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-02-22 11:10:21 +01:00
Benjamin Hackl
7c1c9258d0
Fix publish release workflow (#4600)
* ci: fix publish release workflow

- Add sudo to apt-get calls (runner is non-root)
- Add contents: write permission for uploading release assets
- Drop uv sync before uv build (unnecessary)
- Replace archived upload-release-asset action and redundant
  Python/requests API script with gh release upload; tag name
  is available directly from the release event context
- Strip leading v from tag name to match built artifact filename

* ci: set bash shell for release asset upload step
2026-02-22 11:05:09 +01:00
Benjamin Hackl
000e7792bd
fix: MathTex double-brace splitting no longer fires on natural LaTeX }} (#4602)
* fix: replace double-brace splitting regex with state-machine parser

The previous re.split(r'{{|}}', ...) call split on any occurrence of
{{ or }} in the input string, which broke strings whose only }} came
from closing two nested LaTeX brace groups (e.g. ^{\frac{Mq}{M+m}}).

The new _split_double_braces() static method uses a character-level
state machine with three guards:

* Escape priority: \\ is consumed before \{ / \}, so \\}} is
  correctly read as an escaped backslash followed by a real }}, not
  misinterpreted as \ + \} + lone }.

* Whitespace-gated opener: {{ is only treated as a Manim group opener
  at the start of the string or after whitespace. Naturally-occurring
  {{ in LaTeX is usually preceded by non-whitespace (e.g. \frac{{{n}}}
  or a^{{2}}), so this eliminates the most common false positives.

* Depth-tracking closer: inside a Manim group, }} only closes the
  group when the inner brace depth is zero, so {{ a^{b^{c}} }} is
  handled correctly and nested LaTeX }} cannot trigger an early close.

Fixes #4601.

* docs: document double-brace whitespace requirement

Add a Notes section to the MathTex docstring explaining:
- how {{ }} splits a string into submobjects
- that {{ must be at start-of-string or preceded by whitespace
- that this leaves natural LaTeX like \frac{{{n}}}{k} untouched
- the { { ... } } escape hatch when a split is not wanted

Apply the same explanation to the double-brace paragraph in
docs/source/guides/using_text.rst.

* fix: use r-string for _split_double_braces docstring, halve backslash escaping
2026-02-22 10:48:51 +01:00
Benjamin Hackl
704830ff5f fix: fix permission problem in python-publish workflow 2026-02-21 00:16:28 +01:00
Benjamin Hackl
d1eea48aa6
Prepare new release, v0.20.0 (#4599)
* fix: resolve parameter shadowing bug in release script changelog command

* chore: bump version to v0.20.0, regenerate lockfile, update citation

* generate v0.20.0 changelog, first round

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

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

* a bit of copyediting, second pass

* include newly merged PR, apply suggestions from review

* Update docs/source/changelog/0.20.0-changelog.md

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-02-20 23:50:31 +01:00
F. Muenkel
9504757918
Replace scipy.special.comb with math.comb (#4598)
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-02-20 15:15:43 -03:00
Daniel Bates
7eab4c5450
Fix coords_to_point treating single list/array as batch of x-values (#4596)
When a single flat list or 1D numpy array was passed to
`Axes.coords_to_point()` (e.g. `ax.c2p([1, 2])` or
`ax.c2p(np.array([1, 2, 1]))`), the values were incorrectly
interpreted as multiple x-coordinates instead of a single (x, y) or
(x, y, z) point.

This happened because `np.asarray(([1, 2],))` produces a 2D array
with shape (1, N), which fell through to the code path that treats
`coords[0]` as a list of x-values. The fix detects this case
(ndim == 2, shape[0] == 1) and extracts the inner array so it is
handled identically to `c2p(1, 2)`.

Closes ManimCommunity#4073

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 17:31:19 +01:00
GoThrones
fda336978b
docs: improve TransformFromCopy docstring (#4597) 2026-02-20 17:20:08 +01:00
F. Muenkel
ab7cfc54d8
Add type annotations to point_cloud_mobject.py (#4586)
* Add type annotations to point_cloud_mobject.py

* Make reset_points return Self instead of None

* Fix return type of get_array_attrs()

* Change return type of reset_points() to Self in image_mobject.py
2026-02-18 12:59:15 +00:00
Aarush Deshpande
601a007192
Implement Mobject.always (#4594) 2026-02-17 19:47:58 -05:00
F. Muenkel
ab17eb58a3
Add type annotations to ``opengl_image_mobject.py`` (#4536)
* Add type annotations to opengl_image_mobject.py

* Remove mypy entry

---------

Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-02-16 23:27:05 +00:00
Francisco Manríquez Novoa
a5d4ab134c
Fix YELLOW_C and add PURE_CYAN, PURE_MAGENTA and PURE_YELLOW (#4562)
* Fix YELLOW_C and add PURE_CYAN, PURE_MAGENTA and PURE_YELLOW

* update default colors YELLOW -> PURE_YELLOW throughout the library

* chore: format + check

* Modify RandomColorGenerator.next() doctest whose output changed by adding colors

---------

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
2026-02-16 20:07:09 -03:00
Francisco Manríquez Novoa
f9b12e5d12
Fix ImageMobject 3D rotation/flipping and remove resampling algorithms lanczos (antialias), box and hamming (#4266)
* Fix 3D ImageMobject rotation

* Remove lanczos, box and hamming resampling algorithms

* Consider case where matrix A is singular (points are aligned)

* homographic_matrix -> homography_matrix

* Fix ImageInterpolationEx scene and increase height in ImageInterpolation test

* Regenerate unit test again

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

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

* ManimFloat should probably not be used as dtype

* Import ImageMobject inside TYPE_CHECKING

* Add Camera.points_to_subpixel_coords() and do not render perpendicular images

* Modify algorithm to use height from longest side

* Prevent possible zero division

* Regenerate ImageMobject.npz

* Edit Image.transform() commentary

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
2026-02-16 19:46:01 -03:00
Francisco Manríquez Novoa
ae501baf19
Fix assertion in ImageMobjectFromCamera.interpolate_color() (#4593) 2026-02-16 23:18:47 +01:00
Henrik Skov Midtiby
761bc46cc8
Add type annotations to image_mobject.py (#4458)
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-02-16 15:28:24 +00:00
Henrik Skov Midtiby
357bb3fbba
Rewrite MathTex to make it more robust regarding splitting (#4515)
* Extracted the method get_mob_from_shape_element

* Moved more functionality to get_mob_from_shape_element

* More cleanup

* Parse the svg file while maintaining the group structure.

* Make the svg groups available

* Handle PERF401 issue

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

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

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

* Added an example of the issue

* Experimenting with coloring elements from the latex equation

* ...

* Regular expression can now match more than one object

* Process the string by applying the substrings in the order they match

* Code refactoring and added type annotations

* ...

* Added a lot of test cases

* More examples

* More examples

* Use matched_strings_and_ids to simplify existing methods

* Remove unused code

* Update get_part_by_tex to use matched_strings_and_ids

* This is required for test_MathTable to pass

* Ensure that self.texstring is set.

* Added more examples from exising issues in the github repo

* Ensure that latex groups are maintained by adding an additional pair of curly braces around the extracted part

* ExampleScene -> Scene

* Added comment

* _break_up_by_substrings

* Refactor code

* Added comment to example

* Handle integer inputs well.

* Expose the original tex_string

* Do not treat the content of substrings_to_isolate as regular expressions.

* Updated examples

* Update examples

* Fix SVMobject caching issue.

* Remove traces from brace_notation_split_occurred

* Simplify MathTex::_break_up_by_substrings

* Fix small issue in tex that in some cases moved elements a tiny bit around

* No use of regular expressions for locate substrings.

* Updated notes to the set of test cases

* Handle issues with the center environment.

* Add example

* Fix issue with rectangles (e.g. from sqrt)

* ConvertToOpenGL

* Reduce the number of nesting levels.

* Use the specified arg_seperator

* Deal with the double curly brace markup

* Code cleanup

* Code cleanup

* Rollback a few changes

* Code cleanup

* Adjust paths the generated artefacts in tests that rely on MathTex

* Added a remark to the using text guide on enclosing snippets in curly braces for substrings_to_isolate to work

* Added space around the numerator argument to frac to avoid having double curly braces in the example.

This would otherwise trigger MathTex to split the string at that location.

* Log errors properly and display some information about the errors and their context.

* Code refactoring as suggested by Benjamin

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
2026-02-16 14:22:56 +01:00
F. Muenkel
6e9ec60b6f
Add type annotations to opengl_compatibility.py (#4585)
* Add type annotations to opengl_compatibility.py

* Tighten type hinting on namespace variables
2026-02-12 22:54:29 +00:00
Sacha
c8db165825
Resolve more race conditions potentially happening during directory creation (#4589)
* fixed division by 0

* fixed division by 0

* fixed division by 0

* updated lock file

* Revert "fixed division by 0"

This reverts commit b916a0c9c9.

* Add tests for turn_animation_into_updater with zero/negative run_time

* updated lock file

* tests

* tests

---------

Co-authored-by: Benjamin Hackl <mail@behackl.dev>
2026-02-12 08:18:26 +01:00
dependabot[bot]
b4049bd6c7
Bump pillow from 12.1.0 to 12.1.1 (#4588)
Bumps [pillow](https://github.com/python-pillow/Pillow) from 12.1.0 to 12.1.1.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/12.1.0...12.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-11 16:09:06 -03:00
Oll-iver
8a5267a9ee
Enable strict=True for zip() where safe (#4547)
* sub_alphas is derived directly from to_update so they're guaranteed to be of the same length.

* self.shapes is initialised as a direct copy of the mobject, guaranteed to be of same length.

* linspace in this case guarantees both arrays are of equal size (self.n_segments).

* Any transformation already requires that each datapoint in the first tuple has a corresponding datapoint in the second (ie same length)

* Replaced arange with linspace, eliminates risk of floating point errors and forces rgbas and offset to be the same size for strict=True

* all_arc_configs is either defined specifically by length of point_pairs or strictly forced to be the same length (n). In any case they'll always be the same length so strict=True works.

* There should always be an equal amount of start and end anchors; radius_list is defined directly from the length of vertex_group; both outer_vertices and inner_vertices posess n vertices.

* boundary_times always contains has an even length so both 'slices' in the zup function are the same length.

* colors_in_gradient is defined to be the same length as p_list_complete; labels and parts are seemingly user inputs with no guarantee of equal length; val_range is defined to be same lenght as self.bar_names; however there's no authentication that self.values has a fixed length after it's been defined ie user can append to the list creating a mismatch between len(self.values) and len(self.bars)

* In most cases here, the tuples are either defined to be of same length or manipulated to be by the align_data function. In the match_points function there is currently no validation to ensure both mobjects are the same length.

* Reverting _add_x_axis_labels() zip() function back
to strict=False due to failing test cases

* Reverted strict zip usage

* color_gradient is defined to be same length as p_list_complete & within _add_x_axis_labels we define val_range to be the same length as self.bar_names

* align_data and lock_matching_data have no validation to ensure tuples in the zip() function are of the same length. Every other time zip() is used here it is generally immediately manipulating or explicitly defining the tuples to be of same length

* All tuples in zip() functions here are either clearly the same size or manipulated to be the same size using the make_even function.

* The tuples in the zip() function will clearly be of equal length, the second tuple is simply a cyclic shift of the first.

* In the ingest_submobjects function arrays is a one to one mapping of attrs so they are guaranteed to have equal lengths.

* Every usage of zip() consists of tuples that are either manipulated to be equal size or defined to be equal size.

* the zip() function in bezier_remap will always consist of equal length tuples as current_number_of_curves is read directly from the shape of bezier_tuples and is used to dictate the size of split_factors.

* The zip() function color_gradient() will always consist of equal length tuples as floors is defined directly from alphas (which also defines alphas_mod1)

* The tuples in the zip() function in adjacent_n_tuples will always be the same length so strict=True.

* The find_intersection() contains a zip() function that has been set to strict=True. While it is technically possible to pass tuples to this function that are *not* the same length, this would result in generally unexpected behaviour anyway.

* Changed zip() function to have strict=True in __init__() as custom_labels is dependent on tick_range so guaranteed to have the same length.

* Several instances of zip() set to strict=True. In add_coordinates we have axis manipulated to be the same length as tick_range. In get_riemann_rectangles() we have colors dependent on of x_range_Array forcing them to be the same length. Finally in plot_line_graph()  it is clearly intended that all inputs used in the zip() function are of the same length (except possibly z which may not exist and will be made equal length to x); while it is not guaranteed they will be the same length this would cause unintended behaviour.

* zip() function bool changed to strict=True in all these test cases. Most test cases either a) hardcode two things to be the same length, b) verify things are the same length before the function or c) explicitly exist to check whether two things are the same length.
2026-02-11 10:39:25 +01:00
Tim Hutt
6adc6e4cf6
Add py.typed to declare manim as having type hints (#4553)
* Add py.typed to declare manim as having type hints

This tells tools like Pyright that this package has its own type hints and it should try to look for them in typeshed.

Fixes #4552

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-02-11 10:35:12 +01:00
Henrik Skov Midtiby
21e7e0d9c1
Cleaned up mypy.ini (#4584)
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
2026-02-11 10:04:31 +01:00
Sacha
7efa45492f
Fix TOCTOU Race Conditions when creating directories (#4587)
* fixed division by 0

* fixed division by 0

* fixed division by 0

* updated lock file

* Revert "fixed division by 0"

This reverts commit b916a0c9c9.

* Add tests for turn_animation_into_updater with zero/negative run_time

* updated lock file

* tests

---------

Co-authored-by: Benjamin Hackl <mail@behackl.dev>
2026-02-11 10:00:40 +01:00
dependabot[bot]
c261c61dfd
Bump nbconvert from 7.16.6 to 7.17.0 (#4582)
Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 7.16.6 to 7.17.0.
- [Release notes](https://github.com/jupyter/nbconvert/releases)
- [Changelog](https://github.com/jupyter/nbconvert/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jupyter/nbconvert/compare/v7.16.6...v7.17.0)

---
updated-dependencies:
- dependency-name: nbconvert
  dependency-version: 7.17.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 22:42:58 -03:00
Aarush Deshpande
9d98f00ee9
Remove __future__.annotations from required imports (#4571)
* Remove __future__.annotations from required imports
2026-02-05 21:57:20 -03:00
Benjamin Hackl
587e8d6546
Rework and consolidate release changelog script, add previously skipped changelog entries (#4568)
* rewrite dev_changelog script from scratch, turn into release util cli

* cleanup dependencies

* regenerate skipped changelog entries

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

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

* remove changelog section from PR template

* fix pre-commit checks

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Francisco Manríquez Novoa <49853152+chopan050@users.noreply.github.com>
2026-02-04 10:10:39 +01:00
Sacha
d49115316e
Fixed division by 0 in "turn_animation_into_updater" (#4567)
* fixed division by 0

* fixed division by 0

* fixed division by 0

* updated lock file

* Revert "fixed division by 0"

This reverts commit b916a0c9c9.

* Add tests for turn_animation_into_updater with zero/negative run_time

* updated lock file

---------

Co-authored-by: Benjamin Hackl <mail@behackl.dev>
2026-02-03 22:40:09 +00:00
Rin
2f38426ceb
fix: initialize a_tex to prevent UnboundLocalError in PolarPlane (#4557) 2026-02-02 01:09:50 -03:00
Arnaud
bbdcda1ff5
add type annotations and docstrings for opengl_renderer.py (#4537)
* type annotations and docs for opengl_renderer.py

* add missing self

* Fix RTD build: move Window import to TYPE_CHECKING block

The Window import was moved to module-level in a recent type annotation
commit, but this causes opengl_renderer_window.py to be imported at
load time, triggering pyglet which fails on headless systems (RTD) when
it tries to create a shadow window. Moving the import into TYPE_CHECKING
preserves type hints for mypy while avoiding runtime display dependency.

* Fix mypy errors in opengl_renderer.py

- Cast np.linalg.inv() result to correct type
- Convert quaternion list to ndarray before passing to rotation_matrix_transpose_from_quaternion
- Cast get_center return value
- Fix return type for pixel_coords_to_space_coords (ensure float dtype)
- Add type: ignore for moderngl.create_context backend arg (incorrect stubs)
- Add type: ignore for blend_func assignment (incorrect stubs)

* Fix mypy errors and runtime import issues in opengl_renderer.py

- Import MatrixMN and Point3D at runtime (needed for typing.cast)
- Cast np.linalg.inv() result to correct MatrixMN type
- Convert quaternion list to ndarray before passing to rotation_matrix_transpose_from_quaternion
- Cast get_center and pixel_coords_to_space_coords return values to Point3D
- Ensure float dtype in np.array literals for type consistency

* Add runtime import of Window in init_scene

The Window class is imported inside TYPE_CHECKING for type hints, but
needs to be imported at runtime when actually creating a window. This
import is deferred to avoid triggering pyglet display initialization
on headless systems (RTD build).

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

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

---------

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-01-25 21:24:16 +01:00
BHearron38
2c0b49be9f
fix: infinite recursion caused by accessing color of a highlighted Ta… (#4435)
* fix: infinite recursion caused by accessing color of a highlighted Table cell.

fix: removed type: ignore[attr-defined] mypy was not a fan of

* fix: added back needed mypy type ignore

* Add regression tests for Table/BackgroundRectangle color access

Tests for infinite recursion issue fixed in PR #4435 (issue #4419).

Refs: #4419

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

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

* Refactor BackgroundRectangle color test to geometry tests

Move test_background_rectangle_color from test_table.py to
test_unit_geometry.py since BackgroundRectangle lives in
geometry/shape_matchers.py, not in tables.

Regression test for ManimCommunity/manim#4419 (infinite recursion
when accessing BackgroundRectangle.color)

* Fix: Added missing manim GREEN import needed for test_BackgroundRectangle_color_access()

---------

Co-authored-by: Henrik Skov Midtiby <hemi@mmmi.sdu.dk>
Co-authored-by: Benjamin Hackl <mail@behackl.dev>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: bhearr <None>
2026-01-25 16:47:58 +01:00
Benjamin Hackl
eb8194a640
Fix broken aquabeam OpenGL link using Wayback Machine (#4545)
Refs: #4468, #4509

Co-authored-by: Henrik Skov Midtiby <hemi@mmmi.sdu.dk>
2026-01-25 12:33:39 +00:00
Aditi Juneja
eca7f17853
using color instead of fill_color with MathTeX for node labels (#4501)
* fill_color --> color for MathTeX

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
2026-01-25 11:53:05 +01:00
Arnaud
2317b0ee8b
feat: add a 'seed' option for reproducible outputs (#4532)
* feat: 'seed' option for reproducible outputs

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

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

* enh: let Scene automatically pick global random seed, use arg as override

* chore: move import slightly

* chore: add test for color generator being reproducible with global config seed

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
2026-01-20 22:24:32 +01:00
F. Muenkel
d938533742
Add type annotations to ``rotation.py`` (#4535)
* Add type annotations to rotation.py

* Remove mypy entry

* Make typing of axis, about_point and about_edge consistent

* More typing in transform.py
2026-01-19 17:32:29 -03:00
Francisco Manríquez Novoa
0b3f28a5af
Revert "Set the default Python version to 3.13 in the uv installation guide (…" (#4534)
This reverts commit 83bcafb0e7.
2026-01-18 08:49:10 +01:00
Arnaud
6a56cc5ce6
fix: show doc of RandomColorGenerator (#4533) 2026-01-17 15:53:31 -03:00
Benjamin Hackl
cd7cd1b219
fix release pipeline: install dependencies (#4531) 2026-01-17 11:55:01 +01:00
Benjamin Hackl
9a6550d356
Prepare new release v0.19.2 (#4528)
* chore: bump version numbers etc

* chore: update lockfile

* chore: update date before release
2026-01-17 09:26:40 +01:00
Henrik Skov Midtiby
06f2fe231a
Checking version requirements for dependencies (#4529)
* Update pydub minimum version to 0.22

* numpy>=2.1

* av>=15.0

* isosurfaces>=0.1.1

* Updated lower bounds on dependencies.
2026-01-16 01:31:56 +01:00
Benjamin Hackl
d64af99f89
Add .github/release.yml for improved classifications in automatically generated changelogs (#4526)
* add release.yml file for automated changelog classification

* chore: cleanup leftover comments, add link to doc reference
2026-01-13 22:37:39 +01:00
dependabot[bot]
2e0aa8f529
Bump virtualenv from 20.35.4 to 20.36.1 (#4525)
Bumps [virtualenv](https://github.com/pypa/virtualenv) from 20.35.4 to 20.36.1.
- [Release notes](https://github.com/pypa/virtualenv/releases)
- [Changelog](https://github.com/pypa/virtualenv/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/virtualenv/compare/20.35.4...20.36.1)

---
updated-dependencies:
- dependency-name: virtualenv
  dependency-version: 20.36.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 22:05:07 +01:00
dependabot[bot]
88718ce43f
Bump filelock from 3.20.0 to 3.20.3 (#4527)
Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.20.0 to 3.20.3.
- [Release notes](https://github.com/tox-dev/py-filelock/releases)
- [Changelog](https://github.com/tox-dev/filelock/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/py-filelock/compare/3.20.0...3.20.3)

---
updated-dependencies:
- dependency-name: filelock
  dependency-version: 3.20.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 22:04:42 +01:00
Swarnlata
73eeacb880
fix: correct Circle.point_at_angle calculation (#4236) (#4438)
* fix: correct Circle.point_at_angle calculation for accurate arc mapping

* chore: remove unused variable 'start_angle' to fix pre-commit CI

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

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

* test: add test for Circle.point_at_angle()

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Henrik Skov Midtiby <hemi@mmmi.sdu.dk>
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
Co-authored-by: Benjamin Hackl <mail@behackl.dev>
2026-01-13 18:45:02 +01:00
Jude Nimo
739c2c3f14
Fix type error: pass scene instead of self to get_hash_from_play_call (#4524)
Co-authored-by: Jude Nimo <judenimo@users.noreply.github.com>
2026-01-13 18:19:20 +01:00
dependabot[bot]
d75f84a985
Bump actions/upload-artifact from 5 to 6 (#4522)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 17:54:32 +01:00
dependabot[bot]
b59f311331
Bump actions/cache from 4 to 5 (#4523)
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 17:54:08 +01:00
Christian Clauss
dc4a8bb741
Replace legacy numpy usage ruff rule NPY002 (#4516) 2026-01-13 17:23:34 +01:00
Benjamin Hackl
4bc77b3a00
Bump Python target versions of both mypy and ruff
Merging #4520 - all CI checks passing and approved by JasonGrace2282
2026-01-13 01:13:21 +01:00
Benjamin Hackl
c424f83cb4
Bump minimum Python to 3.11 and av to 14.0.1 (#4385)
* chore: bump minimum supported python to 3.11

* fix: breaking changes from av upgrade

* chore: slightly bump minimum required version of av to 14.0.1

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

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

* chore: update lockfile

* chore: update lockfile again with --upgrade

* Update pyproject.toml

* chore: CI pipeline os/version changes

* fix: indentation in ci.yml

* fix: use result.output instead of result.stdout for test_manim_cfg_subcommand

In Click 8.3.1 (pulled in by av>=14.0.1), help text output
behavior changed for no_args_is_help=True. Using result.output
instead of result.stdout makes the test robust across Click versions,
matching the pattern used in other tests like
test_manim_plugins_subcommand.

* fix: add UP to imports in get_winding_number doctest

* fix: add match_interpolate to imports in doctest

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-01-12 14:27:48 +01:00
Christian Clauss
0d2533231b
Test on Apple Silicon ARM64 (#4496)
Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
2026-01-12 01:22:16 +01:00
Henrik Skov Midtiby
b24b61776b
Add type annotations to three_dimensions.py (#4497)
* Initial type work

* More kwargs Any

* More typing

* _get_u_values_and_v_values cleaned

* self.checkerboard_colors

* Simplify code

* colorscale

* new_colors

* Ugly hacks to make the opengl objects behave

* checkerboard_colors

* Ignored a single type error

* Ignored the last type error

* Remove entry from mypy.ini

* set_fill_by_checkerboard

* resolution

* u_range and v_range

* ThreeDAxes

* Is tuple a color?

* resolution

* Point3D and Vector3D

* More with Point3D

* resolution

* colorscale

* checkerboard_colors Iterable

* Breaking change: checkerboard_colors can now be set to None

* Revert "Breaking change: checkerboard_colors can now be set to None"

This reverts commit d5c54cf5fa.

---------

Co-authored-by: Benjamin Hackl <devel@benjamin-hackl.at>
2026-01-11 23:47:38 +01:00
pre-commit-ci[bot]
d308ae3372
[pre-commit.ci] pre-commit autoupdate (#4518)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.14.9 → v0.14.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.9...v0.14.10)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-01-11 23:41:24 +01:00
Benjamin Hackl
ff86ee6333 Merge remote-tracking branch 'codeberg/main' 2026-01-11 00:01:43 +01:00
TahaShams
ef0cf2a34d Docs: Fix TangentialArc example and add quadrant visualization (codeberg-mirror/#1)
## Overview: What does this pull request change?
This PR fixes the documentation for `TangentialArc`.
1. It replaces the static code block in the `TangentialArc` example with the `.. manim::` directive, allowing the example image/video to actually render in the documentation.
2. It adds a new second example scene, `TangentialArcCorners`, which visually demonstrates how the `corner` parameter (e.g., `(1,1)`, `(-1,1)`) affects the arc's orientation.

## Motivation and Explanation: Why and how do your changes improve the library?
Previously, the `TangentialArc` example was hidden inside a standard code block, so users could not see what the output looked like.
Additionally, the `corner` parameter is complex to visualize mentally. The new `TangentialArcCorners` scene provides a clear visual reference for all four possible quadrant configurations, making the class much easier to learn.

## Links to added or changed documentation pages
N/A

## Further Information and Comments
I have tested these changes locally and verified that the images generate correctly.
I am submitting this to the Codeberg mirror as per the current guidance regarding the GitHub incident.

**Visual Proof of the new example:**
I have attached the image

## Reviewer Checklist
- [ ] The PR title is descriptive enough for the changelog, and the PR is labeled correctly
- [ ] If applicable: newly added non-private functions and classes have a docstring including a short summary and a PARAMETERS section
- [ ] If applicable: newly added functions and classes are tested

Reviewed-on: https://codeberg.org/ManimCommunity/manim/pulls/1
Reviewed-by: Benjamin Hackl <behackl@noreply.codeberg.org>
Co-authored-by: TahaShams <01-134222-153@student.bahria.edu.pk>
Co-committed-by: TahaShams <01-134222-153@student.bahria.edu.pk>
2026-01-10 23:58:06 +01:00
Benjamin Hackl
75285d1f01 chore: update README 2026-01-09 23:30:49 +01:00
pre-commit-ci[bot]
d60154d024
[pre-commit.ci] pre-commit autoupdate (#4514)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.14.8 → v0.14.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.8...v0.14.9)
- [github.com/pre-commit/mirrors-mypy: v1.19.0 → v1.19.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.19.0...v1.19.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-12-16 14:08:38 +01:00
Henrik Skov Midtiby
976d634656
Remove deprecation warning from pytest np.trapz -> np.trapezoid (#4513) 2025-12-15 17:42:23 +01:00
pre-commit-ci[bot]
765f02f3ed
[pre-commit.ci] pre-commit autoupdate (#4506)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.14.7 → v0.14.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.7...v0.14.8)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-12-15 12:10:32 +01:00
dependabot[bot]
a50d8363e7
Bump actions/checkout from 5 to 6 (#4498)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 01:18:38 +01:00
Christian Clauss
de090c1bd0
Add ruff rules PERF for performance (#4492)
* Add ruff rules PERF for performance

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-12-01 10:42:46 +00:00
166 changed files with 8464 additions and 3589 deletions

View file

@ -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/

View file

@ -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. -->

67
.github/release.yml vendored Normal file
View file

@ -0,0 +1,67 @@
# https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes
changelog:
exclude:
labels:
- duplicate/wontfix
- invalid
- question
- release
authors:
- dependabot[bot]
- pre-commit-ci[bot]
categories:
# High Impact
- title: "Breaking Changes 🚨"
labels:
- breaking changes
# Highlights
- title: "Highlights 🌟"
labels:
- highlight
# User-facing
- title: "New Features ✨"
labels:
- new feature
- title: "Enhancements 🚀"
labels:
- enhancement
- title: "Bug Fixes 🐛"
labels:
- pr:bugfix
- title: "Deprecations & Removals ⚠️"
labels:
- pr:deprecation
# Developer-facing
- title: "Documentation 📚"
labels:
- documentation
- title: "Testing 🧪"
labels:
- testing
- title: "Infrastructure & Build 🔨"
labels:
- infrastructure
- title: "Code Quality & Refactoring 🧹"
labels:
- maintenance
- refactor
- title: "Type Hints 📝"
labels:
- typehints
# Catch-all (must be last)
- title: "Other Changes"
labels:
- "*"

View file

@ -144,10 +144,38 @@ def main():
]
)
env_vars = {
# add the venv bin directory to PATH so that meson can find ninja
"PATH": f"{os.path.join(tmpdir, VENV_NAME, 'bin')}{os.pathsep}{os.environ['PATH']}",
}
# Inherit the current environment so PKG_CONFIG_PATH, CFLAGS, LDFLAGS, etc. are preserved.
env_vars = os.environ.copy()
# Prepend the venv bin directory so meson/ninja from the venv are used.
env_vars["PATH"] = f"{os.path.join(tmpdir, VENV_NAME, 'bin')}{os.pathsep}{env_vars.get('PATH','')}"
# Ensure Homebrew-provided pkgconfig and include/lib paths are present on macOS ARM.
if sys.platform == "darwin":
try:
# Try to get specific prefix for lzo (safer for opt path), fall back to generic brew prefix.
brew_prefix = subprocess.check_output(["brew", "--prefix", "lzo"], text=True).strip()
except subprocess.CalledProcessError:
try:
brew_prefix = subprocess.check_output(["brew", "--prefix"], text=True).strip()
except Exception:
brew_prefix = None
if brew_prefix:
# pkg-config files can live in lib/pkgconfig or opt/<pkg>/lib/pkgconfig
pkgconfig_paths = [f"{brew_prefix}/lib/pkgconfig", f"{brew_prefix}/opt/lzo/lib/pkgconfig"]
# merge with any existing PKG_CONFIG_PATH
existing_pc = env_vars.get("PKG_CONFIG_PATH", "")
merged_pc = ":".join([p for p in pkgconfig_paths if p]) + (f":{existing_pc}" if existing_pc else "")
env_vars["PKG_CONFIG_PATH"] = merged_pc
# Ensure compiler & linker flags include brew include/lib
existing_cflags = env_vars.get("CFLAGS", "")
existing_ldflags = env_vars.get("LDFLAGS", "")
env_vars["CFLAGS"] = f"-I{brew_prefix}/include {existing_cflags}".strip()
env_vars["LDFLAGS"] = f"-L{brew_prefix}/lib {existing_ldflags}".strip()
# Debugging: log environment keys relevant to detection
# logger.info(f"env vars for meson: {env_vars}")
with gha_group("Building and Installing Cairo"):
logger.info("Running meson setup")

View file

@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out a copy of the repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Check whether the citation metadata from CITATION.cff is valid
uses: citation-file-format/cffconvert-github-action@2.0.0

View file

@ -22,12 +22,15 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, macos-15-intel, windows-latest]
python: ["3.10", "3.11", "3.12", "3.13"]
os: [ubuntu-22.04, macos-latest, windows-latest]
python: ["3.11", "3.12", "3.13", "3.14"]
include:
- os: macos-15-intel
python: "3.13"
steps:
- name: Checkout the repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Python ${{ matrix.python }}
uses: actions/setup-python@v6
@ -69,12 +72,12 @@ jobs:
sudo /usr/bin/Xvfb $DISPLAY -screen 0 1280x1024x24 &
- name: Setup Cairo Cache
uses: actions/cache@v4
uses: actions/cache@v5
id: cache-cairo
if: runner.os == 'Linux' || runner.os == 'macOS'
with:
path: ${{ github.workspace }}/third_party
key: ${{ runner.os }}-dependencies-cairo-${{ hashFiles('.github/scripts/ci_build_cairo.py') }}
key: ${{ runner.os }}-${{ runner.arch }}-dependencies-cairo-${{ hashFiles('.github/scripts/ci_build_cairo.py') }}
- name: Build and install Cairo (Linux and macOS)
if: (runner.os == 'Linux' || runner.os == 'macOS') && steps.cache-cairo.outputs.cache-hit != 'true'
@ -85,7 +88,7 @@ jobs:
run: python .github/scripts/ci_build_cairo.py --set-env-vars
- name: Setup macOS cache
uses: actions/cache@v4
uses: actions/cache@v5
id: cache-macos
if: runner.os == 'macOS'
with:
@ -101,8 +104,8 @@ jobs:
oriPath=$PATH
sudo mkdir -p $PWD/macos-cache
echo "Install TinyTeX"
sudo curl -L -o "/tmp/TinyTeX.tgz" "https://github.com/yihui/tinytex-releases/releases/download/daily/TinyTeX-1.tgz"
sudo tar zxf "/tmp/TinyTeX.tgz" -C "$PWD/macos-cache"
sudo curl -L -o "/tmp/TinyTeX.tar.xz" "https://github.com/rstudio/tinytex-releases/releases/download/daily/TinyTeX-1-darwin.tar.xz"
sudo tar xJf "/tmp/TinyTeX.tar.xz" -C "$PWD/macos-cache"
export PATH="$PWD/macos-cache/TinyTeX/bin/universal-darwin:$PATH"
sudo tlmgr update --self
for i in "${ttp[@]}"; do
@ -121,12 +124,12 @@ jobs:
- name: Setup Windows cache
id: cache-windows
if: runner.os == 'Windows'
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ${{ github.workspace }}\ManimCache
key: ${{ runner.os }}-dependencies-tinytex-${{ hashFiles('.github/manimdependency.json') }}-${{ steps.cache-vars.outputs.date }}-1
- uses: ssciwr/setup-mesa-dist-win@v2
- uses: ssciwr/setup-mesa-dist-win@v3
- name: Install system dependencies (Windows)
if: runner.os == 'Windows' && steps.cache-windows.outputs.cache-hit != 'true'
@ -134,8 +137,8 @@ jobs:
$tinyTexPackages = $(python -c "import json;print(' '.join(json.load(open('.github/manimdependency.json'))['windows']['tinytex']))") -Split ' '
$OriPath = $env:PATH
echo "Install Tinytex"
Invoke-WebRequest "https://github.com/yihui/tinytex-releases/releases/download/daily/TinyTeX-1.zip" -OutFile "$($env:TMP)\TinyTex.zip"
Expand-Archive -LiteralPath "$($env:TMP)\TinyTex.zip" -DestinationPath "$($PWD)\ManimCache\LatexWindows"
Invoke-WebRequest "https://github.com/rstudio/tinytex-releases/releases/download/daily/TinyTeX-1-windows.exe" -OutFile "$($env:TMP)\TinyTex.exe"
.$env:TMP\TinyTex.exe -o"$($PWD)\ManimCache\LatexWindows"
$env:Path = "$($PWD)\ManimCache\LatexWindows\TinyTeX\bin\windows;$($env:PATH)"
tlmgr update --self
tlmgr install $tinyTexPackages

View file

@ -24,7 +24,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Initialize CodeQL
uses: github/codeql-action/init@v4

View file

@ -13,19 +13,19 @@ jobs:
if: github.event_name != 'release'
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Login to DockerHub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
platforms: linux/arm64,linux/amd64
push: true
@ -38,13 +38,13 @@ jobs:
if: github.event_name == 'release'
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Login to DockerHub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@ -61,7 +61,7 @@ jobs:
print(f"tag_name={ref_tag}", file=f)
- name: Build and push
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
platforms: linux/arm64,linux/amd64
push: true

View file

@ -11,9 +11,13 @@ jobs:
environment: release
permissions:
id-token: write
contents: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Install dependencies
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
@ -25,46 +29,19 @@ jobs:
- name: Build and push release to PyPI
run: |
uv sync
uv build
uv publish
- name: Store artifacts
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
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"

View file

@ -9,7 +9,7 @@ jobs:
build-and-publish-htmldocs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
@ -33,7 +33,7 @@ jobs:
babel-english ctex doublestroke dvisvgm frcursive fundus-calligra jknapltx \
mathastext microtype physics preview ragged2e relsize rsfs setspace standalone \
wasy wasysym
uv sync
uv sync --extra typst
- name: Build and package documentation
run: |
@ -43,7 +43,7 @@ jobs:
tar -czvf ../html-docs.tar.gz *
- name: Store artifacts
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
path: ${{ github.workspace }}/docs/build/html-docs.tar.gz
name: html-docs.tar.gz

View file

@ -12,8 +12,16 @@ repos:
- id: end-of-file-fixer
- id: check-toml
name: Validate pyproject.toml
- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
hooks:
- id: codespell
files: ^.*\.(py|md|rst)$
args: ["-L", "medias,nam"]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.6
rev: v0.14.10
hooks:
- id: ruff
name: ruff lint
@ -21,8 +29,9 @@ repos:
args: [--exit-non-zero-on-fix]
- id: ruff-format
types: [python]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.18.2
rev: v1.19.1
hooks:
- id: mypy
additional_dependencies:
@ -34,10 +43,3 @@ repos:
types-setuptools,
]
files: ^manim/
- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
hooks:
- id: codespell
files: ^.*\.(py|md|rst)$
args: ["-L", "medias,nam"]

View file

@ -4,10 +4,10 @@ authors:
-
name: "The Manim Community Developers"
cff-version: "1.2.0"
date-released: 2025-11-30
date-released: 2026-02-27
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.1"
version: "v0.20.1"
...

View file

@ -1,17 +1,15 @@
<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/manim_community/"><img src="https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=Follow%20%40manim_community" alt="Twitter">
<a href="https://www.manim.community/discord/"><img src="https://img.shields.io/discord/581738731934056449.svg?label=discord&color=yellow&logo=discord" alt="Discord"></a>
<a href="https://github.com/psf/black"><img src="https://img.shields.io/badge/code%20style-black-000000.svg" alt="Code style: black">
<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">
<a href="https://manim.community/discord/"><img src="https://img.shields.io/discord/581738731934056449.svg?label=discord&color=yellow&logo=discord" alt="Discord"></a>
<a href="https://docs.manim.community/"><img src="https://readthedocs.org/projects/manimce/badge/?version=latest" alt="Documentation Status"></a>
<a href="https://pepy.tech/project/manim"><img src="https://pepy.tech/badge/manim/month?" alt="Downloads"> </a>
<img src="https://github.com/ManimCommunity/manim/workflows/CI/badge.svg" alt="CI">
<br />
<br />

382
agents/typst_selector.md Normal file
View file

@ -0,0 +1,382 @@
# Design: Sub-Expression Selection for `Typst` / `TypstMath`
## Problem Statement
Users need to interact with individual parts of a Typst-rendered expression:
color a variable, animate the numerator of a fraction, morph one sub-expression
into another, etc. The `MathTex` class solves this with:
1. **`{{ ... }}` double-brace notation** — splits the TeX string into named
submobject groups at compile time.
2. **`substrings_to_isolate` / `get_part_by_tex`** — identifies submobjects
whose TeX source matches a given string.
Both mechanisms ultimately rely on injecting `\special{dvisvgm:raw <g id='...'>}`
markers into the LaTeX source so that the resulting SVG contains `<g>` elements
with known `id` attributes, which SVGMobject's parser maps to `VGroup`
sub-trees via `id_to_vgroup_dict`.
We need an analogous mechanism for Typst.
## Key Discovery: `data-typst-label` in SVG Output
Typst's SVG renderer (`typst-svg` crate) already emits a `data-typst-label`
attribute on `<g>` elements whenever a `GroupItem` (hard frame) carries a
label. The relevant code path:
```rust
// typst-svg/src/lib.rs — render_group()
if let Some(label) = group.label {
svg.init().attr("data-typst-label", label.resolve());
}
```
A **hard frame** is created by the `box` element (and `block`, etc.). Crucially,
`box` can be used inline inside math mode, and labels can be attached to it.
### Proof of Concept
The following Typst helper wraps content in a labeled `box`:
```typst
#let grp(lbl, body) = [#box(body) #label(lbl)]
```
When used in math:
```typst
$ #grp("numerator", $a + b$) / #grp("denom", $c - d$) = #grp("result", $x$) $
```
The compiled SVG contains:
```xml
<g class="typst-group" ... data-typst-label="numerator">
<!-- glyphs for a + b -->
</g>
<g class="typst-group" ... data-typst-label="denom">
<!-- glyphs for c - d -->
</g>
<g class="typst-group" ... data-typst-label="result">
<!-- glyph for x -->
</g>
```
**Nesting works.** A `grp` wrapping a fraction that itself contains `grp`-ed
sub-parts produces nested `data-typst-label` groups:
```typst
$ #grp("whole-frac", $frac(#grp("numerator", $a + b$), #grp("denom", $c - d$))$) $
```
SVG output:
```xml
<g ... data-typst-label="whole-frac">
<g ... data-typst-label="numerator"> ... </g>
<g ... data-typst-label="denom"> ... </g>
<path class="typst-shape" ... /> <!-- fraction bar -->
</g>
```
### SVG Parser Compatibility
Manim uses `svgelements` to parse SVGs. The library preserves
`data-typst-label` in the `values` dictionary of `Group` objects, and it
propagates to child elements. Manim's `SVGMobject.get_mobjects_from()` already
iterates over groups and builds `id_to_vgroup_dict` keyed by the `id` attribute.
Extending this to also key by `data-typst-label` is straightforward.
## Proposed Interface
### 1. Explicit Groups via `{{ ... }}` Notation (Compile-Time)
Mirror the `MathTex` double-brace convention. Users write:
```python
eq = TypstMath("{{ a + b }} / {{ c - d }} = {{ x }}")
```
The pre-processor splits on `{{ ... }}` (reusing the same whitespace-guard
rules as `MathTex._split_double_braces`) and wraps each group in a labeled
`box` call:
```typst
$ #box[$a + b$] <_grp-0> / #box[$c - d$] <_grp-1> = #box[$x$] <_grp-2> $
```
Each group gets an auto-generated label (`_grp-0`, `_grp-1`, ...).
The `data-typst-label` attributes then appear in the SVG, and
`SVGMobject.get_mobjects_from()` can map them to `VGroup` entries in
`label_to_vgroup_dict` (or reuse `id_to_vgroup_dict`).
These groups become sub-mobjects of the `TypstMath` instance, accessible by
index:
```python
eq[0] # VGroup for "a + b"
eq[1] # VGroup for "c - d"
eq[2] # VGroup for "x"
```
(Non-group content between groups — like `/` and `=` — also becomes
its own submobject, mirroring `MathTex` behavior.)
**For `Typst` (text mode):** the same `{{ ... }}` notation applies, but the
wrapper is `#box[...]` without math delimiters.
### 2. Named Groups via Labels
Users can also assign explicit label names for retrieval by name:
```python
eq = Typst(
r"$ #box[$a + b$] <numerator> / #box[$c - d$] <denom> $"
)
eq.select("numerator").set_color(RED)
eq.select("denom").set_color(BLUE)
```
Alternatively, an even more ergonomic approach that hides the `box` boilerplate
and uses the `{{ ... : label }}` notation:
```python
eq = TypstMath("{{ a + b : numerator }} / {{ c - d : denom }}")
eq.select("numerator").set_color(RED)
```
Here the pre-processor recognizes `{{ content : label }}` and emits
`#box[$content$] <label>` in the Typst source.
### 3. The `.select()` Method
```python
def select(self, key: str | int) -> VGroup:
"""Select a labeled sub-expression.
Parameters
----------
key
Either a label name (string) matching a ``data-typst-label``
in the SVG, or an integer index into the auto-numbered
``{{ ... }}`` groups.
Returns
-------
VGroup
The sub-mobjects corresponding to the selected group.
Raises
------
KeyError
If no group with the given label/index exists.
"""
```
This returns a `VGroup` containing exactly the submobjects (paths) that
were rendered inside the corresponding `<g data-typst-label="...">` in the SVG.
## Implementation Plan
### Step 1: Extend `SVGMobject.get_mobjects_from()` to Track Labels
In `manim/mobject/svg/svg_mobject.py`, the group-walking loop already checks
for `id` attributes. Add a parallel check for `data-typst-label`:
```python
try:
group_name = str(element.values["id"])
except Exception:
# Fall back to data-typst-label if available
label = element.values.get("data-typst-label")
if label:
group_name = f"typst-label:{label}"
else:
group_name = f"numbered_group_{group_id_number}"
group_id_number += 1
```
This automatically populates `id_to_vgroup_dict` with label-keyed entries.
### Step 2: Pre-Processing `{{ ... }}` in Typst Source
Add a `_split_and_label_groups()` method that:
1. Scans the input for `{{ ... }}` or `{{ ... : label }}` patterns
(using the same whitespace-guard rules as `MathTex._split_double_braces`).
2. Replaces each group with `#box[$content$] <label>` (math mode) or
`#box[content] <label>` (text mode).
3. Records the mapping from label → original source string for later lookup.
### Step 3: `Typst.select()` / Index Access
- Store the ordered list of group labels and their source strings.
- `select(label_or_index)` looks up the corresponding `VGroup` from
`id_to_vgroup_dict` (using the `typst-label:...` key).
- `__getitem__(int)` returns the *n*-th group's `VGroup`.
### Step 4: Compatibility with `TransformMatchingTex` (future)
`TransformMatchingTex` (and its successor `TransformMatchingShapes`) works by
matching submobjects between two `MathTex` instances by their TeX string keys.
The same approach extends to `Typst` if each `{{ ... }}` group carries its
original source string as metadata. A `TransformMatchingTypst` animation could
match groups by label name or by source string equality.
## Open Design Questions
### Q1: Context-Aware Wrapping — Math vs. Text Mode
The `box` + `label` mechanism works identically in math and text mode, but the
**wrapping** of group content must match the surrounding context:
- **In text mode:** `{{ Hello : greeting }}``#box[Hello] <greeting>`
- **In math mode:** `{{ y^2 : second }}``#box[$y^2$] <second>`
Getting this wrong is not a silent error — it produces visually broken output.
Wrapping math content with `#box[y^2]` (no `$...$`) renders `y^2` as literal
text in the body font instead of as a math superscript.
This is a real problem for `Typst()`, where a single source string can mix text
and math freely:
```python
Typst("hello world, here is a formula: $x^2 + {{ y^2 : second }} = z^2$")
```
Here `{{ y^2 : second }}` is inside a `$ ... $` block, so it needs the
math-mode wrapper, but the pre-processor has no way to know this unless it
tracks `$` delimiters.
### The `#` prefix problem and math calls
A natural idea is to translate `{{ content }}` into a Typst function call like
`grp("lbl", content)`. However, this has a subtle but critical context
sensitivity: Typst has two different call conventions depending on context:
- **Math call** (no `#` prefix): `$ grp("lbl", a^2 + b) $` — arguments are
parsed **in math mode**. The content `a^2 + b` is math. ✓
- **Code call** (`#` prefix): `$ #grp("lbl", a^2 + b) $` — arguments are
parsed **in code mode**. `a^2` is a syntax error in code! ✗
So in math mode, the function MUST be called without `#` for args to stay in
math mode. In text/markup mode, the function MUST be called WITH `#` (that's
how you invoke code from markup), and content arguments need `[...]` wrapping:
```typst
// Text context: #grp("lbl", [Hello world])
// Math context: grp("lbl", a^2 + b)
```
The function definition is the same either way:
```typst
#let grp(lbl, body) = [#box(body) #label(lbl)]
```
This means the function call approach has **exactly the same context problem**
as the raw `#box` approach: the pre-processor must know whether it's in math or
text to emit the right calling convention.
### Further complication: string literals and content blocks
Even inside `TypstMath` (where everything is math), the scanner must avoid
`{{ }}` matches inside string literals or content blocks:
```python
TypstMath('x^2 + y^2 =_("Hello {{ world }}") z^2')
```
Here `{{ world }}` is inside a `"..."` string literal — it should NOT be
processed. Similarly, content blocks `[...]` inside math switch back to text
mode.
### Options
**A. `TypstMath`: math calls with simple string-aware scanning.**
For `TypstMath`, the entire body is math, so `{{ content }}` always becomes
`grp("_grp-N", content)` (no `#`, no `$...$`). The scanner just needs to
skip `"..."` string literals and `[...]` content blocks — no `$` tracking
needed. This is clean and robust.
**B. `Typst`: context-aware scanning (full parser).**
For the general `Typst` class, the scanner must additionally track `$...$`
math blocks (toggling a mode flag on unescaped `$`) to choose between
`grp(...)` (in math) and `#grp("lbl", [...])` (in text). It must also handle
string literals and content blocks inside math that switch context back. This
is doable but non-trivial — essentially a mini Typst lexer.
**C. `Typst`: no `{{ }}`, manual groups only.**
For the general `Typst` class, don't support `{{ }}` at all. Users write
`grp(...)` / `#grp(...)` themselves (with the helper injected into the
preamble). `{{ }}` is only available on `TypstMath`. This is simpler and
avoids the parsing complexity, at the cost of ergonomics for mixed-mode
documents.
**Recommendation:** Start with A (TypstMath only) and C (manual for Typst).
Upgrade to B later if demand warrants it — the function call infrastructure
is already in place, it's only the scanner that needs upgrading.
### Q2: What about "unlabeled" content between groups?
Like `MathTex`, the pieces of content *between* `{{ ... }}` groups should also
become their own submobjects (auto-labeled with sequential indices). For
example:
```python
TypstMath("{{ a }} + {{ b }} = {{ c }}")
# group-0: "a"
# group-1: "+" (auto-group for inter-group content)
# group-2: "b"
# group-3: "=" (auto-group for inter-group content)
# group-4: "c"
```
Each segment (group or inter-group) gets wrapped in its own labeled `box`.
### Q3: What happens with `box` and baseline alignment?
`box` is an inline element in Typst, and when used inside math mode it
participates in math layout. Testing confirms that fractions, superscripts, and
other constructs render correctly when their children are `box`-wrapped.
However, `box` creates a "hard frame" boundary which may subtly affect spacing
in edge cases (e.g., math operator spacing around a boxed expression). This
needs further testing; if issues arise, we could explore `block(breakable: false)`
or invisible `rect` wrappers as alternatives.
### Q4: Can we avoid the `#grp(...)` / `#box[...] <label>` verbosity?
Yes — the `{{ ... }}` double-brace notation is purely syntactic sugar that gets
pre-processed by Manim before the source reaches the Typst compiler. Users never
need to write raw `#box` or `#label()` calls unless they want finer control.
### Q5: String-based selection without explicit groups?
A future enhancement could support:
```python
eq = TypstMath(r"a + b = c")
eq.select("a") # finds submobjects corresponding to the glyph "a"
```
This is hard to do reliably because:
- Typst SVGs embed glyphs as `<use xlink:href="#gXXX">` references; there's no
text content in the SVG itself.
- A single variable in Typst may span multiple glyphs (e.g., `"alpha"` → one
glyph) or identical glyphs may appear multiple times.
A possible approach: at pre-processing time, wrap every "token" in the Typst
math source in its own labeled `box`. This would require a Typst math tokenizer
and is better suited for a v2 implementation.
## Summary: What Typst Gives Us
| Mechanism | How it works | SVG output |
|---|---|---|
| `#box(body) <label>` | Creates a hard-frame `GroupItem` with a `Label` | `<g data-typst-label="label">...</g>` |
| `#metadata(val) <label>` | Invisible; queryable via `typst query` CLI | No visual output (useful for CLI queries, not SVG) |
| Show rules on labels | `#show <label>: ...` | Transforms visual output but no automatic SVG grouping |
| `context query(<label>)` | Document introspection (positions, counters) | In-document only; not available from Python |
The `box` + `label` mechanism is the **only** one that produces identifiable
groups in the SVG output, making it the correct tool for sub-expression
selection in Manim.

View file

@ -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"]

View file

@ -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.

View file

@ -5,3 +5,4 @@ sphinx-copybutton
sphinxext-opengraph
sphinx-design
sphinx-reredirects
typst>=0.14

View file

@ -1,2 +1,3 @@
jupyterlab
sphinxcontrib-programoutput
typst>=0.14

View file

@ -2,14 +2,18 @@
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.1-changelog
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

View 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.

View 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

View 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 Manims 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

View 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

View 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)

View file

@ -0,0 +1,41 @@
---
short-title: v0.20.1
description: Changelog for v0.20.1
---
# v0.20.1
Date
: February 27, 2026
## What's Changed
### Enhancements 🚀
* Cleanup `TipableVMobject`: avoid mutable default and fix `assign_tip_attr` typo by {user}`josiest` in {pr}`4503`
* enhancement: optimize Docker image build and runtime footprint by {user}`behackl` in {pr}`4604`
### Bug Fixes 🐛
* fix: MathTex double-brace splitting no longer fires on natural LaTeX `}}` by {user}`behackl` in {pr}`4602`
* Fix creation or animation of a zero-length `DashedLine` by {user}`SORVER` in {pr}`4606`
* Fix moving-object detection for nested AnimationGroups with z-indexed mobjects by {user}`Merzlikin-Matvey` in {pr}`4389`
* Fix unintended propagation of `kwargs` in `LaggedStartMap` by {user}`irvanalhaq9` in {pr}`4613`
### Documentation 📚
* Documentation: manual installation of manim as a local package by {user}`u7920349` in {pr}`4456`
* Add alt text to all images in `README.md` by {user}`VerisimilitudeX` in {pr}`4064`
### Code Quality & Refactoring 🧹
* Fix publish release workflow by {user}`behackl` in {pr}`4600`
* Silence pydub ffmpeg/avconv import warning when ffmpeg CLI is absent by {user}`behackl` in {pr}`4603`
### Type Hints 📝
* Add type annotations to `manim/_config/utils.py` by {user}`henrikmidtiby` in {pr}`4230`
## New Contributors
* {user}`SORVER` made their first contribution in {pr}`4606`
* {user}`josiest` made their first contribution in {pr}`4503`
* {user}`u7920349` made their first contribution in {pr}`4456`
* {user}`Merzlikin-Matvey` made their first contribution in {pr}`4389`
* {user}`VerisimilitudeX` made their first contribution in {pr}`4064`
**Full Changelog**: [Compare view](https://github.com/ManimCommunity/manim/compare/v0.20.0...v0.20.1)

View file

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

View file

@ -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.
---

View file

@ -2,17 +2,24 @@
Rendering Text and Formulas
###########################
There are two different ways by which you can render **Text** in videos:
There are three different ways by which you can render **Text** in videos:
1. Using Pango (:mod:`~.text_mobject`)
2. Using LaTeX (:mod:`~.tex_mobject`)
3. Using Typst (:mod:`~.typst_mobject`)
If you want to render simple text, you should use either :class:`~.Text` or
:class:`~.MarkupText`, or one of its derivatives like :class:`~.Paragraph`.
Manim's Pango-based text classes include :class:`~.Text`,
:class:`~.MarkupText`, and derivatives such as :class:`~.Paragraph`.
See :ref:`using-text-objects` for more information.
LaTeX should be used when you need mathematical typesetting. See
:ref:`rendering-with-latex` for more information.
LaTeX rendering is available via :class:`~.Tex` and
:class:`~.MathTex`. See :ref:`rendering-with-latex` for more
information.
Typst support is available via :class:`~.Typst` and
:class:`~.TypstMath`. It offers both general markup and mathematical
typesetting through the Typst compiler without requiring a TeX
distribution. See :ref:`typst-mobjects` for more information.
.. _using-text-objects:
@ -291,6 +298,54 @@ and further references about PangoMarkup.
)
self.add(text)
.. _rendering-with-typst:
Text With Typst
***************
Manim also supports rendering text and formulas with Typst via
:class:`~.Typst` and :class:`~.TypstMath`.
.. important::
Typst support requires the optional ``typst`` dependency. Install it with
``pip install manim[typst]``.
Typst mobjects compile Typst markup directly to SVG and import the result as
vector graphics. This works both for general markup and for mathematical
expressions.
.. manim:: HelloTypst
:save_last_frame:
:ref_classes: Typst
class HelloTypst(Scene):
def construct(self):
text = Typst(r"*Hello* from _Typst!_", font_size=96)
self.add(text)
For mathematical expressions, use :class:`~.TypstMath`:
.. manim:: HelloTypstMath
:save_last_frame:
:ref_classes: TypstMath
class HelloTypstMath(Scene):
def construct(self):
equation = TypstMath(r"sum_(k=1)^n k = (n(n + 1)) / 2", font_size=72)
self.add(equation)
Typst also supports selecting subexpressions via labels in the Typst source,
or via Manim's ``{{ ... }}`` shorthand in :class:`~.TypstMath`:
.. code-block:: python
eq = TypstMath("{{ a + b : lhs }} = {{ c }}")
eq.select("lhs").set_color(BLUE)
eq.select(0).set_color(YELLOW)
See :ref:`typst-mobjects` for more details and additional examples.
.. _rendering-with-latex:
Text With LaTeX
@ -389,8 +444,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 +454,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 +468,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
=======================================================

View file

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

View file

@ -153,10 +153,8 @@ The following commands will
The name for the Python project is *manimations*, which you can change
to anything you like.
Manim does not yet support Python 3.14 and the Python version is therefore set to 3.13.
```bash
uv init --python 3.13 manimations
uv init manimations
cd manimations
uv add manim
```
@ -202,10 +200,8 @@ Manim itself! The following commands will
The name for the Python project is *manimations*, which you can change
to anything you like.
Manim does not yet support Python 3.14 and the Python version is therefore set to 3.13.
```bash
uv init --python 3.13 manimations
uv init manimations
cd manimations
uv add manim
```
@ -257,13 +253,11 @@ As soon as the required dependencies are installed, you can create
a Python project (feel free to change the name *manimations* used below
to some other name) with a local environment containing Manim by running
```bash
uv init --python 3.13 manimations
uv init manimations
cd manimations
uv add manim
```
Manim does not yet support Python 3.14 and the Python version is therefore set to 3.13.
:::::
::::::
@ -335,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
```
:::

View file

@ -195,7 +195,7 @@ Positioning ``Mobject``\s
Next, let's go over some basic techniques for positioning ``Mobject``\s.
1. Open ``scene.py``, and add the following code snippet below the ``SquareToCircle`` method:
1. Open ``scene.py``, and add the following code snippet below the ``SquareToCircle`` class:
.. code-block:: python

View file

@ -73,6 +73,7 @@ from .mobject.text.code_mobject import *
from .mobject.text.numbers import *
from .mobject.text.tex_mobject import *
from .mobject.text.text_mobject import *
from .mobject.text.typst_mobject import *
from .mobject.three_d.polyhedra import *
from .mobject.three_d.three_d_utils import *
from .mobject.three_d.three_dimensions import *

View file

@ -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
@ -33,8 +33,7 @@ from manim.utils.tex import TexTemplate
if TYPE_CHECKING:
from enum import EnumMeta
from typing_extensions import Self
from typing import Self
from manim.typing import StrPath, Vector3D
@ -135,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
@ -300,6 +299,7 @@ class ManimConfig(MutableMapping):
"save_last_frame",
"save_pngs",
"scene_names",
"seed",
"show_in_file_browser",
"tex_dir",
"tex_template",
@ -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
@ -607,6 +608,7 @@ class ManimConfig(MutableMapping):
# the next two must be set BEFORE digesting frame_width and frame_height
"pixel_height",
"pixel_width",
"seed",
"window_monitor",
"zero_pad",
]:
@ -654,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)
@ -670,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
@ -773,6 +777,7 @@ class ManimConfig(MutableMapping):
"dry_run",
"no_latex_cleanup",
"preview_command",
"seed",
]:
if hasattr(args, key):
attr = getattr(args, key)
@ -1042,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"]
@ -1074,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"]
@ -1110,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
@ -1135,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
)
@ -1283,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]
)
@ -1293,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
@ -1310,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
@ -1419,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:
@ -1453,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"]
@ -1637,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)
@ -1736,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"]
@ -1765,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)
@ -1801,9 +1810,20 @@ 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
def seed(self) -> int | None:
"""Random seed for reproducibility. None means no seed is set."""
return self._d["seed"]
@seed.setter
def seed(self, value: int | None) -> None:
if value is None:
return
self._set_pos_number("seed", value, False)
# TODO: to be used in the future - see PR #620
# https://github.com/ManimCommunity/manim/pull/620
@ -1848,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:
@ -1856,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:
@ -1874,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]

View file

@ -17,9 +17,7 @@ __all__ = ["Animation", "Wait", "Add", "override_animation"]
from collections.abc import Callable, Iterable, Sequence
from copy import deepcopy
from functools import partialmethod
from typing import TYPE_CHECKING, Any
from typing_extensions import Self
from typing import TYPE_CHECKING, Any, Self
if TYPE_CHECKING:
from manim.scene.scene import Scene
@ -139,7 +137,7 @@ class Animation:
suspend_mobject_updating: bool = True,
introducer: bool = False,
*,
_on_finish: Callable[[], None] = lambda _: None,
_on_finish: Callable[[Scene], None] = lambda _: None,
use_override: bool = True, # included here to avoid TypeError if passed from a subclass' constructor
) -> None:
self._typecheck_input(mobject)
@ -280,9 +278,12 @@ class Animation:
def get_all_families_zipped(self) -> Iterable[tuple]:
if config["renderer"] == RendererType.OPENGL:
return zip(*(mob.get_family() for mob in self.get_all_mobjects()))
return zip(
*(mob.get_family() for mob in self.get_all_mobjects()), strict=False
)
return zip(
*(mob.family_members_with_points() for mob in self.get_all_mobjects())
*(mob.family_members_with_points() for mob in self.get_all_mobjects()),
strict=False,
)
def update_mobjects(self, dt: float) -> None:

View file

@ -5,9 +5,7 @@ from __future__ import annotations
__all__ = ["AnimatedBoundary", "TracedPath"]
from collections.abc import Callable, Sequence
from typing import Any
from typing_extensions import Self
from typing import Any, Self
from manim.mobject.mobject import Mobject
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
@ -99,7 +97,7 @@ class AnimatedBoundary(VGroup):
) -> Self:
family1 = mob1.family_members_with_points()
family2 = mob2.family_members_with_points()
for sm1, sm2 in zip(family1, family2):
for sm1, sm2 in zip(family1, family2, strict=False):
sm1.pointwise_become_partial(sm2, a, b)
return self

View file

@ -185,7 +185,9 @@ class AnimationGroup(Animation):
else:
sub_alphas[(sub_alphas > 1) | with_zero_run_time] = 1
for anim_to_update, sub_alpha in zip(to_update["anim"], sub_alphas):
for anim_to_update, sub_alpha in zip(
to_update["anim"], sub_alphas, strict=True
):
anim_to_update.interpolate(sub_alpha)
self.anim_group_time = anim_group_time
@ -351,7 +353,7 @@ class LaggedStartMap(LaggedStart):
Parameters
----------
AnimationClass
animation_class
:class:`~.Animation` to apply to mobject.
mobject
:class:`~.Mobject` whose submobjects the animation, and optionally the function,
@ -360,6 +362,17 @@ class LaggedStartMap(LaggedStart):
Function which will be applied to :class:`~.Mobject`.
run_time
The duration of the animation in seconds.
lag_ratio
Defines the delay after which the animation is applied to submobjects. A lag_ratio of
``n.nn`` means the next animation will play when ``nnn%`` of the current animation has played.
Defaults to 0.05, meaning that the next animation will begin when 5% of the current
animation has played.
This does not influence the total runtime of the animation. Instead the runtime
of individual animations is adjusted so that the complete animation has the defined
run time.
kwargs
Further keyword arguments that are passed to `animation_class`.
Examples
--------
@ -390,6 +403,7 @@ class LaggedStartMap(LaggedStart):
mobject: Mobject,
arg_creator: Callable[[Mobject], Iterable[Any]] | None = None,
run_time: float = 2,
lag_ratio: float = DEFAULT_LAGGED_START_LAG_RATIO,
**kwargs: Any,
):
if arg_creator is None:
@ -404,4 +418,4 @@ class LaggedStartMap(LaggedStart):
if "lag_ratio" in anim_kwargs:
anim_kwargs.pop("lag_ratio")
animations = [animation_class(*args, **anim_kwargs) for args in args_list]
super().__init__(*animations, run_time=run_time, **kwargs)
super().__init__(*animations, run_time=run_time, lag_ratio=lag_ratio)

View file

@ -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):
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()

View file

@ -40,10 +40,9 @@ __all__ = [
]
from collections.abc import Iterable
from typing import Any
from typing import Any, Self
import numpy as np
from typing_extensions import Self
from manim.mobject.geometry.arc import Circle, Dot
from manim.mobject.geometry.line import Line
@ -65,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
@ -90,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()
@ -154,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,
):
@ -199,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()
@ -227,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,
@ -350,6 +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=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,

View file

@ -10,7 +10,8 @@ __all__ = [
"MoveAlongPath",
]
from typing import TYPE_CHECKING, Any, Callable
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
import numpy as np
@ -18,7 +19,7 @@ from ..animation.animation import Animation
from ..utils.rate_functions import linear
if TYPE_CHECKING:
from typing_extensions import Self
from typing import Self
from manim.mobject.types.vectorized_mobject import VMobject
from manim.typing import MappingFunction, Point3D

View file

@ -4,11 +4,9 @@ from __future__ import annotations
__all__ = ["Rotating", "Rotate"]
from collections.abc import Callable, Sequence
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
import numpy as np
from ..animation.animation import Animation
from ..animation.transform import Transform
from ..constants import OUT, PI, TAU
@ -16,6 +14,8 @@ from ..utils.rate_functions import linear
if TYPE_CHECKING:
from ..mobject.mobject import Mobject
from ..mobject.opengl.opengl_mobject import OpenGLMobject
from ..typing import Point3DLike, Vector3DLike
class Rotating(Animation):
@ -89,9 +89,9 @@ class Rotating(Animation):
self,
mobject: Mobject,
angle: float = TAU,
axis: np.ndarray = OUT,
about_point: np.ndarray | None = None,
about_edge: np.ndarray | None = None,
axis: Vector3DLike = OUT,
about_point: Point3DLike | None = None,
about_edge: Vector3DLike | None = None,
run_time: float = 5,
rate_func: Callable[[float], float] = linear,
**kwargs: Any,
@ -156,9 +156,9 @@ class Rotate(Transform):
self,
mobject: Mobject,
angle: float = PI,
axis: np.ndarray = OUT,
about_point: Sequence[float] | None = None,
about_edge: Sequence[float] | None = None,
axis: Vector3DLike = OUT,
about_point: Point3DLike | None = None,
about_edge: Vector3DLike | None = None,
**kwargs: Any,
) -> None:
if "path_arc" not in kwargs:
@ -173,7 +173,7 @@ class Rotate(Transform):
self.about_point = mobject.get_center()
super().__init__(mobject, path_arc_centers=self.about_point, **kwargs)
def create_target(self) -> Mobject:
def create_target(self) -> Mobject | OpenGLMobject:
target = self.mobject.copy()
target.rotate(
self.angle,

View file

@ -46,11 +46,13 @@ from ..constants import (
RendererType,
)
from ..mobject.mobject import Group, Mobject
from ..mobject.types.vectorized_mobject import VGroup
from ..utils.paths import path_along_arc, path_along_circles
from ..utils.rate_functions import smooth, squish_rate_func
if TYPE_CHECKING:
from ..scene.scene import Scene
from ..typing import Point3DLike, Point3DLike_Array
class Transform(Animation):
@ -137,12 +139,12 @@ class Transform(Animation):
path_func: Callable | None = None,
path_arc: float = 0,
path_arc_axis: np.ndarray = OUT,
path_arc_centers: np.ndarray = None,
path_arc_centers: Point3DLike | Point3DLike_Array | None = None,
replace_mobject_with_target_in_scene: bool = False,
**kwargs,
) -> None:
self.path_arc_axis: np.ndarray = path_arc_axis
self.path_arc_centers: np.ndarray = path_arc_centers
self.path_arc_centers: Point3DLike | Point3DLike_Array | None = path_arc_centers
self.path_arc: float = path_arc
# path_func is a property a few lines below so it doesn't need to be set in any case
@ -234,8 +236,8 @@ class Transform(Animation):
self.target_copy,
]
if config.renderer == RendererType.OPENGL:
return zip(*(mob.get_family() for mob in mobs))
return zip(*(mob.family_members_with_points() for mob in mobs))
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,
@ -303,7 +305,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)
@ -734,19 +736,36 @@ class CyclicReplace(Transform):
def __init__(
self, *mobjects: Mobject, path_arc: float = 90 * DEGREES, **kwargs
) -> None:
self.group = Group(*mobjects)
if len(mobjects) == 1 and isinstance(mobjects[0], (Group, VGroup)):
self.group = mobjects[0]
else:
self.group = Group(*mobjects)
super().__init__(self.group, path_arc=path_arc, **kwargs)
def create_target(self) -> Group:
def create_target(self) -> Group | VGroup:
target = self.group.copy()
cycled_targets = [target[-1], *target[:-1]]
for m1, m2 in zip(cycled_targets, self.group):
for m1, m2 in zip(cycled_targets, self.group, strict=True):
m1.move_to(m2)
return target
class Swap(CyclicReplace):
pass # Renaming, more understandable for two entries
"""Another name for :class:`~.CyclicReplace`, which is more understandable for two entries.
Examples
--------
.. manim :: SwapExample
class SwapExample(Scene):
def construct(self):
text_a = Text("A").move_to(LEFT)
text_b = Text("B").move_to(RIGHT)
text_group = Group(text_a, text_b)
self.play(FadeIn(text_group))
self.play(Swap(text_group))
self.wait()
"""
# TODO, this may be deprecated...worth reimplementing?
@ -834,7 +853,14 @@ class FadeTransform(Transform):
"""
def __init__(self, mobject, target_mobject, stretch=True, dim_to_match=1, **kwargs):
def __init__(
self,
mobject: Mobject,
target_mobject: Mobject,
stretch: bool = True,
dim_to_match: int = 1,
**kwargs: Any,
):
self.to_add_on_completion = target_mobject
self.stretch = stretch
self.dim_to_match = dim_to_match
@ -928,5 +954,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()):
for sm0, sm1 in zip(source.get_family(), target.get_family(), strict=True):
super().ghost_to(sm0, sm1)

View file

@ -4,12 +4,13 @@ from __future__ import annotations
__all__ = ["TransformMatchingShapes", "TransformMatchingTex"]
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
import numpy as np
from manim.mobject.opengl.opengl_mobject import OpenGLGroup, OpenGLMobject
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup, OpenGLVMobject
from manim.mobject.text.tex_mobject import MathTexPart
from .._config import config
from ..constants import RendererType
@ -74,10 +75,10 @@ class TransformMatchingAbstractBase(AnimationGroup):
transform_mismatches: bool = False,
fade_transform_mismatches: bool = False,
key_map: dict | None = None,
**kwargs,
**kwargs: Any,
):
if isinstance(mobject, OpenGLVMobject):
group_type = OpenGLVGroup
group_type: type[OpenGLVGroup | OpenGLGroup | VGroup | Group] = OpenGLVGroup
elif isinstance(mobject, OpenGLMobject):
group_type = OpenGLGroup
elif isinstance(mobject, VMobject):
@ -141,7 +142,7 @@ class TransformMatchingAbstractBase(AnimationGroup):
self.to_add = target_mobject
def get_shape_map(self, mobject: Mobject) -> dict:
shape_map = {}
shape_map: dict[int | str, VGroup | OpenGLVGroup] = {}
for sm in self.get_mobject_parts(mobject):
key = self.get_mobject_key(sm)
if key not in shape_map:
@ -149,23 +150,25 @@ class TransformMatchingAbstractBase(AnimationGroup):
shape_map[key] = OpenGLVGroup()
else:
shape_map[key] = VGroup()
shape_map[key].add(sm)
# error: Argument 1 to "add" of "OpenGLVGroup" has incompatible type "Mobject"; expected "OpenGLVMobject" [arg-type]
shape_map[key].add(sm) # type: ignore[arg-type]
return shape_map
def clean_up_from_scene(self, scene: Scene) -> None:
# Interpolate all animations back to 0 to ensure source mobjects remain unchanged.
for anim in self.animations:
anim.interpolate(0)
scene.remove(self.mobject)
# error: Argument 1 to "remove" of "Scene" has incompatible type "OpenGLMobject"; expected "Mobject" [arg-type]
scene.remove(self.mobject) # type: ignore[arg-type]
scene.remove(*self.to_remove)
scene.add(self.to_add)
@staticmethod
def get_mobject_parts(mobject: Mobject):
def get_mobject_parts(mobject: Mobject) -> list[Mobject]:
raise NotImplementedError("To be implemented in subclass.")
@staticmethod
def get_mobject_key(mobject: Mobject):
def get_mobject_key(mobject: Mobject) -> int | str:
raise NotImplementedError("To be implemented in subclass.")
@ -205,7 +208,7 @@ class TransformMatchingShapes(TransformMatchingAbstractBase):
transform_mismatches: bool = False,
fade_transform_mismatches: bool = False,
key_map: dict | None = None,
**kwargs,
**kwargs: Any,
):
super().__init__(
mobject,
@ -269,7 +272,7 @@ class TransformMatchingTex(TransformMatchingAbstractBase):
transform_mismatches: bool = False,
fade_transform_mismatches: bool = False,
key_map: dict | None = None,
**kwargs,
**kwargs: Any,
):
super().__init__(
mobject,
@ -294,4 +297,5 @@ class TransformMatchingTex(TransformMatchingAbstractBase):
@staticmethod
def get_mobject_key(mobject: Mobject) -> str:
assert isinstance(mobject, MathTexPart)
return mobject.tex_string

View file

@ -16,7 +16,7 @@ __all__ = [
import inspect
from collections.abc import Callable
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, TypeVar
import numpy as np
@ -29,6 +29,9 @@ if TYPE_CHECKING:
from manim.animation.animation import Animation
M = TypeVar("M", bound=Mobject)
def assert_is_mobject_method(method: Callable) -> None:
assert inspect.ismethod(method)
mobject = method.__self__
@ -43,7 +46,7 @@ def always(method: Callable, *args, **kwargs) -> Mobject:
return mobject
def f_always(method: Callable[[Mobject], None], *arg_generators, **kwargs) -> Mobject:
def f_always(method: Callable[[M], None], *arg_generators, **kwargs) -> M:
"""
More functional version of always, where instead
of taking in args, it takes in functions which output
@ -61,7 +64,7 @@ def f_always(method: Callable[[Mobject], None], *arg_generators, **kwargs) -> Mo
return mobject
def always_redraw(func: Callable[[], Mobject]) -> Mobject:
def always_redraw(func: Callable[[], M]) -> M:
"""Redraw the mobject constructed by a function every frame.
This function returns a mobject with an attached updater that
@ -107,8 +110,8 @@ def always_redraw(func: Callable[[], Mobject]) -> Mobject:
def always_shift(
mobject: Mobject, direction: np.ndarray[np.float64] = RIGHT, rate: float = 0.1
) -> Mobject:
mobject: M, direction: np.ndarray[np.float64] = RIGHT, rate: float = 0.1
) -> M:
"""A mobject which is continuously shifted along some direction
at a certain rate.
@ -145,7 +148,7 @@ def always_shift(
return mobject
def always_rotate(mobject: Mobject, rate: float = 20 * DEGREES, **kwargs) -> Mobject:
def always_rotate(mobject: M, rate: float = 20 * DEGREES, **kwargs) -> M:
"""A mobject which is continuously rotated at a certain rate.
Parameters
@ -214,6 +217,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

View file

@ -10,37 +10,36 @@ import operator as op
import pathlib
from collections.abc import Callable, Iterable
from functools import reduce
from typing import TYPE_CHECKING, Any
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 typing_extensions import Self
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 = {
@ -757,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):
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
@ -999,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(
@ -1128,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
@ -1150,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

View file

@ -10,7 +10,7 @@ from __future__ import annotations
__all__ = ["MovingCamera"]
from collections.abc import Iterable
from typing import Any
from typing import Any, Literal, overload
from cairo import Context
@ -20,7 +20,7 @@ from .. import config
from ..camera.camera import Camera
from ..constants import DOWN, LEFT, RIGHT, UP
from ..mobject.frame import ScreenRectangle
from ..mobject.mobject import Mobject
from ..mobject.mobject import Mobject, _AnimationBuilder
from ..utils.color import WHITE, ManimColor
@ -166,13 +166,31 @@ class MovingCamera(Camera):
"""
return [self.frame]
@overload
def auto_zoom(
self,
mobjects: Iterable[Mobject],
margin: float,
only_mobjects_in_frame: bool,
animate: Literal[False],
) -> Mobject: ...
@overload
def auto_zoom(
self,
mobjects: Iterable[Mobject],
margin: float,
only_mobjects_in_frame: bool,
animate: Literal[True],
) -> _AnimationBuilder: ...
def auto_zoom(
self,
mobjects: Iterable[Mobject],
margin: float = 0,
only_mobjects_in_frame: bool = False,
animate: bool = True,
) -> Mobject:
) -> _AnimationBuilder | Mobject:
"""Zooms on to a given array of mobjects (or a singular mobject)
and automatically resizes to frame all the mobjects.

View file

@ -6,9 +6,7 @@ __all__ = ["MultiCamera"]
from collections.abc import Iterable
from typing import Any
from typing_extensions import Self
from typing import Any, Self
from manim.mobject.mobject import Mobject
from manim.mobject.types.image_mobject import ImageMobjectFromCamera

View file

@ -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"

View file

@ -43,11 +43,10 @@ def select_resolution() -> tuple[int, int]:
tuple[int, int]
Tuple containing height and width.
"""
resolution_options: list[tuple[int, int]] = []
for quality in QUALITIES.items():
resolution_options.append(
(quality[1]["pixel_height"], quality[1]["pixel_width"]),
)
resolution_options: list[tuple[int, int]] = [
(quality[1]["pixel_height"], quality[1]["pixel_width"])
for quality in QUALITIES.items()
]
resolution_options.pop()
choice = click.prompt(
"\nSelect resolution:\n",

View file

@ -144,4 +144,10 @@ global_options = option_group(
help="The command used to preview the output file (for example vlc for video files)",
default=None,
),
option(
"--seed",
type=int,
help="Set the random seed to allow reproducibility.",
default=None,
),
)

View file

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

View file

@ -45,10 +45,9 @@ __all__ = [
import itertools
import warnings
from typing import TYPE_CHECKING, Any, cast
from typing import TYPE_CHECKING, Any, Self, cast
import numpy as np
from typing_extensions import Self
from manim.constants import *
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
@ -102,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
@ -129,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
@ -202,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
@ -216,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:
@ -242,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:
@ -505,9 +506,10 @@ class TangentialArc(ArcBetweenPoints):
You can choose any of the 4 possible corner arcs via the `corner` tuple.
corner = (s1, s2) where each si is ±1 to control direction along each line.
Example
-------
Examples
--------
.. manim:: TangentialArcExample
:save_last_frame:
class TangentialArcExample(Scene):
def construct(self):
@ -518,6 +520,32 @@ class TangentialArc(ArcBetweenPoints):
arc = TangentialArc(line1, line2, radius=2.25, corner=(1, 1), color=TEAL)
self.add(arc, line1, line2)
The following example shows all four possible corner configurations:
.. manim:: TangentialArcCorners
:save_last_frame:
class TangentialArcCorners(Scene):
def construct(self):
# Create two intersecting lines
line1 = DashedLine(start=3 * LEFT, end=3 * RIGHT, color=GREY)
line2 = DashedLine(start=3 * UP, end=3 * DOWN, color=GREY)
# All four corner configurations with different colors
arc_pp = TangentialArc(line1, line2, radius=1.5, corner=(1, 1), color=RED)
arc_pn = TangentialArc(line1, line2, radius=1.5, corner=(1, -1), color=GREEN)
arc_np = TangentialArc(line1, line2, radius=1.5, corner=(-1, 1), color=BLUE)
arc_nn = TangentialArc(line1, line2, radius=1.5, corner=(-1, -1), color=YELLOW)
# Labels for each arc
label_pp = Text("(1,1)", font_size=24, color=RED).next_to(arc_pp, UR, buff=0.1)
label_pn = Text("(1,-1)", font_size=24, color=GREEN).next_to(arc_pn, DR, buff=0.1)
label_np = Text("(-1,1)", font_size=24, color=BLUE).next_to(arc_np, UL, buff=0.1)
label_nn = Text("(-1,-1)", font_size=24, color=YELLOW).next_to(arc_nn, DL, buff=0.1)
self.add(line1, line2, arc_pp, arc_pn, arc_np, arc_nn)
self.add(label_pp, label_pn, label_np, label_nn)
"""
def __init__(
@ -708,8 +736,7 @@ class Circle(Arc):
self.add(circle, s1, s2)
"""
start_angle = angle_of_vector(self.points[0] - self.get_center())
proportion = (angle - start_angle) / TAU
proportion = angle / TAU
proportion -= np.floor(proportion)
return self.point_from_proportion(proportion)
@ -1206,7 +1233,7 @@ class ArcPolygon(VMobject, metaclass=ConvertToOpenGL):
arcs = [
ArcBetweenPoints(*pair, **conf)
for (pair, conf) in zip(point_pairs, all_arc_configs)
for (pair, conf) in zip(point_pairs, all_arc_configs, strict=True)
]
super().__init__(**kwargs)

View file

@ -182,9 +182,9 @@ class Union(_BooleanOps):
if len(vmobjects) < 2:
raise ValueError("At least 2 mobjects needed for Union.")
super().__init__(**kwargs)
paths = []
for vmobject in vmobjects:
paths.append(self._convert_vmobject_to_skia_path(vmobject))
paths = [
self._convert_vmobject_to_skia_path(vmobject) for vmobject in vmobjects
]
outpen = SkiaPath()
union(paths, outpen.getPen())
self._convert_skia_path_to_vmobject(outpen)

View file

@ -15,14 +15,15 @@ from manim.mobject.geometry.shape_matchers import (
BackgroundRectangle,
SurroundingRectangle,
)
from manim.mobject.text.tex_mobject import MathTex, Tex
from manim.mobject.text.tex_mobject import MathTex
from manim.mobject.text.text_mobject import Text
from manim.mobject.text.typst_mobject import Typst
from manim.mobject.types.vectorized_mobject import VGroup
from manim.utils.color import WHITE
from manim.utils.polylabel import polylabel
if TYPE_CHECKING:
from manim.typing import Point3DLike_Array
from manim.typing import ManimTextLabel, Point3DLike_Array
class Label(VGroup):
@ -61,7 +62,7 @@ class Label(VGroup):
def __init__(
self,
label: str | Tex | MathTex | Text,
label: str | ManimTextLabel,
label_config: dict[str, Any] | None = None,
box_config: dict[str, Any] | None = None,
frame_config: dict[str, Any] | None = None,
@ -94,13 +95,15 @@ class Label(VGroup):
frame_config = default_frame_config | (frame_config or {})
# Determine the type of label and instantiate the appropriate object
self.rendered_label: MathTex | Tex | Text
self.rendered_label: ManimTextLabel
if isinstance(label, str):
self.rendered_label = MathTex(label, **label_config)
elif isinstance(label, (MathTex, Tex, Text)):
elif isinstance(label, (MathTex, Text, Typst)):
self.rendered_label = label
else:
raise TypeError("Unsupported label type. Must be MathTex, Tex, or Text.")
raise TypeError(
"Unsupported label type. Must be MathTex, Tex, Text, Typst, or TypstMath."
)
# Add a background box
self.background_rect = BackgroundRectangle(self.rendered_label, **box_config)
@ -155,7 +158,7 @@ class LabeledLine(Line):
def __init__(
self,
label: str | Tex | MathTex | Text,
label: str | ManimTextLabel,
label_position: float = 0.5,
label_config: dict[str, Any] | None = None,
box_config: dict[str, Any] | None = None,
@ -343,7 +346,7 @@ class LabeledPolygram(Polygram):
def __init__(
self,
*vertex_groups: Point3DLike_Array,
label: str | Tex | MathTex | Text,
label: str | ManimTextLabel,
precision: float = 0.01,
label_config: dict[str, Any] | None = None,
box_config: dict[str, Any] | None = None,

View file

@ -14,14 +14,14 @@ __all__ = [
"RightAngle",
]
from typing import TYPE_CHECKING, Any, Literal
from typing import TYPE_CHECKING, Any, Literal, cast
import numpy as np
from manim import config
from manim.constants import *
from manim.mobject.geometry.arc import Arc, ArcBetweenPoints, Dot, TipableVMobject
from manim.mobject.geometry.tips import ArrowTriangleFilledTip
from manim.mobject.geometry.tips import ArrowTip, ArrowTriangleFilledTip
from manim.mobject.mobject import Mobject
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
@ -30,7 +30,7 @@ from manim.utils.color import WHITE
from manim.utils.space_ops import angle_of_vector, line_intersection, normalize
if TYPE_CHECKING:
from typing_extensions import Self, TypeAlias
from typing import Self, TypeAlias
from manim.typing import Point3D, Point3DLike, Vector2DLike, Vector3D, Vector3DLike
from manim.utils.color import ParsableManimColor
@ -648,9 +648,11 @@ class Arrow(Line):
self._set_stroke_width_from_length()
if has_tip:
self.add_tip(tip=old_tips[0])
# error: Argument "tip" to "add_tip" of "TipableVMobject" has incompatible type "VMobject"; expected "ArrowTip | None" [arg-type]
self.add_tip(tip=cast(ArrowTip, old_tips[0]))
if has_start_tip:
self.add_tip(tip=old_tips[1], at_start=True)
# error: Argument "tip" to "add_tip" of "TipableVMobject" has incompatible type "VMobject"; expected "ArrowTip | None" [arg-type]
self.add_tip(tip=cast(ArrowTip, old_tips[1]), at_start=True)
return self
def get_normal_vector(self) -> Vector3D:

View file

@ -32,8 +32,9 @@ from manim.utils.qhull import QuickHull
from manim.utils.space_ops import angle_between_vectors, normalize, regular_vertices
if TYPE_CHECKING:
from typing import Self
import numpy.typing as npt
from typing_extensions import Self
from manim.typing import (
Point3D,
@ -150,7 +151,9 @@ class Polygram(VMobject, metaclass=ConvertToOpenGL):
# TODO: If any of the original vertex groups contained the starting vertex N
# 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()):
for start, end in zip(
self.get_start_anchors(), self.get_end_anchors(), strict=True
):
group.append(start)
if self.consider_points_equals(end, group[0]):
@ -237,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)
radius_list, adjacent_n_tuples(vertex_group, 3), strict=True
):
vect1 = v2 - v1
vect2 = v3 - v2
@ -549,7 +552,7 @@ class Star(Polygon):
)
vertices: list[npt.NDArray] = []
for pair in zip(outer_vertices, inner_vertices):
for pair in zip(outer_vertices, inner_vertices, strict=True):
vertices.extend(pair)
super().__init__(*vertices, **kwargs)

View file

@ -4,9 +4,7 @@ from __future__ import annotations
__all__ = ["SurroundingRectangle", "BackgroundRectangle", "Cross", "Underline"]
from typing import Any
from typing_extensions import Self
from typing import Any, Self
from manim import logger
from manim._config import config
@ -22,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):
@ -52,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,
@ -151,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.

View file

@ -16,7 +16,7 @@ import networkx as nx
import numpy as np
if TYPE_CHECKING:
from typing_extensions import TypeAlias
from typing import TypeAlias
from manim.scene.scene import Scene
from manim.typing import Point3D, Point3DLike
@ -588,9 +588,7 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
self._labels = labels
elif isinstance(labels, bool):
if labels:
self._labels = {
v: MathTex(v, fill_color=label_fill_color) for v in vertices
}
self._labels = {v: MathTex(v, color=label_fill_color) for v in vertices}
else:
self._labels = {}
@ -671,8 +669,30 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
"""Helper method for populating the edges of the graph."""
raise NotImplementedError("To be implemented in concrete subclasses")
def __getitem__(self: Graph, v: Hashable) -> Mobject:
return self.vertices[v]
def __getitem__(self: Graph, k: Hashable | tuple[Hashable, Hashable]) -> Mobject:
"""Get a vertex or edge by its name/identifier.
Parameters
----------
k
A vertex name (hashable) or an edge tuple ``(u, v)``.
Returns
-------
Mobject
The :class:`~.Mobject` corresponding to the given vertex or edge.
Raises
------
KeyError
If ``k`` is not a valid vertex or edge.
"""
if k in self.vertices:
return self.vertices[k]
elif k in self.edges:
return self.edges[k]
else:
raise ValueError(f"Could not find {k} in vertices or edges")
def _create_vertex(
self,
@ -697,7 +717,7 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
)
if label is True:
label = MathTex(vertex, fill_color=label_fill_color)
label = MathTex(vertex, color=label_fill_color)
elif vertex in self._labels:
label = self._labels[vertex]
elif not isinstance(label, (Mobject, OpenGLMobject)):
@ -1021,10 +1041,7 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
"""
if edge_config is None:
edge_config = self.default_edge_config.copy()
added_mobjects = []
for v in edge:
if v not in self.vertices:
added_mobjects.append(self._add_vertex(v))
added_mobjects = [self._add_vertex(v) for v in edge if v not in self.vertices]
u, v = edge
self._graph.add_edge(u, v)
@ -1347,6 +1364,11 @@ class Graph(GenericGraph):
g[2].animate.move_to([-1, 1, 0]),
g[3].animate.move_to([1, -1, 0]),
g[4].animate.move_to([-1, -1, 0]))
self.play(LaggedStart(Wiggle(g[(1, 2)]),
Wiggle(g[(2, 3)]),
Wiggle(g[(3, 4)]),
Wiggle(g[(1, 3)]),
Wiggle(g[(1, 4)])))
self.wait()
There are several automatic positioning algorithms to choose from:

View file

@ -14,10 +14,9 @@ __all__ = [
import fractions as fr
import numbers
from collections.abc import Callable, Iterable, Sequence
from typing import TYPE_CHECKING, Any, TypeVar, overload
from typing import TYPE_CHECKING, Any, Self, TypeVar, overload
import numpy as np
from typing_extensions import Self
from manim import config
from manim.constants import *
@ -43,8 +42,8 @@ from manim.utils.color import (
BLUE,
BLUE_D,
GREEN,
PURE_YELLOW,
WHITE,
YELLOW,
ManimColor,
ParsableManimColor,
color_gradient,
@ -438,14 +437,20 @@ class CoordinateSystem:
if not axes_numbers:
axes_numbers = [None for _ in range(self.dimension)]
for axis, values in zip(self.axes, axes_numbers):
for axis, values in zip(self.axes, axes_numbers, strict=False):
if isinstance(values, dict):
axis.add_labels(values, **kwargs)
labels = axis.labels
elif values is None and axis.scaling.custom_labels:
tick_range = axis.get_tick_range()
axis.add_labels(
dict(zip(tick_range, axis.scaling.get_custom_labels(tick_range)))
dict(
zip(
tick_range,
axis.scaling.get_custom_labels(tick_range),
strict=True,
)
)
)
labels = axis.labels
else:
@ -1295,7 +1300,7 @@ class CoordinateSystem:
colors = color_gradient(color, len(x_range_array))
for x, color in zip(x_range_array, colors):
for x, color in zip(x_range_array, colors, strict=True):
if input_sample_type == "left":
sample_input = x
elif input_sample_type == "right":
@ -1609,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,
@ -1791,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.
@ -2031,7 +2036,9 @@ class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
)
)
"""
for default_config, passed_config in zip(default_configs, passed_configs):
for default_config, passed_config in zip(
default_configs, passed_configs, strict=False
):
if passed_config is not None:
update_dict_recursively(default_config, passed_config)
@ -2082,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
@ -2110,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:
@ -2152,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.
@ -2161,7 +2180,7 @@ class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
# Although "points" and "nums" are in plural, there might be a single point or number.
points = self.x_axis.number_to_point(coords[0])
other_axes = self.axes.submobjects[1:]
for axis, nums in zip(other_axes, coords[1:]):
for axis, nums in zip(other_axes, coords[1:], strict=False):
points += axis.number_to_point(nums) - origin
# Return points as is, except if coords originally looked like
@ -2286,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,
@ -2355,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)
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
@ -3238,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(

View file

@ -17,14 +17,12 @@ from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from manim.mobject.types.vectorized_mobject import VMobject
if TYPE_CHECKING:
from typing import Any
from typing_extensions import Self
from typing import Any, Self
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):
@ -159,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]):
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)),
@ -219,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:

View file

@ -8,16 +8,14 @@ from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
__all__ = ["NumberLine", "UnitInterval"]
from collections.abc import Callable, Iterable, Sequence
from typing import TYPE_CHECKING
from collections.abc import Iterable, Sequence
from typing import TYPE_CHECKING, Any, cast
if TYPE_CHECKING:
from typing import Any
from typing_extensions import Self
from typing import Self
from manim.mobject.geometry.tips import ArrowTip
from manim.typing import Point3D, Point3DLike, Vector3D
from manim.typing import ManimTextLabel, Point3D, Point3DLike, Vector3D
import numpy as np
@ -25,9 +23,9 @@ from manim import config
from manim.constants import *
from manim.mobject.geometry.line import Line
from manim.mobject.graphing.scale import LinearBase, _ScaleBase
from manim.mobject.text.numbers import DecimalNumber, Integer
from manim.mobject.text.tex_mobject import MathTex, Tex
from manim.mobject.text.text_mobject import Text
from manim.mobject.text.numbers import DecimalNumber
from manim.mobject.text.tex_mobject import MathTex, SingleStringMathTex, Tex
from manim.mobject.text.typst_mobject import Typst, TypstMath
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
from manim.utils.bezier import interpolate
from manim.utils.config_ops import merge_dicts_recursively
@ -163,7 +161,7 @@ class NumberLine(Line):
include_numbers: bool = False,
font_size: float = 36,
label_direction: Point3DLike = DOWN,
label_constructor: type[MathTex] = MathTex,
label_constructor: type[ManimTextLabel] = MathTex,
scaling: _ScaleBase = LinearBase(),
line_to_number_buff: float = MED_SMALL_BUFF,
decimal_number_config: dict | None = None,
@ -264,6 +262,7 @@ class NumberLine(Line):
zip(
tick_range,
custom_labels,
strict=True,
)
),
)
@ -451,7 +450,7 @@ class NumberLine(Line):
direction: Vector3D | None = None,
buff: float | None = None,
font_size: float | None = None,
label_constructor: type[MathTex] | None = None,
label_constructor: type[SingleStringMathTex] | None = None,
**number_config: dict[str, Any],
) -> VMobject:
"""Generates a positioned :class:`~.DecimalNumber` mobject
@ -488,7 +487,7 @@ class NumberLine(Line):
if font_size is None:
font_size = self.font_size
if label_constructor is None:
label_constructor = self.label_constructor
label_constructor = cast(type[SingleStringMathTex], self.label_constructor)
num_mob = DecimalNumber(
x,
@ -516,7 +515,7 @@ class NumberLine(Line):
x_values: Iterable[float] | None = None,
excluding: Iterable[float] | None = None,
font_size: float | None = None,
label_constructor: type[MathTex] | None = None,
label_constructor: type[SingleStringMathTex] | None = None,
**kwargs: Any,
) -> Self:
"""Adds :class:`~.DecimalNumber` mobjects representing their position
@ -548,7 +547,7 @@ class NumberLine(Line):
font_size = self.font_size
if label_constructor is None:
label_constructor = self.label_constructor
label_constructor = cast(type[SingleStringMathTex], self.label_constructor)
numbers = VGroup()
for x in x_values:
@ -572,7 +571,7 @@ class NumberLine(Line):
direction: Point3DLike | None = None,
buff: float | None = None,
font_size: float | None = None,
label_constructor: type[MathTex] | None = None,
label_constructor: type[ManimTextLabel] | None = None,
) -> Self:
"""Adds specifically positioned labels to the :class:`~.NumberLine` using a ``dict``.
The labels can be accessed after creation via ``self.labels``.
@ -610,14 +609,18 @@ class NumberLine(Line):
# TODO: remove this check and ability to call
# this method via CoordinateSystem.add_coordinates()
# must be explicitly called
if isinstance(label, str) and label_constructor is MathTex:
label = Tex(label)
if isinstance(label, str):
if label_constructor is MathTex:
label = Tex(label)
elif label_constructor is TypstMath:
label = Typst(label)
else:
label = self._create_label_tex(label, label_constructor)
else:
label = self._create_label_tex(label, label_constructor)
if hasattr(label, "font_size"):
assert isinstance(label, (MathTex, Tex, Text, Integer)), label
label.font_size = font_size
cast(Any, label).font_size = font_size
else:
raise AttributeError(f"{label} is not compatible with add_labels.")
label.next_to(self.number_to_point(x), direction=direction, buff=buff)
@ -630,7 +633,7 @@ class NumberLine(Line):
def _create_label_tex(
self,
label_tex: str | float | VMobject,
label_constructor: Callable | None = None,
label_constructor: type[ManimTextLabel] | None = None,
**kwargs: Any,
) -> VMobject:
"""Checks if the label is a :class:`~.VMobject`, otherwise, creates a

View file

@ -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):
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)
@ -151,7 +151,7 @@ class SampleSpace(Rectangle):
) -> VGroup:
label_mobs = VGroup()
braces = VGroup()
for label, part in zip(labels, parts):
for label, part in zip(labels, parts, strict=False):
brace = Brace(part, direction, min_num_quads=min_num_quads, buff=buff)
if isinstance(label, (VMobject, OpenGLVMobject)):
label_mob = label
@ -207,13 +207,11 @@ class SampleSpace(Rectangle):
if hasattr(parts, subattr):
self.add(getattr(parts, subattr))
def __getitem__(self, index: int) -> SampleSpace:
def __getitem__(self, index: int) -> VMobject:
if hasattr(self, "horizontal_parts"):
val: SampleSpace = self.horizontal_parts[index]
return val
return self.horizontal_parts[index]
elif hasattr(self, "vertical_parts"):
val = self.vertical_parts[index]
return val
return self.vertical_parts[index]
return self.split()[index]
@ -367,11 +365,13 @@ class BarChart(Axes):
labels = VGroup()
for i, (value, bar_name) in enumerate(zip(val_range, self.bar_names)):
for i, (value, bar_name) in enumerate(
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
direction = UP if self.values[i] < 0 else DOWN
bar_name_label: MathTex = self.x_axis.label_constructor(bar_name)
bar_name_label = self.x_axis.label_constructor(bar_name)
bar_name_label.font_size = self.x_axis.font_size
bar_name_label.next_to(
@ -463,7 +463,7 @@ class BarChart(Axes):
self.add(chart, c_bar_lbls)
"""
bar_labels = VGroup()
for bar, value in zip(self.bars, self.values):
for bar, value in zip(self.bars, self.values, strict=False):
bar_lbl: MathTex = label_constructor(str(value))
if color is None:
@ -511,7 +511,7 @@ class BarChart(Axes):
chart.change_bar_values(list(reversed(values)))
self.add(chart.get_bar_labels(font_size=24))
"""
for i, (bar, value) in enumerate(zip(self.bars, values)):
for i, (bar, value) in enumerate(zip(self.bars, values, strict=False)):
chart_val = self.values[i]
if chart_val > 0:

View file

@ -11,7 +11,7 @@ __all__ = ["LogBase", "LinearBase"]
from manim.mobject.text.numbers import Integer
if TYPE_CHECKING:
from typing import Callable
from collections.abc import Callable
from manim.mobject.types.vectorized_mobject import VMobject

View file

@ -40,16 +40,15 @@ __all__ = [
import itertools as it
from collections.abc import Callable, Iterable, Sequence
from typing import Any
from collections.abc import Callable, Iterable
from typing import Any, Self
import numpy as np
from typing_extensions import Self
from manim.mobject.mobject import Mobject
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from manim.mobject.text.numbers import DecimalNumber, Integer
from manim.mobject.text.tex_mobject import MathTex, Tex
from manim.typing import Vector2DLike, Vector3DLike
from ..constants import *
from ..mobject.types.vectorized_mobject import VGroup, VMobject
@ -165,16 +164,16 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
def __init__(
self,
matrix: Iterable,
matrix: Iterable[Iterable[Any] | Vector2DLike],
v_buff: float = 0.8,
h_buff: float = 1.3,
bracket_h_buff: float = MED_SMALL_BUFF,
bracket_v_buff: float = MED_SMALL_BUFF,
add_background_rectangles_to_entries: bool = False,
include_background_rectangle: bool = False,
element_to_mobject: type[Mobject] | Callable[..., Mobject] = MathTex,
element_to_mobject_config: dict = {},
element_alignment_corner: Sequence[float] = DR,
element_to_mobject: type[VMobject] | Callable[..., VMobject] = MathTex,
element_to_mobject_config: dict[str, Any] = {},
element_alignment_corner: Vector3DLike = DR,
left_bracket: str = "[",
right_bracket: str = "]",
stretch_brackets: bool = True,
@ -207,7 +206,9 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
if self.include_background_rectangle:
self.add_background_rectangle()
def _matrix_to_mob_matrix(self, matrix: np.ndarray) -> list[list[Mobject]]:
def _matrix_to_mob_matrix(
self, matrix: Iterable[Iterable[Any]]
) -> list[list[VMobject]]:
return [
[
self.element_to_mobject(item, **self.element_to_mobject_config)
@ -216,7 +217,7 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
for row in matrix
]
def _organize_mob_matrix(self, matrix: list[list[Mobject]]) -> Self:
def _organize_mob_matrix(self, matrix: list[list[VMobject]]) -> Self:
for i, row in enumerate(matrix):
for j, _ in enumerate(row):
mob = matrix[i][j]
@ -333,7 +334,7 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
self.add(m0)
"""
columns = self.get_columns()
for color, column in zip(colors, columns):
for color, column in zip(colors, columns, strict=False):
column.set_color(color)
return self
@ -385,7 +386,7 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
self.add(m0)
"""
rows = self.get_rows()
for color, row in zip(colors, rows):
for color, row in zip(colors, rows, strict=False):
row.set_color(color)
return self
@ -402,7 +403,7 @@ class Matrix(VMobject, metaclass=ConvertToOpenGL):
mob.add_background_rectangle()
return self
def get_mob_matrix(self) -> list[list[Mobject]]:
def get_mob_matrix(self) -> list[list[VMobject]]:
"""Return the underlying mob matrix mobjects.
Returns
@ -484,8 +485,8 @@ class DecimalMatrix(Matrix):
def __init__(
self,
matrix: Iterable,
element_to_mobject: type[Mobject] = DecimalNumber,
matrix: Iterable[Iterable[Any]],
element_to_mobject: type[VMobject] | Callable[..., VMobject] = DecimalNumber,
element_to_mobject_config: dict[str, Any] = {"num_decimal_places": 1},
**kwargs: Any,
):
@ -529,8 +530,8 @@ class IntegerMatrix(Matrix):
def __init__(
self,
matrix: Iterable,
element_to_mobject: type[Mobject] = Integer,
matrix: Iterable[Iterable[Any]],
element_to_mobject: type[VMobject] | Callable[..., VMobject] = Integer,
**kwargs: Any,
):
"""
@ -567,8 +568,8 @@ class MobjectMatrix(Matrix):
def __init__(
self,
matrix: Iterable,
element_to_mobject: type[Mobject] | Callable[..., Mobject] = lambda m: m,
matrix: Iterable[Iterable[Any]],
element_to_mobject: type[VMobject] | Callable[..., VMobject] = lambda m: m,
**kwargs: Any,
):
super().__init__(matrix, element_to_mobject=element_to_mobject, **kwargs)

File diff suppressed because it is too large Load diff

View file

@ -2,21 +2,20 @@ from __future__ import annotations
__all__ = ["TrueDot", "DotCloud"]
from typing import Any
from typing import Any, Self
import numpy as np
from typing_extensions import Self
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,

View file

@ -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)

View file

@ -1,9 +1,8 @@
from __future__ import annotations
from typing import Any, cast
from typing import Any, Self, cast
import numpy as np
from typing_extensions import Self
from manim.constants import *
from manim.mobject.mobject import Mobject
@ -108,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
@ -161,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:

View file

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

View file

@ -6,19 +6,27 @@ import itertools as it
import random
import sys
import types
import warnings
from collections.abc import Callable, Iterable, Iterator, Sequence
from functools import partialmethod, wraps
from math import ceil
from typing import TYPE_CHECKING, Any, ClassVar, Protocol, TypeVar, cast
from typing import (
TYPE_CHECKING,
Any,
ClassVar,
Never,
Protocol,
Self,
TypeAlias,
TypeVar,
cast,
overload,
)
import moderngl
import numpy as np
import numpy.typing as npt
from typing_extensions import (
Never,
Self,
TypeAlias,
overload,
override,
)
@ -1048,7 +1056,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:]):
for m1, m2 in zip(self.submobjects[:-1], self.submobjects[1:], strict=True):
m2.next_to(m1, direction, **kwargs)
if center:
self.center()
@ -1215,7 +1223,7 @@ class OpenGLMobject:
) -> Sequence[Vector3D]:
if str_alignments is None:
# Use cell_alignment as fallback
return [cast(Vector3D, cell_alignment * direction)] * num
return [cast("Vector3D", cell_alignment * direction)] * num
if len(str_alignments) != num:
raise ValueError(f"{name}_alignments has a mismatching size.")
return [mapping[letter] for letter in str_alignments]
@ -2127,26 +2135,33 @@ class OpenGLMobject:
return self
def put_start_and_end_on(self, start: Point3DLike, end: Point3DLike) -> Self:
curr_start, curr_end = self.get_start_and_end()
curr_vect = curr_end - curr_start
if np.all(curr_vect == 0):
raise Exception("Cannot position endpoints of closed loop")
target_vect = np.array(end) - np.array(start)
current_start, current_end = self.get_start_and_end()
current_vector = current_end - current_start
if np.all(current_vector == 0):
warnings.warn(
"put_start_and_end_on has been called on a closed loop or zero-length mobject. "
f"{type(self).__name__} will be shifted to start point instead.",
stacklevel=2,
)
self.shift(np.asarray(start) - current_start)
return self
target_vector = np.asarray(end) - np.asarray(start)
axis = (
normalize(np.cross(curr_vect, target_vect))
if np.linalg.norm(np.cross(curr_vect, target_vect)) != 0
normalize(np.cross(current_vector, target_vector))
if np.linalg.norm(np.cross(current_vector, target_vector)) != 0
else OUT
)
self.scale(
float(np.linalg.norm(target_vect) / np.linalg.norm(curr_vect)),
about_point=curr_start,
np.linalg.norm(target_vector) / np.linalg.norm(current_vector),
about_point=current_start,
)
self.rotate(
angle_between_vectors(curr_vect, target_vect),
about_point=curr_start,
angle_between_vectors(current_vector, target_vector),
about_point=current_start,
axis=axis,
)
self.shift(start - curr_start)
self.shift(np.asarray(start) - current_start)
return self
# Color functions
@ -2183,7 +2198,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))]
[[*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()
@ -2259,7 +2274,7 @@ class OpenGLMobject:
mobs = self.submobjects
new_colors = color_gradient(colors, len(mobs))
for mob, color in zip(mobs, new_colors):
for mob, color in zip(mobs, new_colors, strict=True):
mob.set_color(color)
return self
@ -2474,7 +2489,7 @@ class OpenGLMobject:
return OpenGLGroup(
*(
template.copy().pointwise_become_partial(self, a1, a2)
for a1, a2 in zip(alphas[:-1], alphas[1:])
for a1, a2 in zip(alphas[:-1], alphas[1:], strict=True)
)
)
@ -2575,7 +2590,7 @@ class OpenGLMobject:
def align_data(self, mobject: OpenGLMobject) -> Self:
# In case any data arrays get resized when aligned to shader data
# self.refresh_shader_data()
for mob1, mob2 in zip(self.get_family(), mobject.get_family()):
for mob1, mob2 in zip(self.get_family(), mobject.get_family(), strict=False):
# Separate out how points are treated so that subclasses
# can handle that case differently if they choose
mob1.align_points(mob2)
@ -2605,7 +2620,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):
for sm1, sm2 in zip(mob1.submobjects, mob2.submobjects, strict=True):
sm1.align_family(sm2)
return self
@ -2631,7 +2646,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):
for submob, sf in zip(self.submobjects, split_factors, strict=True):
new_submobs.append(submob)
for _ in range(1, sf):
new_submob = submob.copy()
@ -2773,7 +2788,7 @@ class OpenGLMobject:
mobject.move_to(self.get_center())
self.align_family(mobject)
for sm1, sm2 in zip(self.get_family(), mobject.get_family()):
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)
@ -2802,6 +2817,7 @@ class OpenGLMobject:
self.get_family(),
mobject1.get_family(),
mobject2.get_family(),
strict=False,
):
keys = sm.data.keys() & sm1.data.keys() & sm2.data.keys()
sm.lock_data(
@ -3029,7 +3045,7 @@ class OpenGLPoint(OpenGLMobject):
return self.artificial_height
def get_location(self) -> Point3D:
return cast(Point3D, self.points[0]).copy()
return cast("Point3D", self.points[0]).copy()
@override
def get_bounding_box_point(self, *args: object, **kwargs: Any) -> Point3D:

View file

@ -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,
@ -22,7 +22,7 @@ from manim.utils.config_ops import _Uniforms
from manim.utils.iterables import resize_with_interpolation
if TYPE_CHECKING:
from typing_extensions import Self
from typing import Self
from manim.typing import (
FloatRGBA_Array,
@ -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:

View file

@ -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)

View file

@ -4,11 +4,10 @@ import itertools as it
import operator as op
from collections.abc import Callable, Iterable, Sequence
from functools import reduce, wraps
from typing import Any
from typing import Any, Self
import moderngl
import numpy as np
from typing_extensions import Self
from manim import config
from manim.constants import *
@ -351,7 +350,7 @@ class OpenGLVMobject(OpenGLMobject):
return self
elif len(submobs2) == 0:
submobs2 = [vmobject]
for sm1, sm2 in zip(*make_even(submobs1, submobs2)):
for sm1, sm2 in zip(*make_even(submobs1, submobs2), strict=True):
sm1.match_style(sm2)
return self
@ -581,7 +580,7 @@ class OpenGLVMobject(OpenGLMobject):
new_points.extend(
[
partial_bezier_points(tup, a1, a2)
for a1, a2 in zip(alphas, alphas[1:])
for a1, a2 in zip(alphas[:-1], alphas[1:], strict=True)
],
)
else:
@ -770,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:])
for i1, i2 in zip(split_indices[:-1], split_indices[1:], strict=True)
if (i2 - i1) >= nppc
]
@ -1094,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)))
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
@ -1228,7 +1227,9 @@ class OpenGLVMobject(OpenGLMobject):
def get_nth_subpath(path_list, n):
if n >= len(path_list):
# Create a null path at the very end
return [path_list[-1][-1]] * nppc
if len(path_list) == 0:
return np.tile(np.zeros(3), (nppc, 1))
return np.tile(path_list[-1][-1], (nppc, 1))
path = path_list[n]
# Check for useless points at the end of the path and remove them
# https://github.com/ManimCommunity/manim/issues/1959

View file

@ -4,11 +4,10 @@ from __future__ import annotations
__all__ = ["Brace", "BraceLabel", "ArcBrace", "BraceText", "BraceBetweenPoints"]
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Self
import numpy as np
import svgelements as se
from typing_extensions import Self
from manim._config import config
from manim.mobject.geometry.arc import Arc

View file

@ -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:

View file

@ -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
@ -104,6 +104,8 @@ class Table(VGroup):
Horizontal buffer passed to :meth:`~.Mobject.arrange_in_grid`, by default 1.3.
include_outer_lines
``True`` if the table should include outer lines, by default False.
include_inner_lines
``True`` if the table should include inner lines, by default True.
add_background_rectangles_to_entries
``True`` if background rectangles should be added to entries, by default ``False``.
entries_background_color
@ -193,6 +195,7 @@ class Table(VGroup):
v_buff: float = 0.8,
h_buff: float = 1.3,
include_outer_lines: bool = False,
include_inner_lines: bool = True,
add_background_rectangles_to_entries: bool = False,
entries_background_color: ParsableManimColor = BLACK,
include_background_rectangle: bool = False,
@ -214,6 +217,7 @@ class Table(VGroup):
self.v_buff = v_buff
self.h_buff = h_buff
self.include_outer_lines = include_outer_lines
self.include_inner_lines = include_inner_lines
self.add_background_rectangles_to_entries = add_background_rectangles_to_entries
self.entries_background_color = ManimColor(entries_background_color)
self.include_background_rectangle = include_background_rectangle
@ -349,15 +353,19 @@ class Table(VGroup):
)
line_group.add(line)
self.add(line)
for k in range(len(self.mob_table) - 1):
anchor = self.get_rows()[k + 1].get_top()[1] + 0.5 * (
self.get_rows()[k].get_bottom()[1] - self.get_rows()[k + 1].get_top()[1]
)
line = Line(
[anchor_left, anchor, 0], [anchor_right, anchor, 0], **self.line_config
)
line_group.add(line)
self.add(line)
if self.include_inner_lines:
for k in range(len(self.mob_table) - 1):
anchor = self.get_rows()[k + 1].get_top()[1] + 0.5 * (
self.get_rows()[k].get_bottom()[1]
- self.get_rows()[k + 1].get_top()[1]
)
line = Line(
[anchor_left, anchor, 0],
[anchor_right, anchor, 0],
**self.line_config,
)
line_group.add(line)
self.add(line)
self.horizontal_lines = line_group
return self
@ -379,16 +387,19 @@ class Table(VGroup):
)
line_group.add(line)
self.add(line)
for k in range(len(self.mob_table[0]) - 1):
anchor = self.get_columns()[k + 1].get_left()[0] + 0.5 * (
self.get_columns()[k].get_right()[0]
- self.get_columns()[k + 1].get_left()[0]
)
line = Line(
[anchor, anchor_bottom, 0], [anchor, anchor_top, 0], **self.line_config
)
line_group.add(line)
self.add(line)
if self.include_inner_lines:
for k in range(len(self.mob_table[0]) - 1):
anchor = self.get_columns()[k + 1].get_left()[0] + 0.5 * (
self.get_columns()[k].get_right()[0]
- self.get_columns()[k + 1].get_left()[0]
)
line = Line(
[anchor, anchor_bottom, 0],
[anchor, anchor_top, 0],
**self.line_config,
)
line_group.add(line)
self.add(line)
self.vertical_lines = line_group
return self
@ -526,7 +537,7 @@ class Table(VGroup):
self.add(table)
"""
columns = self.get_columns()
for color, column in zip(colors, columns):
for color, column in zip(colors, columns, strict=False):
column.set_color(color)
return self
@ -555,7 +566,7 @@ class Table(VGroup):
self.add(table)
"""
rows = self.get_rows()
for color, row in zip(colors, rows):
for color, row in zip(colors, rows, strict=False):
row.set_color(color)
return self
@ -811,7 +822,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 +861,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 +1095,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}"})

View file

@ -1,4 +1,4 @@
"""Mobjects used to display Text using Pango or LaTeX.
"""Mobjects used to display Text using Pango, LaTeX, or Typst.
Modules
=======
@ -10,4 +10,5 @@ Modules
~numbers
~tex_mobject
~text_mobject
~typst_mobject
"""

View file

@ -139,7 +139,10 @@ class Code(VMobject, metaclass=ConvertToOpenGL):
if code_file is not None:
code_file = Path(code_file)
code_string = code_file.read_text(encoding="utf-8")
lexer = guess_lexer_for_filename(code_file.name, code_string)
if language is not None:
lexer = get_lexer_by_name(language)
else:
lexer = guess_lexer_for_filename(code_file.name, code_string)
elif code_string is not None:
if language is not None:
lexer = get_lexer_by_name(language)
@ -207,7 +210,7 @@ class Code(VMobject, metaclass=ConvertToOpenGL):
*code_lines,
**base_paragraph_config,
)
for line, color_range in zip(self.code_lines, color_ranges):
for line, color_range in zip(self.code_lines, color_ranges, strict=False):
for start, end, color in color_range:
line[start:end].set_color(color)

View file

@ -4,10 +4,9 @@ from __future__ import annotations
__all__ = ["DecimalNumber", "Integer", "Variable"]
from typing import Any
from typing import Any, Self
import numpy as np
from typing_extensions import Self
from manim import config
from manim.constants import *
@ -295,7 +294,7 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
self._set_submobjects_from_number(number)
self.font_size = old_font_size
self.move_to(move_to_point, self.edge_to_fix)
for sm1, sm2 in zip(self.submobjects, old_submobjects):
for sm1, sm2 in zip(self.submobjects, old_submobjects, strict=False):
sm1.match_style(sm2)
if config.renderer == RendererType.CAIRO:

View file

@ -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,15 +23,12 @@ __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
from typing_extensions import Self
from typing import Any, Self
from manim import config, logger
from manim.constants import *
@ -41,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.
@ -236,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::
@ -266,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:
@ -303,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:
"""
@ -341,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(
@ -411,40 +573,37 @@ 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:
def index_of_part(self, part: VMobject) -> int:
split_self = self.split()
if part not in split_self:
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.

View file

@ -76,7 +76,7 @@ from manim.typing import Point3D
from manim.utils.color import ManimColor, ParsableManimColor, color_gradient
if TYPE_CHECKING:
from typing_extensions import Self
from typing import Self
from manim.typing import Point3D
@ -166,9 +166,12 @@ class Paragraph(VGroup):
lines_str_list = lines_str.split("\n")
self.chars = self._gen_chars(lines_str_list)
self.lines = [list(self.chars), [self.alignment] * len(self.chars)]
self.lines_initial_positions = [line.get_center() for line in self.lines[0]]
self.add(*self.lines[0])
# TODO: If possible get rid of self.lines_chars, as it seems to be a
# listified duplicate of self.chars.
self.lines_chars = list(self.chars)
self.lines_alignments = [self.alignment] * len(self.chars)
self.lines_initial_positions = [line.get_center() for line in self.lines_chars]
self.add(*self.lines_chars)
self.move_to(np.array([0, 0, 0]))
if self.alignment:
self._set_all_lines_alignments(self.alignment)
@ -221,7 +224,7 @@ class Paragraph(VGroup):
alignment
Defines the alignment of paragraph. Possible values are "left", "right", "center".
"""
for line_no in range(len(self.lines[0])):
for line_no in range(len(self.lines_chars)):
self._change_alignment_for_a_line(alignment, line_no)
return self
@ -240,8 +243,8 @@ class Paragraph(VGroup):
def _set_all_lines_to_initial_positions(self) -> Paragraph:
"""Set all lines to their initial positions."""
self.lines[1] = [None] * len(self.lines[0])
for line_no in range(len(self.lines[0])):
self.lines_alignments = [None] * len(self.lines_chars)
for line_no in range(len(self.lines_chars)):
self[line_no].move_to(
self.get_center() + self.lines_initial_positions[line_no],
)
@ -255,7 +258,7 @@ class Paragraph(VGroup):
line_no
Defines the line number for which we want to set given alignment.
"""
self.lines[1][line_no] = None
self.lines_alignments[line_no] = None
self[line_no].move_to(self.get_center() + self.lines_initial_positions[line_no])
return self
@ -269,12 +272,12 @@ class Paragraph(VGroup):
line_no
Defines the line number for which we want to set given alignment.
"""
self.lines[1][line_no] = alignment
if self.lines[1][line_no] == "center":
self.lines_alignments[line_no] = alignment
if self.lines_alignments[line_no] == "center":
self[line_no].move_to(
np.array([self.get_center()[0], self[line_no].get_center()[1], 0]),
)
elif self.lines[1][line_no] == "right":
elif self.lines_alignments[line_no] == "right":
self[line_no].move_to(
np.array(
[
@ -284,7 +287,7 @@ class Paragraph(VGroup):
],
),
)
elif self.lines[1][line_no] == "left":
elif self.lines_alignments[line_no] == "left":
self[line_no].move_to(
np.array(
[
@ -801,8 +804,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 +1351,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")

View file

@ -0,0 +1,818 @@
"""Mobjects representing text rendered using Typst.
.. _typst-mobjects:
.. important::
The ``typst`` Python package must be installed to use these classes.
Install it via ``pip install typst>=0.14`` or add the ``typst`` optional
dependency group (``pip install manim[typst]``).
Typst mobjects compile Typst markup directly to SVG using the ``typst``
Python package and then import the result through :class:`~.SVGMobject`.
Use :class:`~.Typst` for general Typst markup and :class:`~.TypstMath`
for display-style math.
Examples
--------
Basic text and math
^^^^^^^^^^^^^^^^^^^
.. manim:: TypstTextReferenceExample
:save_last_frame:
:ref_classes: Typst
class TypstTextReferenceExample(Scene):
def construct(self):
text = Typst(
r"*Hello* from _Typst!_",
color=YELLOW,
font_size=72,
)
self.add(text)
.. manim:: TypstMathReferenceExample
:save_last_frame:
:ref_classes: TypstMath
class TypstMathReferenceExample(Scene):
def construct(self):
equation = TypstMath(
r"sum_(k=1)^n k = frac(n(n + 1), 2)",
font_size=72,
)
self.add(equation)
Selecting subexpressions
^^^^^^^^^^^^^^^^^^^^^^^^
Typst mobjects expose label-based selection via :meth:`~.Typst.select`.
There are two common ways to create selectable groups:
- use ordinary Typst labels in :class:`~.Typst`
- use Manim's ``{{ ... }}`` shorthand in :class:`~.TypstMath`
.. note::
The ``{{ ... }}`` shorthand is currently only supported by
:class:`~.TypstMath`. For :class:`~.Typst`, create labels directly in the
Typst source, for example with ``#box[body] <label>``.
.. manim:: TypstLabelSelectionExample
:save_last_frame:
:ref_classes: Typst
:ref_methods: Typst.select
class TypstLabelSelectionExample(Scene):
def construct(self):
text = Typst(
r'''
#box[
*Typst* labels also work in regular markup.
] <headline>
#let pick(body) = [#box(body) <picked>]
We can highlight #pick[multiple] #pick[fragments] at once.
''',
font_size=42,
)
text.select("headline").set_color(BLUE)
text.select("picked").set_color(YELLOW)
self.add(text)
.. manim:: TypstMathSelectionExample
:save_last_frame:
:ref_classes: TypstMath
:ref_methods: Typst.select
class TypstMathSelectionExample(Scene):
def construct(self):
equation = TypstMath(
"{{ a^2 + b^2 : lhs }} = {{ c^2 }}",
font_size=72,
)
equation.select("lhs").set_color(BLUE)
equation.select(0).set_color(YELLOW)
self.add(equation)
Inspecting baseline frames
^^^^^^^^^^^^^^^^^^^^^^^^^^
For debugging or alignment tasks, Typst mobjects can optionally track a
per-element baseline frame. Enable this with ``track_baselines=True`` and
query either :attr:`~.Typst.baseline_frames` for all tracked leaf elements or
:meth:`~.Typst.get_baseline_frame` for a specific selected submobject.
.. code-block:: python
text = Typst("Ggf", track_baselines=True)
orig, right, up = text.baseline_frames[0]
eq = TypstMath("{{ a^2 + b^2 : lhs }} = c^2", track_baselines=True)
for part in eq.select("lhs"):
orig, right, up = eq.get_baseline_frame(part)
print(orig, right, up)
"""
from __future__ import annotations
__all__ = [
"Typst",
"TypstMath",
]
import re
from pathlib import Path
from typing import Any, cast
from xml.etree import ElementTree as ET
import numpy as np
import svgelements as se
from manim import config
from manim.constants import DEFAULT_FONT_SIZE, SCALE_FACTOR_PER_FONT_POINT, RendererType
from manim.mobject.svg.svg_mobject import SVGMobject
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
from manim.utils.color import BLACK, ParsableManimColor
from manim.utils.typst_file_writing import typst_to_svg_file
_MANIMGRP_PREAMBLE = "#let manimgrp(lbl, body) = [#box(body) #label(lbl)]"
# Pattern for the label part of {{ content : label }}.
# The label must be a valid Typst label identifier.
_LABEL_RE = re.compile(r"^(.*)\s*:\s*([a-zA-Z_][a-zA-Z0-9_-]*)\s*$", re.DOTALL)
_INTERNAL_TYPST_ID_RE = re.compile(r"g[0-9A-Fa-f]+")
_DUPLICATE_LABEL_SUFFIX = "__manim_typst_dup_"
# Empirical correction so Typst-authored SVG strokes (fraction bars,
# underlines, etc.) visually match the weight of TeX-derived geometry more
# closely after import into Manim's pixel-based stroke model.
_TYPST_SVG_STROKE_WIDTH_SCALE = 0.5
class Typst(SVGMobject):
"""A mobject rendered from a Typst markup string.
The Typst source is compiled to SVG via the ``typst`` Python package
(a self-contained Rust binary extension no system-level install
required) and then imported through :class:`~.SVGMobject`.
Parameters
----------
typst_code
Raw Typst markup to be compiled. This string is placed verbatim
into the body of a minimal Typst document.
font_size
Font size in Manim font-size units (default: ``DEFAULT_FONT_SIZE``,
i.e. 48). The actual scaling is applied *after* SVG import, matching
the approach used by :class:`~.SingleStringMathTex`.
typst_preamble
Extra Typst code inserted before the body. Useful for ``#import``,
``#set``, or ``#show`` rules. Default: ``""``.
color
The color of the mobject. By default the standard VMobject color
(white in dark mode). Overrides the Typst text fill color.
stroke_width
SVG stroke width override. If ``None`` (default), the stroke widths
from Typst's SVG output are preserved.
font_paths
Optional list of additional font directories passed to the Typst
compiler (e.g. for custom fonts not installed system-wide).
track_baselines
Whether to keep enough per-element reference data to recover the
current Typst baseline frame for each imported submobject.
When enabled, :attr:`baseline_frames` and
:meth:`get_baseline_frame` can be used to retrieve the current
``(orig, right, up)`` positions for the imported SVG elements.
should_center
Whether to center the mobject after import (default ``True``).
height
Target height of the mobject. If ``None`` (default), the height is
determined by ``font_size``.
**kwargs
Forwarded to :class:`~.SVGMobject`.
Examples
--------
.. manim:: TypstExample
:ref_classes: Typst
class TypstExample(Scene):
def construct(self):
formula = Typst(r"$ integral_a^b f(x) dif x $")
self.play(Write(formula))
.. manim:: TypstTextExample
:save_last_frame:
:ref_classes: Typst
class TypstTextExample(Scene):
def construct(self):
text = Typst(
r"*Hello* from _Typst!_",
font_size=72,
)
self.add(text)
"""
def __init__(
self,
typst_code: str,
*,
font_size: float = DEFAULT_FONT_SIZE,
typst_preamble: str = "",
color: ParsableManimColor | None = None,
stroke_width: float | None = None,
font_paths: list[str | Path] | None = None,
track_baselines: bool = False,
should_center: bool = True,
height: float | None = None,
**kwargs: Any,
):
if color is None:
color = VMobject().color
self._font_size = font_size
self.typst_code = typst_code
self.typst_preamble = typst_preamble
self.track_baselines = track_baselines
self._preserve_svg_stroke_widths = stroke_width is None
self._baseline_tracked_submobjects: list[VMobject] = []
self._stroke_width_tracked_submobjects: list[VMobject] = []
self._label_aliases: dict[str, list[str]] = {}
file_name = typst_to_svg_file(
typst_code,
preamble=typst_preamble,
font_paths=font_paths,
)
super().__init__(
file_name=file_name,
should_center=should_center,
stroke_width=stroke_width,
height=height,
color=color,
path_string_config={
"should_subdivide_sharp_curves": True,
"should_remove_null_curves": True,
},
**kwargs,
)
self._rebuild_label_aliases()
self._refresh_svg_stroke_widths()
self.init_colors()
# Used for scaling via font_size property (mirrors SingleStringMathTex).
self.initial_height = self.height
if height is None:
self.font_size = self._font_size
def __repr__(self) -> str:
return f"{type(self).__name__}({self.typst_code!r})"
@property
def hash_seed(self) -> tuple:
"""Include baseline tracking in the SVG cache key."""
return (*super().hash_seed, self.track_baselines)
# -- font_size property (same approach as SingleStringMathTex) -----------
@property
def font_size(self) -> float:
"""The font size of the Typst mobject."""
return self.height / self.initial_height / SCALE_FACTOR_PER_FONT_POINT
@font_size.setter
def font_size(self, val: float) -> None:
if val <= 0:
raise ValueError("font_size must be greater than 0.")
if self.height > 0:
self.scale(val / self.font_size)
def scale(
self,
scale_factor: float,
scale_stroke: bool = False,
*,
about_point: np.ndarray | None = None,
about_edge: np.ndarray | None = None,
) -> Typst:
result = super().scale(
scale_factor,
scale_stroke=scale_stroke,
about_point=about_point,
about_edge=about_edge,
)
self._refresh_svg_stroke_widths()
return result
def _refresh_svg_stroke_widths(self) -> None:
"""Refresh pixel stroke widths for Typst-authored SVG strokes.
SVG stroke widths are specified in the SVG's local coordinate system,
while Manim stroke widths are pixel-based. For Typst-authored strokes
such as fraction bars or underlines, rescale them according to the
current geometric scale of the imported element so their visual weight
stays proportional to the rest of the expression.
"""
if not self._preserve_svg_stroke_widths:
return
pixels_per_unit = config.pixel_width / config.frame_width
for submobject in self._stroke_width_tracked_submobjects:
submobject_any = cast(Any, submobject)
reference_size = cast(float, submobject_any._typst_reference_size)
source_stroke_width = cast(
float,
submobject_any._typst_source_stroke_width,
)
current_size = max(submobject.width, submobject.height)
if reference_size <= 0:
continue
current_stroke_width = source_stroke_width * current_size / reference_size
submobject.set_stroke(
width=current_stroke_width
* pixels_per_unit
* _TYPST_SVG_STROKE_WIDTH_SCALE,
family=False,
)
# -- baseline frame tracking ---------------------------------------------
def get_mob_from_shape_element(self, shape: se.SVGElement) -> VMobject | None:
"""Attach Typst-specific metadata to imported shape mobjects."""
mob = super().get_mob_from_shape_element(shape)
if mob is None or not mob.has_points():
return mob
if self._preserve_svg_stroke_widths and shape.stroke_width not in (None, 0):
reference_size = max(mob.width, mob.height)
if reference_size > 0:
mob_any = cast(Any, mob)
mob_any._typst_reference_size = reference_size
mob_any._typst_source_stroke_width = shape.stroke_width
self._stroke_width_tracked_submobjects.append(mob)
if not self.track_baselines:
return mob
baseline_marks = self._get_reference_baseline_frame(shape)
if baseline_marks is None:
return mob
reference_points = mob.points.copy()
reference_xy = np.column_stack(
[
reference_points[:, 0],
reference_points[:, 1],
np.ones(len(reference_points)),
],
)
if np.linalg.matrix_rank(reference_xy) < 3:
return mob
mob_any = cast(Any, mob)
mob_any._typst_reference_points = reference_points
mob_any._typst_reference_baseline_frame = baseline_marks
self._baseline_tracked_submobjects.append(mob)
return mob
@staticmethod
def _get_reference_baseline_frame(
shape: se.SVGElement,
) -> np.ndarray | None:
"""Return the reference ``(orig, right, up)`` frame for a Typst SVG element.
The frame is expressed in the same pre-centering coordinate system as the
imported submobject points after the element's own SVG transform has been
applied.
"""
if not isinstance(shape, se.Transformable):
return None
matrix = shape.transform if shape.apply else se.Matrix()
return np.array(
[
[matrix.e, matrix.f, 0.0],
[matrix.a + matrix.e, matrix.b + matrix.f, 0.0],
[matrix.c + matrix.e, matrix.d + matrix.f, 0.0],
],
)
def get_baseline_frame(
self, submobject: VMobject
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
"""Return the current Typst baseline frame for a tracked submobject.
The returned tuple contains the current positions of ``(orig, right, up)``.
These are recovered from the stored reference frame and the submobject's
current affine position in the scene.
"""
try:
submobject_any = cast(Any, submobject)
reference_points = cast(
np.ndarray,
submobject_any._typst_reference_points,
)
reference_frame = cast(
np.ndarray,
submobject_any._typst_reference_baseline_frame,
)
except AttributeError as err:
raise ValueError(
"No tracked Typst baseline frame is available for this submobject. "
"Construct the Typst mobject with track_baselines=True.",
) from err
reference_xy = np.column_stack(
[
reference_points[:, 0],
reference_points[:, 1],
np.ones(len(reference_points)),
],
)
if np.linalg.matrix_rank(reference_xy) < 3:
raise ValueError(
"The stored Typst reference geometry is degenerate, so its baseline "
"frame cannot be recovered.",
)
transform, _, _, _ = np.linalg.lstsq(
reference_xy, submobject.points, rcond=None
)
frame_xy = np.column_stack(
[
reference_frame[:, 0],
reference_frame[:, 1],
np.ones(len(reference_frame)),
],
)
current_frame = frame_xy @ transform
return tuple(
cast(tuple[np.ndarray, np.ndarray, np.ndarray], tuple(current_frame))
)
@property
def baseline_frames(self) -> list[tuple[np.ndarray, np.ndarray, np.ndarray]]:
"""Current Typst baseline frames for all tracked leaf submobjects."""
if not self.track_baselines:
return []
return [
self.get_baseline_frame(submobject)
for submobject in self._baseline_tracked_submobjects
]
def _rebuild_label_aliases(self) -> None:
"""Rebuild user-facing label aliases from imported SVG ids."""
aliases: dict[str, list[str]] = {}
for key in self.id_to_vgroup_dict:
if key == "root" or key.startswith("numbered_group_"):
continue
if _INTERNAL_TYPST_ID_RE.fullmatch(key) is not None:
continue
base_label = key
if _DUPLICATE_LABEL_SUFFIX in key:
base_label, _, _ = key.partition(_DUPLICATE_LABEL_SUFFIX)
aliases.setdefault(base_label, []).append(key)
self._label_aliases = aliases
def _select_label(self, label: str) -> VGroup:
if label not in self._label_aliases:
raise KeyError(
f"No group with label {label!r} found. "
f"Available labels: {self._user_label_keys()}"
)
result = VGroup()
seen_ids: set[int] = set()
for group_id in self._label_aliases[label]:
for submobject in self.id_to_vgroup_dict[group_id]:
submobject_id = id(submobject)
if submobject_id in seen_ids:
continue
seen_ids.add(submobject_id)
result.add(submobject)
return result
# -- SVG post-processing -------------------------------------------------
def modify_xml_tree(self, element_tree: ET.ElementTree) -> ET.ElementTree:
"""Convert ``data-typst-label`` attributes to ``id`` before parsing.
Typst's SVG renderer emits ``data-typst-label`` on ``<g>`` elements
that carry a label (created via ``#box(body) <label>``). The
``svgelements`` library propagates custom ``data-*`` attributes from
parent groups to all children, making them unusable as unique group
keys. ``id`` attributes, on the other hand, are *not* inherited.
This method walks the XML tree and promotes every
``data-typst-label`` to ``id`` (on ``<g>`` elements only), so that
:meth:`~.SVGMobject.get_mobjects_from` can pick them up via its
existing ``id``-based grouping logic.
"""
# Let the base class inject default style wrappers first.
element_tree = super().modify_xml_tree(element_tree)
# Walk all elements regardless of namespace — ElementTree
# qualifies tags with the namespace URI, so a bare ``"g"``
# won't match ``{http://www.w3.org/2000/svg}g``.
label_counts: dict[str, int] = {}
for element in element_tree.iter():
label = element.get("data-typst-label")
if label is not None:
count = label_counts.get(label, 0)
label_counts[label] = count + 1
svg_id = label
if count > 0:
svg_id = f"{label}{_DUPLICATE_LABEL_SUFFIX}{count}"
element.set("id", svg_id)
del element.attrib["data-typst-label"]
return element_tree
# -- sub-expression selection --------------------------------------------
def select(self, key: str | int) -> VGroup:
"""Select a labeled sub-expression.
Labels are created in the Typst source either manually via the
``manimgrp`` helper or automatically through the ``{{ }}``
double-brace notation in :class:`TypstMath`.
Parameters
----------
key
A label name (``str``) matching a ``data-typst-label`` in the
SVG, or an integer index into the auto-numbered ``{{ }}``
groups (``_grp-0``, ``_grp-1``, ).
Returns
-------
VGroup
The submobjects corresponding to the selected group.
Raises
------
KeyError
If no group with the given label exists.
IndexError
If an integer index is out of range.
Examples
--------
.. manim:: TypstSelectExample
:save_last_frame:
:ref_classes: TypstMath
:ref_methods: Typst.select
class TypstSelectExample(Scene):
def construct(self):
eq = TypstMath(
"{{ a + b : num }} / {{ c : den }} = {{ lambda }} {{ x }}"
)
eq.select("num").set_color(RED) # "a + b"
eq.select("den").set_color(BLUE) # "c"
eq.select(0).set_color(YELLOW) # "lambda" (auto-numbered: "grp-0")
eq.select(1).set_color(GREEN) # "x" (auto-numbered: "grp-1")
self.add(eq)
"""
if isinstance(key, int):
label = f"_grp-{key}"
if label not in self._label_aliases:
raise IndexError(
f"Group index {key} out of range. "
f"Available labels: {self._user_label_keys()}"
)
return self._select_label(label)
return self._select_label(key)
def _user_label_keys(self) -> list[str]:
"""Return the label keys that were created from ``data-typst-label``
attributes (filtering out internal Typst group IDs and auto-numbered
groups).
"""
return list(self._label_aliases)
# -- color handling ------------------------------------------------------
def init_colors(self, propagate_colors: bool = True) -> Typst:
"""Recolor black submobjects to ``self.color``.
Typst renders text in black (``fill="#000000"``) by default.
This mirrors the approach of :meth:`SingleStringMathTex.init_colors`:
any submobject whose color is black is recolored to ``self.color``,
while explicitly colored submobjects (non-black) are preserved.
"""
for submobject in self.submobjects:
if submobject.color != BLACK:
continue
submobject.color = self.color
if config.renderer == RendererType.OPENGL:
submobject.init_colors()
elif config.renderer == RendererType.CAIRO:
submobject.init_colors(propagate_colors=propagate_colors)
return self
class TypstMath(Typst):
r"""Convenience wrapper: wraps the input in Typst math delimiters.
The expression is rendered as a display-level equation
(``$ ... $`` with surrounding spaces).
Supports the ``{{ ... }}`` double-brace notation for grouping
sub-expressions. Each ``{{ content }}`` is wrapped in a labeled
``manimgrp`` call so that the resulting SVG contains identifiable
groups accessible via :meth:`~.Typst.select`.
Groups can optionally be given explicit labels:
``{{ content : label }}``. Without a label, groups are
auto-numbered (``_grp-0``, ``_grp-1``, ).
Parameters
----------
math_expression
Typst math-mode content **without** the ``$ ... $`` delimiters.
May contain ``{{ ... }}`` groups.
**kwargs
Forwarded to :class:`Typst`.
Examples
--------
.. manim:: DisplayMath
:save_last_frame:
:ref_classes: TypstMath
class DisplayMath(Scene):
def construct(self):
eq = TypstMath(r"sum_(k=0)^n k = (n(n+1)) / 2")
self.add(eq)
.. manim:: GroupedMath
:save_last_frame:
:ref_classes: TypstMath
:ref_methods: Typst.select
class GroupedMath(Scene):
def construct(self):
eq = TypstMath("{{ a^2 + b^2 : lhs }} = {{ c^2 }}")
eq.select("lhs").set_color(RED) # "a^2 + b^2"
eq.select(0).set_color(BLUE) # "c^2" (auto-numbered: "grp-0")
self.add(eq)
"""
def __init__(self, math_expression: str, **kwargs: Any):
processed, labels = self._preprocess_groups(math_expression)
self._group_labels = labels
# Inject the manimgrp helper when groups are present.
if labels:
preamble = kwargs.get("typst_preamble", "")
if _MANIMGRP_PREAMBLE not in preamble:
preamble = (
f"{_MANIMGRP_PREAMBLE}\n{preamble}"
if preamble
else _MANIMGRP_PREAMBLE
)
kwargs["typst_preamble"] = preamble
super().__init__(f"$ {processed} $", **kwargs)
# -- double-brace preprocessor -------------------------------------------
@staticmethod
def _preprocess_groups(math_expr: str) -> tuple[str, list[str]]:
"""Replace ``{{ ... }}`` groups with ``manimgrp(...)`` calls.
Parameters
----------
math_expr
The raw math expression (without ``$ ... $`` delimiters).
Returns
-------
tuple[str, list[str]]
The processed expression and an ordered list of group labels.
"""
result: list[str] = []
labels: list[str] = []
auto_index = 0
i = 0
n = len(math_expr)
outer_in_string = False
outer_bracket_depth = 0
while i < n:
ch = math_expr[i]
# Track string literals at the outer level.
if outer_in_string:
result.append(ch)
if ch == "\\" and i + 1 < n:
result.append(math_expr[i + 1])
i += 2
continue
if ch == '"':
outer_in_string = False
i += 1
continue
if ch == '"':
outer_in_string = True
result.append(ch)
i += 1
continue
# Track [...] content blocks at the outer level.
if ch == "[":
outer_bracket_depth += 1
result.append(ch)
i += 1
continue
if ch == "]" and outer_bracket_depth > 0:
outer_bracket_depth -= 1
result.append(ch)
i += 1
continue
if outer_bracket_depth > 0:
result.append(ch)
i += 1
continue
# Look for opening {{ (not a single {)
if i + 1 < n and ch == "{" and math_expr[i + 1] == "{":
i += 2 # skip {{
content_start = i
depth = 1
in_string = False
bracket_depth = 0
while i < n and depth > 0:
ch = math_expr[i]
if in_string:
if ch == "\\" and i + 1 < n:
i += 2
continue
if ch == '"':
in_string = False
i += 1
continue
if ch == '"':
in_string = True
i += 1
continue
if ch == "[":
bracket_depth += 1
i += 1
continue
if ch == "]" and bracket_depth > 0:
bracket_depth -= 1
i += 1
continue
if bracket_depth > 0:
i += 1
continue
if ch == "{" and i + 1 < n and math_expr[i + 1] == "{":
depth += 1
i += 2
continue
if ch == "}" and i + 1 < n and math_expr[i + 1] == "}":
depth -= 1
if depth == 0:
content = math_expr[content_start:i]
i += 2 # skip }}
break
i += 2
continue
i += 1
else:
# Unclosed {{ — emit literally and stop.
result.append("{{")
result.append(math_expr[content_start:])
return "".join(result), labels
# Check for optional `: label` suffix.
m = _LABEL_RE.match(content)
if m is not None:
body = m.group(1).strip()
label = m.group(2)
else:
body = content.strip()
label = f"_grp-{auto_index}"
auto_index += 1
labels.append(label)
result.append(f'manimgrp("{label}", {body})')
else:
result.append(math_expr[i])
i += 1
return "".join(result), labels

View file

@ -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])
edges += zip(face, face[1:] + face[:1], strict=True)
return edges
def create_faces(
@ -154,9 +154,7 @@ class Polyhedron(VGroup):
"""Extracts the coordinates of the vertices in the graph.
Used for updating faces.
"""
new_vertex_coords = []
for v in self.graph.vertices:
new_vertex_coords.append(self.graph[v].get_center())
new_vertex_coords = [self.graph[v].get_center() for v in self.graph.vertices]
layout = dict(enumerate(new_vertex_coords))
return [[layout[j] for j in i] for i in self.faces_list]

View file

@ -17,10 +17,9 @@ __all__ = [
]
from collections.abc import Callable, Iterable, Sequence
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Literal, Self
import numpy as np
from typing_extensions import Self
from manim import config, logger
from manim.constants import *
@ -40,15 +39,22 @@ from manim.utils.color import (
ParsableManimColor,
interpolate_color,
)
from manim.utils.iterables import tuplify
from manim.utils.space_ops import normalize, perpendicular_bisector, z_to_vector
if TYPE_CHECKING:
from manim.typing import Point3D, Point3DLike, Vector3DLike
from manim.mobject.graphing.coordinate_systems import ThreeDAxes
from manim.typing import Point3D, Point3DLike, Vector3D, Vector3DLike
class ThreeDVMobject(VMobject, metaclass=ConvertToOpenGL):
def __init__(self, shade_in_3d: bool = True, **kwargs):
u_index: int
v_index: int
u1: float
u2: float
v1: float
v2: float
def __init__(self, shade_in_3d: bool = True, **kwargs: Any):
super().__init__(shade_in_3d=shade_in_3d, **kwargs)
@ -107,13 +113,16 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
def __init__(
self,
func: Callable[[float, float], np.ndarray],
u_range: Sequence[float] = [0, 1],
v_range: Sequence[float] = [0, 1],
resolution: Sequence[int] = 32,
u_range: tuple[float, float] = (0, 1),
v_range: tuple[float, float] = (0, 1),
resolution: int | Sequence[int] = 32,
surface_piece_config: dict = {},
fill_color: ParsableManimColor = BLUE_D,
fill_opacity: float = 1.0,
checkerboard_colors: Sequence[ParsableManimColor] | bool = [BLUE_D, BLUE_E],
checkerboard_colors: Iterable[ParsableManimColor] | Literal[False] = [
BLUE_D,
BLUE_E,
],
stroke_color: ParsableManimColor = LIGHT_GREY,
stroke_width: float = 0.5,
should_make_jagged: bool = False,
@ -131,16 +140,16 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
)
self.resolution = resolution
self.surface_piece_config = surface_piece_config
if checkerboard_colors:
self.checkerboard_colors: list[ManimColor] = [
ManimColor(x) for x in checkerboard_colors
]
else:
self.checkerboard_colors: list[ManimColor] | Literal[False]
if checkerboard_colors is False:
self.checkerboard_colors = checkerboard_colors
else:
self.checkerboard_colors = [ManimColor(i) for i in checkerboard_colors]
self.should_make_jagged = should_make_jagged
self.pre_function_handle_to_anchor_scale_factor = (
pre_function_handle_to_anchor_scale_factor
)
self.list_of_faces: list[ThreeDVMobject] = []
self._func = func
self._setup_in_uv_space()
self.apply_function(lambda p: func(p[0], p[1]))
@ -151,11 +160,10 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
return self._func(u, v)
def _get_u_values_and_v_values(self) -> tuple[np.ndarray, np.ndarray]:
res = tuplify(self.resolution)
if len(res) == 1:
u_res = v_res = res[0]
if isinstance(self.resolution, int):
u_res = v_res = self.resolution
else:
u_res, v_res = res
u_res, v_res = self.resolution
u_values = np.linspace(*self.u_range, u_res + 1)
v_values = np.linspace(*self.v_range, v_res + 1)
@ -165,6 +173,7 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
def _setup_in_uv_space(self) -> None:
u_values, v_values = self._get_u_values_and_v_values()
faces = VGroup()
self.list_of_faces = []
for i in range(len(u_values) - 1):
for j in range(len(v_values) - 1):
u1, u2 = u_values[i : i + 2]
@ -186,6 +195,7 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
face.u2 = u2
face.v1 = v1
face.v2 = v2
self.list_of_faces.append(face)
faces.set_fill(color=self.fill_color, opacity=self.fill_opacity)
faces.set_stroke(
color=self.stroke_color,
@ -197,7 +207,7 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
self.set_fill_by_checkerboard(*self.checkerboard_colors)
def set_fill_by_checkerboard(
self, *colors: Iterable[ParsableManimColor], opacity: float | None = None
self, *colors: ParsableManimColor, opacity: float | None = None
) -> Self:
"""Sets the fill_color of each face of :class:`Surface` in
an alternating pattern.
@ -216,17 +226,19 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
The parametric surface with an alternating pattern.
"""
n_colors = len(colors)
for face in self:
for face in self.list_of_faces:
c_index = (face.u_index + face.v_index) % n_colors
face.set_fill(colors[c_index], opacity=opacity)
return self
def set_fill_by_value(
self,
axes: Mobject,
colorscale: list[ParsableManimColor] | ParsableManimColor | None = None,
axes: ThreeDAxes,
colorscale: Iterable[ParsableManimColor]
| Iterable[tuple[ParsableManimColor, float]]
| None = None,
axis: int = 2,
**kwargs,
**kwargs: Any,
) -> Self:
"""Sets the color of each mobject of a parametric surface to a color
relative to its axis-value.
@ -286,19 +298,23 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
"the surface fill color has not been changed"
)
return self
colorscale_list = list(colorscale)
ranges = [axes.x_range, axes.y_range, axes.z_range]
if type(colorscale[0]) is tuple:
assert isinstance(colorscale_list, list)
new_colors: list[ManimColor]
if type(colorscale_list[0]) is tuple and len(colorscale_list[0]) == 2:
new_colors, pivots = [
[i for i, j in colorscale],
[j for i, j in colorscale],
[ManimColor(i) for i, j in colorscale_list],
[j for i, j in colorscale_list],
]
else:
new_colors = colorscale
new_colors = [ManimColor(i) for i in colorscale_list]
current_range = ranges[axis]
pivot_min = ranges[axis][0]
pivot_max = ranges[axis][1]
assert current_range is not None
pivot_min = current_range[0]
pivot_max = current_range[1]
pivot_frequency = (pivot_max - pivot_min) / (len(new_colors) - 1)
pivots = np.arange(
start=pivot_min,
@ -325,6 +341,7 @@ class Surface(VGroup, metaclass=ConvertToOpenGL):
color_index,
)
if config.renderer == RendererType.OPENGL:
assert isinstance(mob, OpenGLMobject)
mob.set_color(mob_color, recurse=False)
elif config.renderer == RendererType.CAIRO:
mob.set_color(mob_color, family=False)
@ -362,13 +379,7 @@ class Sphere(Surface):
class ExampleSphere(ThreeDScene):
def construct(self):
self.set_camera_orientation(phi=PI / 6, theta=PI / 6)
sphere1 = Sphere(
center=(3, 0, 0),
radius=1,
resolution=(20, 20),
u_range=[0.001, PI - 0.001],
v_range=[0, TAU]
)
sphere1 = Sphere(center=(3, 0, 0), radius=1, resolution=(20, 20))
sphere1.set_color(RED)
self.add(sphere1)
sphere2 = Sphere(center=(-1, -3, 0), radius=2, resolution=(18, 18))
@ -377,16 +388,67 @@ class Sphere(Surface):
sphere3 = Sphere(center=(-1, 2, 0), radius=2, resolution=(16, 16))
sphere3.set_color(BLUE)
self.add(sphere3)
This example shows that overlapping spheres can intersect with rough transitions.
.. manim:: ExampleSphereOverlap
:save_last_frame:
class ExampleSphereOverlap(ThreeDScene):
def construct(self):
self.set_camera_orientation(phi=PI / 4, theta=PI / 4)
sphere1 = Sphere(center=(0, 0, 0), radius=1, resolution=(20, 20))
sphere1.set_color(RED)
self.add(sphere1)
sphere2 = Sphere(center=(-0.5, -1, 0.5), radius=1.2, resolution=(20, 20))
sphere2.set_color(GREEN)
self.add(sphere2)
sphere3 = Sphere(center=(1, -1, 0), radius=1.1, resolution=(20, 20))
sphere3.set_color(BLUE)
self.add(sphere3)
In this example, by modifying ``u_range`` (the range of the azimuthal angle) and
``v_range`` (the range of the polar angle), it is possible to obtain a portion of a
sphere:
.. manim:: ExamplePartialSpheres
:save_last_frame:
class ExamplePartialSpheres(ThreeDScene):
def construct(self):
self.set_camera_orientation(phi=PI / 4)
sphere1 = Sphere(
center=(-3, 0, 0),
resolution=(10, 20),
u_range=[TAU / 4, 3 * TAU / 4],
)
sphere1.set_color(RED)
self.add(sphere1)
sphere2 = Sphere(
center=(0, 0, 0),
resolution=(20, 10),
v_range=[0, TAU / 4],
)
sphere2.set_color(GREEN)
self.add(sphere2)
sphere3 = Sphere(
center=(3, 0, 0),
resolution=(5, 10),
u_range=[3 * TAU / 4, TAU],
v_range=[TAU / 4, TAU / 2],
)
sphere3.set_color(BLUE)
self.add(sphere3)
"""
def __init__(
self,
center: Point3DLike = ORIGIN,
radius: float = 1,
resolution: Sequence[int] | None = None,
u_range: Sequence[float] = (0, TAU),
v_range: Sequence[float] = (0, PI),
**kwargs,
resolution: int | Sequence[int] | None = None,
u_range: tuple[float, float] = (0, TAU),
v_range: tuple[float, float] = (0, PI),
**kwargs: Any,
) -> None:
if config.renderer == RendererType.OPENGL:
res_value = (101, 51)
@ -409,12 +471,12 @@ class Sphere(Surface):
self.shift(center)
def func(self, u: float, v: float) -> np.ndarray:
def func(self, u: float, v: float) -> Point3D:
"""The z values defining the :class:`Sphere` being plotted.
Returns
-------
:class:`numpy.array`
:class:`Point3D`
The z values defining the :class:`Sphere`.
"""
return self.radius * np.array(
@ -456,11 +518,11 @@ class Dot3D(Sphere):
def __init__(
self,
point: list | np.ndarray = ORIGIN,
point: Point3D = ORIGIN,
radius: float = DEFAULT_DOT_RADIUS,
color: ParsableManimColor = WHITE,
resolution: tuple[int, int] = (8, 8),
**kwargs,
resolution: int | tuple[int, int] | None = (8, 8),
**kwargs: Any,
) -> None:
super().__init__(center=point, radius=radius, resolution=resolution, **kwargs)
self.set_color(color)
@ -502,7 +564,7 @@ class Cube(VGroup):
fill_opacity: float = 0.75,
fill_color: ParsableManimColor = BLUE,
stroke_width: float = 0,
**kwargs,
**kwargs: Any,
) -> None:
self.side_length = side_length
super().__init__(
@ -554,7 +616,9 @@ class Prism(Cube):
"""
def __init__(
self, dimensions: tuple[float, float, float] | np.ndarray = [3, 2, 1], **kwargs
self,
dimensions: Vector3DLike = [3, 2, 1],
**kwargs: Any,
) -> None:
self.dimensions = dimensions
super().__init__(**kwargs)
@ -608,20 +672,20 @@ class Cone(Surface):
self,
base_radius: float = 1,
height: float = 1,
direction: np.ndarray = Z_AXIS,
direction: Vector3DLike = Z_AXIS,
show_base: bool = False,
v_range: Sequence[float] = [0, TAU],
v_range: tuple[float, float] = (0, TAU),
u_min: float = 0,
checkerboard_colors: bool = False,
checkerboard_colors: Iterable[ParsableManimColor] | Literal[False] = False,
**kwargs: Any,
) -> None:
self.direction = direction
self.direction = np.array(direction)
self.theta = PI - np.arctan(base_radius / height)
super().__init__(
self.func,
v_range=v_range,
u_range=[u_min, np.sqrt(base_radius**2 + height**2)],
u_range=(u_min, np.sqrt(base_radius**2 + height**2)),
checkerboard_colors=checkerboard_colors,
**kwargs,
)
@ -642,7 +706,7 @@ class Cone(Surface):
self._rotate_to_direction()
def func(self, u: float, v: float) -> np.ndarray:
def func(self, u: float, v: float) -> Point3D:
"""Converts from spherical coordinates to cartesian.
Parameters
@ -667,10 +731,10 @@ class Cone(Surface):
],
)
def get_start(self) -> np.ndarray:
def get_start(self) -> Point3D:
return self.start_point.get_center()
def get_end(self) -> np.ndarray:
def get_end(self) -> Point3D:
return self.end_point.get_center()
def _rotate_to_direction(self) -> None:
@ -703,7 +767,7 @@ class Cone(Surface):
self._current_theta = theta
self._current_phi = phi
def set_direction(self, direction: np.ndarray) -> None:
def set_direction(self, direction: Vector3DLike) -> None:
"""Changes the direction of the apex of the :class:`Cone`.
Parameters
@ -711,10 +775,10 @@ class Cone(Surface):
direction
The direction of the apex.
"""
self.direction = direction
self.direction = np.array(direction)
self._rotate_to_direction()
def get_direction(self) -> np.ndarray:
def get_direction(self) -> Vector3D:
"""Returns the current direction of the apex of the :class:`Cone`.
Returns
@ -724,7 +788,7 @@ class Cone(Surface):
"""
return self.direction
def _set_start_and_end_attributes(self, direction):
def _set_start_and_end_attributes(self, direction: Vector3D) -> None:
normalized_direction = direction * np.linalg.norm(direction)
start = self.base_circle.get_center()
@ -770,18 +834,18 @@ class Cylinder(Surface):
self,
radius: float = 1,
height: float = 2,
direction: np.ndarray = Z_AXIS,
v_range: Sequence[float] = [0, TAU],
direction: Vector3DLike = Z_AXIS,
v_range: tuple[float, float] = (0, TAU),
show_ends: bool = True,
resolution: Sequence[int] = (24, 24),
**kwargs,
resolution: int | tuple[int, int] = (24, 24),
**kwargs: Any,
) -> None:
self._height = height
self.radius = radius
super().__init__(
self.func,
resolution=resolution,
u_range=[-self._height / 2, self._height / 2],
u_range=(-self._height / 2, self._height / 2),
v_range=v_range,
**kwargs,
)
@ -813,7 +877,9 @@ class Cylinder(Surface):
def add_bases(self) -> None:
"""Adds the end caps of the cylinder."""
opacity: float
if config.renderer == RendererType.OPENGL:
assert isinstance(self, OpenGLMobject)
color = self.color
opacity = self.opacity
elif config.renderer == RendererType.CAIRO:
@ -868,7 +934,7 @@ class Cylinder(Surface):
self._current_theta = theta
self._current_phi = phi
def set_direction(self, direction: np.ndarray) -> None:
def set_direction(self, direction: Vector3DLike) -> None:
"""Sets the direction of the central axis of the :class:`Cylinder`.
Parameters
@ -927,15 +993,17 @@ class Line3D(Cylinder):
def __init__(
self,
start: np.ndarray = LEFT,
end: np.ndarray = RIGHT,
start: Point3DLike = LEFT,
end: Point3DLike = RIGHT,
thickness: float = 0.02,
color: ParsableManimColor | None = None,
resolution: int | Sequence[int] = 24,
**kwargs,
resolution: int | tuple[int, int] = 24,
**kwargs: Any,
):
self.thickness = thickness
self.resolution = (2, resolution) if isinstance(resolution, int) else resolution
self.resolution: tuple[int, int] = (
(2, resolution) if isinstance(resolution, int) else resolution
)
start = np.array(start, dtype=np.float64)
end = np.array(end, dtype=np.float64)
@ -945,7 +1013,7 @@ class Line3D(Cylinder):
self.set_color(color)
def set_start_and_end_attrs(
self, start: np.ndarray, end: np.ndarray, **kwargs
self, start: Point3DLike, end: Point3DLike, **kwargs: Any
) -> None:
"""Sets the start and end points of the line.
@ -963,7 +1031,7 @@ class Line3D(Cylinder):
rough_end = self.pointify(end)
self.vect = rough_end - rough_start
self.length = np.linalg.norm(self.vect)
self.direction = normalize(self.vect)
self.direction: Vector3D = normalize(self.vect)
# Now that we know the direction between them,
# we can the appropriate boundary point from
# start and end, if they're mobjects
@ -1005,7 +1073,7 @@ class Line3D(Cylinder):
return mob.get_boundary_point(direction)
return np.array(mob_or_point)
def get_start(self) -> np.ndarray:
def get_start(self) -> Point3D:
"""Returns the starting point of the :class:`Line3D`.
Returns
@ -1015,7 +1083,7 @@ class Line3D(Cylinder):
"""
return self.start
def get_end(self) -> np.ndarray:
def get_end(self) -> Point3D:
"""Returns the ending point of the :class:`Line3D`.
Returns
@ -1031,7 +1099,7 @@ class Line3D(Cylinder):
line: Line3D,
point: Point3DLike = ORIGIN,
length: float = 5,
**kwargs,
**kwargs: Any,
) -> Line3D:
"""Returns a line parallel to another line going through
a given point.
@ -1077,9 +1145,9 @@ class Line3D(Cylinder):
def perpendicular_to(
cls,
line: Line3D,
point: Vector3DLike = ORIGIN,
point: Point3DLike = ORIGIN,
length: float = 5,
**kwargs,
**kwargs: Any,
) -> Line3D:
"""Returns a line perpendicular to another line going through
a given point.
@ -1167,14 +1235,14 @@ class Arrow3D(Line3D):
def __init__(
self,
start: np.ndarray = LEFT,
end: np.ndarray = RIGHT,
start: Point3DLike = LEFT,
end: Point3DLike = RIGHT,
thickness: float = 0.02,
height: float = 0.3,
base_radius: float = 0.08,
color: ParsableManimColor = WHITE,
resolution: int | Sequence[int] = 24,
**kwargs,
resolution: int | tuple[int, int] = 24,
**kwargs: Any,
) -> None:
super().__init__(
start=start,
@ -1241,10 +1309,10 @@ class Torus(Surface):
self,
major_radius: float = 3,
minor_radius: float = 1,
u_range: Sequence[float] = (0, TAU),
v_range: Sequence[float] = (0, TAU),
resolution: tuple[int, int] | None = None,
**kwargs,
u_range: tuple[float, float] = (0, TAU),
v_range: tuple[float, float] = (0, TAU),
resolution: int | tuple[int, int] | None = None,
**kwargs: Any,
) -> None:
if config.renderer == RendererType.OPENGL:
res_value = (101, 101)
@ -1263,7 +1331,7 @@ class Torus(Surface):
**kwargs,
)
def func(self, u: float, v: float) -> np.ndarray:
def func(self, u: float, v: float) -> Point3D:
"""The z values defining the :class:`Torus` being plotted.
Returns

View file

@ -18,14 +18,21 @@ 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"]
if TYPE_CHECKING:
from typing import Self
import numpy.typing as npt
from typing_extensions import Self
from manim.typing import PixelArray, StrPath
@ -61,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(
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:
"""
@ -86,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(
[
@ -115,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):
@ -156,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__(
@ -209,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(
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:
@ -252,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.
@ -268,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"
@ -291,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,
}
@ -320,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
@ -331,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"

View file

@ -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,
@ -30,8 +30,9 @@ from ...utils.iterables import stretch_array_to_length
__all__ = ["PMobject", "Mobject1D", "Mobject2D", "PGroup", "PointCloudDot", "Point"]
if TYPE_CHECKING:
from typing import Self
import numpy.typing as npt
from typing_extensions import Self
from manim.typing import (
FloatRGBA_Array,
@ -109,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]
@ -129,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
@ -171,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
@ -181,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(
@ -198,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):
for attr, array in zip(attrs, arrays, strict=True):
setattr(self, attr, array)
self.submobjects = []
return self
@ -358,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

View file

@ -47,8 +47,10 @@ from manim.utils.iterables import (
from manim.utils.space_ops import rotate_vector, shoelace_direction
if TYPE_CHECKING:
from collections.abc import Iterator
from typing import Self
import numpy.typing as npt
from typing_extensions import Self
from manim.typing import (
CubicBezierPath,
@ -102,6 +104,7 @@ class VMobject(Mobject):
"""
sheen_factor = 0.0
target: VMobject
def __init__(
self,
@ -152,7 +155,7 @@ class VMobject(Mobject):
self.shade_in_3d: bool = shade_in_3d
self.tolerance_for_point_equality: float = tolerance_for_point_equality
self.n_points_per_cubic_curve: int = n_points_per_cubic_curve
self._bezier_t_values: npt.NDArray[float] = np.linspace(
self._bezier_t_values: npt.NDArray[np.float64] = np.linspace(
0, 1, n_points_per_cubic_curve
)
self.cap_style: CapStyleType = cap_style
@ -171,6 +174,9 @@ class VMobject(Mobject):
def _assert_valid_submobjects(self, submobjects: Iterable[VMobject]) -> Self:
return self._assert_valid_submobjects_internal(submobjects, VMobject)
def __iter__(self) -> Iterator[VMobject]:
return iter(self.split())
# OpenGL compatibility
@property
def n_points_per_curve(self) -> int:
@ -233,7 +239,10 @@ class VMobject(Mobject):
o if (o is not None) else 0.0 for o in tuplify(opacity)
]
rgbas: FloatRGBA_Array = np.array(
[c.to_rgba_with_alpha(o) for c, o in zip(*make_even(colors, opacities))],
[
c.to_rgba_with_alpha(o)
for c, o in zip(*make_even(colors, opacities), strict=True)
],
)
sheen_factor = self.get_sheen_factor()
@ -457,7 +466,7 @@ class VMobject(Mobject):
return self
elif len(submobs2) == 0:
submobs2 = [vmobject]
for sm1, sm2 in zip(*make_even(submobs1, submobs2)):
for sm1, sm2 in zip(*make_even(submobs1, submobs2), strict=True):
sm1.match_style(sm2)
return self
@ -491,8 +500,10 @@ class VMobject(Mobject):
will shrink, and for :math:`|\alpha| > 1` it will grow. Furthermore,
if :math:`\alpha < 0`, the mobject is also flipped.
scale_stroke
Boolean determining if the object's outline is scaled when the object is scaled.
If enabled, and object with 2px outline is scaled by a factor of .5, it will have an outline of 1px.
Boolean determining if each submobject's outline is scaled when the object
is scaled. If enabled, each submobject keeps its relative stroke width (for
example, a submobject with a 2px outline scaled by a factor of .5 will have
a 1px outline, while a submobject with 0px stroke remains at 0px).
kwargs
Additional keyword arguments passed to
:meth:`~.Mobject.scale`.
@ -529,11 +540,17 @@ class VMobject(Mobject):
"""
if scale_stroke:
self.set_stroke(width=abs(scale_factor) * self.get_stroke_width())
self.set_stroke(
width=abs(scale_factor) * self.get_stroke_width(background=True),
background=True,
)
for mob in self.get_family():
if isinstance(mob, VMobject):
mob.set_stroke(
width=abs(scale_factor) * mob.get_stroke_width(),
family=False,
)
mob.set_stroke(
width=abs(scale_factor) * mob.get_stroke_width(background=True),
background=True,
family=False,
)
super().scale(scale_factor, about_point=about_point, about_edge=about_edge)
return self
@ -626,6 +643,17 @@ class VMobject(Mobject):
color: ManimColor = property(get_color, set_color)
def nonempty_submobjects(self) -> Sequence[VMobject]:
return [
submob
for submob in self.submobjects
if len(submob.submobjects) != 0 or len(submob.points) != 0
]
def split(self) -> list[VMobject]:
result: list[VMobject] = [self] if len(self.points) > 0 else []
return result + self.submobjects
def set_sheen_direction(self, direction: Vector3DLike, family: bool = True) -> Self:
"""Sets the direction of the applied sheen.
@ -1333,7 +1361,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:])
for i1, i2 in zip(split_indices[:-1], split_indices[1:], strict=True)
if (i2 - i1) >= nppcc
)
@ -1687,7 +1715,7 @@ class VMobject(Mobject):
s = self.get_start_anchors()
e = self.get_end_anchors()
return list(it.chain.from_iterable(zip(s, e)))
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.
@ -1765,7 +1793,9 @@ class VMobject(Mobject):
def get_nth_subpath(path_list, n):
if n >= len(path_list):
# Create a null path at the very end
return [path_list[-1][-1]] * nppcc
if len(path_list) == 0:
return np.tile(np.zeros(3), (nppcc, 1))
return np.tile(path_list[-1][-1], (nppcc, 1))
path = path_list[n]
# Check for useless points at the end of the path and remove them
# https://github.com/ManimCommunity/manim/issues/1959
@ -2299,6 +2329,11 @@ class VGroup(VMobject, metaclass=ConvertToOpenGL):
self._assert_valid_submobjects(tuplify(value))
self.submobjects[key] = value
def __getitem__(self, key: int | slice) -> VMobject:
if isinstance(key, slice):
return VGroup(self.submobjects[key])
return self.submobjects[key]
class VDict(VMobject, metaclass=ConvertToOpenGL):
"""A VGroup-like class, also offering submobject access by

View file

@ -13,7 +13,7 @@ from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from manim.utils.paths import straight_path
if TYPE_CHECKING:
from typing_extensions import Self
from typing import Self
from manim.typing import PathFuncType

View file

@ -791,13 +791,13 @@ class StreamLines(VectorField):
self.stroke_width = stroke_width
half_noise = self.noise_factor / 2
np.random.seed(0)
rng = np.random.default_rng(0)
start_points = np.array(
[
(x - half_noise) * RIGHT
+ (y - half_noise) * UP
+ (z - half_noise) * OUT
+ self.noise_factor * np.random.random(3)
+ self.noise_factor * rng.random(3)
for n in range(self.n_repeats)
for x in np.arange(*self.x_range)
for y in np.arange(*self.y_range)

0
manim/py.typed Normal file
View file

File diff suppressed because it is too large Load diff

View file

@ -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)

View file

@ -6,12 +6,11 @@ import re
import textwrap
from collections.abc import Callable, Iterator, Sequence
from pathlib import Path
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Self, TypeAlias
import moderngl
import numpy as np
import numpy.typing as npt
from typing_extensions import Self, TypeAlias
if TYPE_CHECKING:
from manim.renderer.opengl_renderer import OpenGLRenderer

View file

@ -5,12 +5,11 @@ import logging
import re
from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Self, TypeAlias
import moderngl
import numpy as np
import numpy.typing as npt
from typing_extensions import Self, TypeAlias
if TYPE_CHECKING:
from manim.typing import FloatRGBLike_Array
@ -141,10 +140,9 @@ class ShaderWrapper:
def replace_code(self, old: str, new: str) -> None:
code_map = self.program_code
for name, _code in code_map.items():
if code_map[name] is None:
continue
code_map[name] = re.sub(old, new, code_map[name])
for name, code in code_map.items():
if code:
code_map[name] = re.sub(old, new, code)
self.refresh_id()
def combine_with(self, *shader_wrappers: "ShaderWrapper") -> Self: # noqa: UP037

View file

@ -33,7 +33,7 @@ except ImportError:
dearpygui_imported = False
from collections.abc import Callable, Iterable, Sequence
from typing import TYPE_CHECKING, Any, Union
from typing import TYPE_CHECKING, Any
import numpy as np
from tqdm import tqdm
@ -62,14 +62,13 @@ from ..utils.module_ops import scene_classes_from_file
if TYPE_CHECKING:
from types import FrameType
from typing_extensions import Self, TypeAlias
from typing import Self, TypeAlias
from manim.typing import Point3D
SceneInteractAction: TypeAlias = Union[
MethodWithArgs, "SceneInteractContinue", "SceneInteractRerun"
]
SceneInteractAction: TypeAlias = (
MethodWithArgs | "SceneInteractContinue" | "SceneInteractRerun"
)
"""The SceneInteractAction type alias is used for elements in the queue
used by :meth:`.Scene.interact()`.
@ -178,7 +177,7 @@ class Scene:
) -> None:
self.camera_class = camera_class
self.always_update_mobjects = always_update_mobjects
self.random_seed = random_seed
self.random_seed = random_seed if random_seed is not None else config.seed
self.skip_animations = skip_animations
self.animations: list[Animation] | None = None
@ -220,9 +219,9 @@ class Scene:
self.mobjects: list[Mobject] = []
# TODO, remove need for foreground mobjects
self.foreground_mobjects: list[Mobject] = []
if self.random_seed is not None:
random.seed(self.random_seed)
np.random.seed(self.random_seed)
random.seed(self.random_seed)
np.random.seed(self.random_seed) # noqa: NPY002 (only way to set seed globally)
@property
def camera(self) -> Camera | OpenGLCamera:
@ -904,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 = [

View file

@ -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__
@ -555,22 +566,22 @@ class SceneFileWriter:
partial_movie_file_codec = "qtrle"
partial_movie_file_pix_fmt = "argb"
with av.open(file_path, mode="w") as video_container:
stream = video_container.add_stream(
partial_movie_file_codec,
rate=fps,
options=av_options,
)
stream.pix_fmt = partial_movie_file_pix_fmt
stream.width = config.pixel_width
stream.height = config.pixel_height
video_container = av.open(file_path, mode="w")
stream = video_container.add_stream(
partial_movie_file_codec,
rate=fps,
options=av_options,
)
stream.pix_fmt = partial_movie_file_pix_fmt
stream.width = config.pixel_width
stream.height = config.pixel_height
self.video_container: OutputContainer = video_container
self.video_stream: Stream = stream
self.video_container: OutputContainer = video_container
self.video_stream: Stream = stream
self.queue: Queue[tuple[int, PixelArray | None]] = Queue()
self.writer_thread = Thread(target=self.listen_and_write, args=())
self.writer_thread.start()
self.queue: Queue[tuple[int, PixelArray | None]] = Queue()
self.writer_thread = Thread(target=self.listen_and_write, args=())
self.writer_thread.start()
def close_partial_movie_stream(self) -> None:
"""Close the currently opened video container.
@ -646,18 +657,15 @@ class SceneFileWriter:
output_container.metadata["comment"] = (
f"Rendered with Manim Community v{__version__}"
)
output_stream = output_container.add_stream(
codec_name="gif" if create_gif else None,
template=partial_movies_stream if not create_gif else None,
)
if config.transparent and config.movie_file_extension == ".webm":
output_stream.pix_fmt = "yuva420p"
if create_gif:
"""The following solution was largely inspired from this comment
https://github.com/imageio/imageio/issues/995#issuecomment-1580533018,
and the following code
https://github.com/imageio/imageio/blob/65d79140018bb7c64c0692ea72cb4093e8d632a0/imageio/plugins/pyav.py#L927-L996.
"""
output_stream = output_container.add_stream(
codec_name="gif",
)
output_stream.pix_fmt = "rgb8"
if config.transparent:
output_stream.pix_fmt = "pal8"
@ -702,6 +710,11 @@ class SceneFileWriter:
output_container.mux(packet)
else:
output_stream = output_container.add_stream_from_template(
template=partial_movies_stream,
)
if config.transparent and config.movie_file_extension == ".webm":
output_stream.pix_fmt = "yuva420p"
for packet in partial_movies_input.demux(partial_movies_stream):
# We need to skip the "flushing" packets that `demux` generates.
if packet.dts is None:
@ -789,8 +802,12 @@ class SceneFileWriter:
output_container = av.open(
str(temp_file_path), mode="w", options=av_options
)
output_video_stream = output_container.add_stream(template=video_stream)
output_audio_stream = output_container.add_stream(template=audio_stream)
output_video_stream = output_container.add_stream_from_template(
template=video_stream
)
output_audio_stream = output_container.add_stream_from_template(
template=audio_stream
)
for packet in video_input.demux(video_stream):
# We need to skip the "flushing" packets that `demux` generates.

View file

@ -428,8 +428,8 @@ class ThreeDScene(Scene):
which have the same meaning as the parameters in set_camera_orientation.
"""
config = dict(
self.default_camera_orientation_kwargs,
) # Where doe this come from?
self.default_angled_camera_orientation_kwargs,
)
config.update(kwargs)
self.set_camera_orientation(**config)

View file

@ -35,9 +35,9 @@ from ..utils.color import (
BLUE_D,
GREEN_C,
GREY,
PURE_YELLOW,
RED_C,
WHITE,
YELLOW,
ManimColor,
ParsableManimColor,
)
@ -45,9 +45,10 @@ from ..utils.rate_functions import rush_from, rush_into
from ..utils.space_ops import angle_of_vector
if TYPE_CHECKING:
from typing_extensions import Self
from typing import Self
from manim.typing import (
ManimTextLabel,
MappingFunction,
Point3D,
Point3DLike,
@ -172,7 +173,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:
@ -281,15 +282,19 @@ class VectorScene(Scene):
color (str),
label_scale_factor=VECTOR_LABEL_SCALE_FACTOR (int, float),
"""
i_hat, j_hat = self.get_basis_vectors()
i_hat = self.get_basis_vectors().submobjects[0]
j_hat = self.get_basis_vectors().submobjects[1]
return VGroup(
*(
self.get_vector_label(
vect, label, color=color, label_scale_factor=1, **kwargs
)
for vect, label, color in [
(i_hat, "\\hat{\\imath}", X_COLOR),
(j_hat, "\\hat{\\jmath}", Y_COLOR),
# Casting i_hat and j_hat to Vector, as the VGroup from
# self.get_basis_vectors() contains two vectors, but the
# type checker is currently not aware of that.
(cast(Vector, i_hat), "\\hat{\\imath}", X_COLOR),
(cast(Vector, j_hat), "\\hat{\\jmath}", Y_COLOR),
]
)
)
@ -297,13 +302,13 @@ class VectorScene(Scene):
def get_vector_label(
self,
vector: Vector,
label: MathTex | str,
label: ManimTextLabel | str,
at_tip: bool = False,
direction: str = "left",
rotate: bool = False,
color: ParsableManimColor | None = None,
label_scale_factor: float = LARGE_BUFF - 0.2,
) -> MathTex:
) -> ManimTextLabel:
"""
Returns naming labels for the passed vector.
@ -326,19 +331,18 @@ class VectorScene(Scene):
Returns
-------
MathTex
The MathTex of the label.
:class:`~.ManimTextLabel`
The rendered label mobject.
"""
if not isinstance(label, MathTex):
if isinstance(label, str):
if len(label) == 1:
label = "\\vec{\\textbf{%s}}" % label # noqa: UP031
label = rf"\vec{{\textbf{{{label}}}}}"
label = MathTex(label)
if color is None:
prepared_color: ParsableManimColor = vector.get_color()
else:
prepared_color = color
label.set_color(prepared_color)
assert isinstance(label, MathTex)
label.scale(label_scale_factor)
label.add_background_rectangle()
@ -361,8 +365,12 @@ class VectorScene(Scene):
return label
def label_vector(
self, vector: Vector, label: MathTex | str, animate: bool = True, **kwargs: Any
) -> MathTex:
self,
vector: Vector,
label: ManimTextLabel | str,
animate: bool = True,
**kwargs: Any,
) -> ManimTextLabel:
"""
Shortcut method for creating, and animating the addition of
a label for the vector.
@ -373,7 +381,7 @@ class VectorScene(Scene):
The vector for which the label must be added.
label
The MathTex/string of the label.
The rendered label mobject or the string used to create one.
animate
Whether or not to animate the labelling w/ Write
@ -383,8 +391,8 @@ class VectorScene(Scene):
Returns
-------
:class:`~.MathTex`
The MathTex of the label.
:class:`~.ManimTextLabel`
The rendered label mobject.
"""
mathtex_label = self.get_vector_label(vector, label, **kwargs)
if animate:
@ -517,7 +525,9 @@ class VectorScene(Scene):
y_line = Line(x_line.get_end(), arrow.get_end())
x_line.set_color(X_COLOR)
y_line.set_color(Y_COLOR)
x_coord, y_coord = cast(VGroup, array.get_entries())
temp = array.get_entries()
x_coord = temp.submobjects[0]
y_coord = temp.submobjects[1]
x_coord_start = self.position_x_coordinate(x_coord.copy(), x_line, vector)
y_coord_start = self.position_y_coordinate(y_coord.copy(), y_line, vector)
brackets = array.get_brackets()
@ -682,7 +692,9 @@ class LinearTransformationScene(VectorScene):
default_configs: Iterable[dict[str, Any]],
passed_configs: Iterable[dict[str, Any] | None],
) -> None:
for default_config, passed_config in zip(default_configs, passed_configs):
for default_config, passed_config in zip(
default_configs, passed_configs, strict=False
):
if passed_config is not None:
update_dict_recursively(default_config, passed_config)
@ -695,7 +707,7 @@ class LinearTransformationScene(VectorScene):
self.foreground_mobjects: list[Mobject] = []
self.transformable_mobjects: list[Mobject] = []
self.moving_vectors: list[Mobject] = []
self.transformable_labels: list[MathTex] = []
self.transformable_labels: list[Any] = []
self.moving_mobjects: list[Mobject] = []
self.background_plane = NumberPlane(**self.background_plane_kwargs)
@ -806,7 +818,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:
@ -873,7 +885,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:
@ -963,16 +975,17 @@ class LinearTransformationScene(VectorScene):
"""
# TODO: Clear up types in this function. This is currently a mess.
label_mob = self.label_vector(vector, label, **kwargs)
label_mob_any = cast(Any, label_mob)
if new_label:
label_mob.target_text = new_label # type: ignore[attr-defined]
label_mob_any.target_text = new_label
else:
label_mob.target_text = ( # type: ignore[attr-defined]
label_mob_any.target_text = (
f"{transformation_name}({label_mob.get_tex_string()})"
)
label_mob.vector = vector # type: ignore[attr-defined]
label_mob.kwargs = kwargs # type: ignore[attr-defined]
if "animate" in label_mob.kwargs:
label_mob.kwargs.pop("animate")
label_mob_any.vector = vector
label_mob_any.kwargs = kwargs
if "animate" in label_mob_any.kwargs:
label_mob_any.kwargs.pop("animate")
self.transformable_labels.append(label_mob)
return cast(MathTex, label_mob)
@ -1141,11 +1154,12 @@ class LinearTransformationScene(VectorScene):
for label in self.transformable_labels:
# TODO: This location and lines 933 and 335 are the only locations in
# the code where the target_text property is referenced.
target_text: MathTex | str = label.target_text # type: ignore[assignment]
label_any = cast(Any, label)
target_text: MathTex | str = label_any.target_text
label.target = self.get_vector_label(
label.vector.target, # type: ignore[attr-defined]
label_any.vector.target,
target_text,
**label.kwargs, # type: ignore[arg-type]
**label_any.kwargs,
)
return self.get_piece_movement(self.transformable_labels)

View file

@ -22,11 +22,15 @@ from __future__ import annotations
from collections.abc import Callable, Sequence
from os import PathLike
from typing import Union
from typing import TYPE_CHECKING, TypeAlias
import numpy as np
import numpy.typing as npt
from typing_extensions import TypeAlias
if TYPE_CHECKING:
from manim.mobject.text.tex_mobject import MathTex
from manim.mobject.text.text_mobject import Text
from manim.mobject.text.typst_mobject import Typst
__all__ = [
"ManimFloat",
@ -109,6 +113,7 @@ __all__ = [
"PathFuncType",
"MappingFunction",
"MultiMappingFunction",
"ManimTextLabel",
"PixelArray",
"GrayscalePixelArray",
"RGBPixelArray",
@ -157,7 +162,7 @@ Its components describe, in order, the intensity of Red, Green, and
Blue in the represented color.
"""
FloatRGBLike: TypeAlias = Union[FloatRGB, tuple[float, float, float]]
FloatRGBLike: TypeAlias = FloatRGB | tuple[float, float, float]
"""``shape: (3,)``
An array of 3 floats between 0 and 1, representing a color in RGB
@ -173,7 +178,7 @@ FloatRGB_Array: TypeAlias = npt.NDArray[ManimColorDType]
A :class:`numpy.ndarray` of many rows of 3 floats representing RGB colors.
"""
FloatRGBLike_Array: TypeAlias = Union[FloatRGB_Array, Sequence[FloatRGBLike]]
FloatRGBLike_Array: TypeAlias = FloatRGB_Array | Sequence[FloatRGBLike]
"""``shape: (M, 3)``
An array of many rows of 3 floats representing RGB colors.
@ -192,7 +197,7 @@ Its components describe, in order, the intensity of Red, Green, and
Blue in the represented color.
"""
IntRGBLike: TypeAlias = Union[IntRGB, tuple[int, int, int]]
IntRGBLike: TypeAlias = IntRGB | tuple[int, int, int]
"""``shape: (3,)``
An array of 3 integers between 0 and 255, representing a color in RGB
@ -212,7 +217,7 @@ Its components describe, in order, the intensity of Red, Green, Blue
and Alpha (opacity) in the represented color.
"""
FloatRGBALike: TypeAlias = Union[FloatRGBA, tuple[float, float, float, float]]
FloatRGBALike: TypeAlias = FloatRGBA | tuple[float, float, float, float]
"""``shape: (4,)``
An array of 4 floats between 0 and 1, representing a color in RGBA
@ -228,7 +233,7 @@ FloatRGBA_Array: TypeAlias = npt.NDArray[ManimColorDType]
A :class:`numpy.ndarray` of many rows of 4 floats representing RGBA colors.
"""
FloatRGBALike_Array: TypeAlias = Union[FloatRGBA_Array, Sequence[FloatRGBALike]]
FloatRGBALike_Array: TypeAlias = FloatRGBA_Array | Sequence[FloatRGBALike]
"""``shape: (M, 4)``
An array of many rows of 4 floats representing RGBA colors.
@ -247,7 +252,7 @@ Its components describe, in order, the intensity of Red, Green, Blue
and Alpha (opacity) in the represented color.
"""
IntRGBALike: TypeAlias = Union[IntRGBA, tuple[int, int, int, int]]
IntRGBALike: TypeAlias = IntRGBA | tuple[int, int, int, int]
"""``shape: (4,)``
An array of 4 integers between 0 and 255, representing a color in RGBA
@ -344,7 +349,7 @@ Point2D: TypeAlias = npt.NDArray[PointDType]
A NumPy array representing a 2-dimensional point: ``[float, float]``.
"""
Point2DLike: TypeAlias = Union[Point2D, tuple[float, float]]
Point2DLike: TypeAlias = Point2D | tuple[float, float]
"""``shape: (2,)``
A 2-dimensional point: ``[float, float]``.
@ -360,7 +365,7 @@ A NumPy array representing a sequence of :class:`.Point2D` objects:
``[[float, float], ...]``.
"""
Point2DLike_Array: TypeAlias = Union[Point2D_Array, Sequence[Point2DLike]]
Point2DLike_Array: TypeAlias = Point2D_Array | Sequence[Point2DLike]
"""``shape: (M, 2)``
An array of :class:`.Point2DLike` objects: ``[[float, float], ...]``.
@ -378,7 +383,7 @@ Point3D: TypeAlias = npt.NDArray[PointDType]
A NumPy array representing a 3-dimensional point: ``[float, float, float]``.
"""
Point3DLike: TypeAlias = Union[Point3D, tuple[float, float, float]]
Point3DLike: TypeAlias = Point3D | tuple[float, float, float]
"""``shape: (3,)``
A 3-dimensional point: ``[float, float, float]``.
@ -394,7 +399,7 @@ A NumPy array representing a sequence of :class:`.Point3D` objects:
``[[float, float, float], ...]``.
"""
Point3DLike_Array: TypeAlias = Union[Point3D_Array, Sequence[Point3DLike]]
Point3DLike_Array: TypeAlias = Point3D_Array | Sequence[Point3DLike]
"""``shape: (M, 3)``
An array of :class:`.Point3DLike` objects: ``[[float, float, float], ...]``.
@ -412,7 +417,7 @@ PointND: TypeAlias = npt.NDArray[PointDType]
A NumPy array representing an N-dimensional point: ``[float, ...]``.
"""
PointNDLike: TypeAlias = Union[PointND, Sequence[float]]
PointNDLike: TypeAlias = PointND | Sequence[float]
"""``shape: (N,)``
An N-dimensional point: ``[float, ...]``.
@ -428,7 +433,7 @@ A NumPy array representing a sequence of :class:`.PointND` objects:
``[[float, ...], ...]``.
"""
PointNDLike_Array: TypeAlias = Union[PointND_Array, Sequence[PointNDLike]]
PointNDLike_Array: TypeAlias = PointND_Array | Sequence[PointNDLike]
"""``shape: (M, N)``
An array of :class:`.PointNDLike` objects: ``[[float, ...], ...]``.
@ -456,7 +461,7 @@ A NumPy array representing a 2-dimensional vector: ``[float, float]``.
VMobjects!
"""
Vector2DLike: TypeAlias = Union[npt.NDArray[PointDType], tuple[float, float]]
Vector2DLike: TypeAlias = npt.NDArray[PointDType] | tuple[float, float]
"""``shape: (2,)``
A 2-dimensional vector: ``[float, float]``.
@ -476,7 +481,7 @@ A NumPy array representing a sequence of :class:`.Vector2D` objects:
``[[float, float], ...]``.
"""
Vector2DLike_Array: TypeAlias = Union[Vector2D_Array, Sequence[Vector2DLike]]
Vector2DLike_Array: TypeAlias = Vector2D_Array | Sequence[Vector2DLike]
"""``shape: (M, 2)``
An array of :class:`.Vector2DLike` objects: ``[[float, float], ...]``.
@ -495,7 +500,7 @@ A NumPy array representing a 3-dimensional vector: ``[float, float, float]``.
VMobjects!
"""
Vector3DLike: TypeAlias = Union[npt.NDArray[PointDType], tuple[float, float, float]]
Vector3DLike: TypeAlias = npt.NDArray[PointDType] | tuple[float, float, float]
"""``shape: (3,)``
A 3-dimensional vector: ``[float, float, float]``.
@ -515,7 +520,7 @@ An NumPy array representing a sequence of :class:`.Vector3D` objects:
``[[float, float, float], ...]``.
"""
Vector3DLike_Array: TypeAlias = Union[npt.NDArray[PointDType], Sequence[Vector3DLike]]
Vector3DLike_Array: TypeAlias = npt.NDArray[PointDType] | Sequence[Vector3DLike]
"""``shape: (M, 3)``
An array of :class:`.Vector3DLike` objects: ``[[float, float, float], ...]``.
@ -535,7 +540,7 @@ A NumPy array representing an :math:`N`-dimensional vector: ``[float, ...]``.
collisions.
"""
VectorNDLike: TypeAlias = Union[npt.NDArray[PointDType], Sequence[float]]
VectorNDLike: TypeAlias = npt.NDArray[PointDType] | Sequence[float]
"""``shape (N,)``
An :math:`N`-dimensional vector: ``[float, ...]``.
@ -556,7 +561,7 @@ A NumPy array representing a sequence of :class:`.VectorND` objects:
``[[float, ...], ...]``.
"""
VectorNDLike_Array: TypeAlias = Union[npt.NDArray[PointDType], Sequence[VectorNDLike]]
VectorNDLike_Array: TypeAlias = npt.NDArray[PointDType] | Sequence[VectorNDLike]
"""``shape (M, N)``
An array of :class:`.VectorNDLike` objects: ``[[float, ...], ...]``.
@ -610,9 +615,9 @@ curve:
``[[float, float, float], [float, float, float], [float, float, float]]``.
"""
QuadraticBezierPointsLike: TypeAlias = Union[
QuadraticBezierPoints, tuple[Point3DLike, Point3DLike, Point3DLike]
]
QuadraticBezierPointsLike: TypeAlias = (
QuadraticBezierPoints | tuple[Point3DLike, Point3DLike, Point3DLike]
)
"""``shape: (3, 3)``
A :class:`.Point3DLike_Array` of three 3D control points for a single quadratic Bézier
@ -630,9 +635,9 @@ A NumPy array containing :math:`N` :class:`.QuadraticBezierPoints` objects:
``[[[float, float, float], [float, float, float], [float, float, float]], ...]``.
"""
QuadraticBezierPointsLike_Array: TypeAlias = Union[
QuadraticBezierPoints_Array, Sequence[QuadraticBezierPointsLike]
]
QuadraticBezierPointsLike_Array: TypeAlias = (
QuadraticBezierPoints_Array | Sequence[QuadraticBezierPointsLike]
)
"""``shape: (N, 3, 3)``
A sequence of :math:`N` :class:`.QuadraticBezierPointsLike` objects:
@ -701,9 +706,9 @@ A :class:`.Point3D_Array` of four 3D control points for a single cubic Bézier c
``[[float, float, float], [float, float, float], [float, float, float], [float, float, float]]``.
"""
CubicBezierPointsLike: TypeAlias = Union[
CubicBezierPoints, tuple[Point3DLike, Point3DLike, Point3DLike, Point3DLike]
]
CubicBezierPointsLike: TypeAlias = (
CubicBezierPoints | tuple[Point3DLike, Point3DLike, Point3DLike, Point3DLike]
)
"""``shape: (4, 3)``
A :class:`.Point3DLike_Array` of 4 control points for a single cubic Bézier curve:
@ -720,9 +725,9 @@ A NumPy array containing :math:`N` :class:`.CubicBezierPoints` objects:
``[[[float, float, float], [float, float, float], [float, float, float], [float, float, float]], ...]``.
"""
CubicBezierPointsLike_Array: TypeAlias = Union[
CubicBezierPoints_Array, Sequence[CubicBezierPointsLike]
]
CubicBezierPointsLike_Array: TypeAlias = (
CubicBezierPoints_Array | Sequence[CubicBezierPointsLike]
)
"""``shape: (N, 4, 3)``
A sequence of :math:`N` :class:`.CubicBezierPointsLike` objects:
@ -823,9 +828,7 @@ Please refer to the documentation of the function you are using for
further type information.
"""
BezierPointsLike_Array: TypeAlias = Union[
BezierPoints_Array, Sequence[BezierPointsLike]
]
BezierPointsLike_Array: TypeAlias = BezierPoints_Array | Sequence[BezierPointsLike]
r"""``shape: (N, PPC, 3)``
A sequence of :math:`N` :class:`.BezierPointsLike` objects containing
@ -898,7 +901,7 @@ Please refer to the documentation of the function you are using for
further type information.
"""
FlatBezierPoints: TypeAlias = Union[npt.NDArray[PointDType], tuple[float, ...]]
FlatBezierPoints: TypeAlias = npt.NDArray[PointDType] | tuple[float, ...]
"""``shape: (3*PPC*N,)``
A flattened array of Bézier control points:
@ -934,6 +937,19 @@ MultiMappingFunction: TypeAlias = Callable[[Point3D_Array], Point3D_Array]
:class:`.Point3D_Array`.
"""
"""
[CATEGORY]
Text mobject types
"""
ManimTextLabel: TypeAlias = "Text | MathTex | Typst"
"""Text-like label mobjects commonly used across Manim.
This includes :class:`~.Text`, :class:`~.MathTex`, and :class:`~.Typst`.
Subtype-specific variants like :class:`~.Tex` and :class:`~.TypstMath` are
covered implicitly through inheritance.
"""
"""
[CATEGORY]
Image types
@ -979,12 +995,12 @@ value is an :class:`.RGBA_Array_Int` object.
Path types
"""
StrPath: TypeAlias = Union[str, PathLike[str]]
StrPath: TypeAlias = str | PathLike[str]
"""A string or :class:`.os.PathLike` representing a path to a
directory or file.
"""
StrOrBytesPath: TypeAlias = Union[str, bytes, PathLike[str], PathLike[bytes]]
StrOrBytesPath: TypeAlias = str | bytes | PathLike[str] | PathLike[bytes]
"""A string, bytes or :class:`.os.PathLike` object representing a path
to a directory or file.
"""

View file

@ -90,6 +90,7 @@ def bezier(
containing :math:`n` values to evaluate the Bézier curve at, returning instead
an :math:`(n, 3)`-shaped :class:`~.Point3D_Array` containing the points
resulting from evaluating the Bézier at each of the :math:`n` values.
.. warning::
If passing a vector of :math:`t`-values to ``bezier_func``, it **must**
be a column vector/matrix of shape :math:`(n, 1)`. Passing an 1D array of
@ -106,6 +107,7 @@ def bezier(
Bézier curve defined by ``points`` is evaluated at the corresponding :math:`i`-th
value in ``t``, returning again an :math:`(M, 3)`-shaped :class:`~.Point3D_Array`
containing those :math:`M` evaluations.
.. warning::
Unlike the previous case, if you pass a :class:`~.ColVector` to ``bezier_func``,
it **must** contain exactly :math:`M` values, each value for each of the :math:`M`
@ -999,7 +1001,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):
for curve, sf in zip(bezier_tuples, split_factors, strict=True):
new_tuples[index : index + sf] = subdivide_bezier(curve, sf).reshape(
sf, nppc, dim
)
@ -1234,6 +1236,7 @@ def match_interpolate(
Examples
--------
>>> from manim import match_interpolate
>>> match_interpolate(0, 100, 10, 20, 15)
np.float64(50.0)
"""

View file

@ -48,9 +48,8 @@ def handle_caching_play(func: Callable[..., None]) -> Callable[..., None]:
return
if not config["disable_caching"]:
mobjects_on_scene = scene.mobjects
# TODO: the first argument seems wrong. Shouldn't it be scene instead?
hash_play = get_hash_from_play_call(
self, # type: ignore[arg-type]
scene,
self.camera,
animations,
mobjects_on_scene,

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