Merge pull request #2 from 3b1b/master

Update
This commit is contained in:
鹤翔万里
2021-02-04 11:27:43 +08:00
committed by GitHub
20 changed files with 132 additions and 70 deletions

View File

@@ -28,6 +28,19 @@ pip install -r requirements.txt
# Try it out
python manim.py example_scenes.py OpeningManimExample
```
### Mac OSX
1. Install FFmpeg, LaTeX, Cairo in terminal using homebrew.
```sh
brew install ffmpeg mactex cairo
```
2. Install latest version of manim using these command.
```sh
git clone https://github.com/3b1b/manim.git
cd manim
pip install -r requirements.txt
python manim.py example_scenes.py OpeningManimExample
```
### Directly (Windows)
1. [Install FFmpeg](https://www.wikihow.com/Install-FFmpeg-on-Windows).

View File

@@ -21,7 +21,7 @@ tex:
template_file: "tex_template.tex"
intermediate_filetype: "dvi"
text_to_replace: "[tex_expression]"
# # For ctex, use the following configuration
# For ctex, use the following configuration
# executable: "xelatex -no-pdf"
# template_file: "ctex_template.tex"
# intermediate_filetype: "xdv"

View File

@@ -25,7 +25,7 @@ class OpeningManimExample(Scene):
transform_title = Text("That was a transform")
transform_title.to_corner(UL)
self.play(
Transform(title, transform_title),
Transform(title[0], transform_title),
LaggedStartMap(FadeOut, basel, shift=DOWN),
)
self.wait()

View File

@@ -70,11 +70,19 @@ class DrawBorderThenFill(Animation):
super().__init__(vmobject, **kwargs)
def begin(self):
# Trigger triangulation calculation
for submob in self.mobject.get_family():
submob.get_triangulation()
self.outline = self.get_outline()
super().begin()
self.mobject.match_style(self.outline)
self.mobject.lock_matching_data(self.mobject, self.outline)
def finish(self):
super().finish()
self.mobject.unlock_data()
def get_outline(self):
outline = self.mobject.copy()
outline.set_fill(opacity=0)
@@ -103,6 +111,7 @@ class DrawBorderThenFill(Animation):
submob.set_data(outline.data)
submob.unlock_data()
submob.lock_matching_data(submob, start)
submob.needs_new_triangulation = False
self.sm_to_index[hash(submob)] = 1
if index == 0:

View File

@@ -294,7 +294,7 @@ class Swap(CyclicReplace):
pass # Renaming, more understandable for two entries
# TODO, this may be depricated...worth reimplementing?
# TODO, this may be deprecated...worth reimplementing?
class TransformAnimations(Transform):
CONFIG = {
"rate_func": squish_rate_func(smooth)

View File

@@ -1,5 +1,5 @@
"""
I won't pretend like this is best practice, by in creating animations for a video,
I won't pretend like this is best practice, but in creating animations for a video,
it can be very nice to simply have all of the Mobjects, Animations, Scenes, etc.
of manim available without having to worry about what namespace they come from.

View File

@@ -11,6 +11,7 @@ class ParametricCurve(VMobject):
"epsilon": 1e-8,
# TODO, automatically figure out discontinuities
"discontinuities": [],
"smoothing": True,
}
def __init__(self, t_func, t_range=None, **kwargs):
@@ -41,7 +42,8 @@ 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:])
self.make_smooth()
if self.smoothing:
self.make_smooth()
return self

View File

@@ -190,10 +190,12 @@ class Mobject(object):
return self.get_num_points() > 0
def get_bounding_box(self):
if not self.needs_new_bounding_box:
return self.data["bounding_box"]
if self.needs_new_bounding_box:
self.data["bounding_box"] = self.compute_bounding_box()
self.needs_new_bounding_box = False
return self.data["bounding_box"]
# all_points = self.get_all_points()
def compute_bounding_box(self):
all_points = np.vstack([
self.get_points(),
*(
@@ -203,15 +205,13 @@ class Mobject(object):
)
])
if len(all_points) == 0:
self.data["bounding_box"] = np.zeros((3, self.dim))
return np.zeros((3, self.dim))
else:
# Lower left and upper right corners
mins = all_points.min(0)
maxs = all_points.max(0)
mids = (mins + maxs) / 2
self.data["bounding_box"] = np.array([mins, mids, maxs])
self.needs_new_bounding_box = False
return self.data["bounding_box"]
return np.array([mins, mids, maxs])
def refresh_bounding_box(self, recurse_down=False, recurse_up=True):
for mob in self.get_family(recurse_down):
@@ -1002,7 +1002,7 @@ class Mobject(object):
def length_over_dim(self, dim):
bb = self.get_bounding_box()
return (bb[2] - bb[0])[dim]
return abs((bb[2] - bb[0])[dim])
def get_width(self):
return self.length_over_dim(0)

View File

@@ -37,7 +37,7 @@ class NumberLine(Line):
"num_decimal_places": 0,
"height": 0.25,
},
"exclude_zero_from_default_numbers": False,
"numbers_to_exclude": None
}
def __init__(self, x_range=None, **kwargs):
@@ -70,7 +70,7 @@ class NumberLine(Line):
if self.include_ticks:
self.add_ticks()
if self.include_numbers:
self.add_numbers()
self.add_numbers(excluding=self.numbers_to_exclude)
def get_tick_range(self):
if self.include_tip:

View File

@@ -1,6 +1,5 @@
import numpy as np
import moderngl
import numbers
from manimlib.constants import GREY_C
from manimlib.mobject.types.point_cloud_mobject import PMobject
@@ -16,7 +15,7 @@ class DotCloud(PMobject):
CONFIG = {
"color": GREY_C,
"opacity": 1,
"radii": DEFAULT_DOT_CLOUD_RADIUS,
"radius": DEFAULT_DOT_CLOUD_RADIUS,
"shader_folder": "true_dot",
"render_primitive": moderngl.POINTS,
"shader_dtype": [
@@ -34,7 +33,7 @@ class DotCloud(PMobject):
def init_data(self):
super().init_data()
self.data["radii"] = np.zeros((1, 1))
self.set_radii(self.radii)
self.set_radius(self.radius)
def to_grid(self, n_rows, n_cols, n_layers=1,
buff_ratio=None,
@@ -58,25 +57,38 @@ class DotCloud(PMobject):
radius = self.get_radius()
ns = [n_cols, n_rows, n_layers]
brs = [h_buff_ratio, v_buff_ratio, d_buff_ratio]
self.set_radius(0)
for n, br, dim in zip(ns, brs, range(3)):
self.rescale_to_fit(2 * radius * (1 + br) * (n - 1), dim, stretch=True)
self.set_radius(radius)
if height is not None:
self.set_height(height)
self.center()
return self
def set_radii(self, radii):
if not isinstance(radii, numbers.Number):
radii = resize_preserving_order(radii, len(self.data["radii"]))
self.data["radii"][:] = radii
self.data["radii"][:] = resize_preserving_order(radii, len(self.data["radii"]))
self.refresh_bounding_box()
return self
def get_radii(self):
return self.data["radii"]
def set_radius(self, radius):
self.data["radii"][:] = radius
self.refresh_bounding_box()
return self
def get_radius(self):
return self.get_radii().max()
def compute_bounding_box(self):
bb = super().compute_bounding_box()
radius = self.get_radius()
bb[0] += np.full((3,), -radius)
bb[2] += np.full((3,), radius)
return bb
def scale(self, scale_factor, scale_radii=True, **kwargs):
super().scale(scale_factor, **kwargs)
if scale_radii:

View File

@@ -765,9 +765,9 @@ class VMobject(Mobject):
self.needs_new_triangulation = False
return self.triangulation
# Rotate points such that unit normal vector is OUT
# TODO, 99% of the time this does nothing. Do a check for that?
points = np.dot(points, z_to_vector(normal_vector))
if not np.isclose(normal_vector, OUT).all():
# Rotate points such that unit normal vector is OUT
points = np.dot(points, z_to_vector(normal_vector))
indices = np.arange(len(points), dtype=int)
b0s = points[0::3]
@@ -797,7 +797,9 @@ class VMobject(Mobject):
# Triangulate
inner_verts = points[inner_vert_indices]
inner_tri_indices = inner_vert_indices[earclip_triangulation(inner_verts, rings)]
inner_tri_indices = inner_vert_indices[
earclip_triangulation(inner_verts, rings)
]
tri_indices = np.hstack([indices, inner_tri_indices])
self.triangulation = tri_indices

View File

@@ -5,7 +5,7 @@ from manimlib.mobject.mobject import Mobject
class ValueTracker(Mobject):
"""
Note meant to be displayed. Instead the position encodes some
Not meant to be displayed. Instead the position encodes some
number, often one which another animation or continual_animation
uses for its update function, and by treating it as a mobject it can
still be animated and manipulated just like anything else.

View File

@@ -125,7 +125,7 @@ class Scene(object):
# once embeded, and add a few custom shortcuts
local_ns = inspect.currentframe().f_back.f_locals
local_ns["touch"] = self.interact
for term in ("play", "add", "remove", "clear", "save_state", "restore"):
for term in ("play", "wait", "add", "remove", "clear", "save_state", "restore"):
local_ns[term] = getattr(self, term)
shell(local_ns=local_ns, stack_depth=2)
# End scene when exiting an embed.

View File

@@ -51,7 +51,7 @@ float sdf(){
float sgn = orientation * sign(v2);
float Fp = (p.x * p.x - p.y);
if(sgn * Fp < 0){
return 0;
return 0.0;
}else{
return min_dist_to_curve(uv_coords, uv_b2, bezier_degree);
}

View File

@@ -121,11 +121,11 @@ void main(){
return;
}
vec3 local_unit_normal = get_unit_normal(vec3[3](bp[0], bp[1], bp[2]));
orientation = sign(dot(v_global_unit_normal[0], local_unit_normal));
vec3 new_bp[3];
bezier_degree = get_reduced_control_points(vec3[3](bp[0], bp[1], bp[2]), new_bp);
vec3 local_unit_normal = get_unit_normal(new_bp);
orientation = sign(dot(v_global_unit_normal[0], local_unit_normal));
if(bezier_degree >= 1){
emit_pentagon(new_bp, local_unit_normal);
}

View File

@@ -66,7 +66,7 @@ void flatten_points(in vec3[3] points, out vec2[3] flat_points){
float angle_between_vectors(vec2 v1, vec2 v2){
float v1_norm = length(v1);
float v2_norm = length(v2);
if(v1_norm == 0 || v2_norm == 0) return 0;
if(v1_norm == 0 || v2_norm == 0) return 0.0;
float dp = dot(v1, v2) / (v1_norm * v2_norm);
float angle = acos(clamp(dp, -1.0, 1.0));
float sn = sign(cross2d(v1, v2));

View File

@@ -23,6 +23,9 @@ def get_text_dir():
def get_mobject_data_dir():
return guarantee_existence(os.path.join(get_temp_dir(), "mobject_data"))
def get_downloads_dir():
return guarantee_existence(os.path.join(get_temp_dir(), "manim_downloads"))
def get_output_dir():
return guarantee_existence(get_directories()["output"])

View File

@@ -3,7 +3,6 @@ import numpy as np
import validators
import urllib.request
import tempfile
def add_extension_if_not_present(file_name, extension):
@@ -24,10 +23,9 @@ def find_file(file_name, directories=None, extensions=None):
# Check if this is a file online first, and if so, download
# it to a temporary directory
if validators.url(file_name):
from manimlib.utils.directories import get_downloads_dir
stem, name = os.path.split(file_name)
folder = guarantee_existence(
os.path.join(tempfile.gettempdir(), "manim_downloads")
)
folder = get_downloads_dir()
path = os.path.join(folder, name)
urllib.request.urlretrieve(file_name, path)
return path

View File

@@ -1,6 +1,6 @@
import numpy as np
import math
import itertools as it
import math
from mapbox_earcut import triangulate_float32 as earcut
from manimlib.constants import RIGHT
@@ -344,58 +344,82 @@ def is_inside_triangle(p, a, b, c):
def norm_squared(v):
return sum(v * v)
return v[0] * v[0] + v[1] * v[1] + v[2] * v[2]
# TODO, fails for polygons drawn over themselves
def earclip_triangulation(verts, rings):
def earclip_triangulation(verts, ring_ends):
"""
Returns a list of indices giving a triangulation
of a polygon, potentially with holes
- verts is an NxM numpy array of points with M > 2
- verts is a numpy array of points
- rings is a list of indices indicating where
- ring_ends is a list of indices indicating where
the ends of new paths are
"""
n = len(verts)
# Establish where loop indices should be connected
loop_connections = dict()
# for e0, e1 in zip(rings, rings[1:]):
e0 = rings[0]
for e1 in rings[1:]:
# Find closet pair of points with the first
# coming from the current ring, and the second
# coming from the next ring
index_pairs = [
(i, j)
for i in range(0, e0)
for j in range(e0, e1)
if i not in loop_connections
if j not in loop_connections
]
i, j = index_pairs[np.argmin([
norm_squared(verts[i] - verts[j])
for i, j in index_pairs
])]
# Connect the polygon at these points so that
# it's treated as a single highly-convex ring
# First, connect all the rings so that the polygon
# with holes is instead treated as a (very convex)
# polygon with one edge. Do this by drawing connections
# between rings close to each other
rings = [
list(range(e0, e1))
for e0, e1 in zip([0, *ring_ends], ring_ends)
]
attached_rings = rings[:1]
detached_rings = rings[1:]
loop_connections = dict()
while detached_rings:
i_range, j_range = [
list(filter(
# Ignore indices that are already being
# used to draw some connection
lambda i: i not in loop_connections,
it.chain(*ring_group)
))
for ring_group in (attached_rings, detached_rings)
]
# Closet point on the atttached rings to an estimated midpoint
# of the detached rings
tmp_j_vert = midpoint(
verts[j_range[0]],
verts[j_range[len(j_range) // 2]]
)
i = min(i_range, key=lambda i: norm_squared(verts[i] - tmp_j_vert))
# Closet point of the detached rings to the aforementioned
# point of the attached rings
j = min(j_range, key=lambda j: norm_squared(verts[i] - verts[j]))
# Recalculate i based on new j
i = min(i_range, key=lambda i: norm_squared(verts[i] - verts[j]))
# Remember to connect the polygon at these points
loop_connections[i] = j
loop_connections[j] = i
e0 = e1
# Move the ring which j belongs to from the
# attached list to the detached list
new_ring = next(filter(
lambda ring: ring[0] <= j < ring[-1],
detached_rings
))
detached_rings.remove(new_ring)
attached_rings.append(new_ring)
# Setup linked list
after = []
e0 = 0
for e1 in rings:
after.extend([*range(e0 + 1, e1), e0])
e0 = e1
end0 = 0
for end1 in ring_ends:
after.extend(range(end0 + 1, end1))
after.append(end0)
end0 = end1
# Find an ordering of indices walking around the polygon
indices = []
i = 0
for x in range(n + len(rings) - 1):
for x in range(len(verts) + len(ring_ends) - 1):
# starting = False
if i in loop_connections:
j = loop_connections[i]

View File

@@ -12,7 +12,6 @@ class Window(PygletWindow):
resizable = True
gl_version = (3, 3)
vsync = True
samples = 1
cursor = True
def __init__(self, scene, **kwargs):