mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-22 10:01:47 +00:00
Compare commits
43 commits
experiment
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e83f4b09a |
||
|
|
f7fd708276 |
||
|
|
4c4622df54 |
||
|
|
66d5a4937a |
||
|
|
c07137dbed |
||
|
|
71ab85f960 |
||
|
|
16f0a3de3e |
||
|
|
537a134360 |
||
|
|
b7bf2ea90f |
||
|
|
bdaf4497b7 |
||
|
|
037b376ec2 |
||
|
|
e9639c2697 |
||
|
|
1b2d5ce72b |
||
|
|
c94a7ea9fc |
||
|
|
bb1be6ef8a |
||
|
|
516c8c8ba7 |
||
|
|
ccee37a614 |
||
|
|
31db147222 |
||
|
|
34124c3f60 |
||
|
|
d999d422c9 |
||
|
|
561de9d72a |
||
|
|
05b3042ab0 |
||
|
|
2ece488b2c |
||
|
|
852ebd1c60 |
||
|
|
82522795f1 |
||
|
|
12c5640a32 |
||
|
|
33424fe43d |
||
|
|
1b3390073c |
||
|
|
56f7eb2a1f |
||
|
|
cfb5c684b7 |
||
|
|
429f25328d |
||
|
|
c45724989d |
||
|
|
af70b6fef2 |
||
|
|
4b32312dd1 |
||
|
|
90141df105 |
||
|
|
82f93b6c3c |
||
|
|
752b46a003 |
||
|
|
21cf9998cc |
||
|
|
ebb230f6f1 |
||
|
|
46177d247e |
||
|
|
468929889b |
||
|
|
98c458b6b2 |
||
|
|
d4af5b2baa |
280 changed files with 18160 additions and 9237 deletions
|
|
@ -1 +0,0 @@
|
|||
BasedOnStyle: Microsoft
|
||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
|
|
@ -104,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
|
||||
|
|
@ -129,7 +129,7 @@ jobs:
|
|||
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'
|
||||
|
|
@ -137,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
|
||||
|
|
|
|||
16
.github/workflows/publish-docker.yml
vendored
16
.github/workflows/publish-docker.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/python-publish.yml
vendored
2
.github/workflows/python-publish.yml
vendored
|
|
@ -33,7 +33,7 @@ jobs:
|
|||
uv publish
|
||||
|
||||
- name: Store artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: dist/*.tar.gz
|
||||
name: manim.tar.gz
|
||||
|
|
|
|||
|
|
@ -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@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: ${{ github.workspace }}/docs/build/html-docs.tar.gz
|
||||
name: html-docs.tar.gz
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ Manim is an animation engine for explanatory math videos. It's used to create pr
|
|||
|
||||
## Installation
|
||||
|
||||
> [!WARNING]
|
||||
> [!CAUTION]
|
||||
> These instructions are for the community version _only_. Trying to use these instructions to install [3b1b/manim](https://github.com/3b1b/manim) or instructions there to install this version will cause problems. Read [this](https://docs.manim.community/en/stable/faq/installation.html#why-are-there-different-versions-of-manim) and decide which version you wish to install, then only follow the instructions for your desired version.
|
||||
|
||||
Manim requires a few dependencies that must be installed prior to using it. If you
|
||||
|
|
@ -71,7 +71,7 @@ In order to view the output of this scene, save the code in a file called `examp
|
|||
manim -p -ql example.py SquareToCircle
|
||||
```
|
||||
|
||||
You should see a window pop up and play a simple scene in which a square is transformed into a circle. You may find some more simple examples within this
|
||||
You should see your native video player program pop up and play a simple scene in which a square is transformed into a circle. You may find some more simple examples within this
|
||||
[GitHub repository](example_scenes). You can also visit the [official gallery](https://docs.manim.community/en/stable/examples.html) for more advanced examples.
|
||||
|
||||
Manim also ships with a `%%manim` IPython magic which allows to use it conveniently in JupyterLab (as well as classic Jupyter) notebooks. See the
|
||||
|
|
@ -84,8 +84,7 @@ The general usage of Manim is as follows:
|
|||
|
||||

|
||||
|
||||
The `-p` flag in the command above is for previewing, meaning a window will show up to render it in real time.
|
||||
The `-ql` flag is for a faster rendering at a lower quality.
|
||||
The `-p` flag in the command above is for previewing, meaning the video file will automatically open when it is done rendering. The `-ql` flag is for a faster rendering at a lower quality.
|
||||
|
||||
Some other useful flags include:
|
||||
|
||||
|
|
|
|||
382
agents/typst_selector.md
Normal file
382
agents/typst_selector.md
Normal 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.
|
||||
|
|
@ -19,7 +19,7 @@ msgid "Bases: :py:class:`manim.mobject.three_d.three_dimensions.Cylinder`"
|
|||
msgstr ""
|
||||
|
||||
#: ../../../manim/mobject/three_d/three_dimensions.py:docstring of manim.mobject.three_d.three_dimensions.Line3D:1
|
||||
msgid "A cylindrical line."
|
||||
msgid "A cylindrical line, for use in ThreeDScene."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/mobject/three_d/three_dimensions.py:docstring of manim.mobject.three_d.three_dimensions.Line3D:4
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ msgid "A spherical dot."
|
|||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.mobject.three_d.three_dimensions.rst:40:<autosummary>:1
|
||||
msgid "A cylindrical line."
|
||||
msgid "A cylindrical line, for use in ThreeDScene."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.mobject.three_d.three_dimensions.rst:40:<autosummary>:1
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Manim \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:2
|
||||
msgid "SpecialThreeDScene"
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:4
|
||||
msgid "Qualified name: ``manim.scene.three\\_d\\_scene.SpecialThreeDScene``"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene:1
|
||||
msgid "Bases: :py:class:`manim.scene.three_d_scene.ThreeDScene`"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene:1
|
||||
msgid "An extension of :class:`ThreeDScene` with more settings."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene:3
|
||||
msgid "It has some extra configuration for axes, spheres, and an override for low quality rendering. Further key differences are:"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene:7
|
||||
msgid "The camera shades applicable 3DMobjects by default, except if rendering in low quality."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene:9
|
||||
msgid "Some default params for Spheres and Axes have been added."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:14
|
||||
msgid "Methods"
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:23:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_axes:1
|
||||
msgid "Return a set of 3D axes."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:23:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_default_camera_position:1
|
||||
msgid "Returns the default_angled_camera position."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:23:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:1
|
||||
msgid "Returns a sphere with the passed keyword arguments as properties."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:23:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.set_camera_to_default_position:1
|
||||
msgid "Sets the camera to its default position."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.SpecialThreeDScene.rst:25
|
||||
msgid "Attributes"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_axes:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_default_camera_position:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:0
|
||||
msgid "Returns"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_axes:3
|
||||
msgid "A set of 3D axes."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_axes:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_default_camera_position:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:0
|
||||
msgid "Return type"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_default_camera_position:3
|
||||
msgid "Dictionary of phi, theta, focal_distance, and gamma."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:0
|
||||
msgid "Parameters"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:3
|
||||
msgid "Any valid parameter of :class:`~.Sphere` or :class:`~.Surface`."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.SpecialThreeDScene.get_sphere:5
|
||||
msgid "The sphere object."
|
||||
msgstr ""
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Manim \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:2
|
||||
msgid "ThreeDScene"
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:4
|
||||
msgid "Qualified name: ``manim.scene.three\\_d\\_scene.ThreeDScene``"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene:1
|
||||
msgid "Bases: :py:class:`manim.scene.scene.Scene`"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene:1
|
||||
msgid "This is a Scene, with special configurations and properties that make it suitable for Three Dimensional Scenes."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:14
|
||||
msgid "Methods"
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
msgid "This method is used to prevent the rotation and movement of mobjects as the camera moves around."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
msgid "This method is used to prevent the rotation and tilting of mobjects as the camera moves around."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_3dillusion_camera_rotation:1
|
||||
msgid "This method creates a 3D camera rotation illusion around the current camera orientation."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_ambient_camera_rotation:1
|
||||
msgid "This method begins an ambient rotation of the camera about the Z_AXIS, in the anticlockwise direction"
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.get_moving_mobjects:1
|
||||
msgid "This method returns a list of all of the Mobjects in the Scene that are moving, that are also in the animations passed."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:1
|
||||
msgid "This method animates the movement of the camera to the given spherical coordinates."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
msgid "This method undoes what add_fixed_in_frame_mobjects does."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
msgid "This method \"unfixes\" the orientation of the mobjects passed, meaning they will no longer be at the same angle relative to the camera."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:1
|
||||
msgid "This method sets the orientation of the camera in the scene."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_to_default_angled_camera_orientation:1
|
||||
msgid "This method sets the default_angled_camera_orientation to the keyword arguments passed, and sets the camera to that orientation."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.stop_3dillusion_camera_rotation:1
|
||||
msgid "This method stops all illusion camera rotations."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:31:<autosummary>:1
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.stop_ambient_camera_rotation:1
|
||||
msgid "This method stops all ambient camera rotation."
|
||||
msgstr ""
|
||||
|
||||
#: ../../source/reference/manim.scene.three_d_scene.ThreeDScene.rst:33
|
||||
msgid "Attributes"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_in_frame_mobjects:1
|
||||
msgid "This method is used to prevent the rotation and movement of mobjects as the camera moves around. The mobject is essentially overlaid, and is not impacted by the camera's movement in any way."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_in_frame_mobjects:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_3dillusion_camera_rotation:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_ambient_camera_rotation:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.get_moving_mobjects:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_in_frame_mobjects:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_orientation_mobjects:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:0
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_to_default_angled_camera_orientation:0
|
||||
msgid "Parameters"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_in_frame_mobjects:6
|
||||
msgid "The Mobjects whose orientation must be fixed."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:1
|
||||
msgid "This method is used to prevent the rotation and tilting of mobjects as the camera moves around. The mobject can still move in the x,y,z directions, but will always be at the angle (relative to the camera) that it was at when it was passed through this method.)"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:7
|
||||
msgid "The Mobject(s) whose orientation must be fixed."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:9
|
||||
msgid "Some valid kwargs are use_static_center_func : bool center_func : function"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:11
|
||||
msgid "Some valid kwargs are"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.add_fixed_orientation_mobjects:11
|
||||
msgid "use_static_center_func : bool center_func : function"
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_3dillusion_camera_rotation:4
|
||||
msgid "The rate at which the camera rotation illusion should operate."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_3dillusion_camera_rotation:5
|
||||
msgid "The polar angle the camera should move around. Defaults to the current phi angle."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_3dillusion_camera_rotation:7
|
||||
msgid "The azimutal angle the camera should move around. Defaults to the current theta angle."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_ambient_camera_rotation:4
|
||||
msgid "The rate at which the camera should rotate about the Z_AXIS. Negative rate means clockwise rotation."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.begin_ambient_camera_rotation:7
|
||||
msgid "one of 3 options: [\"theta\", \"phi\", \"gamma\"]. defaults to theta."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.get_moving_mobjects:4
|
||||
msgid "The animations whose mobjects will be checked."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:4
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:3
|
||||
msgid "The polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:6
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:5
|
||||
msgid "The azimuthal angle i.e the angle that spins the camera around the Z_AXIS."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:8
|
||||
msgid "The radial focal_distance between ORIGIN and Camera."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:10
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:9
|
||||
msgid "The rotation of the camera about the vector from the ORIGIN to the Camera."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:12
|
||||
msgid "The zoom factor of the camera."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:14
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:13
|
||||
msgid "The new center of the camera frame in cartesian coordinates."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.move_camera:16
|
||||
msgid "Any other animations to be played at the same time."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_in_frame_mobjects:1
|
||||
msgid "This method undoes what add_fixed_in_frame_mobjects does. It allows the mobject to be affected by the movement of the camera."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_in_frame_mobjects:5
|
||||
msgid "The Mobjects whose position and orientation must be unfixed."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_orientation_mobjects:1
|
||||
msgid "This method \"unfixes\" the orientation of the mobjects passed, meaning they will no longer be at the same angle relative to the camera. This only makes sense if the mobject was passed through add_fixed_orientation_mobjects first."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.remove_fixed_orientation_mobjects:6
|
||||
msgid "The Mobjects whose orientation must be unfixed."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:7
|
||||
msgid "The focal_distance of the Camera."
|
||||
msgstr ""
|
||||
|
||||
#: ../../../manim/scene/three_d_scene.py:docstring of manim.scene.three_d_scene.ThreeDScene.set_camera_orientation:11
|
||||
msgid "The zoom factor of the scene."
|
||||
msgstr ""
|
||||
|
||||
|
||||
|
|
@ -5,3 +5,4 @@ sphinx-copybutton
|
|||
sphinxext-opengraph
|
||||
sphinx-design
|
||||
sphinx-reredirects
|
||||
typst>=0.14
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
jupyterlab
|
||||
sphinxcontrib-programoutput
|
||||
typst>=0.14
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ This page contains a list of changes made between releases.
|
|||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
changelog/experimental
|
||||
changelog/0.20.1-changelog
|
||||
changelog/0.20.0-changelog
|
||||
changelog/0.19.2-changelog
|
||||
|
|
|
|||
|
|
@ -1,97 +0,0 @@
|
|||
# Migrating from v0.19.0 to v0.20.0
|
||||
|
||||
This constitutes a list of all the changes needed to migrate your code
|
||||
to work with the latest version of Manim
|
||||
|
||||
## Manager
|
||||
If you ever used `Scene.render`, you must replace it with {class}`.Manager`.
|
||||
|
||||
Original code:
|
||||
```py
|
||||
scene = SceneClass()
|
||||
scene.render()
|
||||
```
|
||||
should be changed to:
|
||||
```py
|
||||
with Manager(SceneClass) as manager:
|
||||
manager.render()
|
||||
```
|
||||
|
||||
If you are a plugin author that subclasses `Scene` and changed `Scene.render`, you should migrate
|
||||
your code to use the specific public methods on {class}`.Manager` instead.
|
||||
|
||||
## ThreeDScene and Camera
|
||||
`ThreeDScene` has been completely removed, and all of its functionality has been replaced
|
||||
with methods on {class}`.Camera`, which can be accessed via {attr}`.Scene.camera`.
|
||||
|
||||
For example, the following code
|
||||
```py
|
||||
class MyScene(ThreeDScene):
|
||||
def construct(self):
|
||||
t = Text("Hello")
|
||||
self.add_fixed_in_frame_mobjects(t)
|
||||
self.begin_ambient_camera_rotation()
|
||||
self.wait(3)
|
||||
```
|
||||
should be changed to
|
||||
```py
|
||||
# change ThreeDScene -> Scene
|
||||
class MyScene(Scene):
|
||||
def construct(self):
|
||||
t = Text("Hello")
|
||||
# add_fixed_in_frame_mobjects() no longer exists.
|
||||
# Now you must use Mobject.fix_in_frame() manually for each Mobject.
|
||||
t.fix_in_frame()
|
||||
self.add(t)
|
||||
|
||||
# access the method on the camera
|
||||
self.camera.begin_ambient_rotation()
|
||||
self.add(self.camera)
|
||||
self.wait(3)
|
||||
```
|
||||
|
||||
## Animation
|
||||
`Animation.interpolate_mobject` has been combined into `Animation.interpolate`.
|
||||
|
||||
Methods `Animation._setup_scene` and `Animation.clean_up_from_scene` have been removed
|
||||
in favor of `Animation.begin` and `Animation.finish`. If you need to access the scene,
|
||||
you can use a simple buffer to communicate. Note that this buffer cannot access
|
||||
methods on the {class}`.Scene`, but can only do basic actions like {meth}`.Scene.add`,
|
||||
{meth}`.Scene.remove`, and {meth}`.Scene.replace`.
|
||||
|
||||
For example, the following code:
|
||||
```py
|
||||
class MyAnimation(Animation):
|
||||
def begin(self) -> None:
|
||||
self._sqrs = VGroup(Square())
|
||||
|
||||
def _setup_scene(self, scene: Scene) -> None:
|
||||
scene.add(self._sqrs)
|
||||
self.scene = scene
|
||||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
sqr = Square().move_to((alpha, 0, 0))
|
||||
self._sqrs.add(sqr)
|
||||
self.scene.add(sqr)
|
||||
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
scene.remove(self._sqrs)
|
||||
```
|
||||
|
||||
should be changed to
|
||||
```py
|
||||
class MyAnimation(Animation):
|
||||
def begin(self) -> None:
|
||||
self._sqrs = VGroup(Square())
|
||||
self.buffer.add(self._sqrs)
|
||||
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
sqr = Square().move_to((alpha, 0, 0))
|
||||
self._sqrs.add(sqr)
|
||||
self.buffer.add(sqr)
|
||||
# tell the scene to empty the buffer
|
||||
self.apply_buffer = True
|
||||
|
||||
def finish(self) -> None:
|
||||
self.buffer.remove(self._sqrs)
|
||||
```
|
||||
|
|
@ -24,8 +24,8 @@ to the bottom of the file:
|
|||
.. code-block:: python
|
||||
|
||||
with tempconfig({"quality": "medium_quality", "disable_caching": True}):
|
||||
manager = Manager(SceneName)
|
||||
manager.render()
|
||||
scene = SceneName()
|
||||
scene.render()
|
||||
|
||||
Where ``SceneName`` is the name of the scene you want to run. You can then run the
|
||||
file directly, and can thus follow the instructions for most profilers.
|
||||
|
|
@ -58,8 +58,8 @@ to ``square_to_circle.py``:
|
|||
|
||||
|
||||
with tempconfig({"quality": "medium_quality", "disable_caching": True}):
|
||||
manager = Manager(SquareToCircle)
|
||||
manager.render()
|
||||
scene = SquareToCircle()
|
||||
scene.render()
|
||||
|
||||
Now run the following in the terminal:
|
||||
|
||||
|
|
|
|||
|
|
@ -213,11 +213,11 @@ The decorator can be used with or without parentheses. **By default, the test on
|
|||
circle = Circle()
|
||||
scene.play(Animation(circle))
|
||||
|
||||
You can also specify, when needed, which base scene you need (VectorScene, for example) :
|
||||
You can also specify, when needed, which base scene you need (ThreeDScene, for example) :
|
||||
|
||||
.. code:: python
|
||||
|
||||
@frames_comparison(last_frame=False, base_scene=VectorScene)
|
||||
@frames_comparison(last_frame=False, base_scene=ThreeDScene)
|
||||
def test_circle(scene):
|
||||
circle = Circle()
|
||||
scene.play(Animation(circle))
|
||||
|
|
|
|||
|
|
@ -597,25 +597,25 @@ Special Camera Settings
|
|||
|
||||
.. manim:: FixedInFrameMObjectTest
|
||||
:save_last_frame:
|
||||
:ref_classes: Scene
|
||||
:ref_methods: Camera.set_orientation OpenGLMobject.fix_in_frame
|
||||
:ref_classes: ThreeDScene
|
||||
:ref_methods: ThreeDScene.set_camera_orientation ThreeDScene.add_fixed_in_frame_mobjects
|
||||
|
||||
class FixedInFrameMObjectTest(Scene):
|
||||
class FixedInFrameMObjectTest(ThreeDScene):
|
||||
def construct(self):
|
||||
axes = ThreeDAxes()
|
||||
self.camera.set_orientation(theta=-45 * DEGREES, phi=75 * DEGREES)
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=-45 * DEGREES)
|
||||
text3d = Text("This is a 3D text")
|
||||
text3d.fix_in_frame()
|
||||
self.add_fixed_in_frame_mobjects(text3d)
|
||||
text3d.to_corner(UL)
|
||||
self.add(axes)
|
||||
self.wait()
|
||||
|
||||
.. manim:: ThreeDLightSourcePosition
|
||||
:save_last_frame:
|
||||
:ref_classes: Scene ThreeDAxes Surface
|
||||
:ref_methods: Camera.set_orientation
|
||||
:ref_classes: ThreeDScene ThreeDAxes Surface
|
||||
:ref_methods: ThreeDScene.set_camera_orientation
|
||||
|
||||
class ThreeDLightSourcePosition(Scene):
|
||||
class ThreeDLightSourcePosition(ThreeDScene):
|
||||
def construct(self):
|
||||
axes = ThreeDAxes()
|
||||
sphere = Surface(
|
||||
|
|
@ -626,57 +626,49 @@ Special Camera Settings
|
|||
]), v_range=[0, TAU], u_range=[-PI / 2, PI / 2],
|
||||
checkerboard_colors=[RED_D, RED_E], resolution=(15, 32)
|
||||
)
|
||||
# TODO: implement light source
|
||||
self.camera.light_source.move_to(3*IN) # changes the source of the light
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
self.renderer.camera.light_source.move_to(3*IN) # changes the source of the light
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
self.add(axes, sphere)
|
||||
|
||||
|
||||
.. manim:: ThreeDCameraRotation
|
||||
:ref_classes: Circle Scene ThreeDAxes
|
||||
:ref_methods: Camera.begin_ambient_rotation Camera.stop_ambient_rotation
|
||||
:ref_classes: ThreeDScene ThreeDAxes
|
||||
:ref_methods: ThreeDScene.begin_ambient_camera_rotation ThreeDScene.stop_ambient_camera_rotation
|
||||
|
||||
class ThreeDCameraRotation(Scene):
|
||||
class ThreeDCameraRotation(ThreeDScene):
|
||||
def construct(self):
|
||||
axes = ThreeDAxes()
|
||||
circle = Circle()
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
circle=Circle()
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
self.add(circle,axes)
|
||||
self.camera.begin_ambient_rotation(rate=0.1)
|
||||
self.add(self.camera)
|
||||
self.begin_ambient_camera_rotation(rate=0.1)
|
||||
self.wait()
|
||||
self.camera.stop_ambient_rotation()
|
||||
self.play(
|
||||
self.camera.animate.set_orientation(
|
||||
theta=30 * DEGREES, phi=75 * DEGREES
|
||||
),
|
||||
)
|
||||
self.stop_ambient_camera_rotation()
|
||||
self.move_camera(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
self.wait()
|
||||
|
||||
.. manim:: ThreeDCameraPrecession
|
||||
:ref_classes: Circle Scene ThreeDAxes
|
||||
:ref_methods: Camera.begin_precession Camera.stop_precession
|
||||
.. manim:: ThreeDCameraIllusionRotation
|
||||
:ref_classes: ThreeDScene ThreeDAxes
|
||||
:ref_methods: ThreeDScene.begin_3dillusion_camera_rotation ThreeDScene.stop_3dillusion_camera_rotation
|
||||
|
||||
class ThreeDCameraPrecession(Scene):
|
||||
class ThreeDCameraIllusionRotation(ThreeDScene):
|
||||
def construct(self):
|
||||
axes = ThreeDAxes()
|
||||
circle = Circle()
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
circle=Circle()
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
self.add(circle,axes)
|
||||
self.camera.begin_precession(rate=2)
|
||||
self.add(self.camera)
|
||||
self.begin_3dillusion_camera_rotation(rate=2)
|
||||
self.wait(PI/2)
|
||||
self.camera.stop_precession()
|
||||
self.stop_3dillusion_camera_rotation()
|
||||
|
||||
.. manim:: ThreeDSurfacePlot
|
||||
:save_last_frame:
|
||||
:ref_classes: Scene Surface
|
||||
:ref_methods: Camera.set_orientation
|
||||
:ref_classes: ThreeDScene Surface
|
||||
|
||||
class ThreeDSurfacePlot(Scene):
|
||||
class ThreeDSurfacePlot(ThreeDScene):
|
||||
def construct(self):
|
||||
resolution_fa = 24
|
||||
self.camera.set_orientation(theta=-30 * DEGREES, phi=75 * DEGREES)
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=-30 * DEGREES)
|
||||
|
||||
def param_gauss(u, v):
|
||||
x = u
|
||||
|
|
|
|||
|
|
@ -357,10 +357,10 @@ A list of all config options
|
|||
'log_dir', 'log_to_file', 'max_files_cached', 'media_dir', 'media_width',
|
||||
'movie_file_extension', 'notify_outdated_version', 'output_file', 'partial_movie_dir',
|
||||
'pixel_height', 'pixel_width', 'plugins', 'preview',
|
||||
'progress_bar', 'quality', 'right_side', 'save_last_frame',
|
||||
'scene_names', 'show_in_file_browser', 'sound', 'tex_dir',
|
||||
'progress_bar', 'quality', 'right_side', 'save_as_gif', 'save_last_frame',
|
||||
'save_pngs', 'scene_names', 'show_in_file_browser', 'sound', 'tex_dir',
|
||||
'tex_template', 'tex_template_file', 'text_dir', 'top', 'transparent',
|
||||
'upto_animation_number', 'verbosity', 'video_dir',
|
||||
'upto_animation_number', 'use_opengl_renderer', 'verbosity', 'video_dir',
|
||||
'window_position', 'window_monitor', 'window_size', 'write_all', 'write_to_movie',
|
||||
'enable_wireframe', 'force_window']
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
A deep dive into Manim's internals
|
||||
==================================
|
||||
|
||||
**Authors:** `Benjamin Hackl <https://benjamin-hackl.at>`__ and `Aarush Deshpande <https://github.com/JasonGrace2282>`__
|
||||
**Author:** `Benjamin Hackl <https://benjamin-hackl.at>`__
|
||||
|
||||
.. admonition:: Disclaimer
|
||||
|
||||
This guide reflects the state of the library as of version ``v0.20.0``
|
||||
This guide reflects the state of the library as of version ``v0.16.0``
|
||||
and primarily treats the Cairo renderer. The situation in the latest
|
||||
version of Manim might be different; in case of substantial deviations
|
||||
we will add a note below.
|
||||
|
|
@ -84,7 +84,7 @@ discussing the contents of the following chapters on a very high level.
|
|||
to prepare a scene for rendering; right until the point where the user-overridden
|
||||
``construct`` method is ran. This includes a brief discussion on using Manim's CLI
|
||||
versus other means of rendering (e.g., via Jupyter notebooks, or in your Python
|
||||
script by calling the :meth:`.Manager.render` method yourself).
|
||||
script by calling the :meth:`.Scene.render` method yourself).
|
||||
- `Mobject Initialization`_: For the second chapter we dive into creating and handling
|
||||
Mobjects, the basic elements that should be displayed in our scene.
|
||||
We discuss the :class:`.Mobject` base class, how there are essentially
|
||||
|
|
@ -107,25 +107,6 @@ discussing the contents of the following chapters on a very high level.
|
|||
:meth:`.Scene.construct` has been run, the library combines the partial movie
|
||||
files to one video.
|
||||
|
||||
.. hint::
|
||||
|
||||
As we move forward, try to keep in mind the responsibilities of every
|
||||
class we introduce. We'll talk more about them in detail, but here's a brief
|
||||
overview
|
||||
|
||||
* :class:`.Scene` is responsible for managing the classes :class:`Mobject`, :class:`.Animation`,
|
||||
and :class:`.Camera`.
|
||||
|
||||
* :class:`.Manager` is responsible for coordinating the :class:`.Scene`, :class:`.Renderer`,
|
||||
and :class:`.FileWriter`.
|
||||
|
||||
* :class:`.FileWriter` is responsible for writing frames and partial movie files, as well
|
||||
as combining them all into a final movie file.
|
||||
|
||||
* :class:`.Renderer` is an abstract class which has to be subclassed.
|
||||
It's job is to take information related to the :class:`.Camera`, and the mobjects
|
||||
on the :class:`.Scene` at a certain frame, and to return the pixels in a frame.
|
||||
|
||||
And with that, let us get *in medias res*.
|
||||
|
||||
Preliminaries
|
||||
|
|
@ -142,8 +123,8 @@ like
|
|||
::
|
||||
|
||||
with tempconfig({"quality": "medium_quality", "preview": True}):
|
||||
manager = Manager(ToyExample)
|
||||
manager.render()
|
||||
scene = ToyExample()
|
||||
scene.render()
|
||||
|
||||
or whether you are rendering the code in a Jupyter notebook, you are still telling your
|
||||
python interpreter to import the library. The usual pattern used to do this is
|
||||
|
|
@ -221,8 +202,8 @@ have created a file ``toy_example.py`` which looks like this::
|
|||
self.play(FadeOut(blue_circle, small_dot))
|
||||
|
||||
with tempconfig({"quality": "medium_quality", "preview": True}):
|
||||
manager = Manager(ToyExample)
|
||||
manager.render()
|
||||
scene = ToyExample()
|
||||
scene.render()
|
||||
|
||||
With such a file, the desired scene is rendered by simply running this Python
|
||||
script via ``python toy_example.py``. Then, as described above, the library
|
||||
|
|
@ -237,10 +218,10 @@ dictionary, and upon leaving the context the original version of the
|
|||
configuration is restored. TL;DR: it provides a fancy way of temporarily setting
|
||||
configuration options.
|
||||
|
||||
Inside the context manager, two things happen: a :class:`.Manager` is created for
|
||||
the ``ToyExample``-scene, and the ``render`` method is called. Every way of using
|
||||
Inside the context manager, two things happen: an actual ``ToyExample``-scene
|
||||
object is instantiated, and the ``render`` method is called. Every way of using
|
||||
Manim ultimately does something along of these lines, the library always instantiates
|
||||
the manager of the scene object and then calls its ``render`` method. To illustrate that this
|
||||
the scene object and then calls its ``render`` method. To illustrate that this
|
||||
really is the case, let us briefly look at the two most common ways of rendering
|
||||
scenes:
|
||||
|
||||
|
|
@ -262,75 +243,54 @@ and the code creating the scene class and calling its render method is located
|
|||
`here <https://github.com/ManimCommunity/manim/blob/ac1ee9a683ce8b92233407351c681f7d71a4f2db/manim/utils/ipython_magic.py#L137-L138>`__.
|
||||
|
||||
|
||||
Now that we know that either way, a :class:`.Manager` for a :class:`.Scene` object is created, let us investigate
|
||||
what Manim does when that happens. When instantiating our manager
|
||||
Now that we know that either way, a :class:`.Scene` object is created, let us investigate
|
||||
what Manim does when that happens. When instantiating our scene object
|
||||
|
||||
::
|
||||
|
||||
manager = Manager(ToyExample)
|
||||
scene = ToyExample()
|
||||
|
||||
The :meth:`.Manager.__init__` method is called. Looking at the source code (`here <https://github.com/ManimCommunity/manim/blob/experimental/manim/manager.py>`__),
|
||||
we see that the :meth:`.Scene.__init__` method is called,
|
||||
given that we did not implement our own initialization
|
||||
method. Inspecting the corresponding code (see `here <https://github.com/ManimCommunity/manim/blob/main/manim/scene/scene.py>`__)
|
||||
reveals that :class:`Scene.__init__` first sets several attributes of the scene objects that do not
|
||||
depend on any configuration options set in ``config``. It then initializes it's :class:`.Camera`.
|
||||
The purpose of a :class:`.Camera` is to keep track of what you can see in the scene. Think of it
|
||||
as a pair of eyes, that limit how far you can look sideways and vertically.
|
||||
the ``Scene.__init__`` method is called, given that we did not implement our own initialization
|
||||
method. Inspecting the corresponding code (see
|
||||
`here <https://github.com/ManimCommunity/manim/blob/main/manim/scene/scene.py>`__)
|
||||
reveals that ``Scene.__init__`` first sets several attributes of the scene objects that do not
|
||||
depend on any configuration options set in ``config``. Then the scene inspects the value of
|
||||
``config.renderer``, and based on its value, either instantiates a ``CairoRenderer`` or an
|
||||
``OpenGLRenderer`` object and assigns it to its ``renderer`` attribute.
|
||||
|
||||
The :class:`.Scene` also sets up :attr:`.Scene.mobjects`. This attribute keeps track of all the :class:`.Mobject`
|
||||
that have been added to the scene.
|
||||
The scene then asks its renderer to initialize the scene by calling
|
||||
|
||||
The :class:`.Manager` then continues on to create a :class:`.Window`, which is the popopen interactive window,
|
||||
and creates the renderer::
|
||||
::
|
||||
|
||||
self.renderer = self.create_renderer()
|
||||
self.renderer.use_window()
|
||||
self.renderer.init_scene(self)
|
||||
|
||||
If you hover over :attr:`.Manager.renderer`, you might see that the type is a :class:`.RendererProtocol`.
|
||||
A :class:`~typing.Protocol` is a contract for a class. It says that whatever the class is, it will implement
|
||||
the methods defined inside the protocol. In this case, it means that the renderer will have all the methods
|
||||
defined in :class:`.RendererProtocol`.
|
||||
Inspecting both the default Cairo renderer and the OpenGL renderer shows that the ``init_scene``
|
||||
method effectively makes the renderer instantiate a :class:`.SceneFileWriter` object, which
|
||||
basically is Manim's interface to ``libav`` (FFMPEG) and actually writes the movie file. The Cairo
|
||||
renderer (see the implementation `here <https://github.com/ManimCommunity/manim/blob/main/manim/renderer/cairo_renderer.py>`__) does not require any further initialization. The OpenGL renderer
|
||||
does some additional setup to enable the realtime rendering preview window, which we do not go
|
||||
into detail further here.
|
||||
|
||||
.. note::
|
||||
.. warning::
|
||||
|
||||
The point of using :class:`~typing.Protocol` is so that in the future, plugins
|
||||
can swap out the renderer with their own version - either for speed, or for a different
|
||||
behavior.
|
||||
Currently, there is a lot of interplay between a scene and its renderer. This is a flaw
|
||||
in Manim's current architecture, and we are working on reducing this interdependency to
|
||||
achieve a less convoluted code flow.
|
||||
|
||||
|
||||
For the rest of this article to take a concrete example, we'll use :class:`.OpenGLRenderer`.
|
||||
|
||||
Finally, the :class:`.Manager` creates a :class:`.FileWriter`. This is the object that actually
|
||||
writes the partial movie files.
|
||||
After the renderer has been instantiated and initialized its file writer, the scene populates
|
||||
further initial attributes (notable mention: the ``mobjects`` attribute which keeps track
|
||||
of the mobjects that have been added to the scene). It is then done with its instantiation
|
||||
and ready to be rendered.
|
||||
|
||||
The rest of this article is concerned with the last line in our toy example script::
|
||||
|
||||
manager.render()
|
||||
scene.render()
|
||||
|
||||
This is where the actual magic happens.
|
||||
|
||||
.. note::
|
||||
|
||||
TODO TO REVIEWERS - Replace this link with the proper permanent link
|
||||
|
||||
Inspecting the `implementation of the render method <https://github.com/ManimCommunity/manim/blob/df1a60421ea1119cbbbd143ef288d294851baaac/manim/scene/scene.py#L211>`__
|
||||
we see that there are two passes of rendering.
|
||||
|
||||
.. note::
|
||||
|
||||
As of the experimental branch at June 30th, 2024, two pass rendering
|
||||
does not exist. This will proceed to explain the single pass rendering system.
|
||||
|
||||
Looking around, we find that there are several hooks that can be used for pre- or postprocessing
|
||||
a scene (check out :meth:`.Manager._setup`, and :meth:`.Manager._tear_down`).
|
||||
|
||||
.. note::
|
||||
|
||||
You might notice :attr:`.Manager.virtual_animation_start_time` and :attr:`.Manager.real_animation_start_time`
|
||||
when looking through :meth:`.Manager._setup`. These will be explained later.
|
||||
|
||||
Unsurprisingly, :meth:`.Manager.render` describes the full *render cycle*
|
||||
reveals that there are several hooks that can be used for pre- or postprocessing
|
||||
a scene. Unsurprisingly, :meth:`.Scene.render` describes the full *render cycle*
|
||||
of a scene. During this life cycle, there are three custom methods whose base
|
||||
implementation is empty and that can be overwritten to suit your purposes. In
|
||||
the order they are called, these customizable methods are:
|
||||
|
|
@ -348,14 +308,14 @@ the order they are called, these customizable methods are:
|
|||
Python scripts).
|
||||
|
||||
After these three methods are run, the animations have been fully rendered,
|
||||
and Manim calls :meth:`.Manager.tear_down` to gracefully
|
||||
and Manim calls :meth:`.CairoRenderer.scene_finished` to gracefully
|
||||
complete the rendering process. This checks whether any animations have been
|
||||
played -- and if so, it tells the :class:`.SceneFileWriter` to close the output
|
||||
file. If not, Manim assumes that a static image should be output
|
||||
which it then renders using the same strategy by calling the render loop
|
||||
(see below) once.
|
||||
|
||||
**Back in our toy example,** the call to :meth:`.Manager.render` first
|
||||
**Back in our toy example,** the call to :meth:`.Scene.render` first
|
||||
triggers :meth:`.Scene.setup` (which only consists of ``pass``), followed by
|
||||
a call of :meth:`.Scene.construct`. At this point, our *animation script*
|
||||
is run, starting with the initialization of ``orange_square``.
|
||||
|
|
@ -388,12 +348,16 @@ of :class:`.Mobject`, you will find that not too much happens in there:
|
|||
- and finally, ``init_colors`` is called.
|
||||
|
||||
Digging deeper, you will find that :meth:`.Mobject.reset_points` simply
|
||||
sets the ``points`` attribute of the mobject to an empty NumPy array,
|
||||
sets the ``points`` attribute of the mobject to an empty NumPy vector,
|
||||
while the other two methods, :meth:`.Mobject.generate_points` and
|
||||
:meth:`.Mobject.init_colors` are just implemented as ``pass``.
|
||||
|
||||
This makes sense: :class:`.Mobject` is not supposed to be used as
|
||||
an *actual* object that is displayed on screen.
|
||||
an *actual* object that is displayed on screen; in fact the camera
|
||||
(which we will discuss later in more detail; it is the class that is,
|
||||
for the Cairo renderer, responsible for "taking a picture" of the
|
||||
current scene) does not process "pure" :class:`Mobjects <.Mobject>`
|
||||
in any way, they *cannot* even appear in the rendered output.
|
||||
|
||||
This is where different types of mobjects come into play. Roughly
|
||||
speaking, the Cairo renderer setup knows three different types of
|
||||
|
|
@ -412,24 +376,24 @@ mobjects that can be rendered:
|
|||
|
||||
As just mentioned, :class:`VMobjects <.VMobject>` represent vectorized
|
||||
mobjects. To render a :class:`.VMobject`, the camera looks at the
|
||||
:attr:`~.VMobject.points` attribute of a :class:`.VMobject` and divides it into sets
|
||||
of three points each. Each of these sets is then used to construct a
|
||||
quadratic Bézier curve with the first and last entry describing the
|
||||
end points of the curve ("anchors"), and the second entry
|
||||
describing the control points in between ("handle").
|
||||
``points`` attribute of a :class:`.VMobject` and divides it into sets
|
||||
of four points each. Each of these sets is then used to construct a
|
||||
cubic Bézier curve with the first and last entry describing the
|
||||
end points of the curve ("anchors"), and the second and third entry
|
||||
describing the control points in between ("handles").
|
||||
|
||||
.. hint::
|
||||
To learn more about Bézier curves, take a look at the excellent
|
||||
online textbook `A Primer on Bézier curves <https://pomax.github.io/bezierinfo/>`__
|
||||
by `Pomax <https://twitter.com/TheRealPomax>`__ -- there is a playground representing
|
||||
quadratic Bézier curves `in §1 <https://pomax.github.io/bezierinfo/#introduction>`__,
|
||||
cubic Bézier curves `in §1 <https://pomax.github.io/bezierinfo/#introduction>`__,
|
||||
the red and yellow points are "anchors", and the green and blue
|
||||
points are "handles".
|
||||
|
||||
In contrast to :class:`.Mobject`, :class:`.VMobject` can be displayed
|
||||
on screen (even though, technically, it is still considered a base class).
|
||||
To illustrate how points are processed, consider the following short example
|
||||
of a :class:`.VMobject` with 6 points (and thus made out of 6/3 = 2 cubic
|
||||
of a :class:`.VMobject` with 8 points (and thus made out of 8/4 = 2 cubic
|
||||
Bézier curves). The resulting :class:`.VMobject` is drawn in green.
|
||||
The handles are drawn as red dots with a line to their closest anchor.
|
||||
|
||||
|
|
@ -466,7 +430,6 @@ The handles are drawn as red dots with a line to their closest anchor.
|
|||
|
||||
|
||||
.. warning::
|
||||
|
||||
Manually setting the points of your :class:`.VMobject` is usually
|
||||
discouraged; there are specialized methods that can take care of
|
||||
that for you -- but it might be relevant when implementing your own,
|
||||
|
|
@ -598,12 +561,59 @@ is not a "flat" list of mobjects, but a list of mobjects which
|
|||
might contain mobjects themselves, and so on.
|
||||
|
||||
Stepping through the code in :meth:`.Scene.add`, we see that first
|
||||
we remove all the mobjects that are being added -- this is to make
|
||||
sure we don't add a :class:`.Mobject` twice! After that, we can safely
|
||||
add it to :attr:`.Scene.mobjects`.
|
||||
it is checked whether we are currently using the OpenGL renderer
|
||||
(which we are not) -- adding mobjects to the scene works slightly
|
||||
different (and actually easier!) for the OpenGL renderer. Then, the
|
||||
code branch for the Cairo renderer is entered and the list of so-called
|
||||
foreground mobjects (which are rendered on top of all other mobjects)
|
||||
is added to the list of passed mobjects. This is to ensure that the
|
||||
foreground mobjects will stay above of the other mobjects, even after
|
||||
adding the new ones. In our case, the list of foreground mobjects
|
||||
is actually empty, and nothing changes.
|
||||
|
||||
We will hear more from :class:`.Scene` soon.
|
||||
Before we do that, let us look at the next line
|
||||
Next, :meth:`.Scene.restructure_mobjects` is called with the list
|
||||
of mobjects to be added as the ``to_remove`` argument, which might
|
||||
sound odd at first. Practically, this ensures that mobjects are not
|
||||
added twice, as mentioned above: if they were present in the scene
|
||||
``Scene.mobjects`` list before (even if they were contained as a
|
||||
child of some other mobject), they are first removed from the list.
|
||||
The way :meth:`.Scene.restructure_mobjects` works is rather aggressive:
|
||||
It always operates on a given list of mobjects; in the ``add`` method
|
||||
two different lists occur: the default one, ``Scene.mobjects`` (no extra
|
||||
keyword argument is passed), and ``Scene.moving_mobjects`` (which we will
|
||||
discuss later in more detail). It iterates through all of the members of
|
||||
the list, and checks whether any of the mobjects passed in ``to_remove``
|
||||
are contained as children (in any nesting level). If so, **their parent
|
||||
mobject is deconstructed** and their siblings are inserted directly
|
||||
one level higher. Consider the following example::
|
||||
|
||||
>>> from manim import Scene, Square, Circle, Group
|
||||
>>> test_scene = Scene()
|
||||
>>> mob1 = Square()
|
||||
>>> mob2 = Circle()
|
||||
>>> mob_group = Group(mob1, mob2)
|
||||
>>> test_scene.add(mob_group)
|
||||
<manim.scene.scene.Scene object at ...>
|
||||
>>> test_scene.mobjects
|
||||
[Group]
|
||||
>>> test_scene.restructure_mobjects(to_remove=[mob1])
|
||||
<manim.scene.scene.Scene object at ...>
|
||||
>>> test_scene.mobjects
|
||||
[Circle]
|
||||
|
||||
Note that the group is disbanded and the circle moves into the
|
||||
root layer of mobjects in ``test_scene.mobjects``.
|
||||
|
||||
After the mobject list is "restructured", the mobject to be added
|
||||
are simply appended to ``Scene.mobjects``. In our toy example,
|
||||
the ``Scene.mobjects`` list is actually empty, so the
|
||||
``restructure_mobjects`` method does not actually do anything. The
|
||||
``orange_square`` is simply added to ``Scene.mobjects``, and as
|
||||
the aforementioned ``Scene.moving_mobjects`` list is, at this point,
|
||||
also still empty, nothing happens and :meth:`.Scene.add` returns.
|
||||
|
||||
We will hear more about the ``moving_mobject`` list when we discuss
|
||||
the render loop. Before we do that, let us look at the next line
|
||||
of code in our toy example, which includes the initialization of
|
||||
an animation class,
|
||||
::
|
||||
|
|
@ -632,11 +642,11 @@ the run time of animations is also fixed and known beforehand.
|
|||
The initialization of animations actually is not very exciting,
|
||||
:meth:`.Animation.__init__` merely sets some attributes derived
|
||||
from the passed keyword arguments and additionally ensures that
|
||||
the :attr:`~Animation.starting_mobject` and :attr:`~.Animation.mobject`
|
||||
the ``Animation.starting_mobject`` and ``Animation.mobject``
|
||||
attributes are populated. Once the animation is played, the
|
||||
:attr:`~.Animation.starting_mobject` attribute holds an unmodified copy of the
|
||||
``starting_mobject`` attribute holds an unmodified copy of the
|
||||
mobject the animation is attached to; during the initialization
|
||||
it is set to a placeholder mobject. The :attr:`~.Animation.mobject` attribute
|
||||
it is set to a placeholder mobject. The ``mobject`` attribute
|
||||
is set to the mobject the animation is attached to.
|
||||
|
||||
Animations have a few special methods which are called during the
|
||||
|
|
@ -671,80 +681,77 @@ animation (like its ``run_time``, the ``rate_func``, etc.) are
|
|||
processed there -- and then the animation object is fully
|
||||
initialized and ready to be played.
|
||||
|
||||
The Animation Buffer
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
There's an attribute of animations that we have glossed
|
||||
over, and that is :attr:`.Animation.buffer`, of type :class:`.SceneBuffer`.
|
||||
The :attr:`~.Animation.buffer` is the animations way of communicating
|
||||
with what happens on the scene. If you want to modify
|
||||
the scene during the interpolation stage (outside of :meth:`~.Animation.begin` or :meth:`~.Animation.finish`),
|
||||
the attribute :attr:`.Animation.apply_buffer` is what tells the scene that the buffer
|
||||
should be processed.
|
||||
|
||||
For example, an animation that adds a circle to the scene every frame might look like this
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class CircleAnimation(Animation):
|
||||
def begin(self) -> None:
|
||||
self.circles = VGroup()
|
||||
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
# create and arrange the circles
|
||||
self.circles.add(Circle())
|
||||
self.circles().arrange()
|
||||
# add the new circle to the scene
|
||||
self.buffer.add(self.circles[-1])
|
||||
# make sure the scene actually realizes something changed
|
||||
self.apply_buffer = True
|
||||
|
||||
Every time the :class:`.Scene` applies the buffer, it gets emptied out
|
||||
for use the next time.
|
||||
|
||||
The ``play`` call: preparing to enter Manim's render loop
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
We are finally there, the render loop is in our reach. Let us
|
||||
walk through the code that is run when :meth:`.Scene.play` is called.
|
||||
|
||||
.. note::
|
||||
.. hint::
|
||||
|
||||
In the future, control will not be passed to the Manager.
|
||||
Instead, the Scene will keep track of every animation and
|
||||
at the very end, the Manager will render everything.
|
||||
Recall that this article is specifically about the Cairo renderer.
|
||||
Up to here, things were more or less the same for the OpenGL renderer
|
||||
as well; while some base mobjects might be different, the control flow
|
||||
and lifecycle of mobjects is still more or less the same. There are more
|
||||
substantial differences when it comes to the rendering loop.
|
||||
|
||||
As you will see when inspecting the method, :meth:`.Scene.play` almost
|
||||
immediately passes over to the :class:`~.Manager._play` method of the :class:`.Manager`.
|
||||
The one thing :meth:`.Scene.play` does before that is preparing the animations.
|
||||
Whenever :attr:`.Mobject.animate` is called, it creates a new object called a
|
||||
:class:`._AnimationBuilder`. We have to make sure to convert that into an actual
|
||||
animation by calling it's :meth:`._AnimationBuilder.build` method.
|
||||
We also have to update the animations with the correct rate functions, lag ratios,
|
||||
and run time.
|
||||
|
||||
.. note::
|
||||
|
||||
Methods in :class:`.Manager` starting with an underscore ``_`` are intended to be
|
||||
private, and are not guaranteed to be stable across versions of Manim. The :class:`.Manager`
|
||||
class provides some "public" methods (methods not prefixed with ``_``) that can be overridden to
|
||||
change the behavior of the program.
|
||||
immediately passes over to the ``play`` method of the renderer,
|
||||
in our case :class:`.CairoRenderer.play`. The one thing :meth:`.Scene.play`
|
||||
takes care of is the management of subcaptions that you might have
|
||||
passed to it (see the the documentation of :meth:`.Scene.play` and
|
||||
:meth:`.Scene.add_subcaption` for more information).
|
||||
|
||||
.. warning::
|
||||
|
||||
Subcaptions and audio is still in progress
|
||||
As has been said before, the communication between scene and renderer
|
||||
is not in a very clean state at this point, so the following paragraphs
|
||||
might be confusing if you don't run a debugger and step through the
|
||||
code yourself a bit.
|
||||
|
||||
Inside :meth:`.CairoRenderer.play`, the renderer first checks whether
|
||||
it may skip rendering of the current play call. This might happen, for example,
|
||||
when ``-s`` is passed to the CLI (i.e., only the last frame should be rendered),
|
||||
or when the ``-n`` flag is passed and the current play call is outside of the
|
||||
specified render bounds. The "skipping status" is updated in form of the
|
||||
call to :meth:`.CairoRenderer.update_skipping_status`.
|
||||
|
||||
Next, the renderer asks the scene to process the animations in the play
|
||||
call so that renderer obtains all of the information it needs. To
|
||||
be more concrete, :meth:`.Scene.compile_animation_data` is called,
|
||||
which then takes care of several things:
|
||||
|
||||
- The method processes all animations and the keyword arguments passed
|
||||
to the initial :meth:`.Scene.play` call. In particular, this means
|
||||
that it makes sure all arguments passed to the play call are actually
|
||||
animations (or ``.animate`` syntax calls, which are also assembled to
|
||||
be actual :class:`.Animation`-objects at that point). It also propagates
|
||||
any animation-related keyword arguments (like ``run_time``,
|
||||
or ``rate_func``) passed to :class:`.Scene.play` to each individual
|
||||
animation. The processed animations are then stored in the ``animations``
|
||||
attribute of the scene (which the renderer later reads...).
|
||||
- It adds all mobjects to which the animations that are played are
|
||||
bound to to the scene (provided the animation is not an mobject-introducing
|
||||
animation -- for these, the addition to the scene happens later).
|
||||
- In case the played animation is a :class:`.Wait` animation (this is the
|
||||
case in a :meth:`.Scene.wait` call), the method checks whether a static
|
||||
image should be rendered, or whether the render loop should be processed
|
||||
as usual (see :meth:`.Scene.should_update_mobjects` for the exact conditions,
|
||||
basically it checks whether there are any time-dependent updater functions
|
||||
and so on).
|
||||
- Finally, the method determines the total run time of the play call (which
|
||||
at this point is computed as the maximum of the run times of the passed
|
||||
animations). This is stored in the ``duration`` attribute of the scene.
|
||||
|
||||
|
||||
After the :class:`.Scene` has done all the processing of animations,
|
||||
it hands out control to the :class:`.Manager`. The :class:`.Manager`
|
||||
then updates the skipping status of the :class:`.Scene`. This makes sure
|
||||
that if ``-s`` or ``-n`` is used for sections, the scene does the correct
|
||||
thing.
|
||||
After the animation data has been compiled by the scene, the renderer
|
||||
continues to prepare for entering the render loop. It now checks the
|
||||
skipping status which has been determined before. If the renderer can
|
||||
skip this play call, it does so: it sets the current play call hash (which
|
||||
we will get back to in a moment) to ``None`` and increases the time of the
|
||||
renderer by the determined animation run time.
|
||||
|
||||
The next important line is::
|
||||
|
||||
self._write_hashed_movie_file()
|
||||
|
||||
Here, the :class:`.Manager` checks whether or not Manim's caching system should
|
||||
Otherwise, the renderer checks whether or not Manim's caching system should
|
||||
be used. The idea of the caching system is simple: for every play call, a
|
||||
hash value is computed, which is then stored and upon re-rendering the scene,
|
||||
the hash is generated again and checked against the stored value. If it is the
|
||||
|
|
@ -754,8 +761,8 @@ to learn more, the :func:`.get_hash_from_play_call` function in the
|
|||
:mod:`.utils.hashing` module is essentially the entry point to the caching
|
||||
mechanism.
|
||||
|
||||
In the event that the animation has to be rendered, the manager asks
|
||||
its :class:`.FileWriter` to open an output container. The process
|
||||
In the event that the animation has to be rendered, the renderer asks
|
||||
its :class:`.SceneFileWriter` to open an output container. The process
|
||||
is started by a call to ``libav`` and opens a container to which rendered
|
||||
raw frames can be written. As long as the output is open, the container
|
||||
can be accessed via the ``output_container`` attribute of the file writer.
|
||||
|
|
@ -763,18 +770,31 @@ With the writing process in place, the renderer then asks the scene
|
|||
to "begin" the animations.
|
||||
|
||||
First, it literally *begins* all of the animations by calling their
|
||||
setup methods (:meth:`.Animation.begin`).
|
||||
setup methods (:meth:`.Animation._setup_scene`, :meth:`.Animation.begin`).
|
||||
In doing so, the mobjects that are newly introduced by an animation
|
||||
(like via :class:`.Create` etc.) are added to the scene. Furthermore, the
|
||||
animation suspends updater functions being called on its mobject, and
|
||||
it sets its mobject to the state that corresponds to the first frame
|
||||
of the animation.
|
||||
|
||||
.. note::
|
||||
After this has happened for all animations in the current ``play`` call,
|
||||
the Cairo renderer determines which of the scene's mobjects can be
|
||||
painted statically to the background, and which ones have to be
|
||||
redrawn every frame. It does so by calling
|
||||
:meth:`.Scene.get_moving_and_static_mobjects`, and the resulting
|
||||
partition of mobjects is stored in the corresponding ``moving_mobjects``
|
||||
and ``static_mobjects`` attributes.
|
||||
|
||||
Implementation of figuring out which mobjects have to be redrawn
|
||||
is still in progress.
|
||||
.. NOTE::
|
||||
|
||||
The mechanism that determines static and moving mobjects is
|
||||
specific for the Cairo renderer, the OpenGL renderer works differently.
|
||||
Basically, moving mobjects are determined by checking whether they,
|
||||
any of their children, or any of the mobjects "below" them (in the
|
||||
sense of the order in which mobjects are processed in the scene)
|
||||
either have an update function attached, or whether they appear
|
||||
in one of the current animations. See the implementation of
|
||||
:meth:`.Scene.get_moving_mobjects` for more details.
|
||||
|
||||
Up to this very point, we did not actually render any (partial)
|
||||
image or movie files from the scene yet. This is, however, about to change.
|
||||
|
|
@ -815,28 +835,68 @@ Time to render some frames.
|
|||
|
||||
The render loop (for real this time)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Now we get to the meat of rendering, which happens in :meth:`.Manager._progress_through_animations`.
|
||||
|
||||
- The manager determines the run time of the animations by calling
|
||||
:meth:`.Manager._calc_run_time`. This method basically takes the maximum
|
||||
As mentioned above, due to the mechanism that determines static and moving
|
||||
mobjects in the scene, the renderer knows which mobjects it can paint
|
||||
statically to the background of the scene. Practically, this means that
|
||||
it partially renders a scene (to produce a background image), and then
|
||||
when iterating through the time progression of the animation only the
|
||||
"moving mobjects" are re-painted on top of the static background.
|
||||
|
||||
The renderer calls :meth:`.CairoRenderer.save_static_frame_data`, which
|
||||
first checks whether there are currently any static mobjects, and if there
|
||||
are, it updates the frame (only with the static mobjects; more about how
|
||||
exactly this works in a moment) and then saves a NumPy array representing
|
||||
the rendered frame in the ``static_image`` attribute. In our toy example,
|
||||
there are no static mobjects, and so the ``static_image`` attribute is
|
||||
simply set to ``None``.
|
||||
|
||||
Next, the renderer asks the scene whether the current animation is
|
||||
a "frozen frame" animation, which would mean that the renderer actually
|
||||
does not have to repaint the moving mobjects in every frame of the time
|
||||
progression. It can then just take the latest static frame, and display it
|
||||
throughout the animation.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
An animation is considered a "frozen frame" animation if only a
|
||||
static :class:`.Wait` animation is played. See the description
|
||||
of :meth:`.Scene.compile_animation_data` above, or the
|
||||
implementation of :meth:`.Scene.should_update_mobjects` for
|
||||
more details.
|
||||
|
||||
If this is not the case (just as in our toy example), the renderer
|
||||
then calls the :meth:`.Scene.play_internal` method, which is the
|
||||
integral part of the render loop (in which the library steps through
|
||||
the time progression of the animation and renders the corresponding
|
||||
frames).
|
||||
|
||||
Within :meth:`.Scene.play_internal`, the following steps are performed:
|
||||
|
||||
- The scene determines the run time of the animations by calling
|
||||
:meth:`.Scene.get_run_time`. This method basically takes the maximum
|
||||
``run_time`` attribute of all of the animations passed to the
|
||||
:meth:`.Scene.play` call.
|
||||
- Then, the progressbar is created by :meth:`.Manager._create_progressbar`,
|
||||
which returns a ``tqdm`` `progress bar object <https://tqdm.github.io>`__
|
||||
object (from the ``tqdm`` library), or a fake progressbar if
|
||||
:attr:`.ManimConfig.write_to_movie` is ``False``.
|
||||
- Then the *time progression* is constructed via
|
||||
:meth:`.Manager._calc_time_progression` method, which returns
|
||||
``np.arange(0, run_time, 1 / config.frame_rate)``. In
|
||||
- Then the *time progression* is constructed via the (internal)
|
||||
:meth:`.Scene._get_animation_time_progression` method, which wraps
|
||||
the actual :meth:`.Scene.get_time_progression` method. The time
|
||||
progression is a ``tqdm`` `progress bar object <https://tqdm.github.io>`__
|
||||
for an iterator over ``np.arange(0, run_time, 1 / config.frame_rate)``. In
|
||||
other words, the time progression holds the time stamps (relative to the
|
||||
current animations, so starting at 0 and ending at the total animation run time,
|
||||
with the step size determined by the render frame rate) of the timeline where
|
||||
a new animation frame should be rendered.
|
||||
- Then the scene iterates over the time progression: for each time stamp ``t``,
|
||||
we find the time difference between the current and previous frame (AKA ``dt``).
|
||||
We then update the animations in the scene using ``dt`` by
|
||||
- iterating over each animation
|
||||
- next, we update the animations mobjects
|
||||
:meth:`.Scene.update_to_time` is called, which ...
|
||||
|
||||
- ... first computes the time passed since the last update (which might be 0,
|
||||
especially for the initial call) and references it as ``dt``,
|
||||
- then (in the order in which the animations are passed to :meth:`.Scene.play`)
|
||||
calls :meth:`.Animation.update_mobjects` to trigger all updater functions that
|
||||
are attached to the respective animation except for the "main mobject" of
|
||||
the animation (that is, for example, for :class:`.Transform` the unmodified
|
||||
copies of start and target mobject -- see :meth:`.Animation.get_all_mobjects_to_update`
|
||||
for more details),
|
||||
- then the relative time progression with respect to the current animation
|
||||
is computed (``alpha = t / animation.run_time``), which is then used to
|
||||
update the state of the animation with a call to :meth:`.Animation.interpolate`.
|
||||
|
|
@ -844,29 +904,62 @@ Now we get to the meat of rendering, which happens in :meth:`.Manager._progress_
|
|||
of all mobjects in the scene, all meshes, and finally those attached to
|
||||
the scene itself are run.
|
||||
|
||||
After updating the animations, we pass ``dt`` to :meth:`.Manager._update_frame` which...
|
||||
|
||||
- ... updates the total time passed
|
||||
- Updates all the mobjects by calling :meth:`.Scene._update_mobjects`. This in turn
|
||||
iterates over all the mobjects on the screen and updates them.
|
||||
- After that, the current state of the scene is computed by :meth:`.Scene.get_state`,
|
||||
which returns a :class:`.SceneState`.
|
||||
- The state is then passed into :meth:`.Manager._render_frame`, which gets
|
||||
the renderer to create the pixels. With :class:`.OpenGLRenderer`, this
|
||||
also updates the window. :meth:`~.Manager._render_frame` also checks if it should write a frame,
|
||||
and if so, writes a frame via the :class:`.FileWriter`.
|
||||
- Finally, it uses a concept of virtual time vs real time to see
|
||||
if the right amount of time has passed in the window. The virtual
|
||||
time is the amount of time that is supposed to have passed (that is, ``t``).
|
||||
The real time is how much time has actually passed in the window
|
||||
(current time - start time of play). If the animations are progressing
|
||||
faster than they would in real life, it will slow down the window by calling
|
||||
:meth:`~.Manager._update_frame` with ``dt=0`` until that's no longer the case.
|
||||
This is to make sure that animations never go too fast: it doesn't do anything if
|
||||
animations are too slow!
|
||||
|
||||
At this point, the internal (Python) state of all mobjects has been updated
|
||||
to match the currently processed timestamp.
|
||||
to match the currently processed timestamp. If rendering should not be skipped,
|
||||
then it is now time to *take a picture*!
|
||||
|
||||
.. NOTE::
|
||||
|
||||
The update of the internal state (iteration over the time progression) happens
|
||||
*always* once :meth:`.Scene.play_internal` is entered. This ensures that even
|
||||
if frames do not need to be rendered (because, e.g., the ``-n`` CLI flag has
|
||||
been passed, something has been cached, or because we might be in a *Section*
|
||||
with skipped rendering), updater functions still run correctly, and the state
|
||||
of the first frame that *is* rendered is kept consistent.
|
||||
|
||||
To render an image, the scene calls the corresponding method of its renderer,
|
||||
:meth:`.CairoRenderer.render` and passes just the list of *moving mobjects* (remember,
|
||||
the *static mobjects* are assumed to have already been painted statically to
|
||||
the background of the scene). All of the hard work then happens when the renderer
|
||||
updates its current frame via a call to :meth:`.CairoRenderer.update_frame`:
|
||||
|
||||
First, the renderer prepares its :class:`.Camera` by checking whether the renderer
|
||||
has a ``static_image`` different from ``None`` stored already. If so, it sets the
|
||||
image as the *background image* of the camera via :meth:`.Camera.set_frame_to_background`,
|
||||
and otherwise it just resets the camera via :meth:`.Camera.reset`. The camera is then
|
||||
asked to capture the scene with a call to :meth:`.Camera.capture_mobjects`.
|
||||
|
||||
Things get a bit technical here, and at some point it is more efficient to
|
||||
delve into the implementation -- but here is a summary of what happens once the
|
||||
camera is asked to capture the scene:
|
||||
|
||||
- First, a flat list of mobjects is created (so submobjects get extracted from
|
||||
their parents). This list is then processed in groups of the same type of
|
||||
mobjects (e.g., a batch of vectorized mobjects, followed by a batch of image mobjects,
|
||||
followed by more vectorized mobjects, etc. -- in many cases there will just be
|
||||
one batch of vectorized mobjects).
|
||||
- Depending on the type of the currently processed batch, the camera uses dedicated
|
||||
*display functions* to convert the :class:`.Mobject` Python object to
|
||||
a NumPy array stored in the camera's ``pixel_array`` attribute.
|
||||
The most important example in that context is the display function for
|
||||
vectorized mobjects, :meth:`.Camera.display_multiple_vectorized_mobjects`,
|
||||
or the more particular (in case you did not add a background image to your
|
||||
:class:`.VMobject`), :meth:`.Camera.display_multiple_non_background_colored_vmobjects`.
|
||||
This method first gets the current Cairo context, and then, for every (vectorized)
|
||||
mobject in the batch, calls :meth:`.Camera.display_vectorized`. There,
|
||||
the actual background stroke, fill, and then stroke of the mobject is
|
||||
drawn onto the context. See :meth:`.Camera.apply_stroke` and
|
||||
:meth:`.Camera.set_cairo_context_color` for more details -- but it does not get
|
||||
much deeper than that, in the latter method the actual Bézier curves
|
||||
determined by the points of the mobject are drawn; this is where the low-level
|
||||
interaction with Cairo happens.
|
||||
|
||||
After all batches have been processed, the camera has an image representation
|
||||
of the Scene at the current time stamp in form of a NumPy array stored in its
|
||||
``pixel_array`` attribute. The renderer then takes this array and passes it to
|
||||
its :class:`.SceneFileWriter`. This concludes one iteration of the render loop,
|
||||
and once the time progression has been processed completely, a final bit
|
||||
of cleanup is performed before the :meth:`.Scene.play_internal` call is completed.
|
||||
|
||||
A TL;DR for the render loop, in the context of our toy example, reads as follows:
|
||||
|
||||
|
|
@ -875,20 +968,23 @@ A TL;DR for the render loop, in the context of our toy example, reads as follows
|
|||
medium render quality, the frame rate is 30 frames per second, and so the time
|
||||
progression with steps ``[0, 1/30, 2/30, ..., 89/30]`` is created.
|
||||
- In the internal render loop, each of these time stamps is processed:
|
||||
there are no updater functions, so effectively the manager updates the
|
||||
there are no updater functions, so effectively the scene updates the
|
||||
state of the transformation animation to the desired time stamp (for example,
|
||||
at time stamp ``t = 45/30``, the animation is completed to a rate of
|
||||
``alpha = 0.5``).
|
||||
- Then the manager asks the renderer to do its job. The renderer then produces
|
||||
the pixels, which are then fed into the :class:`.FileWriter`.
|
||||
- Then the scene asks the renderer to do its job. The renderer asks its camera
|
||||
to capture the scene, the only mobject that needs to be processed at this point
|
||||
is the main mobject attached to the transformation; the camera converts the
|
||||
current state of the mobject to entries in a NumPy array. The renderer passes
|
||||
this array to the file writer.
|
||||
- At the end of the loop, 90 frames have been passed to the file writer.
|
||||
|
||||
Completing the render loop
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The last few steps in the :meth:`.Manager._play` call are not too
|
||||
The last few steps in the :meth:`.Scene.play_internal` call are not too
|
||||
exciting: for every animation, the corresponding :meth:`.Animation.finish`
|
||||
method is called.
|
||||
and :meth:`.Animation.clean_up_from_scene` methods are called.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
|
|
@ -903,6 +999,10 @@ method is called.
|
|||
would be slightly longer than 1 second. We decided against this at some point.
|
||||
|
||||
In the end, the time progression is closed (which completes the displayed progress bar)
|
||||
in the terminal. With the closing of the time progression, the
|
||||
:meth:`.Scene.play_internal` call is completed, and we return to the renderer,
|
||||
which now orders the :class:`.SceneFileWriter` to close the output container that has
|
||||
been opened for this animation: a partial movie file is written.
|
||||
|
||||
This pretty much concludes the walkthrough of a :class:`.Scene.play` call,
|
||||
and actually there is not too much more to say for our toy example either: at
|
||||
|
|
@ -925,4 +1025,5 @@ which triggers the combination of the partial movie files into the final product
|
|||
And there you go! This is a more or less detailed description of how Manim works
|
||||
under the hood. While we did not discuss every single line of code in detail
|
||||
in this walkthrough, it should still give you a fairly good idea of how the general
|
||||
structural design of the library looks like.
|
||||
structural design of the library and at least the Cairo rendering flow in particular
|
||||
looks like.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -39,8 +39,12 @@ Cameras
|
|||
|
||||
.. inheritance-diagram::
|
||||
manim.camera.camera
|
||||
manim.camera.mapping_camera
|
||||
manim.camera.moving_camera
|
||||
manim.camera.multi_camera
|
||||
manim.camera.three_d_camera
|
||||
:parts: 1
|
||||
:top-classes: manim.camera.camera.Camera, manim.mobject.opengl.opengl_mobject.OpenGLMobject
|
||||
:top-classes: manim.camera.camera.Camera, manim.mobject.mobject.Mobject
|
||||
|
||||
Mobjects
|
||||
********
|
||||
|
|
|
|||
|
|
@ -297,9 +297,9 @@ Creating a custom animation
|
|||
|
||||
Even though Manim has many built-in animations, you will find times when you need to smoothly animate from one state of a :class:`~.Mobject` to another.
|
||||
If you find yourself in that situation, then you can define your own custom animation.
|
||||
You start by extending the :class:`~.Animation` class and overriding its :meth:`~.Animation.interpolate`.
|
||||
The :meth:`~.Animation.interpolate` method receives alpha as a parameter that starts at 0 and changes throughout the animation.
|
||||
So, you just have to manipulate self.mobject inside Animation according to the alpha value in its interpolate method.
|
||||
You start by extending the :class:`~.Animation` class and overriding its :meth:`~.Animation.interpolate_mobject`.
|
||||
The :meth:`~.Animation.interpolate_mobject` method receives alpha as a parameter that starts at 0 and changes throughout the animation.
|
||||
So, you just have to manipulate self.mobject inside Animation according to the alpha value in its interpolate_mobject method.
|
||||
Then you get all the benefits of :class:`~.Animation` such as playing it for different run times or using different rate functions.
|
||||
|
||||
Let's say you start with a number and want to create a :class:`~.Transform` animation that transforms it to a target number.
|
||||
|
|
@ -312,11 +312,11 @@ The class can have a constructor with three arguments, a :class:`~.DecimalNumber
|
|||
The constructor will pass the :class:`~.DecimalNumber` Mobject to the super constructor (in this case, the :class:`~.Animation` constructor) and will set start and end.
|
||||
|
||||
The only thing that you need to do is to define how you want it to look at every step of the animation.
|
||||
Manim provides you with the alpha value in the :meth:`~.Animation.interpolate` method based on frame rate of video, rate function, and run time of animation played.
|
||||
Manim provides you with the alpha value in the :meth:`~.Animation.interpolate_mobject` method based on frame rate of video, rate function, and run time of animation played.
|
||||
The alpha parameter holds a value between 0 and 1 representing the step of the currently playing animation.
|
||||
For example, 0 means the beginning of the animation, 0.5 means halfway through the animation, and 1 means the end of the animation.
|
||||
|
||||
In the case of the ``Count`` animation, you just have to figure out a way to determine the number to display at the given alpha value and then set that value in the :meth:`~.Animation.interpolate` method of the ``Count`` animation.
|
||||
In the case of the ``Count`` animation, you just have to figure out a way to determine the number to display at the given alpha value and then set that value in the :meth:`~.Animation.interpolate_mobject` method of the ``Count`` animation.
|
||||
Suppose you are starting at 50 and incrementing until the :class:`~.DecimalNumber` reaches 100 at the end of the animation.
|
||||
|
||||
* If alpha is 0, you want the value to be 50.
|
||||
|
|
@ -338,7 +338,7 @@ Once you have defined your ``Count`` animation, you can play it in your :class:`
|
|||
|
||||
.. manim:: CountingScene
|
||||
:ref_classes: Animation DecimalNumber
|
||||
:ref_methods: Animation.interpolate Scene.play
|
||||
:ref_methods: Animation.interpolate_mobject Scene.play
|
||||
|
||||
class Count(Animation):
|
||||
def __init__(self, number: DecimalNumber, start: float, end: float, **kwargs) -> None:
|
||||
|
|
@ -348,7 +348,7 @@ Once you have defined your ``Count`` animation, you can play it in your :class:`
|
|||
self.start = start
|
||||
self.end = end
|
||||
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
# Set value of DecimalNumber according to alpha
|
||||
value = self.start + (self.rate_func(alpha) * (self.end - self.start))
|
||||
self.mobject.set_value(value)
|
||||
|
|
|
|||
|
|
@ -266,44 +266,6 @@ You can also skip rendering all animations belonging to a section like this:
|
|||
|
||||
|
||||
|
||||
Groups
|
||||
******
|
||||
Sections are a powerful tool to organize your animations into different parts. However, sometimes it's
|
||||
more useful to look at bigger parts of your animations. *Groups* are effectively sections of sections.
|
||||
|
||||
The syntax is fairly simple::
|
||||
|
||||
class MyScene(Scene):
|
||||
# enable groups
|
||||
groups_api = True
|
||||
|
||||
@group
|
||||
def introduction(self) -> None:
|
||||
self.play(Write(Text("Hello World!")))
|
||||
self.next_section(...)
|
||||
self.play(Write(Text("This is a group!")))
|
||||
self.next_section(...)
|
||||
|
||||
@group
|
||||
def main_part(self) -> None:
|
||||
self.play(Write(Text("This is the main part!")))
|
||||
self.next_section(...)
|
||||
self.play(Write(Text("This is a group as well!")))
|
||||
self.next_section(...)
|
||||
|
||||
@group
|
||||
def conclusion(self) -> None:
|
||||
self.play(FadeOut(*self.mobjects))
|
||||
|
||||
You can then play specific groups by using the ``--groups`` flag::
|
||||
|
||||
manim --groups introduction,conclusion scene.py
|
||||
|
||||
Note that they must be separated by commas and without spaces.
|
||||
Alternatively, you can set it on Manim's global ``config`` variable::
|
||||
|
||||
config.groups = ["introduction", "conclusion"]
|
||||
|
||||
|
||||
Some command line flags
|
||||
***********************
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
from manim import *
|
||||
|
||||
|
||||
class Test(Scene):
|
||||
def construct(scene):
|
||||
scene.camera.set_euler_angles(phi=75 * DEGREES, theta=-45 * DEGREES)
|
||||
text = Tex("This is a 3D tex").fix_in_frame()
|
||||
scene.add(text)
|
||||
scene.wait()
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
# import pyglet
|
||||
from pyglet.gl import Config
|
||||
from pyglet.window import Window
|
||||
|
||||
import manim.utils.color.manim_colors as col
|
||||
from manim._config import tempconfig
|
||||
from manim.animation.creation import DrawBorderThenFill
|
||||
from manim.camera.camera import Camera
|
||||
from manim.constants import LEFT, OUT, RIGHT, UP
|
||||
from manim.mobject.geometry.arc import Circle
|
||||
from manim.mobject.geometry.polygram import Square
|
||||
from manim.mobject.logo import ManimBanner
|
||||
from manim.mobject.text.numbers import DecimalNumber
|
||||
from manim.renderer.opengl_renderer import OpenGLRenderer
|
||||
|
||||
if __name__ == "__main__":
|
||||
with tempconfig({"renderer": "opengl"}):
|
||||
win = Window(
|
||||
width=1920,
|
||||
height=1080,
|
||||
fullscreen=True,
|
||||
vsync=True,
|
||||
config=Config(double_buffer=True, samples=0),
|
||||
)
|
||||
renderer = OpenGLRenderer(1920, 1080, background_color=col.GRAY)
|
||||
# vm = OpenGLVMobject([col.RED, col.GREEN])
|
||||
vm = (
|
||||
Circle(
|
||||
radius=1,
|
||||
stroke_color=col.YELLOW,
|
||||
)
|
||||
.shift(3 * RIGHT + OUT)
|
||||
.set_opacity(0.6)
|
||||
)
|
||||
vm2 = Square(stroke_color=col.GREEN, fill_opacity=0, stroke_opacity=1).move_to(
|
||||
(0, 0, -0.5)
|
||||
)
|
||||
vm3 = ManimBanner().set_opacity(0.6)
|
||||
vm4 = (
|
||||
Circle(0.5, col.GREEN)
|
||||
.set_opacity(0.6)
|
||||
.shift(OUT)
|
||||
.set_fill(col.BLUE, opacity=0.2)
|
||||
)
|
||||
# vm.set_points_as_corners([[-1920/2, 0, 0], [1920/2, 0, 0], [0, 1080/2, 0]])
|
||||
# print(vm.color)
|
||||
# print(vm.fill_color)
|
||||
# print(vm.stroke_color)
|
||||
|
||||
clock_mobject = DecimalNumber(0.0).shift(4 * LEFT + 2.5 * UP)
|
||||
clock_mobject.fix_in_frame()
|
||||
|
||||
camera = Camera()
|
||||
camera.save_state()
|
||||
# renderer.init_camera(camera)
|
||||
|
||||
# renderer.render(camera, [vm, vm2])
|
||||
# image = renderer.get_pixels()
|
||||
# print(image.shape)
|
||||
# Image.fromarray(image, "RGBA").show()
|
||||
# exit(0)
|
||||
renderer.use_window()
|
||||
|
||||
# clock = pyglet.clock.get_default()
|
||||
|
||||
def update_circle(dt):
|
||||
vm.move_to((np.sin(dt) * 4, np.cos(dt) * 4, -1))
|
||||
|
||||
def p2m(x, y, z):
|
||||
from manim._config import config
|
||||
|
||||
return (
|
||||
config.frame_width * (x / config.pixel_width - 0.5),
|
||||
config.frame_height * (y / config.pixel_height - 0.5),
|
||||
z,
|
||||
)
|
||||
|
||||
@win.event
|
||||
def on_close():
|
||||
win.close()
|
||||
|
||||
@win.event
|
||||
def on_mouse_motion(x, y, dx, dy):
|
||||
# vm.move_to((14.2222 * (x / 1920 - 0.5), 8 * (y / 1080 - 0.5), 0))
|
||||
# camera.move_to(p2m(x,y,camera.get_center()[2]))
|
||||
from scipy.spatial.transform import Rotation
|
||||
|
||||
camera.set_orientation(
|
||||
Rotation.from_rotvec(
|
||||
(-UP * (x / 1920 - 0.5) + RIGHT * (y / 1080 - 0.5)) * 2 * 3.1415
|
||||
)
|
||||
)
|
||||
# vm.set_color(col.RED.interpolate(col.GREEN,x/1920))
|
||||
# print(x,y)
|
||||
|
||||
@win.event
|
||||
def on_draw():
|
||||
# dt = clock.update_time()
|
||||
renderer.render(camera, [vm2, vm3, vm4, clock_mobject, vm])
|
||||
# update_circle(counter)
|
||||
|
||||
@win.event
|
||||
def on_resize(width, height):
|
||||
super(Window, win).on_resize(width, height)
|
||||
|
||||
# pyglet.app.run()
|
||||
has_started = False
|
||||
is_finished = False
|
||||
|
||||
run_time = 5
|
||||
new_vm = Square(fill_color=col.GREEN, stroke_color=col.BLUE).shift(
|
||||
2.5 * RIGHT - UP + 2 * OUT
|
||||
)
|
||||
animation = DrawBorderThenFill(vm3, run_time=run_time)
|
||||
|
||||
real_time = 0
|
||||
virtual_time = 0
|
||||
start_timestamp = time.time()
|
||||
dt = 1 / 30
|
||||
|
||||
while True:
|
||||
# pyglet.app.platform_event_loop.step()
|
||||
win.switch_to()
|
||||
if not has_started:
|
||||
animation.begin()
|
||||
has_started = True
|
||||
|
||||
real_time = time.time() - start_timestamp
|
||||
while virtual_time < real_time:
|
||||
virtual_time += dt
|
||||
if not is_finished:
|
||||
if virtual_time >= run_time:
|
||||
animation.finish()
|
||||
buffer = str(animation.buffer)
|
||||
print(f"buffer = {buffer}")
|
||||
has_finished = True
|
||||
else:
|
||||
animation.update_mobjects(dt)
|
||||
animation.interpolate(virtual_time / run_time)
|
||||
# update_circle(virtual_time)
|
||||
clock_mobject.set_value(virtual_time)
|
||||
win.dispatch_event("on_draw")
|
||||
win.dispatch_events()
|
||||
win.flip()
|
||||
|
|
@ -1,12 +1,8 @@
|
|||
from pathlib import Path
|
||||
|
||||
from manim.opengl import *
|
||||
|
||||
import manim.utils.opengl as opengl
|
||||
from manim import *
|
||||
from manim.mobject.opengl.opengl_surface import OpenGLTexturedSurface
|
||||
from manim.mobject.opengl.opengl_three_dimensions import OpenGLSurfaceMesh
|
||||
from manim.mobject.opengl.shader import FullScreenQuad, Mesh, Shader
|
||||
from manim.opengl import *
|
||||
|
||||
# Copied from https://3b1b.github.io/manim/getting_started/example_scenes.html#surfaceexample.
|
||||
# Lines that do not yet work with the Community Version are commented.
|
||||
|
|
|
|||
|
|
@ -1,114 +0,0 @@
|
|||
import numpy as np
|
||||
import pyglet
|
||||
from pyglet.gl import Config
|
||||
from pyglet.window import Window
|
||||
|
||||
import manim.utils.color.manim_colors as col
|
||||
from manim._config import tempconfig
|
||||
from manim.camera.camera import Camera
|
||||
from manim.constants import OUT, RIGHT, UP
|
||||
from manim.mobject.geometry.arc import Circle
|
||||
from manim.mobject.geometry.polygram import Square
|
||||
from manim.mobject.logo import ManimBanner
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
|
||||
from manim.mobject.text.numbers import DecimalNumber
|
||||
from manim.renderer.opengl_renderer import OpenGLRenderer
|
||||
|
||||
if __name__ == "__main__":
|
||||
with tempconfig({"renderer": "opengl"}):
|
||||
win = Window(
|
||||
width=1920,
|
||||
height=1080,
|
||||
vsync=True,
|
||||
config=Config(double_buffer=True, samples=0),
|
||||
)
|
||||
renderer = OpenGLRenderer(1920, 1080, background_color=col.GRAY)
|
||||
# vm = OpenGLVMobject([col.RED, col.GREEN])
|
||||
vm = (
|
||||
Circle(
|
||||
radius=1,
|
||||
stroke_color=col.YELLOW,
|
||||
)
|
||||
.shift(RIGHT)
|
||||
.set_opacity(0.5)
|
||||
)
|
||||
vm2 = Square(stroke_color=col.GREEN, fill_opacity=0, stroke_opacity=1).move_to(
|
||||
(0, 0, -0.5)
|
||||
)
|
||||
vm3 = ManimBanner().set_opacity(1.0)
|
||||
vm4 = (
|
||||
Circle(0.5, col.GREEN)
|
||||
.set_opacity(0.6)
|
||||
.shift(OUT)
|
||||
.set_fill(col.BLUE, opacity=0.2)
|
||||
)
|
||||
# vm.set_points_as_corners([[-1920/2, 0, 0], [1920/2, 0, 0], [0, 1080/2, 0]])
|
||||
# print(vm.color)
|
||||
# print(vm.fill_color)
|
||||
# print(vm.stroke_color)
|
||||
|
||||
camera = Camera()
|
||||
camera.save_state()
|
||||
renderer.init_camera(camera)
|
||||
|
||||
# renderer.render(camera, [vm, vm2])
|
||||
# image = renderer.get_pixels()
|
||||
# print(image.shape)
|
||||
# Image.fromarray(image, "RGBA").show()
|
||||
# exit(0)
|
||||
renderer.use_window()
|
||||
|
||||
clock = pyglet.clock.get_default()
|
||||
|
||||
def update_circle(dt):
|
||||
vm.move_to((np.sin(dt) * 4, np.cos(dt) * 4, -1))
|
||||
|
||||
def p2m(x, y, z):
|
||||
from manim._config import config
|
||||
|
||||
return (
|
||||
config.frame_width * (x / config.pixel_width - 0.5),
|
||||
config.frame_height * (y / config.pixel_height - 0.5),
|
||||
z,
|
||||
)
|
||||
|
||||
@win.event
|
||||
def on_close():
|
||||
win.close()
|
||||
|
||||
@win.event
|
||||
def on_mouse_motion(x, y, dx, dy):
|
||||
# vm.move_to((14.2222 * (x / 1920 - 0.5), 8 * (y / 1080 - 0.5), 0))
|
||||
# camera.move_to(p2m(x,y,camera.get_center()[2]))
|
||||
from scipy.spatial.transform import Rotation
|
||||
|
||||
camera.set_orientation(
|
||||
Rotation.from_rotvec(
|
||||
(-UP * (x / 1920 - 0.5) + RIGHT * (y / 1080 - 0.5)) * 2 * 3.1415
|
||||
)
|
||||
)
|
||||
# vm.set_color(col.RED.interpolate(col.GREEN,x/1920))
|
||||
# print(x,y)
|
||||
|
||||
@win.event
|
||||
def on_draw():
|
||||
dt = clock.update_time()
|
||||
fps: OpenGLVMobject = DecimalNumber(dt)
|
||||
fps.fix_in_frame()
|
||||
renderer.render(camera, [vm, vm2, vm3, vm4, fps])
|
||||
# update_circle(counter)
|
||||
|
||||
@win.event
|
||||
def on_resize(width, height):
|
||||
super(Window, win).on_resize(width, height)
|
||||
|
||||
pyglet.app.run()
|
||||
# while True:
|
||||
# pyglet.clock.tick()
|
||||
# pyglet.app.platform_event_loop.step()
|
||||
# win.switch_to()
|
||||
# counter += 0.01
|
||||
# update_circle(counter)
|
||||
# win.dispatch_event("on_draw")
|
||||
# win.dispatch_events()
|
||||
# win.flip()
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
from manim import *
|
||||
|
||||
|
||||
class Test(Scene):
|
||||
groups_api = True
|
||||
|
||||
@group
|
||||
def first_section(self) -> None:
|
||||
line = Line()
|
||||
line.add_updater(lambda m, dt: m.rotate(PI * dt))
|
||||
line.to_edge(LEFT)
|
||||
self.add(line)
|
||||
square = Square()
|
||||
tex = Tex(
|
||||
"Hello, ",
|
||||
"world",
|
||||
r" $e^{i\theta}$",
|
||||
stroke_color=RED,
|
||||
fill_color=BLUE,
|
||||
stroke_width=2,
|
||||
).to_edge(RIGHT)
|
||||
tex.set_color_by_tex("world", GREEN)
|
||||
self.add(tex)
|
||||
self.play(Create(tex), Rotate(square, PI / 2))
|
||||
self.wait(1)
|
||||
self.play(FadeOut(square))
|
||||
|
||||
@group
|
||||
def three_mobjects(self) -> None:
|
||||
hexagon = RegularPolygon(6)
|
||||
circle = Circle()
|
||||
star = Star()
|
||||
VGroup(hexagon, circle, star).arrange()
|
||||
self.play(
|
||||
Succession(
|
||||
Create(hexagon),
|
||||
DrawBorderThenFill(circle),
|
||||
SpinInFromNothing(star),
|
||||
)
|
||||
)
|
||||
self.play(FadeOut(VGroup(hexagon, circle, star)))
|
||||
|
||||
@group
|
||||
def manim_banner(self) -> None:
|
||||
banner = ManimBanner().scale(0.5)
|
||||
self.play(banner.create())
|
||||
self.play(banner.expand())
|
||||
self.wait(1)
|
||||
self.play(Unwrite(banner))
|
||||
|
||||
@group
|
||||
def graph(self):
|
||||
vertices = [1, 2, 3]
|
||||
edges = [(1, 2), (2, 3), (3, 1)]
|
||||
graph = Graph(vertices, edges, layout="circular")
|
||||
self.play(Create(graph))
|
||||
self.play(
|
||||
graph.animate.add_vertices(
|
||||
4,
|
||||
5,
|
||||
vertex_config={4: {"fill_color": RED}, 5: {"fill_color": RED}},
|
||||
positions={4: [2, 1, 0], 5: [2, -1, 0]},
|
||||
)
|
||||
)
|
||||
self.play( # TODO: this animation is currently broken
|
||||
graph.animate.add_edges(
|
||||
(2, 4),
|
||||
(3, 5),
|
||||
(4, 5),
|
||||
edge_config={
|
||||
(2, 4): {"stroke_color": GREEN},
|
||||
(3, 5): {"stroke_color": GREEN},
|
||||
(4, 5): {"stroke_color": YELLOW},
|
||||
},
|
||||
)
|
||||
)
|
||||
self.wait(1)
|
||||
self.play(graph.animate.remove_vertices(1))
|
||||
self.play(graph.animate.remove_edges((4, 5)))
|
||||
self.play(Uncreate(graph))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with (
|
||||
tempconfig(
|
||||
{
|
||||
"preview": True,
|
||||
"write_to_movie": False,
|
||||
"disable_caching": True,
|
||||
"frame_rate": 60,
|
||||
"disable_caching_warning": True,
|
||||
}
|
||||
),
|
||||
Manager(Test) as manager,
|
||||
):
|
||||
manager.render()
|
||||
|
|
@ -17,91 +17,99 @@ except PackageNotFoundError:
|
|||
|
||||
# Importing the config module should be the first thing we do, since other
|
||||
# modules depend on the global config dict for initialization.
|
||||
from manim._config import *
|
||||
from ._config import *
|
||||
|
||||
# many scripts depend on this -> has to be loaded first
|
||||
from manim.utils.commands import *
|
||||
from .utils.commands import *
|
||||
|
||||
# isort: on
|
||||
import numpy as np
|
||||
|
||||
from manim.animation.animation import *
|
||||
from manim.animation.changing import *
|
||||
from manim.animation.composition import *
|
||||
from manim.animation.creation import *
|
||||
from manim.animation.fading import *
|
||||
from manim.animation.growing import *
|
||||
from manim.animation.indication import *
|
||||
from manim.animation.movement import *
|
||||
from manim.animation.numbers import *
|
||||
from manim.animation.rotation import *
|
||||
from manim.animation.specialized import *
|
||||
from manim.animation.speedmodifier import *
|
||||
from manim.animation.transform import *
|
||||
from manim.animation.transform_matching_parts import *
|
||||
from manim.animation.updaters.mobject_update_utils import *
|
||||
from manim.animation.updaters.update import *
|
||||
from manim.constants import *
|
||||
from manim.file_writer import *
|
||||
from manim.manager import *
|
||||
from manim.mobject.frame import *
|
||||
from manim.mobject.geometry.arc import *
|
||||
from manim.mobject.geometry.boolean_ops import *
|
||||
from manim.mobject.geometry.labeled import *
|
||||
from manim.mobject.geometry.line import *
|
||||
from manim.mobject.geometry.polygram import *
|
||||
from manim.mobject.geometry.shape_matchers import *
|
||||
from manim.mobject.geometry.tips import *
|
||||
from manim.mobject.graph import *
|
||||
from manim.mobject.graphing.coordinate_systems import *
|
||||
from manim.mobject.graphing.functions import *
|
||||
from manim.mobject.graphing.number_line import *
|
||||
from manim.mobject.graphing.probability import *
|
||||
from manim.mobject.graphing.scale import *
|
||||
from manim.mobject.logo import *
|
||||
from manim.mobject.matrix import *
|
||||
from manim.mobject.mobject import *
|
||||
from manim.mobject.opengl.dot_cloud import *
|
||||
from manim.mobject.opengl.opengl_point_cloud_mobject import *
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import *
|
||||
from manim.mobject.svg.brace import *
|
||||
from manim.mobject.svg.svg_mobject import *
|
||||
from manim.mobject.table import *
|
||||
from manim.mobject.text.code_mobject import *
|
||||
from manim.mobject.text.numbers import *
|
||||
from manim.mobject.text.tex_mobject import *
|
||||
from manim.mobject.text.text_mobject import *
|
||||
from manim.mobject.three_d.polyhedra import *
|
||||
from manim.mobject.three_d.three_d_utils import *
|
||||
from manim.mobject.three_d.three_dimensions import *
|
||||
from manim.mobject.types.image_mobject import *
|
||||
from manim.mobject.types.point_cloud_mobject import *
|
||||
from manim.mobject.types.vectorized_mobject import *
|
||||
from manim.mobject.value_tracker import *
|
||||
from manim.mobject.vector_field import *
|
||||
from manim.scene.scene import *
|
||||
from manim.scene.sections import *
|
||||
from manim.scene.vector_space_scene import *
|
||||
from manim.utils import color, rate_functions, unit
|
||||
from manim.utils.bezier import *
|
||||
from manim.utils.color import *
|
||||
from manim.utils.config_ops import *
|
||||
from manim.utils.debug import *
|
||||
from manim.utils.file_ops import *
|
||||
from manim.utils.images import *
|
||||
from manim.utils.iterables import *
|
||||
from manim.utils.paths import *
|
||||
from manim.utils.rate_functions import *
|
||||
from manim.utils.simple_functions import *
|
||||
from manim.utils.sounds import *
|
||||
from manim.utils.space_ops import *
|
||||
from manim.utils.tex import *
|
||||
from manim.utils.tex_templates import *
|
||||
from .animation.animation import *
|
||||
from .animation.changing import *
|
||||
from .animation.composition import *
|
||||
from .animation.creation import *
|
||||
from .animation.fading import *
|
||||
from .animation.growing import *
|
||||
from .animation.indication import *
|
||||
from .animation.movement import *
|
||||
from .animation.numbers import *
|
||||
from .animation.rotation import *
|
||||
from .animation.specialized import *
|
||||
from .animation.speedmodifier import *
|
||||
from .animation.transform import *
|
||||
from .animation.transform_matching_parts import *
|
||||
from .animation.updaters.mobject_update_utils import *
|
||||
from .animation.updaters.update import *
|
||||
from .camera.camera import *
|
||||
from .camera.mapping_camera import *
|
||||
from .camera.moving_camera import *
|
||||
from .camera.multi_camera import *
|
||||
from .camera.three_d_camera import *
|
||||
from .constants import *
|
||||
from .mobject.frame import *
|
||||
from .mobject.geometry.arc import *
|
||||
from .mobject.geometry.boolean_ops import *
|
||||
from .mobject.geometry.labeled import *
|
||||
from .mobject.geometry.line import *
|
||||
from .mobject.geometry.polygram import *
|
||||
from .mobject.geometry.shape_matchers import *
|
||||
from .mobject.geometry.tips import *
|
||||
from .mobject.graph import *
|
||||
from .mobject.graphing.coordinate_systems import *
|
||||
from .mobject.graphing.functions import *
|
||||
from .mobject.graphing.number_line import *
|
||||
from .mobject.graphing.probability import *
|
||||
from .mobject.graphing.scale import *
|
||||
from .mobject.logo import *
|
||||
from .mobject.matrix import *
|
||||
from .mobject.mobject import *
|
||||
from .mobject.opengl.dot_cloud import *
|
||||
from .mobject.opengl.opengl_point_cloud_mobject import *
|
||||
from .mobject.svg.brace import *
|
||||
from .mobject.svg.svg_mobject import *
|
||||
from .mobject.table import *
|
||||
from .mobject.text.code_mobject import *
|
||||
from .mobject.text.numbers import *
|
||||
from .mobject.text.tex_mobject import *
|
||||
from .mobject.text.text_mobject import *
|
||||
from .mobject.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 *
|
||||
from .mobject.types.image_mobject import *
|
||||
from .mobject.types.point_cloud_mobject import *
|
||||
from .mobject.types.vectorized_mobject import *
|
||||
from .mobject.value_tracker import *
|
||||
from .mobject.vector_field import *
|
||||
from .renderer.cairo_renderer import *
|
||||
from .scene.moving_camera_scene import *
|
||||
from .scene.scene import *
|
||||
from .scene.scene_file_writer import *
|
||||
from .scene.section import *
|
||||
from .scene.three_d_scene import *
|
||||
from .scene.vector_space_scene import *
|
||||
from .scene.zoomed_scene import *
|
||||
from .utils import color, rate_functions, unit
|
||||
from .utils.bezier import *
|
||||
from .utils.color import *
|
||||
from .utils.config_ops import *
|
||||
from .utils.debug import *
|
||||
from .utils.file_ops import *
|
||||
from .utils.images import *
|
||||
from .utils.iterables import *
|
||||
from .utils.paths import *
|
||||
from .utils.rate_functions import *
|
||||
from .utils.simple_functions import *
|
||||
from .utils.sounds import *
|
||||
from .utils.space_ops import *
|
||||
from .utils.tex import *
|
||||
from .utils.tex_templates import *
|
||||
|
||||
try:
|
||||
from IPython import get_ipython
|
||||
|
||||
from manim.utils.ipython_magic import ManimMagic
|
||||
from .utils.ipython_magic import ManimMagic
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
|
|
@ -109,4 +117,4 @@ else:
|
|||
if ipy is not None:
|
||||
ipy.register_magics(ManimMagic)
|
||||
|
||||
from manim.plugins import *
|
||||
from .plugins import *
|
||||
|
|
|
|||
|
|
@ -7,18 +7,18 @@
|
|||
|
||||
# Each of the following will be set to True if the corresponding CLI flag
|
||||
# is present when executing manim. If the flag is not present, they will
|
||||
# be set to the value found here. For example, running manim with the --format=mp4
|
||||
# flag will set FORMAT to mp4. However, since the default value
|
||||
# of FORMAT defined in this file is also mp4, running manim
|
||||
# without the --format=mp4 value will also output an mp4 movie file. To change that, set
|
||||
# FORMAT = webm so that running manim without the --format=mp4 flag will not
|
||||
# generate an mp4 movie file.
|
||||
# be set to the value found here. For example, running manim with the -w
|
||||
# flag will set WRITE_TO_MOVIE to True. However, since the default value
|
||||
# of WRITE_TO_MOVIE defined in this file is also True, running manim
|
||||
# without the -w value will also output a movie file. To change that, set
|
||||
# WRITE_TO_MOVIE = False so that running manim without the -w flag will not
|
||||
# generate a movie file. Note all of the following accept boolean values.
|
||||
|
||||
# --notify_outdated_version
|
||||
notify_outdated_version = True
|
||||
|
||||
# -w, --write_to_movie
|
||||
write_to_movie = False
|
||||
write_to_movie = True
|
||||
|
||||
format = mp4
|
||||
|
||||
|
|
@ -29,9 +29,15 @@ save_last_frame = False
|
|||
# -a, --write_all
|
||||
write_all = False
|
||||
|
||||
# -g, --save_pngs
|
||||
save_pngs = False
|
||||
|
||||
# -0, --zero_pad
|
||||
zero_pad = 4
|
||||
|
||||
# -i, --save_as_gif
|
||||
save_as_gif = False
|
||||
|
||||
# --save_sections
|
||||
save_sections = False
|
||||
|
||||
|
|
@ -88,7 +94,7 @@ text_dir = {media_dir}/texts
|
|||
partial_movie_dir = {video_dir}/partial_movie_files/{scene_name}
|
||||
|
||||
# --renderer [cairo|opengl]
|
||||
renderer = opengl
|
||||
renderer = cairo
|
||||
|
||||
# --enable_gui
|
||||
enable_gui = False
|
||||
|
|
@ -115,6 +121,12 @@ window_monitor = 0
|
|||
# --force_window
|
||||
force_window = False
|
||||
|
||||
# --use_projection_fill_shaders
|
||||
use_projection_fill_shaders = False
|
||||
|
||||
# --use_projection_stroke_shaders
|
||||
use_projection_stroke_shaders = False
|
||||
|
||||
movie_file_extension = .mp4
|
||||
|
||||
# These now override the --quality option.
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import logging
|
|||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import Iterator, Mapping, MutableMapping, Sequence
|
||||
from collections.abc import Iterator, Mapping, MutableMapping
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, NoReturn
|
||||
|
||||
|
|
@ -276,7 +276,6 @@ class ManimConfig(MutableMapping):
|
|||
"frame_x_radius",
|
||||
"frame_y_radius",
|
||||
"from_animation_number",
|
||||
"groups",
|
||||
"images_dir",
|
||||
"input_file",
|
||||
"media_embed",
|
||||
|
|
@ -295,8 +294,10 @@ class ManimConfig(MutableMapping):
|
|||
"preview",
|
||||
"progress_bar",
|
||||
"quality",
|
||||
"save_as_gif",
|
||||
"save_sections",
|
||||
"save_last_frame",
|
||||
"save_pngs",
|
||||
"scene_names",
|
||||
"seed",
|
||||
"show_in_file_browser",
|
||||
|
|
@ -308,6 +309,8 @@ class ManimConfig(MutableMapping):
|
|||
"renderer",
|
||||
"enable_gui",
|
||||
"gui_location",
|
||||
"use_projection_fill_shaders",
|
||||
"use_projection_stroke_shaders",
|
||||
"verbosity",
|
||||
"video_dir",
|
||||
"sections_dir",
|
||||
|
|
@ -326,22 +329,6 @@ class ManimConfig(MutableMapping):
|
|||
def __init__(self) -> None:
|
||||
self._d: dict[str, Any | None] = dict.fromkeys(self._OPTS)
|
||||
|
||||
def _warn_about_config_options(self) -> None:
|
||||
"""Warns about incorrect config options, or permutations of config options."""
|
||||
logger = logging.getLogger("manim")
|
||||
if self.format == "webm":
|
||||
logger.warning(
|
||||
"Output format set as webm, this can be slower than other formats",
|
||||
)
|
||||
if not self.preview and not self.write_to_movie:
|
||||
logger.warning(
|
||||
"preview and write_to_movie disabled, this is a dry run. Try passing -p or -w."
|
||||
)
|
||||
elif self.preview and self.write_to_movie:
|
||||
logger.warning(
|
||||
"Both preview and write_to_movie enabled, this can be slower than just previewing."
|
||||
)
|
||||
|
||||
# behave like a dict
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return iter(self._d)
|
||||
|
|
@ -592,6 +579,8 @@ class ManimConfig(MutableMapping):
|
|||
"write_to_movie",
|
||||
"save_last_frame",
|
||||
"write_all",
|
||||
"save_pngs",
|
||||
"save_as_gif",
|
||||
"save_sections",
|
||||
"preview",
|
||||
"show_in_file_browser",
|
||||
|
|
@ -602,6 +591,8 @@ class ManimConfig(MutableMapping):
|
|||
"custom_folders",
|
||||
"enable_gui",
|
||||
"fullscreen",
|
||||
"use_projection_fill_shaders",
|
||||
"use_projection_stroke_shaders",
|
||||
"enable_wireframe",
|
||||
"force_window",
|
||||
"no_latex_cleanup",
|
||||
|
|
@ -709,8 +700,6 @@ class ManimConfig(MutableMapping):
|
|||
if quality:
|
||||
self.quality = _determine_quality(quality)
|
||||
|
||||
self.groups = parser["CLI"].get("groups", fallback="", raw=True) or []
|
||||
|
||||
return self
|
||||
|
||||
def digest_args(self, args: argparse.Namespace) -> Self:
|
||||
|
|
@ -765,6 +754,8 @@ class ManimConfig(MutableMapping):
|
|||
"show_in_file_browser",
|
||||
"write_to_movie",
|
||||
"save_last_frame",
|
||||
"save_pngs",
|
||||
"save_as_gif",
|
||||
"save_sections",
|
||||
"write_all",
|
||||
"disable_caching",
|
||||
|
|
@ -778,6 +769,8 @@ class ManimConfig(MutableMapping):
|
|||
"background_color",
|
||||
"enable_gui",
|
||||
"fullscreen",
|
||||
"use_projection_fill_shaders",
|
||||
"use_projection_stroke_shaders",
|
||||
"zero_pad",
|
||||
"enable_wireframe",
|
||||
"force_window",
|
||||
|
|
@ -950,7 +943,6 @@ class ManimConfig(MutableMapping):
|
|||
def notify_outdated_version(self, value: bool) -> None:
|
||||
self._set_boolean("notify_outdated_version", value)
|
||||
|
||||
# TODO: Rename to write_to_file
|
||||
@property
|
||||
def write_to_movie(self) -> bool:
|
||||
"""Whether to render the scene to a movie file (-w)."""
|
||||
|
|
@ -978,6 +970,24 @@ class ManimConfig(MutableMapping):
|
|||
def write_all(self, value: bool) -> None:
|
||||
self._set_boolean("write_all", value)
|
||||
|
||||
@property
|
||||
def save_pngs(self) -> bool:
|
||||
"""Whether to save all frames in the scene as images files (-g)."""
|
||||
return self._d["save_pngs"]
|
||||
|
||||
@save_pngs.setter
|
||||
def save_pngs(self, value: bool) -> None:
|
||||
self._set_boolean("save_pngs", value)
|
||||
|
||||
@property
|
||||
def save_as_gif(self) -> bool:
|
||||
"""Whether to save the rendered scene in .gif format (-i)."""
|
||||
return self._d["save_as_gif"]
|
||||
|
||||
@save_as_gif.setter
|
||||
def save_as_gif(self, value: bool) -> None:
|
||||
self._set_boolean("save_as_gif", value)
|
||||
|
||||
@property
|
||||
def save_sections(self) -> bool:
|
||||
"""Whether to save single videos for each section in addition to the movie file."""
|
||||
|
|
@ -1049,6 +1059,10 @@ class ManimConfig(MutableMapping):
|
|||
[None, "png", "gif", "mp4", "mov", "webm"],
|
||||
)
|
||||
self.resolve_movie_file_extension(self.transparent)
|
||||
if self.format == "webm":
|
||||
logger.warning(
|
||||
"Output format set as webm, this can be slower than other formats",
|
||||
)
|
||||
|
||||
@property
|
||||
def ffmpeg_loglevel(self) -> str:
|
||||
|
|
@ -1204,24 +1218,6 @@ class ManimConfig(MutableMapping):
|
|||
def upto_animation_number(self, value: int) -> None:
|
||||
self._set_pos_number("upto_animation_number", value, True)
|
||||
|
||||
@property
|
||||
def groups(self) -> tuple[str, ...]:
|
||||
"""The name of the groups to play.
|
||||
|
||||
If not passed, it will play all groups. Otherwise,
|
||||
it will play only the groups passed in.
|
||||
"""
|
||||
return self._d["groups"] # type: ignore[misc]
|
||||
|
||||
@groups.setter
|
||||
def groups(self, value: str | Sequence[str]) -> None:
|
||||
if isinstance(value, str):
|
||||
self._set_str("groups", value.replace(" ", "").split(","))
|
||||
else:
|
||||
if not all(isinstance(v, str) for v in value):
|
||||
raise ValueError("groups must be a string or a sequence of strings")
|
||||
self._d["groups"] = tuple(value)
|
||||
|
||||
@property
|
||||
def max_files_cached(self) -> int:
|
||||
"""Maximum number of files cached. Use -1 for infinity (no flag)."""
|
||||
|
|
@ -1482,6 +1478,24 @@ class ManimConfig(MutableMapping):
|
|||
def fullscreen(self, value: bool) -> None:
|
||||
self._set_boolean("fullscreen", value)
|
||||
|
||||
@property
|
||||
def use_projection_fill_shaders(self) -> bool:
|
||||
"""Use shaders for OpenGLVMobject fill which are compatible with transformation matrices."""
|
||||
return self._d["use_projection_fill_shaders"]
|
||||
|
||||
@use_projection_fill_shaders.setter
|
||||
def use_projection_fill_shaders(self, value: bool) -> None:
|
||||
self._set_boolean("use_projection_fill_shaders", value)
|
||||
|
||||
@property
|
||||
def use_projection_stroke_shaders(self) -> bool:
|
||||
"""Use shaders for OpenGLVMobject stroke which are compatible with transformation matrices."""
|
||||
return self._d["use_projection_stroke_shaders"]
|
||||
|
||||
@use_projection_stroke_shaders.setter
|
||||
def use_projection_stroke_shaders(self, value: bool) -> None:
|
||||
self._set_boolean("use_projection_stroke_shaders", value)
|
||||
|
||||
@property
|
||||
def zero_pad(self) -> int:
|
||||
"""PNG zero padding. A number between 0 (no zero padding) and 9 (9 columns minimum)."""
|
||||
|
|
|
|||
|
|
@ -2,45 +2,32 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
|
||||
from .. import config, logger
|
||||
from ..constants import RendererType
|
||||
from ..mobject import mobject
|
||||
from ..mobject.mobject import Group, Mobject
|
||||
from ..mobject.opengl import opengl_mobject
|
||||
from ..utils.rate_functions import linear, smooth
|
||||
|
||||
__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, Self, assert_never, cast, overload
|
||||
|
||||
import numpy as np
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
OpenGLGroup as Group,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
OpenGLMobject as Mobject,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
_AnimationBuilder,
|
||||
)
|
||||
|
||||
from .. import logger
|
||||
from ..utils.rate_functions import linear, smooth
|
||||
from .protocol import AnimationProtocol, MobjectAnimation
|
||||
from .scene_buffer import SceneBuffer, SceneOperation
|
||||
from typing import TYPE_CHECKING, Any, Self
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Self
|
||||
|
||||
from manim.scene.scene import Scene
|
||||
|
||||
M = TypeVar("M", bound=Mobject)
|
||||
|
||||
|
||||
__all__ = ["Animation", "Wait", "override_animation"]
|
||||
|
||||
|
||||
DEFAULT_ANIMATION_RUN_TIME: float = 1.0
|
||||
DEFAULT_ANIMATION_LAG_RATIO: float = 0.0
|
||||
|
||||
|
||||
class Animation(AnimationProtocol):
|
||||
class Animation:
|
||||
"""An animation.
|
||||
|
||||
Animations have a fixed time span.
|
||||
|
|
@ -82,9 +69,9 @@ class Animation(AnimationProtocol):
|
|||
.. NOTE::
|
||||
|
||||
In the current implementation of this class, the specified rate function is applied
|
||||
within :meth:`.Animation.interpolate` call as part of the call to
|
||||
within :meth:`.Animation.interpolate_mobject` call as part of the call to
|
||||
:meth:`.Animation.interpolate_submobject`. For subclasses of :class:`.Animation`
|
||||
that are implemented by overriding :meth:`interpolate`, the rate function
|
||||
that are implemented by overriding :meth:`interpolate_mobject`, the rate function
|
||||
has to be applied manually (e.g., by passing ``self.rate_func(alpha)`` instead
|
||||
of just ``alpha``).
|
||||
|
||||
|
|
@ -140,33 +127,37 @@ class Animation(AnimationProtocol):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject | None,
|
||||
mobject: Mobject | OpenGLMobject | None,
|
||||
lag_ratio: float = DEFAULT_ANIMATION_LAG_RATIO,
|
||||
run_time: float = DEFAULT_ANIMATION_RUN_TIME,
|
||||
rate_func: Callable[[float], float] = smooth,
|
||||
reverse_rate_function: bool = False,
|
||||
name: str = "",
|
||||
remover: bool = False, # remove a mobject from the screen at end of animation
|
||||
name: str = None,
|
||||
remover: bool = False, # remove a mobject from the screen?
|
||||
suspend_mobject_updating: bool = True,
|
||||
introducer: bool = False,
|
||||
*,
|
||||
_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)
|
||||
self.run_time: float = run_time
|
||||
self.rate_func: Callable[[float], float] = rate_func
|
||||
self.reverse_rate_function: bool = reverse_rate_function
|
||||
self.name: str = name
|
||||
self.name: str | None = name
|
||||
self.remover: bool = remover
|
||||
self.introducer: bool = introducer
|
||||
self.suspend_mobject_updating: bool = suspend_mobject_updating
|
||||
self.lag_ratio: float = lag_ratio
|
||||
|
||||
self.buffer = SceneBuffer()
|
||||
self.apply_buffer = False # ask scene to apply buffer
|
||||
|
||||
self.starting_mobject: Mobject = Mobject()
|
||||
self.mobject: Mobject = mobject if mobject is not None else Mobject()
|
||||
self._on_finish: Callable[[Scene], None] = _on_finish
|
||||
if config["renderer"] == RendererType.OPENGL:
|
||||
self.starting_mobject: OpenGLMobject = OpenGLMobject()
|
||||
self.mobject: OpenGLMobject = (
|
||||
mobject if mobject is not None else OpenGLMobject()
|
||||
)
|
||||
else:
|
||||
self.starting_mobject: Mobject = Mobject()
|
||||
self.mobject: Mobject = mobject if mobject is not None else Mobject()
|
||||
|
||||
if hasattr(self, "CONFIG"):
|
||||
logger.error(
|
||||
|
|
@ -192,7 +183,7 @@ class Animation(AnimationProtocol):
|
|||
def _typecheck_input(self, mobject: Mobject | None) -> None:
|
||||
if mobject is None:
|
||||
logger.debug("Animation with empty mobject")
|
||||
elif not isinstance(mobject, Mobject):
|
||||
elif not isinstance(mobject, (Mobject, OpenGLMobject)):
|
||||
raise TypeError("Animation only works on Mobjects")
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
|
@ -203,17 +194,6 @@ class Animation(AnimationProtocol):
|
|||
def __repr__(self) -> str:
|
||||
return str(self)
|
||||
|
||||
def update_rate_info(
|
||||
self,
|
||||
run_time: float | None = None,
|
||||
rate_func: Callable[[float], float] | None = None,
|
||||
lag_ratio: float | None = None,
|
||||
):
|
||||
self.run_time = run_time or self.run_time
|
||||
self.rate_func = rate_func or self.rate_func
|
||||
self.lag_ratio = lag_ratio or self.lag_ratio
|
||||
return self
|
||||
|
||||
def begin(self) -> None:
|
||||
"""Begin the animation.
|
||||
|
||||
|
|
@ -233,12 +213,10 @@ class Animation(AnimationProtocol):
|
|||
self.mobject.suspend_updating()
|
||||
self.interpolate(0)
|
||||
|
||||
# TODO: Figure out a way to check
|
||||
# if self.mobject in scene.get_mobject_family
|
||||
if self.introducer:
|
||||
self.buffer.add(self.mobject)
|
||||
|
||||
def finish(self) -> None:
|
||||
# TODO: begin and finish should require a scene as parameter.
|
||||
# That way Animation.clean_up_from_screen and Scene.add_mobjects_from_animations
|
||||
# could be removed as they fulfill basically the same purpose.
|
||||
"""Finish the animation.
|
||||
|
||||
This method gets called when the animation is over.
|
||||
|
|
@ -248,14 +226,45 @@ class Animation(AnimationProtocol):
|
|||
if self.suspend_mobject_updating and self.mobject is not None:
|
||||
self.mobject.resume_updating()
|
||||
|
||||
if self.remover:
|
||||
self.buffer.remove(self.mobject)
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
"""Clean up the :class:`~.Scene` after finishing the animation.
|
||||
|
||||
def create_starting_mobject(self) -> Mobject:
|
||||
This includes to :meth:`~.Scene.remove` the Animation's
|
||||
:class:`~.Mobject` if the animation is a remover.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
scene
|
||||
The scene the animation should be cleaned up from.
|
||||
"""
|
||||
self._on_finish(scene)
|
||||
if self.is_remover():
|
||||
scene.remove(self.mobject)
|
||||
|
||||
def _setup_scene(self, scene: Scene) -> None:
|
||||
"""Setup up the :class:`~.Scene` before starting the animation.
|
||||
|
||||
This includes to :meth:`~.Scene.add` the Animation's
|
||||
:class:`~.Mobject` if the animation is an introducer.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
scene
|
||||
The scene the animation should be cleaned up from.
|
||||
"""
|
||||
if scene is None:
|
||||
return
|
||||
if (
|
||||
self.is_introducer()
|
||||
and self.mobject not in scene.get_mobject_family_members()
|
||||
):
|
||||
scene.add(self.mobject)
|
||||
|
||||
def create_starting_mobject(self) -> Mobject | OpenGLMobject:
|
||||
# Keep track of where the mobject starts
|
||||
return self.mobject.copy()
|
||||
|
||||
def get_all_mobjects(self) -> Sequence[Mobject]:
|
||||
def get_all_mobjects(self) -> Sequence[Mobject | OpenGLMobject]:
|
||||
"""Get all mobjects involved in the animation.
|
||||
|
||||
Ordering must match the ordering of arguments to interpolate_submobject
|
||||
|
|
@ -268,7 +277,14 @@ class Animation(AnimationProtocol):
|
|||
return self.mobject, self.starting_mobject
|
||||
|
||||
def get_all_families_zipped(self) -> Iterable[tuple]:
|
||||
return zip(*(mob.get_family() for mob in self.get_all_mobjects()), strict=False)
|
||||
if config["renderer"] == RendererType.OPENGL:
|
||||
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()),
|
||||
strict=False,
|
||||
)
|
||||
|
||||
def update_mobjects(self, dt: float) -> None:
|
||||
"""
|
||||
|
|
@ -281,24 +297,7 @@ class Animation(AnimationProtocol):
|
|||
for mob in self.get_all_mobjects_to_update():
|
||||
mob.update(dt)
|
||||
|
||||
def process_subanimation_buffer(self, buffer: SceneBuffer):
|
||||
"""
|
||||
This is used in animations that are proxies around
|
||||
other animations, like :class:`.AnimationGroup`
|
||||
"""
|
||||
for op, args, kwargs in buffer:
|
||||
match op:
|
||||
case SceneOperation.ADD:
|
||||
self.buffer.add(*args, **kwargs)
|
||||
case SceneOperation.REMOVE:
|
||||
self.buffer.remove(*args, **kwargs)
|
||||
case SceneOperation.REPLACE:
|
||||
self.buffer.replace(*args, **kwargs)
|
||||
case _:
|
||||
assert_never(op)
|
||||
buffer.clear()
|
||||
|
||||
def get_all_mobjects_to_update(self) -> Sequence[Mobject]:
|
||||
def get_all_mobjects_to_update(self) -> list[Mobject]:
|
||||
"""Get all mobjects to be updated during the animation.
|
||||
|
||||
Returns
|
||||
|
|
@ -309,9 +308,9 @@ class Animation(AnimationProtocol):
|
|||
# The surrounding scene typically handles
|
||||
# updating of self.mobject. Besides, in
|
||||
# most cases its updating is suspended anyway
|
||||
return [m for m in self.get_all_mobjects() if m is not self.mobject]
|
||||
return list(filter(lambda m: m is not self.mobject, self.get_all_mobjects()))
|
||||
|
||||
def copy(self) -> Self:
|
||||
def copy(self) -> Animation:
|
||||
"""Create a copy of the animation.
|
||||
|
||||
Returns
|
||||
|
|
@ -325,6 +324,19 @@ class Animation(AnimationProtocol):
|
|||
|
||||
# TODO: stop using alpha as parameter name in different meanings.
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
"""Set the animation progress.
|
||||
|
||||
This method gets called for every frame during an animation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
alpha
|
||||
The relative time to set the animation to, 0 meaning the start, 1 meaning
|
||||
the end.
|
||||
"""
|
||||
self.interpolate_mobject(alpha)
|
||||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
"""Interpolates the mobject of the :class:`Animation` based on alpha value.
|
||||
|
||||
Parameters
|
||||
|
|
@ -334,10 +346,10 @@ class Animation(AnimationProtocol):
|
|||
is completed. For example, alpha-values of 0, 0.5, and 1 correspond
|
||||
to the animation being completed 0%, 50%, and 100%, respectively.
|
||||
"""
|
||||
families = tuple(self.get_all_families_zipped())
|
||||
families = list(self.get_all_families_zipped())
|
||||
for i, mobs in enumerate(families):
|
||||
sub_alpha = self.get_sub_alpha(alpha, i, len(families))
|
||||
self.interpolate_submobject(*mobs, sub_alpha) # type: ignore[call-arg]
|
||||
self.interpolate_submobject(*mobs, sub_alpha)
|
||||
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
|
|
@ -346,7 +358,8 @@ class Animation(AnimationProtocol):
|
|||
# target_copy: Mobject, #Todo: fix - signature of interpolate_submobject differs in Transform().
|
||||
alpha: float,
|
||||
) -> Animation:
|
||||
raise NotImplementedError("Implement in subclass")
|
||||
# Typically implemented by subclass
|
||||
pass
|
||||
|
||||
def get_sub_alpha(self, alpha: float, index: int, num_submobjects: int) -> float:
|
||||
"""Get the animation progress of any submobjects subanimation.
|
||||
|
|
@ -372,14 +385,13 @@ class Animation(AnimationProtocol):
|
|||
full_length = (num_submobjects - 1) * lag_ratio + 1
|
||||
value = alpha * full_length
|
||||
lower = index * lag_ratio
|
||||
raw_sub_alpha = np.clip((value - lower), 0, 1)
|
||||
if self.reverse_rate_function:
|
||||
return self.rate_func(1 - raw_sub_alpha)
|
||||
return self.rate_func(1 - (value - lower))
|
||||
else:
|
||||
return self.rate_func(raw_sub_alpha)
|
||||
return self.rate_func(value - lower)
|
||||
|
||||
# Getters and setters
|
||||
def set_run_time(self, run_time: float) -> Self:
|
||||
def set_run_time(self, run_time: float) -> Animation:
|
||||
"""Set the run time of the animation.
|
||||
|
||||
Parameters
|
||||
|
|
@ -414,7 +426,7 @@ class Animation(AnimationProtocol):
|
|||
def set_rate_func(
|
||||
self,
|
||||
rate_func: Callable[[float], float],
|
||||
) -> Self:
|
||||
) -> Animation:
|
||||
"""Set the rate function of the animation.
|
||||
|
||||
Parameters
|
||||
|
|
@ -443,7 +455,7 @@ class Animation(AnimationProtocol):
|
|||
"""
|
||||
return self.rate_func
|
||||
|
||||
def set_name(self, name: str) -> Self:
|
||||
def set_name(self, name: str) -> Animation:
|
||||
"""Set the name of the animation.
|
||||
|
||||
Parameters
|
||||
|
|
@ -459,6 +471,26 @@ class Animation(AnimationProtocol):
|
|||
self.name = name
|
||||
return self
|
||||
|
||||
def is_remover(self) -> bool:
|
||||
"""Test if the animation is a remover.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
``True`` if the animation is a remover, ``False`` otherwise.
|
||||
"""
|
||||
return self.remover
|
||||
|
||||
def is_introducer(self) -> bool:
|
||||
"""Test if the animation is an introducer.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
``True`` if the animation is an introducer, ``False`` otherwise.
|
||||
"""
|
||||
return self.introducer
|
||||
|
||||
@classmethod
|
||||
def __init_subclass__(cls, **kwargs) -> None:
|
||||
super().__init_subclass__(**kwargs)
|
||||
|
|
@ -508,19 +540,9 @@ class Animation(AnimationProtocol):
|
|||
cls.__init__ = cls._original__init__
|
||||
|
||||
|
||||
@overload
|
||||
def prepare_animation(anim: MobjectAnimation[M]) -> MobjectAnimation[M]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def prepare_animation(
|
||||
anim: AnimationProtocol | _AnimationBuilder | Mobject,
|
||||
) -> AnimationProtocol: ...
|
||||
|
||||
|
||||
def prepare_animation(
|
||||
anim: AnimationProtocol | _AnimationBuilder | Mobject,
|
||||
) -> AnimationProtocol:
|
||||
anim: Animation | mobject._AnimationBuilder | opengl_mobject._AnimationBuilder,
|
||||
) -> Animation:
|
||||
r"""Returns either an unchanged animation, or the animation built
|
||||
from a passed animation factory.
|
||||
|
||||
|
|
@ -547,17 +569,16 @@ def prepare_animation(
|
|||
TypeError: Object 42 cannot be converted to an animation
|
||||
|
||||
"""
|
||||
if isinstance(anim, _AnimationBuilder):
|
||||
if isinstance(anim, mobject._AnimationBuilder):
|
||||
return anim.build()
|
||||
|
||||
# if it has these three methods it probably is an AnimationProtocol
|
||||
# but we don't use isinstance because it's slow
|
||||
try:
|
||||
for method in ("begin", "finish", "update_mobjects"):
|
||||
getattr(anim, method)
|
||||
return cast(AnimationProtocol, anim)
|
||||
except AttributeError:
|
||||
raise TypeError(f"Object {anim} cannot be converted to an animation") from None
|
||||
if isinstance(anim, opengl_mobject._AnimationBuilder):
|
||||
return anim.build()
|
||||
|
||||
if isinstance(anim, Animation):
|
||||
return anim
|
||||
|
||||
raise TypeError(f"Object {anim} cannot be converted to an animation")
|
||||
|
||||
|
||||
class Wait(Animation):
|
||||
|
|
@ -594,9 +615,12 @@ class Wait(Animation):
|
|||
if stop_condition and frozen_frame:
|
||||
raise ValueError("A static Wait animation cannot have a stop condition.")
|
||||
|
||||
self.duration: float = run_time
|
||||
self.stop_condition = stop_condition
|
||||
self.is_static_wait: bool = bool(frozen_frame)
|
||||
self.is_static_wait: bool = frozen_frame
|
||||
super().__init__(None, run_time=run_time, rate_func=rate_func, **kwargs)
|
||||
# quick fix to work in opengl setting:
|
||||
self.mobject.shader_wrapper_list = []
|
||||
|
||||
def begin(self) -> None:
|
||||
pass
|
||||
|
|
@ -604,6 +628,9 @@ class Wait(Animation):
|
|||
def finish(self) -> None:
|
||||
pass
|
||||
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
pass
|
||||
|
||||
def update_mobjects(self, dt: float) -> None:
|
||||
pass
|
||||
|
||||
|
|
@ -733,10 +760,9 @@ def override_animation(
|
|||
self.play(FadeIn(MySquare()))
|
||||
|
||||
"""
|
||||
_F = TypeVar("_F", bound=Callable)
|
||||
|
||||
def decorator(func: _F) -> _F:
|
||||
func._override_animation = animation_class # type: ignore[attr-defined]
|
||||
def decorator(func):
|
||||
func._override_animation = animation_class
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
|
|
|||
|
|
@ -4,13 +4,12 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["AnimatedBoundary", "TracedPath"]
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Self
|
||||
|
||||
import numpy as np
|
||||
from collections.abc import Callable, Sequence
|
||||
from typing import Any, Self
|
||||
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.utils.color import (
|
||||
BLUE_B,
|
||||
BLUE_D,
|
||||
|
|
@ -21,11 +20,6 @@ from manim.utils.color import (
|
|||
)
|
||||
from manim.utils.rate_functions import RateFunction, smooth
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Sequence
|
||||
|
||||
import numpy.typing as npt
|
||||
|
||||
|
||||
class AnimatedBoundary(VGroup):
|
||||
"""Boundary of a :class:`.VMobject` with animated color change.
|
||||
|
|
@ -68,7 +62,7 @@ class AnimatedBoundary(VGroup):
|
|||
]
|
||||
self.add(*self.boundary_copies)
|
||||
self.total_time = 0.0
|
||||
self.add_updater(lambda _, dt: self.update_boundary_copies(dt))
|
||||
self.add_updater(lambda m, dt: self.update_boundary_copies(dt))
|
||||
|
||||
def update_boundary_copies(self, dt: float) -> None:
|
||||
# Not actual time, but something which passes at
|
||||
|
|
@ -108,7 +102,7 @@ class AnimatedBoundary(VGroup):
|
|||
return self
|
||||
|
||||
|
||||
class TracedPath(VMobject):
|
||||
class TracedPath(VMobject, metaclass=ConvertToOpenGL):
|
||||
"""Traces the path of a point returned by a function call.
|
||||
|
||||
Parameters
|
||||
|
|
@ -152,32 +146,23 @@ class TracedPath(VMobject):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
traced_point_func: Callable[
|
||||
[], npt.NDArray[npt.float]
|
||||
], # TODO: Replace with Callable[[], Point3D]
|
||||
traced_point_func: Callable,
|
||||
stroke_width: float = 2,
|
||||
stroke_color: ParsableManimColor | None = WHITE,
|
||||
dissipating_time: float | None = None,
|
||||
fill_opacity: float = 0.0,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(
|
||||
stroke_color=stroke_color,
|
||||
stroke_width=stroke_width,
|
||||
fill_opacity=fill_opacity,
|
||||
**kwargs,
|
||||
)
|
||||
) -> None:
|
||||
super().__init__(stroke_color=stroke_color, stroke_width=stroke_width, **kwargs)
|
||||
self.traced_point_func = traced_point_func
|
||||
self.dissipating_time = dissipating_time
|
||||
self.time = 1.0 if self.dissipating_time else None
|
||||
self.add_updater(self.update_path)
|
||||
|
||||
def update_path(self, _mob: Mobject, dt: float) -> None:
|
||||
def update_path(self, mob: Mobject, dt: float) -> None:
|
||||
new_point = self.traced_point_func()
|
||||
if not self.has_points():
|
||||
self.start_new_path(new_point)
|
||||
if not np.allclose(self.get_end(), new_point):
|
||||
self.add_line_to(new_point)
|
||||
self.add_line_to(new_point)
|
||||
if self.dissipating_time:
|
||||
assert self.time is not None
|
||||
self.time += dt
|
||||
|
|
|
|||
|
|
@ -7,19 +7,19 @@ from typing import TYPE_CHECKING, Any
|
|||
|
||||
import numpy as np
|
||||
|
||||
from manim._config import config
|
||||
from manim.animation.animation import Animation, prepare_animation
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
OpenGLGroup as Group,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
OpenGLMobject as Mobject,
|
||||
)
|
||||
from manim.constants import RendererType
|
||||
from manim.mobject.mobject import Group, Mobject
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLGroup, OpenGLMobject
|
||||
from manim.scene.scene import Scene
|
||||
from manim.utils.iterables import remove_list_redundancies
|
||||
from manim.utils.parameter_parsing import flatten_iterable_parameters
|
||||
from manim.utils.rate_functions import linear
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup
|
||||
from manim.mobject.types.vectorized_mobject import VGroup
|
||||
|
||||
__all__ = ["AnimationGroup", "Succession", "LaggedStart", "LaggedStartMap"]
|
||||
|
||||
|
|
@ -54,21 +54,25 @@ class AnimationGroup(Animation):
|
|||
def __init__(
|
||||
self,
|
||||
*animations: Animation | Iterable[Animation],
|
||||
group: Group | VGroup | None = None,
|
||||
group: Group | VGroup | OpenGLGroup | OpenGLVGroup | None = None,
|
||||
run_time: float | None = None,
|
||||
rate_func: Callable[[float], float] = linear,
|
||||
lag_ratio: float = 0,
|
||||
**kwargs: Any,
|
||||
):
|
||||
arg_anim = flatten_iterable_parameters(animations)
|
||||
|
||||
self.animations = [prepare_animation(anim) for anim in arg_anim]
|
||||
self.rate_func = rate_func
|
||||
if group is None:
|
||||
mobjects = remove_list_redundancies(
|
||||
[anim.mobject for anim in self.animations if not anim.introducer],
|
||||
[anim.mobject for anim in self.animations if not anim.is_introducer()],
|
||||
)
|
||||
self.group = Group(*mobjects)
|
||||
if config["renderer"] == RendererType.OPENGL:
|
||||
self.group: Group | VGroup | OpenGLGroup | OpenGLVGroup = OpenGLGroup(
|
||||
*mobjects
|
||||
)
|
||||
else:
|
||||
self.group = Group(*mobjects)
|
||||
else:
|
||||
self.group = group
|
||||
super().__init__(
|
||||
|
|
@ -76,7 +80,7 @@ class AnimationGroup(Animation):
|
|||
)
|
||||
self.run_time: float = self.init_run_time(run_time)
|
||||
|
||||
def get_all_mobjects(self) -> Sequence[Mobject]:
|
||||
def get_all_mobjects(self) -> Sequence[Mobject | OpenGLMobject]:
|
||||
return list(self.group)
|
||||
|
||||
def begin(self) -> None:
|
||||
|
|
@ -85,31 +89,31 @@ class AnimationGroup(Animation):
|
|||
f"Trying to play {self} without animations, this is not supported. "
|
||||
"Please add at least one subanimation."
|
||||
)
|
||||
|
||||
for anim in self.animations:
|
||||
if self.introducer:
|
||||
anim.introducer = True
|
||||
anim.begin()
|
||||
self.process_subanimation_buffer(anim.buffer)
|
||||
|
||||
self.anim_group_time = 0.0
|
||||
if self.suspend_mobject_updating:
|
||||
self.group.suspend_updating()
|
||||
for anim in self.animations:
|
||||
anim.begin()
|
||||
|
||||
def _setup_scene(self, scene: Scene) -> None:
|
||||
for anim in self.animations:
|
||||
anim._setup_scene(scene)
|
||||
|
||||
def finish(self) -> None:
|
||||
for anim in self.animations:
|
||||
anim.finish()
|
||||
self.anims_begun[:] = True
|
||||
self.anims_finished[:] = True
|
||||
for anim in self.animations:
|
||||
if self.remover:
|
||||
anim.remover = True
|
||||
anim.finish()
|
||||
self.process_subanimation_buffer(anim.buffer)
|
||||
|
||||
if self.suspend_mobject_updating:
|
||||
self.group.resume_updating()
|
||||
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
self._on_finish(scene)
|
||||
for anim in self.animations:
|
||||
if self.remover:
|
||||
anim.remover = self.remover
|
||||
anim.clean_up_from_scene(scene)
|
||||
|
||||
def update_mobjects(self, dt: float) -> None:
|
||||
for anim in self.anims_with_timings["anim"][
|
||||
self.anims_begun & ~self.anims_finished
|
||||
|
|
@ -247,6 +251,16 @@ class Succession(AnimationGroup):
|
|||
if self.active_animation:
|
||||
self.active_animation.update_mobjects(dt)
|
||||
|
||||
def _setup_scene(self, scene: Scene | None) -> None:
|
||||
if scene is None:
|
||||
return
|
||||
if self.is_introducer():
|
||||
for anim in self.animations:
|
||||
if not anim.is_introducer() and anim.mobject is not None:
|
||||
scene.add(anim.mobject)
|
||||
|
||||
self.scene = scene
|
||||
|
||||
def update_active_animation(self, index: int) -> None:
|
||||
self.active_index = index
|
||||
if index >= len(self.animations):
|
||||
|
|
@ -255,9 +269,8 @@ class Succession(AnimationGroup):
|
|||
self.active_end_time: float | None = None
|
||||
else:
|
||||
self.active_animation = self.animations[index]
|
||||
self.active_animation._setup_scene(self.scene)
|
||||
self.active_animation.begin()
|
||||
self.process_subanimation_buffer(self.active_animation.buffer)
|
||||
self.apply_buffer = True
|
||||
self.active_start_time = self.anims_with_timings[index]["start"]
|
||||
self.active_end_time = self.anims_with_timings[index]["end"]
|
||||
|
||||
|
|
@ -268,7 +281,6 @@ class Succession(AnimationGroup):
|
|||
"""
|
||||
if self.active_animation is not None:
|
||||
self.active_animation.finish()
|
||||
self.process_subanimation_buffer(self.active_animation.buffer)
|
||||
self.update_active_animation(self.active_index + 1)
|
||||
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
|
|
|
|||
|
|
@ -82,26 +82,19 @@ from typing import TYPE_CHECKING
|
|||
import numpy as np
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Self
|
||||
|
||||
from manim.mobject.text.text_mobject import Text
|
||||
from manim.scene.scene import Scene
|
||||
|
||||
from manim.constants import RIGHT, TAU
|
||||
from manim.mobject.opengl.opengl_surface import OpenGLSurface
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
|
||||
from manim.utils.color import ManimColor
|
||||
from manim.utils.space_ops import rotate_vector
|
||||
|
||||
from .. import config
|
||||
from ..animation.animation import Animation
|
||||
from ..animation.composition import Succession
|
||||
from ..mobject.opengl.opengl_mobject import (
|
||||
OpenGLGroup as Group,
|
||||
)
|
||||
from ..mobject.opengl.opengl_mobject import (
|
||||
OpenGLMobject as Mobject,
|
||||
)
|
||||
from ..mobject.mobject import Group, Mobject
|
||||
from ..mobject.types.vectorized_mobject import VMobject
|
||||
from ..utils.bezier import integer_interpolate
|
||||
from ..utils.rate_functions import double_smooth, linear
|
||||
|
||||
|
|
@ -120,7 +113,11 @@ class ShowPartial(Animation):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, mobject: VMobject | OpenGLSurface | None, **kwargs: Any):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: VMobject | OpenGLVMobject | OpenGLSurface | None,
|
||||
**kwargs,
|
||||
):
|
||||
pointwise = getattr(mobject, "pointwise_become_partial", None)
|
||||
if not callable(pointwise):
|
||||
raise TypeError(f"{self.__class__.__name__} only works for VMobjects.")
|
||||
|
|
@ -131,11 +128,10 @@ class ShowPartial(Animation):
|
|||
submobject: Mobject,
|
||||
starting_submobject: Mobject,
|
||||
alpha: float,
|
||||
) -> Self:
|
||||
) -> None:
|
||||
submobject.pointwise_become_partial(
|
||||
starting_submobject, *self._get_bounds(alpha)
|
||||
)
|
||||
return self
|
||||
|
||||
def _get_bounds(self, alpha: float) -> tuple[float, float]:
|
||||
raise NotImplementedError("Please use Create or ShowPassingFlash")
|
||||
|
|
@ -170,7 +166,7 @@ class Create(ShowPartial):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
mobject: VMobject | OpenGLSurface,
|
||||
mobject: VMobject | OpenGLVMobject | OpenGLSurface,
|
||||
lag_ratio: float = 1.0,
|
||||
introducer: bool = True,
|
||||
**kwargs,
|
||||
|
|
@ -200,7 +196,7 @@ class Uncreate(Create):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
mobject: VMobject,
|
||||
mobject: VMobject | OpenGLVMobject,
|
||||
reverse_rate_function: bool = True,
|
||||
remover: bool = True,
|
||||
**kwargs,
|
||||
|
|
@ -228,13 +224,11 @@ class DrawBorderThenFill(Animation):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
vmobject: VMobject,
|
||||
vmobject: VMobject | OpenGLVMobject,
|
||||
run_time: float = 2,
|
||||
rate_func: Callable[[float], float] = double_smooth,
|
||||
stroke_width: float = 2,
|
||||
stroke_color: ManimColor | None = None,
|
||||
draw_border_animation_config: dict = {}, # what does this dict accept?
|
||||
fill_animation_config: dict = {},
|
||||
stroke_color: str = None,
|
||||
introducer: bool = True,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
|
|
@ -250,15 +244,13 @@ class DrawBorderThenFill(Animation):
|
|||
self.stroke_color = stroke_color
|
||||
self.outline = self.get_outline()
|
||||
|
||||
def _typecheck_input(self, vmobject: VMobject) -> None:
|
||||
if not isinstance(vmobject, VMobject):
|
||||
def _typecheck_input(self, vmobject: VMobject | OpenGLVMobject) -> None:
|
||||
if not isinstance(vmobject, (VMobject, OpenGLVMobject)):
|
||||
raise TypeError(
|
||||
f"{self.__class__.__name__} only works for vectorized Mobjects"
|
||||
)
|
||||
|
||||
def begin(self) -> None:
|
||||
# this self.get_outline() has to be called
|
||||
# before super().begin(), for whatever reason
|
||||
self.outline = self.get_outline()
|
||||
super().begin()
|
||||
|
||||
|
|
@ -269,7 +261,7 @@ class DrawBorderThenFill(Animation):
|
|||
sm.set_stroke(color=self.get_stroke_color(sm), width=self.stroke_width)
|
||||
return outline
|
||||
|
||||
def get_stroke_color(self, vmobject: VMobject) -> ManimColor:
|
||||
def get_stroke_color(self, vmobject: VMobject | OpenGLVMobject) -> ManimColor:
|
||||
if self.stroke_color:
|
||||
return self.stroke_color
|
||||
elif vmobject.get_stroke_width() > 0:
|
||||
|
|
@ -283,9 +275,9 @@ class DrawBorderThenFill(Animation):
|
|||
self,
|
||||
submobject: Mobject,
|
||||
starting_submobject: Mobject,
|
||||
outline: Mobject,
|
||||
outline,
|
||||
alpha: float,
|
||||
) -> None:
|
||||
) -> None: # Fixme: not matching the parent class? What is outline doing here?
|
||||
index: int
|
||||
subalpha: float
|
||||
index, subalpha = integer_interpolate(0, 2, alpha)
|
||||
|
|
@ -325,13 +317,13 @@ class Write(DrawBorderThenFill):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
vmobject: VMobject,
|
||||
vmobject: VMobject | OpenGLVMobject,
|
||||
rate_func: Callable[[float], float] = linear,
|
||||
reverse: bool = False,
|
||||
run_time: float | None = None,
|
||||
lag_ratio: float | None = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
run_time: float | None = kwargs.pop("run_time", None)
|
||||
lag_ratio: float | None = kwargs.pop("lag_ratio", None)
|
||||
run_time, lag_ratio = self._set_default_config_from_length(
|
||||
vmobject,
|
||||
run_time,
|
||||
|
|
@ -351,7 +343,7 @@ class Write(DrawBorderThenFill):
|
|||
|
||||
def _set_default_config_from_length(
|
||||
self,
|
||||
vmobject: VMobject,
|
||||
vmobject: VMobject | OpenGLVMobject,
|
||||
run_time: float | None,
|
||||
lag_ratio: float | None,
|
||||
) -> tuple[float, float]:
|
||||
|
|
@ -362,15 +354,18 @@ class Write(DrawBorderThenFill):
|
|||
lag_ratio = min(4.0 / max(1.0, length), 0.2)
|
||||
return run_time, lag_ratio
|
||||
|
||||
def reverse_submobjects(self) -> None:
|
||||
self.mobject.invert(recursive=True)
|
||||
|
||||
def begin(self) -> None:
|
||||
if self.reverse:
|
||||
self.mobject.reverse_submobjects(recursive=True)
|
||||
self.reverse_submobjects()
|
||||
super().begin()
|
||||
|
||||
def finish(self) -> None:
|
||||
super().finish()
|
||||
if self.reverse:
|
||||
self.mobject.reverse_submobjects(recursive=True)
|
||||
self.reverse_submobjects()
|
||||
|
||||
|
||||
class Unwrite(Write):
|
||||
|
|
@ -457,33 +452,28 @@ class SpiralIn(Animation):
|
|||
self,
|
||||
shapes: Mobject,
|
||||
scale_factor: float = 8,
|
||||
fade_in_fraction: float = 0.3,
|
||||
**kwargs: Any,
|
||||
fade_in_fraction=0.3,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self.shapes = shapes.copy()
|
||||
self.scale_factor = scale_factor
|
||||
self.shape_center = shapes.get_center()
|
||||
self.fade_in_fraction = fade_in_fraction
|
||||
self.final_positions = [shape.get_center() for shape in shapes]
|
||||
self.initial_positions = [
|
||||
final_pos + (final_pos - self.shape_center) * self.scale_factor
|
||||
for final_pos in self.final_positions
|
||||
]
|
||||
for shape in shapes:
|
||||
shape.final_position = shape.get_center()
|
||||
shape.initial_position = (
|
||||
shape.final_position
|
||||
+ (shape.final_position - self.shape_center) * self.scale_factor
|
||||
)
|
||||
shape.move_to(shape.initial_position)
|
||||
shape.save_state()
|
||||
|
||||
super().__init__(shapes, introducer=True, **kwargs)
|
||||
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
alpha = self.rate_func(alpha)
|
||||
for i, shape in enumerate(self.mobject):
|
||||
initial_pos = self.initial_positions[i]
|
||||
final_pos = self.final_positions[i]
|
||||
# Avoid shape.rotate() in order to preserve the bounding box of the shape.
|
||||
# Instead, rotate the vector itself to calculate the current shape position.
|
||||
vector = initial_pos - self.shape_center + (final_pos - initial_pos) * alpha
|
||||
vector = rotate_vector(vector, TAU * alpha)
|
||||
shape.move_to(self.shape_center + vector)
|
||||
|
||||
original_shape = self.shapes[i]
|
||||
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()
|
||||
new_fill_opacity = min(
|
||||
|
|
@ -492,6 +482,9 @@ class SpiralIn(Animation):
|
|||
new_stroke_opacity = min(
|
||||
stroke_opacity, alpha * stroke_opacity / self.fade_in_fraction
|
||||
)
|
||||
shape.shift((shape.final_position - shape.initial_position) * alpha)
|
||||
shape.rotate(TAU * alpha, about_point=self.shape_center)
|
||||
shape.rotate(-TAU * alpha, about_point=shape.get_center_of_mass())
|
||||
shape.set_fill(opacity=new_fill_opacity)
|
||||
shape.set_stroke(opacity=new_stroke_opacity)
|
||||
|
||||
|
|
@ -531,7 +524,7 @@ class ShowIncreasingSubsets(Animation):
|
|||
**kwargs,
|
||||
)
|
||||
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
n_submobs = len(self.all_submobs)
|
||||
value = (
|
||||
1 - self.rate_func(alpha)
|
||||
|
|
|
|||
|
|
@ -23,15 +23,12 @@ from typing import Any
|
|||
|
||||
import numpy as np
|
||||
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
OpenGLGroup as Group,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
OpenGLMobject as Mobject,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
|
||||
from ..animation.transform import Transform
|
||||
from ..constants import ORIGIN
|
||||
from ..mobject.mobject import Group, Mobject
|
||||
from ..scene.scene import Scene
|
||||
|
||||
|
||||
class _Fade(Transform):
|
||||
|
|
@ -67,7 +64,7 @@ class _Fade(Transform):
|
|||
self.point_target = False
|
||||
if shift is None:
|
||||
if target_position is not None:
|
||||
if isinstance(target_position, Mobject):
|
||||
if isinstance(target_position, (Mobject, OpenGLMobject)):
|
||||
target_position = target_position.get_center()
|
||||
shift = target_position - mobject.get_center()
|
||||
self.point_target = True
|
||||
|
|
@ -77,12 +74,12 @@ class _Fade(Transform):
|
|||
self.scale_factor = scale
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def _create_faded_mobject(self, fade_in: bool) -> Mobject:
|
||||
def _create_faded_mobject(self, fadeIn: bool) -> Mobject:
|
||||
"""Create a faded, shifted and scaled copy of the mobject.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fade_in
|
||||
fadeIn
|
||||
Whether the faded mobject is used to fade in.
|
||||
|
||||
Returns
|
||||
|
|
@ -92,7 +89,7 @@ class _Fade(Transform):
|
|||
"""
|
||||
faded_mobject: Mobject = self.mobject.copy() # type: ignore[assignment]
|
||||
faded_mobject.fade(1)
|
||||
direction_modifier = -1 if fade_in and not self.point_target else 1
|
||||
direction_modifier = -1 if fadeIn and not self.point_target else 1
|
||||
faded_mobject.shift(self.shift_vector * direction_modifier)
|
||||
faded_mobject.scale(self.scale_factor)
|
||||
return faded_mobject
|
||||
|
|
@ -140,10 +137,10 @@ class FadeIn(_Fade):
|
|||
super().__init__(*mobjects, introducer=True, **kwargs)
|
||||
|
||||
def create_target(self) -> Mobject:
|
||||
return self.mobject
|
||||
return self.mobject # type: ignore[return-value]
|
||||
|
||||
def create_starting_mobject(self) -> Mobject:
|
||||
return self._create_faded_mobject(fade_in=True)
|
||||
return self._create_faded_mobject(fadeIn=True)
|
||||
|
||||
|
||||
class FadeOut(_Fade):
|
||||
|
|
@ -188,8 +185,8 @@ class FadeOut(_Fade):
|
|||
super().__init__(*mobjects, remover=True, **kwargs)
|
||||
|
||||
def create_target(self) -> Mobject:
|
||||
return self._create_faded_mobject(fade_in=False)
|
||||
return self._create_faded_mobject(fadeIn=False)
|
||||
|
||||
def begin(self) -> None:
|
||||
super().begin()
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
super().clean_up_from_scene(scene)
|
||||
self.interpolate(0)
|
||||
|
|
|
|||
|
|
@ -39,10 +39,12 @@ from ..utils.paths import spiral_path
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.geometry.line import Arrow
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.typing import Point3DLike, Vector3DLike
|
||||
from manim.utils.color import ParsableManimColor
|
||||
|
||||
from ..mobject.mobject import Mobject
|
||||
|
||||
|
||||
class GrowFromPoint(Transform):
|
||||
"""Introduce an :class:`~.Mobject` by growing it from a point.
|
||||
|
|
@ -85,10 +87,10 @@ class GrowFromPoint(Transform):
|
|||
self.point_color = point_color
|
||||
super().__init__(mobject, introducer=True, **kwargs)
|
||||
|
||||
def create_target(self) -> Mobject:
|
||||
def create_target(self) -> Mobject | OpenGLMobject:
|
||||
return self.mobject
|
||||
|
||||
def create_starting_mobject(self) -> Mobject:
|
||||
def create_starting_mobject(self) -> Mobject | OpenGLMobject:
|
||||
start = super().create_starting_mobject()
|
||||
start.scale(0)
|
||||
start.move_to(self.point)
|
||||
|
|
@ -167,7 +169,7 @@ class GrowFromEdge(GrowFromPoint):
|
|||
point_color: ParsableManimColor | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
point = mobject.get_bounding_box_point(edge)
|
||||
point = mobject.get_critical_point(edge)
|
||||
super().__init__(mobject, point, point_color=point_color, **kwargs)
|
||||
|
||||
|
||||
|
|
@ -201,7 +203,7 @@ class GrowArrow(GrowFromPoint):
|
|||
point = arrow.get_start()
|
||||
super().__init__(arrow, point, point_color=point_color, **kwargs)
|
||||
|
||||
def create_starting_mobject(self) -> Mobject:
|
||||
def create_starting_mobject(self) -> Mobject | OpenGLMobject:
|
||||
start_arrow = self.mobject.copy()
|
||||
start_arrow.scale(0, scale_tips=True, about_point=self.point)
|
||||
if self.point_color:
|
||||
|
|
|
|||
|
|
@ -48,7 +48,8 @@ from manim.mobject.geometry.arc import Circle, Dot
|
|||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.geometry.polygram import Rectangle
|
||||
from manim.mobject.geometry.shape_matchers import SurroundingRectangle
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.scene.scene import Scene
|
||||
|
||||
from .. import config
|
||||
from ..animation.animation import Animation
|
||||
|
|
@ -59,12 +60,8 @@ from ..animation.movement import Homotopy
|
|||
from ..animation.transform import Transform
|
||||
from ..animation.updaters.update import UpdateFromFunc
|
||||
from ..constants import *
|
||||
from ..mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from ..mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
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, PURE_YELLOW, ParsableManimColor
|
||||
|
|
@ -164,7 +161,7 @@ class Indicate(Transform):
|
|||
self.scale_factor = scale_factor
|
||||
super().__init__(mobject, rate_func=rate_func, **kwargs)
|
||||
|
||||
def create_target(self) -> Mobject:
|
||||
def create_target(self) -> Mobject | OpenGLMobject:
|
||||
target = self.mobject.copy()
|
||||
target.scale(self.scale_factor)
|
||||
target.set_color(self.color)
|
||||
|
|
@ -322,8 +319,8 @@ class ShowPassingFlash(ShowPartial):
|
|||
lower = max(lower, 0)
|
||||
return (lower, upper)
|
||||
|
||||
def finish(self) -> None:
|
||||
super().finish()
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
super().clean_up_from_scene(scene)
|
||||
for submob, start in self.get_all_families_zipped():
|
||||
submob.pointwise_become_partial(start, 0, 1)
|
||||
|
||||
|
|
@ -410,7 +407,6 @@ class ApplyWave(Homotopy):
|
|||
time_width: float = 1,
|
||||
ripples: int = 1,
|
||||
run_time: float = 2,
|
||||
introducer: bool = True,
|
||||
**kwargs: Any,
|
||||
):
|
||||
x_min = mobject.get_left()[0]
|
||||
|
|
@ -486,9 +482,7 @@ class ApplyWave(Homotopy):
|
|||
return_value: tuple[float, float, float] = np.array([x, y, z]) + nudge
|
||||
return return_value
|
||||
|
||||
super().__init__(
|
||||
homotopy, mobject, run_time=run_time, introducer=introducer, **kwargs
|
||||
)
|
||||
super().__init__(homotopy, mobject, run_time=run_time, **kwargs)
|
||||
|
||||
|
||||
class Wiggle(Animation):
|
||||
|
|
@ -574,7 +568,6 @@ class Wiggle(Animation):
|
|||
return self
|
||||
|
||||
|
||||
# TODO: get rid of this if condition madness
|
||||
class Circumscribe(Succession):
|
||||
r"""Draw a temporary line surrounding the mobject.
|
||||
|
||||
|
|
|
|||
|
|
@ -82,7 +82,9 @@ class Homotopy(Animation):
|
|||
**kwargs: Any,
|
||||
):
|
||||
self.homotopy = homotopy
|
||||
self.apply_function_kwargs = apply_function_kwargs or {}
|
||||
self.apply_function_kwargs = (
|
||||
apply_function_kwargs if apply_function_kwargs is not None else {}
|
||||
)
|
||||
super().__init__(mobject, run_time=run_time, **kwargs)
|
||||
|
||||
def function_at_time_t(self, t: float) -> MappingFunction:
|
||||
|
|
@ -98,7 +100,7 @@ class Homotopy(Animation):
|
|||
starting_submobject: Mobject,
|
||||
alpha: float,
|
||||
) -> Self:
|
||||
submobject.match_points(starting_submobject)
|
||||
submobject.points = starting_submobject.points
|
||||
submobject.apply_function(
|
||||
self.function_at_time_t(alpha),
|
||||
**self.apply_function_kwargs,
|
||||
|
|
@ -159,7 +161,7 @@ class PhaseFlow(Animation):
|
|||
**kwargs,
|
||||
)
|
||||
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
if hasattr(self, "last_alpha"):
|
||||
dt = self.virtual_time * (
|
||||
self.rate_func(alpha) - self.rate_func(self.last_alpha)
|
||||
|
|
@ -195,6 +197,6 @@ class MoveAlongPath(Animation):
|
|||
mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs
|
||||
)
|
||||
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
point = self.path.point_from_proportion(self.rate_func(alpha))
|
||||
self.mobject.move_to(point)
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ class ChangingDecimal(Animation):
|
|||
if not isinstance(decimal_mob, DecimalNumber):
|
||||
raise TypeError("ChangingDecimal can only take in a DecimalNumber")
|
||||
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
self.mobject.set_value(self.number_update_func(self.rate_func(alpha))) # type: ignore[attr-defined]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Protocol
|
||||
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.utils.rate_functions import RateFunction
|
||||
|
||||
from .scene_buffer import SceneBuffer
|
||||
|
||||
M = TypeVar("M", bound="Mobject", default="Mobject")
|
||||
|
||||
|
||||
__all__ = ("AnimationProtocol",)
|
||||
|
||||
|
||||
class AnimationProtocol(Protocol):
|
||||
"""A protocol that all animations must implement."""
|
||||
|
||||
buffer: SceneBuffer
|
||||
"""The interface to the scene. This can be used to add, remove, or replace mobjects on the scene."""
|
||||
|
||||
apply_buffer: bool
|
||||
"""Normally, the buffer is only applied at the beginning and end of an animation.
|
||||
|
||||
To apply it mid animation, set :attr:`apply_buffer` to ``True``."""
|
||||
|
||||
def begin(self) -> object:
|
||||
"""Called before the animation starts.
|
||||
|
||||
This is where all setup for the animation should be done, such
|
||||
as creating copies/targets of the mobject to animate, etc.
|
||||
"""
|
||||
|
||||
def finish(self) -> object:
|
||||
"""Called after the animation finishes.
|
||||
|
||||
This is where all cleanup should happen, such as removing
|
||||
mobjects from the scene, etc.
|
||||
"""
|
||||
|
||||
def interpolate(self, alpha: float) -> object:
|
||||
"""This is called every frame of the animation.
|
||||
|
||||
This method should update the animation to the given ``alpha`` value.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
alpha : a value in the interval :math:`[0, 1]` representing the proportion of the animation that has passed.
|
||||
"""
|
||||
|
||||
def get_run_time(self) -> float:
|
||||
"""Compute and return the run time of the animation."""
|
||||
raise NotImplementedError
|
||||
|
||||
def update_rate_info(
|
||||
self,
|
||||
run_time: float | None,
|
||||
rate_func: RateFunction | None,
|
||||
lag_ratio: float | None,
|
||||
) -> object:
|
||||
"""Update the rate information for the animation.
|
||||
|
||||
If any value is ``None``, it should not update
|
||||
the animation's corresponding attribute.
|
||||
"""
|
||||
|
||||
def update_mobjects(self, dt: float) -> object:
|
||||
"""Update the mobjects during the animation.
|
||||
|
||||
This method is called every frame of the animation
|
||||
"""
|
||||
|
||||
|
||||
class MobjectAnimation(AnimationProtocol, Protocol[M]):
|
||||
mobject: M
|
||||
"""The mobject that is being animated."""
|
||||
|
||||
suspend_mobject_updating: bool
|
||||
"""Whether to suspend updating the mobject during the animation."""
|
||||
|
|
@ -4,16 +4,18 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["Rotating", "Rotate"]
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from manim.animation.animation import Animation
|
||||
from manim.constants import ORIGIN, OUT, PI, TAU
|
||||
from manim.utils.rate_functions import RateFunction, linear
|
||||
from ..animation.animation import Animation
|
||||
from ..animation.transform import Transform
|
||||
from ..constants import OUT, PI, TAU
|
||||
from ..utils.rate_functions import linear
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.typing import Point3DLike, Vector3DLike
|
||||
from manim.utils.rate_functions import RateFunction
|
||||
from ..mobject.mobject import Mobject
|
||||
from ..mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from ..typing import Point3DLike, Vector3DLike
|
||||
|
||||
|
||||
class Rotating(Animation):
|
||||
|
|
@ -90,30 +92,18 @@ class Rotating(Animation):
|
|||
axis: Vector3DLike = OUT,
|
||||
about_point: Point3DLike | None = None,
|
||||
about_edge: Vector3DLike | None = None,
|
||||
rate_func: RateFunction = linear,
|
||||
suspend_mobject_updating: bool = False,
|
||||
run_time: float = 5,
|
||||
rate_func: Callable[[float], float] = linear,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(
|
||||
mobject,
|
||||
rate_func=rate_func,
|
||||
suspend_mobject_updating=suspend_mobject_updating,
|
||||
**kwargs,
|
||||
)
|
||||
) -> None:
|
||||
self.angle = angle
|
||||
self.axis = axis
|
||||
self.about_point = about_point
|
||||
self.about_edge = about_edge
|
||||
super().__init__(mobject, run_time=run_time, rate_func=rate_func, **kwargs)
|
||||
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
pairs = zip(
|
||||
self.mobject.family_members_with_points(),
|
||||
self.starting_mobject.family_members_with_points(),
|
||||
strict=True,
|
||||
)
|
||||
for sm1, sm2 in pairs:
|
||||
sm1.points[:] = sm2.points
|
||||
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
self.mobject.become(self.starting_mobject)
|
||||
self.mobject.rotate(
|
||||
self.rate_func(alpha) * self.angle,
|
||||
axis=self.axis,
|
||||
|
|
@ -122,7 +112,7 @@ class Rotating(Animation):
|
|||
)
|
||||
|
||||
|
||||
class Rotate(Rotating):
|
||||
class Rotate(Transform):
|
||||
"""Animation that rotates a Mobject.
|
||||
|
||||
Parameters
|
||||
|
|
@ -154,7 +144,7 @@ class Rotate(Rotating):
|
|||
rate_func=linear,
|
||||
),
|
||||
Rotate(Square(side_length=0.5), angle=2*PI, rate_func=linear),
|
||||
)
|
||||
)
|
||||
|
||||
See also
|
||||
--------
|
||||
|
|
@ -167,16 +157,28 @@ class Rotate(Rotating):
|
|||
mobject: Mobject,
|
||||
angle: float = PI,
|
||||
axis: Vector3DLike = OUT,
|
||||
run_time: float = 1,
|
||||
about_edge: Vector3DLike = ORIGIN,
|
||||
about_point: Point3DLike | None = None,
|
||||
about_edge: Vector3DLike | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(
|
||||
mobject,
|
||||
angle,
|
||||
axis,
|
||||
run_time=run_time,
|
||||
about_edge=about_edge,
|
||||
introducer=True,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
if "path_arc" not in kwargs:
|
||||
kwargs["path_arc"] = angle
|
||||
if "path_arc_axis" not in kwargs:
|
||||
kwargs["path_arc_axis"] = axis
|
||||
self.angle = angle
|
||||
self.axis = axis
|
||||
self.about_edge = about_edge
|
||||
self.about_point = about_point
|
||||
if self.about_point is None:
|
||||
self.about_point = mobject.get_center()
|
||||
super().__init__(mobject, path_arc_centers=self.about_point, **kwargs)
|
||||
|
||||
def create_target(self) -> Mobject | OpenGLMobject:
|
||||
target = self.mobject.copy()
|
||||
target.rotate(
|
||||
self.angle,
|
||||
axis=self.axis,
|
||||
about_point=self.about_point,
|
||||
about_edge=self.about_edge,
|
||||
)
|
||||
return target
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator, Sequence
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
|
||||
__all__ = ["SceneBuffer", "SceneOperation"]
|
||||
|
||||
|
||||
class SceneOperation(Enum):
|
||||
ADD = "add"
|
||||
REMOVE = "remove"
|
||||
REPLACE = "replace"
|
||||
|
||||
|
||||
class SceneBuffer:
|
||||
"""
|
||||
A "buffer" between :class:`.Scene` and :class:`.Animation`
|
||||
|
||||
Operations an animation wants to do on :class:`.Scene` should be
|
||||
done here (eg. :meth:`.Scene.add`, :meth:`.Scene.remove`). The
|
||||
scene will then apply these changes at specific points (namely
|
||||
at the beginning and end of animations)
|
||||
|
||||
It is the scenes job to clear the buffer in between the beginning
|
||||
and end of animations.
|
||||
|
||||
To iterate over the operations, simply iterate over the buffer.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> buffer = SceneBuffer()
|
||||
>>> buffer.add(Square())
|
||||
>>> buffer.remove(Circle())
|
||||
>>> buffer.replace(Square(), Circle(), flag=True)
|
||||
>>> for operation in buffer:
|
||||
... print(operation)
|
||||
(SceneOperation.ADD, (Square(),), {})
|
||||
(SceneOperation.REMOVE, (Circle(),), {})
|
||||
(SceneOperation.REPLACE, (Square(), Circle()), {"flag": True})
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.operations: list[
|
||||
tuple[SceneOperation, Sequence[Mobject], dict[str, Any]]
|
||||
] = []
|
||||
|
||||
def add(self, *mobs: Mobject, **kwargs: Any) -> None:
|
||||
"""Add mobjects to the scene."""
|
||||
self.operations.append((SceneOperation.ADD, mobs, kwargs))
|
||||
|
||||
def remove(self, *mobs: Mobject, **kwargs: Any) -> None:
|
||||
"""Remove mobjects from the scene."""
|
||||
self.operations.append((SceneOperation.REMOVE, mobs, kwargs))
|
||||
|
||||
def replace(self, mob: Mobject, *replacements: Mobject, **kwargs: Any) -> None:
|
||||
"""Replace a ``mob`` with ``replacements`` on the scene."""
|
||||
self.operations.append((SceneOperation.REPLACE, (mob, *replacements), kwargs))
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear the buffer."""
|
||||
self.operations.clear()
|
||||
|
||||
def __str__(self) -> str:
|
||||
operations = self.operations
|
||||
return f"{type(self).__name__}({operations=})"
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
def __iter__(
|
||||
self,
|
||||
) -> Iterator[tuple[SceneOperation, Sequence[Mobject], dict[str, Any]]]:
|
||||
return iter(self.operations)
|
||||
|
|
@ -12,10 +12,10 @@ from numpy import piecewise
|
|||
from ..animation.animation import Animation, Wait, prepare_animation
|
||||
from ..animation.composition import AnimationGroup
|
||||
from ..mobject.mobject import Mobject, _AnimationBuilder
|
||||
from ..scene.scene import Scene
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..mobject.mobject import Updater
|
||||
from .protocol import MobjectAnimation
|
||||
|
||||
__all__ = ["ChangeSpeed"]
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ class ChangeSpeed(Animation):
|
|||
affects_speed_updaters: bool = True,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
if isinstance(anim, AnimationGroup):
|
||||
if issubclass(type(anim), AnimationGroup):
|
||||
self.anim = type(anim)(
|
||||
*map(self.setup, anim.animations),
|
||||
group=anim.group,
|
||||
|
|
@ -209,11 +209,11 @@ class ChangeSpeed(Animation):
|
|||
super().__init__(
|
||||
self.anim.mobject,
|
||||
rate_func=self.rate_func,
|
||||
run_time=scaled_total_time * self.anim.get_run_time(),
|
||||
run_time=scaled_total_time * self.anim.run_time,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def setup(self, anim: MobjectAnimation):
|
||||
def setup(self, anim):
|
||||
if type(anim) is Wait:
|
||||
anim.interpolate = types.MethodType(
|
||||
lambda self, alpha: self.rate_func(alpha), anim
|
||||
|
|
@ -282,11 +282,15 @@ class ChangeSpeed(Animation):
|
|||
def update_mobjects(self, dt: float) -> None:
|
||||
self.anim.update_mobjects(dt)
|
||||
|
||||
def begin(self) -> None:
|
||||
self.anim.begin()
|
||||
self.process_subanimation_buffer(self.anim.buffer)
|
||||
|
||||
def finish(self) -> None:
|
||||
ChangeSpeed.is_changing_dt = False
|
||||
self.anim.finish()
|
||||
self.process_subanimation_buffer(self.anim.buffer)
|
||||
|
||||
def begin(self) -> None:
|
||||
self.anim.begin()
|
||||
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
self.anim.clean_up_from_scene(scene)
|
||||
|
||||
def _setup_scene(self, scene) -> None:
|
||||
self.anim._setup_scene(scene)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from manim.typing import PathFuncType
|
||||
|
||||
__all__ = [
|
||||
"Transform",
|
||||
"ReplacementTransform",
|
||||
|
|
@ -30,31 +28,31 @@ __all__ = [
|
|||
|
||||
import inspect
|
||||
import types
|
||||
from collections.abc import Callable, Sequence
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.data_structures import MethodWithArgs
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
OpenGLGroup as Group,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
OpenGLMobject as Mobject,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLGroup, OpenGLMobject
|
||||
|
||||
from .. import config
|
||||
from ..animation.animation import Animation
|
||||
from ..constants import (
|
||||
DEFAULT_POINTWISE_FUNCTION_RUN_TIME,
|
||||
DEGREES,
|
||||
ORIGIN,
|
||||
OUT,
|
||||
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 manim.typing import Point3DLike, Point3DLike_Array
|
||||
from ..scene.scene import Scene
|
||||
from ..typing import Point3DLike, Point3DLike_Array
|
||||
|
||||
|
||||
class Transform(Animation):
|
||||
|
|
@ -182,33 +180,46 @@ class Transform(Animation):
|
|||
@property
|
||||
def path_func(
|
||||
self,
|
||||
) -> PathFuncType:
|
||||
) -> Callable[
|
||||
[Iterable[np.ndarray], Iterable[np.ndarray], float],
|
||||
Iterable[np.ndarray],
|
||||
]:
|
||||
return self._path_func
|
||||
|
||||
@path_func.setter
|
||||
def path_func(
|
||||
self,
|
||||
path_func: PathFuncType,
|
||||
path_func: Callable[
|
||||
[Iterable[np.ndarray], Iterable[np.ndarray], float],
|
||||
Iterable[np.ndarray],
|
||||
],
|
||||
) -> None:
|
||||
if path_func is not None:
|
||||
self._path_func = path_func
|
||||
|
||||
def begin(self) -> None:
|
||||
# Use a copy of target_mobject for the align_data
|
||||
# call so that the actual target_mobject stays
|
||||
# preserved.
|
||||
self.target_mobject = self.create_target()
|
||||
self.target_copy = self.target_mobject.copy()
|
||||
self.mobject.align_data_and_family(self.target_copy)
|
||||
|
||||
# Note, this potentially changes the structure
|
||||
# of both mobject and target_mobject
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
self.mobject.align_data_and_family(self.target_copy)
|
||||
else:
|
||||
self.mobject.align_data(self.target_copy)
|
||||
super().begin()
|
||||
|
||||
def create_target(self) -> Mobject:
|
||||
def create_target(self) -> Mobject | OpenGLMobject:
|
||||
# Has no meaningful effect here, but may be useful
|
||||
# in subclasses
|
||||
return self.target_mobject
|
||||
|
||||
def finish(self) -> None:
|
||||
super().finish()
|
||||
def clean_up_from_scene(self, scene: Scene) -> None:
|
||||
super().clean_up_from_scene(scene)
|
||||
if self.replace_mobject_with_target_in_scene:
|
||||
self.buffer.replace(self.mobject, self.target_mobject)
|
||||
scene.replace(self.mobject, self.target_mobject)
|
||||
|
||||
def get_all_mobjects(self) -> Sequence[Mobject]:
|
||||
return [
|
||||
|
|
@ -218,13 +229,15 @@ class Transform(Animation):
|
|||
self.target_copy,
|
||||
]
|
||||
|
||||
def get_all_families_zipped(self) -> zip[tuple[Mobject, Mobject, Mobject]]:
|
||||
def get_all_families_zipped(self) -> Iterable[tuple]: # more precise typing?
|
||||
mobs = [
|
||||
self.mobject,
|
||||
self.starting_mobject,
|
||||
self.target_copy,
|
||||
]
|
||||
return zip(*(mob.get_family() for mob in mobs), strict=True)
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
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,
|
||||
|
|
@ -471,7 +484,7 @@ class ApplyMethod(Transform):
|
|||
"Whoops, looks like you accidentally invoked "
|
||||
"the method you want to animate",
|
||||
)
|
||||
assert isinstance(method.__self__, Mobject)
|
||||
assert isinstance(method.__self__, (Mobject, OpenGLMobject))
|
||||
|
||||
def create_target(self) -> Mobject:
|
||||
method = self.method
|
||||
|
|
@ -615,7 +628,7 @@ class ApplyFunction(Transform):
|
|||
|
||||
def create_target(self) -> Any:
|
||||
target = self.function(self.mobject.copy())
|
||||
if not isinstance(target, Mobject):
|
||||
if not isinstance(target, (Mobject, OpenGLMobject)):
|
||||
raise TypeError(
|
||||
"Functions passed to ApplyFunction must return object of type Mobject",
|
||||
)
|
||||
|
|
@ -680,7 +693,6 @@ class ApplyComplexFunction(ApplyMethod):
|
|||
super().__init__(method, function, **kwargs)
|
||||
|
||||
def _init_path_func(self) -> None:
|
||||
# TODO: this seems broken?
|
||||
func1 = self.function(complex(1))
|
||||
self.path_arc = np.log(func1).imag
|
||||
super()._init_path_func()
|
||||
|
|
@ -724,10 +736,13 @@ 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, strict=True):
|
||||
|
|
@ -736,7 +751,21 @@ class CyclicReplace(Transform):
|
|||
|
||||
|
||||
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?
|
||||
|
|
@ -824,12 +853,22 @@ 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
|
||||
mobject.save_state()
|
||||
group = Group(mobject, target_mobject.copy())
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
group = OpenGLGroup(mobject, target_mobject.copy())
|
||||
else:
|
||||
group = Group(mobject, target_mobject.copy())
|
||||
super().__init__(group, **kwargs)
|
||||
|
||||
def begin(self):
|
||||
|
|
@ -870,11 +909,11 @@ class FadeTransform(Transform):
|
|||
def get_all_families_zipped(self):
|
||||
return Animation.get_all_families_zipped(self)
|
||||
|
||||
def finish(self):
|
||||
Animation.finish(self) # TODO: is this really needed over super()?
|
||||
self.buffer.remove(self.mobject)
|
||||
def clean_up_from_scene(self, scene):
|
||||
Animation.clean_up_from_scene(self, scene)
|
||||
scene.remove(self.mobject)
|
||||
self.mobject[0].restore()
|
||||
self.buffer.add(self.to_add_on_completion)
|
||||
scene.add(self.to_add_on_completion)
|
||||
|
||||
|
||||
class FadeTransformPieces(FadeTransform):
|
||||
|
|
|
|||
|
|
@ -4,28 +4,25 @@ from __future__ import annotations
|
|||
|
||||
__all__ = ["TransformMatchingShapes", "TransformMatchingTex"]
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
OpenGLGroup as Group,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
OpenGLMobject as Mobject,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_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
|
||||
from ..mobject.mobject import Group, Mobject
|
||||
from ..mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from .composition import AnimationGroup
|
||||
from .fading import FadeIn, FadeOut
|
||||
from .transform import FadeTransformPieces, Transform
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..scene.scene import Scene
|
||||
|
||||
|
||||
class TransformMatchingAbstractBase(AnimationGroup):
|
||||
"""Abstract base class for transformations that keep track of matching parts.
|
||||
|
|
@ -78,9 +75,16 @@ class TransformMatchingAbstractBase(AnimationGroup):
|
|||
transform_mismatches: bool = False,
|
||||
fade_transform_mismatches: bool = False,
|
||||
key_map: dict | None = None,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
group_type = VGroup if isinstance(mobject, VMobject) else Group
|
||||
if isinstance(mobject, OpenGLVMobject):
|
||||
group_type: type[OpenGLVGroup | OpenGLGroup | VGroup | Group] = OpenGLVGroup
|
||||
elif isinstance(mobject, OpenGLMobject):
|
||||
group_type = OpenGLGroup
|
||||
elif isinstance(mobject, VMobject):
|
||||
group_type = VGroup
|
||||
else:
|
||||
group_type = Group
|
||||
|
||||
source_map = self.get_shape_map(mobject)
|
||||
target_map = self.get_shape_map(target_mobject)
|
||||
|
|
@ -138,32 +142,33 @@ 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:
|
||||
if config["renderer"] == RendererType.OPENGL:
|
||||
shape_map[key] = VGroup()
|
||||
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 finish(self) -> None:
|
||||
super().finish()
|
||||
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)
|
||||
self.buffer.remove(self.mobject)
|
||||
self.buffer.remove(*self.to_remove)
|
||||
self.buffer.add(self.to_add)
|
||||
# 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.")
|
||||
|
||||
|
||||
|
|
@ -203,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,
|
||||
|
|
@ -267,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,
|
||||
|
|
@ -280,7 +285,7 @@ class TransformMatchingTex(TransformMatchingAbstractBase):
|
|||
|
||||
@staticmethod
|
||||
def get_mobject_parts(mobject: Mobject) -> list[Mobject]:
|
||||
if isinstance(mobject, (Group, VGroup)):
|
||||
if isinstance(mobject, (Group, VGroup, OpenGLGroup, OpenGLVGroup)):
|
||||
return [
|
||||
p
|
||||
for s in mobject.submobjects
|
||||
|
|
@ -292,4 +297,5 @@ class TransformMatchingTex(TransformMatchingAbstractBase):
|
|||
|
||||
@staticmethod
|
||||
def get_mobject_key(mobject: Mobject) -> str:
|
||||
assert isinstance(mobject, MathTexPart)
|
||||
return mobject.tex_string
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
__all__ = [
|
||||
"assert_is_mobject_method",
|
||||
"always",
|
||||
"f_always",
|
||||
"always_redraw",
|
||||
"always_shift",
|
||||
"always_rotate",
|
||||
"turn_animation_into_updater",
|
||||
"cycle_animation",
|
||||
]
|
||||
|
|
@ -13,56 +16,44 @@ __all__ = [
|
|||
|
||||
import inspect
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.constants import DEGREES, RIGHT
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.opengl import OpenGLMobject
|
||||
from manim.utils.space_ops import normalize
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import types
|
||||
from typing import Concatenate
|
||||
|
||||
from typing_extensions import ParamSpec, TypeIs
|
||||
|
||||
from manim.animation.protocol import MobjectAnimation
|
||||
|
||||
P = ParamSpec("P")
|
||||
from manim.animation.animation import Animation
|
||||
|
||||
|
||||
M = TypeVar("M", bound=Mobject)
|
||||
|
||||
|
||||
# TODO: figure out how to typehint as MethodType[Mobject] to avoid the cast
|
||||
# madness in always/f_always
|
||||
def is_mobject_method(method: Callable[..., Any]) -> TypeIs[types.MethodType]:
|
||||
return inspect.ismethod(method) and isinstance(method.__self__, Mobject)
|
||||
def assert_is_mobject_method(method: Callable) -> None:
|
||||
assert inspect.ismethod(method)
|
||||
mobject = method.__self__
|
||||
assert isinstance(mobject, (Mobject, OpenGLMobject))
|
||||
|
||||
|
||||
def always(
|
||||
method: Callable[Concatenate[M, P], object], *args: P.args, **kwargs: P.kwargs
|
||||
) -> M:
|
||||
if not is_mobject_method(method):
|
||||
raise ValueError("always must take a method of a Mobject")
|
||||
mobject = cast(M, method.__self__)
|
||||
def always(method: Callable, *args, **kwargs) -> Mobject:
|
||||
assert_is_mobject_method(method)
|
||||
mobject = method.__self__
|
||||
func = method.__func__
|
||||
mobject.add_updater(lambda m: func(m, *args, **kwargs))
|
||||
return mobject
|
||||
|
||||
|
||||
def f_always(
|
||||
method: Callable[Concatenate[M, ...], None],
|
||||
*arg_generators: Callable[[], object],
|
||||
**kwargs,
|
||||
) -> M:
|
||||
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
|
||||
the relevant arguments.
|
||||
"""
|
||||
if not is_mobject_method(method):
|
||||
raise ValueError("f_always must take a method of a Mobject")
|
||||
mobject = cast(M, method.__self__)
|
||||
assert_is_mobject_method(method)
|
||||
mobject = method.__self__
|
||||
func = method.__func__
|
||||
|
||||
def updater(mob):
|
||||
|
|
@ -88,6 +79,7 @@ def always_redraw(func: Callable[[], M]) -> M:
|
|||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim:: TangentAnimation
|
||||
|
||||
class TangentAnimation(Scene):
|
||||
|
|
@ -117,11 +109,81 @@ def always_redraw(func: Callable[[], M]) -> M:
|
|||
return mob
|
||||
|
||||
|
||||
def turn_animation_into_updater(
|
||||
animation: MobjectAnimation[M],
|
||||
cycle: bool = False,
|
||||
delay: float = 0,
|
||||
def always_shift(
|
||||
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.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mobject
|
||||
The mobject to shift.
|
||||
direction
|
||||
The direction to shift. The vector is normalized, the specified magnitude
|
||||
is not relevant.
|
||||
rate
|
||||
Length in Manim units which the mobject travels in one
|
||||
second along the specified direction.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim:: ShiftingSquare
|
||||
|
||||
class ShiftingSquare(Scene):
|
||||
def construct(self):
|
||||
sq = Square().set_fill(opacity=1)
|
||||
tri = Triangle()
|
||||
VGroup(sq, tri).arrange(LEFT)
|
||||
|
||||
# construct a square which is continuously
|
||||
# shifted to the right
|
||||
always_shift(sq, RIGHT, rate=5)
|
||||
|
||||
self.add(sq)
|
||||
self.play(tri.animate.set_fill(opacity=1))
|
||||
"""
|
||||
mobject.add_updater(lambda m, dt: m.shift(dt * rate * normalize(direction)))
|
||||
return mobject
|
||||
|
||||
|
||||
def always_rotate(mobject: M, rate: float = 20 * DEGREES, **kwargs) -> M:
|
||||
"""A mobject which is continuously rotated at a certain rate.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mobject
|
||||
The mobject to be rotated.
|
||||
rate
|
||||
The angle which the mobject is rotated by
|
||||
over one second.
|
||||
kwags
|
||||
Further arguments to be passed to :meth:`.Mobject.rotate`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim:: SpinningTriangle
|
||||
|
||||
class SpinningTriangle(Scene):
|
||||
def construct(self):
|
||||
tri = Triangle().set_fill(opacity=1).set_z_index(2)
|
||||
sq = Square().to_edge(LEFT)
|
||||
|
||||
# will keep spinning while there is an animation going on
|
||||
always_rotate(tri, rate=2*PI, about_point=ORIGIN)
|
||||
|
||||
self.add(tri, sq)
|
||||
self.play(sq.animate.to_edge(RIGHT), rate_func=linear, run_time=1)
|
||||
"""
|
||||
mobject.add_updater(lambda m, dt: m.rotate(dt * rate, **kwargs))
|
||||
return mobject
|
||||
|
||||
|
||||
def turn_animation_into_updater(
|
||||
animation: Animation, cycle: bool = False, delay: float = 0, **kwargs
|
||||
) -> Mobject:
|
||||
"""
|
||||
Add an updater to the animation's mobject which applies
|
||||
the interpolation and update functions of the animation
|
||||
|
|
@ -150,12 +212,10 @@ def turn_animation_into_updater(
|
|||
mobject = animation.mobject
|
||||
animation.suspend_mobject_updating = False
|
||||
animation.begin()
|
||||
animation.total_time = -delay
|
||||
|
||||
total_time = -delay
|
||||
|
||||
def update(m: M, dt: float):
|
||||
nonlocal total_time
|
||||
if total_time >= 0:
|
||||
def update(m: Mobject, dt: float):
|
||||
if animation.total_time >= 0:
|
||||
run_time = animation.get_run_time()
|
||||
|
||||
# handle zero/negative runtime safely
|
||||
|
|
@ -167,7 +227,7 @@ def turn_animation_into_updater(
|
|||
m.remove_updater(update)
|
||||
return
|
||||
|
||||
time_ratio = total_time / run_time
|
||||
time_ratio = animation.total_time / run_time
|
||||
if cycle:
|
||||
alpha = time_ratio % 1
|
||||
else:
|
||||
|
|
@ -178,11 +238,11 @@ def turn_animation_into_updater(
|
|||
return
|
||||
animation.interpolate(alpha)
|
||||
animation.update_mobjects(dt)
|
||||
total_time += dt
|
||||
animation.total_time += dt
|
||||
|
||||
mobject.add_updater(update)
|
||||
return mobject
|
||||
|
||||
|
||||
def cycle_animation(animation: MobjectAnimation[M], **kwargs) -> M:
|
||||
def cycle_animation(animation: Animation, **kwargs) -> Mobject:
|
||||
return turn_animation_into_updater(animation, cycle=True, **kwargs)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from typing import TYPE_CHECKING, Any
|
|||
from manim.animation.animation import Animation
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.mobject.mobject import Mobject
|
||||
|
||||
|
||||
class UpdateFromFunc(Animation):
|
||||
|
|
@ -25,22 +25,22 @@ class UpdateFromFunc(Animation):
|
|||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
update_function: Callable[[Mobject], object],
|
||||
update_function: Callable[[Mobject], Any],
|
||||
suspend_mobject_updating: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.update_function = update_function
|
||||
super().__init__(
|
||||
mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs
|
||||
)
|
||||
self.update_function = update_function
|
||||
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
self.update_function(self.mobject)
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
self.update_function(self.mobject) # type: ignore[arg-type]
|
||||
|
||||
|
||||
class UpdateFromAlphaFunc(UpdateFromFunc):
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
self.update_function(self.mobject, self.rate_func(alpha)) # type: ignore[call-arg]
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
self.update_function(self.mobject, self.rate_func(alpha)) # type: ignore[call-arg, arg-type]
|
||||
|
||||
|
||||
class MaintainPositionRelativeTo(Animation):
|
||||
|
|
@ -54,7 +54,7 @@ class MaintainPositionRelativeTo(Animation):
|
|||
)
|
||||
super().__init__(mobject, **kwargs)
|
||||
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
def interpolate_mobject(self, alpha: float) -> None:
|
||||
target = self.tracked_mobject.get_center()
|
||||
location = self.mobject.get_center()
|
||||
self.mobject.shift(target - location + self.diff)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
170
manim/camera/mapping_camera.py
Normal file
170
manim/camera/mapping_camera.py
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
"""A camera module that supports spatial mapping between objects for distortion effects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["MappingCamera", "OldMultiCamera", "SplitScreenCamera"]
|
||||
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..camera.camera import Camera
|
||||
from ..mobject.types.vectorized_mobject import VMobject
|
||||
from ..utils.config_ops import DictAsObject
|
||||
|
||||
# TODO: Add an attribute to mobjects under which they can specify that they should just
|
||||
# map their centers but remain otherwise undistorted (useful for labels, etc.)
|
||||
|
||||
|
||||
class MappingCamera(Camera):
|
||||
"""Parameters
|
||||
----------
|
||||
mapping_func : callable
|
||||
Function to map 3D points to new 3D points (identity by default).
|
||||
min_num_curves : int
|
||||
Minimum number of curves for VMobjects to avoid visual glitches.
|
||||
allow_object_intrusion : bool
|
||||
If True, modifies original mobjects; else works on copies.
|
||||
kwargs : dict
|
||||
Additional arguments passed to Camera base class.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mapping_func=lambda p: p,
|
||||
min_num_curves=50,
|
||||
allow_object_intrusion=False,
|
||||
**kwargs,
|
||||
):
|
||||
self.mapping_func = mapping_func
|
||||
self.min_num_curves = min_num_curves
|
||||
self.allow_object_intrusion = allow_object_intrusion
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def points_to_pixel_coords(self, mobject, points):
|
||||
# Map points with custom function before converting to pixels
|
||||
return super().points_to_pixel_coords(
|
||||
mobject,
|
||||
np.apply_along_axis(self.mapping_func, 1, points),
|
||||
)
|
||||
|
||||
def capture_mobjects(self, mobjects, **kwargs):
|
||||
"""Capture mobjects for rendering after applying the spatial mapping.
|
||||
|
||||
Copies mobjects unless intrusion is allowed, and ensures
|
||||
vector objects have enough curves for smooth distortion.
|
||||
"""
|
||||
mobjects = self.get_mobjects_to_display(mobjects, **kwargs)
|
||||
if self.allow_object_intrusion:
|
||||
mobject_copies = mobjects
|
||||
else:
|
||||
mobject_copies = [mobject.copy() for mobject in mobjects]
|
||||
for mobject in mobject_copies:
|
||||
if (
|
||||
isinstance(mobject, VMobject)
|
||||
and 0 < mobject.get_num_curves() < self.min_num_curves
|
||||
):
|
||||
mobject.insert_n_curves(self.min_num_curves)
|
||||
super().capture_mobjects(
|
||||
mobject_copies,
|
||||
include_submobjects=False,
|
||||
excluded_mobjects=None,
|
||||
)
|
||||
|
||||
|
||||
# Note: This allows layering of multiple cameras onto the same portion of the pixel array,
|
||||
# the later cameras overwriting the former
|
||||
#
|
||||
# TODO: Add optional separator borders between cameras (or perhaps peel this off into a
|
||||
# CameraPlusOverlay class)
|
||||
|
||||
|
||||
# TODO, the classes below should likely be deleted
|
||||
class OldMultiCamera(Camera):
|
||||
"""Parameters
|
||||
----------
|
||||
cameras_with_start_positions : tuple
|
||||
Tuples of (Camera, (start_y, start_x)) indicating camera and
|
||||
its pixel offset on the final frame.
|
||||
"""
|
||||
|
||||
def __init__(self, *cameras_with_start_positions, **kwargs):
|
||||
self.shifted_cameras = [
|
||||
DictAsObject(
|
||||
{
|
||||
"camera": camera_with_start_positions[0],
|
||||
"start_x": camera_with_start_positions[1][1],
|
||||
"start_y": camera_with_start_positions[1][0],
|
||||
"end_x": camera_with_start_positions[1][1]
|
||||
+ camera_with_start_positions[0].pixel_width,
|
||||
"end_y": camera_with_start_positions[1][0]
|
||||
+ camera_with_start_positions[0].pixel_height,
|
||||
},
|
||||
)
|
||||
for camera_with_start_positions in cameras_with_start_positions
|
||||
]
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def capture_mobjects(self, mobjects, **kwargs):
|
||||
for shifted_camera in self.shifted_cameras:
|
||||
shifted_camera.camera.capture_mobjects(mobjects, **kwargs)
|
||||
|
||||
self.pixel_array[
|
||||
shifted_camera.start_y : shifted_camera.end_y,
|
||||
shifted_camera.start_x : shifted_camera.end_x,
|
||||
] = shifted_camera.camera.pixel_array
|
||||
|
||||
def set_background(self, pixel_array, **kwargs):
|
||||
for shifted_camera in self.shifted_cameras:
|
||||
shifted_camera.camera.set_background(
|
||||
pixel_array[
|
||||
shifted_camera.start_y : shifted_camera.end_y,
|
||||
shifted_camera.start_x : shifted_camera.end_x,
|
||||
],
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def set_pixel_array(self, pixel_array, **kwargs):
|
||||
super().set_pixel_array(pixel_array, **kwargs)
|
||||
for shifted_camera in self.shifted_cameras:
|
||||
shifted_camera.camera.set_pixel_array(
|
||||
pixel_array[
|
||||
shifted_camera.start_y : shifted_camera.end_y,
|
||||
shifted_camera.start_x : shifted_camera.end_x,
|
||||
],
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def init_background(self):
|
||||
super().init_background()
|
||||
for shifted_camera in self.shifted_cameras:
|
||||
shifted_camera.camera.init_background()
|
||||
|
||||
|
||||
# A OldMultiCamera which, when called with two full-size cameras, initializes itself
|
||||
# as a split screen, also taking care to resize each individual camera within it
|
||||
|
||||
|
||||
class SplitScreenCamera(OldMultiCamera):
|
||||
"""Initializes a split screen camera setup with two side-by-side cameras.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
left_camera : Camera
|
||||
right_camera : Camera
|
||||
kwargs : dict
|
||||
"""
|
||||
|
||||
def __init__(self, left_camera, right_camera, **kwargs):
|
||||
Camera.__init__(self, **kwargs) # to set attributes such as pixel_width
|
||||
self.left_camera = left_camera
|
||||
self.right_camera = right_camera
|
||||
|
||||
half_width = math.ceil(self.pixel_width / 2)
|
||||
for camera in [self.left_camera, self.right_camera]:
|
||||
camera.reset_pixel_shape(camera.pixel_height, half_width)
|
||||
|
||||
super().__init__(
|
||||
(left_camera, (0, 0)),
|
||||
(right_camera, (0, half_width)),
|
||||
)
|
||||
292
manim/camera/moving_camera.py
Normal file
292
manim/camera/moving_camera.py
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
"""Defines the MovingCamera class, a camera that can pan and zoom through a scene.
|
||||
|
||||
.. SEEALSO::
|
||||
|
||||
:mod:`.moving_camera_scene`
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["MovingCamera"]
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import Any, Literal, overload
|
||||
|
||||
from cairo import Context
|
||||
|
||||
from manim.typing import PixelArray, Point3D, Point3DLike
|
||||
|
||||
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, _AnimationBuilder
|
||||
from ..utils.color import WHITE, ManimColor
|
||||
|
||||
|
||||
class MovingCamera(Camera):
|
||||
"""A camera that follows and matches the size and position of its 'frame', a Rectangle (or similar Mobject).
|
||||
|
||||
The frame defines the region of space the camera displays and can move or resize dynamically.
|
||||
|
||||
.. SEEALSO::
|
||||
|
||||
:class:`.MovingCameraScene`
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
frame: Mobject | None = None,
|
||||
fixed_dimension: int = 0, # width
|
||||
default_frame_stroke_color: ManimColor = WHITE,
|
||||
default_frame_stroke_width: int = 0,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Frame is a Mobject, (should almost certainly be a rectangle)
|
||||
determining which region of space the camera displays
|
||||
"""
|
||||
self.fixed_dimension = fixed_dimension
|
||||
self.default_frame_stroke_color = default_frame_stroke_color
|
||||
self.default_frame_stroke_width = default_frame_stroke_width
|
||||
if frame is None:
|
||||
frame = ScreenRectangle(height=config["frame_height"])
|
||||
frame.set_stroke(
|
||||
self.default_frame_stroke_color,
|
||||
self.default_frame_stroke_width,
|
||||
)
|
||||
self.frame = frame
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# TODO, make these work for a rotated frame
|
||||
@property
|
||||
def frame_height(self) -> float:
|
||||
"""Returns the height of the frame.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The height of the frame.
|
||||
"""
|
||||
return self.frame.height
|
||||
|
||||
@frame_height.setter
|
||||
def frame_height(self, frame_height: float) -> None:
|
||||
"""Sets the height of the frame in MUnits.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
frame_height
|
||||
The new frame_height.
|
||||
"""
|
||||
self.frame.stretch_to_fit_height(frame_height)
|
||||
|
||||
@property
|
||||
def frame_width(self) -> float:
|
||||
"""Returns the width of the frame
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The width of the frame.
|
||||
"""
|
||||
return self.frame.width
|
||||
|
||||
@frame_width.setter
|
||||
def frame_width(self, frame_width: float) -> None:
|
||||
"""Sets the width of the frame in MUnits.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
frame_width
|
||||
The new frame_width.
|
||||
"""
|
||||
self.frame.stretch_to_fit_width(frame_width)
|
||||
|
||||
@property
|
||||
def frame_center(self) -> Point3D:
|
||||
"""Returns the centerpoint of the frame in cartesian coordinates.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.array
|
||||
The cartesian coordinates of the center of the frame.
|
||||
"""
|
||||
return self.frame.get_center()
|
||||
|
||||
@frame_center.setter
|
||||
def frame_center(self, frame_center: Point3DLike | Mobject) -> None:
|
||||
"""Sets the centerpoint of the frame.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
frame_center
|
||||
The point to which the frame must be moved.
|
||||
If is of type mobject, the frame will be moved to
|
||||
the center of that mobject.
|
||||
"""
|
||||
self.frame.move_to(frame_center)
|
||||
|
||||
def capture_mobjects(self, mobjects: Iterable[Mobject], **kwargs: Any) -> None:
|
||||
# self.reset_frame_center()
|
||||
# self.realign_frame_shape()
|
||||
super().capture_mobjects(mobjects, **kwargs)
|
||||
|
||||
def get_cached_cairo_context(self, pixel_array: PixelArray) -> None:
|
||||
"""Since the frame can be moving around, the cairo
|
||||
context used for updating should be regenerated
|
||||
at each frame. So no caching.
|
||||
"""
|
||||
return None
|
||||
|
||||
def cache_cairo_context(self, pixel_array: PixelArray, ctx: Context) -> None:
|
||||
"""Since the frame can be moving around, the cairo
|
||||
context used for updating should be regenerated
|
||||
at each frame. So no caching.
|
||||
"""
|
||||
pass
|
||||
|
||||
# def reset_frame_center(self):
|
||||
# self.frame_center = self.frame.get_center()
|
||||
|
||||
# def realign_frame_shape(self):
|
||||
# height, width = self.frame_shape
|
||||
# if self.fixed_dimension == 0:
|
||||
# self.frame_shape = (height, self.frame.width
|
||||
# else:
|
||||
# self.frame_shape = (self.frame.height, width)
|
||||
# self.resize_frame_shape(fixed_dimension=self.fixed_dimension)
|
||||
|
||||
def get_mobjects_indicating_movement(self) -> list[Mobject]:
|
||||
"""Returns all mobjects whose movement implies that the camera
|
||||
should think of all other mobjects on the screen as moving
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[Mobject]
|
||||
"""
|
||||
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,
|
||||
) -> _AnimationBuilder | Mobject:
|
||||
"""Zooms on to a given array of mobjects (or a singular mobject)
|
||||
and automatically resizes to frame all the mobjects.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
This method only works when 2D-objects in the XY-plane are considered, it
|
||||
will not work correctly when the camera has been rotated.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mobjects
|
||||
The mobject or array of mobjects that the camera will focus on.
|
||||
|
||||
margin
|
||||
The width of the margin that is added to the frame (optional, 0 by default).
|
||||
|
||||
only_mobjects_in_frame
|
||||
If set to ``True``, only allows focusing on mobjects that are already in frame.
|
||||
|
||||
animate
|
||||
If set to ``False``, applies the changes instead of returning the corresponding animation
|
||||
|
||||
Returns
|
||||
-------
|
||||
Union[_AnimationBuilder, ScreenRectangle]
|
||||
_AnimationBuilder that zooms the camera view to a given list of mobjects
|
||||
or ScreenRectangle with position and size updated to zoomed position.
|
||||
|
||||
"""
|
||||
(
|
||||
scene_critical_x_left,
|
||||
scene_critical_x_right,
|
||||
scene_critical_y_up,
|
||||
scene_critical_y_down,
|
||||
) = self._get_bounding_box(mobjects, only_mobjects_in_frame)
|
||||
|
||||
# calculate center x and y
|
||||
x = (scene_critical_x_left + scene_critical_x_right) / 2
|
||||
y = (scene_critical_y_up + scene_critical_y_down) / 2
|
||||
|
||||
# calculate proposed width and height of zoomed scene
|
||||
new_width = abs(scene_critical_x_left - scene_critical_x_right)
|
||||
new_height = abs(scene_critical_y_up - scene_critical_y_down)
|
||||
|
||||
m_target = self.frame.animate if animate else self.frame
|
||||
# zoom to fit all mobjects along the side that has the largest size
|
||||
if new_width / self.frame.width > new_height / self.frame.height:
|
||||
return m_target.set_x(x).set_y(y).set(width=new_width + margin)
|
||||
else:
|
||||
return m_target.set_x(x).set_y(y).set(height=new_height + margin)
|
||||
|
||||
def _get_bounding_box(
|
||||
self, mobjects: Iterable[Mobject], only_mobjects_in_frame: bool
|
||||
) -> tuple[float, float, float, float]:
|
||||
bounding_box_located = False
|
||||
scene_critical_x_left: float = 0
|
||||
scene_critical_x_right: float = 1
|
||||
scene_critical_y_up: float = 1
|
||||
scene_critical_y_down: float = 0
|
||||
|
||||
for m in mobjects:
|
||||
if (m == self.frame) or (
|
||||
only_mobjects_in_frame and not self.is_in_frame(m)
|
||||
):
|
||||
# detected camera frame, should not be used to calculate final position of camera
|
||||
continue
|
||||
|
||||
# initialize scene critical points with first mobjects critical points
|
||||
if not bounding_box_located:
|
||||
scene_critical_x_left = m.get_critical_point(LEFT)[0]
|
||||
scene_critical_x_right = m.get_critical_point(RIGHT)[0]
|
||||
scene_critical_y_up = m.get_critical_point(UP)[1]
|
||||
scene_critical_y_down = m.get_critical_point(DOWN)[1]
|
||||
bounding_box_located = True
|
||||
|
||||
else:
|
||||
if m.get_critical_point(LEFT)[0] < scene_critical_x_left:
|
||||
scene_critical_x_left = m.get_critical_point(LEFT)[0]
|
||||
|
||||
if m.get_critical_point(RIGHT)[0] > scene_critical_x_right:
|
||||
scene_critical_x_right = m.get_critical_point(RIGHT)[0]
|
||||
|
||||
if m.get_critical_point(UP)[1] > scene_critical_y_up:
|
||||
scene_critical_y_up = m.get_critical_point(UP)[1]
|
||||
|
||||
if m.get_critical_point(DOWN)[1] < scene_critical_y_down:
|
||||
scene_critical_y_down = m.get_critical_point(DOWN)[1]
|
||||
|
||||
if not bounding_box_located:
|
||||
raise Exception(
|
||||
"Could not determine bounding box of the mobjects given to 'auto_zoom'."
|
||||
)
|
||||
|
||||
return (
|
||||
scene_critical_x_left,
|
||||
scene_critical_x_right,
|
||||
scene_critical_y_up,
|
||||
scene_critical_y_down,
|
||||
)
|
||||
107
manim/camera/multi_camera.py
Normal file
107
manim/camera/multi_camera.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
"""A camera supporting multiple perspectives."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["MultiCamera"]
|
||||
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import Any, Self
|
||||
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.types.image_mobject import ImageMobjectFromCamera
|
||||
|
||||
from ..camera.moving_camera import MovingCamera
|
||||
from ..utils.iterables import list_difference_update
|
||||
|
||||
|
||||
class MultiCamera(MovingCamera):
|
||||
"""Camera Object that allows for multiple perspectives."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
image_mobjects_from_cameras: Iterable[ImageMobjectFromCamera] | None = None,
|
||||
allow_cameras_to_capture_their_own_display: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialises the MultiCamera
|
||||
|
||||
Parameters
|
||||
----------
|
||||
image_mobjects_from_cameras
|
||||
|
||||
kwargs
|
||||
Any valid keyword arguments of MovingCamera.
|
||||
"""
|
||||
self.image_mobjects_from_cameras: list[ImageMobjectFromCamera] = []
|
||||
if image_mobjects_from_cameras is not None:
|
||||
for imfc in image_mobjects_from_cameras:
|
||||
self.add_image_mobject_from_camera(imfc)
|
||||
self.allow_cameras_to_capture_their_own_display = (
|
||||
allow_cameras_to_capture_their_own_display
|
||||
)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def add_image_mobject_from_camera(
|
||||
self, image_mobject_from_camera: ImageMobjectFromCamera
|
||||
) -> None:
|
||||
"""Adds an ImageMobject that's been obtained from the camera
|
||||
into the list ``self.image_mobject_from_cameras``
|
||||
|
||||
Parameters
|
||||
----------
|
||||
image_mobject_from_camera
|
||||
The ImageMobject to add to self.image_mobject_from_cameras
|
||||
"""
|
||||
# A silly method to have right now, but maybe there are things
|
||||
# we want to guarantee about any imfc's added later.
|
||||
imfc = image_mobject_from_camera
|
||||
assert isinstance(imfc.camera, MovingCamera)
|
||||
self.image_mobjects_from_cameras.append(imfc)
|
||||
|
||||
def update_sub_cameras(self) -> None:
|
||||
"""Reshape sub_camera pixel_arrays"""
|
||||
for imfc in self.image_mobjects_from_cameras:
|
||||
pixel_height, pixel_width = self.pixel_array.shape[:2]
|
||||
# imfc.camera.frame_shape = (
|
||||
# imfc.camera.frame.height,
|
||||
# imfc.camera.frame.width,
|
||||
# )
|
||||
imfc.camera.reset_pixel_shape(
|
||||
int(pixel_height * imfc.height / self.frame_height),
|
||||
int(pixel_width * imfc.width / self.frame_width),
|
||||
)
|
||||
|
||||
def reset(self) -> Self:
|
||||
"""Resets the MultiCamera.
|
||||
|
||||
Returns
|
||||
-------
|
||||
MultiCamera
|
||||
The reset MultiCamera
|
||||
"""
|
||||
for imfc in self.image_mobjects_from_cameras:
|
||||
imfc.camera.reset()
|
||||
super().reset()
|
||||
return self
|
||||
|
||||
def capture_mobjects(self, mobjects: Iterable[Mobject], **kwargs: Any) -> None:
|
||||
self.update_sub_cameras()
|
||||
for imfc in self.image_mobjects_from_cameras:
|
||||
to_add = list(mobjects)
|
||||
if not self.allow_cameras_to_capture_their_own_display:
|
||||
to_add = list_difference_update(to_add, imfc.get_family())
|
||||
imfc.camera.capture_mobjects(to_add, **kwargs)
|
||||
super().capture_mobjects(mobjects, **kwargs)
|
||||
|
||||
def get_mobjects_indicating_movement(self) -> list[Mobject]:
|
||||
"""Returns all mobjects whose movement implies that the camera
|
||||
should think of all other mobjects on the screen as moving
|
||||
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
"""
|
||||
return [self.frame] + [
|
||||
imfc.camera.frame for imfc in self.image_mobjects_from_cameras
|
||||
]
|
||||
459
manim/camera/three_d_camera.py
Normal file
459
manim/camera/three_d_camera.py
Normal file
|
|
@ -0,0 +1,459 @@
|
|||
"""A camera that can be positioned and oriented in three-dimensional space."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["ThreeDCamera"]
|
||||
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.three_d.three_d_utils import (
|
||||
get_3d_vmob_end_corner,
|
||||
get_3d_vmob_end_corner_unit_normal,
|
||||
get_3d_vmob_start_corner,
|
||||
get_3d_vmob_start_corner_unit_normal,
|
||||
)
|
||||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
from manim.mobject.value_tracker import ValueTracker
|
||||
from manim.typing import (
|
||||
FloatRGBA_Array,
|
||||
MatrixMN,
|
||||
Point3D,
|
||||
Point3D_Array,
|
||||
Point3DLike,
|
||||
)
|
||||
|
||||
from .. import config
|
||||
from ..camera.camera import Camera
|
||||
from ..constants import *
|
||||
from ..mobject.types.point_cloud_mobject import Point
|
||||
from ..utils.color import get_shaded_rgb
|
||||
from ..utils.family import extract_mobject_family_members
|
||||
from ..utils.space_ops import rotation_about_z, rotation_matrix
|
||||
|
||||
|
||||
class ThreeDCamera(Camera):
|
||||
def __init__(
|
||||
self,
|
||||
focal_distance: float = 20.0,
|
||||
shading_factor: float = 0.2,
|
||||
default_distance: float = 5.0,
|
||||
light_source_start_point: Point3DLike = 9 * DOWN + 7 * LEFT + 10 * OUT,
|
||||
should_apply_shading: bool = True,
|
||||
exponential_projection: bool = False,
|
||||
phi: float = 0,
|
||||
theta: float = -90 * DEGREES,
|
||||
gamma: float = 0,
|
||||
zoom: float = 1,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Initializes the ThreeDCamera
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*kwargs
|
||||
Any keyword argument of Camera.
|
||||
"""
|
||||
self._frame_center = Point(kwargs.get("frame_center", ORIGIN), stroke_width=0)
|
||||
super().__init__(**kwargs)
|
||||
self.focal_distance = focal_distance
|
||||
self.phi = phi
|
||||
self.theta = theta
|
||||
self.gamma = gamma
|
||||
self.zoom = zoom
|
||||
self.shading_factor = shading_factor
|
||||
self.default_distance = default_distance
|
||||
self.light_source_start_point = light_source_start_point
|
||||
self.light_source = Point(self.light_source_start_point)
|
||||
self.should_apply_shading = should_apply_shading
|
||||
self.exponential_projection = exponential_projection
|
||||
self.max_allowable_norm = 3 * config["frame_width"]
|
||||
self.phi_tracker = ValueTracker(self.phi)
|
||||
self.theta_tracker = ValueTracker(self.theta)
|
||||
self.focal_distance_tracker = ValueTracker(self.focal_distance)
|
||||
self.gamma_tracker = ValueTracker(self.gamma)
|
||||
self.zoom_tracker = ValueTracker(self.zoom)
|
||||
self.fixed_orientation_mobjects: dict[Mobject, Callable[[], Point3D]] = {}
|
||||
self.fixed_in_frame_mobjects: set[Mobject] = set()
|
||||
self.reset_rotation_matrix()
|
||||
|
||||
@property
|
||||
def frame_center(self) -> Point3D:
|
||||
return self._frame_center.points[0]
|
||||
|
||||
@frame_center.setter
|
||||
def frame_center(self, point: Point3DLike) -> None:
|
||||
self._frame_center.move_to(point)
|
||||
|
||||
def capture_mobjects(self, mobjects: Iterable[Mobject], **kwargs: Any) -> None:
|
||||
self.reset_rotation_matrix()
|
||||
super().capture_mobjects(mobjects, **kwargs)
|
||||
|
||||
def get_value_trackers(self) -> list[ValueTracker]:
|
||||
"""A list of :class:`ValueTrackers <.ValueTracker>` of phi, theta, focal_distance,
|
||||
gamma and zoom.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
list of ValueTracker objects
|
||||
"""
|
||||
return [
|
||||
self.phi_tracker,
|
||||
self.theta_tracker,
|
||||
self.focal_distance_tracker,
|
||||
self.gamma_tracker,
|
||||
self.zoom_tracker,
|
||||
]
|
||||
|
||||
def modified_rgbas(
|
||||
self, vmobject: VMobject, rgbas: FloatRGBA_Array
|
||||
) -> FloatRGBA_Array:
|
||||
if not self.should_apply_shading:
|
||||
return rgbas
|
||||
if vmobject.shade_in_3d and (vmobject.get_num_points() > 0):
|
||||
light_source_point = self.light_source.points[0]
|
||||
if len(rgbas) < 2:
|
||||
shaded_rgbas = rgbas.repeat(2, axis=0)
|
||||
else:
|
||||
shaded_rgbas = np.array(rgbas[:2])
|
||||
shaded_rgbas[0, :3] = get_shaded_rgb(
|
||||
shaded_rgbas[0, :3],
|
||||
get_3d_vmob_start_corner(vmobject),
|
||||
get_3d_vmob_start_corner_unit_normal(vmobject),
|
||||
light_source_point,
|
||||
)
|
||||
shaded_rgbas[1, :3] = get_shaded_rgb(
|
||||
shaded_rgbas[1, :3],
|
||||
get_3d_vmob_end_corner(vmobject),
|
||||
get_3d_vmob_end_corner_unit_normal(vmobject),
|
||||
light_source_point,
|
||||
)
|
||||
return shaded_rgbas
|
||||
return rgbas
|
||||
|
||||
def get_stroke_rgbas(
|
||||
self,
|
||||
vmobject: VMobject,
|
||||
background: bool = False,
|
||||
) -> FloatRGBA_Array: # NOTE : DocStrings From parent
|
||||
return self.modified_rgbas(vmobject, vmobject.get_stroke_rgbas(background))
|
||||
|
||||
def get_fill_rgbas(
|
||||
self, vmobject: VMobject
|
||||
) -> FloatRGBA_Array: # NOTE : DocStrings From parent
|
||||
return self.modified_rgbas(vmobject, vmobject.get_fill_rgbas())
|
||||
|
||||
def get_mobjects_to_display(
|
||||
self, *args: Any, **kwargs: Any
|
||||
) -> list[Mobject]: # NOTE : DocStrings From parent
|
||||
mobjects = super().get_mobjects_to_display(*args, **kwargs)
|
||||
rot_matrix = self.get_rotation_matrix()
|
||||
|
||||
def z_key(mob: Mobject) -> float:
|
||||
if not (hasattr(mob, "shade_in_3d") and mob.shade_in_3d):
|
||||
return np.inf # type: ignore[no-any-return]
|
||||
# Assign a number to a three dimensional mobjects
|
||||
# based on how close it is to the camera
|
||||
distance: float = np.dot(mob.get_z_index_reference_point(), rot_matrix.T)[2]
|
||||
return distance
|
||||
|
||||
return sorted(mobjects, key=z_key)
|
||||
|
||||
def get_phi(self) -> float:
|
||||
"""Returns the Polar angle (the angle off Z_AXIS) phi.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The Polar angle in radians.
|
||||
"""
|
||||
return self.phi_tracker.get_value()
|
||||
|
||||
def get_theta(self) -> float:
|
||||
"""Returns the Azimuthal i.e the angle that spins the camera around the Z_AXIS.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The Azimuthal angle in radians.
|
||||
"""
|
||||
return self.theta_tracker.get_value()
|
||||
|
||||
def get_focal_distance(self) -> float:
|
||||
"""Returns focal_distance of the Camera.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The focal_distance of the Camera in MUnits.
|
||||
"""
|
||||
return self.focal_distance_tracker.get_value()
|
||||
|
||||
def get_gamma(self) -> float:
|
||||
"""Returns the rotation of the camera about the vector from the ORIGIN to the Camera.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The angle of rotation of the camera about the vector
|
||||
from the ORIGIN to the Camera in radians
|
||||
"""
|
||||
return self.gamma_tracker.get_value()
|
||||
|
||||
def get_zoom(self) -> float:
|
||||
"""Returns the zoom amount of the camera.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The zoom amount of the camera.
|
||||
"""
|
||||
return self.zoom_tracker.get_value()
|
||||
|
||||
def set_phi(self, value: float) -> None:
|
||||
"""Sets the polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value
|
||||
The new value of the polar angle in radians.
|
||||
"""
|
||||
self.phi_tracker.set_value(value)
|
||||
|
||||
def set_theta(self, value: float) -> None:
|
||||
"""Sets the azimuthal angle i.e the angle that spins the camera around Z_AXIS in radians.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value
|
||||
The new value of the azimuthal angle in radians.
|
||||
"""
|
||||
self.theta_tracker.set_value(value)
|
||||
|
||||
def set_focal_distance(self, value: float) -> None:
|
||||
"""Sets the focal_distance of the Camera.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value
|
||||
The focal_distance of the Camera.
|
||||
"""
|
||||
self.focal_distance_tracker.set_value(value)
|
||||
|
||||
def set_gamma(self, value: float) -> None:
|
||||
"""Sets the angle of rotation of the camera about the vector from the ORIGIN to the Camera.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value
|
||||
The new angle of rotation of the camera.
|
||||
"""
|
||||
self.gamma_tracker.set_value(value)
|
||||
|
||||
def set_zoom(self, value: float) -> None:
|
||||
"""Sets the zoom amount of the camera.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value
|
||||
The zoom amount of the camera.
|
||||
"""
|
||||
self.zoom_tracker.set_value(value)
|
||||
|
||||
def reset_rotation_matrix(self) -> None:
|
||||
"""Sets the value of self.rotation_matrix to
|
||||
the matrix corresponding to the current position of the camera
|
||||
"""
|
||||
self.rotation_matrix = self.generate_rotation_matrix()
|
||||
|
||||
def get_rotation_matrix(self) -> MatrixMN:
|
||||
"""Returns the matrix corresponding to the current position of the camera.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.array
|
||||
The matrix corresponding to the current position of the camera.
|
||||
"""
|
||||
return self.rotation_matrix
|
||||
|
||||
def generate_rotation_matrix(self) -> MatrixMN:
|
||||
"""Generates a rotation matrix based off the current position of the camera.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.array
|
||||
The matrix corresponding to the current position of the camera.
|
||||
"""
|
||||
phi = self.get_phi()
|
||||
theta = self.get_theta()
|
||||
gamma = self.get_gamma()
|
||||
matrices = [
|
||||
rotation_about_z(-theta - 90 * DEGREES),
|
||||
rotation_matrix(-phi, RIGHT),
|
||||
rotation_about_z(gamma),
|
||||
]
|
||||
result = np.identity(3)
|
||||
for matrix in matrices:
|
||||
result = np.dot(matrix, result)
|
||||
return result
|
||||
|
||||
def project_points(self, points: Point3D_Array) -> Point3D_Array:
|
||||
"""Applies the current rotation_matrix as a projection
|
||||
matrix to the passed array of points.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
points
|
||||
The list of points to project.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.array
|
||||
The points after projecting.
|
||||
"""
|
||||
frame_center = self.frame_center
|
||||
focal_distance = self.get_focal_distance()
|
||||
zoom = self.get_zoom()
|
||||
rot_matrix = self.get_rotation_matrix()
|
||||
|
||||
points = points - frame_center
|
||||
points = np.dot(points, rot_matrix.T)
|
||||
zs = points[:, 2]
|
||||
for i in 0, 1:
|
||||
if self.exponential_projection:
|
||||
# Proper projection would involve multiplying
|
||||
# x and y by d / (d-z). But for points with high
|
||||
# z value that causes weird artifacts, and applying
|
||||
# the exponential helps smooth it out.
|
||||
factor = np.exp(zs / focal_distance)
|
||||
lt0 = zs < 0
|
||||
factor[lt0] = focal_distance / (focal_distance - zs[lt0])
|
||||
else:
|
||||
factor = focal_distance / (focal_distance - zs)
|
||||
factor[(focal_distance - zs) < 0] = 10**6
|
||||
points[:, i] *= factor * zoom
|
||||
return points
|
||||
|
||||
def project_point(self, point: Point3D) -> Point3D:
|
||||
"""Applies the current rotation_matrix as a projection
|
||||
matrix to the passed point.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
point
|
||||
The point to project.
|
||||
|
||||
Returns
|
||||
-------
|
||||
np.array
|
||||
The point after projection.
|
||||
"""
|
||||
return self.project_points(point.reshape((1, 3)))[0, :]
|
||||
|
||||
def transform_points_pre_display(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
points: Point3D_Array,
|
||||
) -> Point3D_Array: # TODO: Write Docstrings for this Method.
|
||||
points = super().transform_points_pre_display(mobject, points)
|
||||
fixed_orientation = mobject in self.fixed_orientation_mobjects
|
||||
fixed_in_frame = mobject in self.fixed_in_frame_mobjects
|
||||
|
||||
if fixed_in_frame:
|
||||
return points
|
||||
if fixed_orientation:
|
||||
center_func = self.fixed_orientation_mobjects[mobject]
|
||||
center = center_func()
|
||||
new_center = self.project_point(center)
|
||||
return points + (new_center - center)
|
||||
else:
|
||||
return self.project_points(points)
|
||||
|
||||
def add_fixed_orientation_mobjects(
|
||||
self,
|
||||
*mobjects: Mobject,
|
||||
use_static_center_func: bool = False,
|
||||
center_func: Callable[[], Point3D] | None = None,
|
||||
) -> None:
|
||||
"""This method allows the mobject to have a fixed orientation,
|
||||
even when the camera moves around.
|
||||
E.G If it was passed through this method, facing the camera, it
|
||||
will continue to face the camera even as the camera moves.
|
||||
Highly useful when adding labels to graphs and the like.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*mobjects
|
||||
The mobject whose orientation must be fixed.
|
||||
use_static_center_func
|
||||
Whether or not to use the function that takes the mobject's
|
||||
center as centerpoint, by default False
|
||||
center_func
|
||||
The function which returns the centerpoint
|
||||
with respect to which the mobject will be oriented, by default None
|
||||
"""
|
||||
|
||||
# This prevents the computation of mobject.get_center
|
||||
# every single time a projection happens
|
||||
def get_static_center_func(mobject: Mobject) -> Callable[[], Point3D]:
|
||||
point = mobject.get_center()
|
||||
return lambda: point
|
||||
|
||||
for mobject in mobjects:
|
||||
if center_func:
|
||||
func = center_func
|
||||
elif use_static_center_func:
|
||||
func = get_static_center_func(mobject)
|
||||
else:
|
||||
func = mobject.get_center
|
||||
for submob in mobject.get_family():
|
||||
self.fixed_orientation_mobjects[submob] = func
|
||||
|
||||
def add_fixed_in_frame_mobjects(self, *mobjects: Mobject) -> None:
|
||||
"""This method allows the mobject to have a fixed position,
|
||||
even when the camera moves around.
|
||||
E.G If it was passed through this method, at the top of the frame, it
|
||||
will continue to be displayed at the top of the frame.
|
||||
|
||||
Highly useful when displaying Titles or formulae or the like.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
**mobjects
|
||||
The mobject to fix in frame.
|
||||
"""
|
||||
for mobject in extract_mobject_family_members(mobjects):
|
||||
self.fixed_in_frame_mobjects.add(mobject)
|
||||
|
||||
def remove_fixed_orientation_mobjects(self, *mobjects: Mobject) -> None:
|
||||
"""If a mobject was fixed in its orientation by passing it through
|
||||
:meth:`.add_fixed_orientation_mobjects`, then this undoes that fixing.
|
||||
The Mobject will no longer have a fixed orientation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mobjects
|
||||
The mobjects whose orientation need not be fixed any longer.
|
||||
"""
|
||||
for mobject in extract_mobject_family_members(mobjects):
|
||||
if mobject in self.fixed_orientation_mobjects:
|
||||
del self.fixed_orientation_mobjects[mobject]
|
||||
|
||||
def remove_fixed_in_frame_mobjects(self, *mobjects: Mobject) -> None:
|
||||
"""If a mobject was fixed in frame by passing it through
|
||||
:meth:`.add_fixed_in_frame_mobjects`, then this undoes that fixing.
|
||||
The Mobject will no longer be fixed in frame.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mobjects
|
||||
The mobjects which need not be fixed in frame any longer.
|
||||
"""
|
||||
for mobject in extract_mobject_family_members(mobjects):
|
||||
if mobject in self.fixed_in_frame_mobjects:
|
||||
self.fixed_in_frame_mobjects.remove(mobject)
|
||||
|
|
@ -84,9 +84,7 @@ def checkhealth() -> None:
|
|||
self.execution_time = timeit.timeit(self._inner_construct, number=1)
|
||||
|
||||
with mn.tempconfig({"preview": True, "disable_caching": True}):
|
||||
with mn.Manager(CheckHealthDemo) as manager:
|
||||
manager.render()
|
||||
scene = CheckHealthDemo()
|
||||
scene.render()
|
||||
|
||||
click.echo(
|
||||
f"Scene rendered in {manager.scene.execution_time:.2f} seconds."
|
||||
)
|
||||
click.echo(f"Scene rendered in {scene.execution_time:.2f} seconds.")
|
||||
|
|
|
|||
|
|
@ -31,8 +31,7 @@ from manim.cli.render.ease_of_access_options import ease_of_access_options
|
|||
from manim.cli.render.global_options import global_options
|
||||
from manim.cli.render.output_options import output_options
|
||||
from manim.cli.render.render_options import render_options
|
||||
from manim.constants import EPILOG
|
||||
from manim.manager import Manager
|
||||
from manim.constants import EPILOG, RendererType
|
||||
from manim.utils.module_ops import scene_classes_from_file
|
||||
|
||||
__all__ = ["render"]
|
||||
|
|
@ -76,6 +75,14 @@ def render(**kwargs: Any) -> ClickArgs | dict[str, Any]:
|
|||
|
||||
SCENES is an optional list of scenes in the file.
|
||||
"""
|
||||
if kwargs["save_as_gif"]:
|
||||
logger.warning("--save_as_gif is deprecated, please use --format=gif instead!")
|
||||
kwargs["format"] = "gif"
|
||||
|
||||
if kwargs["save_pngs"]:
|
||||
logger.warning("--save_pngs is deprecated, please use --format=png instead!")
|
||||
kwargs["format"] = "png"
|
||||
|
||||
if kwargs["show_in_file_browser"]:
|
||||
logger.warning(
|
||||
"The short form of show_in_file_browser is deprecated and will be moved to support --format.",
|
||||
|
|
@ -87,13 +94,38 @@ def render(**kwargs: Any) -> ClickArgs | dict[str, Any]:
|
|||
|
||||
config.digest_args(click_args)
|
||||
file = Path(config.input_file)
|
||||
try:
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
from manim.renderer.opengl_renderer import OpenGLRenderer
|
||||
|
||||
try:
|
||||
renderer = OpenGLRenderer()
|
||||
keep_running = True
|
||||
while keep_running:
|
||||
for SceneClass in scene_classes_from_file(file):
|
||||
with tempconfig({}):
|
||||
scene = SceneClass(renderer)
|
||||
rerun = scene.render()
|
||||
if rerun or config["write_all"]:
|
||||
renderer.num_plays = 0
|
||||
continue
|
||||
else:
|
||||
keep_running = False
|
||||
break
|
||||
if config["write_all"]:
|
||||
keep_running = False
|
||||
|
||||
except Exception:
|
||||
error_console.print_exception()
|
||||
sys.exit(1)
|
||||
else:
|
||||
for SceneClass in scene_classes_from_file(file):
|
||||
with tempconfig({}), Manager(SceneClass) as manager:
|
||||
manager.render()
|
||||
except Exception:
|
||||
error_console.print_exception()
|
||||
sys.exit(1)
|
||||
try:
|
||||
with tempconfig({}):
|
||||
scene = SceneClass()
|
||||
scene.render()
|
||||
except Exception:
|
||||
error_console.print_exception()
|
||||
sys.exit(1)
|
||||
|
||||
if config.notify_outdated_version:
|
||||
manim_info_url = "https://pypi.org/pypi/manim/json"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ if TYPE_CHECKING:
|
|||
|
||||
__all__ = ["global_options"]
|
||||
|
||||
|
||||
logger = logging.getLogger("manim")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -21,11 +21,10 @@ output_options = option_group(
|
|||
help="Zero padding for PNG file names.",
|
||||
),
|
||||
option(
|
||||
"-w",
|
||||
"--write_to_movie",
|
||||
is_flag=True,
|
||||
default=None,
|
||||
help="Write the video to a file.",
|
||||
help="Write the video rendered with opengl to a file.",
|
||||
),
|
||||
option(
|
||||
"--media_dir",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
|
|||
|
||||
from cloup import Choice, option, option_group
|
||||
|
||||
from manim.constants import QUALITIES
|
||||
from manim.constants import QUALITIES, RendererType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from click import Context, Option
|
||||
|
|
@ -16,8 +16,6 @@ __all__ = ["render_options"]
|
|||
|
||||
logger = logging.getLogger("manim")
|
||||
|
||||
__all__ = ["render_options"]
|
||||
|
||||
|
||||
def validate_scene_range(
|
||||
ctx: Context, param: Option, value: str | None
|
||||
|
|
@ -114,13 +112,6 @@ render_options = option_group(
|
|||
"renders all scenes after n_0.",
|
||||
default=None,
|
||||
),
|
||||
option(
|
||||
"-g",
|
||||
"--groups",
|
||||
callback=lambda ctx, param, value: value.split(","),
|
||||
help="Render only the specified groups.",
|
||||
default=[],
|
||||
),
|
||||
option(
|
||||
"-a",
|
||||
"--write_all",
|
||||
|
|
@ -174,6 +165,29 @@ render_options = option_group(
|
|||
default=None,
|
||||
help="Render at this frame rate.",
|
||||
),
|
||||
option(
|
||||
"--renderer",
|
||||
type=Choice(
|
||||
[renderer_type.value for renderer_type in RendererType],
|
||||
case_sensitive=False,
|
||||
),
|
||||
help="Select a renderer for your Scene.",
|
||||
default="cairo",
|
||||
),
|
||||
option(
|
||||
"-g",
|
||||
"--save_pngs",
|
||||
is_flag=True,
|
||||
default=None,
|
||||
help="Save each frame as png (Deprecated).",
|
||||
),
|
||||
option(
|
||||
"-i",
|
||||
"--save_as_gif",
|
||||
default=None,
|
||||
is_flag=True,
|
||||
help="Save as a gif (Deprecated).",
|
||||
),
|
||||
option(
|
||||
"--save_sections",
|
||||
default=None,
|
||||
|
|
@ -186,4 +200,16 @@ render_options = option_group(
|
|||
is_flag=True,
|
||||
help="Render scenes with alpha channel.",
|
||||
),
|
||||
option(
|
||||
"--use_projection_fill_shaders",
|
||||
is_flag=True,
|
||||
help="Use shaders for OpenGLVMobject fill which are compatible with transformation matrices.",
|
||||
default=None,
|
||||
),
|
||||
option(
|
||||
"--use_projection_stroke_shaders",
|
||||
is_flag=True,
|
||||
help="Use shaders for OpenGLVMobject stroke which are compatible with transformation matrices.",
|
||||
default=None,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -67,11 +67,12 @@ __all__ = [
|
|||
"PI",
|
||||
"TAU",
|
||||
"DEGREES",
|
||||
"RADIANS",
|
||||
"QUALITIES",
|
||||
"DEFAULT_QUALITY",
|
||||
"EPILOG",
|
||||
"CONTEXT_SETTINGS",
|
||||
"SHIFT_VALUE",
|
||||
"CTRL_VALUE",
|
||||
"RendererType",
|
||||
"LineJointType",
|
||||
"CapStyleType",
|
||||
|
|
@ -193,9 +194,6 @@ TAU = 2 * PI
|
|||
DEGREES = TAU / 360
|
||||
"""The exchange rate between radians and degrees."""
|
||||
|
||||
RADIANS: float = 1.0
|
||||
"""Just a default to select for camera."""
|
||||
|
||||
|
||||
class QualityDict(TypedDict):
|
||||
flag: str | None
|
||||
|
|
@ -247,6 +245,8 @@ QUALITIES: dict[str, QualityDict] = {
|
|||
DEFAULT_QUALITY = "high_quality"
|
||||
|
||||
EPILOG = "Made with <3 by Manim Community developers."
|
||||
SHIFT_VALUE = 65505
|
||||
CTRL_VALUE = 65507
|
||||
|
||||
CONTEXT_SETTINGS = Context.settings(
|
||||
align_option_groups=True,
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from manim.event_handler.event_dispatcher import EventDispatcher
|
||||
|
||||
# This is supposed to be a Singleton
|
||||
# i.e., during runtime there should be only one object of Event Dispatcher
|
||||
EVENT_DISPATCHER = EventDispatcher()
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Self
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.event_handler.event_listener import EventListener
|
||||
from manim.event_handler.event_type import EventType
|
||||
|
||||
|
||||
class EventDispatcher:
|
||||
def __init__(self) -> None:
|
||||
self.event_listeners: dict[EventType, list[EventListener]] = {
|
||||
event_type: [] for event_type in EventType
|
||||
}
|
||||
self.mouse_point = np.array((0.0, 0.0, 0.0))
|
||||
self.mouse_drag_point = np.array((0.0, 0.0, 0.0))
|
||||
self.pressed_keys: set[int] = set()
|
||||
self.draggable_object_listeners: list[EventListener] = []
|
||||
|
||||
def add_listener(self, event_listener: EventListener) -> Self:
|
||||
assert isinstance(event_listener, EventListener)
|
||||
self.event_listeners[event_listener.event_type].append(event_listener)
|
||||
return self
|
||||
|
||||
def remove_listener(self, event_listener: EventListener) -> Self:
|
||||
assert isinstance(event_listener, EventListener)
|
||||
try:
|
||||
while event_listener in self.event_listeners[event_listener.event_type]:
|
||||
self.event_listeners[event_listener.event_type].remove(event_listener)
|
||||
except Exception:
|
||||
# raise ValueError("Handler is not handling this event, so cannot remove it.")
|
||||
pass
|
||||
return self
|
||||
|
||||
def dispatch(self, event_type: EventType, **event_data: Any) -> bool | None:
|
||||
if event_type == EventType.MouseMotionEvent:
|
||||
self.mouse_point = event_data["point"]
|
||||
elif event_type == EventType.MouseDragEvent:
|
||||
self.mouse_drag_point = event_data["point"]
|
||||
elif event_type == EventType.KeyPressEvent:
|
||||
self.pressed_keys.add(event_data["symbol"]) # Modifiers?
|
||||
elif event_type == EventType.KeyReleaseEvent:
|
||||
self.pressed_keys.difference_update({event_data["symbol"]}) # Modifiers?
|
||||
elif event_type == EventType.MousePressEvent:
|
||||
self.draggable_object_listeners = [
|
||||
listener
|
||||
for listener in self.event_listeners[EventType.MouseDragEvent]
|
||||
if listener.mobject.is_point_touching(self.mouse_point)
|
||||
]
|
||||
elif event_type == EventType.MouseReleaseEvent:
|
||||
self.draggable_object_listeners = []
|
||||
|
||||
propagate_event = None
|
||||
|
||||
if event_type == EventType.MouseDragEvent:
|
||||
for listener in self.draggable_object_listeners:
|
||||
assert isinstance(listener, EventListener)
|
||||
propagate_event = listener.callback(listener.mobject, event_data)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return propagate_event
|
||||
|
||||
elif event_type.value.startswith("mouse"):
|
||||
for listener in self.event_listeners[event_type]:
|
||||
if listener.mobject.is_point_touching(self.mouse_point):
|
||||
propagate_event = listener.callback(listener.mobject, event_data)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return propagate_event
|
||||
|
||||
elif event_type.value.startswith("key"):
|
||||
for listener in self.event_listeners[event_type]:
|
||||
propagate_event = listener.callback(listener.mobject, event_data)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return propagate_event
|
||||
|
||||
return propagate_event
|
||||
|
||||
def get_listeners_count(self) -> int:
|
||||
return sum([len(value) for key, value in self.event_listeners.items()])
|
||||
|
||||
def get_mouse_point(self) -> np.ndarray:
|
||||
return self.mouse_point
|
||||
|
||||
def get_mouse_drag_point(self) -> np.ndarray:
|
||||
return self.mouse_drag_point
|
||||
|
||||
def is_key_pressed(self, symbol: int) -> bool:
|
||||
return symbol in self.pressed_keys
|
||||
|
||||
__iadd__ = add_listener
|
||||
__isub__ = remove_listener
|
||||
__call__ = dispatch
|
||||
__len__ = get_listeners_count
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from manim.event_handler.event_type import EventType
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
|
||||
|
||||
class EventListener:
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
event_type: EventType,
|
||||
event_callback: Callable[[Mobject, dict[str, str]], None],
|
||||
) -> None:
|
||||
self.mobject = mobject
|
||||
self.event_type = event_type
|
||||
self.callback = event_callback
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return_val = False
|
||||
if isinstance(other, EventListener):
|
||||
with contextlib.suppress(Exception):
|
||||
return_val = (
|
||||
self.callback == other.callback
|
||||
and self.mobject == other.mobject
|
||||
and self.event_type == other.event_type
|
||||
)
|
||||
return return_val
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class EventType(Enum):
|
||||
MouseMotionEvent = "mouse_motion_event"
|
||||
MousePressEvent = "mouse_press_event"
|
||||
MouseReleaseEvent = "mouse_release_event"
|
||||
MouseDragEvent = "mouse_drag_event"
|
||||
MouseScrollEvent = "mouse_scroll_event"
|
||||
KeyPressEvent = "key_press_event"
|
||||
KeyReleaseEvent = "key_release_event"
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
class WindowProtocol(Protocol):
|
||||
@property
|
||||
def is_closing(self) -> bool: ...
|
||||
|
||||
def swap_buffers(self) -> object: ...
|
||||
|
||||
def close(self) -> object: ...
|
||||
|
||||
def clear(self) -> object: ...
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from .file_writer import FileWriter
|
||||
from .sections import *
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
from manim.typing import PixelArray
|
||||
|
||||
|
||||
class FileWriterProtocol(Protocol):
|
||||
"""Protocol for a file writer.
|
||||
|
||||
This is mainly useful for testing purposes, to create
|
||||
a mock file writer. However, it can be used in plugins.
|
||||
"""
|
||||
|
||||
num_plays: int
|
||||
|
||||
def __init__(self, scene_name: str) -> None: ...
|
||||
|
||||
def begin_animation(self, allow_write: bool = False) -> object: ...
|
||||
|
||||
def end_animation(self, allow_write: bool = False) -> object: ...
|
||||
|
||||
def is_already_cached(self, hash_invocation: str) -> bool: ...
|
||||
|
||||
def add_partial_movie_file(self, hash_animation: str) -> object: ...
|
||||
|
||||
def write_frame(self, frame: PixelArray) -> object: ...
|
||||
|
||||
def next_section(self, name: str, type_: str, skip_animations: bool) -> object: ...
|
||||
|
||||
def finish(self) -> None: ...
|
||||
|
||||
def save_image(self, image: PixelArray) -> object: ...
|
||||
482
manim/manager.py
482
manim/manager.py
|
|
@ -1,482 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
__all__ = ["Manager"]
|
||||
|
||||
import contextlib
|
||||
import platform
|
||||
import time
|
||||
import warnings
|
||||
from collections.abc import Iterable, Iterator, Sequence
|
||||
from typing import TYPE_CHECKING, Generic, TypeVar
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim import config, logger
|
||||
from manim.animation.animation import Wait
|
||||
from manim.event_handler.window import WindowProtocol
|
||||
from manim.file_writer import FileWriter
|
||||
from manim.renderer.opengl_renderer import OpenGLRenderer
|
||||
from manim.renderer.opengl_renderer_window import Window
|
||||
from manim.scene.scene import Scene, SceneState
|
||||
from manim.utils.exceptions import EndSceneEarlyException
|
||||
from manim.utils.hashing import get_hash_from_play_call
|
||||
from manim.utils.progressbar import (
|
||||
ExperimentalProgressBarWarning,
|
||||
NullProgressBar,
|
||||
ProgressBar,
|
||||
ProgressBarProtocol,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import TracebackType
|
||||
from typing import Any, Self
|
||||
|
||||
import numpy.typing as npt
|
||||
|
||||
from manim.animation.protocol import AnimationProtocol
|
||||
from manim.file_writer.protocols import FileWriterProtocol
|
||||
from manim.renderer.renderer import RendererProtocol
|
||||
|
||||
SceneT = TypeVar("SceneT", bound=Scene)
|
||||
|
||||
|
||||
class Manager(Generic[SceneT]):
|
||||
"""
|
||||
The Brain of Manim
|
||||
|
||||
.. admonition:: Warning for Developers
|
||||
|
||||
Only methods of this class that are not prefixed with an
|
||||
underscore (``_``) are stable. If you override any of the
|
||||
``_`` methods, consider pinning your version of Manim.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Manimation(Scene):
|
||||
def construct(self):
|
||||
self.play(FadeIn(Circle()))
|
||||
|
||||
|
||||
# make sure to use it as a context manager
|
||||
# to ensure proper resource cleanup
|
||||
with Manager(Manimation) as manager:
|
||||
manager.render()
|
||||
"""
|
||||
|
||||
def __init__(self, scene_cls: type[SceneT]) -> None:
|
||||
config._warn_about_config_options()
|
||||
self.scene: SceneT = scene_cls(manager=self)
|
||||
|
||||
if not isinstance(self.scene, Scene):
|
||||
raise ValueError(f"{self.scene!r} is not an instance of Scene")
|
||||
|
||||
self.time = 0.0
|
||||
|
||||
# Initialize window, if applicable
|
||||
self.window = self.create_window()
|
||||
|
||||
# this must be done AFTER instantiating a window
|
||||
self.renderer = self.create_renderer()
|
||||
|
||||
self.file_writer = self.create_file_writer()
|
||||
self._write_files = config.write_to_movie
|
||||
|
||||
# internal state
|
||||
self._skipping = config.save_last_frame
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.scene!r}) at time {self.time:.2f}s"
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> None:
|
||||
self.release()
|
||||
|
||||
# keep these as instance methods so subclasses
|
||||
# have access to everything
|
||||
def create_renderer(self) -> RendererProtocol:
|
||||
"""Create and return a renderer instance.
|
||||
|
||||
This can be overridden in subclasses (plugins), if more processing
|
||||
is needed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
An instance of a renderer
|
||||
"""
|
||||
renderer = OpenGLRenderer(
|
||||
background_color=config.background_color,
|
||||
background_opacity=config.background_opacity,
|
||||
)
|
||||
if config.preview:
|
||||
renderer.use_window()
|
||||
return renderer
|
||||
|
||||
def create_window(self) -> WindowProtocol | None:
|
||||
"""Create and return a window instance.
|
||||
|
||||
This can be overridden in subclasses (plugins), if more
|
||||
processing is needed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
A window if previewing, else None
|
||||
"""
|
||||
return Window() if config.preview else None
|
||||
|
||||
def create_file_writer(self) -> FileWriterProtocol:
|
||||
"""Create and return a file writer instance.
|
||||
|
||||
This can be overridden in subclasses (plugins), if more
|
||||
processing is needed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
A file writer satisfying :class:`.FileWriterProtocol`
|
||||
"""
|
||||
return FileWriter(scene_name=self.scene.get_default_scene_name())
|
||||
|
||||
def setup(self) -> None:
|
||||
"""Set up processes and manager"""
|
||||
self.scene.setup()
|
||||
|
||||
# these are used for making sure it feels like the correct
|
||||
# amount of time has passed in the window instead of rendering
|
||||
# at full speed
|
||||
# See the docstring of :meth:`_wait_for_animation_time`
|
||||
self.virtual_animation_start_time = 0.0
|
||||
self.real_animation_start_time = time.perf_counter()
|
||||
|
||||
def render(self) -> None:
|
||||
"""
|
||||
Entry point to running a Manim class
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyScene(Scene):
|
||||
def construct(self):
|
||||
self.play(Create(Circle()))
|
||||
|
||||
|
||||
with tempconfig({"preview": True}), Manager(MyScene) as manager:
|
||||
manager.render()
|
||||
"""
|
||||
self._render_first_pass()
|
||||
self._render_second_pass()
|
||||
|
||||
def _render_first_pass(self) -> None:
|
||||
"""
|
||||
Temporarily use the normal single pass
|
||||
rendering system
|
||||
"""
|
||||
self.setup()
|
||||
|
||||
with contextlib.suppress(EndSceneEarlyException):
|
||||
self.construct()
|
||||
self.post_construct()
|
||||
self._interact()
|
||||
|
||||
self.tear_down()
|
||||
|
||||
def construct(self) -> None:
|
||||
if not self.scene.groups_api:
|
||||
self.scene.construct()
|
||||
return
|
||||
|
||||
for group in self.scene.find_groups():
|
||||
if not config.groups or group.name in config.groups:
|
||||
group()
|
||||
elif group.name not in config.groups:
|
||||
with self.no_render():
|
||||
group()
|
||||
|
||||
def _render_second_pass(self) -> None:
|
||||
"""
|
||||
In the future, this method could be used
|
||||
for two pass rendering
|
||||
"""
|
||||
...
|
||||
|
||||
def release(self) -> None:
|
||||
self.renderer.release()
|
||||
|
||||
def post_construct(self) -> None:
|
||||
"""Run post-construct hooks, and clean up the file writer."""
|
||||
should_write_image = config.save_last_frame or (
|
||||
config.write_to_movie and not self.file_writer.num_plays
|
||||
)
|
||||
if self.file_writer.num_plays:
|
||||
self.file_writer.finish()
|
||||
# otherwise no animations were played
|
||||
if should_write_image:
|
||||
self.render_state(write_frame=False)
|
||||
# FIXME: for some reason the OpenGLRenderer does not give out the
|
||||
# correct frame values here
|
||||
frame = self.renderer.get_pixels()
|
||||
self.file_writer.save_image(frame)
|
||||
|
||||
self._write_files = False
|
||||
|
||||
def tear_down(self) -> None:
|
||||
"""Tear down the scene and the window."""
|
||||
self.scene.tear_down()
|
||||
|
||||
if self.window is not None:
|
||||
self.window.close()
|
||||
self.window = None
|
||||
|
||||
def _interact(self) -> None:
|
||||
"""Live interaction with the Window"""
|
||||
if self.window is None:
|
||||
return
|
||||
logger.info(
|
||||
"\nTips: Using the keys `d`, `f`, or `z` "
|
||||
"you can interact with the scene. "
|
||||
"Press `command + q` or `esc` to quit"
|
||||
)
|
||||
last_time = time.perf_counter()
|
||||
while not self.window.is_closing:
|
||||
current_time = time.perf_counter()
|
||||
dt = current_time - last_time
|
||||
last_time = current_time
|
||||
self._update_frame(dt)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def no_render(self) -> Iterator[None]:
|
||||
"""Context manager to temporarily disable rendering.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
with manager.no_render():
|
||||
manager._play(FadeIn(Circle()))
|
||||
"""
|
||||
self._skipping = True
|
||||
yield
|
||||
self._skipping = False
|
||||
|
||||
# ----------------------------------#
|
||||
# Animation Pipeline #
|
||||
# ----------------------------------#
|
||||
|
||||
def _update_frame(
|
||||
self, dt: float, *, write_frame: bool | None = None, run_updaters: bool = True
|
||||
) -> None:
|
||||
"""Update the current frame by ``dt``
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dt : the time in between frames
|
||||
write_frame : Whether to write the result to the output stream (videos ONLY).
|
||||
Default value checks :attr:`_write_files` to see if it should be written.
|
||||
"""
|
||||
self.time += dt
|
||||
if run_updaters:
|
||||
self.scene._update_mobjects(dt)
|
||||
self.scene.time = self.time
|
||||
|
||||
if self.window is not None:
|
||||
if not self._skipping:
|
||||
self.window.clear()
|
||||
|
||||
# if it's closing, then any subsequent methods will
|
||||
# raise an error because the internal C window pointer is nullptr.
|
||||
if self.window.is_closing:
|
||||
raise EndSceneEarlyException()
|
||||
|
||||
if not self._skipping:
|
||||
self.render_state(write_frame=write_frame)
|
||||
self._wait_for_animation_time()
|
||||
|
||||
def _wait_for_animation_time(self) -> None:
|
||||
"""Wait for the real time to catch up to the "virtual" animation time.
|
||||
|
||||
Animations can render faster than real time, so we have to
|
||||
slow the window down for the correct amount of time, such
|
||||
as during a wait animation.
|
||||
"""
|
||||
if self.window is None:
|
||||
return
|
||||
|
||||
self.window.swap_buffers()
|
||||
|
||||
if self._skipping:
|
||||
return
|
||||
|
||||
vt = self.time - self.virtual_animation_start_time
|
||||
rt = time.perf_counter() - self.real_animation_start_time
|
||||
# we can't sleep because we still need to poll for events,
|
||||
# e.g. hitting Escape or close
|
||||
while rt < vt:
|
||||
if self.window.is_closing:
|
||||
raise EndSceneEarlyException()
|
||||
# make sure to poll for events
|
||||
self.window.swap_buffers()
|
||||
rt = time.perf_counter() - self.real_animation_start_time
|
||||
|
||||
def _play(self, *animations: AnimationProtocol) -> None:
|
||||
"""Play a bunch of animations"""
|
||||
self.scene.pre_play()
|
||||
|
||||
if self.window is not None:
|
||||
self.real_animation_start_time = time.perf_counter()
|
||||
self.virtual_animation_start_time = self.time
|
||||
|
||||
self._write_hashed_movie_file(animations)
|
||||
|
||||
self.scene.begin_animations(animations)
|
||||
self._progress_through_animations(animations)
|
||||
self.scene.finish_animations(animations)
|
||||
|
||||
self.scene.post_play()
|
||||
|
||||
self.file_writer.end_animation(allow_write=self._write_files)
|
||||
|
||||
def _write_hashed_movie_file(self, animations: Sequence[AnimationProtocol]) -> None:
|
||||
"""Compute the hash of a self.play call, and write it to a file
|
||||
|
||||
Essentially, a series of methods that need to be called to successfully
|
||||
render a frame.
|
||||
"""
|
||||
if not config.write_to_movie or self._skipping:
|
||||
return
|
||||
|
||||
if config.disable_caching:
|
||||
if not config.disable_caching_warning:
|
||||
logger.info("Caching disabled...")
|
||||
hash_current_play = f"uncached_{self.file_writer.num_plays:05}"
|
||||
else:
|
||||
hash_current_play = get_hash_from_play_call(
|
||||
self.scene,
|
||||
self.scene.camera,
|
||||
animations,
|
||||
self.scene.mobjects,
|
||||
)
|
||||
if self.file_writer.is_already_cached(hash_current_play):
|
||||
logger.info(
|
||||
f"Animation {self.file_writer.num_plays} : Using cached data (hash : {hash_current_play})"
|
||||
)
|
||||
# TODO: think about how to skip
|
||||
raise NotImplementedError(
|
||||
"Skipping cached animations is not implemented yet"
|
||||
)
|
||||
|
||||
self.file_writer.add_partial_movie_file(hash_current_play)
|
||||
self.file_writer.begin_animation(allow_write=self._write_files)
|
||||
|
||||
def _create_progressbar(
|
||||
self, total: float, description: str, **kwargs: Any
|
||||
) -> contextlib.AbstractContextManager[ProgressBarProtocol]:
|
||||
"""Create a progressbar"""
|
||||
if not config.progress_bar:
|
||||
return contextlib.nullcontext(NullProgressBar())
|
||||
|
||||
with warnings.catch_warnings():
|
||||
if config.verbosity != "DEBUG":
|
||||
# Note: update when rich/notebook tqdm is no longer experimental
|
||||
warnings.simplefilter("ignore", category=ExperimentalProgressBarWarning)
|
||||
|
||||
return ProgressBar(
|
||||
total=total,
|
||||
unit="frames",
|
||||
desc=description % {"num": self.file_writer.num_plays},
|
||||
ascii=True if platform.system() == "Windows" else None,
|
||||
leave=config.progress_bar == "leave",
|
||||
disable=config.progress_bar == "none",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def _progress_through_animations(
|
||||
self, animations: Sequence[AnimationProtocol]
|
||||
) -> None:
|
||||
last_t = 0.0
|
||||
run_time = _calc_runtime(animations)
|
||||
progression = _calc_time_progression(run_time)
|
||||
with self._create_progressbar(
|
||||
progression.shape[0],
|
||||
f"Animation %(num)d: {animations[0]}{', etc.' if len(animations) > 1 else ''}",
|
||||
) as progress:
|
||||
if self._skipping:
|
||||
self.scene._update_animations(animations, run_time, run_time)
|
||||
self._update_frame(run_time, run_updaters=False)
|
||||
return
|
||||
for t in progression:
|
||||
dt, last_t = t - last_t, t
|
||||
self.scene._update_animations(animations, t, dt)
|
||||
run_updaters = not self.scene.is_current_animation_frozen_frame(
|
||||
animations
|
||||
)
|
||||
self._update_frame(dt, run_updaters=run_updaters)
|
||||
for anim in animations:
|
||||
if (
|
||||
isinstance(anim, Wait)
|
||||
and anim.stop_condition is not None
|
||||
and anim.stop_condition()
|
||||
):
|
||||
return
|
||||
progress.update(1)
|
||||
|
||||
# -------------------------#
|
||||
# Rendering #
|
||||
# -------------------------#
|
||||
|
||||
def render_state(self, write_frame: bool | None = None) -> None:
|
||||
"""Render the current state of the scene.
|
||||
|
||||
Any extra kwargs are passed to :meth:`_render_frame`.
|
||||
"""
|
||||
state = self.scene.get_state()
|
||||
self._render_frame(state, write_frame=write_frame)
|
||||
|
||||
def _render_frame(
|
||||
self, state: SceneState, *, write_frame: bool | None = None
|
||||
) -> None:
|
||||
"""Renders a frame based on a state, and writes it to the file writers stream.
|
||||
|
||||
This is used for writing a single frame. Any extra kwargs are passed to :meth:`write_frame`.
|
||||
|
||||
.. warning::
|
||||
|
||||
This method will not work if :meth:`.FileWriter.begin_animation` and
|
||||
:meth:`.FileWriter.add_partial_movie_file` have not been called. Do NOT
|
||||
use this to write a single frame!
|
||||
"""
|
||||
self.renderer.render(state)
|
||||
|
||||
should_write = write_frame if write_frame is not None else self._write_files
|
||||
if should_write:
|
||||
self.write_frame()
|
||||
|
||||
def write_frame(self) -> None:
|
||||
"""Take a frame from the renderer and write it in the file writer."""
|
||||
frame = self.renderer.get_pixels()
|
||||
self.file_writer.write_frame(frame)
|
||||
|
||||
|
||||
def _calc_time_progression(run_time: float) -> npt.NDArray[np.float64]:
|
||||
"""Compute the time values at which to evaluate the animation"""
|
||||
return np.arange(0, run_time, 1 / config.frame_rate)
|
||||
|
||||
|
||||
def _calc_runtime(animations: Iterable[AnimationProtocol]) -> float:
|
||||
"""Calculate the runtime of an iterable of animations.
|
||||
|
||||
.. warning::
|
||||
|
||||
If animations is a generator, this will consume the generator.
|
||||
"""
|
||||
return max(animation.get_run_time() for animation in animations)
|
||||
|
|
@ -50,8 +50,8 @@ from typing import TYPE_CHECKING, Any, Self, cast
|
|||
import numpy as np
|
||||
|
||||
from manim.constants import *
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.utils.color import BLACK, BLUE, RED, WHITE, ParsableManimColor
|
||||
from manim.utils.iterables import adjacent_pairs
|
||||
from manim.utils.space_ops import (
|
||||
|
|
@ -68,7 +68,7 @@ if TYPE_CHECKING:
|
|||
|
||||
import manim.mobject.geometry.tips as tips
|
||||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.text.tex_mobject import SingleStringMathTex, Tex
|
||||
from manim.mobject.text.text_mobject import Text
|
||||
from manim.typing import (
|
||||
|
|
@ -79,7 +79,7 @@ if TYPE_CHECKING:
|
|||
)
|
||||
|
||||
|
||||
class TipableVMobject(VMobject):
|
||||
class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
|
||||
"""Meant for shared functionality between Arc and Line.
|
||||
Functionality can be classified broadly into these groups:
|
||||
|
||||
|
|
@ -1093,7 +1093,7 @@ class Annulus(Circle):
|
|||
self.generate_points()
|
||||
|
||||
|
||||
class CubicBezier(VMobject):
|
||||
class CubicBezier(VMobject, metaclass=ConvertToOpenGL):
|
||||
"""A cubic Bézier curve.
|
||||
|
||||
Example
|
||||
|
|
@ -1128,7 +1128,7 @@ class CubicBezier(VMobject):
|
|||
self.add_cubic_bezier_curve(start_anchor, start_handle, end_handle, end_anchor)
|
||||
|
||||
|
||||
class ArcPolygon(VMobject):
|
||||
class ArcPolygon(VMobject, metaclass=ConvertToOpenGL):
|
||||
"""A generalized polygon allowing for points to be connected with arcs.
|
||||
|
||||
This version tries to stick close to the way :class:`Polygon` is used. Points
|
||||
|
|
@ -1249,7 +1249,7 @@ class ArcPolygon(VMobject):
|
|||
self.arcs = arcs
|
||||
|
||||
|
||||
class ArcPolygonFromArcs(VMobject):
|
||||
class ArcPolygonFromArcs(VMobject, metaclass=ConvertToOpenGL):
|
||||
"""A generalized polygon allowing for points to be connected with arcs.
|
||||
|
||||
This version takes in pre-defined arcs to generate the arcpolygon and introduces
|
||||
|
|
|
|||
|
|
@ -8,16 +8,19 @@ import numpy as np
|
|||
from pathops import Path as SkiaPath
|
||||
from pathops import PathVerb, difference, intersection, union, xor
|
||||
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
from manim import config
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.typing import Point2DLike_Array, Point3D_Array, Point3DLike_Array
|
||||
|
||||
from ...constants import RendererType
|
||||
|
||||
__all__ = ["Union", "Intersection", "Difference", "Exclusion"]
|
||||
|
||||
|
||||
class _BooleanOps(VMobject):
|
||||
class _BooleanOps(VMobject, metaclass=ConvertToOpenGL):
|
||||
"""This class contains some helper functions which
|
||||
helps to convert to and from skia objects and manim
|
||||
objects (:class:`~.VMobject`).
|
||||
|
|
@ -81,15 +84,29 @@ class _BooleanOps(VMobject):
|
|||
if len(points) == 0: # what? No points so return empty path
|
||||
return path
|
||||
|
||||
subpaths = vmobject.get_subpaths_from_points(points)
|
||||
for subpath in subpaths:
|
||||
quads = vmobject.get_bezier_tuples_from_points(subpath)
|
||||
start = subpath[0]
|
||||
path.moveTo(*start[:2])
|
||||
for _p0, p1, p2 in quads:
|
||||
path.quadTo(*p1[:2], *p2[:2])
|
||||
if vmobject.consider_points_equals(subpath[0], subpath[-1]):
|
||||
path.close()
|
||||
# In OpenGL it's quadratic beizer curves while on Cairo it's cubic...
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
subpaths = vmobject.get_subpaths_from_points(points)
|
||||
for subpath in subpaths:
|
||||
quads = vmobject.get_bezier_tuples_from_points(subpath)
|
||||
start = subpath[0]
|
||||
path.moveTo(*start[:2])
|
||||
for _p0, p1, p2 in quads:
|
||||
path.quadTo(*p1[:2], *p2[:2])
|
||||
if vmobject.consider_points_equals(subpath[0], subpath[-1]):
|
||||
path.close()
|
||||
elif config.renderer == RendererType.CAIRO:
|
||||
subpaths = vmobject.gen_subpaths_from_points_2d(points) # type: ignore[assignment]
|
||||
for subpath in subpaths:
|
||||
quads = vmobject.gen_cubic_bezier_tuples_from_points(subpath)
|
||||
start = subpath[0]
|
||||
path.moveTo(*start[:2])
|
||||
for _p0, p1, p2, p3 in quads:
|
||||
path.cubicTo(*p1[:2], *p2[:2], *p3[:2])
|
||||
|
||||
if vmobject.consider_points_equals_2d(subpath[0], subpath[-1]):
|
||||
path.close()
|
||||
|
||||
return path
|
||||
|
||||
def _convert_skia_path_to_vmobject(self, path: SkiaPath) -> VMobject:
|
||||
|
|
|
|||
|
|
@ -15,14 +15,15 @@ from manim.mobject.geometry.shape_matchers import (
|
|||
BackgroundRectangle,
|
||||
SurroundingRectangle,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -14,19 +14,18 @@ __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.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLDashedVMobject as DashedVMobject,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
from manim.mobject.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
|
||||
from manim.mobject.types.vectorized_mobject import DashedVMobject, VGroup, VMobject
|
||||
from manim.utils.color import WHITE
|
||||
from manim.utils.space_ops import angle_of_vector, line_intersection, normalize
|
||||
|
||||
|
|
@ -188,7 +187,7 @@ class Line(TipableVMobject):
|
|||
direction
|
||||
The direction.
|
||||
"""
|
||||
if isinstance(mob_or_point, Mobject):
|
||||
if isinstance(mob_or_point, (Mobject, OpenGLMobject)):
|
||||
mob = mob_or_point
|
||||
if direction is None:
|
||||
return mob.get_center()
|
||||
|
|
@ -459,7 +458,7 @@ class TangentLine(Line):
|
|||
self.scale(self.length / self.get_length())
|
||||
|
||||
|
||||
class Elbow(VMobject):
|
||||
class Elbow(VMobject, metaclass=ConvertToOpenGL):
|
||||
"""Two lines that create a right angle about each other: L-shape.
|
||||
|
||||
Parameters
|
||||
|
|
@ -600,7 +599,7 @@ class Arrow(Line):
|
|||
super().__init__(*args, buff=buff, stroke_width=stroke_width, **kwargs) # type: ignore[misc]
|
||||
# TODO, should this be affected when
|
||||
# Arrow.set_stroke is called?
|
||||
self.initial_stroke_width = stroke_width
|
||||
self.initial_stroke_width = self.stroke_width
|
||||
self.add_tip(tip_shape=tip_shape)
|
||||
self._set_stroke_width_from_length()
|
||||
|
||||
|
|
@ -649,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:
|
||||
|
|
@ -689,13 +690,20 @@ class Arrow(Line):
|
|||
def _set_stroke_width_from_length(self) -> Self:
|
||||
"""Sets stroke width based on length."""
|
||||
max_ratio = self.max_stroke_width_to_length_ratio
|
||||
self.set_stroke(
|
||||
width=min(
|
||||
self.initial_stroke_width,
|
||||
[max_ratio * self.get_length()] * len(self.initial_stroke_width),
|
||||
),
|
||||
recurse=False,
|
||||
)
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
# Mypy does not recognize that the self object in this case
|
||||
# is a OpenGLVMobject and that the set_stroke method is
|
||||
# defined here:
|
||||
# mobject/opengl/opengl_vectorized_mobject.py#L248
|
||||
self.set_stroke( # type: ignore[call-arg]
|
||||
width=min(self.initial_stroke_width, max_ratio * self.get_length()),
|
||||
recurse=False,
|
||||
)
|
||||
else:
|
||||
self.set_stroke(
|
||||
width=min(self.initial_stroke_width, max_ratio * self.get_length()),
|
||||
family=False,
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
|
|
@ -857,7 +865,7 @@ class DoubleArrow(Arrow):
|
|||
self.add_tip(at_start=True, tip_shape=tip_shape_start)
|
||||
|
||||
|
||||
class Angle(VMobject):
|
||||
class Angle(VMobject, metaclass=ConvertToOpenGL):
|
||||
"""A circular arc or elbow-type mobject representing an angle of two lines.
|
||||
|
||||
Parameters
|
||||
|
|
|
|||
|
|
@ -24,12 +24,8 @@ import numpy as np
|
|||
|
||||
from manim.constants import *
|
||||
from manim.mobject.geometry.arc import ArcBetweenPoints
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.utils.color import BLUE, WHITE, ParsableManimColor
|
||||
from manim.utils.iterables import adjacent_n_tuples, adjacent_pairs
|
||||
from manim.utils.qhull import QuickHull
|
||||
|
|
@ -49,7 +45,7 @@ if TYPE_CHECKING:
|
|||
from manim.utils.color import ParsableManimColor
|
||||
|
||||
|
||||
class Polygram(VMobject):
|
||||
class Polygram(VMobject, metaclass=ConvertToOpenGL):
|
||||
"""A generalized :class:`Polygon`, allowing for disconnected sets of edges.
|
||||
|
||||
Parameters
|
||||
|
|
@ -747,7 +743,7 @@ class RoundedRectangle(Rectangle):
|
|||
self.round_corners(self.corner_radius)
|
||||
|
||||
|
||||
class Cutout(VMobject):
|
||||
class Cutout(VMobject, metaclass=ConvertToOpenGL):
|
||||
"""A shape with smaller cutouts.
|
||||
|
||||
Parameters
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ from manim.constants import (
|
|||
)
|
||||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.geometry.polygram import RoundedRectangle
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
|
||||
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, PURE_YELLOW, RED, ParsableManimColor
|
||||
|
||||
|
||||
|
|
@ -54,10 +55,12 @@ class SurroundingRectangle(RoundedRectangle):
|
|||
corner_radius: float = 0.0,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLGroup as Group
|
||||
from manim.mobject.mobject import Group
|
||||
|
||||
if not all(isinstance(mob, Mobject) for mob in mobjects):
|
||||
raise TypeError("Expected all inputs for parameter mobjects to be Mobjects")
|
||||
if not all(isinstance(mob, (Mobject, OpenGLMobject)) for mob in mobjects):
|
||||
raise TypeError(
|
||||
"Expected all inputs for parameter mobjects to be a Mobjects"
|
||||
)
|
||||
|
||||
if isinstance(buff, tuple):
|
||||
buff_x = buff[0]
|
||||
|
|
@ -124,7 +127,7 @@ class BackgroundRectangle(SurroundingRectangle):
|
|||
buff=buff,
|
||||
**kwargs,
|
||||
)
|
||||
self.original_fill_opacity: float = self.get_fill_opacity()
|
||||
self.original_fill_opacity: float = self.fill_opacity
|
||||
|
||||
def pointwise_become_partial(self, mobject: Mobject, a: Any, b: float) -> Self:
|
||||
self.set_fill(opacity=b * self.original_fill_opacity)
|
||||
|
|
|
|||
|
|
@ -20,14 +20,15 @@ import numpy as np
|
|||
from manim.constants import *
|
||||
from manim.mobject.geometry.arc import Circle
|
||||
from manim.mobject.geometry.polygram import Square, Triangle
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
from manim.utils.space_ops import angle_of_vector
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.typing import Point3D, Vector3D
|
||||
|
||||
|
||||
class ArrowTip(VMobject):
|
||||
class ArrowTip(VMobject, metaclass=ConvertToOpenGL):
|
||||
r"""Base class for arrow tips.
|
||||
|
||||
.. seealso::
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import numpy as np
|
|||
if TYPE_CHECKING:
|
||||
from typing import TypeAlias
|
||||
|
||||
from manim.scene.scene import Scene
|
||||
from manim.typing import Point3D, Point3DLike
|
||||
|
||||
NxGraph: TypeAlias = nx.classes.graph.Graph | nx.classes.digraph.DiGraph
|
||||
|
|
@ -26,12 +27,11 @@ from manim.animation.composition import AnimationGroup
|
|||
from manim.animation.creation import Create, Uncreate
|
||||
from manim.mobject.geometry.arc import Dot, LabeledDot
|
||||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
override_animate,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
from manim.mobject.mobject import Mobject, override_animate
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.mobject.text.tex_mobject import MathTex
|
||||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
from manim.utils.color import BLACK
|
||||
|
||||
|
||||
|
|
@ -476,7 +476,7 @@ def _determine_graph_layout(
|
|||
) from e
|
||||
|
||||
|
||||
class GenericGraph(VMobject):
|
||||
class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
||||
"""Abstract base class for graphs (that is, a collection of vertices
|
||||
connected with edges).
|
||||
|
||||
|
|
@ -569,10 +569,10 @@ class GenericGraph(VMobject):
|
|||
layout: LayoutName | dict[Hashable, Point3DLike] | LayoutFunction = "spring",
|
||||
layout_scale: float | tuple[float, float, float] = 2,
|
||||
layout_config: dict | None = None,
|
||||
vertex_type: type[VMobject] = Dot,
|
||||
vertex_type: type[Mobject] = Dot,
|
||||
vertex_config: dict | None = None,
|
||||
vertex_mobjects: dict | None = None,
|
||||
edge_type: type[VMobject] = Line,
|
||||
edge_type: type[Mobject] = Line,
|
||||
partitions: Sequence[Sequence[Hashable]] | None = None,
|
||||
root_vertex: Hashable | None = None,
|
||||
edge_config: dict | None = None,
|
||||
|
|
@ -664,13 +664,35 @@ class GenericGraph(VMobject):
|
|||
raise NotImplementedError("To be implemented in concrete subclasses")
|
||||
|
||||
def _populate_edge_dict(
|
||||
self, edges: list[tuple[Hashable, Hashable]], edge_type: type[VMobject]
|
||||
self, edges: list[tuple[Hashable, Hashable]], edge_type: type[Mobject]
|
||||
):
|
||||
"""Helper method for populating the edges of the graph."""
|
||||
raise NotImplementedError("To be implemented in concrete subclasses")
|
||||
|
||||
def __getitem__(self: Graph, v: Hashable) -> VMobject:
|
||||
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,
|
||||
|
|
@ -678,10 +700,10 @@ class GenericGraph(VMobject):
|
|||
position: Point3DLike | None = None,
|
||||
label: bool = False,
|
||||
label_fill_color: str = BLACK,
|
||||
vertex_type: type[VMobject] = Dot,
|
||||
vertex_type: type[Mobject] = Dot,
|
||||
vertex_config: dict | None = None,
|
||||
vertex_mobject: dict | None = None,
|
||||
) -> tuple[Hashable, Point3D, dict, VMobject]:
|
||||
) -> tuple[Hashable, Point3D, dict, Mobject]:
|
||||
np_position: Point3D = (
|
||||
self.get_center() if position is None else np.asarray(position)
|
||||
)
|
||||
|
|
@ -698,7 +720,7 @@ class GenericGraph(VMobject):
|
|||
label = MathTex(vertex, color=label_fill_color)
|
||||
elif vertex in self._labels:
|
||||
label = self._labels[vertex]
|
||||
elif not isinstance(label, VMobject):
|
||||
elif not isinstance(label, (Mobject, OpenGLMobject)):
|
||||
label = None
|
||||
|
||||
base_vertex_config = copy(self.default_vertex_config)
|
||||
|
|
@ -722,8 +744,8 @@ class GenericGraph(VMobject):
|
|||
vertex: Hashable,
|
||||
position: Point3DLike,
|
||||
vertex_config: dict,
|
||||
vertex_mobject: VMobject,
|
||||
) -> VMobject:
|
||||
vertex_mobject: Mobject,
|
||||
) -> Mobject:
|
||||
if vertex in self.vertices:
|
||||
raise ValueError(
|
||||
f"Vertex identifier '{vertex}' is already used for a vertex in this graph.",
|
||||
|
|
@ -749,10 +771,10 @@ class GenericGraph(VMobject):
|
|||
position: Point3DLike | None = None,
|
||||
label: bool = False,
|
||||
label_fill_color: str = BLACK,
|
||||
vertex_type: type[VMobject] = Dot,
|
||||
vertex_type: type[Mobject] = Dot,
|
||||
vertex_config: dict | None = None,
|
||||
vertex_mobject: dict | None = None,
|
||||
) -> VMobject:
|
||||
) -> Mobject:
|
||||
"""Add a vertex to the graph.
|
||||
|
||||
Parameters
|
||||
|
|
@ -767,7 +789,7 @@ class GenericGraph(VMobject):
|
|||
Controls whether or not the vertex is labeled. If ``False`` (the default),
|
||||
the vertex is not labeled; if ``True`` it is labeled using its
|
||||
names (as specified in ``vertex``) via :class:`~.MathTex`. Alternatively,
|
||||
any :class:`~.VMobject` can be passed to be used as the label.
|
||||
any :class:`~.Mobject` can be passed to be used as the label.
|
||||
label_fill_color
|
||||
Sets the fill color of the default labels generated when ``labels``
|
||||
is set to ``True``. Has no effect for other values of ``label``.
|
||||
|
|
@ -798,10 +820,10 @@ class GenericGraph(VMobject):
|
|||
positions: dict | None = None,
|
||||
labels: bool = False,
|
||||
label_fill_color: str = BLACK,
|
||||
vertex_type: type[VMobject] = Dot,
|
||||
vertex_type: type[Mobject] = Dot,
|
||||
vertex_config: dict | None = None,
|
||||
vertex_mobjects: dict | None = None,
|
||||
) -> Iterable[tuple[Hashable, Point3D, dict, VMobject]]:
|
||||
) -> Iterable[tuple[Hashable, Point3D, dict, Mobject]]:
|
||||
if positions is None:
|
||||
positions = {}
|
||||
if vertex_mobjects is None:
|
||||
|
|
@ -852,10 +874,10 @@ class GenericGraph(VMobject):
|
|||
positions: dict | None = None,
|
||||
labels: bool = False,
|
||||
label_fill_color: str = BLACK,
|
||||
vertex_type: type[VMobject] = Dot,
|
||||
vertex_type: type[Mobject] = Dot,
|
||||
vertex_config: dict | None = None,
|
||||
vertex_mobjects: dict | None = None,
|
||||
) -> VGroup:
|
||||
):
|
||||
"""Add a list of vertices to the graph.
|
||||
|
||||
Parameters
|
||||
|
|
@ -870,7 +892,7 @@ class GenericGraph(VMobject):
|
|||
Controls whether or not the vertex is labeled. If ``False`` (the default),
|
||||
the vertex is not labeled; if ``True`` it is labeled using its
|
||||
names (as specified in ``vertex``) via :class:`~.MathTex`. Alternatively,
|
||||
any :class:`~.VMobject` can be passed to be used as the label.
|
||||
any :class:`~.Mobject` can be passed to be used as the label.
|
||||
label_fill_color
|
||||
Sets the fill color of the default labels generated when ``labels``
|
||||
is set to ``True``. Has no effect for other values of ``labels``.
|
||||
|
|
@ -884,7 +906,7 @@ class GenericGraph(VMobject):
|
|||
values are mobjects that should be used as vertices. Overrides
|
||||
all other vertex customization options.
|
||||
"""
|
||||
return VGroup(
|
||||
return [
|
||||
self._add_created_vertex(*v)
|
||||
for v in self._create_vertices(
|
||||
*vertices,
|
||||
|
|
@ -895,30 +917,29 @@ class GenericGraph(VMobject):
|
|||
vertex_config=vertex_config,
|
||||
vertex_mobjects=vertex_mobjects,
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
@override_animate(add_vertices)
|
||||
def _add_vertices_animation(
|
||||
self,
|
||||
*vertices: Hashable,
|
||||
anim_args: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AnimationGroup:
|
||||
# Use introducer=False to prevent re-adding the vertices when animating them
|
||||
base_anim_args = {"animation": Create, "introducer": False}
|
||||
if anim_args is not None:
|
||||
base_anim_args.update(anim_args)
|
||||
animation = base_anim_args.pop("animation")
|
||||
def _add_vertices_animation(self, *args, anim_args=None, **kwargs):
|
||||
if anim_args is None:
|
||||
anim_args = {}
|
||||
|
||||
animation = anim_args.pop("animation", Create)
|
||||
|
||||
vertex_mobjects = self._create_vertices(*args, **kwargs)
|
||||
|
||||
def on_finish(scene: Scene):
|
||||
for v in vertex_mobjects:
|
||||
scene.remove(v[-1])
|
||||
self._add_created_vertex(*v)
|
||||
|
||||
vertex_mobjects = self.add_vertices(*vertices, **kwargs)
|
||||
return AnimationGroup(
|
||||
*(
|
||||
animation(vertex_mobject, **base_anim_args)
|
||||
for vertex_mobject in vertex_mobjects
|
||||
),
|
||||
*(animation(v[-1], **anim_args) for v in vertex_mobjects),
|
||||
group=self,
|
||||
_on_finish=on_finish,
|
||||
)
|
||||
|
||||
def _remove_vertex(self, vertex: Hashable) -> VGroup:
|
||||
def _remove_vertex(self, vertex):
|
||||
"""Remove a vertex (as well as all incident edges) from the graph.
|
||||
|
||||
Parameters
|
||||
|
|
@ -952,9 +973,9 @@ class GenericGraph(VMobject):
|
|||
to_remove.append(self.vertices.pop(vertex))
|
||||
|
||||
self.remove(*to_remove)
|
||||
return VGroup(*to_remove)
|
||||
return self.get_group_class()(*to_remove)
|
||||
|
||||
def remove_vertices(self, *vertices: Hashable) -> VGroup:
|
||||
def remove_vertices(self, *vertices):
|
||||
"""Remove several vertices from the graph.
|
||||
|
||||
Parameters
|
||||
|
|
@ -968,8 +989,7 @@ class GenericGraph(VMobject):
|
|||
::
|
||||
|
||||
>>> G = Graph([1, 2, 3], [(1, 2), (2, 3)])
|
||||
>>> removed = G.remove_vertices(2, 3)
|
||||
>>> removed
|
||||
>>> removed = G.remove_vertices(2, 3); removed
|
||||
VGroup(Line, Line, Dot, Dot)
|
||||
>>> G
|
||||
Undirected graph on 1 vertices and 0 edges
|
||||
|
|
@ -978,32 +998,26 @@ class GenericGraph(VMobject):
|
|||
mobjects = []
|
||||
for v in vertices:
|
||||
mobjects.extend(self._remove_vertex(v).submobjects)
|
||||
return VGroup(*mobjects)
|
||||
return self.get_group_class()(*mobjects)
|
||||
|
||||
@override_animate(remove_vertices)
|
||||
def _remove_vertices_animation(
|
||||
self, *vertices: Hashable, anim_args: dict[str, Any] | None = None
|
||||
) -> AnimationGroup:
|
||||
base_anim_args = {"animation": Uncreate}
|
||||
if anim_args is not None:
|
||||
base_anim_args.update(anim_args)
|
||||
animation = base_anim_args.pop("animation")
|
||||
def _remove_vertices_animation(self, *vertices, anim_args=None):
|
||||
if anim_args is None:
|
||||
anim_args = {}
|
||||
|
||||
vertex_and_edge_mobjects = self.remove_vertices(*vertices)
|
||||
animation = anim_args.pop("animation", Uncreate)
|
||||
|
||||
mobjects = self.remove_vertices(*vertices)
|
||||
return AnimationGroup(
|
||||
*(
|
||||
animation(vertex_or_edge_mobject, **anim_args)
|
||||
for vertex_or_edge_mobject in vertex_and_edge_mobjects
|
||||
),
|
||||
introducer=True, # Reintroduce vertices and edges temporarily to animate them
|
||||
*(animation(mobj, **anim_args) for mobj in mobjects), group=self
|
||||
)
|
||||
|
||||
def _add_edge(
|
||||
self,
|
||||
edge: tuple[Hashable, Hashable],
|
||||
edge_type: type[VMobject] = Line,
|
||||
edge_type: type[Mobject] = Line,
|
||||
edge_config: dict | None = None,
|
||||
) -> VGroup:
|
||||
):
|
||||
"""Add a new edge to the graph.
|
||||
|
||||
Parameters
|
||||
|
|
@ -1047,17 +1061,15 @@ class GenericGraph(VMobject):
|
|||
|
||||
self.add(edge_mobject)
|
||||
added_mobjects.append(edge_mobject)
|
||||
return VGroup(*added_mobjects)
|
||||
return self.get_group_class()(*added_mobjects)
|
||||
|
||||
def add_edges(
|
||||
self,
|
||||
*edges: tuple[Hashable, Hashable],
|
||||
edge_type: type[VMobject] = Line,
|
||||
edge_config: dict[str, Any]
|
||||
| dict[tuple[Hashable, Hashable], dict[str, Any]]
|
||||
| None = None,
|
||||
**kwargs: Any,
|
||||
) -> VGroup:
|
||||
edge_type: type[Mobject] = Line,
|
||||
edge_config: dict | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Add new edges to the graph.
|
||||
|
||||
Parameters
|
||||
|
|
@ -1110,33 +1122,20 @@ class GenericGraph(VMobject):
|
|||
),
|
||||
added_vertices,
|
||||
)
|
||||
return VGroup(*added_mobjects)
|
||||
return self.get_group_class()(*added_mobjects)
|
||||
|
||||
@override_animate(add_edges)
|
||||
def _add_edges_animation(
|
||||
self,
|
||||
*edges: tuple[Hashable, Hashable],
|
||||
anim_args: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AnimationGroup:
|
||||
# TODO: the animation is broken with introducer=False, but not passing it
|
||||
# disbands the graph upon re-adding the edges and vertices. Fix this
|
||||
def _add_edges_animation(self, *args, anim_args=None, **kwargs):
|
||||
if anim_args is None:
|
||||
anim_args = {}
|
||||
animation = anim_args.pop("animation", Create)
|
||||
|
||||
# Use introducer=False to prevent re-adding the edges and vertices when animating
|
||||
base_anim_args = {"animation": Create, "introducer": False}
|
||||
if anim_args is not None:
|
||||
base_anim_args.update(anim_args)
|
||||
animation = base_anim_args.pop("animation")
|
||||
|
||||
edge_and_vertex_mobjects = self.add_edges(*edges, **kwargs)
|
||||
mobjects = self.add_edges(*args, **kwargs)
|
||||
return AnimationGroup(
|
||||
*(
|
||||
animation(edge_or_vertex_mobject, **base_anim_args)
|
||||
for edge_or_vertex_mobject in edge_and_vertex_mobjects
|
||||
)
|
||||
*(animation(mobj, **anim_args) for mobj in mobjects), group=self
|
||||
)
|
||||
|
||||
def _remove_edge(self, edge: tuple[Hashable]) -> VMobject:
|
||||
def _remove_edge(self, edge: tuple[Hashable]):
|
||||
"""Remove an edge from the graph.
|
||||
|
||||
Parameters
|
||||
|
|
@ -1148,7 +1147,7 @@ class GenericGraph(VMobject):
|
|||
Returns
|
||||
-------
|
||||
|
||||
VMobject
|
||||
Mobject
|
||||
The removed edge.
|
||||
|
||||
"""
|
||||
|
|
@ -1163,7 +1162,7 @@ class GenericGraph(VMobject):
|
|||
self.remove(edge_mobject)
|
||||
return edge_mobject
|
||||
|
||||
def remove_edges(self, *edges: tuple[Hashable]) -> VGroup:
|
||||
def remove_edges(self, *edges: tuple[Hashable]):
|
||||
"""Remove several edges from the graph.
|
||||
|
||||
Parameters
|
||||
|
|
@ -1178,25 +1177,17 @@ class GenericGraph(VMobject):
|
|||
|
||||
"""
|
||||
edge_mobjects = [self._remove_edge(edge) for edge in edges]
|
||||
return VGroup(*edge_mobjects)
|
||||
return self.get_group_class()(*edge_mobjects)
|
||||
|
||||
@override_animate(remove_edges)
|
||||
def _remove_edges_animation(
|
||||
self, *edges: tuple[Hashable, Hashable], anim_args: dict[str, Any] | None = None
|
||||
) -> AnimationGroup:
|
||||
base_anim_args = {"animation": Uncreate}
|
||||
if anim_args is not None:
|
||||
base_anim_args.update(anim_args)
|
||||
animation = base_anim_args.pop("animation")
|
||||
def _remove_edges_animation(self, *edges, anim_args=None):
|
||||
if anim_args is None:
|
||||
anim_args = {}
|
||||
|
||||
edge_and_vertex_mobjects = self.remove_edges(*edges)
|
||||
return AnimationGroup(
|
||||
*(
|
||||
animation(edge_or_vertex_mobject, **anim_args)
|
||||
for edge_or_vertex_mobject in edge_and_vertex_mobjects
|
||||
),
|
||||
introducer=True, # Reintroduce edges and vertices temporarily to animate them
|
||||
)
|
||||
animation = anim_args.pop("animation", Uncreate)
|
||||
|
||||
mobjects = self.remove_edges(*edges)
|
||||
return AnimationGroup(*(animation(mobj, **anim_args) for mobj in mobjects))
|
||||
|
||||
@classmethod
|
||||
def from_networkx(
|
||||
|
|
@ -1373,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:
|
||||
|
|
@ -1568,7 +1564,7 @@ class Graph(GenericGraph):
|
|||
return nx.Graph()
|
||||
|
||||
def _populate_edge_dict(
|
||||
self, edges: list[tuple[Hashable, Hashable]], edge_type: type[VMobject]
|
||||
self, edges: list[tuple[Hashable, Hashable]], edge_type: type[Mobject]
|
||||
):
|
||||
self.edges = {
|
||||
(u, v): edge_type(
|
||||
|
|
@ -1593,9 +1589,6 @@ class Graph(GenericGraph):
|
|||
def __repr__(self: Graph) -> str:
|
||||
return f"Undirected graph on {len(self.vertices)} vertices and {len(self.edges)} edges"
|
||||
|
||||
def __str__(self: Graph) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
class DiGraph(GenericGraph):
|
||||
"""A directed graph.
|
||||
|
|
@ -1778,7 +1771,7 @@ class DiGraph(GenericGraph):
|
|||
return nx.DiGraph()
|
||||
|
||||
def _populate_edge_dict(
|
||||
self, edges: list[tuple[Hashable, Hashable]], edge_type: type[VMobject]
|
||||
self, edges: list[tuple[Hashable, Hashable]], edge_type: type[Mobject]
|
||||
):
|
||||
self.edges = {
|
||||
(u, v): edge_type(
|
||||
|
|
@ -1801,7 +1794,7 @@ class DiGraph(GenericGraph):
|
|||
"""
|
||||
for (u, v), edge in graph.edges.items():
|
||||
tip = edge.pop_tips()[0]
|
||||
# Passing the VMobject instead of the vertex makes the tip
|
||||
# Passing the Mobject instead of the vertex makes the tip
|
||||
# stop on the bounding box of the vertex.
|
||||
edge.set_points_by_ends(
|
||||
graph[u],
|
||||
|
|
@ -1813,6 +1806,3 @@ class DiGraph(GenericGraph):
|
|||
|
||||
def __repr__(self: DiGraph) -> str:
|
||||
return f"Directed graph on {len(self.vertices)} vertices and {len(self.edges)} edges"
|
||||
|
||||
def __str__(self: DiGraph) -> str:
|
||||
return self.__repr__()
|
||||
|
|
|
|||
|
|
@ -26,22 +26,17 @@ from manim.mobject.geometry.polygram import Polygon, Rectangle, RegularPolygon
|
|||
from manim.mobject.graphing.functions import ImplicitFunction, ParametricFunction
|
||||
from manim.mobject.graphing.number_line import NumberLine
|
||||
from manim.mobject.graphing.scale import LinearBase
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.opengl.opengl_surface import OpenGLSurface
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVDict as VDict,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVectorizedPoint as VectorizedPoint,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from manim.mobject.text.tex_mobject import MathTex
|
||||
from manim.mobject.three_d.three_dimensions import Surface
|
||||
from manim.mobject.types.vectorized_mobject import (
|
||||
VDict,
|
||||
VectorizedPoint,
|
||||
VGroup,
|
||||
VMobject,
|
||||
)
|
||||
from manim.utils.color import (
|
||||
BLACK,
|
||||
BLUE,
|
||||
|
|
@ -60,6 +55,7 @@ from manim.utils.simple_functions import binary_search
|
|||
from manim.utils.space_ops import angle_of_vector
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.typing import (
|
||||
ManimFloat,
|
||||
Point2D,
|
||||
|
|
@ -979,10 +975,10 @@ class CoordinateSystem:
|
|||
.. manim:: PlotSurfaceExample
|
||||
:save_last_frame:
|
||||
|
||||
class PlotSurfaceExample(Scene):
|
||||
class PlotSurfaceExample(ThreeDScene):
|
||||
def construct(self):
|
||||
resolution_fa = 16
|
||||
self.camera.set_orientation(theta=-60 * DEGREES, phi=75 * DEGREES)
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=-60 * DEGREES)
|
||||
axes = ThreeDAxes(x_range=(-3, 3, 1), y_range=(-3, 3, 1), z_range=(-5, 5, 1))
|
||||
def param_trig(u, v):
|
||||
x = u
|
||||
|
|
@ -998,8 +994,21 @@ class CoordinateSystem:
|
|||
)
|
||||
self.add(axes, trig_plane)
|
||||
"""
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
if config.renderer == RendererType.CAIRO:
|
||||
surface = Surface(
|
||||
lambda u, v: self.c2p(u, v, function(u, v)),
|
||||
u_range=u_range,
|
||||
v_range=v_range,
|
||||
**kwargs,
|
||||
)
|
||||
if colorscale:
|
||||
surface.set_fill_by_value(
|
||||
axes=self.copy(),
|
||||
colorscale=colorscale,
|
||||
axis=colorscale_axis,
|
||||
)
|
||||
elif config.renderer == RendererType.OPENGL:
|
||||
surface = OpenGLSurface(
|
||||
lambda u, v: self.c2p(u, v, function(u, v)),
|
||||
u_range=u_range,
|
||||
v_range=v_range,
|
||||
|
|
@ -1008,9 +1017,6 @@ class CoordinateSystem:
|
|||
colorscale_axis=colorscale_axis,
|
||||
**kwargs,
|
||||
)
|
||||
elif config.renderer == RendererType.CAIRO:
|
||||
# TODO: CairoSurface?
|
||||
raise NotImplementedError
|
||||
|
||||
return surface
|
||||
|
||||
|
|
@ -1867,7 +1873,7 @@ class CoordinateSystem:
|
|||
def _origin_shift(axis_range: Sequence[float]) -> float: ...
|
||||
|
||||
|
||||
class Axes(VGroup, CoordinateSystem):
|
||||
class Axes(VGroup, CoordinateSystem, metaclass=ConvertToOpenGL):
|
||||
"""Creates a set of axes.
|
||||
|
||||
Parameters
|
||||
|
|
@ -2506,7 +2512,6 @@ class ThreeDAxes(Axes):
|
|||
self.z_axis = z_axis
|
||||
|
||||
if config.renderer == RendererType.CAIRO:
|
||||
# TODO: check in how far these methods are supported by new VMobject class
|
||||
self._add_3d_pieces()
|
||||
self._set_axis_shading()
|
||||
|
||||
|
|
@ -2568,11 +2573,11 @@ class ThreeDAxes(Axes):
|
|||
.. manim:: GetYAxisLabelExample
|
||||
:save_last_frame:
|
||||
|
||||
class GetYAxisLabelExample(Scene):
|
||||
class GetYAxisLabelExample(ThreeDScene):
|
||||
def construct(self):
|
||||
ax = ThreeDAxes()
|
||||
lab = ax.get_y_axis_label(Tex("$y$-label"))
|
||||
self.camera.set_orientation(theta=PI/5, phi=2*PI/5)
|
||||
self.set_camera_orientation(phi=2*PI/5, theta=PI/5)
|
||||
self.add(ax, lab)
|
||||
"""
|
||||
positioned_label = self._get_axis_label(
|
||||
|
|
@ -2618,11 +2623,11 @@ class ThreeDAxes(Axes):
|
|||
.. manim:: GetZAxisLabelExample
|
||||
:save_last_frame:
|
||||
|
||||
class GetZAxisLabelExample(Scene):
|
||||
class GetZAxisLabelExample(ThreeDScene):
|
||||
def construct(self):
|
||||
ax = ThreeDAxes()
|
||||
lab = ax.get_z_axis_label(Tex("$z$-label"))
|
||||
self.camera.set_orientation(theta=PI/5, phi=2*PI/5)
|
||||
self.set_camera_orientation(phi=2*PI/5, theta=PI/5)
|
||||
self.add(ax, lab)
|
||||
"""
|
||||
positioned_label = self._get_axis_label(
|
||||
|
|
@ -2669,9 +2674,9 @@ class ThreeDAxes(Axes):
|
|||
.. manim:: GetAxisLabelsExample
|
||||
:save_last_frame:
|
||||
|
||||
class GetAxisLabelsExample(Scene):
|
||||
class GetAxisLabelsExample(ThreeDScene):
|
||||
def construct(self):
|
||||
self.camera.set_orientation(theta=PI/5, phi=2*PI/5)
|
||||
self.set_camera_orientation(phi=2*PI/5, theta=PI/5)
|
||||
axes = ThreeDAxes()
|
||||
labels = axes.get_axis_labels(
|
||||
Text("x-axis").scale(0.7), Text("y-axis").scale(0.45), Text("z-axis").scale(0.45)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ from isosurfaces import plot_isoline
|
|||
|
||||
from manim import config
|
||||
from manim.mobject.graphing.scale import LinearBase, _ScaleBase
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Self
|
||||
|
|
@ -24,7 +25,7 @@ if TYPE_CHECKING:
|
|||
from manim.utils.color import PURE_YELLOW
|
||||
|
||||
|
||||
class ParametricFunction(VMobject):
|
||||
class ParametricFunction(VMobject, metaclass=ConvertToOpenGL):
|
||||
"""A parametric curve.
|
||||
|
||||
Parameters
|
||||
|
|
@ -65,7 +66,7 @@ class ParametricFunction(VMobject):
|
|||
.. manim:: ThreeDParametricSpring
|
||||
:save_last_frame:
|
||||
|
||||
class ThreeDParametricSpring(Scene):
|
||||
class ThreeDParametricSpring(ThreeDScene):
|
||||
def construct(self):
|
||||
curve1 = ParametricFunction(
|
||||
lambda u: (
|
||||
|
|
@ -76,7 +77,7 @@ class ParametricFunction(VMobject):
|
|||
).set_shade_in_3d(True)
|
||||
axes = ThreeDAxes()
|
||||
self.add(axes, curve1)
|
||||
self.camera.set_orientation(theta=-60 * DEGREES, phi=80 * DEGREES)
|
||||
self.set_camera_orientation(phi=80 * DEGREES, theta=-60 * DEGREES)
|
||||
self.wait()
|
||||
|
||||
.. attention::
|
||||
|
|
@ -236,7 +237,7 @@ class FunctionGraph(ParametricFunction):
|
|||
return self.parametric_function(x)
|
||||
|
||||
|
||||
class ImplicitFunction(VMobject):
|
||||
class ImplicitFunction(VMobject, metaclass=ConvertToOpenGL):
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[float, float], float],
|
||||
|
|
|
|||
|
|
@ -2,17 +2,20 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from manim.mobject.mobject import Mobject
|
||||
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, 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
|
||||
|
||||
|
|
@ -20,16 +23,10 @@ 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.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from manim.mobject.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
|
||||
from manim.utils.space_ops import normalize
|
||||
|
|
@ -164,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,
|
||||
|
|
@ -453,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
|
||||
|
|
@ -490,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,
|
||||
|
|
@ -518,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
|
||||
|
|
@ -550,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:
|
||||
|
|
@ -574,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``.
|
||||
|
|
@ -612,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)
|
||||
|
|
@ -632,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
|
||||
|
|
@ -654,7 +655,7 @@ class NumberLine(Line):
|
|||
:class:`~.VMobject`
|
||||
The label.
|
||||
"""
|
||||
if isinstance(label_tex, VMobject):
|
||||
if isinstance(label_tex, (VMobject, OpenGLVMobject)):
|
||||
return label_tex
|
||||
if label_constructor is None:
|
||||
label_constructor = self.label_constructor
|
||||
|
|
|
|||
|
|
@ -14,14 +14,10 @@ from manim import config, logger
|
|||
from manim.constants import *
|
||||
from manim.mobject.geometry.polygram import Rectangle
|
||||
from manim.mobject.graphing.coordinate_systems import Axes
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
|
||||
from manim.mobject.svg.brace import Brace
|
||||
from manim.mobject.text.tex_mobject import MathTex, Tex
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.typing import Vector3D
|
||||
from manim.utils.color import (
|
||||
BLUE_E,
|
||||
|
|
@ -148,7 +144,7 @@ class SampleSpace(Rectangle):
|
|||
def get_subdivision_braces_and_labels(
|
||||
self,
|
||||
parts: VGroup,
|
||||
labels: list[str | VMobject],
|
||||
labels: list[str | VMobject | OpenGLVMobject],
|
||||
direction: Vector3D,
|
||||
buff: float = SMALL_BUFF,
|
||||
min_num_quads: int = 1,
|
||||
|
|
@ -157,7 +153,7 @@ class SampleSpace(Rectangle):
|
|||
braces = VGroup()
|
||||
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):
|
||||
if isinstance(label, (VMobject, OpenGLVMobject)):
|
||||
label_mob = label
|
||||
else:
|
||||
label_mob = MathTex(label)
|
||||
|
|
@ -178,7 +174,7 @@ class SampleSpace(Rectangle):
|
|||
|
||||
def get_side_braces_and_labels(
|
||||
self,
|
||||
labels: list[str | VMobject],
|
||||
labels: list[str | VMobject | OpenGLVMobject],
|
||||
direction: Vector3D = LEFT,
|
||||
**kwargs: Any,
|
||||
) -> VGroup:
|
||||
|
|
@ -189,14 +185,14 @@ class SampleSpace(Rectangle):
|
|||
)
|
||||
|
||||
def get_top_braces_and_labels(
|
||||
self, labels: list[str | VMobject], **kwargs: Any
|
||||
self, labels: list[str | VMobject | OpenGLVMobject], **kwargs: Any
|
||||
) -> VGroup:
|
||||
assert hasattr(self, "vertical_parts")
|
||||
parts = self.vertical_parts
|
||||
return self.get_subdivision_braces_and_labels(parts, labels, UP, **kwargs)
|
||||
|
||||
def get_bottom_braces_and_labels(
|
||||
self, labels: list[str | VMobject], **kwargs: Any
|
||||
self, labels: list[str | VMobject | OpenGLVMobject], **kwargs: Any
|
||||
) -> VGroup:
|
||||
assert hasattr(self, "vertical_parts")
|
||||
parts = self.vertical_parts
|
||||
|
|
@ -211,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]
|
||||
|
||||
|
||||
|
|
@ -377,7 +371,7 @@ class BarChart(Axes):
|
|||
# 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(
|
||||
|
|
|
|||
|
|
@ -13,9 +13,7 @@ from manim.mobject.text.numbers import Integer
|
|||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
|
||||
|
||||
class _ScaleBase:
|
||||
|
|
|
|||
|
|
@ -11,12 +11,7 @@ import svgelements as se
|
|||
from manim.animation.updaters.update import UpdateFromAlphaFunc
|
||||
from manim.mobject.geometry.arc import Circle
|
||||
from manim.mobject.geometry.polygram import Square, Triangle
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.typing import Vector3D
|
||||
|
||||
from .. import constants as cst
|
||||
|
|
@ -25,6 +20,7 @@ from ..animation.composition import AnimationGroup, Succession
|
|||
from ..animation.creation import Create, SpiralIn
|
||||
from ..animation.fading import FadeIn
|
||||
from ..mobject.svg.svg_mobject import VMobjectFromSVGPath
|
||||
from ..mobject.types.vectorized_mobject import VGroup
|
||||
from ..utils.rate_functions import ease_in_out_cubic, smooth
|
||||
|
||||
MANIM_SVG_PATHS: list[se.Path] = [
|
||||
|
|
@ -157,7 +153,7 @@ class ManimBanner(VGroup):
|
|||
self.scale_factor = 1.0
|
||||
|
||||
self.M = VMobjectFromSVGPath(MANIM_SVG_PATHS[0]).flip(cst.RIGHT).center()
|
||||
self.M.set_stroke(width=0).scale(
|
||||
self.M.set(stroke_width=0).scale(
|
||||
7 * cst.DEFAULT_FONT_SIZE * cst.SCALE_FACTOR_PER_FONT_POINT
|
||||
)
|
||||
self.M.set_fill(color=self.font_color, opacity=1).shift(
|
||||
|
|
@ -174,7 +170,7 @@ class ManimBanner(VGroup):
|
|||
anim = VGroup()
|
||||
for ind, path in enumerate(MANIM_SVG_PATHS[1:]):
|
||||
tex = VMobjectFromSVGPath(path).flip(cst.RIGHT).center()
|
||||
tex.set_stroke(width=0).scale(
|
||||
tex.set(stroke_width=0).scale(
|
||||
cst.DEFAULT_FONT_SIZE * cst.SCALE_FACTOR_PER_FONT_POINT
|
||||
)
|
||||
if ind > 0:
|
||||
|
|
@ -269,7 +265,7 @@ class ManimBanner(VGroup):
|
|||
)
|
||||
|
||||
"""
|
||||
if direction.lower() not in {"left", "right", "center"}:
|
||||
if direction not in ["left", "right", "center"]:
|
||||
raise ValueError("direction must be 'left', 'right' or 'center'.")
|
||||
|
||||
m_shape_offset = 6.25 * self.scale_factor
|
||||
|
|
@ -296,7 +292,7 @@ class ManimBanner(VGroup):
|
|||
elif direction == "left":
|
||||
left_group.shift(-vector)
|
||||
|
||||
def slide_and_uncover(mob: VMobject, alpha: float) -> None:
|
||||
def slide_and_uncover(mob: Mobject, alpha: float) -> None:
|
||||
shift(alpha * (m_shape_offset + shape_sliding_overshoot) * cst.RIGHT)
|
||||
|
||||
# Add letters when they are covered
|
||||
|
|
@ -309,11 +305,11 @@ class ManimBanner(VGroup):
|
|||
if alpha == 1:
|
||||
self.remove(*[self.anim])
|
||||
self.add_to_back(self.anim)
|
||||
mob.shapes.set_z(0)
|
||||
mob.shapes.set_z_index(0)
|
||||
mob.shapes.save_state()
|
||||
mob.M.save_state()
|
||||
|
||||
def slide_back(mob: VMobject, alpha: float) -> None:
|
||||
def slide_back(mob: Mobject, alpha: float) -> None:
|
||||
if alpha == 0:
|
||||
m_clone.set_opacity(1)
|
||||
m_clone.move_to(mob.anim[-1])
|
||||
|
|
@ -331,13 +327,11 @@ class ManimBanner(VGroup):
|
|||
slide_and_uncover,
|
||||
run_time=run_time * 2 / 3,
|
||||
rate_func=ease_in_out_cubic,
|
||||
introducer=True,
|
||||
),
|
||||
UpdateFromAlphaFunc(
|
||||
self,
|
||||
slide_back,
|
||||
run_time=run_time * 1 / 3,
|
||||
rate_func=smooth,
|
||||
introducer=True,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -40,22 +40,18 @@ __all__ = [
|
|||
|
||||
|
||||
import itertools as it
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Any, Self
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_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
|
||||
|
||||
# TO DO : The following two functions are not used in this file.
|
||||
# Not sure if we should keep it or not.
|
||||
|
|
@ -76,7 +72,7 @@ def matrix_to_mobject(matrix: np.ndarray) -> MathTex:
|
|||
return MathTex(matrix_to_tex_string(matrix))
|
||||
|
||||
|
||||
class Matrix(VMobject):
|
||||
class Matrix(VMobject, metaclass=ConvertToOpenGL):
|
||||
r"""A mobject that displays a matrix on the screen.
|
||||
|
||||
Parameters
|
||||
|
|
@ -168,16 +164,16 @@ class Matrix(VMobject):
|
|||
|
||||
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,
|
||||
|
|
@ -210,7 +206,9 @@ class Matrix(VMobject):
|
|||
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)
|
||||
|
|
@ -219,7 +217,7 @@ class Matrix(VMobject):
|
|||
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]
|
||||
|
|
@ -405,7 +403,7 @@ class Matrix(VMobject):
|
|||
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
|
||||
|
|
@ -487,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,
|
||||
):
|
||||
|
|
@ -532,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,
|
||||
):
|
||||
"""
|
||||
|
|
@ -570,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
File diff suppressed because it is too large
Load diff
|
|
@ -4,6 +4,7 @@ __all__ = ["OpenGLPMobject", "OpenGLPGroup", "OpenGLPMPoint"]
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import moderngl
|
||||
import numpy as np
|
||||
|
||||
from manim.constants import *
|
||||
|
|
@ -17,6 +18,7 @@ from manim.utils.color import (
|
|||
color_gradient,
|
||||
color_to_rgba,
|
||||
)
|
||||
from manim.utils.config_ops import _Uniforms
|
||||
from manim.utils.iterables import resize_with_interpolation
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -33,17 +35,25 @@ __all__ = ["OpenGLPMobject", "OpenGLPGroup", "OpenGLPMPoint"]
|
|||
|
||||
|
||||
class OpenGLPMobject(OpenGLMobject):
|
||||
shader_folder = "true_dot"
|
||||
# Scale for consistency with cairo units
|
||||
OPENGL_POINT_RADIUS_SCALE_FACTOR = 0.01
|
||||
shader_dtype = [
|
||||
("point", np.float32, (3,)),
|
||||
("color", np.float32, (4,)),
|
||||
]
|
||||
|
||||
point_radius = _Uniforms()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
stroke_width: float = 2.0,
|
||||
color: ParsableManimColor = PURE_YELLOW,
|
||||
render_primitive: int = moderngl.POINTS,
|
||||
**kwargs,
|
||||
):
|
||||
self.stroke_width = stroke_width
|
||||
super().__init__(color=color, **kwargs)
|
||||
super().__init__(color=color, render_primitive=render_primitive, **kwargs)
|
||||
self.point_radius = (
|
||||
self.stroke_width * OpenGLPMobject.OPENGL_POINT_RADIUS_SCALE_FACTOR
|
||||
)
|
||||
|
|
@ -138,26 +148,21 @@ class OpenGLPMobject(OpenGLMobject):
|
|||
def filter_out(self, condition):
|
||||
for mob in self.family_members_with_points():
|
||||
to_keep = ~np.apply_along_axis(condition, 1, mob.points)
|
||||
for attr_name in mob.get_array_attrs():
|
||||
array = getattr(mob, attr_name)
|
||||
filtered_array = array[to_keep]
|
||||
setattr(mob, attr_name, filtered_array)
|
||||
for key in mob.data:
|
||||
mob.data[key] = mob.data[key][to_keep]
|
||||
return self
|
||||
|
||||
def sort_points(self, function=lambda p: p[0]):
|
||||
"""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))
|
||||
for attr_name in mob.get_array_attrs():
|
||||
array = getattr(mob, attr_name)
|
||||
sorted_array = array[indices]
|
||||
setattr(mob, attr_name, sorted_array)
|
||||
for key in mob.data:
|
||||
mob.data[key] = mob.data[key][indices]
|
||||
return self
|
||||
|
||||
def ingest_submobjects(self):
|
||||
for attr_name in self.get_array_attrs():
|
||||
submob_arrays = [getattr(sm, attr_name) for sm in self.get_family()]
|
||||
setattr(self, attr_name, np.vstack(submob_arrays))
|
||||
for key in self.data:
|
||||
self.data[key] = np.vstack([sm.data[key] for sm in self.get_family()])
|
||||
return self
|
||||
|
||||
def point_from_proportion(self, alpha):
|
||||
|
|
@ -167,12 +172,16 @@ class OpenGLPMobject(OpenGLMobject):
|
|||
def pointwise_become_partial(self, pmobject, a, b):
|
||||
lower_index = int(a * pmobject.get_num_points())
|
||||
upper_index = int(b * pmobject.get_num_points())
|
||||
for attr_name in self.get_array_attrs():
|
||||
pmob_array = getattr(pmobject, attr_name)
|
||||
partial_pmob_array = pmob_array[lower_index:upper_index]
|
||||
setattr(self, attr_name, partial_pmob_array)
|
||||
for key in self.data:
|
||||
self.data[key] = pmobject.data[key][lower_index:upper_index]
|
||||
return self
|
||||
|
||||
def get_shader_data(self):
|
||||
shader_data = np.zeros(len(self.points), dtype=self.shader_dtype)
|
||||
self.read_data_to_shader(shader_data, "point", "points")
|
||||
self.read_data_to_shader(shader_data, "color", "rgbas")
|
||||
return shader_data
|
||||
|
||||
@staticmethod
|
||||
def get_mobject_type_class():
|
||||
return OpenGLPMobject
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from collections.abc import Iterable
|
|||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import moderngl
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
|
|
@ -11,6 +12,7 @@ from manim.constants import *
|
|||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.utils.bezier import integer_interpolate, interpolate
|
||||
from manim.utils.color import *
|
||||
from manim.utils.config_ops import _Data, _Uniforms
|
||||
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
|
||||
|
|
@ -23,7 +25,6 @@ if TYPE_CHECKING:
|
|||
__all__ = ["OpenGLSurface", "OpenGLTexturedSurface"]
|
||||
|
||||
|
||||
# TODO: Those will not work in the current state we will have to think about a different method to render these with shaders in our current pipeline
|
||||
class OpenGLSurface(OpenGLMobject):
|
||||
r"""Creates a Surface.
|
||||
|
||||
|
|
@ -56,6 +57,14 @@ class OpenGLSurface(OpenGLMobject):
|
|||
to 1 being fully opaque. Defaults to 1.
|
||||
"""
|
||||
|
||||
shader_dtype = [
|
||||
("point", np.float32, (3,)),
|
||||
("du_point", np.float32, (3,)),
|
||||
("dv_point", np.float32, (3,)),
|
||||
("color", np.float32, (4,)),
|
||||
]
|
||||
shader_folder = "surface"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
uv_func=None,
|
||||
|
|
@ -76,7 +85,9 @@ class OpenGLSurface(OpenGLMobject):
|
|||
# For du and dv steps. Much smaller and numerical error
|
||||
# can crop up in the shaders.
|
||||
epsilon=1e-5,
|
||||
render_primitive=moderngl.TRIANGLES,
|
||||
depth_test=True,
|
||||
shader_folder=None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self.passed_uv_func = uv_func
|
||||
|
|
@ -100,6 +111,8 @@ class OpenGLSurface(OpenGLMobject):
|
|||
opacity=opacity,
|
||||
gloss=gloss,
|
||||
shadow=shadow,
|
||||
shader_folder=shader_folder if shader_folder is not None else "surface",
|
||||
render_primitive=render_primitive,
|
||||
depth_test=depth_test,
|
||||
**kwargs,
|
||||
)
|
||||
|
|
@ -239,6 +252,106 @@ class OpenGLSurface(OpenGLMobject):
|
|||
tri_is[k::3] = tri_is[k::3][indices]
|
||||
return self
|
||||
|
||||
# For shaders
|
||||
def get_shader_data(self):
|
||||
"""Called by parent Mobject to calculate and return
|
||||
the shader data.
|
||||
|
||||
Returns
|
||||
-------
|
||||
shader_dtype
|
||||
An array containing the shader data (vertices and
|
||||
color of each vertex)
|
||||
"""
|
||||
s_points, du_points, dv_points = self.get_surface_points_and_nudged_points()
|
||||
shader_data = np.zeros(len(s_points), dtype=self.shader_dtype)
|
||||
if "points" not in self.locked_data_keys:
|
||||
shader_data["point"] = s_points
|
||||
shader_data["du_point"] = du_points
|
||||
shader_data["dv_point"] = dv_points
|
||||
if self.colorscale:
|
||||
if not hasattr(self, "color_by_val"):
|
||||
self.color_by_val = self._get_color_by_value(s_points)
|
||||
shader_data["color"] = self.color_by_val
|
||||
else:
|
||||
self.fill_in_shader_color_info(shader_data)
|
||||
return shader_data
|
||||
|
||||
def fill_in_shader_color_info(self, shader_data):
|
||||
"""Fills in the shader color data when the surface
|
||||
is all one color.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
shader_data
|
||||
The vertices of the surface.
|
||||
|
||||
Returns
|
||||
-------
|
||||
shader_dtype
|
||||
An array containing the shader data (vertices and
|
||||
color of each vertex)
|
||||
"""
|
||||
self.read_data_to_shader(shader_data, "color", "rgbas")
|
||||
return shader_data
|
||||
|
||||
def _get_color_by_value(self, s_points):
|
||||
"""Matches each vertex to a color associated to it's z-value.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
s_points
|
||||
The vertices of the surface.
|
||||
|
||||
Returns
|
||||
-------
|
||||
List
|
||||
A list of colors matching the vertex inputs.
|
||||
"""
|
||||
if type(self.colorscale[0]) in (list, tuple):
|
||||
new_colors, pivots = [
|
||||
[i for i, j in self.colorscale],
|
||||
[j for i, j in self.colorscale],
|
||||
]
|
||||
else:
|
||||
new_colors = self.colorscale
|
||||
|
||||
pivot_min = self.axes.z_range[0]
|
||||
pivot_max = self.axes.z_range[1]
|
||||
pivot_frequency = (pivot_max - pivot_min) / (len(new_colors) - 1)
|
||||
pivots = np.arange(
|
||||
start=pivot_min,
|
||||
stop=pivot_max + pivot_frequency,
|
||||
step=pivot_frequency,
|
||||
)
|
||||
|
||||
return_colors = []
|
||||
for point in s_points:
|
||||
axis_value = self.axes.point_to_coords(point)[self.colorscale_axis]
|
||||
if axis_value <= pivots[0]:
|
||||
return_colors.append(color_to_rgba(new_colors[0], self.opacity))
|
||||
elif axis_value >= pivots[-1]:
|
||||
return_colors.append(color_to_rgba(new_colors[-1], self.opacity))
|
||||
else:
|
||||
for i, pivot in enumerate(pivots):
|
||||
if pivot > axis_value:
|
||||
color_index = (axis_value - pivots[i - 1]) / (
|
||||
pivots[i] - pivots[i - 1]
|
||||
)
|
||||
color_index = max(min(color_index, 1), 0)
|
||||
temp_color = interpolate_color(
|
||||
new_colors[i - 1],
|
||||
new_colors[i],
|
||||
color_index,
|
||||
)
|
||||
break
|
||||
return_colors.append(color_to_rgba(temp_color, self.opacity))
|
||||
|
||||
return return_colors
|
||||
|
||||
def get_shader_vert_indices(self):
|
||||
return self.get_triangle_indices()
|
||||
|
||||
|
||||
class OpenGLSurfaceGroup(OpenGLSurface):
|
||||
def __init__(self, *parametric_surfaces, resolution=None, **kwargs):
|
||||
|
|
@ -251,14 +364,29 @@ class OpenGLSurfaceGroup(OpenGLSurface):
|
|||
|
||||
|
||||
class OpenGLTexturedSurface(OpenGLSurface):
|
||||
shader_dtype = [
|
||||
("point", np.float32, (3,)),
|
||||
("du_point", np.float32, (3,)),
|
||||
("dv_point", np.float32, (3,)),
|
||||
("im_coords", np.float32, (2,)),
|
||||
("opacity", np.float32, (1,)),
|
||||
]
|
||||
shader_folder = "textured_surface"
|
||||
im_coords = _Data()
|
||||
opacity = _Data()
|
||||
num_textures = _Uniforms()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
uv_surface: OpenGLSurface,
|
||||
image_file: str | Path | npt.NDArray,
|
||||
dark_image_file: str | Path = None,
|
||||
image_mode: str | Iterable[str] = "RGBA",
|
||||
shader_folder: str | Path = None,
|
||||
**kwargs,
|
||||
):
|
||||
self.uniforms = {}
|
||||
|
||||
if not isinstance(uv_surface, OpenGLSurface):
|
||||
raise Exception("uv_surface must be of type OpenGLSurface")
|
||||
if isinstance(image_file, np.ndarray):
|
||||
|
|
@ -268,8 +396,7 @@ class OpenGLTexturedSurface(OpenGLSurface):
|
|||
if isinstance(image_mode, (str, Path)):
|
||||
image_mode = [image_mode] * 2
|
||||
image_mode_light, image_mode_dark = image_mode
|
||||
# TODO: move to renderer
|
||||
_texture_paths = {
|
||||
texture_paths = {
|
||||
"LightTexture": self.get_image_from_file(
|
||||
image_file,
|
||||
image_mode_light,
|
||||
|
|
@ -288,7 +415,7 @@ class OpenGLTexturedSurface(OpenGLSurface):
|
|||
self.v_range = uv_surface.v_range
|
||||
self.resolution = uv_surface.resolution
|
||||
self.gloss = self.uv_surface.gloss
|
||||
super().__init__(**kwargs)
|
||||
super().__init__(texture_paths=texture_paths, **kwargs)
|
||||
|
||||
def get_image_from_file(
|
||||
self,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -12,8 +12,8 @@ import svgelements as se
|
|||
from manim._config import config
|
||||
from manim.mobject.geometry.arc import Arc
|
||||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.text.tex_mobject import MathTex, SingleStringMathTex, Tex
|
||||
from manim.mobject.text.text_mobject import Text
|
||||
|
||||
|
|
@ -22,6 +22,7 @@ from ...animation.composition import AnimationGroup
|
|||
from ...animation.fading import FadeIn
|
||||
from ...animation.growing import GrowFromCenter
|
||||
from ...constants import *
|
||||
from ...mobject.types.vectorized_mobject import VMobject
|
||||
from ...utils.color import BLACK
|
||||
from ..svg.svg_mobject import VMobjectFromSVGPath
|
||||
|
||||
|
|
@ -203,7 +204,7 @@ class Brace(VMobjectFromSVGPath):
|
|||
return vect / np.linalg.norm(vect)
|
||||
|
||||
|
||||
class BraceLabel(VMobject):
|
||||
class BraceLabel(VMobject, metaclass=ConvertToOpenGL):
|
||||
"""Create a brace with a label attached.
|
||||
|
||||
Parameters
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ import numpy as np
|
|||
import svgelements as se
|
||||
|
||||
from manim import config, logger
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
from manim.utils.color import ManimColor, ParsableManimColor
|
||||
|
||||
from ...constants import RIGHT
|
||||
from ...utils.bezier import get_quadratic_approximation_of_cubic
|
||||
|
|
@ -21,6 +20,8 @@ from ...utils.iterables import hash_obj
|
|||
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 VGroup, VMobject
|
||||
|
||||
__all__ = ["SVGMobject", "VMobjectFromSVGPath"]
|
||||
|
||||
|
|
@ -32,7 +33,7 @@ def _convert_point_to_3d(x: float, y: float) -> np.ndarray:
|
|||
return np.array([x, y, 0.0])
|
||||
|
||||
|
||||
class SVGMobject(VMobject):
|
||||
class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
||||
"""A vectorized mobject created from importing an SVG file.
|
||||
|
||||
Parameters
|
||||
|
|
@ -99,21 +100,36 @@ class SVGMobject(VMobject):
|
|||
should_center: bool = True,
|
||||
height: float | None = 2,
|
||||
width: float | None = None,
|
||||
color: ParsableManimColor | None = None,
|
||||
opacity: float | None = None,
|
||||
fill_color: ParsableManimColor | None = None,
|
||||
fill_opacity: float | None = None,
|
||||
stroke_color: ParsableManimColor | None = None,
|
||||
stroke_opacity: float | None = None,
|
||||
stroke_width: float | None = None,
|
||||
svg_default: dict | None = None,
|
||||
path_string_config: dict | None = None,
|
||||
use_svg_cache: bool = True,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
super().__init__(color=None, stroke_color=None, fill_color=None, **kwargs)
|
||||
|
||||
# process keyword arguments
|
||||
self.file_name = Path(file_name) if file_name is not None else None
|
||||
|
||||
self.should_center = should_center
|
||||
self.svg_height = height
|
||||
self.svg_width = width
|
||||
self.color = ManimColor(color)
|
||||
self.opacity = opacity
|
||||
self.fill_color = fill_color
|
||||
self.fill_opacity = fill_opacity # type: ignore[assignment]
|
||||
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
|
||||
|
||||
if svg_default is None:
|
||||
svg_default = {
|
||||
|
|
@ -121,20 +137,24 @@ class SVGMobject(VMobject):
|
|||
"opacity": None,
|
||||
"fill_color": None,
|
||||
"fill_opacity": None,
|
||||
"stroke_width": [0],
|
||||
"stroke_width": 0,
|
||||
"stroke_color": None,
|
||||
"stroke_opacity": None,
|
||||
}
|
||||
self.svg_default = svg_default
|
||||
|
||||
self.path_string_config = path_string_config or {}
|
||||
if path_string_config is None:
|
||||
path_string_config = {}
|
||||
self.path_string_config = path_string_config
|
||||
|
||||
self.init_svg_mobject(use_svg_cache=use_svg_cache)
|
||||
|
||||
self.set_style(
|
||||
fill_color=self.fill_color,
|
||||
stroke_color=self.stroke_color,
|
||||
stroke_width=self.stroke_width,
|
||||
fill_color=fill_color,
|
||||
fill_opacity=fill_opacity,
|
||||
stroke_color=stroke_color,
|
||||
stroke_opacity=stroke_opacity,
|
||||
stroke_width=stroke_width,
|
||||
)
|
||||
self.move_into_position()
|
||||
|
||||
|
|
@ -477,7 +497,7 @@ class SVGMobject(VMobject):
|
|||
self.set(width=self.svg_width)
|
||||
|
||||
|
||||
class VMobjectFromSVGPath(VMobject):
|
||||
class VMobjectFromSVGPath(VMobject, metaclass=ConvertToOpenGL):
|
||||
"""A vectorized mobject representing an SVG path.
|
||||
|
||||
.. note::
|
||||
|
|
@ -527,12 +547,13 @@ class VMobjectFromSVGPath(VMobject):
|
|||
|
||||
self.handle_commands()
|
||||
|
||||
if self.should_subdivide_sharp_curves:
|
||||
# For a healthy triangulation later
|
||||
self.subdivide_sharp_curves()
|
||||
if self.should_remove_null_curves:
|
||||
# Get rid of any null curves
|
||||
self.set_points(self.get_points_without_null_curves())
|
||||
if config.renderer == "opengl":
|
||||
if self.should_subdivide_sharp_curves:
|
||||
# For a healthy triangulation later
|
||||
self.subdivide_sharp_curves()
|
||||
if self.should_remove_null_curves:
|
||||
# Get rid of any null curves
|
||||
self.set_points(self.get_points_without_null_curves())
|
||||
|
||||
def init_points(self) -> None:
|
||||
self.generate_points()
|
||||
|
|
|
|||
|
|
@ -70,12 +70,6 @@ from collections.abc import Callable, Iterable, Sequence
|
|||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.geometry.polygram import Polygon
|
||||
from manim.mobject.geometry.shape_matchers import BackgroundRectangle
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from manim.mobject.text.numbers import DecimalNumber, Integer
|
||||
from manim.mobject.text.tex_mobject import MathTex
|
||||
from manim.mobject.text.text_mobject import Paragraph
|
||||
|
|
@ -84,7 +78,9 @@ from ..animation.animation import Animation
|
|||
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, PURE_YELLOW, ManimColor, ParsableManimColor
|
||||
from .utils import get_vectorized_mobject_class
|
||||
|
||||
|
||||
class Table(VGroup):
|
||||
|
|
@ -108,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
|
||||
|
|
@ -197,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,
|
||||
|
|
@ -218,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
|
||||
|
|
@ -327,7 +327,8 @@ class Table(VGroup):
|
|||
mob_table.insert(0, col_labels)
|
||||
else:
|
||||
# Placeholder to use arrange_in_grid if top_left_entry is not set.
|
||||
dummy_mobject = VMobject()
|
||||
# Import OpenGLVMobject to work with --renderer=opengl
|
||||
dummy_mobject = get_vectorized_mobject_class()()
|
||||
col_labels = [dummy_mobject] + self.col_labels
|
||||
mob_table.insert(0, col_labels)
|
||||
else:
|
||||
|
|
@ -352,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
|
||||
|
||||
|
|
@ -382,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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -19,17 +19,13 @@ from pygments.styles import get_all_styles
|
|||
from manim.constants import *
|
||||
from manim.mobject.geometry.arc import Dot
|
||||
from manim.mobject.geometry.shape_matchers import SurroundingRectangle
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.typing import StrPath
|
||||
from manim.utils.color import WHITE, ManimColor
|
||||
|
||||
|
||||
class Code(VMobject):
|
||||
class Code(VMobject, metaclass=ConvertToOpenGL):
|
||||
"""A highlighted source code listing.
|
||||
|
||||
Examples
|
||||
|
|
@ -143,7 +139,10 @@ class Code(VMobject):
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -10,16 +10,17 @@ import numpy as np
|
|||
|
||||
from manim import config
|
||||
from manim.constants import *
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
|
||||
from manim.mobject.text.tex_mobject import MathTex, SingleStringMathTex, Tex
|
||||
from manim.mobject.text.text_mobject import Text
|
||||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
from manim.mobject.value_tracker import ValueTracker
|
||||
from manim.typing import Vector3DLike
|
||||
|
||||
string_to_mob_map: dict[str, SingleStringMathTex] = {}
|
||||
|
||||
|
||||
class DecimalNumber(VMobject):
|
||||
class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
|
||||
r"""An mobject representing a decimal number.
|
||||
|
||||
Parameters
|
||||
|
|
@ -153,8 +154,6 @@ class DecimalNumber(VMobject):
|
|||
|
||||
def _set_submobjects_from_number(self, number: float) -> None:
|
||||
self.number = number
|
||||
# the self.add below will recalculate the family,
|
||||
# no need to do it here.
|
||||
self.submobjects = []
|
||||
|
||||
num_string = self._get_num_string(number)
|
||||
|
|
@ -342,7 +341,7 @@ class Integer(DecimalNumber):
|
|||
return int(np.round(super().get_value()))
|
||||
|
||||
|
||||
class Variable(VMobject):
|
||||
class Variable(VMobject, metaclass=ConvertToOpenGL):
|
||||
"""A class for displaying text that shows "label = value" with
|
||||
the value continuously updated from a :class:`~.ValueTracker`.
|
||||
|
||||
|
|
|
|||
|
|
@ -33,12 +33,13 @@ from typing import Any, Self
|
|||
from manim import config, logger
|
||||
from manim.constants import *
|
||||
from manim.mobject.geometry.line import Line
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject as VMobject
|
||||
from manim.mobject.svg.svg_mobject import SVGMobject
|
||||
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"
|
||||
|
||||
|
||||
|
|
@ -66,12 +67,15 @@ class SingleStringMathTex(SVGMobject):
|
|||
color: ParsableManimColor | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
if color is None:
|
||||
color = VMobject().color
|
||||
|
||||
self._font_size = font_size
|
||||
self.organize_left_to_right = organize_left_to_right
|
||||
self.tex_environment = tex_environment
|
||||
if tex_template is None:
|
||||
tex_template = config.tex_template
|
||||
self.tex_template = tex_template
|
||||
tex_template = config["tex_template"]
|
||||
self.tex_template: TexTemplate = tex_template
|
||||
|
||||
self.tex_string = tex_string
|
||||
file_name = tex_to_svg_file(
|
||||
|
|
@ -308,7 +312,7 @@ class MathTex(SingleStringMathTex):
|
|||
# Save the original tex_string
|
||||
self.tex_string = self.arg_separator.join(self.tex_strings)
|
||||
self._break_up_by_substrings()
|
||||
except ValueError:
|
||||
except ValueError as compilation_error:
|
||||
if self.brace_notation_split_occurred:
|
||||
logger.error(
|
||||
dedent(
|
||||
|
|
@ -322,7 +326,7 @@ class MathTex(SingleStringMathTex):
|
|||
""",
|
||||
),
|
||||
)
|
||||
raise
|
||||
raise compilation_error
|
||||
self.set_color_by_tex_to_color_map(self.tex_to_color_map)
|
||||
|
||||
if self.organize_left_to_right:
|
||||
|
|
@ -530,12 +534,6 @@ class MathTex(SingleStringMathTex):
|
|||
)
|
||||
new_submobjects.append(self.id_to_vgroup_dict["root"])
|
||||
self.submobjects = new_submobjects
|
||||
|
||||
# 5 hours of work went into this line
|
||||
# and it's still not perfect
|
||||
# July 18, 2024
|
||||
self.note_changed_family()
|
||||
|
||||
return self
|
||||
|
||||
def get_part_by_tex(self, tex: str, **kwargs: Any) -> VGroup | None:
|
||||
|
|
@ -589,7 +587,7 @@ class MathTex(SingleStringMathTex):
|
|||
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")
|
||||
|
|
@ -597,10 +595,9 @@ class MathTex(SingleStringMathTex):
|
|||
|
||||
def sort_alphabetically(self) -> None:
|
||||
self.submobjects.sort(key=lambda m: m.get_tex_string())
|
||||
self.note_changed_family()
|
||||
|
||||
|
||||
class MathTexPart(VMobject):
|
||||
class MathTexPart(VMobject, metaclass=ConvertToOpenGL):
|
||||
tex_string: str
|
||||
|
||||
def __repr__(self) -> str:
|
||||
|
|
|
|||
|
|
@ -70,16 +70,14 @@ from manimpango import MarkupUtils, PangoUtils, TextSetting
|
|||
from manim import config, logger
|
||||
from manim.constants import *
|
||||
from manim.mobject.geometry.arc import Dot
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVGroup as VGroup,
|
||||
)
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from manim.mobject.svg.svg_mobject import SVGMobject
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.typing import Point3D
|
||||
from manim.utils.color import ManimColor, ParsableManimColor, color_gradient
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Self
|
||||
|
||||
from manim.typing import Point3D
|
||||
|
||||
TEXT_MOB_SCALE_FACTOR = 0.05
|
||||
|
|
@ -168,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)
|
||||
|
|
@ -223,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
|
||||
|
||||
|
|
@ -242,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],
|
||||
)
|
||||
|
|
@ -257,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
|
||||
|
||||
|
|
@ -271,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(
|
||||
[
|
||||
|
|
@ -286,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(
|
||||
[
|
||||
|
|
@ -512,7 +513,7 @@ class Text(SVGMobject):
|
|||
else:
|
||||
self.line_spacing = self._font_size + self._font_size * self.line_spacing
|
||||
|
||||
parsed_color = ManimColor(color)
|
||||
parsed_color: ManimColor = ManimColor(color) if color else VMobject().color
|
||||
file_name = self._text2svg(parsed_color.to_hex())
|
||||
PangoUtils.remove_last_M(file_name)
|
||||
super().__init__(
|
||||
|
|
@ -528,7 +529,6 @@ class Text(SVGMobject):
|
|||
self.text = text
|
||||
if self.disable_ligatures:
|
||||
self.submobjects = [*self._gen_chars()]
|
||||
self.note_changed_family()
|
||||
self.chars = self.get_group_class()(*self.submobjects)
|
||||
self.text = text_without_tabs.replace(" ", "").replace("\n", "")
|
||||
nppc = self.n_points_per_curve
|
||||
|
|
@ -591,11 +591,6 @@ class Text(SVGMobject):
|
|||
# anti-aliasing
|
||||
if height is None and width is None:
|
||||
self.scale(TEXT_MOB_SCALE_FACTOR)
|
||||
|
||||
# Just a temporary hack to get better triangulation
|
||||
# See pr #1552 for details
|
||||
for i in self.submobjects:
|
||||
i.insert_n_curves(len(i.get_all_points()))
|
||||
self.initial_height = self.height
|
||||
|
||||
def __repr__(self) -> str:
|
||||
|
|
@ -835,6 +830,13 @@ class Text(SVGMobject):
|
|||
|
||||
return svg_file
|
||||
|
||||
def init_colors(self, propagate_colors: bool = True) -> Self:
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
super().init_colors()
|
||||
elif config.renderer == RendererType.CAIRO:
|
||||
super().init_colors(propagate_colors=propagate_colors)
|
||||
return self
|
||||
|
||||
|
||||
class MarkupText(SVGMobject):
|
||||
r"""Display (non-LaTeX) text rendered using `Pango <https://pango.org/>`_.
|
||||
|
|
@ -1209,7 +1211,7 @@ class MarkupText(SVGMobject):
|
|||
else:
|
||||
self.line_spacing = self._font_size + self._font_size * self.line_spacing
|
||||
|
||||
parsed_color = ManimColor(color)
|
||||
parsed_color: ManimColor = ManimColor(color) if color else VMobject().color
|
||||
file_name = self._text2svg(parsed_color)
|
||||
|
||||
PangoUtils.remove_last_M(file_name)
|
||||
|
|
|
|||
818
manim/mobject/text/typst_mobject.py
Normal file
818
manim/mobject/text/typst_mobject.py
Normal 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
|
||||
|
|
@ -9,12 +9,12 @@ import numpy as np
|
|||
|
||||
from manim.mobject.geometry.polygram import Polygon
|
||||
from manim.mobject.graph import Graph
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup as VGroup
|
||||
from manim.mobject.three_d.three_dimensions import Dot3D
|
||||
from manim.mobject.types.vectorized_mobject import VGroup
|
||||
from manim.utils.qhull import QuickHull
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.typing import Point3D, Point3DLike_Array
|
||||
|
||||
__all__ = [
|
||||
|
|
@ -52,9 +52,9 @@ class Polyhedron(VGroup):
|
|||
.. manim:: SquarePyramidScene
|
||||
:save_last_frame:
|
||||
|
||||
class SquarePyramidScene(Scene):
|
||||
class SquarePyramidScene(ThreeDScene):
|
||||
def construct(self):
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
vertex_coords = [
|
||||
[1, 1, 0],
|
||||
[1, -1, 0],
|
||||
|
|
@ -86,9 +86,9 @@ class Polyhedron(VGroup):
|
|||
.. manim:: PolyhedronSubMobjects
|
||||
:save_last_frame:
|
||||
|
||||
class PolyhedronSubMobjects(Scene):
|
||||
class PolyhedronSubMobjects(ThreeDScene):
|
||||
def construct(self):
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
octahedron = Octahedron(edge_length = 3)
|
||||
octahedron.graph[0].set_color(RED)
|
||||
octahedron.faces[2].set_color(YELLOW)
|
||||
|
|
@ -173,9 +173,9 @@ class Tetrahedron(Polyhedron):
|
|||
.. manim:: TetrahedronScene
|
||||
:save_last_frame:
|
||||
|
||||
class TetrahedronScene(Scene):
|
||||
class TetrahedronScene(ThreeDScene):
|
||||
def construct(self):
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
obj = Tetrahedron()
|
||||
self.add(obj)
|
||||
"""
|
||||
|
|
@ -208,9 +208,9 @@ class Octahedron(Polyhedron):
|
|||
.. manim:: OctahedronScene
|
||||
:save_last_frame:
|
||||
|
||||
class OctahedronScene(Scene):
|
||||
class OctahedronScene(ThreeDScene):
|
||||
def construct(self):
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
obj = Octahedron()
|
||||
self.add(obj)
|
||||
"""
|
||||
|
|
@ -254,9 +254,9 @@ class Icosahedron(Polyhedron):
|
|||
.. manim:: IcosahedronScene
|
||||
:save_last_frame:
|
||||
|
||||
class IcosahedronScene(Scene):
|
||||
class IcosahedronScene(ThreeDScene):
|
||||
def construct(self):
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
obj = Icosahedron()
|
||||
self.add(obj)
|
||||
"""
|
||||
|
|
@ -319,9 +319,9 @@ class Dodecahedron(Polyhedron):
|
|||
.. manim:: DodecahedronScene
|
||||
:save_last_frame:
|
||||
|
||||
class DodecahedronScene(Scene):
|
||||
class DodecahedronScene(ThreeDScene):
|
||||
def construct(self):
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
obj = Dodecahedron()
|
||||
self.add(obj)
|
||||
"""
|
||||
|
|
@ -389,9 +389,9 @@ class ConvexHull3D(Polyhedron):
|
|||
:save_last_frame:
|
||||
:quality: high
|
||||
|
||||
class ConvexHull3DExample(Scene):
|
||||
class ConvexHull3DExample(ThreeDScene):
|
||||
def construct(self):
|
||||
self.camera.set_orientation(theta=30 * DEGREES, phi=75 * DEGREES)
|
||||
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
|
||||
points = [
|
||||
[ 1.93192757, 0.44134585, -1.52407061],
|
||||
[-0.93302521, 1.23206983, 0.64117067],
|
||||
|
|
|
|||
|
|
@ -22,11 +22,10 @@ from manim.constants import ORIGIN, UP
|
|||
from manim.utils.space_ops import get_unit_normal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import (
|
||||
OpenGLVMobject as VMobject,
|
||||
)
|
||||
from manim.typing import Point3D, Vector3D
|
||||
|
||||
from ..types.vectorized_mobject import VMobject
|
||||
|
||||
|
||||
def get_3d_vmob_gradient_start_and_end_points(
|
||||
vmob: VMobject,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue