mirror of
https://github.com/3b1b/manim.git
synced 2025-08-05 16:49:03 +00:00
Merge branch '3b1b:master' into master
This commit is contained in:
commit
23662d093f
51 changed files with 688 additions and 390 deletions
4
.github/workflows/docs.yml
vendored
4
.github/workflows/docs.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
|||
- name: Install sphinx and manim env
|
||||
run: |
|
||||
pip3 install --upgrade pip
|
||||
sudo apt install python3-setuptools
|
||||
sudo apt install python3-setuptools libpango1.0-dev
|
||||
pip3 install -r docs/requirements.txt
|
||||
pip3 install -r requirements.txt
|
||||
|
||||
|
@ -37,4 +37,4 @@ jobs:
|
|||
with:
|
||||
ACCESS_TOKEN: ${{ secrets.DOC_DEPLOY_TOKEN }}
|
||||
BRANCH: gh-pages
|
||||
FOLDER: docs/build/html
|
||||
FOLDER: docs/build/html
|
||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -15,6 +15,8 @@ __pycache__/
|
|||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
manimlib.egg-info/
|
||||
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
|
@ -147,4 +149,4 @@ dmypy.json
|
|||
|
||||
# For manim
|
||||
/videos
|
||||
/custom_config.yml
|
||||
/custom_config.yml
|
||||
|
|
61
README.md
61
README.md
|
@ -4,27 +4,37 @@
|
|||
</a>
|
||||
</p>
|
||||
|
||||
[](https://pypi.org/project/manimgl/)
|
||||
[](http://choosealicense.com/licenses/mit/)
|
||||
[](https://www.reddit.com/r/manim/)
|
||||
[](https://discord.gg/mMRrZQW)
|
||||
[](https://www.reddit.com/r/manim/)
|
||||
[](https://discord.gg/mMRrZQW)
|
||||
[](https://3b1b.github.io/manim/)
|
||||
|
||||
Manim is an engine for precise programatic animations, designed for creating explanatory math videos.
|
||||
Manim is an engine for precise programmatic animations, designed for creating explanatory math videos.
|
||||
|
||||
Note, there are two versions of manim. This repository began as a personal project by the author of [3Blue1Brown](https://www.3blue1brown.com/) for the purpose of animating those videos, with video-specific code available [here](https://github.com/3b1b/videos). In 2020 a group of devlopers forked it into what is now the [community edition](https://github.com/ManimCommunity/manim/), with a goal of being more stable, better tested, quicker to respond to community contributions, and all around friendlier to get started with. You can engage with that community by joining the discord.
|
||||
Note, there are two versions of manim. This repository began as a personal project by the author of [3Blue1Brown](https://www.3blue1brown.com/) for the purpose of animating those videos, with video-specific code available [here](https://github.com/3b1b/videos). In 2020 a group of developers forked it into what is now the [community edition](https://github.com/ManimCommunity/manim/), with a goal of being more stable, better tested, quicker to respond to community contributions, and all around friendlier to get started with. You can engage with that community by joining the discord.
|
||||
|
||||
Since the fork, this version has evolved to work on top of OpenGL, and allows real-time rendering to an interactive window before scenes are finalized and written to a file.
|
||||
|
||||
## Installation
|
||||
Manim runs on Python 3.6 or higher (Python 3.8 is recommended).
|
||||
|
||||
System requirements are [FFmpeg](https://ffmpeg.org/), [OpenGL](https://www.opengl.org//), [LaTeX](https://www.latex-project.org) (optional, if you want to use LaTeX)
|
||||
and [cairo](https://www.cairographics.org/) (optional, if you want to use Text).
|
||||
System requirements are [FFmpeg](https://ffmpeg.org/), [OpenGL](https://www.opengl.org/) and [LaTeX](https://www.latex-project.org) (optional, if you want to use LaTeX).
|
||||
For Linux, [Pango](https://pango.gnome.org) along with its development headers are required. See instruction [here](https://github.com/ManimCommunity/ManimPango#building).
|
||||
|
||||
For more options, take a look at the [Using manim](#using-manim) sections further below.
|
||||
|
||||
### Directly
|
||||
|
||||
```sh
|
||||
# Install manimgl
|
||||
pip install manimgl
|
||||
|
||||
# Try it out
|
||||
manimgl
|
||||
```
|
||||
|
||||
For more options, take a look at the [Using manim](#using-manim) sections further below.
|
||||
|
||||
If you want to hack on manimlib itself, clone this repository and in that directory execute:
|
||||
|
||||
```sh
|
||||
|
@ -36,21 +46,9 @@ manimgl example_scenes.py OpeningManimExample
|
|||
# or
|
||||
manim-render example_scenes.py OpeningManimExample
|
||||
```
|
||||
### Mac OSX
|
||||
1. Install FFmpeg, LaTeX, Cairo in terminal using homebrew.
|
||||
```sh
|
||||
brew install ffmpeg mactex cairo
|
||||
```
|
||||
|
||||
2. Install latest version of manim using these command.
|
||||
```sh
|
||||
git clone https://github.com/3b1b/manim.git
|
||||
cd manim
|
||||
pip install -e .
|
||||
manimgl example_scenes.py OpeningManimExample
|
||||
```
|
||||
|
||||
### Directly (Windows)
|
||||
|
||||
1. [Install FFmpeg](https://www.wikihow.com/Install-FFmpeg-on-Windows).
|
||||
2. Install a LaTeX distribution. [MiKTeX](https://miktex.org/download) is recommended.
|
||||
3. Install the remaining Python packages.
|
||||
|
@ -61,12 +59,27 @@ manim-render example_scenes.py OpeningManimExample
|
|||
manimgl example_scenes.py OpeningManimExample
|
||||
```
|
||||
|
||||
### Mac OSX
|
||||
|
||||
1. Install FFmpeg, LaTeX in terminal using homebrew.
|
||||
```sh
|
||||
brew install ffmpeg mactex
|
||||
```
|
||||
|
||||
2. Install latest version of manim using these command.
|
||||
```sh
|
||||
git clone https://github.com/3b1b/manim.git
|
||||
cd manim
|
||||
pip install -e .
|
||||
manimgl example_scenes.py OpeningManimExample
|
||||
```
|
||||
|
||||
## Anaconda Install
|
||||
|
||||
* Install LaTeX as above.
|
||||
* Create a conda environment using `conda create -n manim python=3.8`.
|
||||
* Activate the environment using `conda activate manim`.
|
||||
* Install manimgl using `pip install -e .`.
|
||||
1. Install LaTeX as above.
|
||||
2. Create a conda environment using `conda create -n manim python=3.8`.
|
||||
3. Activate the environment using `conda activate manim`.
|
||||
4. Install manimgl using `pip install -e .`.
|
||||
|
||||
|
||||
## Using manim
|
||||
|
|
|
@ -16,7 +16,7 @@ faster rendering speed, and supports real-time rendering and interaction.
|
|||
|
||||
- `ManimCommunity/manim <https://github.com/ManimCommunity/manim>`_ : Maintained by Manim Community Dev Team.
|
||||
|
||||
Using cairo to use CPU for rendering. There is better documentation and
|
||||
Using multiple backend rendering. There is better documentation and
|
||||
a more open contribution community.
|
||||
|
||||
About this documentation
|
||||
|
|
|
@ -1,4 +1,44 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
No changes now.
|
||||
Unreleased
|
||||
----------
|
||||
|
||||
Fixed bugs
|
||||
^^^^^^^^^^
|
||||
|
||||
- Fixed the bug of :func:`~manimlib.utils.iterables.resize_with_interpolation` in the case of ``length=0``
|
||||
- Fixed the bug of ``__init__`` in :class:`~manimlib.mobject.geometry.Elbow`
|
||||
- If chosen monitor is not available, choose one that does exist
|
||||
- Make sure mobject data gets unlocked after animations
|
||||
- Fixed a bug for off-center vector fields
|
||||
- Had ``Mobject.match_points`` return self
|
||||
- Fixed chaining animation in example scenes
|
||||
- Fixed the default color of tip
|
||||
- Fixed a typo in ``ShowPassingFlashWithThinningStrokeWidth``
|
||||
|
||||
New Features
|
||||
^^^^^^^^^^^^
|
||||
|
||||
- Added :class:`~manimlib.animation.indication.VShowPassingFlash`
|
||||
- Added ``COLORMAP_3B1B``
|
||||
- Added some methods to coordinate system to access all axes ranges
|
||||
|
||||
- :meth:`~manimlib.mobject.coordinate_systems.CoordinateSystem.get_origin`
|
||||
- :meth:`~manimlib.mobject.coordinate_systems.CoordinateSystem.get_all_ranges`
|
||||
- Added :meth:`~manimlib.mobject.mobject.Mobject.set_color_by_rgba_func`
|
||||
- Updated :class:`~manimlib.mobject.vector_field.VectorField` and :class:`~manimlib.mobject.vector_field.StreamLines`
|
||||
- Allow ``3b1b_colormap`` as an option for :func:`~manimlib.utils.color.get_colormap_list`
|
||||
- Return ``stroke_width`` as 1d array
|
||||
- Added :meth:`~manimlib.mobject.svg.text_mobject.Text.get_parts_by_text`
|
||||
- Use Text not TexText for Brace
|
||||
- Update to Cross to make it default to variable stroke width
|
||||
- Added :class:`~manimlib.animation.indication.FlashAround` and :class:`~manimlib.animation.indication.FlashUnder`
|
||||
- Allowed configuration in ``Brace.get_text``
|
||||
- Added :meth:`~manimlib.camera.camera.CameraFrame.reorient` for quicker changes to frame angle
|
||||
- Added ``units`` to :meth:`~manimlib.camera.camera.CameraFrame.set_euler_angles`
|
||||
- Allowed any ``VMobject`` to be passed into ``TransformMatchingTex``
|
||||
- Removed double brace convention in ``Tex`` and ``TexText``
|
||||
- Added support for debugger launch
|
||||
- Added CLI flag ``--config_file`` to load configuration file manually
|
||||
- Added ``tip_style`` to ``tip_config``
|
|
@ -56,7 +56,7 @@ custom_config
|
|||
|
||||
- ``raster_images``
|
||||
The directory for storing raster images to be used in the code (including
|
||||
``.jpg``, ``.png`` and ``.gif``), which will be read by ``ImageMobject``.
|
||||
``.jpg``, ``.jpeg``, ``.png`` and ``.gif``), which will be read by ``ImageMobject``.
|
||||
|
||||
- ``vector_images``
|
||||
The directory for storing vector images to be used in the code (including
|
||||
|
@ -108,6 +108,11 @@ The relative position of the playback window on the display (two characters,
|
|||
the first character means upper(U) / middle(O) / lower(D), the second character
|
||||
means left(L) / middle(O) / right(R)).
|
||||
|
||||
``window_monitor``
|
||||
------------------
|
||||
|
||||
The number of the monitor you want the preview window to pop up on. (default is 0)
|
||||
|
||||
``break_into_partial_movies``
|
||||
-----------------------------
|
||||
|
||||
|
|
|
@ -58,6 +58,7 @@ flag abbr function
|
|||
``--color COLOR`` ``-c`` Background color
|
||||
``--leave_progress_bars`` Leave progress bars displayed in terminal
|
||||
``--video_dir VIDEO_DIR`` directory to write video
|
||||
``--config_file CONFIG_FILE`` Path to the custom configuration file
|
||||
========================================================== ====== =================================================================================================================================================================================================
|
||||
|
||||
custom_config
|
||||
|
@ -85,5 +86,11 @@ following the directory structure:
|
|||
└── custom_config.yml
|
||||
|
||||
When you enter the ``project/`` folder and run ``manimgl code.py <Scene>``,
|
||||
it will overwrite ``manim/custom_config.yml`` with ``custom_config.yml``
|
||||
in the ``project`` folder.
|
||||
it will overwrite ``manim/default_config.yml`` with ``custom_config.yml``
|
||||
in the ``project`` folder.
|
||||
|
||||
Alternatively, you can use ``--config_file`` flag in CLI to specify configuration file manually.
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
manimgl project/code.py --config_file /path/to/custom_config.yml
|
|
@ -8,12 +8,12 @@ the simplest and one by one.
|
|||
InteractiveDevlopment
|
||||
---------------------
|
||||
|
||||
.. manim-example:: InteractiveDevlopment
|
||||
:media: ../_static/example_scenes/InteractiveDevlopment.mp4
|
||||
.. manim-example:: InteractiveDevelopment
|
||||
:media: ../_static/example_scenes/InteractiveDevelopment.mp4
|
||||
|
||||
from manimlib import *
|
||||
|
||||
class InteractiveDevlopment(Scene):
|
||||
class InteractiveDevelopment(Scene):
|
||||
def construct(self):
|
||||
circle = Circle()
|
||||
circle.set_fill(BLUE, opacity=0.5)
|
||||
|
@ -128,6 +128,8 @@ TextExample
|
|||
|
||||
class TextExample(Scene):
|
||||
def construct(self):
|
||||
# To run this scene properly, you should have "Consolas" font in your computer
|
||||
# for full usage, you can see https://github.com/3b1b/manim/pull/680
|
||||
text = Text("Here is a text", font="Consolas", font_size=90)
|
||||
difference = Text(
|
||||
"""
|
||||
|
@ -135,6 +137,7 @@ TextExample
|
|||
you can change the font more easily, but can't use the LaTeX grammar
|
||||
""",
|
||||
font="Arial", font_size=24,
|
||||
# t2c is a dict that you can choose color for different text
|
||||
t2c={"Text": BLUE, "TexText": BLUE, "LaTeX": ORANGE}
|
||||
)
|
||||
VGroup(text, difference).arrange(DOWN, buff=1)
|
||||
|
@ -148,6 +151,7 @@ TextExample
|
|||
t2f={"font": "Consolas", "words": "Consolas"},
|
||||
t2c={"font": BLUE, "words": GREEN}
|
||||
)
|
||||
fonts.set_width(FRAME_WIDTH - 1)
|
||||
slant = Text(
|
||||
"And the same as slant and weight",
|
||||
font="Consolas",
|
||||
|
@ -180,20 +184,24 @@ TexTransformExample
|
|||
def construct(self):
|
||||
to_isolate = ["B", "C", "=", "(", ")"]
|
||||
lines = VGroup(
|
||||
# Surrounding substrings with double braces
|
||||
# will ensure that those parts are separated
|
||||
# out in the Tex. For example, here the
|
||||
# Tex will have 5 submobjects, corresponding
|
||||
# to the strings [A^2, +, B^2, =, C^2]
|
||||
Tex("{{A^2}} + {{B^2}} = {{C^2}}"),
|
||||
Tex("{{A^2}} = {{C^2}} - {{B^2}}"),
|
||||
# Passing in muliple arguments to Tex will result
|
||||
# in the same expression as if those arguments had
|
||||
# been joined together, except that the submobject
|
||||
# heirarchy of the resulting mobject ensure that the
|
||||
# Tex mobject has a subject corresponding to
|
||||
# each of these strings. For example, the Tex mobject
|
||||
# below will have 5 subjects, corresponding to the
|
||||
# expressions [A^2, +, B^2, =, C^2]
|
||||
Tex("A^2", "+", "B^2", "=", "C^2"),
|
||||
# Likewise here
|
||||
Tex("A^2", "=", "C^2", "-", "B^2"),
|
||||
# Alternatively, you can pass in the keyword argument
|
||||
# "isolate" with a list of strings that should be out as
|
||||
# their own submobject. So both lines below are equivalent
|
||||
# to what you'd get by wrapping every instance of "B", "C"
|
||||
# "=", "(" and ")" with double braces
|
||||
Tex("{{A^2}} = (C + B)(C - B)", isolate=to_isolate),
|
||||
Tex("A = \\sqrt{(C + B)(C - B)}", isolate=to_isolate)
|
||||
# their own submobject. So the line below is equivalent
|
||||
# to the commented out line below it.
|
||||
Tex("A^2 = (C + B)(C - B)", isolate=["A^2", *to_isolate]),
|
||||
# Tex("A^2", "=", "(", "C", "+", "B", ")", "(", "C", "-", "B", ")"),
|
||||
Tex("A = \\sqrt{(C + B)(C - B)}", isolate=["A", *to_isolate])
|
||||
)
|
||||
lines.arrange(DOWN, buff=LARGE_BUFF)
|
||||
for line in lines:
|
||||
|
@ -252,7 +260,7 @@ TexTransformExample
|
|||
# new_line2 and the "\sqrt" from the final line. By passing in,
|
||||
# transform_mismatches=True, it will transform this "^2" part into
|
||||
# the "\sqrt" part.
|
||||
new_line2 = Tex("{{A}}^2 = (C + B)(C - B)", isolate=to_isolate)
|
||||
new_line2 = Tex("A^2 = (C + B)(C - B)", isolate=["A", *to_isolate])
|
||||
new_line2.replace(lines[2])
|
||||
new_line2.match_style(lines[2])
|
||||
|
||||
|
@ -343,7 +351,7 @@ UpdatersExample
|
|||
)
|
||||
self.wait()
|
||||
self.play(
|
||||
square.set_width(5, stretch=True),
|
||||
square.animate.set_width(5, stretch=True),
|
||||
run_time=3,
|
||||
)
|
||||
self.wait()
|
||||
|
@ -387,7 +395,7 @@ CoordinateSystemExample
|
|||
axes = Axes(
|
||||
# x-axis ranges from -1 to 10, with a default step size of 1
|
||||
x_range=(-1, 10),
|
||||
# y-axis ranges from -2 to 10 with a step size of 0.5
|
||||
# y-axis ranges from -2 to 2 with a step size of 0.5
|
||||
y_range=(-2, 2, 0.5),
|
||||
# The axes will be stretched so as to match the specified
|
||||
# height and width
|
||||
|
@ -450,8 +458,7 @@ CoordinateSystemExample
|
|||
# system defined by them.
|
||||
f_always(dot.move_to, lambda: axes.c2p(1, 1))
|
||||
self.play(
|
||||
axes.animate.scale(0.75),
|
||||
axes.animate.to_corner(UL),
|
||||
axes.animate.scale(0.75).to_corner(UL),
|
||||
run_time=2,
|
||||
)
|
||||
self.wait()
|
||||
|
|
|
@ -1,18 +1,27 @@
|
|||
Installation
|
||||
============
|
||||
|
||||
Manim runs on Python 3.8.
|
||||
Manim runs on Python 3.6 or higher (Python 3.8 is recommended).
|
||||
|
||||
System requirements are:
|
||||
|
||||
- `FFmpeg <https://ffmpeg.org/>`__
|
||||
- `OpenGL <https://www.opengl.org//>`__ (included in python package ``PyOpenGL``)
|
||||
- `LaTeX <https://www.latex-project.org>`__ (optional, if you want to use LaTeX)
|
||||
- `cairo <https://www.cairographics.org/>`_ (included in python package ``pycairo``. optional, if you want to use ``Text`` in manim)
|
||||
- `Pango <https://pango.org>`__ (only for Linux)
|
||||
|
||||
|
||||
Directly
|
||||
--------
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
# Install manimgl
|
||||
pip install manimgl
|
||||
|
||||
# Try it out
|
||||
manimgl
|
||||
|
||||
If you want to hack on manimlib itself, clone this repository and in
|
||||
that directory execute:
|
||||
|
||||
|
@ -57,4 +66,4 @@ For Anaconda
|
|||
cd manim
|
||||
conda create -n manim python=3.8
|
||||
conda activate manim
|
||||
pip install -e .
|
||||
pip install -e .
|
||||
|
|
|
@ -221,7 +221,7 @@ For example: input the following lines (without comment lines) into it respectiv
|
|||
.. code-block:: python
|
||||
|
||||
# Stretched 4 times in the vertical direction
|
||||
play(circle.animate.stretch(4, dim=0}))
|
||||
play(circle.animate.stretch(4, dim=0))
|
||||
# Rotate the ellipse 90°
|
||||
play(Rotate(circle, TAU / 4))
|
||||
# Move 2 units to the right and shrink to 1/4 of the original
|
||||
|
|
|
@ -59,7 +59,7 @@ Below is the directory structure of manim:
|
|||
│ │ ├── brace.py # Brace
|
||||
│ │ ├── drawings.py # Some special mobject of svg image
|
||||
│ │ ├── tex_mobject.py # Tex and TexText implemented by LaTeX
|
||||
│ │ └── text_mobject.py # Text implemented by cairo
|
||||
│ │ └── text_mobject.py # Text implemented by manimpango
|
||||
│ ├── changing.py # Dynamically changing mobject
|
||||
│ ├── coordinate_systems.py # coordinate system
|
||||
│ ├── frame.py # mobject related to frame
|
||||
|
@ -99,6 +99,7 @@ Below is the directory structure of manim:
|
|||
├── config_ops.py # Process CONFIG
|
||||
├── customization.py # Read from custom_config.yml
|
||||
├── debug.py # Utilities for debugging in program
|
||||
├── directories.py # Read directories from config file
|
||||
├── family_ops.py # Process family members
|
||||
├── file_ops.py # Process files and directories
|
||||
├── images.py # Read image
|
||||
|
|
|
@ -6,7 +6,7 @@ Manim's documentation
|
|||
Manim is an animation engine for explanatory math videos. It's used to create precise animations programmatically, as seen in the videos
|
||||
at `3Blue1Brown <https://www.3blue1brown.com/>`_.
|
||||
|
||||
And here is a Chinese version of this documentation: https://manim.ml/shaders
|
||||
And here is a Chinese version of this documentation: https://manim.wiki/shaders
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
|
|
@ -161,20 +161,24 @@ class TexTransformExample(Scene):
|
|||
def construct(self):
|
||||
to_isolate = ["B", "C", "=", "(", ")"]
|
||||
lines = VGroup(
|
||||
# Surrounding substrings with double braces
|
||||
# will ensure that those parts are separated
|
||||
# out in the Tex. For example, here the
|
||||
# Tex will have 5 submobjects, corresponding
|
||||
# to the strings [A^2, +, B^2, =, C^2]
|
||||
Tex("{{A^2}} + {{B^2}} = {{C^2}}"),
|
||||
Tex("{{A^2}} = {{C^2}} - {{B^2}}"),
|
||||
# Passing in muliple arguments to Tex will result
|
||||
# in the same expression as if those arguments had
|
||||
# been joined together, except that the submobject
|
||||
# heirarchy of the resulting mobject ensure that the
|
||||
# Tex mobject has a subject corresponding to
|
||||
# each of these strings. For example, the Tex mobject
|
||||
# below will have 5 subjects, corresponding to the
|
||||
# expressions [A^2, +, B^2, =, C^2]
|
||||
Tex("A^2", "+", "B^2", "=", "C^2"),
|
||||
# Likewise here
|
||||
Tex("A^2", "=", "C^2", "-", "B^2"),
|
||||
# Alternatively, you can pass in the keyword argument
|
||||
# "isolate" with a list of strings that should be out as
|
||||
# their own submobject. So both lines below are equivalent
|
||||
# to what you'd get by wrapping every instance of "B", "C"
|
||||
# "=", "(" and ")" with double braces
|
||||
Tex("{{A^2}} = (C + B)(C - B)", isolate=to_isolate),
|
||||
Tex("A = \\sqrt{(C + B)(C - B)}", isolate=to_isolate)
|
||||
# their own submobject. So the line below is equivalent
|
||||
# to the commented out line below it.
|
||||
Tex("A^2 = (C + B)(C - B)", isolate=["A^2", *to_isolate]),
|
||||
# Tex("A^2", "=", "(", "C", "+", "B", ")", "(", "C", "-", "B", ")"),
|
||||
Tex("A = \\sqrt{(C + B)(C - B)}", isolate=["A", *to_isolate])
|
||||
)
|
||||
lines.arrange(DOWN, buff=LARGE_BUFF)
|
||||
for line in lines:
|
||||
|
@ -233,7 +237,7 @@ class TexTransformExample(Scene):
|
|||
# new_line2 and the "\sqrt" from the final line. By passing in,
|
||||
# transform_mismatches=True, it will transform this "^2" part into
|
||||
# the "\sqrt" part.
|
||||
new_line2 = Tex("{{A}}^2 = (C + B)(C - B)", isolate=to_isolate)
|
||||
new_line2 = Tex("A^2 = (C + B)(C - B)", isolate=["A", *to_isolate])
|
||||
new_line2.replace(lines[2])
|
||||
new_line2.match_style(lines[2])
|
||||
|
||||
|
@ -311,7 +315,7 @@ class UpdatersExample(Scene):
|
|||
)
|
||||
self.wait()
|
||||
self.play(
|
||||
square.set_width(5, stretch=True),
|
||||
square.animate.set_width(5, stretch=True),
|
||||
run_time=3,
|
||||
)
|
||||
self.wait()
|
||||
|
@ -338,7 +342,7 @@ class CoordinateSystemExample(Scene):
|
|||
axes = Axes(
|
||||
# x-axis ranges from -1 to 10, with a default step size of 1
|
||||
x_range=(-1, 10),
|
||||
# y-axis ranges from -2 to 10 with a step size of 0.5
|
||||
# y-axis ranges from -2 to 2 with a step size of 0.5
|
||||
y_range=(-2, 2, 0.5),
|
||||
# The axes will be stretched so as to match the specified
|
||||
# height and width
|
||||
|
@ -401,8 +405,7 @@ class CoordinateSystemExample(Scene):
|
|||
# system defined by them.
|
||||
f_always(dot.move_to, lambda: axes.c2p(1, 1))
|
||||
self.play(
|
||||
axes.animate.scale(0.75),
|
||||
axes.animate.to_corner(UL),
|
||||
axes.animate.scale(0.75).to_corner(UL),
|
||||
run_time=2,
|
||||
)
|
||||
self.wait()
|
||||
|
@ -584,7 +587,7 @@ class SurfaceExample(Scene):
|
|||
self.wait()
|
||||
|
||||
|
||||
class InteractiveDevlopment(Scene):
|
||||
class InteractiveDevelopment(Scene):
|
||||
def construct(self):
|
||||
circle = Circle()
|
||||
circle.set_fill(BLUE, opacity=0.5)
|
||||
|
|
|
@ -6,12 +6,15 @@ import manimlib.utils.init_config
|
|||
|
||||
def main():
|
||||
args = manimlib.config.parse_cli()
|
||||
|
||||
|
||||
if args.config:
|
||||
manimlib.utils.init_config.init_customization()
|
||||
else:
|
||||
config = manimlib.config.get_configuration(args)
|
||||
scenes = manimlib.extract_scene.main(config)
|
||||
|
||||
|
||||
for scene in scenes:
|
||||
scene.run()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
@ -25,6 +25,10 @@ class ShowPartial(Animation):
|
|||
if not self.should_match_start:
|
||||
self.mobject.lock_matching_data(self.mobject, self.starting_mobject)
|
||||
|
||||
def finish(self):
|
||||
super().finish()
|
||||
self.mobject.unlock_data()
|
||||
|
||||
def interpolate_submobject(self, submob, start_submob, alpha):
|
||||
submob.pointwise_become_partial(
|
||||
start_submob, *self.get_bounds(alpha)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import numpy as np
|
||||
import math
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.animation.animation import Animation
|
||||
|
@ -13,6 +14,7 @@ from manimlib.mobject.types.vectorized_mobject import VMobject
|
|||
from manimlib.mobject.geometry import Circle
|
||||
from manimlib.mobject.geometry import Dot
|
||||
from manimlib.mobject.shape_matchers import SurroundingRectangle
|
||||
from manimlib.mobject.shape_matchers import Underline
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.utils.bezier import interpolate
|
||||
|
@ -152,6 +154,87 @@ class ShowPassingFlash(ShowPartial):
|
|||
submob.pointwise_become_partial(start, 0, 1)
|
||||
|
||||
|
||||
class VShowPassingFlash(Animation):
|
||||
CONFIG = {
|
||||
"time_width": 0.3,
|
||||
"taper_width": 0.02,
|
||||
"remover": True,
|
||||
}
|
||||
|
||||
def begin(self):
|
||||
self.mobject.align_stroke_width_data_to_points()
|
||||
# Compute an array of stroke widths for each submobject
|
||||
# which tapers out at either end
|
||||
self.submob_to_anchor_widths = dict()
|
||||
for sm in self.mobject.get_family():
|
||||
original_widths = sm.get_stroke_widths()
|
||||
anchor_widths = np.array([*original_widths[0::3], original_widths[-1]])
|
||||
|
||||
def taper_kernel(x):
|
||||
if x < self.taper_width:
|
||||
return x
|
||||
elif x > 1 - self.taper_width:
|
||||
return 1.0 - x
|
||||
return 1.0
|
||||
|
||||
taper_array = list(map(taper_kernel, np.linspace(0, 1, len(anchor_widths))))
|
||||
self.submob_to_anchor_widths[hash(sm)] = anchor_widths * taper_array
|
||||
super().begin()
|
||||
|
||||
def interpolate_submobject(self, submobject, starting_sumobject, alpha):
|
||||
anchor_widths = self.submob_to_anchor_widths[hash(submobject)]
|
||||
# Create a gaussian such that 3 sigmas out on either side
|
||||
# will equals time_width
|
||||
tw = self.time_width
|
||||
sigma = tw / 6
|
||||
mu = interpolate(-tw / 2, 1 + tw / 2, alpha)
|
||||
|
||||
def gauss_kernel(x):
|
||||
if abs(x - mu) > 3 * sigma:
|
||||
return 0
|
||||
z = (x - mu) / sigma
|
||||
return math.exp(-0.5 * z * z)
|
||||
|
||||
kernel_array = list(map(gauss_kernel, np.linspace(0, 1, len(anchor_widths))))
|
||||
scaled_widths = anchor_widths * kernel_array
|
||||
new_widths = np.zeros(submobject.get_num_points())
|
||||
new_widths[0::3] = scaled_widths[:-1]
|
||||
new_widths[2::3] = scaled_widths[1:]
|
||||
new_widths[1::3] = (new_widths[0::3] + new_widths[2::3]) / 2
|
||||
submobject.set_stroke(width=new_widths)
|
||||
|
||||
def finish(self):
|
||||
super().finish()
|
||||
for submob, start in self.get_all_families_zipped():
|
||||
submob.match_style(start)
|
||||
|
||||
|
||||
class FlashAround(VShowPassingFlash):
|
||||
CONFIG = {
|
||||
"stroke_width": 4.0,
|
||||
"color": YELLOW,
|
||||
"buff": SMALL_BUFF,
|
||||
"time_width": 1.0,
|
||||
"n_inserted_curves": 20,
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
path = self.get_path(mobject)
|
||||
path.insert_n_curves(self.n_inserted_curves)
|
||||
path.set_points(path.get_points_without_null_curves())
|
||||
path.set_stroke(self.color, self.stroke_width)
|
||||
super().__init__(path, **kwargs)
|
||||
|
||||
def get_path(self, mobject):
|
||||
return SurroundingRectangle(mobject, buff=self.buff)
|
||||
|
||||
|
||||
class FlashUnder(FlashAround):
|
||||
def get_path(self, mobject):
|
||||
return Underline(mobject, buff=self.buff)
|
||||
|
||||
|
||||
class ShowCreationThenDestruction(ShowPassingFlash):
|
||||
CONFIG = {
|
||||
"time_width": 2.0,
|
||||
|
|
|
@ -10,7 +10,6 @@ from manimlib.mobject.mobject import Mobject
|
|||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
|
||||
|
||||
|
@ -129,7 +128,7 @@ class TransformMatchingShapes(TransformMatchingParts):
|
|||
|
||||
class TransformMatchingTex(TransformMatchingParts):
|
||||
CONFIG = {
|
||||
"mobject_type": Tex,
|
||||
"mobject_type": VMobject,
|
||||
"group_type": VGroup,
|
||||
}
|
||||
|
||||
|
|
|
@ -77,16 +77,24 @@ class CameraFrame(Mobject):
|
|||
self.set_euler_angles(theta, phi, gamma)
|
||||
return self
|
||||
|
||||
def set_euler_angles(self, theta=None, phi=None, gamma=None):
|
||||
def set_euler_angles(self, theta=None, phi=None, gamma=None, units=RADIANS):
|
||||
if theta is not None:
|
||||
self.data["euler_angles"][0] = theta
|
||||
self.data["euler_angles"][0] = theta * units
|
||||
if phi is not None:
|
||||
self.data["euler_angles"][1] = phi
|
||||
self.data["euler_angles"][1] = phi * units
|
||||
if gamma is not None:
|
||||
self.data["euler_angles"][2] = gamma
|
||||
self.data["euler_angles"][2] = gamma * units
|
||||
self.refresh_rotation_matrix()
|
||||
return self
|
||||
|
||||
def reorient(self, theta_degrees=None, phi_degrees=None, gamma_degrees=None):
|
||||
"""
|
||||
Shortcut for set_euler_angles, defaulting to taking
|
||||
in angles in degrees
|
||||
"""
|
||||
self.set_euler_angles(theta_degrees, phi_degrees, gamma_degrees, units=DEGREES)
|
||||
return self
|
||||
|
||||
def set_theta(self, theta):
|
||||
return self.set_euler_angles(theta=theta)
|
||||
|
||||
|
|
|
@ -130,7 +130,11 @@ def parse_cli():
|
|||
)
|
||||
parser.add_argument(
|
||||
"--video_dir",
|
||||
help="directory to write video",
|
||||
help="Directory to write video",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config_file",
|
||||
help="Path to the custom configuration file",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
@ -155,17 +159,19 @@ def get_module(file_name):
|
|||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
__config_file__ = "custom_config.yml"
|
||||
|
||||
def get_custom_config():
|
||||
filename = "custom_config.yml"
|
||||
global __config_file__
|
||||
|
||||
global_defaults_file = os.path.join(get_manim_dir(), "manimlib", "default_config.yml")
|
||||
|
||||
if os.path.exists(global_defaults_file):
|
||||
with open(global_defaults_file, "r") as file:
|
||||
config = yaml.safe_load(file)
|
||||
|
||||
if os.path.exists(filename):
|
||||
with open(filename, "r") as file:
|
||||
if os.path.exists(__config_file__):
|
||||
with open(__config_file__, "r") as file:
|
||||
local_defaults = yaml.safe_load(file)
|
||||
if local_defaults:
|
||||
config = merge_dicts_recursively(
|
||||
|
@ -173,22 +179,41 @@ def get_custom_config():
|
|||
local_defaults,
|
||||
)
|
||||
else:
|
||||
with open(filename, "r") as file:
|
||||
with open(__config_file__, "r") as file:
|
||||
config = yaml.safe_load(file)
|
||||
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def get_configuration(args):
|
||||
local_config_file = "custom_config.yml"
|
||||
global __config_file__
|
||||
|
||||
# ensure __config_file__ always exists
|
||||
if args.config_file is not None:
|
||||
if not os.path.exists(args.config_file):
|
||||
print(f"Can't find {args.config_file}.")
|
||||
if sys.platform == 'win32':
|
||||
print(f"Copying default configuration file to {args.config_file}...")
|
||||
os.system(f"copy default_config.yml {args.config_file}")
|
||||
elif sys.platform in ["linux2", "darwin"]:
|
||||
print(f"Copying default configuration file to {args.config_file}...")
|
||||
os.system(f"cp default_config.yml {args.config_file}")
|
||||
else:
|
||||
print("Please create the configuration file manually.")
|
||||
print("Read configuration from default_config.yml.")
|
||||
else:
|
||||
__config_file__ = args.config_file
|
||||
|
||||
global_defaults_file = os.path.join(get_manim_dir(), "manimlib", "default_config.yml")
|
||||
if not (os.path.exists(global_defaults_file) or os.path.exists(local_config_file)):
|
||||
|
||||
if not (os.path.exists(global_defaults_file) or os.path.exists(__config_file__)):
|
||||
print("There is no configuration file detected. Initial configuration:\n")
|
||||
init_customization()
|
||||
elif not os.path.exists(local_config_file):
|
||||
print(f"""Warning: Using the default configuration file, which you can modify in {global_defaults_file}
|
||||
If you want to create a local configuration file, you can create a file named {local_config_file}, or run manimgl --config
|
||||
""")
|
||||
|
||||
elif not os.path.exists(__config_file__):
|
||||
print(f"Warning: Using the default configuration file, which you can modify in {global_defaults_file}")
|
||||
print(f"If you want to create a local configuration file, you can create a file named {__config_file__}, or run manimgl --config")
|
||||
|
||||
custom_config = get_custom_config()
|
||||
|
||||
write_file = any([args.write_file, args.open, args.finder])
|
||||
|
@ -234,7 +259,9 @@ def get_configuration(args):
|
|||
|
||||
# Default to making window half the screen size
|
||||
# but make it full screen if -f is passed in
|
||||
monitor = get_monitors()[custom_config["window_monitor"]]
|
||||
monitors = get_monitors()
|
||||
mon_index = custom_config["window_monitor"]
|
||||
monitor = monitors[min(mon_index, len(monitors) - 1)]
|
||||
window_width = monitor.width
|
||||
if not args.full_screen:
|
||||
window_width //= 2
|
||||
|
|
|
@ -50,6 +50,9 @@ RIGHT_SIDE = FRAME_X_RADIUS * RIGHT
|
|||
PI = np.pi
|
||||
TAU = 2 * PI
|
||||
DEGREES = TAU / 360
|
||||
# Nice to have a constant for readability
|
||||
# when juxtaposed with expressions like 30 * DEGREES
|
||||
RADIANS = 1
|
||||
|
||||
FFMPEG_BIN = "ffmpeg"
|
||||
|
||||
|
@ -136,3 +139,5 @@ RED = RED_C
|
|||
MAROON = MAROON_C
|
||||
PURPLE = PURPLE_C
|
||||
GREY = GREY_C
|
||||
|
||||
COLORMAP_3B1B = [BLUE_E, GREEN, YELLOW, RED]
|
||||
|
|
|
@ -25,8 +25,8 @@ class CoordinateSystem():
|
|||
"""
|
||||
CONFIG = {
|
||||
"dimension": 2,
|
||||
"x_range": np.array([-8, 8, 1.0]),
|
||||
"y_range": np.array([-4, 4, 1.0]),
|
||||
"x_range": np.array([-8.0, 8.0, 1.0]),
|
||||
"y_range": np.array([-4.0, 4.0, 1.0]),
|
||||
"width": None,
|
||||
"height": None,
|
||||
"num_sampled_graph_points_per_tick": 5,
|
||||
|
@ -46,9 +46,15 @@ class CoordinateSystem():
|
|||
"""Abbreviation for point_to_coords"""
|
||||
return self.point_to_coords(point)
|
||||
|
||||
def get_origin(self):
|
||||
return self.c2p(*[0] * self.dimension)
|
||||
|
||||
def get_axes(self):
|
||||
raise Exception("Not implemented")
|
||||
|
||||
def get_all_ranges(self):
|
||||
raise Exception("Not implemented")
|
||||
|
||||
def get_axis(self, index):
|
||||
return self.get_axes()[index]
|
||||
|
||||
|
@ -319,6 +325,9 @@ class Axes(VGroup, CoordinateSystem):
|
|||
def get_axes(self):
|
||||
return self.axes
|
||||
|
||||
def get_all_ranges(self):
|
||||
return [self.x_range, self.y_range]
|
||||
|
||||
def add_coordinate_labels(self,
|
||||
x_values=None,
|
||||
y_values=None,
|
||||
|
@ -334,11 +343,13 @@ class Axes(VGroup, CoordinateSystem):
|
|||
class ThreeDAxes(Axes):
|
||||
CONFIG = {
|
||||
"dimension": 3,
|
||||
"x_range": np.array([-6, 6, 1]),
|
||||
"y_range": np.array([-5, 5, 1]),
|
||||
"z_range": np.array([-4, 4, 1]),
|
||||
"x_range": np.array([-6.0, 6.0, 1.0]),
|
||||
"y_range": np.array([-5.0, 5.0, 1.0]),
|
||||
"z_range": np.array([-4.0, 4.0, 1.0]),
|
||||
"z_axis_config": {},
|
||||
"z_normal": DOWN,
|
||||
"height": None,
|
||||
"width": None,
|
||||
"depth": None,
|
||||
"num_axis_pieces": 20,
|
||||
"gloss": 0.5,
|
||||
|
@ -346,9 +357,11 @@ class ThreeDAxes(Axes):
|
|||
|
||||
def __init__(self, x_range=None, y_range=None, z_range=None, **kwargs):
|
||||
Axes.__init__(self, x_range, y_range, **kwargs)
|
||||
if z_range is not None:
|
||||
self.z_range[:len(z_range)] = z_range
|
||||
|
||||
z_axis = self.create_axis(
|
||||
z_range or self.z_range,
|
||||
self.z_range,
|
||||
self.z_axis_config,
|
||||
self.depth,
|
||||
)
|
||||
|
@ -365,6 +378,9 @@ class ThreeDAxes(Axes):
|
|||
for axis in self.axes:
|
||||
axis.insert_n_curves(self.num_axis_pieces - 1)
|
||||
|
||||
def get_all_ranges(self):
|
||||
return [self.x_range, self.y_range, self.z_range]
|
||||
|
||||
|
||||
class NumberPlane(Axes):
|
||||
CONFIG = {
|
||||
|
|
|
@ -20,6 +20,9 @@ class ScreenRectangle(Rectangle):
|
|||
class FullScreenRectangle(ScreenRectangle):
|
||||
CONFIG = {
|
||||
"height": FRAME_HEIGHT,
|
||||
"fill_color": GREY_E,
|
||||
"fill_opacity": 1,
|
||||
"stroke_width": 0,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ class TipableVMobject(VMobject):
|
|||
"tip_config": {
|
||||
"fill_opacity": 1,
|
||||
"stroke_width": 0,
|
||||
"tip_style": 0, # triangle=0, inner_smooth=1, dot=2
|
||||
},
|
||||
"normal_vector": OUT,
|
||||
}
|
||||
|
@ -63,6 +64,7 @@ class TipableVMobject(VMobject):
|
|||
tip = self.create_tip(at_start, **kwargs)
|
||||
self.reset_endpoints_based_on_tip(tip, at_start)
|
||||
self.asign_tip_attr(tip, at_start)
|
||||
tip.set_color(self.get_stroke_color())
|
||||
self.add(tip)
|
||||
return self
|
||||
|
||||
|
@ -569,7 +571,7 @@ class Elbow(VMobject):
|
|||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(self, **kwargs)
|
||||
super().__init__(**kwargs)
|
||||
self.set_points_as_corners([UP, UP + RIGHT, RIGHT])
|
||||
self.set_width(self.width, about_point=ORIGIN)
|
||||
self.rotate(self.angle, about_point=ORIGIN)
|
||||
|
@ -786,12 +788,20 @@ class ArrowTip(Triangle):
|
|||
"width": DEFAULT_ARROW_TIP_WIDTH,
|
||||
"length": DEFAULT_ARROW_TIP_LENGTH,
|
||||
"angle": 0,
|
||||
"tip_style": 0, # triangle=0, inner_smooth=1, dot=2
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
Triangle.__init__(self, start_angle=0, **kwargs)
|
||||
self.set_height(self.width)
|
||||
self.set_width(self.length, stretch=True)
|
||||
if self.tip_style == 1:
|
||||
self.set_height(self.length * 0.9, stretch=True)
|
||||
self.data["points"][4] += np.array([0.6 * self.length, 0, 0])
|
||||
elif self.tip_style == 2:
|
||||
h = self.length / 2
|
||||
self.clear_points()
|
||||
self.data["points"] = Dot().set_width(h).get_points()
|
||||
self.rotate(self.angle)
|
||||
|
||||
def get_base(self):
|
||||
|
|
|
@ -57,13 +57,13 @@ class Matrix(VMobject):
|
|||
CONFIG = {
|
||||
"v_buff": 0.8,
|
||||
"h_buff": 1.3,
|
||||
"bracket_h_buff": MED_SMALL_BUFF,
|
||||
"bracket_v_buff": MED_SMALL_BUFF,
|
||||
"bracket_h_buff": 0.2,
|
||||
"bracket_v_buff": 0.25,
|
||||
"add_background_rectangles_to_entries": False,
|
||||
"include_background_rectangle": False,
|
||||
"element_to_mobject": Tex,
|
||||
"element_to_mobject_config": {},
|
||||
"element_alignment_corner": DR,
|
||||
"element_alignment_corner": DOWN,
|
||||
}
|
||||
|
||||
def __init__(self, matrix, **kwargs):
|
||||
|
@ -132,6 +132,12 @@ class Matrix(VMobject):
|
|||
for i in range(len(self.mob_matrix[0]))
|
||||
])
|
||||
|
||||
def get_rows(self):
|
||||
return VGroup(*[
|
||||
VGroup(*row)
|
||||
for row in self.mob_matrix
|
||||
])
|
||||
|
||||
def set_column_colors(self, *colors):
|
||||
columns = self.get_columns()
|
||||
for color, column in zip(colors, columns):
|
||||
|
@ -163,6 +169,7 @@ class DecimalMatrix(Matrix):
|
|||
class IntegerMatrix(Matrix):
|
||||
CONFIG = {
|
||||
"element_to_mobject": Integer,
|
||||
"element_alignment_corner": UP,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -176,6 +176,7 @@ class Mobject(object):
|
|||
|
||||
def match_points(self, mobject):
|
||||
self.set_points(mobject.get_points())
|
||||
return self
|
||||
|
||||
def get_points(self):
|
||||
return self.data["points"]
|
||||
|
@ -504,7 +505,7 @@ class Mobject(object):
|
|||
|
||||
self.refresh_has_updater_status()
|
||||
if call_updater:
|
||||
self.update()
|
||||
self.update(dt=0)
|
||||
return self
|
||||
|
||||
def remove_updater(self, update_function):
|
||||
|
@ -561,7 +562,7 @@ class Mobject(object):
|
|||
)
|
||||
return self
|
||||
|
||||
def scale(self, scale_factor, **kwargs):
|
||||
def scale(self, scale_factor, min_scale_factor=1e-8, **kwargs):
|
||||
"""
|
||||
Default behavior is to scale about the center of the mobject.
|
||||
The argument about_edge can be a vector, indicating which side of
|
||||
|
@ -571,6 +572,7 @@ class Mobject(object):
|
|||
Otherwise, if about_point is given a value, scaling is done with
|
||||
respect to that point.
|
||||
"""
|
||||
scale_factor = max(scale_factor, min_scale_factor)
|
||||
self.apply_points_function(
|
||||
lambda points: scale_factor * points,
|
||||
works_on_bounding_box=True,
|
||||
|
@ -841,7 +843,30 @@ class Mobject(object):
|
|||
|
||||
# Color functions
|
||||
|
||||
def set_rgba_array(self, color=None, opacity=None, name="rgbas", recurse=True):
|
||||
def set_rgba_array(self, rgba_array, name="rgbas", recurse=False):
|
||||
for mob in self.get_family(recurse):
|
||||
mob.data[name] = np.array(rgba_array)
|
||||
return self
|
||||
|
||||
def set_color_by_rgba_func(self, func, recurse=True):
|
||||
"""
|
||||
Func should take in a point in R3 and output an rgba value
|
||||
"""
|
||||
for mob in self.get_family(recurse):
|
||||
rgba_array = [func(point) for point in mob.get_points()]
|
||||
mob.set_rgba_array(rgba_array)
|
||||
return self
|
||||
|
||||
def set_color_by_rgb_func(self, func, opacity=1, recurse=True):
|
||||
"""
|
||||
Func should take in a point in R3 and output an rgb value
|
||||
"""
|
||||
for mob in self.get_family(recurse):
|
||||
rgba_array = [[*func(point), opacity] for point in mob.get_points()]
|
||||
mob.set_rgba_array(rgba_array)
|
||||
return self
|
||||
|
||||
def set_rgba_array_by_color(self, color=None, opacity=None, name="rgbas", recurse=True):
|
||||
if color is not None:
|
||||
rgbs = np.array([color_to_rgb(c) for c in listify(color)])
|
||||
if opacity is not None:
|
||||
|
@ -870,8 +895,8 @@ class Mobject(object):
|
|||
return self
|
||||
|
||||
def set_color(self, color, opacity=None, recurse=True):
|
||||
self.set_rgba_array(color, opacity, recurse=False)
|
||||
# Recurse to submobjects differently from how set_rgba_array
|
||||
self.set_rgba_array_by_color(color, opacity, recurse=False)
|
||||
# Recurse to submobjects differently from how set_rgba_array_by_color
|
||||
# in case they implement set_color differently
|
||||
if recurse:
|
||||
for submob in self.submobjects:
|
||||
|
@ -879,7 +904,7 @@ class Mobject(object):
|
|||
return self
|
||||
|
||||
def set_opacity(self, opacity, recurse=True):
|
||||
self.set_rgba_array(color=None, opacity=opacity, recurse=False)
|
||||
self.set_rgba_array_by_color(color=None, opacity=opacity, recurse=False)
|
||||
if recurse:
|
||||
for submob in self.submobjects:
|
||||
submob.set_opacity(opacity, recurse=True)
|
||||
|
|
|
@ -5,7 +5,6 @@ from manimlib.mobject.types.vectorized_mobject import VGroup
|
|||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.config_ops import merge_dicts_recursively
|
||||
from manimlib.utils.iterables import list_difference_update
|
||||
from manimlib.utils.simple_functions import fdiv
|
||||
from manimlib.utils.space_ops import normalize
|
||||
|
||||
|
@ -144,7 +143,7 @@ class NumberLine(Line):
|
|||
direction=direction,
|
||||
buff=buff
|
||||
)
|
||||
if x < 0 and self.line_to_number_direction[0] == 0:
|
||||
if x < 0 and direction[0] == 0:
|
||||
# Align without the minus sign
|
||||
num_mob.shift(num_mob[0].get_width() * LEFT / 2)
|
||||
return num_mob
|
||||
|
@ -155,10 +154,11 @@ class NumberLine(Line):
|
|||
|
||||
kwargs["font_size"] = font_size
|
||||
|
||||
if excluding is None:
|
||||
excluding = self.numbers_to_exclude
|
||||
|
||||
numbers = VGroup()
|
||||
for x in x_values:
|
||||
if x in self.numbers_to_exclude:
|
||||
continue
|
||||
if excluding is not None and x in excluding:
|
||||
continue
|
||||
numbers.add(self.get_number_mobject(x, **kwargs))
|
||||
|
|
|
@ -128,10 +128,11 @@ class DecimalNumber(VMobject):
|
|||
|
||||
def set_value(self, number):
|
||||
move_to_point = self.get_edge_center(self.edge_to_fix)
|
||||
style = self.get_style()
|
||||
old_submobjects = self.submobjects
|
||||
self.set_submobjects_from_number(number)
|
||||
self.move_to(move_to_point, self.edge_to_fix)
|
||||
self.set_style(**style)
|
||||
for sm1, sm2 in zip(self.submobjects, old_submobjects):
|
||||
sm1.match_style(sm2)
|
||||
return self
|
||||
|
||||
def scale(self, scale_factor, **kwargs):
|
||||
|
|
|
@ -64,16 +64,17 @@ class BackgroundRectangle(SurroundingRectangle):
|
|||
class Cross(VGroup):
|
||||
CONFIG = {
|
||||
"stroke_color": RED,
|
||||
"stroke_width": 6,
|
||||
"stroke_width": [0, 6, 0],
|
||||
}
|
||||
|
||||
def __init__(self, mobject, **kwargs):
|
||||
VGroup.__init__(self,
|
||||
Line(UP + LEFT, DOWN + RIGHT),
|
||||
Line(UP + RIGHT, DOWN + LEFT),
|
||||
)
|
||||
super().__init__(
|
||||
Line(UL, DR),
|
||||
Line(UR, DL),
|
||||
)
|
||||
self.insert_n_curves(2)
|
||||
self.replace(mobject, stretch=True)
|
||||
self.set_stroke(self.stroke_color, self.stroke_width)
|
||||
self.set_stroke(self.stroke_color, width=self.stroke_width)
|
||||
|
||||
|
||||
class Underline(Line):
|
||||
|
|
|
@ -8,6 +8,7 @@ from manimlib.animation.growing import GrowFromCenter
|
|||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.mobject.svg.tex_mobject import SingleStringTex
|
||||
from manimlib.mobject.svg.tex_mobject import TexText
|
||||
from manimlib.mobject.svg.text_mobject import Text
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
|
@ -61,9 +62,10 @@ class Brace(SingleStringTex):
|
|||
mob.shift(self.get_direction() * shift_distance)
|
||||
return self
|
||||
|
||||
def get_text(self, *text, **kwargs):
|
||||
text_mob = TexText(*text)
|
||||
self.put_at_tip(text_mob, **kwargs)
|
||||
def get_text(self, text, **kwargs):
|
||||
buff = kwargs.pop("buff", SMALL_BUFF)
|
||||
text_mob = Text(text, **kwargs)
|
||||
self.put_at_tip(text_mob, buff=buff)
|
||||
return text_mob
|
||||
|
||||
def get_tex(self, *tex, **kwargs):
|
||||
|
|
|
@ -41,7 +41,6 @@ class Exmark(TexText):
|
|||
|
||||
class Lightbulb(SVGMobject):
|
||||
CONFIG = {
|
||||
"file_name": "lightbulb",
|
||||
"height": 1,
|
||||
"stroke_color": YELLOW,
|
||||
"stroke_width": 3,
|
||||
|
@ -49,6 +48,9 @@ class Lightbulb(SVGMobject):
|
|||
"fill_opacity": 0,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__("lightbulb", **kwargs)
|
||||
|
||||
|
||||
class Speedometer(VMobject):
|
||||
CONFIG = {
|
||||
|
|
|
@ -169,7 +169,11 @@ class SVGMobject(VMobject):
|
|||
else 0.0
|
||||
for key in ("cx", "cy", "rx", "ry")
|
||||
]
|
||||
return Circle().scale(rx * RIGHT + ry * UP).shift(x * RIGHT + y * DOWN)
|
||||
result = Circle()
|
||||
result.stretch(rx, 0)
|
||||
result.stretch(ry, 1)
|
||||
result.shift(x * RIGHT + y * DOWN)
|
||||
return result
|
||||
|
||||
def rect_to_mobject(self, rect_element):
|
||||
fill_color = rect_element.getAttribute("fill")
|
||||
|
|
|
@ -65,6 +65,8 @@ class SingleStringTex(VMobject):
|
|||
if self.math_mode:
|
||||
new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}"
|
||||
|
||||
new_tex = self.alignment + "\n" + new_tex
|
||||
|
||||
tex_config = get_tex_config()
|
||||
return tex_config["tex_body"].replace(
|
||||
tex_config["text_to_replace"],
|
||||
|
@ -72,10 +74,7 @@ class SingleStringTex(VMobject):
|
|||
)
|
||||
|
||||
def get_modified_expression(self, tex_string):
|
||||
result = self.alignment + " " + tex_string
|
||||
result = result.strip()
|
||||
result = self.modify_special_strings(result)
|
||||
return result
|
||||
return self.modify_special_strings(tex_string.strip())
|
||||
|
||||
def modify_special_strings(self, tex):
|
||||
tex = tex.strip()
|
||||
|
@ -152,10 +151,7 @@ class SingleStringTex(VMobject):
|
|||
|
||||
class Tex(SingleStringTex):
|
||||
CONFIG = {
|
||||
"arg_separator": " ",
|
||||
# Note, use of isolate is largely rendered
|
||||
# moot by the fact that you can surround such strings in
|
||||
# {{ and }} as needed.
|
||||
"arg_separator": "",
|
||||
"isolate": [],
|
||||
"tex_to_color_map": {},
|
||||
}
|
||||
|
@ -172,18 +168,22 @@ class Tex(SingleStringTex):
|
|||
self.organize_submobjects_left_to_right()
|
||||
|
||||
def break_up_tex_strings(self, tex_strings):
|
||||
# Separate out anything surrounded in double braces
|
||||
patterns = ["{{", "}}"]
|
||||
# Separate out any strings specified in the isolate
|
||||
# or tex_to_color_map lists.
|
||||
patterns.extend([
|
||||
substrings_to_isolate = [*self.isolate, *self.tex_to_color_map.keys()]
|
||||
if len(substrings_to_isolate) == 0:
|
||||
return tex_strings
|
||||
patterns = (
|
||||
"({})".format(re.escape(ss))
|
||||
for ss in it.chain(self.isolate, self.tex_to_color_map.keys())
|
||||
])
|
||||
for ss in substrings_to_isolate
|
||||
)
|
||||
pattern = "|".join(patterns)
|
||||
pieces = []
|
||||
for s in tex_strings:
|
||||
pieces.extend(re.split(pattern, s))
|
||||
if pattern:
|
||||
pieces.extend(re.split(pattern, s))
|
||||
else:
|
||||
pieces.append(s)
|
||||
return list(filter(lambda s: s, pieces))
|
||||
|
||||
def break_up_by_substrings(self):
|
||||
|
|
|
@ -10,6 +10,7 @@ import manimpango
|
|||
from manimlib.constants import *
|
||||
from manimlib.mobject.geometry import Dot
|
||||
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.customization import get_customization
|
||||
from manimlib.utils.directories import get_downloads_dir, get_text_dir
|
||||
|
@ -100,6 +101,19 @@ class Text(SVGMobject):
|
|||
index = self.text.find(word, index + len(word))
|
||||
return indexes
|
||||
|
||||
def get_parts_by_text(self, word):
|
||||
return VGroup(*(
|
||||
self[i:j]
|
||||
for i, j in self.find_indexes(word)
|
||||
))
|
||||
|
||||
def get_part_by_text(self, word):
|
||||
parts = self.get_parts_by_text(word)
|
||||
if len(parts) > 0:
|
||||
return parts[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def full2short(self, config):
|
||||
for kwargs in [config, self.CONFIG]:
|
||||
if kwargs.__contains__('line_spacing_height'):
|
||||
|
@ -212,6 +226,7 @@ class Text(SVGMobject):
|
|||
self.text,
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def register_font(font_file: typing.Union[str, Path]):
|
||||
"""Temporarily add a font file to Pango's search path.
|
||||
|
@ -240,8 +255,8 @@ def register_font(font_file: typing.Union[str, Path]):
|
|||
-----
|
||||
This method of adding font files also works with :class:`CairoText`.
|
||||
.. important ::
|
||||
This method isn't available for macOS. Using this
|
||||
method on macOS will raise an :class:`AttributeError`.
|
||||
This method is available for macOS for ``ManimPango>=v0.2.3``. Using this
|
||||
method with previous releases will raise an :class:`AttributeError` on macOS.
|
||||
"""
|
||||
|
||||
input_folder = Path(get_downloads_dir()).parent.resolve()
|
||||
|
|
|
@ -52,7 +52,7 @@ class VMobject(Mobject):
|
|||
"fill_shader_folder": "quadratic_bezier_fill",
|
||||
# Could also be "bevel", "miter", "round"
|
||||
"joint_type": "auto",
|
||||
"flat_stroke": True,
|
||||
"flat_stroke": False,
|
||||
"render_primitive": moderngl.TRIANGLES,
|
||||
"triangulation_locked": False,
|
||||
"fill_dtype": [
|
||||
|
@ -106,24 +106,42 @@ class VMobject(Mobject):
|
|||
self.set_flat_stroke(self.flat_stroke)
|
||||
return self
|
||||
|
||||
def set_rgba_array(self, rgba_array, name=None, recurse=False):
|
||||
if name is None:
|
||||
names = ["fill_rgba", "stroke_rgba"]
|
||||
else:
|
||||
names = [name]
|
||||
|
||||
for name in names:
|
||||
super().set_rgba_array(rgba_array, name, recurse)
|
||||
return self
|
||||
|
||||
def set_fill(self, color=None, opacity=None, recurse=True):
|
||||
self.set_rgba_array(color, opacity, 'fill_rgba', recurse)
|
||||
self.set_rgba_array_by_color(color, opacity, 'fill_rgba', recurse)
|
||||
return self
|
||||
|
||||
def set_stroke(self, color=None, width=None, opacity=None, background=None, recurse=True):
|
||||
self.set_rgba_array(color, opacity, 'stroke_rgba', recurse)
|
||||
self.set_rgba_array_by_color(color, opacity, 'stroke_rgba', recurse)
|
||||
|
||||
if width is not None:
|
||||
for mob in self.get_family(recurse):
|
||||
mob.data['stroke_width'] = np.array([
|
||||
[width] for width in listify(width)
|
||||
])
|
||||
if isinstance(width, np.ndarray):
|
||||
arr = width.reshape((len(width), 1))
|
||||
else:
|
||||
arr = np.array([[w] for w in listify(width)])
|
||||
mob.data['stroke_width'] = arr
|
||||
|
||||
if background is not None:
|
||||
for mob in self.get_family(recurse):
|
||||
mob.draw_stroke_behind_fill = background
|
||||
return self
|
||||
|
||||
def align_stroke_width_data_to_points(self, recurse=True):
|
||||
for mob in self.get_family(recurse):
|
||||
mob.data["stroke_width"] = resize_with_interpolation(
|
||||
mob.data["stroke_width"], len(mob.get_points())
|
||||
)
|
||||
|
||||
def set_style(self,
|
||||
fill_color=None,
|
||||
fill_opacity=None,
|
||||
|
@ -226,7 +244,7 @@ class VMobject(Mobject):
|
|||
return self.data['stroke_rgba'][:, 3]
|
||||
|
||||
def get_stroke_widths(self):
|
||||
return self.data['stroke_width']
|
||||
return self.data['stroke_width'][:, 0]
|
||||
|
||||
# TODO, it's weird for these to return the first of various lists
|
||||
# rather than the full information
|
||||
|
@ -259,7 +277,7 @@ class VMobject(Mobject):
|
|||
return self.get_fill_color()
|
||||
|
||||
def has_stroke(self):
|
||||
return any(self.get_stroke_widths()) and any(self.get_stroke_opacities())
|
||||
return self.get_stroke_widths().any() and self.get_stroke_opacities().any()
|
||||
|
||||
def has_fill(self):
|
||||
return any(self.get_fill_opacities())
|
||||
|
@ -830,8 +848,8 @@ class VMobject(Mobject):
|
|||
old_points = self.get_points()
|
||||
func(self, *args, **kwargs)
|
||||
if not np.all(self.get_points() == old_points):
|
||||
self.refresh_triangulation()
|
||||
self.refresh_unit_normal()
|
||||
self.refresh_triangulation()
|
||||
return wrapper
|
||||
|
||||
@triggers_refreshed_triangulation
|
||||
|
@ -852,9 +870,10 @@ class VMobject(Mobject):
|
|||
self.make_approximately_smooth()
|
||||
return self
|
||||
|
||||
@triggers_refreshed_triangulation
|
||||
def flip(self, *args, **kwargs):
|
||||
super().flip(*args, **kwargs)
|
||||
self.refresh_unit_normal()
|
||||
self.refresh_triangulation()
|
||||
return self
|
||||
|
||||
# For shaders
|
||||
|
|
|
@ -1,66 +1,32 @@
|
|||
import numpy as np
|
||||
import os
|
||||
import itertools as it
|
||||
from PIL import Image
|
||||
import random
|
||||
|
||||
from manimlib.constants import *
|
||||
|
||||
from manimlib.animation.composition import AnimationGroup
|
||||
from manimlib.animation.indication import ShowPassingFlash
|
||||
from manimlib.mobject.geometry import Vector
|
||||
from manimlib.animation.indication import VShowPassingFlash
|
||||
from manimlib.mobject.geometry import Arrow
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.bezier import inverse_interpolate
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.color import color_to_rgb
|
||||
from manimlib.utils.color import rgb_to_color
|
||||
from manimlib.utils.color import get_colormap_list
|
||||
from manimlib.utils.config_ops import merge_dicts_recursively
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.rate_functions import linear
|
||||
from manimlib.utils.simple_functions import sigmoid
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
# from manimlib.utils.space_ops import normalize
|
||||
|
||||
|
||||
DEFAULT_SCALAR_FIELD_COLORS = [BLUE_E, GREEN, YELLOW, RED]
|
||||
|
||||
|
||||
def get_colored_background_image(scalar_field_func,
|
||||
number_to_rgb_func,
|
||||
pixel_height=DEFAULT_PIXEL_HEIGHT,
|
||||
pixel_width=DEFAULT_PIXEL_WIDTH):
|
||||
ph = pixel_height
|
||||
pw = pixel_width
|
||||
fw = FRAME_WIDTH
|
||||
fh = FRAME_HEIGHT
|
||||
points_array = np.zeros((ph, pw, 3))
|
||||
x_array = np.linspace(-fw / 2, fw / 2, pw)
|
||||
x_array = x_array.reshape((1, len(x_array)))
|
||||
x_array = x_array.repeat(ph, axis=0)
|
||||
|
||||
y_array = np.linspace(fh / 2, -fh / 2, ph)
|
||||
y_array = y_array.reshape((len(y_array), 1))
|
||||
y_array.repeat(pw, axis=1)
|
||||
points_array[:, :, 0] = x_array
|
||||
points_array[:, :, 1] = y_array
|
||||
scalars = np.apply_along_axis(scalar_field_func, 2, points_array)
|
||||
rgb_array = number_to_rgb_func(scalars.flatten()).reshape((ph, pw, 3))
|
||||
return Image.fromarray((rgb_array * 255).astype('uint8'))
|
||||
|
||||
|
||||
def get_rgb_gradient_function(min_value=0, max_value=1,
|
||||
colors=[BLUE, RED],
|
||||
flip_alphas=True, # Why?
|
||||
):
|
||||
rgbs = np.array(list(map(color_to_rgb, colors)))
|
||||
def get_vectorized_rgb_gradient_function(min_value, max_value, color_map):
|
||||
rgbs = np.array(get_colormap_list(color_map))
|
||||
|
||||
def func(values):
|
||||
alphas = inverse_interpolate(
|
||||
min_value, max_value, np.array(values)
|
||||
)
|
||||
alphas = np.clip(alphas, 0, 1)
|
||||
# if flip_alphas:
|
||||
# alphas = 1 - alphas
|
||||
scaled_alphas = alphas * (len(rgbs) - 1)
|
||||
indices = scaled_alphas.astype(int)
|
||||
next_indices = np.clip(indices + 1, 0, len(rgbs) - 1)
|
||||
|
@ -71,29 +37,9 @@ def get_rgb_gradient_function(min_value=0, max_value=1,
|
|||
return func
|
||||
|
||||
|
||||
def get_color_field_image_file(scalar_func,
|
||||
min_value=0, max_value=2,
|
||||
colors=DEFAULT_SCALAR_FIELD_COLORS
|
||||
):
|
||||
# try_hash
|
||||
np.random.seed(0)
|
||||
sample_inputs = 5 * np.random.random(size=(10, 3)) - 10
|
||||
sample_outputs = np.apply_along_axis(scalar_func, 1, sample_inputs)
|
||||
func_hash = hash(
|
||||
str(min_value) + str(max_value) + str(colors) + str(sample_outputs)
|
||||
)
|
||||
file_name = "%d.png" % func_hash
|
||||
full_path = os.path.join(RASTER_IMAGE_DIR, file_name)
|
||||
if not os.path.exists(full_path):
|
||||
print("Rendering color field image " + str(func_hash))
|
||||
rgb_gradient_func = get_rgb_gradient_function(
|
||||
min_value=min_value,
|
||||
max_value=max_value,
|
||||
colors=colors
|
||||
)
|
||||
image = get_colored_background_image(scalar_func, rgb_gradient_func)
|
||||
image.save(full_path)
|
||||
return full_path
|
||||
def get_rgb_gradient_function(min_value, max_value, color_map):
|
||||
vectorized_func = get_vectorized_rgb_gradient_function(min_value, max_value, color_map)
|
||||
return lambda value: vectorized_func([value])[0]
|
||||
|
||||
|
||||
def move_along_vector_field(mobject, func):
|
||||
|
@ -116,217 +62,200 @@ def move_submobjects_along_vector_field(mobject, func):
|
|||
return mobject
|
||||
|
||||
|
||||
def move_points_along_vector_field(mobject, func):
|
||||
def move_points_along_vector_field(mobject, func, coordinate_system):
|
||||
cs = coordinate_system
|
||||
origin = cs.get_origin()
|
||||
|
||||
def apply_nudge(self, dt):
|
||||
self.mobject.apply_function(
|
||||
lambda p: p + func(p) * dt
|
||||
mobject.apply_function(
|
||||
lambda p: p + (cs.c2p(*func(*cs.p2c(p))) - origin) * dt
|
||||
)
|
||||
mobject.add_updater(apply_nudge)
|
||||
return mobject
|
||||
|
||||
|
||||
def get_sample_points_from_coordinate_system(coordinate_system, step_multiple):
|
||||
ranges = []
|
||||
for range_args in coordinate_system.get_all_ranges():
|
||||
_min, _max, step = range_args
|
||||
step *= step_multiple
|
||||
ranges.append(np.arange(_min, _max + step, step))
|
||||
return it.product(*ranges)
|
||||
|
||||
|
||||
# Mobjects
|
||||
|
||||
class VectorField(VGroup):
|
||||
CONFIG = {
|
||||
"delta_x": 0.5,
|
||||
"delta_y": 0.5,
|
||||
"x_min": int(np.floor(-FRAME_WIDTH / 2)),
|
||||
"x_max": int(np.ceil(FRAME_WIDTH / 2)),
|
||||
"y_min": int(np.floor(-FRAME_HEIGHT / 2)),
|
||||
"y_max": int(np.ceil(FRAME_HEIGHT / 2)),
|
||||
"min_magnitude": 0,
|
||||
"max_magnitude": 2,
|
||||
"colors": DEFAULT_SCALAR_FIELD_COLORS,
|
||||
"step_multiple": 0.5,
|
||||
"magnitude_range": (0, 2),
|
||||
"color_map": "3b1b_colormap",
|
||||
# Takes in actual norm, spits out displayed norm
|
||||
"length_func": lambda norm: 0.45 * sigmoid(norm),
|
||||
"opacity": 1.0,
|
||||
"vector_config": {},
|
||||
}
|
||||
|
||||
def __init__(self, func, **kwargs):
|
||||
def __init__(self, func, coordinate_system, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.func = func
|
||||
self.rgb_gradient_function = get_rgb_gradient_function(
|
||||
self.min_magnitude,
|
||||
self.max_magnitude,
|
||||
self.colors,
|
||||
flip_alphas=False
|
||||
self.coordinate_system = coordinate_system
|
||||
self.value_to_rgb = get_rgb_gradient_function(
|
||||
*self.magnitude_range, self.color_map,
|
||||
)
|
||||
x_range = np.arange(
|
||||
self.x_min,
|
||||
self.x_max + self.delta_x,
|
||||
self.delta_x
|
||||
)
|
||||
y_range = np.arange(
|
||||
self.y_min,
|
||||
self.y_max + self.delta_y,
|
||||
self.delta_y
|
||||
)
|
||||
for x, y in it.product(x_range, y_range):
|
||||
point = x * RIGHT + y * UP
|
||||
self.add(self.get_vector(point))
|
||||
self.set_opacity(self.opacity)
|
||||
|
||||
def get_vector(self, point, **kwargs):
|
||||
output = np.array(self.func(point))
|
||||
norm = get_norm(output)
|
||||
if norm == 0:
|
||||
output *= 0
|
||||
else:
|
||||
output *= self.length_func(norm) / norm
|
||||
vector_config = dict(self.vector_config)
|
||||
vector_config.update(kwargs)
|
||||
vect = Vector(output, **vector_config)
|
||||
vect.shift(point)
|
||||
fill_color = rgb_to_color(
|
||||
self.rgb_gradient_function(np.array([norm]))[0]
|
||||
samples = get_sample_points_from_coordinate_system(
|
||||
coordinate_system, self.step_multiple
|
||||
)
|
||||
vect.set_color(fill_color)
|
||||
self.add(*(
|
||||
self.get_vector(coords)
|
||||
for coords in samples
|
||||
))
|
||||
|
||||
def get_vector(self, coords, **kwargs):
|
||||
vector_config = merge_dicts_recursively(
|
||||
self.vector_config,
|
||||
kwargs
|
||||
)
|
||||
|
||||
output = np.array(self.func(*coords))
|
||||
norm = get_norm(output)
|
||||
if norm > 0:
|
||||
output *= self.length_func(norm) / norm
|
||||
|
||||
origin = self.coordinate_system.get_origin()
|
||||
_input = self.coordinate_system.c2p(*coords)
|
||||
_output = self.coordinate_system.c2p(*output)
|
||||
|
||||
vect = Arrow(
|
||||
origin, _output, buff=0,
|
||||
**vector_config
|
||||
)
|
||||
vect.shift(_input - origin)
|
||||
vect.set_rgba_array([[*self.value_to_rgb(norm), self.opacity]])
|
||||
return vect
|
||||
|
||||
|
||||
class StreamLines(VGroup):
|
||||
CONFIG = {
|
||||
# TODO, this is an awkward way to inherit
|
||||
# defaults to a method.
|
||||
"start_points_generator_config": {},
|
||||
# Config for choosing start points
|
||||
"x_min": -8,
|
||||
"x_max": 8,
|
||||
"y_min": -5,
|
||||
"y_max": 5,
|
||||
"delta_x": 0.5,
|
||||
"delta_y": 0.5,
|
||||
"step_multiple": 0.5,
|
||||
"n_repeats": 1,
|
||||
"noise_factor": None,
|
||||
# Config for drawing lines
|
||||
"dt": 0.05,
|
||||
"virtual_time": 3,
|
||||
"n_anchors_per_line": 100,
|
||||
"arc_len": 3,
|
||||
"max_time_steps": 200,
|
||||
"n_samples_per_line": 10,
|
||||
"cutoff_norm": 15,
|
||||
# Style info
|
||||
"stroke_width": 1,
|
||||
"stroke_color": WHITE,
|
||||
"color_by_arc_length": True,
|
||||
# Min and max arc lengths meant to define
|
||||
# the color range, should color_by_arc_length be True
|
||||
"min_arc_length": 0,
|
||||
"max_arc_length": 12,
|
||||
"color_by_magnitude": False,
|
||||
# Min and max magnitudes meant to define
|
||||
# the color range, should color_by_magnitude be True
|
||||
"min_magnitude": 0.5,
|
||||
"max_magnitude": 1.5,
|
||||
"colors": DEFAULT_SCALAR_FIELD_COLORS,
|
||||
"cutoff_norm": 15,
|
||||
"stroke_opacity": 1,
|
||||
"color_by_magnitude": True,
|
||||
"magnitude_range": (0, 2.0),
|
||||
"taper_stroke_width": False,
|
||||
"color_map": "3b1b_colormap",
|
||||
}
|
||||
|
||||
def __init__(self, func, **kwargs):
|
||||
VGroup.__init__(self, **kwargs)
|
||||
def __init__(self, func, coordinate_system, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.func = func
|
||||
dt = self.dt
|
||||
self.coordinate_system = coordinate_system
|
||||
self.draw_lines()
|
||||
self.init_style()
|
||||
|
||||
start_points = self.get_start_points(
|
||||
**self.start_points_generator_config
|
||||
)
|
||||
for point in start_points:
|
||||
def point_func(self, point):
|
||||
in_coords = self.coordinate_system.p2c(point)
|
||||
out_coords = self.func(*in_coords)
|
||||
return self.coordinate_system.c2p(*out_coords)
|
||||
|
||||
def draw_lines(self):
|
||||
lines = []
|
||||
origin = self.coordinate_system.get_origin()
|
||||
for point in self.get_start_points():
|
||||
points = [point]
|
||||
for t in np.arange(0, self.virtual_time, dt):
|
||||
total_arc_len = 0
|
||||
time = 0
|
||||
for x in range(self.max_time_steps):
|
||||
time += self.dt
|
||||
last_point = points[-1]
|
||||
points.append(last_point + dt * func(last_point))
|
||||
new_point = last_point + self.dt * (self.point_func(last_point) - origin)
|
||||
points.append(new_point)
|
||||
total_arc_len += get_norm(new_point - last_point)
|
||||
if get_norm(last_point) > self.cutoff_norm:
|
||||
break
|
||||
if total_arc_len > self.arc_len:
|
||||
break
|
||||
line = VMobject()
|
||||
step = max(1, int(len(points) / self.n_anchors_per_line))
|
||||
line.set_points_smoothly(points[::step])
|
||||
self.add(line)
|
||||
|
||||
self.set_stroke(self.stroke_color, self.stroke_width)
|
||||
|
||||
if self.color_by_arc_length:
|
||||
len_to_rgb = get_rgb_gradient_function(
|
||||
self.min_arc_length,
|
||||
self.max_arc_length,
|
||||
colors=self.colors,
|
||||
)
|
||||
for line in self:
|
||||
arc_length = line.get_arc_length()
|
||||
rgb = len_to_rgb([arc_length])[0]
|
||||
color = rgb_to_color(rgb)
|
||||
line.set_color(color)
|
||||
elif self.color_by_magnitude:
|
||||
image_file = get_color_field_image_file(
|
||||
lambda p: get_norm(func(p)),
|
||||
min_value=self.min_magnitude,
|
||||
max_value=self.max_magnitude,
|
||||
colors=self.colors,
|
||||
)
|
||||
self.color_using_background_image(image_file)
|
||||
line.virtual_time = time
|
||||
step = max(1, int(len(points) / self.n_samples_per_line))
|
||||
line.set_points_as_corners(points[::step])
|
||||
line.make_approximately_smooth()
|
||||
lines.append(line)
|
||||
self.set_submobjects(lines)
|
||||
|
||||
def get_start_points(self):
|
||||
x_min = self.x_min
|
||||
x_max = self.x_max
|
||||
y_min = self.y_min
|
||||
y_max = self.y_max
|
||||
delta_x = self.delta_x
|
||||
delta_y = self.delta_y
|
||||
n_repeats = self.n_repeats
|
||||
cs = self.coordinate_system
|
||||
sample_coords = get_sample_points_from_coordinate_system(
|
||||
cs, self.step_multiple,
|
||||
)
|
||||
|
||||
noise_factor = self.noise_factor
|
||||
|
||||
if noise_factor is None:
|
||||
noise_factor = delta_y / 2
|
||||
noise_factor = cs.x_range[2] * self.step_multiple * 0.5
|
||||
|
||||
return np.array([
|
||||
x * RIGHT + y * UP + noise_factor * np.random.random(3)
|
||||
for n in range(n_repeats)
|
||||
for x in np.arange(x_min, x_max + delta_x, delta_x)
|
||||
for y in np.arange(y_min, y_max + delta_y, delta_y)
|
||||
cs.c2p(*coords) + noise_factor * np.random.random(3)
|
||||
for n in range(self.n_repeats)
|
||||
for coords in sample_coords
|
||||
])
|
||||
|
||||
|
||||
# TODO: Make it so that you can have a group of stream_lines
|
||||
# varying in response to a changing vector field, and still
|
||||
# animate the resulting flow
|
||||
class ShowPassingFlashWithThinningStrokeWidth(AnimationGroup):
|
||||
CONFIG = {
|
||||
"n_segments": 10,
|
||||
"time_width": 0.1,
|
||||
"remover": True
|
||||
}
|
||||
|
||||
def __init__(self, vmobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
max_stroke_width = vmobject.get_stroke_width()
|
||||
max_time_width = kwargs.pop("time_width", self.time_width)
|
||||
AnimationGroup.__init__(self, *[
|
||||
ShowPassingFlash(
|
||||
vmobject.deepcopy().set_stroke(width=stroke_width),
|
||||
time_width=time_width,
|
||||
**kwargs
|
||||
def init_style(self):
|
||||
if self.color_by_magnitude:
|
||||
values_to_rgbs = get_vectorized_rgb_gradient_function(
|
||||
*self.magnitude_range, self.color_map,
|
||||
)
|
||||
for stroke_width, time_width in zip(
|
||||
np.linspace(0, max_stroke_width, self.n_segments),
|
||||
np.linspace(max_time_width, 0, self.n_segments)
|
||||
)
|
||||
])
|
||||
cs = self.coordinate_system
|
||||
for line in self.submobjects:
|
||||
norms = [
|
||||
get_norm(self.func(*cs.p2c(point)))
|
||||
for point in line.get_points()
|
||||
]
|
||||
rgbs = values_to_rgbs(norms)
|
||||
rgbas = np.zeros((len(rgbs), 4))
|
||||
rgbas[:, :3] = rgbs
|
||||
rgbas[:, 3] = self.stroke_opacity
|
||||
line.set_rgba_array(rgbas, "stroke_rgba")
|
||||
else:
|
||||
self.set_stroke(self.stroke_color, opacity=self.stroke_opacity)
|
||||
|
||||
if self.taper_stroke_width:
|
||||
width = [0, self.stroke_width, 0]
|
||||
else:
|
||||
width = self.stroke_width
|
||||
self.set_stroke(width=width)
|
||||
|
||||
|
||||
# TODO, this is untested after turning it from a
|
||||
# ContinualAnimation into a VGroup
|
||||
class AnimatedStreamLines(VGroup):
|
||||
CONFIG = {
|
||||
"lag_range": 4,
|
||||
"line_anim_class": ShowPassingFlash,
|
||||
"line_anim_class": VShowPassingFlash,
|
||||
"line_anim_config": {
|
||||
"run_time": 4,
|
||||
# "run_time": 4,
|
||||
"rate_func": linear,
|
||||
"time_width": 0.3,
|
||||
"time_width": 0.5,
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, stream_lines, **kwargs):
|
||||
VGroup.__init__(self, **kwargs)
|
||||
super().__init__(**kwargs)
|
||||
self.stream_lines = stream_lines
|
||||
for line in stream_lines:
|
||||
line.anim = self.line_anim_class(line, **self.line_anim_config)
|
||||
line.anim = self.line_anim_class(
|
||||
line,
|
||||
run_time=line.virtual_time,
|
||||
**self.line_anim_config,
|
||||
)
|
||||
line.anim.begin()
|
||||
line.time = -self.lag_range * random.random()
|
||||
self.add(line.anim.mobject)
|
||||
|
@ -339,3 +268,28 @@ class AnimatedStreamLines(VGroup):
|
|||
line.time += dt
|
||||
adjusted_time = max(line.time, 0) % line.anim.run_time
|
||||
line.anim.update(adjusted_time / line.anim.run_time)
|
||||
|
||||
|
||||
# TODO: This class should be deleted
|
||||
class ShowPassingFlashWithThinningStrokeWidth(AnimationGroup):
|
||||
CONFIG = {
|
||||
"n_segments": 10,
|
||||
"time_width": 0.1,
|
||||
"remover": True
|
||||
}
|
||||
|
||||
def __init__(self, vmobject, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
max_stroke_width = vmobject.get_stroke_width()
|
||||
max_time_width = kwargs.pop("time_width", self.time_width)
|
||||
AnimationGroup.__init__(self, *[
|
||||
VShowPassingFlash(
|
||||
vmobject.deepcopy().set_stroke(width=stroke_width),
|
||||
time_width=time_width,
|
||||
**kwargs
|
||||
)
|
||||
for stroke_width, time_width in zip(
|
||||
np.linspace(0, max_stroke_width, self.n_segments),
|
||||
np.linspace(max_time_width, 0, self.n_segments)
|
||||
)
|
||||
])
|
||||
|
|
|
@ -74,12 +74,14 @@ class SwitchOff(LaggedStartMap):
|
|||
|
||||
class Lighthouse(SVGMobject):
|
||||
CONFIG = {
|
||||
"file_name": "lighthouse",
|
||||
"height": LIGHTHOUSE_HEIGHT,
|
||||
"fill_color": WHITE,
|
||||
"fill_opacity": 1.0,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__("lighthouse", **kwargs)
|
||||
|
||||
def move_to(self, point):
|
||||
self.next_to(point, DOWN, buff=0)
|
||||
|
||||
|
|
|
@ -117,7 +117,7 @@ class Scene(object):
|
|||
self.stop_skipping()
|
||||
self.linger_after_completion = False
|
||||
self.update_frame()
|
||||
|
||||
|
||||
from IPython.terminal.embed import InteractiveShellEmbed
|
||||
shell = InteractiveShellEmbed()
|
||||
# Have the frame update after each command
|
||||
|
@ -272,7 +272,7 @@ class Scene(object):
|
|||
times,
|
||||
total=n_iterations,
|
||||
leave=self.leave_progress_bars,
|
||||
ascii=False if platform.system() != 'Windows' else True
|
||||
ascii=True if platform.system() == 'Windows' else None
|
||||
)
|
||||
return time_progression
|
||||
|
||||
|
|
|
@ -9,5 +9,5 @@ out vec4 frag_color;
|
|||
|
||||
void main() {
|
||||
frag_color = texture(Texture, v_im_coords);
|
||||
frag_color.a = v_opacity;
|
||||
frag_color.a *= v_opacity;
|
||||
}
|
|
@ -17,16 +17,16 @@ vec4 add_light(vec4 color,
|
|||
float shadow){
|
||||
if(gloss == 0.0 && shadow == 0.0) return color;
|
||||
|
||||
// TODO, do we actually want this? It effectively treats surfaces as two-sided
|
||||
if(unit_normal.z < 0){
|
||||
unit_normal *= -1;
|
||||
}
|
||||
|
||||
// TODO, read this in as a uniform?
|
||||
float camera_distance = 6;
|
||||
float camera_distance = focal_distance;
|
||||
// Assume everything has already been rotated such that camera is in the z-direction
|
||||
vec3 to_camera = vec3(0, 0, camera_distance) - point;
|
||||
vec3 to_light = light_coords - point;
|
||||
|
||||
// TODO, do we actually want this? It effectively treats surfaces as two-sided
|
||||
if(dot(to_camera,unit_normal) < 0){
|
||||
unit_normal *= -1;
|
||||
}
|
||||
|
||||
vec3 light_reflection = -to_light + 2 * unit_normal * dot(to_light, unit_normal);
|
||||
float dot_prod = dot(normalize(light_reflection), normalize(to_camera));
|
||||
float shine = gloss * exp(-3 * pow(1 - dot_prod, 2));
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
uniform vec3 light_source_position;
|
||||
uniform float gloss;
|
||||
uniform float shadow;
|
||||
uniform float focal_distance;
|
||||
|
||||
in vec3 xyz_coords;
|
||||
in vec3 v_normal;
|
||||
|
|
|
@ -6,6 +6,7 @@ uniform float num_textures;
|
|||
uniform vec3 light_source_position;
|
||||
uniform float gloss;
|
||||
uniform float shadow;
|
||||
uniform float focal_distance;
|
||||
|
||||
in vec3 xyz_coords;
|
||||
in vec3 v_normal;
|
||||
|
|
|
@ -4,6 +4,7 @@ uniform vec3 light_source_position;
|
|||
uniform float gloss;
|
||||
uniform float shadow;
|
||||
uniform float anti_alias_width;
|
||||
uniform float focal_distance;
|
||||
|
||||
in vec4 color;
|
||||
in float radius;
|
||||
|
|
|
@ -4,7 +4,9 @@ from colour import Color
|
|||
import numpy as np
|
||||
|
||||
from manimlib.constants import WHITE
|
||||
from manimlib.constants import COLORMAP_3B1B
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.iterables import resize_with_interpolation
|
||||
from manimlib.utils.simple_functions import clip_in_place
|
||||
from manimlib.utils.space_ops import normalize
|
||||
|
||||
|
@ -114,10 +116,22 @@ def get_shaded_rgb(rgb, point, unit_normal_vect, light_source):
|
|||
|
||||
|
||||
def get_colormap_list(map_name="viridis", n_colors=9):
|
||||
"""
|
||||
Options for map_name:
|
||||
3b1b_colormap
|
||||
magma
|
||||
inferno
|
||||
plasma
|
||||
viridis
|
||||
cividis
|
||||
twilight
|
||||
twilight_shifted
|
||||
turbo
|
||||
"""
|
||||
from matplotlib.cm import get_cmap
|
||||
|
||||
rgbs = get_cmap(map_name).colors # Make more general?
|
||||
return [
|
||||
rgbs[int(n)]
|
||||
for n in np.linspace(0, len(rgbs) - 1, n_colors)
|
||||
]
|
||||
if map_name == "3b1b_colormap":
|
||||
rgbs = [color_to_rgb(color) for color in COLORMAP_3B1B]
|
||||
else:
|
||||
rgbs = get_cmap(map_name).colors # Make more general?
|
||||
return resize_with_interpolation(np.array(rgbs), n_colors)
|
||||
|
|
|
@ -23,6 +23,7 @@ def get_text_dir():
|
|||
def get_mobject_data_dir():
|
||||
return guarantee_existence(os.path.join(get_temp_dir(), "mobject_data"))
|
||||
|
||||
|
||||
def get_downloads_dir():
|
||||
return guarantee_existence(os.path.join(get_temp_dir(), "manim_downloads"))
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ def get_full_raster_image_path(image_file_name):
|
|||
return find_file(
|
||||
image_file_name,
|
||||
directories=[get_raster_image_dir()],
|
||||
extensions=[".jpg", ".png", ".gif", ""]
|
||||
extensions=[".jpg", ".jpeg", ".png", ".gif", ""]
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ def init_customization():
|
|||
"background_color": "",
|
||||
},
|
||||
"window_position": "UR",
|
||||
"window_position": 0,
|
||||
"break_into_partial_movies": False,
|
||||
"camera_qualities": {
|
||||
"low": {
|
||||
|
|
|
@ -98,6 +98,8 @@ def resize_preserving_order(nparray, length):
|
|||
def resize_with_interpolation(nparray, length):
|
||||
if len(nparray) == length:
|
||||
return nparray
|
||||
if length == 0:
|
||||
return np.zeros((0, *nparray.shape[1:]))
|
||||
cont_indices = np.linspace(0, len(nparray) - 1, length)
|
||||
return np.array([
|
||||
(1 - a) * nparray[lh] + a * nparray[rh]
|
||||
|
|
|
@ -15,7 +15,7 @@ class Window(PygletWindow):
|
|||
cursor = True
|
||||
|
||||
def __init__(self, scene, size=(1280, 720), **kwargs):
|
||||
super().__init__()
|
||||
super().__init__(size=size)
|
||||
digest_config(self, kwargs)
|
||||
|
||||
self.scene = scene
|
||||
|
@ -38,7 +38,9 @@ class Window(PygletWindow):
|
|||
|
||||
def find_initial_position(self, size):
|
||||
custom_position = get_customization()["window_position"]
|
||||
monitor = get_monitors()[get_customization()["window_monitor"]]
|
||||
monitors = get_monitors()
|
||||
mon_index = get_customization()["window_monitor"]
|
||||
monitor = monitors[min(mon_index, len(monitors) - 1)]
|
||||
window_width, window_height = size
|
||||
# Position might be specified with a string of the form
|
||||
# x,y for integers x and y
|
||||
|
|
|
@ -2,7 +2,6 @@ argparse
|
|||
colour
|
||||
numpy
|
||||
Pillow
|
||||
progressbar
|
||||
scipy
|
||||
sympy
|
||||
tqdm
|
||||
|
@ -17,4 +16,4 @@ pyreadline; sys_platform == 'win32'
|
|||
validators
|
||||
ipython
|
||||
PyOpenGL
|
||||
manimpango>=0.2.0,<0.3.0'
|
||||
manimpango>=0.2.0,<0.4.0
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
[metadata]
|
||||
name = manimgl
|
||||
version = 1.0.0
|
||||
author = Grant Sanderson
|
||||
author-email= grant@3blue1brown.com
|
||||
summary = Animation engine for explanatory math videos
|
||||
|
|
Loading…
Add table
Reference in a new issue