diff --git a/README.md b/README.md index 64e561f5..7101b55e 100644 --- a/README.md +++ b/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). diff --git a/custom_defaults.yml b/custom_defaults.yml index 7b89767c..d34236ae 100644 --- a/custom_defaults.yml +++ b/custom_defaults.yml @@ -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" diff --git a/example_scenes.py b/example_scenes.py index f2a95727..fc44f8f1 100644 --- a/example_scenes.py +++ b/example_scenes.py @@ -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() diff --git a/manimlib/animation/creation.py b/manimlib/animation/creation.py index f888bb73..4f6aba64 100644 --- a/manimlib/animation/creation.py +++ b/manimlib/animation/creation.py @@ -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: diff --git a/manimlib/animation/transform.py b/manimlib/animation/transform.py index 297f322e..257feb17 100644 --- a/manimlib/animation/transform.py +++ b/manimlib/animation/transform.py @@ -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) diff --git a/manimlib/imports.py b/manimlib/imports.py index 084aa0ac..6a2035dd 100644 --- a/manimlib/imports.py +++ b/manimlib/imports.py @@ -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. diff --git a/manimlib/mobject/functions.py b/manimlib/mobject/functions.py index 11e8daf5..e67abeee 100644 --- a/manimlib/mobject/functions.py +++ b/manimlib/mobject/functions.py @@ -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 diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 8f711a6d..bd7c487e 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -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) diff --git a/manimlib/mobject/number_line.py b/manimlib/mobject/number_line.py index d6e94ece..1e50692e 100644 --- a/manimlib/mobject/number_line.py +++ b/manimlib/mobject/number_line.py @@ -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: diff --git a/manimlib/mobject/types/dot_cloud.py b/manimlib/mobject/types/dot_cloud.py index d579e8fd..82e2b0ca 100644 --- a/manimlib/mobject/types/dot_cloud.py +++ b/manimlib/mobject/types/dot_cloud.py @@ -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: diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index b5b33e29..425af386 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -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 diff --git a/manimlib/mobject/value_tracker.py b/manimlib/mobject/value_tracker.py index ab707a78..def00c14 100644 --- a/manimlib/mobject/value_tracker.py +++ b/manimlib/mobject/value_tracker.py @@ -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. diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index d82a4539..1f4e4926 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -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. diff --git a/manimlib/shaders/quadratic_bezier_fill/frag.glsl b/manimlib/shaders/quadratic_bezier_fill/frag.glsl index c16c167e..b2a1c82a 100644 --- a/manimlib/shaders/quadratic_bezier_fill/frag.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/frag.glsl @@ -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); } diff --git a/manimlib/shaders/quadratic_bezier_fill/geom.glsl b/manimlib/shaders/quadratic_bezier_fill/geom.glsl index acd15617..4fd9245f 100644 --- a/manimlib/shaders/quadratic_bezier_fill/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/geom.glsl @@ -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); } diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index ffb4e8eb..8baea0f9 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -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)); diff --git a/manimlib/utils/directories.py b/manimlib/utils/directories.py index e5629140..dfc9aaec 100644 --- a/manimlib/utils/directories.py +++ b/manimlib/utils/directories.py @@ -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"]) diff --git a/manimlib/utils/file_ops.py b/manimlib/utils/file_ops.py index 761bf039..75587761 100644 --- a/manimlib/utils/file_ops.py +++ b/manimlib/utils/file_ops.py @@ -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 diff --git a/manimlib/utils/space_ops.py b/manimlib/utils/space_ops.py index 177f9ae6..c124cbec 100644 --- a/manimlib/utils/space_ops.py +++ b/manimlib/utils/space_ops.py @@ -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] diff --git a/manimlib/window.py b/manimlib/window.py index 9a883e22..eeb60ae5 100644 --- a/manimlib/window.py +++ b/manimlib/window.py @@ -12,7 +12,6 @@ class Window(PygletWindow): resizable = True gl_version = (3, 3) vsync = True - samples = 1 cursor = True def __init__(self, scene, **kwargs):