Merge pull request #3 from 3b1b/master

Update
This commit is contained in:
鹤翔万里 2021-02-07 10:53:58 +08:00 committed by GitHub
commit 634c3d672e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 492 additions and 283 deletions

30
.github/workflows/publish.yml vendored Normal file
View 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/*

View file

@ -1,13 +1,19 @@
![logo](logo/cropped.png)
<p align="center">
<a href="https://github.com/3b1b/manim">
<img src="https://raw.githubusercontent.com/3b1b/manim/master/logo/cropped.png">
</a>
</p>
[![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 Discord](https://img.shields.io/discord/581738731934056449.svg?label=discord)](https://discord.gg/mMRrZQW)
[![docs](https://github.com/3b1b/manim/workflows/docs/badge.svg)](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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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