mirror of
https://github.com/3b1b/manim.git
synced 2026-04-26 03:00:23 -04:00
13
README.md
13
README.md
@@ -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).
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -12,7 +12,6 @@ class Window(PygletWindow):
|
||||
resizable = True
|
||||
gl_version = (3, 3)
|
||||
vsync = True
|
||||
samples = 1
|
||||
cursor = True
|
||||
|
||||
def __init__(self, scene, **kwargs):
|
||||
|
||||
Reference in New Issue
Block a user