Merge branch '3b1b:master' into master

This commit is contained in:
鹤翔万里 2021-06-15 17:48:25 +08:00
commit 23662d093f
51 changed files with 688 additions and 390 deletions

View file

@ -20,7 +20,7 @@ jobs:
- name: Install sphinx and manim env - name: Install sphinx and manim env
run: | run: |
pip3 install --upgrade pip 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 docs/requirements.txt
pip3 install -r requirements.txt pip3 install -r requirements.txt

2
.gitignore vendored
View file

@ -15,6 +15,8 @@ __pycache__/
build/ build/
develop-eggs/ develop-eggs/
dist/ dist/
manimlib.egg-info/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/

View file

@ -4,27 +4,37 @@
</a> </a>
</p> </p>
[![pypi version](https://img.shields.io/pypi/v/manimgl?logo=pypi)](https://pypi.org/project/manimgl/)
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](http://choosealicense.com/licenses/mit/) [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](http://choosealicense.com/licenses/mit/)
[![Manim Subreddit](https://img.shields.io/reddit/subreddit-subscribers/manim.svg?color=ff4301&label=reddit)](https://www.reddit.com/r/manim/) [![Manim Subreddit](https://img.shields.io/reddit/subreddit-subscribers/manim.svg?color=ff4301&label=reddit&logo=reddit)](https://www.reddit.com/r/manim/)
[![Manim Discord](https://img.shields.io/discord/581738731934056449.svg?label=discord)](https://discord.gg/mMRrZQW) [![Manim Discord](https://img.shields.io/discord/581738731934056449.svg?label=discord&logo=discord)](https://discord.gg/mMRrZQW)
[![docs](https://github.com/3b1b/manim/workflows/docs/badge.svg)](https://3b1b.github.io/manim/) [![docs](https://github.com/3b1b/manim/workflows/docs/badge.svg)](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. 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 ## Installation
Manim runs on Python 3.6 or higher (Python 3.8 is recommended). 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) 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).
and [cairo](https://www.cairographics.org/) (optional, if you want to use Text). 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 ### 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: If you want to hack on manimlib itself, clone this repository and in that directory execute:
```sh ```sh
@ -36,21 +46,9 @@ manimgl example_scenes.py OpeningManimExample
# or # or
manim-render example_scenes.py OpeningManimExample 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) ### Directly (Windows)
1. [Install FFmpeg](https://www.wikihow.com/Install-FFmpeg-on-Windows). 1. [Install FFmpeg](https://www.wikihow.com/Install-FFmpeg-on-Windows).
2. Install a LaTeX distribution. [MiKTeX](https://miktex.org/download) is recommended. 2. Install a LaTeX distribution. [MiKTeX](https://miktex.org/download) is recommended.
3. Install the remaining Python packages. 3. Install the remaining Python packages.
@ -61,12 +59,27 @@ manim-render example_scenes.py OpeningManimExample
manimgl 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 ## Anaconda Install
* Install LaTeX as above. 1. Install LaTeX as above.
* Create a conda environment using `conda create -n manim python=3.8`. 2. Create a conda environment using `conda create -n manim python=3.8`.
* Activate the environment using `conda activate manim`. 3. Activate the environment using `conda activate manim`.
* Install manimgl using `pip install -e .`. 4. Install manimgl using `pip install -e .`.
## Using manim ## Using manim

View file

@ -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. - `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. a more open contribution community.
About this documentation About this documentation

View file

@ -1,4 +1,44 @@
Changelog 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``

View file

@ -56,7 +56,7 @@ custom_config
- ``raster_images`` - ``raster_images``
The directory for storing raster images to be used in the code (including 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`` - ``vector_images``
The directory for storing vector images to be used in the code (including 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 the first character means upper(U) / middle(O) / lower(D), the second character
means left(L) / middle(O) / right(R)). 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`` ``break_into_partial_movies``
----------------------------- -----------------------------

View file

@ -58,6 +58,7 @@ flag abbr function
``--color COLOR`` ``-c`` Background color ``--color COLOR`` ``-c`` Background color
``--leave_progress_bars`` Leave progress bars displayed in terminal ``--leave_progress_bars`` Leave progress bars displayed in terminal
``--video_dir VIDEO_DIR`` directory to write video ``--video_dir VIDEO_DIR`` directory to write video
``--config_file CONFIG_FILE`` Path to the custom configuration file
========================================================== ====== ================================================================================================================================================================================================= ========================================================== ====== =================================================================================================================================================================================================
custom_config custom_config
@ -85,5 +86,11 @@ following the directory structure:
└── custom_config.yml └── custom_config.yml
When you enter the ``project/`` folder and run ``manimgl code.py <Scene>``, When you enter the ``project/`` folder and run ``manimgl code.py <Scene>``,
it will overwrite ``manim/custom_config.yml`` with ``custom_config.yml`` it will overwrite ``manim/default_config.yml`` with ``custom_config.yml``
in the ``project`` folder. 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

View file

@ -8,12 +8,12 @@ the simplest and one by one.
InteractiveDevlopment InteractiveDevlopment
--------------------- ---------------------
.. manim-example:: InteractiveDevlopment .. manim-example:: InteractiveDevelopment
:media: ../_static/example_scenes/InteractiveDevlopment.mp4 :media: ../_static/example_scenes/InteractiveDevelopment.mp4
from manimlib import * from manimlib import *
class InteractiveDevlopment(Scene): class InteractiveDevelopment(Scene):
def construct(self): def construct(self):
circle = Circle() circle = Circle()
circle.set_fill(BLUE, opacity=0.5) circle.set_fill(BLUE, opacity=0.5)
@ -128,6 +128,8 @@ TextExample
class TextExample(Scene): class TextExample(Scene):
def construct(self): 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) text = Text("Here is a text", font="Consolas", font_size=90)
difference = Text( difference = Text(
""" """
@ -135,6 +137,7 @@ TextExample
you can change the font more easily, but can't use the LaTeX grammar you can change the font more easily, but can't use the LaTeX grammar
""", """,
font="Arial", font_size=24, font="Arial", font_size=24,
# t2c is a dict that you can choose color for different text
t2c={"Text": BLUE, "TexText": BLUE, "LaTeX": ORANGE} t2c={"Text": BLUE, "TexText": BLUE, "LaTeX": ORANGE}
) )
VGroup(text, difference).arrange(DOWN, buff=1) VGroup(text, difference).arrange(DOWN, buff=1)
@ -148,6 +151,7 @@ TextExample
t2f={"font": "Consolas", "words": "Consolas"}, t2f={"font": "Consolas", "words": "Consolas"},
t2c={"font": BLUE, "words": GREEN} t2c={"font": BLUE, "words": GREEN}
) )
fonts.set_width(FRAME_WIDTH - 1)
slant = Text( slant = Text(
"And the same as slant and weight", "And the same as slant and weight",
font="Consolas", font="Consolas",
@ -180,20 +184,24 @@ TexTransformExample
def construct(self): def construct(self):
to_isolate = ["B", "C", "=", "(", ")"] to_isolate = ["B", "C", "=", "(", ")"]
lines = VGroup( lines = VGroup(
# Surrounding substrings with double braces # Passing in muliple arguments to Tex will result
# will ensure that those parts are separated # in the same expression as if those arguments had
# out in the Tex. For example, here the # been joined together, except that the submobject
# Tex will have 5 submobjects, corresponding # heirarchy of the resulting mobject ensure that the
# to the strings [A^2, +, B^2, =, C^2] # Tex mobject has a subject corresponding to
Tex("{{A^2}} + {{B^2}} = {{C^2}}"), # each of these strings. For example, the Tex mobject
Tex("{{A^2}} = {{C^2}} - {{B^2}}"), # 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 # Alternatively, you can pass in the keyword argument
# "isolate" with a list of strings that should be out as # "isolate" with a list of strings that should be out as
# their own submobject. So both lines below are equivalent # their own submobject. So the line below is equivalent
# to what you'd get by wrapping every instance of "B", "C" # to the commented out line below it.
# "=", "(" and ")" with double braces Tex("A^2 = (C + B)(C - B)", isolate=["A^2", *to_isolate]),
Tex("{{A^2}} = (C + B)(C - B)", isolate=to_isolate), # Tex("A^2", "=", "(", "C", "+", "B", ")", "(", "C", "-", "B", ")"),
Tex("A = \\sqrt{(C + B)(C - B)}", isolate=to_isolate) Tex("A = \\sqrt{(C + B)(C - B)}", isolate=["A", *to_isolate])
) )
lines.arrange(DOWN, buff=LARGE_BUFF) lines.arrange(DOWN, buff=LARGE_BUFF)
for line in lines: for line in lines:
@ -252,7 +260,7 @@ TexTransformExample
# new_line2 and the "\sqrt" from the final line. By passing in, # new_line2 and the "\sqrt" from the final line. By passing in,
# transform_mismatches=True, it will transform this "^2" part into # transform_mismatches=True, it will transform this "^2" part into
# the "\sqrt" part. # 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.replace(lines[2])
new_line2.match_style(lines[2]) new_line2.match_style(lines[2])
@ -343,7 +351,7 @@ UpdatersExample
) )
self.wait() self.wait()
self.play( self.play(
square.set_width(5, stretch=True), square.animate.set_width(5, stretch=True),
run_time=3, run_time=3,
) )
self.wait() self.wait()
@ -387,7 +395,7 @@ CoordinateSystemExample
axes = Axes( axes = Axes(
# x-axis ranges from -1 to 10, with a default step size of 1 # x-axis ranges from -1 to 10, with a default step size of 1
x_range=(-1, 10), 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), y_range=(-2, 2, 0.5),
# The axes will be stretched so as to match the specified # The axes will be stretched so as to match the specified
# height and width # height and width
@ -450,8 +458,7 @@ CoordinateSystemExample
# system defined by them. # system defined by them.
f_always(dot.move_to, lambda: axes.c2p(1, 1)) f_always(dot.move_to, lambda: axes.c2p(1, 1))
self.play( self.play(
axes.animate.scale(0.75), axes.animate.scale(0.75).to_corner(UL),
axes.animate.to_corner(UL),
run_time=2, run_time=2,
) )
self.wait() self.wait()

View file

@ -1,18 +1,27 @@
Installation Installation
============ ============
Manim runs on Python 3.8. Manim runs on Python 3.6 or higher (Python 3.8 is recommended).
System requirements are System requirements are
- `FFmpeg <https://ffmpeg.org/>`__ - `FFmpeg <https://ffmpeg.org/>`__
- `OpenGL <https://www.opengl.org//>`__ (included in python package ``PyOpenGL``) - `OpenGL <https://www.opengl.org//>`__ (included in python package ``PyOpenGL``)
- `LaTeX <https://www.latex-project.org>`__ (optional, if you want to use LaTeX) - `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 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 If you want to hack on manimlib itself, clone this repository and in
that directory execute: that directory execute:

View file

@ -221,7 +221,7 @@ For example: input the following lines (without comment lines) into it respectiv
.. code-block:: python .. code-block:: python
# Stretched 4 times in the vertical direction # 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° # Rotate the ellipse 90°
play(Rotate(circle, TAU / 4)) play(Rotate(circle, TAU / 4))
# Move 2 units to the right and shrink to 1/4 of the original # Move 2 units to the right and shrink to 1/4 of the original

View file

@ -59,7 +59,7 @@ Below is the directory structure of manim:
│ │ ├── brace.py # Brace │ │ ├── brace.py # Brace
│ │ ├── drawings.py # Some special mobject of svg image │ │ ├── drawings.py # Some special mobject of svg image
│ │ ├── tex_mobject.py # Tex and TexText implemented by LaTeX │ │ ├── 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 │ ├── changing.py # Dynamically changing mobject
│ ├── coordinate_systems.py # coordinate system │ ├── coordinate_systems.py # coordinate system
│ ├── frame.py # mobject related to frame │ ├── frame.py # mobject related to frame
@ -99,6 +99,7 @@ Below is the directory structure of manim:
├── config_ops.py # Process CONFIG ├── config_ops.py # Process CONFIG
├── customization.py # Read from custom_config.yml ├── customization.py # Read from custom_config.yml
├── debug.py # Utilities for debugging in program ├── debug.py # Utilities for debugging in program
├── directories.py # Read directories from config file
├── family_ops.py # Process family members ├── family_ops.py # Process family members
├── file_ops.py # Process files and directories ├── file_ops.py # Process files and directories
├── images.py # Read image ├── images.py # Read image

View file

@ -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 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/>`_. 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:: .. toctree::
:maxdepth: 2 :maxdepth: 2

View file

@ -161,20 +161,24 @@ class TexTransformExample(Scene):
def construct(self): def construct(self):
to_isolate = ["B", "C", "=", "(", ")"] to_isolate = ["B", "C", "=", "(", ")"]
lines = VGroup( lines = VGroup(
# Surrounding substrings with double braces # Passing in muliple arguments to Tex will result
# will ensure that those parts are separated # in the same expression as if those arguments had
# out in the Tex. For example, here the # been joined together, except that the submobject
# Tex will have 5 submobjects, corresponding # heirarchy of the resulting mobject ensure that the
# to the strings [A^2, +, B^2, =, C^2] # Tex mobject has a subject corresponding to
Tex("{{A^2}} + {{B^2}} = {{C^2}}"), # each of these strings. For example, the Tex mobject
Tex("{{A^2}} = {{C^2}} - {{B^2}}"), # 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 # Alternatively, you can pass in the keyword argument
# "isolate" with a list of strings that should be out as # "isolate" with a list of strings that should be out as
# their own submobject. So both lines below are equivalent # their own submobject. So the line below is equivalent
# to what you'd get by wrapping every instance of "B", "C" # to the commented out line below it.
# "=", "(" and ")" with double braces Tex("A^2 = (C + B)(C - B)", isolate=["A^2", *to_isolate]),
Tex("{{A^2}} = (C + B)(C - B)", isolate=to_isolate), # Tex("A^2", "=", "(", "C", "+", "B", ")", "(", "C", "-", "B", ")"),
Tex("A = \\sqrt{(C + B)(C - B)}", isolate=to_isolate) Tex("A = \\sqrt{(C + B)(C - B)}", isolate=["A", *to_isolate])
) )
lines.arrange(DOWN, buff=LARGE_BUFF) lines.arrange(DOWN, buff=LARGE_BUFF)
for line in lines: for line in lines:
@ -233,7 +237,7 @@ class TexTransformExample(Scene):
# new_line2 and the "\sqrt" from the final line. By passing in, # new_line2 and the "\sqrt" from the final line. By passing in,
# transform_mismatches=True, it will transform this "^2" part into # transform_mismatches=True, it will transform this "^2" part into
# the "\sqrt" part. # 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.replace(lines[2])
new_line2.match_style(lines[2]) new_line2.match_style(lines[2])
@ -311,7 +315,7 @@ class UpdatersExample(Scene):
) )
self.wait() self.wait()
self.play( self.play(
square.set_width(5, stretch=True), square.animate.set_width(5, stretch=True),
run_time=3, run_time=3,
) )
self.wait() self.wait()
@ -338,7 +342,7 @@ class CoordinateSystemExample(Scene):
axes = Axes( axes = Axes(
# x-axis ranges from -1 to 10, with a default step size of 1 # x-axis ranges from -1 to 10, with a default step size of 1
x_range=(-1, 10), 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), y_range=(-2, 2, 0.5),
# The axes will be stretched so as to match the specified # The axes will be stretched so as to match the specified
# height and width # height and width
@ -401,8 +405,7 @@ class CoordinateSystemExample(Scene):
# system defined by them. # system defined by them.
f_always(dot.move_to, lambda: axes.c2p(1, 1)) f_always(dot.move_to, lambda: axes.c2p(1, 1))
self.play( self.play(
axes.animate.scale(0.75), axes.animate.scale(0.75).to_corner(UL),
axes.animate.to_corner(UL),
run_time=2, run_time=2,
) )
self.wait() self.wait()
@ -584,7 +587,7 @@ class SurfaceExample(Scene):
self.wait() self.wait()
class InteractiveDevlopment(Scene): class InteractiveDevelopment(Scene):
def construct(self): def construct(self):
circle = Circle() circle = Circle()
circle.set_fill(BLUE, opacity=0.5) circle.set_fill(BLUE, opacity=0.5)

View file

@ -15,3 +15,6 @@ def main():
for scene in scenes: for scene in scenes:
scene.run() scene.run()
if __name__ == '__main__':
main()

View file

@ -25,6 +25,10 @@ class ShowPartial(Animation):
if not self.should_match_start: if not self.should_match_start:
self.mobject.lock_matching_data(self.mobject, self.starting_mobject) 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): def interpolate_submobject(self, submob, start_submob, alpha):
submob.pointwise_become_partial( submob.pointwise_become_partial(
start_submob, *self.get_bounds(alpha) start_submob, *self.get_bounds(alpha)

View file

@ -1,4 +1,5 @@
import numpy as np import numpy as np
import math
from manimlib.constants import * from manimlib.constants import *
from manimlib.animation.animation import Animation 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 Circle
from manimlib.mobject.geometry import Dot from manimlib.mobject.geometry import Dot
from manimlib.mobject.shape_matchers import SurroundingRectangle 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.types.vectorized_mobject import VGroup
from manimlib.mobject.geometry import Line from manimlib.mobject.geometry import Line
from manimlib.utils.bezier import interpolate from manimlib.utils.bezier import interpolate
@ -152,6 +154,87 @@ class ShowPassingFlash(ShowPartial):
submob.pointwise_become_partial(start, 0, 1) 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): class ShowCreationThenDestruction(ShowPassingFlash):
CONFIG = { CONFIG = {
"time_width": 2.0, "time_width": 2.0,

View file

@ -10,7 +10,6 @@ from manimlib.mobject.mobject import Mobject
from manimlib.mobject.mobject import Group from manimlib.mobject.mobject import Group
from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.utils.config_ops import digest_config from manimlib.utils.config_ops import digest_config
@ -129,7 +128,7 @@ class TransformMatchingShapes(TransformMatchingParts):
class TransformMatchingTex(TransformMatchingParts): class TransformMatchingTex(TransformMatchingParts):
CONFIG = { CONFIG = {
"mobject_type": Tex, "mobject_type": VMobject,
"group_type": VGroup, "group_type": VGroup,
} }

View file

@ -77,16 +77,24 @@ class CameraFrame(Mobject):
self.set_euler_angles(theta, phi, gamma) self.set_euler_angles(theta, phi, gamma)
return self 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: if theta is not None:
self.data["euler_angles"][0] = theta self.data["euler_angles"][0] = theta * units
if phi is not None: if phi is not None:
self.data["euler_angles"][1] = phi self.data["euler_angles"][1] = phi * units
if gamma is not None: if gamma is not None:
self.data["euler_angles"][2] = gamma self.data["euler_angles"][2] = gamma * units
self.refresh_rotation_matrix() self.refresh_rotation_matrix()
return self 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): def set_theta(self, theta):
return self.set_euler_angles(theta=theta) return self.set_euler_angles(theta=theta)

View file

@ -130,7 +130,11 @@ def parse_cli():
) )
parser.add_argument( parser.add_argument(
"--video_dir", "--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() args = parser.parse_args()
return args return args
@ -155,17 +159,19 @@ def get_module(file_name):
spec.loader.exec_module(module) spec.loader.exec_module(module)
return module return module
__config_file__ = "custom_config.yml"
def get_custom_config(): def get_custom_config():
filename = "custom_config.yml" global __config_file__
global_defaults_file = os.path.join(get_manim_dir(), "manimlib", "default_config.yml") global_defaults_file = os.path.join(get_manim_dir(), "manimlib", "default_config.yml")
if os.path.exists(global_defaults_file): if os.path.exists(global_defaults_file):
with open(global_defaults_file, "r") as file: with open(global_defaults_file, "r") as file:
config = yaml.safe_load(file) config = yaml.safe_load(file)
if os.path.exists(filename): if os.path.exists(__config_file__):
with open(filename, "r") as file: with open(__config_file__, "r") as file:
local_defaults = yaml.safe_load(file) local_defaults = yaml.safe_load(file)
if local_defaults: if local_defaults:
config = merge_dicts_recursively( config = merge_dicts_recursively(
@ -173,22 +179,41 @@ def get_custom_config():
local_defaults, local_defaults,
) )
else: else:
with open(filename, "r") as file: with open(__config_file__, "r") as file:
config = yaml.safe_load(file) config = yaml.safe_load(file)
return config return config
def get_configuration(args): 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") 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") print("There is no configuration file detected. Initial configuration:\n")
init_customization() 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} elif not os.path.exists(__config_file__):
If you want to create a local configuration file, you can create a file named {local_config_file}, or run manimgl --config 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() custom_config = get_custom_config()
write_file = any([args.write_file, args.open, args.finder]) 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 # Default to making window half the screen size
# but make it full screen if -f is passed in # 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 window_width = monitor.width
if not args.full_screen: if not args.full_screen:
window_width //= 2 window_width //= 2

View file

@ -50,6 +50,9 @@ RIGHT_SIDE = FRAME_X_RADIUS * RIGHT
PI = np.pi PI = np.pi
TAU = 2 * PI TAU = 2 * PI
DEGREES = TAU / 360 DEGREES = TAU / 360
# Nice to have a constant for readability
# when juxtaposed with expressions like 30 * DEGREES
RADIANS = 1
FFMPEG_BIN = "ffmpeg" FFMPEG_BIN = "ffmpeg"
@ -136,3 +139,5 @@ RED = RED_C
MAROON = MAROON_C MAROON = MAROON_C
PURPLE = PURPLE_C PURPLE = PURPLE_C
GREY = GREY_C GREY = GREY_C
COLORMAP_3B1B = [BLUE_E, GREEN, YELLOW, RED]

View file

@ -25,8 +25,8 @@ class CoordinateSystem():
""" """
CONFIG = { CONFIG = {
"dimension": 2, "dimension": 2,
"x_range": np.array([-8, 8, 1.0]), "x_range": np.array([-8.0, 8.0, 1.0]),
"y_range": np.array([-4, 4, 1.0]), "y_range": np.array([-4.0, 4.0, 1.0]),
"width": None, "width": None,
"height": None, "height": None,
"num_sampled_graph_points_per_tick": 5, "num_sampled_graph_points_per_tick": 5,
@ -46,9 +46,15 @@ class CoordinateSystem():
"""Abbreviation for point_to_coords""" """Abbreviation for point_to_coords"""
return self.point_to_coords(point) return self.point_to_coords(point)
def get_origin(self):
return self.c2p(*[0] * self.dimension)
def get_axes(self): def get_axes(self):
raise Exception("Not implemented") raise Exception("Not implemented")
def get_all_ranges(self):
raise Exception("Not implemented")
def get_axis(self, index): def get_axis(self, index):
return self.get_axes()[index] return self.get_axes()[index]
@ -319,6 +325,9 @@ class Axes(VGroup, CoordinateSystem):
def get_axes(self): def get_axes(self):
return self.axes return self.axes
def get_all_ranges(self):
return [self.x_range, self.y_range]
def add_coordinate_labels(self, def add_coordinate_labels(self,
x_values=None, x_values=None,
y_values=None, y_values=None,
@ -334,11 +343,13 @@ class Axes(VGroup, CoordinateSystem):
class ThreeDAxes(Axes): class ThreeDAxes(Axes):
CONFIG = { CONFIG = {
"dimension": 3, "dimension": 3,
"x_range": np.array([-6, 6, 1]), "x_range": np.array([-6.0, 6.0, 1.0]),
"y_range": np.array([-5, 5, 1]), "y_range": np.array([-5.0, 5.0, 1.0]),
"z_range": np.array([-4, 4, 1]), "z_range": np.array([-4.0, 4.0, 1.0]),
"z_axis_config": {}, "z_axis_config": {},
"z_normal": DOWN, "z_normal": DOWN,
"height": None,
"width": None,
"depth": None, "depth": None,
"num_axis_pieces": 20, "num_axis_pieces": 20,
"gloss": 0.5, "gloss": 0.5,
@ -346,9 +357,11 @@ class ThreeDAxes(Axes):
def __init__(self, x_range=None, y_range=None, z_range=None, **kwargs): def __init__(self, x_range=None, y_range=None, z_range=None, **kwargs):
Axes.__init__(self, x_range, y_range, **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_axis = self.create_axis(
z_range or self.z_range, self.z_range,
self.z_axis_config, self.z_axis_config,
self.depth, self.depth,
) )
@ -365,6 +378,9 @@ class ThreeDAxes(Axes):
for axis in self.axes: for axis in self.axes:
axis.insert_n_curves(self.num_axis_pieces - 1) 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): class NumberPlane(Axes):
CONFIG = { CONFIG = {

View file

@ -20,6 +20,9 @@ class ScreenRectangle(Rectangle):
class FullScreenRectangle(ScreenRectangle): class FullScreenRectangle(ScreenRectangle):
CONFIG = { CONFIG = {
"height": FRAME_HEIGHT, "height": FRAME_HEIGHT,
"fill_color": GREY_E,
"fill_opacity": 1,
"stroke_width": 0,
} }

View file

@ -49,6 +49,7 @@ class TipableVMobject(VMobject):
"tip_config": { "tip_config": {
"fill_opacity": 1, "fill_opacity": 1,
"stroke_width": 0, "stroke_width": 0,
"tip_style": 0, # triangle=0, inner_smooth=1, dot=2
}, },
"normal_vector": OUT, "normal_vector": OUT,
} }
@ -63,6 +64,7 @@ class TipableVMobject(VMobject):
tip = self.create_tip(at_start, **kwargs) tip = self.create_tip(at_start, **kwargs)
self.reset_endpoints_based_on_tip(tip, at_start) self.reset_endpoints_based_on_tip(tip, at_start)
self.asign_tip_attr(tip, at_start) self.asign_tip_attr(tip, at_start)
tip.set_color(self.get_stroke_color())
self.add(tip) self.add(tip)
return self return self
@ -569,7 +571,7 @@ class Elbow(VMobject):
} }
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(self, **kwargs) super().__init__(**kwargs)
self.set_points_as_corners([UP, UP + RIGHT, RIGHT]) self.set_points_as_corners([UP, UP + RIGHT, RIGHT])
self.set_width(self.width, about_point=ORIGIN) self.set_width(self.width, about_point=ORIGIN)
self.rotate(self.angle, about_point=ORIGIN) self.rotate(self.angle, about_point=ORIGIN)
@ -786,12 +788,20 @@ class ArrowTip(Triangle):
"width": DEFAULT_ARROW_TIP_WIDTH, "width": DEFAULT_ARROW_TIP_WIDTH,
"length": DEFAULT_ARROW_TIP_LENGTH, "length": DEFAULT_ARROW_TIP_LENGTH,
"angle": 0, "angle": 0,
"tip_style": 0, # triangle=0, inner_smooth=1, dot=2
} }
def __init__(self, **kwargs): def __init__(self, **kwargs):
Triangle.__init__(self, start_angle=0, **kwargs) Triangle.__init__(self, start_angle=0, **kwargs)
self.set_height(self.width) self.set_height(self.width)
self.set_width(self.length, stretch=True) 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) self.rotate(self.angle)
def get_base(self): def get_base(self):

View file

@ -57,13 +57,13 @@ class Matrix(VMobject):
CONFIG = { CONFIG = {
"v_buff": 0.8, "v_buff": 0.8,
"h_buff": 1.3, "h_buff": 1.3,
"bracket_h_buff": MED_SMALL_BUFF, "bracket_h_buff": 0.2,
"bracket_v_buff": MED_SMALL_BUFF, "bracket_v_buff": 0.25,
"add_background_rectangles_to_entries": False, "add_background_rectangles_to_entries": False,
"include_background_rectangle": False, "include_background_rectangle": False,
"element_to_mobject": Tex, "element_to_mobject": Tex,
"element_to_mobject_config": {}, "element_to_mobject_config": {},
"element_alignment_corner": DR, "element_alignment_corner": DOWN,
} }
def __init__(self, matrix, **kwargs): def __init__(self, matrix, **kwargs):
@ -132,6 +132,12 @@ class Matrix(VMobject):
for i in range(len(self.mob_matrix[0])) 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): def set_column_colors(self, *colors):
columns = self.get_columns() columns = self.get_columns()
for color, column in zip(colors, columns): for color, column in zip(colors, columns):
@ -163,6 +169,7 @@ class DecimalMatrix(Matrix):
class IntegerMatrix(Matrix): class IntegerMatrix(Matrix):
CONFIG = { CONFIG = {
"element_to_mobject": Integer, "element_to_mobject": Integer,
"element_alignment_corner": UP,
} }

View file

@ -176,6 +176,7 @@ class Mobject(object):
def match_points(self, mobject): def match_points(self, mobject):
self.set_points(mobject.get_points()) self.set_points(mobject.get_points())
return self
def get_points(self): def get_points(self):
return self.data["points"] return self.data["points"]
@ -504,7 +505,7 @@ class Mobject(object):
self.refresh_has_updater_status() self.refresh_has_updater_status()
if call_updater: if call_updater:
self.update() self.update(dt=0)
return self return self
def remove_updater(self, update_function): def remove_updater(self, update_function):
@ -561,7 +562,7 @@ class Mobject(object):
) )
return self 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. Default behavior is to scale about the center of the mobject.
The argument about_edge can be a vector, indicating which side of 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 Otherwise, if about_point is given a value, scaling is done with
respect to that point. respect to that point.
""" """
scale_factor = max(scale_factor, min_scale_factor)
self.apply_points_function( self.apply_points_function(
lambda points: scale_factor * points, lambda points: scale_factor * points,
works_on_bounding_box=True, works_on_bounding_box=True,
@ -841,7 +843,30 @@ class Mobject(object):
# Color functions # 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: if color is not None:
rgbs = np.array([color_to_rgb(c) for c in listify(color)]) rgbs = np.array([color_to_rgb(c) for c in listify(color)])
if opacity is not None: if opacity is not None:
@ -870,8 +895,8 @@ class Mobject(object):
return self return self
def set_color(self, color, opacity=None, recurse=True): def set_color(self, color, opacity=None, recurse=True):
self.set_rgba_array(color, opacity, recurse=False) self.set_rgba_array_by_color(color, opacity, recurse=False)
# Recurse to submobjects differently from how set_rgba_array # Recurse to submobjects differently from how set_rgba_array_by_color
# in case they implement set_color differently # in case they implement set_color differently
if recurse: if recurse:
for submob in self.submobjects: for submob in self.submobjects:
@ -879,7 +904,7 @@ class Mobject(object):
return self return self
def set_opacity(self, opacity, recurse=True): 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: if recurse:
for submob in self.submobjects: for submob in self.submobjects:
submob.set_opacity(opacity, recurse=True) submob.set_opacity(opacity, recurse=True)

View file

@ -5,7 +5,6 @@ from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.utils.bezier import interpolate from manimlib.utils.bezier import interpolate
from manimlib.utils.config_ops import digest_config from manimlib.utils.config_ops import digest_config
from manimlib.utils.config_ops import merge_dicts_recursively 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.simple_functions import fdiv
from manimlib.utils.space_ops import normalize from manimlib.utils.space_ops import normalize
@ -144,7 +143,7 @@ class NumberLine(Line):
direction=direction, direction=direction,
buff=buff 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 # Align without the minus sign
num_mob.shift(num_mob[0].get_width() * LEFT / 2) num_mob.shift(num_mob[0].get_width() * LEFT / 2)
return num_mob return num_mob
@ -155,10 +154,11 @@ class NumberLine(Line):
kwargs["font_size"] = font_size kwargs["font_size"] = font_size
if excluding is None:
excluding = self.numbers_to_exclude
numbers = VGroup() numbers = VGroup()
for x in x_values: for x in x_values:
if x in self.numbers_to_exclude:
continue
if excluding is not None and x in excluding: if excluding is not None and x in excluding:
continue continue
numbers.add(self.get_number_mobject(x, **kwargs)) numbers.add(self.get_number_mobject(x, **kwargs))

View file

@ -128,10 +128,11 @@ class DecimalNumber(VMobject):
def set_value(self, number): def set_value(self, number):
move_to_point = self.get_edge_center(self.edge_to_fix) 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.set_submobjects_from_number(number)
self.move_to(move_to_point, self.edge_to_fix) 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 return self
def scale(self, scale_factor, **kwargs): def scale(self, scale_factor, **kwargs):

View file

@ -64,16 +64,17 @@ class BackgroundRectangle(SurroundingRectangle):
class Cross(VGroup): class Cross(VGroup):
CONFIG = { CONFIG = {
"stroke_color": RED, "stroke_color": RED,
"stroke_width": 6, "stroke_width": [0, 6, 0],
} }
def __init__(self, mobject, **kwargs): def __init__(self, mobject, **kwargs):
VGroup.__init__(self, super().__init__(
Line(UP + LEFT, DOWN + RIGHT), Line(UL, DR),
Line(UP + RIGHT, DOWN + LEFT), Line(UR, DL),
) )
self.insert_n_curves(2)
self.replace(mobject, stretch=True) 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): class Underline(Line):

View file

@ -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 Tex
from manimlib.mobject.svg.tex_mobject import SingleStringTex from manimlib.mobject.svg.tex_mobject import SingleStringTex
from manimlib.mobject.svg.tex_mobject import TexText 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.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.config_ops import digest_config from manimlib.utils.config_ops import digest_config
from manimlib.utils.space_ops import get_norm from manimlib.utils.space_ops import get_norm
@ -61,9 +62,10 @@ class Brace(SingleStringTex):
mob.shift(self.get_direction() * shift_distance) mob.shift(self.get_direction() * shift_distance)
return self return self
def get_text(self, *text, **kwargs): def get_text(self, text, **kwargs):
text_mob = TexText(*text) buff = kwargs.pop("buff", SMALL_BUFF)
self.put_at_tip(text_mob, **kwargs) text_mob = Text(text, **kwargs)
self.put_at_tip(text_mob, buff=buff)
return text_mob return text_mob
def get_tex(self, *tex, **kwargs): def get_tex(self, *tex, **kwargs):

View file

@ -41,7 +41,6 @@ class Exmark(TexText):
class Lightbulb(SVGMobject): class Lightbulb(SVGMobject):
CONFIG = { CONFIG = {
"file_name": "lightbulb",
"height": 1, "height": 1,
"stroke_color": YELLOW, "stroke_color": YELLOW,
"stroke_width": 3, "stroke_width": 3,
@ -49,6 +48,9 @@ class Lightbulb(SVGMobject):
"fill_opacity": 0, "fill_opacity": 0,
} }
def __init__(self, **kwargs):
super().__init__("lightbulb", **kwargs)
class Speedometer(VMobject): class Speedometer(VMobject):
CONFIG = { CONFIG = {

View file

@ -169,7 +169,11 @@ class SVGMobject(VMobject):
else 0.0 else 0.0
for key in ("cx", "cy", "rx", "ry") 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): def rect_to_mobject(self, rect_element):
fill_color = rect_element.getAttribute("fill") fill_color = rect_element.getAttribute("fill")

View file

@ -65,6 +65,8 @@ class SingleStringTex(VMobject):
if self.math_mode: if self.math_mode:
new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}" new_tex = "\\begin{align*}\n" + new_tex + "\n\\end{align*}"
new_tex = self.alignment + "\n" + new_tex
tex_config = get_tex_config() tex_config = get_tex_config()
return tex_config["tex_body"].replace( return tex_config["tex_body"].replace(
tex_config["text_to_replace"], tex_config["text_to_replace"],
@ -72,10 +74,7 @@ class SingleStringTex(VMobject):
) )
def get_modified_expression(self, tex_string): def get_modified_expression(self, tex_string):
result = self.alignment + " " + tex_string return self.modify_special_strings(tex_string.strip())
result = result.strip()
result = self.modify_special_strings(result)
return result
def modify_special_strings(self, tex): def modify_special_strings(self, tex):
tex = tex.strip() tex = tex.strip()
@ -153,9 +152,6 @@ class SingleStringTex(VMobject):
class Tex(SingleStringTex): class Tex(SingleStringTex):
CONFIG = { CONFIG = {
"arg_separator": "", "arg_separator": "",
# Note, use of isolate is largely rendered
# moot by the fact that you can surround such strings in
# {{ and }} as needed.
"isolate": [], "isolate": [],
"tex_to_color_map": {}, "tex_to_color_map": {},
} }
@ -172,18 +168,22 @@ class Tex(SingleStringTex):
self.organize_submobjects_left_to_right() self.organize_submobjects_left_to_right()
def break_up_tex_strings(self, tex_strings): def break_up_tex_strings(self, tex_strings):
# Separate out anything surrounded in double braces
patterns = ["{{", "}}"]
# Separate out any strings specified in the isolate # Separate out any strings specified in the isolate
# or tex_to_color_map lists. # 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)) "({})".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) pattern = "|".join(patterns)
pieces = [] pieces = []
for s in tex_strings: for s in tex_strings:
if pattern:
pieces.extend(re.split(pattern, s)) pieces.extend(re.split(pattern, s))
else:
pieces.append(s)
return list(filter(lambda s: s, pieces)) return list(filter(lambda s: s, pieces))
def break_up_by_substrings(self): def break_up_by_substrings(self):

View file

@ -10,6 +10,7 @@ import manimpango
from manimlib.constants import * from manimlib.constants import *
from manimlib.mobject.geometry import Dot from manimlib.mobject.geometry import Dot
from manimlib.mobject.svg.svg_mobject import SVGMobject 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.config_ops import digest_config
from manimlib.utils.customization import get_customization from manimlib.utils.customization import get_customization
from manimlib.utils.directories import get_downloads_dir, get_text_dir 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)) index = self.text.find(word, index + len(word))
return indexes 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): def full2short(self, config):
for kwargs in [config, self.CONFIG]: for kwargs in [config, self.CONFIG]:
if kwargs.__contains__('line_spacing_height'): if kwargs.__contains__('line_spacing_height'):
@ -212,6 +226,7 @@ class Text(SVGMobject):
self.text, self.text,
) )
@contextmanager @contextmanager
def register_font(font_file: typing.Union[str, Path]): def register_font(font_file: typing.Union[str, Path]):
"""Temporarily add a font file to Pango's search 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`. This method of adding font files also works with :class:`CairoText`.
.. important :: .. important ::
This method isn't available for macOS. Using this This method is available for macOS for ``ManimPango>=v0.2.3``. Using this
method on macOS will raise an :class:`AttributeError`. method with previous releases will raise an :class:`AttributeError` on macOS.
""" """
input_folder = Path(get_downloads_dir()).parent.resolve() input_folder = Path(get_downloads_dir()).parent.resolve()

View file

@ -52,7 +52,7 @@ class VMobject(Mobject):
"fill_shader_folder": "quadratic_bezier_fill", "fill_shader_folder": "quadratic_bezier_fill",
# Could also be "bevel", "miter", "round" # Could also be "bevel", "miter", "round"
"joint_type": "auto", "joint_type": "auto",
"flat_stroke": True, "flat_stroke": False,
"render_primitive": moderngl.TRIANGLES, "render_primitive": moderngl.TRIANGLES,
"triangulation_locked": False, "triangulation_locked": False,
"fill_dtype": [ "fill_dtype": [
@ -106,24 +106,42 @@ class VMobject(Mobject):
self.set_flat_stroke(self.flat_stroke) self.set_flat_stroke(self.flat_stroke)
return self 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): 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 return self
def set_stroke(self, color=None, width=None, opacity=None, background=None, recurse=True): 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: if width is not None:
for mob in self.get_family(recurse): for mob in self.get_family(recurse):
mob.data['stroke_width'] = np.array([ if isinstance(width, np.ndarray):
[width] for width in listify(width) 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: if background is not None:
for mob in self.get_family(recurse): for mob in self.get_family(recurse):
mob.draw_stroke_behind_fill = background mob.draw_stroke_behind_fill = background
return self 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, def set_style(self,
fill_color=None, fill_color=None,
fill_opacity=None, fill_opacity=None,
@ -226,7 +244,7 @@ class VMobject(Mobject):
return self.data['stroke_rgba'][:, 3] return self.data['stroke_rgba'][:, 3]
def get_stroke_widths(self): 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 # TODO, it's weird for these to return the first of various lists
# rather than the full information # rather than the full information
@ -259,7 +277,7 @@ class VMobject(Mobject):
return self.get_fill_color() return self.get_fill_color()
def has_stroke(self): 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): def has_fill(self):
return any(self.get_fill_opacities()) return any(self.get_fill_opacities())
@ -830,8 +848,8 @@ class VMobject(Mobject):
old_points = self.get_points() old_points = self.get_points()
func(self, *args, **kwargs) func(self, *args, **kwargs)
if not np.all(self.get_points() == old_points): if not np.all(self.get_points() == old_points):
self.refresh_triangulation()
self.refresh_unit_normal() self.refresh_unit_normal()
self.refresh_triangulation()
return wrapper return wrapper
@triggers_refreshed_triangulation @triggers_refreshed_triangulation
@ -852,9 +870,10 @@ class VMobject(Mobject):
self.make_approximately_smooth() self.make_approximately_smooth()
return self return self
@triggers_refreshed_triangulation
def flip(self, *args, **kwargs): def flip(self, *args, **kwargs):
super().flip(*args, **kwargs) super().flip(*args, **kwargs)
self.refresh_unit_normal()
self.refresh_triangulation()
return self return self
# For shaders # For shaders

View file

@ -1,66 +1,32 @@
import numpy as np import numpy as np
import os
import itertools as it import itertools as it
from PIL import Image
import random import random
from manimlib.constants import * from manimlib.constants import *
from manimlib.animation.composition import AnimationGroup from manimlib.animation.composition import AnimationGroup
from manimlib.animation.indication import ShowPassingFlash from manimlib.animation.indication import VShowPassingFlash
from manimlib.mobject.geometry import Vector from manimlib.mobject.geometry import Arrow
from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.bezier import inverse_interpolate from manimlib.utils.bezier import inverse_interpolate
from manimlib.utils.bezier import interpolate from manimlib.utils.bezier import interpolate
from manimlib.utils.color import color_to_rgb from manimlib.utils.color import get_colormap_list
from manimlib.utils.color import rgb_to_color from manimlib.utils.config_ops import merge_dicts_recursively
from manimlib.utils.config_ops import digest_config from manimlib.utils.config_ops import digest_config
from manimlib.utils.rate_functions import linear from manimlib.utils.rate_functions import linear
from manimlib.utils.simple_functions import sigmoid from manimlib.utils.simple_functions import sigmoid
from manimlib.utils.space_ops import get_norm 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_vectorized_rgb_gradient_function(min_value, max_value, color_map):
rgbs = np.array(get_colormap_list(color_map))
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 func(values): def func(values):
alphas = inverse_interpolate( alphas = inverse_interpolate(
min_value, max_value, np.array(values) min_value, max_value, np.array(values)
) )
alphas = np.clip(alphas, 0, 1) alphas = np.clip(alphas, 0, 1)
# if flip_alphas:
# alphas = 1 - alphas
scaled_alphas = alphas * (len(rgbs) - 1) scaled_alphas = alphas * (len(rgbs) - 1)
indices = scaled_alphas.astype(int) indices = scaled_alphas.astype(int)
next_indices = np.clip(indices + 1, 0, len(rgbs) - 1) 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 return func
def get_color_field_image_file(scalar_func, def get_rgb_gradient_function(min_value, max_value, color_map):
min_value=0, max_value=2, vectorized_func = get_vectorized_rgb_gradient_function(min_value, max_value, color_map)
colors=DEFAULT_SCALAR_FIELD_COLORS return lambda value: vectorized_func([value])[0]
):
# 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 move_along_vector_field(mobject, func): def move_along_vector_field(mobject, func):
@ -116,217 +62,200 @@ def move_submobjects_along_vector_field(mobject, func):
return mobject 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): def apply_nudge(self, dt):
self.mobject.apply_function( mobject.apply_function(
lambda p: p + func(p) * dt lambda p: p + (cs.c2p(*func(*cs.p2c(p))) - origin) * dt
) )
mobject.add_updater(apply_nudge) mobject.add_updater(apply_nudge)
return mobject 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 # Mobjects
class VectorField(VGroup): class VectorField(VGroup):
CONFIG = { CONFIG = {
"delta_x": 0.5, "step_multiple": 0.5,
"delta_y": 0.5, "magnitude_range": (0, 2),
"x_min": int(np.floor(-FRAME_WIDTH / 2)), "color_map": "3b1b_colormap",
"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,
# Takes in actual norm, spits out displayed norm # Takes in actual norm, spits out displayed norm
"length_func": lambda norm: 0.45 * sigmoid(norm), "length_func": lambda norm: 0.45 * sigmoid(norm),
"opacity": 1.0, "opacity": 1.0,
"vector_config": {}, "vector_config": {},
} }
def __init__(self, func, **kwargs): def __init__(self, func, coordinate_system, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.func = func self.func = func
self.rgb_gradient_function = get_rgb_gradient_function( self.coordinate_system = coordinate_system
self.min_magnitude, self.value_to_rgb = get_rgb_gradient_function(
self.max_magnitude, *self.magnitude_range, self.color_map,
self.colors,
flip_alphas=False
) )
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): samples = get_sample_points_from_coordinate_system(
output = np.array(self.func(point)) coordinate_system, self.step_multiple
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]
) )
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 return vect
class StreamLines(VGroup): class StreamLines(VGroup):
CONFIG = { CONFIG = {
# TODO, this is an awkward way to inherit "step_multiple": 0.5,
# 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,
"n_repeats": 1, "n_repeats": 1,
"noise_factor": None, "noise_factor": None,
# Config for drawing lines # Config for drawing lines
"dt": 0.05, "dt": 0.05,
"virtual_time": 3, "arc_len": 3,
"n_anchors_per_line": 100, "max_time_steps": 200,
"n_samples_per_line": 10,
"cutoff_norm": 15,
# Style info
"stroke_width": 1, "stroke_width": 1,
"stroke_color": WHITE, "stroke_color": WHITE,
"color_by_arc_length": True, "stroke_opacity": 1,
# Min and max arc lengths meant to define "color_by_magnitude": True,
# the color range, should color_by_arc_length be True "magnitude_range": (0, 2.0),
"min_arc_length": 0, "taper_stroke_width": False,
"max_arc_length": 12, "color_map": "3b1b_colormap",
"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,
} }
def __init__(self, func, **kwargs): def __init__(self, func, coordinate_system, **kwargs):
VGroup.__init__(self, **kwargs) super().__init__(**kwargs)
self.func = func self.func = func
dt = self.dt self.coordinate_system = coordinate_system
self.draw_lines()
self.init_style()
start_points = self.get_start_points( def point_func(self, point):
**self.start_points_generator_config in_coords = self.coordinate_system.p2c(point)
) out_coords = self.func(*in_coords)
for point in start_points: 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] 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] 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: if get_norm(last_point) > self.cutoff_norm:
break break
if total_arc_len > self.arc_len:
break
line = VMobject() line = VMobject()
step = max(1, int(len(points) / self.n_anchors_per_line)) line.virtual_time = time
line.set_points_smoothly(points[::step]) step = max(1, int(len(points) / self.n_samples_per_line))
self.add(line) line.set_points_as_corners(points[::step])
line.make_approximately_smooth()
self.set_stroke(self.stroke_color, self.stroke_width) lines.append(line)
self.set_submobjects(lines)
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)
def get_start_points(self): def get_start_points(self):
x_min = self.x_min cs = self.coordinate_system
x_max = self.x_max sample_coords = get_sample_points_from_coordinate_system(
y_min = self.y_min cs, self.step_multiple,
y_max = self.y_max )
delta_x = self.delta_x
delta_y = self.delta_y
n_repeats = self.n_repeats
noise_factor = self.noise_factor noise_factor = self.noise_factor
if noise_factor is None: if noise_factor is None:
noise_factor = delta_y / 2 noise_factor = cs.x_range[2] * self.step_multiple * 0.5
return np.array([ return np.array([
x * RIGHT + y * UP + noise_factor * np.random.random(3) cs.c2p(*coords) + noise_factor * np.random.random(3)
for n in range(n_repeats) for n in range(self.n_repeats)
for x in np.arange(x_min, x_max + delta_x, delta_x) for coords in sample_coords
for y in np.arange(y_min, y_max + delta_y, delta_y)
]) ])
def init_style(self):
# TODO: Make it so that you can have a group of stream_lines if self.color_by_magnitude:
# varying in response to a changing vector field, and still values_to_rgbs = get_vectorized_rgb_gradient_function(
# animate the resulting flow *self.magnitude_range, self.color_map,
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
) )
for stroke_width, time_width in zip( cs = self.coordinate_system
np.linspace(0, max_stroke_width, self.n_segments), for line in self.submobjects:
np.linspace(max_time_width, 0, self.n_segments) 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): class AnimatedStreamLines(VGroup):
CONFIG = { CONFIG = {
"lag_range": 4, "lag_range": 4,
"line_anim_class": ShowPassingFlash, "line_anim_class": VShowPassingFlash,
"line_anim_config": { "line_anim_config": {
"run_time": 4, # "run_time": 4,
"rate_func": linear, "rate_func": linear,
"time_width": 0.3, "time_width": 0.5,
}, },
} }
def __init__(self, stream_lines, **kwargs): def __init__(self, stream_lines, **kwargs):
VGroup.__init__(self, **kwargs) super().__init__(**kwargs)
self.stream_lines = stream_lines self.stream_lines = stream_lines
for line in 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.anim.begin()
line.time = -self.lag_range * random.random() line.time = -self.lag_range * random.random()
self.add(line.anim.mobject) self.add(line.anim.mobject)
@ -339,3 +268,28 @@ class AnimatedStreamLines(VGroup):
line.time += dt line.time += dt
adjusted_time = max(line.time, 0) % line.anim.run_time adjusted_time = max(line.time, 0) % line.anim.run_time
line.anim.update(adjusted_time / 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)
)
])

View file

@ -74,12 +74,14 @@ class SwitchOff(LaggedStartMap):
class Lighthouse(SVGMobject): class Lighthouse(SVGMobject):
CONFIG = { CONFIG = {
"file_name": "lighthouse",
"height": LIGHTHOUSE_HEIGHT, "height": LIGHTHOUSE_HEIGHT,
"fill_color": WHITE, "fill_color": WHITE,
"fill_opacity": 1.0, "fill_opacity": 1.0,
} }
def __init__(self, **kwargs):
super().__init__("lighthouse", **kwargs)
def move_to(self, point): def move_to(self, point):
self.next_to(point, DOWN, buff=0) self.next_to(point, DOWN, buff=0)

View file

@ -272,7 +272,7 @@ class Scene(object):
times, times,
total=n_iterations, total=n_iterations,
leave=self.leave_progress_bars, leave=self.leave_progress_bars,
ascii=False if platform.system() != 'Windows' else True ascii=True if platform.system() == 'Windows' else None
) )
return time_progression return time_progression

View file

@ -9,5 +9,5 @@ out vec4 frag_color;
void main() { void main() {
frag_color = texture(Texture, v_im_coords); frag_color = texture(Texture, v_im_coords);
frag_color.a = v_opacity; frag_color.a *= v_opacity;
} }

View file

@ -17,16 +17,16 @@ vec4 add_light(vec4 color,
float shadow){ float shadow){
if(gloss == 0.0 && shadow == 0.0) return color; if(gloss == 0.0 && shadow == 0.0) return color;
// TODO, do we actually want this? It effectively treats surfaces as two-sided float camera_distance = focal_distance;
if(unit_normal.z < 0){
unit_normal *= -1;
}
// TODO, read this in as a uniform?
float camera_distance = 6;
// Assume everything has already been rotated such that camera is in the z-direction // 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_camera = vec3(0, 0, camera_distance) - point;
vec3 to_light = light_coords - 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); vec3 light_reflection = -to_light + 2 * unit_normal * dot(to_light, unit_normal);
float dot_prod = dot(normalize(light_reflection), normalize(to_camera)); float dot_prod = dot(normalize(light_reflection), normalize(to_camera));
float shine = gloss * exp(-3 * pow(1 - dot_prod, 2)); float shine = gloss * exp(-3 * pow(1 - dot_prod, 2));

View file

@ -3,6 +3,7 @@
uniform vec3 light_source_position; uniform vec3 light_source_position;
uniform float gloss; uniform float gloss;
uniform float shadow; uniform float shadow;
uniform float focal_distance;
in vec3 xyz_coords; in vec3 xyz_coords;
in vec3 v_normal; in vec3 v_normal;

View file

@ -6,6 +6,7 @@ uniform float num_textures;
uniform vec3 light_source_position; uniform vec3 light_source_position;
uniform float gloss; uniform float gloss;
uniform float shadow; uniform float shadow;
uniform float focal_distance;
in vec3 xyz_coords; in vec3 xyz_coords;
in vec3 v_normal; in vec3 v_normal;

View file

@ -4,6 +4,7 @@ uniform vec3 light_source_position;
uniform float gloss; uniform float gloss;
uniform float shadow; uniform float shadow;
uniform float anti_alias_width; uniform float anti_alias_width;
uniform float focal_distance;
in vec4 color; in vec4 color;
in float radius; in float radius;

View file

@ -4,7 +4,9 @@ from colour import Color
import numpy as np import numpy as np
from manimlib.constants import WHITE from manimlib.constants import WHITE
from manimlib.constants import COLORMAP_3B1B
from manimlib.utils.bezier import interpolate 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.simple_functions import clip_in_place
from manimlib.utils.space_ops import normalize 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): 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 from matplotlib.cm import get_cmap
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? rgbs = get_cmap(map_name).colors # Make more general?
return [ return resize_with_interpolation(np.array(rgbs), n_colors)
rgbs[int(n)]
for n in np.linspace(0, len(rgbs) - 1, n_colors)
]

View file

@ -23,6 +23,7 @@ def get_text_dir():
def get_mobject_data_dir(): def get_mobject_data_dir():
return guarantee_existence(os.path.join(get_temp_dir(), "mobject_data")) return guarantee_existence(os.path.join(get_temp_dir(), "mobject_data"))
def get_downloads_dir(): def get_downloads_dir():
return guarantee_existence(os.path.join(get_temp_dir(), "manim_downloads")) return guarantee_existence(os.path.join(get_temp_dir(), "manim_downloads"))

View file

@ -10,7 +10,7 @@ def get_full_raster_image_path(image_file_name):
return find_file( return find_file(
image_file_name, image_file_name,
directories=[get_raster_image_dir()], directories=[get_raster_image_dir()],
extensions=[".jpg", ".png", ".gif", ""] extensions=[".jpg", ".jpeg", ".png", ".gif", ""]
) )

View file

@ -23,6 +23,7 @@ def init_customization():
"background_color": "", "background_color": "",
}, },
"window_position": "UR", "window_position": "UR",
"window_position": 0,
"break_into_partial_movies": False, "break_into_partial_movies": False,
"camera_qualities": { "camera_qualities": {
"low": { "low": {

View file

@ -98,6 +98,8 @@ def resize_preserving_order(nparray, length):
def resize_with_interpolation(nparray, length): def resize_with_interpolation(nparray, length):
if len(nparray) == length: if len(nparray) == length:
return nparray return nparray
if length == 0:
return np.zeros((0, *nparray.shape[1:]))
cont_indices = np.linspace(0, len(nparray) - 1, length) cont_indices = np.linspace(0, len(nparray) - 1, length)
return np.array([ return np.array([
(1 - a) * nparray[lh] + a * nparray[rh] (1 - a) * nparray[lh] + a * nparray[rh]

View file

@ -15,7 +15,7 @@ class Window(PygletWindow):
cursor = True cursor = True
def __init__(self, scene, size=(1280, 720), **kwargs): def __init__(self, scene, size=(1280, 720), **kwargs):
super().__init__() super().__init__(size=size)
digest_config(self, kwargs) digest_config(self, kwargs)
self.scene = scene self.scene = scene
@ -38,7 +38,9 @@ class Window(PygletWindow):
def find_initial_position(self, size): def find_initial_position(self, size):
custom_position = get_customization()["window_position"] 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 window_width, window_height = size
# Position might be specified with a string of the form # Position might be specified with a string of the form
# x,y for integers x and y # x,y for integers x and y

View file

@ -2,7 +2,6 @@ argparse
colour colour
numpy numpy
Pillow Pillow
progressbar
scipy scipy
sympy sympy
tqdm tqdm
@ -17,4 +16,4 @@ pyreadline; sys_platform == 'win32'
validators validators
ipython ipython
PyOpenGL PyOpenGL
manimpango>=0.2.0,<0.3.0' manimpango>=0.2.0,<0.4.0

View file

@ -1,6 +1,5 @@
[metadata] [metadata]
name = manimgl name = manimgl
version = 1.0.0
author = Grant Sanderson author = Grant Sanderson
author-email= grant@3blue1brown.com author-email= grant@3blue1brown.com
summary = Animation engine for explanatory math videos summary = Animation engine for explanatory math videos