mirror of
https://github.com/3b1b/manim.git
synced 2025-09-19 04:41:56 +00:00
commit
634c3d672e
23 changed files with 492 additions and 283 deletions
30
.github/workflows/publish.yml
vendored
Normal file
30
.github/workflows/publish.yml
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
name: Upload Python Package
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.8'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools wheel twine
|
||||
|
||||
- name: Build and publish
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
19
README.md
19
README.md
|
@ -1,13 +1,19 @@
|
|||

|
||||
<p align="center">
|
||||
<a href="https://github.com/3b1b/manim">
|
||||
<img src="https://raw.githubusercontent.com/3b1b/manim/master/logo/cropped.png">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
[](http://choosealicense.com/licenses/mit/)
|
||||
[](https://www.reddit.com/r/manim/)
|
||||
[](https://discord.gg/mMRrZQW)
|
||||
[](https://3b1b.github.io/manim/)
|
||||
|
||||
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/).
|
||||
Manim is an engine for precise programatic animations, designed for creating explanatory math videos.
|
||||
|
||||
This repository contains the version of manim used by 3Blue1Brown. There is also a community maintained version at https://github.com/ManimCommunity/manim/. To get help or to join the development effort, please join 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 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.
|
||||
|
||||
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.8.
|
||||
|
@ -86,19 +92,16 @@ Some useful flags include:
|
|||
|
||||
Take a look at custom_defaults.yml for further configuration. To add your customization, you can either edit this file, or add another file by the same name "custom_defaults.yml" to whatever directory you are running manim from. For example [this is the one](https://github.com/3b1b/videos/blob/master/custom_defaults.yml) for 3blue1brown videos. There you can specify where videos should be output to, where manim should look for image files and sounds you want to read in, and other defaults regarding style and video quality.
|
||||
|
||||
Look through [https://github.com/3b1b/videos](https://github.com/3b1b/videos) to see the code for previous 3b1b videos. Note, however, that developments are often made to the library without considering backwards compatibility with those old projects. To run an old project with a guarantee that it will work, you will have to go back to the commit which completed that project.
|
||||
Look through the [example scenes](https://3b1b.github.io/manim/getting_started/example_scenes.html) to get a sense of how it is used, and feel free to look through the code behind [3blue1brown videos](https://github.com/3b1b/videos) for a much larger set of example. Note, however, that developments are often made to the library without considering backwards compatibility with those old videos. To run an old project with a guarantee that it will work, you will have to go back to the commit which completed that project.
|
||||
|
||||
### Documentation
|
||||
Documentation is in progress at [3b1b.github.io/manim](https://3b1b.github.io/manim/). And there is also a Chinese version maintained by **@manim-kindergarten**: [manim.ml](https://manim.ml/) (in Chinese).
|
||||
|
||||
### Walkthrough
|
||||
Todd Zimmerman put together a [tutorial](https://talkingphysics.wordpress.com/2019/01/08/getting-started-animating-with-manim-and-python-3-7/) on getting started with manim, which has been updated to run on Python 3.7.
|
||||
|
||||
[manim-kindergarten](https://github.com/manim-kindergarten/) wrote and collected some useful extra classes and some codes of videos in [manim_sandbox repo](https://github.com/manim-kindergarten/manim_sandbox).
|
||||
|
||||
|
||||
## Contributing
|
||||
Is always welcome. In particular, there is a dire need for tests and documentation.
|
||||
Is always welcome. As mentioned above, the [community edition](https://github.com/ManimCommunity/manim) has the most active ecosystem for contributions, with testing and continuous integration, but pull requests are welcome here too. Please explain the motivation for a given change and examples of its effect.
|
||||
|
||||
|
||||
## License
|
||||
|
|
Binary file not shown.
|
@ -437,93 +437,64 @@ OpeningManimExample
|
|||
.. manim-example:: OpeningManimExample
|
||||
:media: ../_static/example_scenes/OpeningManimExample.mp4
|
||||
|
||||
|
||||
class OpeningManimExample(Scene):
|
||||
def construct(self):
|
||||
title = TexText("This is some \\LaTeX")
|
||||
basel = Tex(
|
||||
"\\sum_{n=1}^\\infty "
|
||||
"\\frac{1}{n^2} = \\frac{\\pi^2}{6}"
|
||||
)
|
||||
VGroup(title, basel).arrange(DOWN)
|
||||
self.play(
|
||||
Write(title),
|
||||
FadeIn(basel, UP),
|
||||
)
|
||||
self.wait()
|
||||
intro_words = Text("""
|
||||
The original motivation for manim was to
|
||||
better illustrate mathematical functions
|
||||
as transformations.
|
||||
""")
|
||||
intro_words.to_edge(UP)
|
||||
|
||||
transform_title = Text("That was a transform")
|
||||
transform_title.to_corner(UL)
|
||||
self.play(
|
||||
Transform(title, transform_title),
|
||||
LaggedStartMap(FadeOut, basel, shift=DOWN),
|
||||
)
|
||||
self.wait()
|
||||
|
||||
fade_comment = Text(
|
||||
"""
|
||||
You probably don't want to overuse
|
||||
Transforms, though, a simple fade often
|
||||
looks nicer.
|
||||
""",
|
||||
font_size=36,
|
||||
color=GREY_B,
|
||||
)
|
||||
fade_comment.next_to(
|
||||
transform_title, DOWN,
|
||||
buff=LARGE_BUFF,
|
||||
aligned_edge=LEFT
|
||||
)
|
||||
self.play(FadeIn(fade_comment, shift=DOWN))
|
||||
self.wait(3)
|
||||
self.play(Write(intro_words))
|
||||
self.wait(2)
|
||||
|
||||
# Linear transform
|
||||
grid = NumberPlane((-10, 10), (-5, 5))
|
||||
grid_title = Text(
|
||||
"But manim is for illustrating math, not text",
|
||||
)
|
||||
grid_title.to_edge(UP)
|
||||
grid_title.add_background_rectangle()
|
||||
|
||||
self.add(grid, grid_title) # Make sure title is on top of grid
|
||||
self.play(
|
||||
FadeOut(title, shift=LEFT),
|
||||
FadeOut(fade_comment, shift=LEFT),
|
||||
FadeIn(grid_title),
|
||||
ShowCreation(grid, run_time=3, lag_ratio=0.1),
|
||||
)
|
||||
self.wait()
|
||||
|
||||
matrix = [[1, 1], [0, 1]]
|
||||
linear_transform_title = VGroup(
|
||||
linear_transform_words = VGroup(
|
||||
Text("This is what the matrix"),
|
||||
IntegerMatrix(matrix, include_background_rectangle=True),
|
||||
Text("looks like")
|
||||
)
|
||||
linear_transform_title.arrange(RIGHT)
|
||||
linear_transform_title.to_edge(UP)
|
||||
linear_transform_words.arrange(RIGHT)
|
||||
linear_transform_words.to_edge(UP)
|
||||
linear_transform_words.set_stroke(BLACK, 10, background=True)
|
||||
|
||||
self.play(
|
||||
FadeOut(grid_title),
|
||||
FadeIn(linear_transform_title),
|
||||
ShowCreation(grid),
|
||||
FadeTransform(intro_words, linear_transform_words)
|
||||
)
|
||||
self.wait()
|
||||
self.play(grid.apply_matrix, matrix, run_time=3)
|
||||
self.wait()
|
||||
|
||||
grid_transform_title = Text(
|
||||
"And this is a nonlinear transformation"
|
||||
)
|
||||
grid_transform_title.set_stroke(BLACK, 5, background=True)
|
||||
grid_transform_title.to_edge(UP)
|
||||
grid.prepare_for_nonlinear_transform(100)
|
||||
# Complex map
|
||||
c_grid = ComplexPlane()
|
||||
moving_c_grid = c_grid.copy()
|
||||
moving_c_grid.prepare_for_nonlinear_transform()
|
||||
c_grid.set_stroke(BLUE_E, 1)
|
||||
c_grid.add_coordinate_labels(font_size=24)
|
||||
complex_map_words = TexText("""
|
||||
Or thinking of the plane as $\\mathds{C}$,\\\\
|
||||
this is the map $z \\rightarrow z^2$
|
||||
""")
|
||||
complex_map_words.to_corner(UR)
|
||||
complex_map_words.set_stroke(BLACK, 5, background=True)
|
||||
|
||||
self.play(
|
||||
ApplyPointwiseFunction(
|
||||
lambda p: p + np.array([np.sin(p[1]), np.sin(p[0]), 0]),
|
||||
grid,
|
||||
run_time=5,
|
||||
),
|
||||
FadeOut(linear_transform_title),
|
||||
FadeIn(grid_transform_title),
|
||||
FadeOut(grid),
|
||||
Write(c_grid, run_time=3),
|
||||
FadeIn(moving_c_grid),
|
||||
FadeTransform(linear_transform_words, complex_map_words),
|
||||
)
|
||||
self.wait()
|
||||
self.play(
|
||||
moving_c_grid.apply_complex_function, lambda z: z**2,
|
||||
run_time=6,
|
||||
)
|
||||
self.wait(2)
|
||||
|
||||
This scene is a comprehensive application of a two-dimensional scene.
|
||||
|
||||
|
|
|
@ -10,94 +10,64 @@ from manimlib.imports import *
|
|||
|
||||
class OpeningManimExample(Scene):
|
||||
def construct(self):
|
||||
title = TexText("This is some \\LaTeX")
|
||||
basel = Tex(
|
||||
"\\sum_{n=1}^\\infty "
|
||||
"\\frac{1}{n^2} = \\frac{\\pi^2}{6}"
|
||||
)
|
||||
VGroup(title, basel).arrange(DOWN)
|
||||
self.play(
|
||||
Write(title),
|
||||
FadeIn(basel, UP),
|
||||
)
|
||||
self.wait()
|
||||
intro_words = Text("""
|
||||
The original motivation for manim was to
|
||||
better illustrate mathematical functions
|
||||
as transformations.
|
||||
""")
|
||||
intro_words.to_edge(UP)
|
||||
|
||||
transform_title = Text("That was a transform")
|
||||
transform_title.to_corner(UL)
|
||||
self.play(
|
||||
Transform(title[0], transform_title),
|
||||
LaggedStartMap(FadeOut, basel, shift=DOWN),
|
||||
)
|
||||
self.wait()
|
||||
|
||||
fade_comment = Text(
|
||||
"""
|
||||
You probably don't want to overuse
|
||||
Transforms, though, a simple fade often
|
||||
looks nicer.
|
||||
""",
|
||||
font_size=36,
|
||||
color=GREY_B,
|
||||
)
|
||||
fade_comment.next_to(
|
||||
transform_title, DOWN,
|
||||
buff=LARGE_BUFF,
|
||||
aligned_edge=LEFT
|
||||
)
|
||||
self.play(FadeIn(fade_comment, shift=DOWN))
|
||||
self.wait(3)
|
||||
self.play(Write(intro_words))
|
||||
self.wait(2)
|
||||
|
||||
# Linear transform
|
||||
grid = NumberPlane((-10, 10), (-5, 5))
|
||||
grid_title = Text(
|
||||
"But manim is for illustrating math, not text",
|
||||
)
|
||||
grid_title.to_edge(UP)
|
||||
grid_title.add_background_rectangle()
|
||||
|
||||
self.add(grid, grid_title) # Make sure title is on top of grid
|
||||
self.play(
|
||||
FadeOut(title, shift=LEFT),
|
||||
FadeOut(fade_comment, shift=LEFT),
|
||||
FadeIn(grid_title),
|
||||
ShowCreation(grid, run_time=3, lag_ratio=0.1),
|
||||
)
|
||||
self.wait()
|
||||
|
||||
matrix = [[1, 1], [0, 1]]
|
||||
linear_transform_title = VGroup(
|
||||
linear_transform_words = VGroup(
|
||||
Text("This is what the matrix"),
|
||||
IntegerMatrix(matrix, include_background_rectangle=True),
|
||||
Text("looks like")
|
||||
)
|
||||
linear_transform_title.arrange(RIGHT)
|
||||
linear_transform_title.to_edge(UP)
|
||||
linear_transform_words.arrange(RIGHT)
|
||||
linear_transform_words.to_edge(UP)
|
||||
linear_transform_words.set_stroke(BLACK, 10, background=True)
|
||||
|
||||
self.play(
|
||||
FadeOut(grid_title),
|
||||
FadeIn(linear_transform_title),
|
||||
ShowCreation(grid),
|
||||
FadeTransform(intro_words, linear_transform_words)
|
||||
)
|
||||
self.wait()
|
||||
self.play(grid.apply_matrix, matrix, run_time=3)
|
||||
self.wait()
|
||||
|
||||
grid_transform_title = Text(
|
||||
"And this is a nonlinear transformation"
|
||||
)
|
||||
grid_transform_title.set_stroke(BLACK, 5, background=True)
|
||||
grid_transform_title.to_edge(UP)
|
||||
grid.prepare_for_nonlinear_transform(100)
|
||||
# Complex map
|
||||
c_grid = ComplexPlane()
|
||||
moving_c_grid = c_grid.copy()
|
||||
moving_c_grid.prepare_for_nonlinear_transform()
|
||||
c_grid.set_stroke(BLUE_E, 1)
|
||||
c_grid.add_coordinate_labels(font_size=24)
|
||||
complex_map_words = TexText("""
|
||||
Or thinking of the plane as $\\mathds{C}$,\\\\
|
||||
this is the map $z \\rightarrow z^2$
|
||||
""")
|
||||
complex_map_words.to_corner(UR)
|
||||
complex_map_words.set_stroke(BLACK, 5, background=True)
|
||||
|
||||
self.play(
|
||||
ApplyPointwiseFunction(
|
||||
lambda p: p + np.array([np.sin(p[1]), np.sin(p[0]), 0]),
|
||||
grid,
|
||||
run_time=5,
|
||||
),
|
||||
FadeOut(linear_transform_title),
|
||||
FadeIn(grid_transform_title),
|
||||
FadeOut(grid),
|
||||
Write(c_grid, run_time=3),
|
||||
FadeIn(moving_c_grid),
|
||||
FadeTransform(linear_transform_words, complex_map_words),
|
||||
)
|
||||
self.wait()
|
||||
self.play(
|
||||
moving_c_grid.apply_complex_function, lambda z: z**2,
|
||||
run_time=6,
|
||||
)
|
||||
self.wait(2)
|
||||
|
||||
|
||||
class SquareToCircle(Scene):
|
||||
class InteractiveDevlopment(Scene):
|
||||
def construct(self):
|
||||
circle = Circle()
|
||||
circle.set_fill(BLUE, opacity=0.5)
|
||||
|
@ -106,18 +76,37 @@ class SquareToCircle(Scene):
|
|||
|
||||
self.play(ShowCreation(square))
|
||||
self.wait()
|
||||
self.play(ReplacementTransform(square, circle))
|
||||
self.wait()
|
||||
|
||||
# This opens an iPython termnial where you can keep writing
|
||||
# lines as if they were part of this construct method
|
||||
self.embed()
|
||||
# Try typing the following lines
|
||||
# self.play(circle.stretch, 4, {"dim": 0})
|
||||
# self.play(Rotate(circle, TAU / 4))
|
||||
# self.play(circle.shift, 2 * RIGHT, circle.scale, 0.25)
|
||||
# circle.insert_n_curves(10)
|
||||
# self.play(circle.apply_complex_function, lambda z: z**2)
|
||||
# Try copying and pasting some of the lines below into
|
||||
# the interactive shell
|
||||
self.play(ReplacementTransform(square, circle))
|
||||
self.wait()
|
||||
self.play(circle.stretch, 4, 0)
|
||||
self.play(Rotate(circle, 90 * DEGREES))
|
||||
self.play(circle.shift, 2 * RIGHT, circle.scale, 0.25)
|
||||
|
||||
text = Text("""
|
||||
In general, using the interactive shell
|
||||
is very helpful when developing new scenes
|
||||
""")
|
||||
self.play(Write(text))
|
||||
|
||||
# In the interactive shell, you can just type
|
||||
# play, add, remove, clear, wait, save_state and restore,
|
||||
# instead of self.play, self.add, self.remove, etc.
|
||||
|
||||
# To interact with the window, type touch(). You can then
|
||||
# scroll in the window, or zoom by holding down 'z' while scrolling,
|
||||
# and change camera perspective by holding down 'd' while moving
|
||||
# the mouse. Press 'r' to reset to the standard camera position.
|
||||
# Press 'q' to stop interacting with the window and go back to
|
||||
# typing new commands into the shell.
|
||||
|
||||
# In principle you can customize a scene
|
||||
always(circle.move_to, self.mouse_point)
|
||||
|
||||
|
||||
class AnimatingMethods(Scene):
|
||||
|
|
|
@ -28,20 +28,22 @@ class Homotopy(Animation):
|
|||
|
||||
|
||||
class SmoothedVectorizedHomotopy(Homotopy):
|
||||
def interpolate_submobject(self, submob, start, alpha):
|
||||
Homotopy.interpolate_submobject(self, submob, start, alpha)
|
||||
submob.make_smooth()
|
||||
CONFIG = {
|
||||
"apply_function_kwargs": {"make_smooth": True},
|
||||
}
|
||||
|
||||
|
||||
class ComplexHomotopy(Homotopy):
|
||||
def __init__(self, complex_homotopy, mobject, **kwargs):
|
||||
"""
|
||||
Complex Hootopy a function Cx[0, 1] to C
|
||||
Given a function form (z, t) -> w, where z and w
|
||||
are complex numbers and t is time, this animates
|
||||
the state over time
|
||||
"""
|
||||
def homotopy(x, y, z, t):
|
||||
c = complex_homotopy(complex(x, y), t)
|
||||
return (c.real, c.imag, z)
|
||||
Homotopy.__init__(self, homotopy, mobject, **kwargs)
|
||||
super().__init__(homotopy, mobject, **kwargs)
|
||||
|
||||
|
||||
class PhaseFlow(Animation):
|
||||
|
|
|
@ -134,7 +134,9 @@ def parse_cli():
|
|||
|
||||
|
||||
def get_manim_dir():
|
||||
return os.path.dirname(inspect.getabsfile(importlib.import_module("manim")))
|
||||
manimlib_module = importlib.import_module("manimlib")
|
||||
manimlib_dir = os.path.dirname(inspect.getabsfile(manimlib_module))
|
||||
return os.path.abspath(os.path.join(manimlib_dir, ".."))
|
||||
|
||||
|
||||
def get_module(file_name):
|
||||
|
@ -150,7 +152,7 @@ def get_module(file_name):
|
|||
|
||||
def get_custom_defaults():
|
||||
filename = "custom_defaults.yml"
|
||||
manim_defaults_file = os.path.join(get_manim_dir(), filename)
|
||||
manim_defaults_file = os.path.join(get_manim_dir(), "manimlib", "defaults.yml")
|
||||
with open(manim_defaults_file, "r") as file:
|
||||
custom_defaults = yaml.safe_load(file)
|
||||
|
||||
|
|
56
manimlib/defaults.yml
Normal file
56
manimlib/defaults.yml
Normal file
|
@ -0,0 +1,56 @@
|
|||
directories:
|
||||
# Set this to true if you want the path to video files
|
||||
# to match the directory structure of the path to the
|
||||
# sourcecode generating that video
|
||||
mirror_module_path: False
|
||||
# Where should manim output video and image files?
|
||||
output: ""
|
||||
# If you want to use images, manim will look to these folders to find them
|
||||
raster_images: ""
|
||||
vector_images: ""
|
||||
# If you want to use sounds, manim will look here to find it.
|
||||
sounds: ""
|
||||
# Manim often generates tex_files or other kinds of serialized data
|
||||
# to keep from having to generate the same thing too many times. By
|
||||
# default, these will be stored at tempfile.gettempdir(), e.g. this might
|
||||
# return whatever is at to the TMPDIR environment variable. If you want to
|
||||
# specify them elsewhere,
|
||||
temporary_storage: ""
|
||||
tex:
|
||||
executable: "latex"
|
||||
template_file: "tex_template.tex"
|
||||
intermediate_filetype: "dvi"
|
||||
text_to_replace: "[tex_expression]"
|
||||
# # For ctex, use the following configuration
|
||||
# executable: "xelatex -no-pdf"
|
||||
# template_file: "ctex_template.tex"
|
||||
# intermediate_filetype: "xdv"
|
||||
universal_import_line: "from manimlib.imports import *"
|
||||
style:
|
||||
font: "Consolas"
|
||||
background_color: "#333333"
|
||||
# Set the position of preview window, you can use directions, e.g. UL/DR/OL/OO/...
|
||||
# also, you can also specify the position(pixel) of the upper left corner of
|
||||
# the window on the monitor, e.g. "960,540"
|
||||
window_position: UR
|
||||
# If break_into_partial_movies is set to True, then many small
|
||||
# files will be written corresponding to each Scene.play and
|
||||
# Scene.wait call, and these files will then be combined
|
||||
# to form the full scene. Sometimes video-editing is made
|
||||
# easier when working with the broken up scene, which
|
||||
# effectively has cuts at all the places you might want.
|
||||
break_into_partial_movies: False
|
||||
camera_qualities:
|
||||
low:
|
||||
resolution: "854x480"
|
||||
frame_rate: 15
|
||||
medium:
|
||||
resolution: "1280x720"
|
||||
frame_rate: 30
|
||||
high:
|
||||
resolution: "1920x1080"
|
||||
frame_rate: 30
|
||||
ultra_high:
|
||||
resolution: "3840x2160"
|
||||
frame_rate: 60
|
||||
default_quality: "high"
|
|
@ -26,18 +26,20 @@ def is_child_scene(obj, module):
|
|||
|
||||
def prompt_user_for_choice(scene_classes):
|
||||
name_to_class = {}
|
||||
for scene_class in scene_classes:
|
||||
max_digits = len(str(len(scene_classes)))
|
||||
for idx, scene_class in enumerate(scene_classes, start=1):
|
||||
name = scene_class.__name__
|
||||
print(name)
|
||||
print(f"{str(idx).zfill(max_digits)}: {name}")
|
||||
name_to_class[name] = scene_class
|
||||
try:
|
||||
user_input = input(
|
||||
"\nThat module has mulziple scenes, which "
|
||||
"ones would you like to render?\n Scene Name: "
|
||||
"\nThat module has multiple scenes, "
|
||||
"which ones would you like to render?"
|
||||
"\nScene Name or Number: "
|
||||
)
|
||||
return [
|
||||
name_to_class[user_input]
|
||||
for num_str in user_input.replace(" ", "").split(",")
|
||||
name_to_class[split_str] if not split_str.isnumeric() else scene_classes[int(split_str)-1]
|
||||
for split_str in user_input.replace(" ", "").split(",")
|
||||
]
|
||||
except KeyError:
|
||||
logging.log(logging.ERROR, "Invalid scene")
|
||||
|
|
|
@ -67,11 +67,8 @@ from manimlib.once_useful_constructs.fractals import *
|
|||
from manimlib.once_useful_constructs.graph_theory import *
|
||||
from manimlib.once_useful_constructs.light import *
|
||||
|
||||
from manimlib.scene.graph_scene import *
|
||||
from manimlib.scene.reconfigurable_scene import *
|
||||
from manimlib.scene.scene import *
|
||||
from manimlib.scene.sample_space_scene import *
|
||||
from manimlib.scene.graph_scene import *
|
||||
from manimlib.scene.three_d_scene import *
|
||||
from manimlib.scene.vector_space_scene import *
|
||||
|
||||
|
|
|
@ -5,14 +5,17 @@ from manimlib.constants import *
|
|||
from manimlib.mobject.functions import ParametricCurve
|
||||
from manimlib.mobject.geometry import Arrow
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
from manimlib.mobject.number_line import NumberLine
|
||||
from manimlib.mobject.svg.tex_mobject import Tex
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.utils.config_ops import merge_dicts_recursively
|
||||
from manimlib.utils.simple_functions import binary_search
|
||||
from manimlib.utils.space_ops import angle_of_vector
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
from manimlib.utils.space_ops import rotate_vector
|
||||
|
||||
# TODO: There should be much more code reuse between Axes, NumberPlane and GraphScene
|
||||
EPSILON = 1e-8
|
||||
|
||||
|
||||
class CoordinateSystem():
|
||||
|
@ -25,6 +28,7 @@ class CoordinateSystem():
|
|||
"y_range": [-4, 4, 1],
|
||||
"width": None,
|
||||
"height": None,
|
||||
"num_sampled_graph_points_per_tick": 5,
|
||||
}
|
||||
|
||||
def coords_to_point(self, *coords):
|
||||
|
@ -84,12 +88,21 @@ class CoordinateSystem():
|
|||
)
|
||||
return self.axis_labels
|
||||
|
||||
# Useful for graphing
|
||||
def get_graph(self, function, x_range=None, **kwargs):
|
||||
if x_range is None:
|
||||
x_range = self.x_range
|
||||
t_range = list(self.x_range)
|
||||
if x_range is not None:
|
||||
for i in range(len(x_range)):
|
||||
t_range[i] = x_range[i]
|
||||
# For axes, the third coordinate of x_range indicates
|
||||
# tick frequency. But for functions, it indicates a
|
||||
# sample frequency
|
||||
if x_range is None or len(x_range) < 3:
|
||||
t_range[2] /= self.num_sampled_graph_points_per_tick
|
||||
|
||||
graph = ParametricCurve(
|
||||
lambda t: self.coords_to_point(t, function(t)),
|
||||
t_range=x_range,
|
||||
lambda t: self.c2p(t, function(t)),
|
||||
t_range=t_range,
|
||||
**kwargs
|
||||
)
|
||||
graph.underlying_function = function
|
||||
|
@ -121,6 +134,111 @@ class CoordinateSystem():
|
|||
else:
|
||||
return None
|
||||
|
||||
def itgp(self, x, graph):
|
||||
"""
|
||||
Alias for input_to_graph_point
|
||||
"""
|
||||
return self.input_to_graph_point(x, graph)
|
||||
|
||||
def get_graph_label(self,
|
||||
graph,
|
||||
label="f(x)",
|
||||
x=None,
|
||||
direction=RIGHT,
|
||||
buff=MED_SMALL_BUFF,
|
||||
color=None):
|
||||
label = Tex(label)
|
||||
if color is None:
|
||||
label.match_color(graph)
|
||||
if x is None:
|
||||
# Searching from the right, find a point
|
||||
# whose y value is in bounds
|
||||
max_y = FRAME_Y_RADIUS - label.get_height()
|
||||
for x0 in np.arange(*self.x_range)[-1::-1]:
|
||||
if abs(self.itgp(x0, graph)[1]) < max_y:
|
||||
x = x0
|
||||
break
|
||||
if x is None:
|
||||
x = self.x_range[1]
|
||||
|
||||
point = self.input_to_graph_point(x, graph)
|
||||
angle = self.angle_of_tangent(x, graph)
|
||||
normal = rotate_vector(RIGHT, angle + 90 * DEGREES)
|
||||
if normal[1] < 0:
|
||||
normal *= -1
|
||||
label.next_to(point, normal, buff=buff)
|
||||
label.shift_onto_screen()
|
||||
return label
|
||||
|
||||
def get_vertical_line_to_graph(self, x, graph, line_func=Line):
|
||||
return line_func(
|
||||
self.coords_to_point(x, 0),
|
||||
self.input_to_graph_point(x, graph),
|
||||
)
|
||||
|
||||
# For calculus
|
||||
def angle_of_tangent(self, x, graph, dx=EPSILON):
|
||||
p0 = self.input_to_graph_point(x, graph)
|
||||
p1 = self.input_to_graph_point(x + dx, graph)
|
||||
return angle_of_vector(p1 - p0)
|
||||
|
||||
def slope_of_tangent(self, x, graph, **kwargs):
|
||||
return np.tan(self.angle_of_tangent(x, graph, **kwargs))
|
||||
|
||||
def get_tangent_line(self, x, graph, length=5, line_func=Line):
|
||||
line = line_func(LEFT, RIGHT)
|
||||
line.set_width(length)
|
||||
line.rotate(self.angle_of_tangent(x, graph))
|
||||
line.move_to(self.input_to_graph_point(x, graph))
|
||||
return line
|
||||
|
||||
def get_riemann_rectangles(self,
|
||||
graph,
|
||||
x_range=None,
|
||||
dx=None,
|
||||
input_sample_type="left",
|
||||
stroke_width=1,
|
||||
stroke_color=BLACK,
|
||||
fill_opacity=1,
|
||||
colors=(BLUE, GREEN),
|
||||
show_signed_area=True):
|
||||
if x_range is None:
|
||||
x_range = self.x_range[:2]
|
||||
if dx is None:
|
||||
dx = self.x_range[2]
|
||||
if len(x_range) < 3:
|
||||
x_range = [*x_range, dx]
|
||||
|
||||
rects = []
|
||||
xs = np.arange(*x_range)
|
||||
for x0, x1 in zip(xs, xs[1:]):
|
||||
if input_sample_type == "left":
|
||||
sample = x0
|
||||
elif input_sample_type == "right":
|
||||
sample = x1
|
||||
elif input_sample_type == "center":
|
||||
sample = 0.5 * x0 + 0.5 * x1
|
||||
else:
|
||||
raise Exception("Invalid input sample type")
|
||||
height = get_norm(
|
||||
self.itgp(sample, graph) - self.c2p(sample, 0)
|
||||
)
|
||||
rect = Rectangle(width=x1 - x0, height=height)
|
||||
rect.move_to(self.c2p(x0, 0), DL)
|
||||
rects.append(rect)
|
||||
result = VGroup(*rects)
|
||||
result.set_submobject_colors_by_gradient(*colors)
|
||||
result.set_style(
|
||||
stroke_width=stroke_width,
|
||||
stroke_color=stroke_color,
|
||||
fill_opacity=fill_opacity,
|
||||
)
|
||||
return result
|
||||
|
||||
def get_area_under_graph(self, graph, x_range, fill_color=BLUE, fill_opacity=1):
|
||||
# TODO
|
||||
pass
|
||||
|
||||
|
||||
class Axes(VGroup, CoordinateSystem):
|
||||
CONFIG = {
|
||||
|
@ -135,15 +253,18 @@ class Axes(VGroup, CoordinateSystem):
|
|||
|
||||
def __init__(self, x_range=None, y_range=None, **kwargs):
|
||||
VGroup.__init__(self, **kwargs)
|
||||
if x_range is not None:
|
||||
for i in range(len(x_range)):
|
||||
self.x_range[i] = x_range[i]
|
||||
if y_range is not None:
|
||||
for i in range(len(y_range)):
|
||||
self.y_range[i] = y_range[i]
|
||||
|
||||
self.x_axis = self.create_axis(
|
||||
x_range or self.x_range,
|
||||
self.x_axis_config,
|
||||
self.width,
|
||||
self.x_range, self.x_axis_config, self.width,
|
||||
)
|
||||
self.y_axis = self.create_axis(
|
||||
y_range or self.y_range,
|
||||
self.y_axis_config,
|
||||
self.height
|
||||
self.y_range, self.y_axis_config, self.height
|
||||
)
|
||||
self.y_axis.rotate(90 * DEGREES, about_point=ORIGIN)
|
||||
# Add as a separate group in case various other
|
||||
|
@ -162,7 +283,7 @@ class Axes(VGroup, CoordinateSystem):
|
|||
|
||||
def coords_to_point(self, *coords):
|
||||
origin = self.x_axis.number_to_point(0)
|
||||
result = np.array(origin)
|
||||
result = origin.copy()
|
||||
for axis, coord in zip(self.get_axes(), coords):
|
||||
result += (axis.number_to_point(coord) - origin)
|
||||
return result
|
||||
|
@ -176,11 +297,17 @@ class Axes(VGroup, CoordinateSystem):
|
|||
def get_axes(self):
|
||||
return self.axes
|
||||
|
||||
def add_coordinate_labels(self, x_values=None, y_values=None):
|
||||
def add_coordinate_labels(self,
|
||||
x_values=None,
|
||||
y_values=None,
|
||||
excluding=[0],
|
||||
**kwargs):
|
||||
axes = self.get_axes()
|
||||
self.coordinate_labels = VGroup()
|
||||
for axis, values in zip(axes, [x_values, y_values]):
|
||||
numbers = axis.add_numbers(values, excluding=[0])
|
||||
numbers = axis.add_numbers(
|
||||
values, excluding=excluding, **kwargs
|
||||
)
|
||||
self.coordinate_labels.add(numbers)
|
||||
return self.coordinate_labels
|
||||
|
||||
|
@ -229,9 +356,6 @@ class NumberPlane(Axes):
|
|||
"include_tip": False,
|
||||
"line_to_number_buff": SMALL_BUFF,
|
||||
"line_to_number_direction": DL,
|
||||
"decimal_number_config": {
|
||||
"height": 0.2,
|
||||
}
|
||||
},
|
||||
"y_axis_config": {
|
||||
"line_to_number_direction": DL,
|
||||
|
@ -316,6 +440,7 @@ class NumberPlane(Axes):
|
|||
num_curves = mob.get_num_curves()
|
||||
if num_inserted_curves > num_curves:
|
||||
mob.insert_n_curves(num_inserted_curves - num_curves)
|
||||
mob.make_smooth_after_applying_functions = True
|
||||
return self
|
||||
|
||||
|
||||
|
@ -355,10 +480,7 @@ class ComplexPlane(NumberPlane):
|
|||
if abs(z.imag) > abs(z.real):
|
||||
axis = self.get_y_axis()
|
||||
value = z.imag
|
||||
kwargs = merge_dicts_recursively(
|
||||
kwargs,
|
||||
{"number_config": {"unit": "i"}},
|
||||
)
|
||||
kwargs["unit"] = "i"
|
||||
else:
|
||||
axis = self.get_x_axis()
|
||||
value = z.real
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
from manimlib.constants import *
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
|
||||
|
||||
class ParametricCurve(VMobject):
|
||||
CONFIG = {
|
||||
"t_range": [0, 1, 0.1],
|
||||
"min_samples": 10,
|
||||
"epsilon": 1e-8,
|
||||
# TODO, automatically figure out discontinuities
|
||||
"discontinuities": [],
|
||||
"smoothing": True,
|
||||
}
|
||||
|
||||
def __init__(self, t_func, t_range=None, **kwargs):
|
||||
|
@ -42,8 +39,7 @@ class ParametricCurve(VMobject):
|
|||
points = np.array([self.t_func(t) for t in t_range])
|
||||
self.start_new_path(points[0])
|
||||
self.add_points_as_corners(points[1:])
|
||||
if self.smoothing:
|
||||
self.make_smooth()
|
||||
self.make_approximately_smooth()
|
||||
return self
|
||||
|
||||
|
||||
|
|
|
@ -636,6 +636,10 @@ class Arrow(Line):
|
|||
super().scale(length / self.get_length())
|
||||
|
||||
self.rotate(angle_of_vector(vect) - self.get_angle())
|
||||
self.rotate(
|
||||
PI / 2 - np.arccos(normalize(vect)[2]),
|
||||
axis=rotate_vector(self.get_unit_vector(), -PI / 2),
|
||||
)
|
||||
self.shift(start - self.get_start())
|
||||
self.refresh_triangulation()
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import itertools as it
|
|||
import random
|
||||
import sys
|
||||
import moderngl
|
||||
from functools import wraps
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
@ -1287,6 +1288,7 @@ class Mobject(object):
|
|||
# Operations touching shader uniforms
|
||||
|
||||
def affects_shader_info_id(func):
|
||||
@wraps(func)
|
||||
def wrapper(self):
|
||||
for mob in self.get_family():
|
||||
func(mob)
|
||||
|
@ -1440,7 +1442,7 @@ class Mobject(object):
|
|||
return self.shader_indices
|
||||
|
||||
# Event Handlers
|
||||
"""
|
||||
"""
|
||||
Event handling follows the Event Bubbling model of DOM in javascript.
|
||||
Return false to stop the event bubbling.
|
||||
To learn more visit https://www.quirksmode.org/js/events_order.html
|
||||
|
@ -1475,7 +1477,7 @@ class Mobject(object):
|
|||
|
||||
def get_event_listners(self):
|
||||
return self.event_listners
|
||||
|
||||
|
||||
def get_family_event_listners(self):
|
||||
return list(it.chain(*[sm.get_event_listners() for sm in self.get_family()]))
|
||||
|
||||
|
@ -1487,36 +1489,43 @@ class Mobject(object):
|
|||
|
||||
def add_mouse_motion_listner(self, callback):
|
||||
self.add_event_listner(EventType.MouseMotionEvent, callback)
|
||||
|
||||
def remove_mouse_motion_listner(self, callback):
|
||||
self.remove_event_listner(EventType.MouseMotionEvent, callback)
|
||||
|
||||
def add_mouse_press_listner(self, callback):
|
||||
self.add_event_listner(EventType.MousePressEvent, callback)
|
||||
|
||||
def remove_mouse_press_listner(self, callback):
|
||||
self.remove_event_listner(EventType.MousePressEvent, callback)
|
||||
|
||||
def add_mouse_release_listner(self, callback):
|
||||
self.add_event_listner(EventType.MouseReleaseEvent, callback)
|
||||
|
||||
def remove_mouse_release_listner(self, callback):
|
||||
self.remove_event_listner(EventType.MouseReleaseEvent, callback)
|
||||
|
||||
def add_mouse_drag_listner(self, callback):
|
||||
self.add_event_listner(EventType.MouseDragEvent, callback)
|
||||
|
||||
def remove_mouse_drag_listner(self, callback):
|
||||
self.remove_event_listner(EventType.MouseDragEvent, callback)
|
||||
|
||||
def add_mouse_scroll_listner(self, callback):
|
||||
self.add_event_listner(EventType.MouseScrollEvent, callback)
|
||||
|
||||
def remove_mouse_scroll_listner(self, callback):
|
||||
self.remove_event_listner(EventType.MouseScrollEvent, callback)
|
||||
|
||||
def add_key_press_listner(self, callback):
|
||||
self.add_event_listner(EventType.KeyPressEvent, callback)
|
||||
|
||||
def remove_key_press_listner(self, callback):
|
||||
self.remove_event_listner(EventType.KeyPressEvent, callback)
|
||||
|
||||
def add_key_release_listner(self, callback):
|
||||
self.add_event_listner(EventType.KeyReleaseEvent, callback)
|
||||
|
||||
def remove_key_release_listner(self, callback):
|
||||
self.remove_event_listner(EventType.KeyReleaseEvent, callback)
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ class NumberLine(Line):
|
|||
},
|
||||
"decimal_number_config": {
|
||||
"num_decimal_places": 0,
|
||||
"height": 0.25,
|
||||
"font_size": 36,
|
||||
},
|
||||
"numbers_to_exclude": None
|
||||
}
|
||||
|
@ -57,6 +57,7 @@ class NumberLine(Line):
|
|||
super().__init__(self.x_min * RIGHT, self.x_max * RIGHT, **kwargs)
|
||||
if self.width:
|
||||
self.set_width(self.width)
|
||||
self.unit_size = self.get_unit_size()
|
||||
else:
|
||||
self.scale(self.unit_size)
|
||||
self.center()
|
||||
|
@ -99,7 +100,7 @@ class NumberLine(Line):
|
|||
return result
|
||||
|
||||
def get_tick_marks(self):
|
||||
return self.tick_marks
|
||||
return self.ticks
|
||||
|
||||
def number_to_point(self, number):
|
||||
alpha = float(number - self.x_min) / (self.x_max - self.x_min)
|
||||
|
@ -123,14 +124,12 @@ class NumberLine(Line):
|
|||
return self.point_to_number(point)
|
||||
|
||||
def get_unit_size(self):
|
||||
return (self.x_max - self.x_min) / self.get_length()
|
||||
return self.get_length() / (self.x_max - self.x_min)
|
||||
|
||||
def get_number_mobject(self, x,
|
||||
number_config=None,
|
||||
direction=None,
|
||||
buff=None):
|
||||
if number_config is None:
|
||||
number_config = {}
|
||||
buff=None,
|
||||
**number_config):
|
||||
number_config = merge_dicts_recursively(
|
||||
self.decimal_number_config, number_config
|
||||
)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import math
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.types.surface import ParametricSurface
|
||||
from manimlib.mobject.types.surface import Surface
|
||||
from manimlib.mobject.types.surface import SGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
|
@ -20,8 +20,8 @@ class SurfaceMesh(VGroup):
|
|||
}
|
||||
|
||||
def __init__(self, uv_surface, **kwargs):
|
||||
if not isinstance(uv_surface, ParametricSurface):
|
||||
raise Exception("uv_surface must be of type ParametricSurface")
|
||||
if not isinstance(uv_surface, Surface):
|
||||
raise Exception("uv_surface must be of type Surface")
|
||||
self.uv_surface = uv_surface
|
||||
super().__init__(**kwargs)
|
||||
|
||||
|
@ -49,18 +49,9 @@ class SurfaceMesh(VGroup):
|
|||
self.add(path)
|
||||
|
||||
|
||||
# Sphere, cylinder, cube, prism
|
||||
# 3D shapes
|
||||
|
||||
class ArglessSurface(ParametricSurface):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(self.uv_func, **kwargs)
|
||||
|
||||
def uv_func(self, u, v):
|
||||
# To be implemented by a subclass
|
||||
return [u, v, 0]
|
||||
|
||||
|
||||
class Sphere(ArglessSurface):
|
||||
class Sphere(Surface):
|
||||
CONFIG = {
|
||||
"resolution": (101, 51),
|
||||
"radius": 1,
|
||||
|
@ -76,7 +67,7 @@ class Sphere(ArglessSurface):
|
|||
])
|
||||
|
||||
|
||||
class Torus(ArglessSurface):
|
||||
class Torus(Surface):
|
||||
CONFIG = {
|
||||
"u_range": (0, TAU),
|
||||
"v_range": (0, TAU),
|
||||
|
@ -89,7 +80,7 @@ class Torus(ArglessSurface):
|
|||
return (self.r1 - self.r2 * math.cos(v)) * P - math.sin(v) * OUT
|
||||
|
||||
|
||||
class Cylinder(ArglessSurface):
|
||||
class Cylinder(Surface):
|
||||
CONFIG = {
|
||||
"height": 2,
|
||||
"radius": 1,
|
||||
|
@ -127,7 +118,7 @@ class Line3D(Cylinder):
|
|||
self.shift((start + end) / 2)
|
||||
|
||||
|
||||
class Disk3D(ArglessSurface):
|
||||
class Disk3D(Surface):
|
||||
CONFIG = {
|
||||
"radius": 1,
|
||||
"u_range": (0, 1),
|
||||
|
@ -147,7 +138,7 @@ class Disk3D(ArglessSurface):
|
|||
]
|
||||
|
||||
|
||||
class Square3D(ArglessSurface):
|
||||
class Square3D(Surface):
|
||||
CONFIG = {
|
||||
"side_length": 2,
|
||||
"u_range": (-1, 1),
|
||||
|
|
|
@ -5,19 +5,18 @@ from manimlib.constants import *
|
|||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.bezier import integer_interpolate
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.config_ops import digest_config
|
||||
from manimlib.utils.images import get_full_raster_image_path
|
||||
from manimlib.utils.iterables import listify
|
||||
from manimlib.utils.space_ops import normalize_along_axis
|
||||
|
||||
|
||||
class ParametricSurface(Mobject):
|
||||
class Surface(Mobject):
|
||||
CONFIG = {
|
||||
"u_range": (0, 1),
|
||||
"v_range": (0, 1),
|
||||
# Resolution counts number of points sampled, which for
|
||||
# each coordinate is one more than the the number of rows/columns
|
||||
# of approximating squares
|
||||
# each coordinate is one more than the the number of
|
||||
# rows/columns of approximating squares
|
||||
"resolution": (101, 101),
|
||||
"color": GREY,
|
||||
"opacity": 1.0,
|
||||
|
@ -38,11 +37,13 @@ class ParametricSurface(Mobject):
|
|||
]
|
||||
}
|
||||
|
||||
def __init__(self, uv_func, **kwargs):
|
||||
digest_config(self, kwargs)
|
||||
self.uv_func = uv_func
|
||||
self.compute_triangle_indices()
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.compute_triangle_indices()
|
||||
|
||||
def uv_func(self, u, v):
|
||||
# To be implemented in subclasses
|
||||
return (u, v, 0.0)
|
||||
|
||||
def init_points(self):
|
||||
dim = self.dim
|
||||
|
@ -102,7 +103,7 @@ class ParametricSurface(Mobject):
|
|||
def pointwise_become_partial(self, smobject, a, b, axis=None):
|
||||
if axis is None:
|
||||
axis = self.prefered_creation_axis
|
||||
assert(isinstance(smobject, ParametricSurface))
|
||||
assert(isinstance(smobject, Surface))
|
||||
if a <= 0 and b >= 1:
|
||||
self.match_points(smobject)
|
||||
return self
|
||||
|
@ -165,7 +166,16 @@ class ParametricSurface(Mobject):
|
|||
return self.get_triangle_indices()
|
||||
|
||||
|
||||
class SGroup(ParametricSurface):
|
||||
class ParametricSurface(Surface):
|
||||
def __init__(self, uv_func, **kwargs):
|
||||
self.passed_uv_func = uv_func
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def uv_func(self, u, v):
|
||||
return self.passed_uv_func(u, v)
|
||||
|
||||
|
||||
class SGroup(Surface):
|
||||
CONFIG = {
|
||||
"resolution": (0, 0),
|
||||
}
|
||||
|
@ -178,7 +188,7 @@ class SGroup(ParametricSurface):
|
|||
pass # Needed?
|
||||
|
||||
|
||||
class TexturedSurface(ParametricSurface):
|
||||
class TexturedSurface(Surface):
|
||||
CONFIG = {
|
||||
"shader_folder": "textured_surface",
|
||||
"shader_dtype": [
|
||||
|
@ -191,8 +201,8 @@ class TexturedSurface(ParametricSurface):
|
|||
}
|
||||
|
||||
def __init__(self, uv_surface, image_file, dark_image_file=None, **kwargs):
|
||||
if not isinstance(uv_surface, ParametricSurface):
|
||||
raise Exception("uv_surface must be of type ParametricSurface")
|
||||
if not isinstance(uv_surface, Surface):
|
||||
raise Exception("uv_surface must be of type Surface")
|
||||
# Set texture information
|
||||
if dark_image_file is None:
|
||||
dark_image_file = image_file
|
||||
|
@ -210,7 +220,7 @@ class TexturedSurface(ParametricSurface):
|
|||
self.v_range = uv_surface.v_range
|
||||
self.resolution = uv_surface.resolution
|
||||
self.gloss = self.uv_surface.gloss
|
||||
super().__init__(self.uv_func, **kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_data(self):
|
||||
super().init_data()
|
||||
|
|
|
@ -2,7 +2,7 @@ import itertools as it
|
|||
import operator as op
|
||||
import moderngl
|
||||
|
||||
from functools import reduce
|
||||
from functools import reduce, wraps
|
||||
|
||||
from manimlib.constants import *
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
|
@ -403,13 +403,13 @@ class VMobject(Mobject):
|
|||
])
|
||||
return self
|
||||
|
||||
def set_points_smoothly(self, points):
|
||||
def set_points_smoothly(self, points, true_smooth=False):
|
||||
self.set_points_as_corners(points)
|
||||
self.make_smooth()
|
||||
return self
|
||||
|
||||
def change_anchor_mode(self, mode):
|
||||
assert(mode in ["jagged", "smooth"])
|
||||
assert(mode in ("jagged", "approx_smooth", "true_smooth"))
|
||||
nppc = self.n_points_per_curve
|
||||
for submob in self.family_members_with_points():
|
||||
subpaths = submob.get_subpaths()
|
||||
|
@ -417,12 +417,9 @@ class VMobject(Mobject):
|
|||
for subpath in subpaths:
|
||||
anchors = np.vstack([subpath[::nppc], subpath[-1:]])
|
||||
new_subpath = np.array(subpath)
|
||||
if mode == "smooth":
|
||||
# TOOD, it's not clear which of the two options below should be the default,
|
||||
# leaving option 1 here commented out as a temporary note.
|
||||
# Option 1:
|
||||
# new_subpath[1::nppc] = get_smooth_quadratic_bezier_handle_points(anchors)
|
||||
# Option 2:
|
||||
if mode == "approx_smooth":
|
||||
new_subpath[1::nppc] = get_smooth_quadratic_bezier_handle_points(anchors)
|
||||
elif mode == "true_smooth":
|
||||
h1, h2 = get_smooth_cubic_bezier_handle_points(anchors)
|
||||
new_subpath = get_quadratic_approximation_of_cubic(anchors[:-1], h1, h2, anchors[1:])
|
||||
elif mode == "jagged":
|
||||
|
@ -432,15 +429,34 @@ class VMobject(Mobject):
|
|||
return self
|
||||
|
||||
def make_smooth(self):
|
||||
# TODO, Change this to not rely on a cubic-to-quadratic conversion
|
||||
return self.change_anchor_mode("smooth")
|
||||
"""
|
||||
This will double the number of points in the mobject,
|
||||
so should not be called repeatedly. It also means
|
||||
transforming between states before and after calling
|
||||
this might have strange artifacts
|
||||
"""
|
||||
self.change_anchor_mode("true_smooth")
|
||||
return self
|
||||
|
||||
def make_approximately_smooth(self):
|
||||
"""
|
||||
Unlike make_smooth, this will not change the number of
|
||||
points, but it also does not result in a perfectly smooth
|
||||
curve. It's most useful when the points have been
|
||||
sampled at a not-too-low rate from a continuous function,
|
||||
as in the case of ParametricCurve
|
||||
"""
|
||||
self.change_anchor_mode("approx_smooth")
|
||||
return self
|
||||
|
||||
def make_jagged(self):
|
||||
return self.change_anchor_mode("jagged")
|
||||
self.change_anchor_mode("jagged")
|
||||
return self
|
||||
|
||||
def add_subpath(self, points):
|
||||
assert(len(points) % self.n_points_per_curve == 0)
|
||||
self.append_points(points)
|
||||
return self
|
||||
|
||||
def append_vectorized_mobject(self, vectorized_mobject):
|
||||
new_points = list(vectorized_mobject.get_points())
|
||||
|
@ -450,6 +466,7 @@ class VMobject(Mobject):
|
|||
# a new path
|
||||
self.resize_data(len(self.get_points() - 1))
|
||||
self.append_points(new_points)
|
||||
return self
|
||||
|
||||
#
|
||||
def consider_points_equals(self, p0, p1):
|
||||
|
@ -807,6 +824,7 @@ class VMobject(Mobject):
|
|||
return tri_indices
|
||||
|
||||
def triggers_refreshed_triangulation(func):
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
old_points = self.get_points()
|
||||
func(self, *args, **kwargs)
|
||||
|
@ -827,10 +845,10 @@ class VMobject(Mobject):
|
|||
|
||||
# TODO, how to be smart about tangents here?
|
||||
@triggers_refreshed_triangulation
|
||||
def apply_function(self, function):
|
||||
super().apply_function(function)
|
||||
if self.make_smooth_after_applying_functions:
|
||||
self.make_smooth()
|
||||
def apply_function(self, function, make_smooth=False, **kwargs):
|
||||
super().apply_function(function, **kwargs)
|
||||
if self.make_smooth_after_applying_functions or make_smooth:
|
||||
self.make_approximately_smooth()
|
||||
return self
|
||||
|
||||
@triggers_refreshed_triangulation
|
||||
|
|
|
@ -19,10 +19,9 @@ from manimlib.utils.color import color_gradient
|
|||
from manimlib.utils.color import invert_color
|
||||
from manimlib.utils.space_ops import angle_of_vector
|
||||
|
||||
# TODO, this should probably reimplemented entirely, especially so as to
|
||||
# better reuse code from mobject/coordinate_systems.
|
||||
# Also, I really dislike how the configuration is set up, this
|
||||
# is way too messy to work with.
|
||||
# TODO, this class should be deprecated, with all its
|
||||
# functionality moved to Axes and handled at the mobject
|
||||
# level rather than the scene level
|
||||
|
||||
|
||||
class GraphScene(Scene):
|
||||
|
@ -82,7 +81,7 @@ class GraphScene(Scene):
|
|||
if len(self.x_labeled_nums) > 0:
|
||||
if self.exclude_zero_label:
|
||||
self.x_labeled_nums = [x for x in self.x_labeled_nums if x != 0]
|
||||
x_axis.add_numbers(*self.x_labeled_nums)
|
||||
x_axis.add_numbers(self.x_labeled_nums)
|
||||
if self.x_axis_label:
|
||||
x_label = TexText(self.x_axis_label)
|
||||
x_label.next_to(
|
||||
|
@ -116,7 +115,7 @@ class GraphScene(Scene):
|
|||
if len(self.y_labeled_nums) > 0:
|
||||
if self.exclude_zero_label:
|
||||
self.y_labeled_nums = [y for y in self.y_labeled_nums if y != 0]
|
||||
y_axis.add_numbers(*self.y_labeled_nums)
|
||||
y_axis.add_numbers(self.y_labeled_nums)
|
||||
if self.y_axis_label:
|
||||
y_label = TexText(self.y_axis_label)
|
||||
y_label.next_to(
|
|
@ -3,6 +3,7 @@ import random
|
|||
import platform
|
||||
import itertools as it
|
||||
import logging
|
||||
from functools import wraps
|
||||
|
||||
from tqdm import tqdm as ProgressDisplay
|
||||
import numpy as np
|
||||
|
@ -372,6 +373,7 @@ class Scene(object):
|
|||
return animations
|
||||
|
||||
def handle_play_like_call(func):
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
self.update_skipping_status()
|
||||
should_write = not self.skip_animations
|
||||
|
|
|
@ -115,30 +115,34 @@ def match_interpolate(new_start, new_end, old_start, old_end, old_value):
|
|||
)
|
||||
|
||||
|
||||
# Figuring out which bezier curves most smoothly connect a sequence of points
|
||||
def get_smooth_quadratic_bezier_handle_points(points):
|
||||
n = len(points)
|
||||
# Top matrix sets the constraint h_i + h_{i + 1} = 2 * P_i
|
||||
top_mat = np.zeros((n - 2, n - 1))
|
||||
np.fill_diagonal(top_mat, 1)
|
||||
np.fill_diagonal(top_mat[:, 1:], 1)
|
||||
"""
|
||||
Figuring out which bezier curves most smoothly connect a sequence of points.
|
||||
|
||||
# Lower matrix sets the constraint that 2(h1 - h0)= p2 - p0 and 2(h_{n-1}- h_{n-2}) = p_n - p_{n-2}
|
||||
low_mat = np.zeros((2, n - 1))
|
||||
low_mat[0, :2] = [-2, 2]
|
||||
low_mat[1, -2:] = [-2, 2]
|
||||
Given three successive points, P0, P1 and P2, you can compute that by defining
|
||||
h = (1/4) P0 + P1 - (1/4)P2, the bezier curve defined by (P0, h, P1) will pass
|
||||
through the point P2.
|
||||
|
||||
# Use the pseudoinverse to find a near solution to these constraints
|
||||
full_mat = np.vstack([top_mat, low_mat])
|
||||
full_mat_pinv = np.linalg.pinv(full_mat)
|
||||
|
||||
rhs = np.vstack([
|
||||
2 * points[1:-1],
|
||||
[points[2] - points[0]],
|
||||
[points[-1] - points[-3]],
|
||||
])
|
||||
|
||||
return np.dot(full_mat_pinv, rhs)
|
||||
So for a given set of four successive points, P0, P1, P2, P3, if we want to add
|
||||
a handle point h between P1 and P2 so that the quadratic bezier (P1, h, P2) is
|
||||
part of a smooth curve passing through all four points, we calculate one solution
|
||||
for h that would produce a parbola passing through P3, call it smooth_to_right, and
|
||||
another that would produce a parabola passing through P0, call it smooth_to_left,
|
||||
and use the midpoint between the two.
|
||||
"""
|
||||
smooth_to_right, smooth_to_left = [
|
||||
0.25 * ps[0:-2] + ps[1:-1] - 0.25 * ps[2:]
|
||||
for ps in (points, points[::-1])
|
||||
]
|
||||
if np.isclose(points[0], points[-1]).all():
|
||||
last_str = 0.25 * points[-2] + points[-1] - 0.25 * points[1]
|
||||
last_stl = 0.25 * points[1] + points[0] - 0.25 * points[-2]
|
||||
else:
|
||||
last_str = smooth_to_left[0]
|
||||
last_stl = smooth_to_right[0]
|
||||
handles = 0.5 * np.vstack([smooth_to_right, [last_str]])
|
||||
handles += 0.5 * np.vstack([last_stl, smooth_to_left[::-1]])
|
||||
return handles
|
||||
|
||||
|
||||
def get_smooth_cubic_bezier_handle_points(points):
|
||||
|
@ -259,7 +263,7 @@ def get_quadratic_approximation_of_cubic(a0, h0, h1, a1):
|
|||
ti_min_in_range = has_infl & (0 < ti_min) & (ti_min < 1)
|
||||
ti_max_in_range = has_infl & (0 < ti_max) & (ti_max < 1)
|
||||
|
||||
# Choose a value of t which is starts as 0.5,
|
||||
# Choose a value of t which starts at 0.5,
|
||||
# but is updated to one of the inflection points
|
||||
# if they lie between 0 and 1
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
[metadata]
|
||||
name = manimlib
|
||||
name = manimgl
|
||||
version = 1.0.0
|
||||
author = Grant Sanderson
|
||||
author-email= grant@3blue1brown.com
|
||||
summary = Animation engine for explanatory math videos
|
||||
|
@ -8,13 +9,15 @@ description-content-type = text/markdown; charset=UTF-8
|
|||
home-page = https://github.com/3b1b/manim
|
||||
project_urls =
|
||||
Bug Tracker = https://github.com/3b1b/manim/issues
|
||||
Documentation = https://eulertour.com/learn/manim
|
||||
Documentation = https://3b1b.github.io/manim/
|
||||
Source Code = https://github.com/3b1b/manim
|
||||
license = MIT
|
||||
|
||||
[files]
|
||||
packages = manimlib
|
||||
extra_files = requirements.txt
|
||||
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
manim = manimlib:main
|
||||
manimgl = manimlib:main
|
||||
manim-render = manimlib:main
|
||||
|
|
Loading…
Add table
Reference in a new issue