diff --git a/docs/source/development/changelog.rst b/docs/source/development/changelog.rst index 8ed21827..597fb44a 100644 --- a/docs/source/development/changelog.rst +++ b/docs/source/development/changelog.rst @@ -4,7 +4,28 @@ Changelog Unreleased ---------- -No changes +Fixed bugs +^^^^^^^^^^ + +- `#1592 `__: Fixed ``put_start_and_end_on`` in 3D +- `#1601 `__: Fixed ``DecimalNumber``'s scaling issue + +New Features +^^^^^^^^^^^^ + +- `#1598 `__: Supported the elliptical arc command ``A`` for ``SVGMobject`` +- `#1607 `__: Added ``FlashyFadeIn`` +- `#1607 `__: Save triangulation +- `#1625 `__: Added new ``Code`` mobject +- `bd356da `__: Added ``VCube`` +- `6d72893 `__: Supported ``ValueTracker`` to track vectors + +Refactor +^^^^^^^^ + +- `#1601 `__: Change back to simpler ``Mobject.scale`` implementation +- `b667db2 `__: Simplify ``Square`` +- `40290ad `__: Removed unused parameter ``triangulation_locked`` v1.1.0 ------- diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index 7ce70271..aca7646c 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -316,17 +316,6 @@ class Camera(object): self.frame.set_height(frame_height) self.frame.set_width(frame_width) - def pixel_coords_to_space_coords(self, px, py, relative=False): - pw, ph = self.fbo.size - fw, fh = self.get_frame_shape() - fc = self.get_frame_center() - if relative: - return 2 * np.array([px / pw, py / ph, 0]) - else: - # Only scale wrt one axis - scale = fh / ph - return fc + scale * np.array([(px - pw / 2), (py - ph / 2), 0]) - # Rendering def capture(self, *mobjects, **kwargs): self.refresh_perspective_uniforms() @@ -423,6 +412,8 @@ class Camera(object): shader[name].value = tid for name, value in it.chain(shader_wrapper.uniforms.items(), self.perspective_uniforms.items()): try: + if isinstance(value, np.ndarray): + value = tuple(value) shader[name].value = value except KeyError: pass @@ -449,12 +440,13 @@ class Camera(object): } def init_textures(self): - self.path_to_texture_id = {} + self.n_textures = 0 + self.path_to_texture = {} def get_texture_id(self, path): - if path not in self.path_to_texture_id: - # A way to increase tid's sequentially - tid = len(self.path_to_texture_id) + if path not in self.path_to_texture: + tid = self.n_textures + self.n_textures += 1 im = Image.open(path).convert("RGBA") texture = self.ctx.texture( size=im.size, @@ -462,8 +454,14 @@ class Camera(object): data=im.tobytes(), ) texture.use(location=tid) - self.path_to_texture_id[path] = tid - return self.path_to_texture_id[path] + self.path_to_texture[path] = (tid, texture) + return self.path_to_texture[path][0] + + def release_texture(self, path): + tid_and_texture = self.path_to_texture.pop(path, None) + if tid_and_texture: + tid_and_texture[1].release() + return self # Mostly just defined so old scenes don't break diff --git a/manimlib/mobject/changing.py b/manimlib/mobject/changing.py index 69e9302b..febe4acb 100644 --- a/manimlib/mobject/changing.py +++ b/manimlib/mobject/changing.py @@ -1,8 +1,13 @@ -from manimlib.constants import * +import numpy as np +from manimlib.constants import BLUE_D +from manimlib.constants import BLUE_B +from manimlib.constants import BLUE_E +from manimlib.constants import GREY_BROWN +from manimlib.constants import WHITE +from manimlib.mobject.mobject import Mobject from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.utils.rate_functions import smooth -from manimlib.utils.space_ops import get_norm class AnimatedBoundary(VGroup): @@ -74,25 +79,63 @@ class TracedPath(VMobject): CONFIG = { "stroke_width": 2, "stroke_color": WHITE, - "min_distance_to_new_point": 0.1, + "time_traced": np.inf, + "fill_opacity": 0, + "time_per_anchor": 1 / 15, } def __init__(self, traced_point_func, **kwargs): super().__init__(**kwargs) self.traced_point_func = traced_point_func - self.add_updater(lambda m: m.update_path()) + self.time = 0 + self.traced_points = [] + self.add_updater(lambda m, dt: m.update_path(dt)) - def update_path(self): - new_point = self.traced_point_func() - if not self.has_points(): - self.start_new_path(new_point) - self.add_line_to(new_point) + def update_path(self, dt): + if dt == 0: + return self + point = self.traced_point_func().copy() + self.traced_points.append(point) + + if self.time_traced < np.inf: + n_relevant_points = int(self.time_traced / dt + 0.5) + # n_anchors = int(self.time_traced / self.time_per_anchor) + n_tps = len(self.traced_points) + if n_tps < n_relevant_points: + points = self.traced_points + [point] * (n_relevant_points - n_tps) + else: + points = self.traced_points[n_tps - n_relevant_points:] + # points = [ + # self.traced_points[max(n_tps - int(alpha * n_relevant_points) - 1, 0)] + # for alpha in np.linspace(1, 0, n_anchors) + # ] + # Every now and then refresh the list + if n_tps > 10 * n_relevant_points: + self.traced_points = self.traced_points[-n_relevant_points:] else: - # Set the end to be the new point - self.get_points()[-1] = new_point + # sparseness = max(int(self.time_per_anchor / dt), 1) + # points = self.traced_points[::sparseness] + # points[-1] = self.traced_points[-1] + points = self.traced_points - # Second to last point - nppcc = self.n_points_per_curve - dist = get_norm(new_point - self.get_points()[-nppcc]) - if dist >= self.min_distance_to_new_point: - self.add_line_to(new_point) + if points: + self.set_points_smoothly(points) + + self.time += dt + return self + + +class TracingTail(TracedPath): + CONFIG = { + "stroke_width": (0, 3), + "stroke_opacity": (0, 1), + "stroke_color": WHITE, + "time_traced": 1.0, + } + + def __init__(self, mobject_or_func, **kwargs): + if isinstance(mobject_or_func, Mobject): + func = mobject_or_func.get_center + else: + func = mobject_or_func + super().__init__(func, **kwargs) diff --git a/manimlib/mobject/coordinate_systems.py b/manimlib/mobject/coordinate_systems.py index a52b2f82..25e5d587 100644 --- a/manimlib/mobject/coordinate_systems.py +++ b/manimlib/mobject/coordinate_systems.py @@ -10,6 +10,7 @@ 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 digest_config 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 @@ -25,13 +26,18 @@ class CoordinateSystem(): """ CONFIG = { "dimension": 2, - "x_range": np.array([-8.0, 8.0, 1.0]), - "y_range": np.array([-4.0, 4.0, 1.0]), - "width": None, - "height": None, + "default_x_range": [-8.0, 8.0, 1.0], + "default_y_range": [-4.0, 4.0, 1.0], + "width": FRAME_WIDTH, + "height": FRAME_HEIGHT, "num_sampled_graph_points_per_tick": 20, } + def __init__(self, **kwargs): + digest_config(self, kwargs) + self.x_range = np.array(self.default_x_range) + self.y_range = np.array(self.default_y_range) + def coords_to_point(self, *coords): raise Exception("Not implemented") @@ -127,6 +133,7 @@ class CoordinateSystem(): **kwargs ) graph.underlying_function = function + graph.x_range = x_range return graph def get_parametric_curve(self, function, **kwargs): @@ -282,7 +289,9 @@ class Axes(VGroup, CoordinateSystem): x_range=None, y_range=None, **kwargs): - super().__init__(**kwargs) + CoordinateSystem.__init__(self, **kwargs) + VGroup.__init__(self, **kwargs) + if x_range is not None: self.x_range[:len(x_range)] = x_range if y_range is not None: @@ -441,7 +450,7 @@ class NumberPlane(Axes): return lines1, lines2 def get_lines_parallel_to_axis(self, axis1, axis2): - freq = axis1.x_step + freq = axis2.x_step ratio = self.faded_line_ratio line = Line(axis1.get_start(), axis1.get_end()) dense_freq = (1 + ratio) @@ -501,15 +510,15 @@ class ComplexPlane(NumberPlane): def p2n(self, point): return self.point_to_number(point) - def get_default_coordinate_values(self): + def get_default_coordinate_values(self, skip_first=True): x_numbers = self.get_x_axis().get_tick_range()[1:] y_numbers = self.get_y_axis().get_tick_range()[1:] y_numbers = [complex(0, y) for y in y_numbers if y != 0] return [*x_numbers, *y_numbers] - def add_coordinate_labels(self, numbers=None, **kwargs): + def add_coordinate_labels(self, numbers=None, skip_first=True, **kwargs): if numbers is None: - numbers = self.get_default_coordinate_values() + numbers = self.get_default_coordinate_values(skip_first) self.coordinate_labels = VGroup() for number in numbers: @@ -522,6 +531,15 @@ class ComplexPlane(NumberPlane): axis = self.get_x_axis() value = z.real number_mob = axis.get_number_mobject(value, **kwargs) + # For i and -i, remove the "1" + if z.imag == 1: + number_mob.remove(number_mob[0]) + if z.imag == -1: + number_mob.remove(number_mob[1]) + number_mob[0].next_to( + number_mob[1], LEFT, + buff=number_mob[0].get_width() / 4 + ) self.coordinate_labels.add(number_mob) self.add(self.coordinate_labels) return self diff --git a/manimlib/mobject/geometry.py b/manimlib/mobject/geometry.py index ee6ecb93..1b88ec35 100644 --- a/manimlib/mobject/geometry.py +++ b/manimlib/mobject/geometry.py @@ -1,4 +1,6 @@ import numpy as np +import math +import numbers from manimlib.constants import * from manimlib.mobject.mobject import Mobject @@ -27,6 +29,7 @@ DEFAULT_ARROW_TIP_LENGTH = 0.35 DEFAULT_ARROW_TIP_WIDTH = 0.35 +# Deprecate? class TipableVMobject(VMobject): """ Meant for shared functionality between Arc and Line. @@ -404,32 +407,38 @@ class Line(TipableVMobject): self.set_points_by_ends(self.start, self.end, self.buff, self.path_arc) def set_points_by_ends(self, start, end, buff=0, path_arc=0): - if path_arc: - self.set_points(Arc.create_quadratic_bezier_points(path_arc)) - self.put_start_and_end_on(start, end) - else: + vect = end - start + dist = get_norm(vect) + if np.isclose(dist, 0): self.set_points_as_corners([start, end]) - self.account_for_buff(self.buff) + return self + if path_arc: + neg = path_arc < 0 + if neg: + path_arc = -path_arc + start, end = end, start + radius = (dist / 2) / math.sin(path_arc / 2) + alpha = (PI - path_arc) / 2 + center = start + radius * normalize(rotate_vector(end - start, alpha)) + + raw_arc_points = Arc.create_quadratic_bezier_points( + angle=path_arc - 2 * buff / radius, + start_angle=angle_of_vector(start - center) + buff / radius, + ) + if neg: + raw_arc_points = raw_arc_points[::-1] + self.set_points(center + radius * raw_arc_points) + else: + if buff > 0 and dist > 0: + start = start + vect * (buff / dist) + end = end - vect * (buff / dist) + self.set_points_as_corners([start, end]) + return self def set_path_arc(self, new_value): self.path_arc = new_value self.init_points() - def account_for_buff(self, buff): - if buff == 0: - return - # - if self.path_arc == 0: - length = self.get_length() - else: - length = self.get_arc_length() - # - if length < 2 * buff: - return - buff_prop = buff / length - self.pointwise_become_partial(self, buff_prop, 1 - buff_prop) - return self - def set_start_and_end_attrs(self, start, end): # If either start or end are Mobjects, this # gives their centers @@ -439,8 +448,8 @@ class Line(TipableVMobject): # Now that we know the direction between them, # we can find the appropriate boundary point from # start and end, if they're mobjects - self.start = self.pointify(start, vect) + self.buff * vect - self.end = self.pointify(end, -vect) - self.buff * vect + self.start = self.pointify(start, vect) + self.end = self.pointify(end, -vect) def pointify(self, mob_or_point, direction=None): """ @@ -461,8 +470,10 @@ class Line(TipableVMobject): def put_start_and_end_on(self, start, end): curr_start, curr_end = self.get_start_and_end() - if (curr_start == curr_end).all(): - self.set_points_by_ends(start, end, self.path_arc) + if np.isclose(curr_start, curr_end).all(): + # Handle null lines more gracefully + self.set_points_by_ends(start, end, buff=0, path_arc=self.path_arc) + return self return super().put_start_and_end_on(start, end) def get_vector(self): @@ -494,8 +505,8 @@ class Line(TipableVMobject): ) return self - def set_length(self, length): - self.scale(length / self.get_length()) + def set_length(self, length, **kwargs): + self.scale(length / self.get_length(), **kwargs) class DashedLine(Line): @@ -578,6 +589,80 @@ class Elbow(VMobject): class Arrow(Line): + CONFIG = { + "stroke_color": GREY_A, + "stroke_width": 5, + "tip_width_ratio": 4, + "width_to_tip_len": 0.0075, + "max_tip_length_to_length_ratio": 0.3, + "max_width_to_length_ratio": 10, + "buff": 0.25, + } + + def set_points_by_ends(self, start, end, buff=0, path_arc=0): + super().set_points_by_ends(start, end, buff, path_arc) + self.insert_tip_anchor() + return self + + def init_colors(self): + super().init_colors() + self.create_tip_with_stroke_width() + + def get_arc_length(self): + # Push up into Line? + arc_len = get_norm(self.get_vector()) + if self.path_arc > 0: + arc_len *= self.path_arc / (2 * math.sin(self.path_arc / 2)) + return arc_len + + def insert_tip_anchor(self): + prev_end = self.get_end() + arc_len = self.get_arc_length() + tip_len = self.get_stroke_width() * self.width_to_tip_len * self.tip_width_ratio + if tip_len >= self.max_tip_length_to_length_ratio * arc_len: + alpha = self.max_tip_length_to_length_ratio + else: + alpha = tip_len / arc_len + self.pointwise_become_partial(self, 0, 1 - alpha) + self.add_line_to(prev_end) + return self + + def create_tip_with_stroke_width(self): + width = min( + self.max_stroke_width, + self.max_width_to_length_ratio * self.get_length(), + ) + widths_array = np.full(self.get_num_points(), width) + nppc = self.n_points_per_curve + if len(widths_array) > nppc: + widths_array[-nppc:] = [ + a * self.tip_width_ratio * width + for a in np.linspace(1, 0, nppc) + ] + self.set_stroke(width=widths_array) + return self + + def reset_tip(self): + self.set_points_by_ends( + self.get_start(), + self.get_end(), + path_arc=self.path_arc, + ) + self.create_tip_with_stroke_width() + return self + + def set_stroke(self, color=None, width=None, *args, **kwargs): + super().set_stroke(color=color, width=width, *args, **kwargs) + if isinstance(width, numbers.Number): + self.max_stroke_width = width + self.reset_tip() + return self + + def _handle_scale_side_effects(self, scale_factor): + return self.reset_tip() + + +class FillArrow(Line): CONFIG = { "fill_color": GREY_A, "fill_opacity": 1, diff --git a/manimlib/mobject/matrix.py b/manimlib/mobject/matrix.py index 4800aa01..3c30fff0 100644 --- a/manimlib/mobject/matrix.py +++ b/manimlib/mobject/matrix.py @@ -68,7 +68,7 @@ class Matrix(VMobject): def __init__(self, matrix, **kwargs): """ - Matrix can either either include numbres, tex_strings, + Matrix can either include numbers, tex_strings, or mobjects """ VMobject.__init__(self, **kwargs) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 79b489de..056db669 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -365,14 +365,17 @@ class Mobject(object): self.center() return self + def replicate(self, n): + return self.get_group_class()( + *(self.copy() for x in range(n)) + ) + def get_grid(self, n_rows, n_cols, height=None, **kwargs): """ Returns a new mobject containing multiple copies of this one arranged in a grid """ - grid = self.get_group_class()( - *(self.copy() for n in range(n_rows * n_cols)) - ) + grid = self.replicate(n_rows * n_cols) grid.arrange_in_grid(n_rows, n_cols, **kwargs) if height is not None: grid.set_height(height) @@ -408,8 +411,10 @@ class Mobject(object): for key in self.data: copy_mobject.data[key] = self.data[key].copy() - # TODO, are uniforms ever numpy arrays? copy_mobject.uniforms = dict(self.uniforms) + for key in self.uniforms: + if isinstance(self.uniforms[key], np.ndarray): + copy_mobject.uniforms[key] = self.uniforms[key].copy() copy_mobject.submobjects = [] copy_mobject.add(*[sm.copy() for sm in self.submobjects]) @@ -572,14 +577,14 @@ class Mobject(object): respect to that point. """ scale_factor = max(scale_factor, min_scale_factor) - for mob in self.get_family(): - mob._handle_scale_side_effects(scale_factor) self.apply_points_function( lambda points: scale_factor * points, about_point=about_point, about_edge=about_edge, works_on_bounding_box=True, ) + for mob in self.get_family(): + mob._handle_scale_side_effects(scale_factor) return self def _handle_scale_side_effects(self, scale_factor): @@ -773,6 +778,21 @@ class Mobject(object): def set_depth(self, depth, stretch=False, **kwargs): return self.rescale_to_fit(depth, 2, stretch=stretch, **kwargs) + def set_max_width(self, max_width, **kwargs): + if self.get_width() > max_width: + self.set_width(max_width, **kwargs) + return self + + def set_max_height(self, max_height, **kwargs): + if self.get_height() > max_height: + self.set_height(max_height, **kwargs) + return self + + def set_max_depth(self, max_depth, **kwargs): + if self.get_depth() > max_depth: + self.set_depth(max_depth, **kwargs) + return self + def set_coord(self, value, dim, direction=ORIGIN): curr = self.get_coord(dim, direction) shift_vect = np.zeros(self.dim) @@ -844,8 +864,8 @@ class Mobject(object): angle_of_vector(target_vect) - angle_of_vector(curr_vect), ) self.rotate( - np.arctan2(curr_vect[2], get_norm(curr_vect[:2])) - np.arctan2(target_vect[2], get_norm(target_vect[:2])), - axis = np.array([-target_vect[1], target_vect[0], 0]), + np.arctan2(curr_vect[2], get_norm(curr_vect[:2])) - np.arctan2(target_vect[2], get_norm(target_vect[:2])), + axis=np.array([-target_vect[1], target_vect[0], 0]), ) self.shift(start - self.get_start()) return self @@ -1073,14 +1093,16 @@ class Mobject(object): def get_start(self): self.throw_error_if_no_points() - return np.array(self.get_points()[0]) + return self.get_points()[0].copy() def get_end(self): self.throw_error_if_no_points() - return np.array(self.get_points()[-1]) + return self.get_points()[-1].copy() def get_start_and_end(self): - return self.get_start(), self.get_end() + self.throw_error_if_no_points() + points = self.get_points() + return (points[0].copy(), points[-1].copy()) def point_from_proportion(self, alpha): points = self.get_points() diff --git a/manimlib/mobject/number_line.py b/manimlib/mobject/number_line.py index 0346399c..cb9a04fa 100644 --- a/manimlib/mobject/number_line.py +++ b/manimlib/mobject/number_line.py @@ -6,7 +6,6 @@ from manimlib.utils.bezier import interpolate from manimlib.utils.config_ops import digest_config from manimlib.utils.config_ops import merge_dicts_recursively from manimlib.utils.simple_functions import fdiv -from manimlib.utils.space_ops import normalize class NumberLine(Line): @@ -83,7 +82,7 @@ class NumberLine(Line): ticks = VGroup() for x in self.get_tick_range(): size = self.tick_size - if x in self.numbers_with_elongated_ticks: + if np.isclose(self.numbers_with_elongated_ticks, x).any(): size *= self.longer_tick_multiple ticks.add(self.get_tick(x, size)) self.add(ticks) @@ -106,11 +105,13 @@ class NumberLine(Line): return interpolate(self.get_start(), self.get_end(), alpha) def point_to_number(self, point): - start, end = self.get_start_and_end() - unit_vect = normalize(end - start) + points = self.get_points() + start = points[0] + end = points[-1] + vect = end - start proportion = fdiv( - np.dot(point - start, unit_vect), - np.dot(end - start, unit_vect), + np.dot(point - start, vect), + np.dot(end - start, vect), ) return interpolate(self.x_min, self.x_max, proportion) diff --git a/manimlib/mobject/numbers.py b/manimlib/mobject/numbers.py index a1949f51..7a8d05ba 100644 --- a/manimlib/mobject/numbers.py +++ b/manimlib/mobject/numbers.py @@ -36,7 +36,9 @@ class DecimalNumber(VMobject): # Add non-numerical bits if self.show_ellipsis: - self.add(self.string_to_mob("...")) + dots = self.string_to_mob("...") + dots.arrange(RIGHT, buff=2 * dots[0].get_width()) + self.add(dots) if self.unit is not None: self.unit_sign = self.string_to_mob(self.unit, SingleStringTex) self.add(self.unit_sign) @@ -128,7 +130,7 @@ class DecimalNumber(VMobject): def set_value(self, number): move_to_point = self.get_edge_center(self.edge_to_fix) - old_submobjects = self.submobjects + old_submobjects = list(self.submobjects) self.set_submobjects_from_number(number) self.move_to(move_to_point, self.edge_to_fix) for sm1, sm2 in zip(self.submobjects, old_submobjects): diff --git a/manimlib/mobject/svg/svg_mobject.py b/manimlib/mobject/svg/svg_mobject.py index 2fd3ce2f..ba1b314b 100644 --- a/manimlib/mobject/svg/svg_mobject.py +++ b/manimlib/mobject/svg/svg_mobject.py @@ -184,8 +184,9 @@ class SVGMobject(VMobject): corner_radius = rect_element.getAttribute("rx") # input preprocessing + fill_opacity = 1 if fill_color in ["", "none", "#FFF", "#FFFFFF"] or Color(fill_color) == Color(WHITE): - opacity = 0 + fill_opacity = 0 fill_color = BLACK # shdn't be necessary but avoids error msgs if fill_color in ["#000", "#000000"]: fill_color = WHITE @@ -213,7 +214,7 @@ class SVGMobject(VMobject): stroke_width=stroke_width, stroke_color=stroke_color, fill_color=fill_color, - fill_opacity=opacity + fill_opacity=fill_opacity ) else: mob = RoundedRectangle( @@ -321,7 +322,7 @@ class SVGMobject(VMobject): class VMobjectFromSVGPathstring(VMobject): CONFIG = { - "long_lines": True, + "long_lines": False, "should_subdivide_sharp_curves": False, "should_remove_null_curves": False, } diff --git a/manimlib/mobject/svg/tex_mobject.py b/manimlib/mobject/svg/tex_mobject.py index 0680645f..08c8665e 100644 --- a/manimlib/mobject/svg/tex_mobject.py +++ b/manimlib/mobject/svg/tex_mobject.py @@ -1,7 +1,6 @@ from functools import reduce import operator as op import re -import itertools as it from manimlib.constants import * from manimlib.mobject.geometry import Line diff --git a/manimlib/mobject/svg/text_mobject.py b/manimlib/mobject/svg/text_mobject.py index ad923e85..34f0ed15 100644 --- a/manimlib/mobject/svg/text_mobject.py +++ b/manimlib/mobject/svg/text_mobject.py @@ -25,7 +25,7 @@ from manimlib.utils.directories import get_downloads_dir, get_text_dir from manimpango import PangoUtils, TextSetting, MarkupUtils TEXT_MOB_SCALE_FACTOR = 0.0076 -DEFAULT_LINE_SPACING_SCALE = 0.3 +DEFAULT_LINE_SPACING_SCALE = 0.6 class Text(SVGMobject): diff --git a/manimlib/mobject/types/dot_cloud.py b/manimlib/mobject/types/dot_cloud.py index 7557698b..de5ce517 100644 --- a/manimlib/mobject/types/dot_cloud.py +++ b/manimlib/mobject/types/dot_cloud.py @@ -68,7 +68,9 @@ class DotCloud(PMobject): return self def set_radii(self, radii): - self.data["radii"][:] = resize_preserving_order(radii, len(self.data["radii"])) + n_points = len(self.get_points()) + radii = np.array(radii).reshape((len(radii), 1)) + self.data["radii"] = resize_preserving_order(radii, n_points) self.refresh_bounding_box() return self diff --git a/manimlib/mobject/types/image_mobject.py b/manimlib/mobject/types/image_mobject.py index bada35e3..334b389d 100644 --- a/manimlib/mobject/types/image_mobject.py +++ b/manimlib/mobject/types/image_mobject.py @@ -22,10 +22,13 @@ class ImageMobject(Mobject): } def __init__(self, filename, **kwargs): - path = get_full_raster_image_path(filename) + self.set_image_path(get_full_raster_image_path(filename)) + super().__init__(**kwargs) + + def set_image_path(self, path): + self.path = path self.image = Image.open(path) self.texture_paths = {"Texture": path} - super().__init__(**kwargs) def init_data(self): self.data = { diff --git a/manimlib/mobject/types/point_cloud_mobject.py b/manimlib/mobject/types/point_cloud_mobject.py index e2ef6461..28ccee7e 100644 --- a/manimlib/mobject/types/point_cloud_mobject.py +++ b/manimlib/mobject/types/point_cloud_mobject.py @@ -14,10 +14,17 @@ class PMobject(Mobject): def resize_points(self, size, resize_func=resize_array): # TODO for key in self.data: + if key == "bounding_box": + continue if len(self.data[key]) != size: self.data[key] = resize_array(self.data[key], size) return self + def set_points(self, points): + super().set_points(points) + self.resize_points(len(points)) + return self + def add_points(self, points, rgbas=None, color=None, opacity=None): """ points must be a Nx3 numpy array, as must rgbas if it is not None @@ -54,6 +61,8 @@ class PMobject(Mobject): for mob in self.family_members_with_points(): to_keep = ~np.apply_along_axis(condition, 1, mob.get_points()) for key in mob.data: + if key == "bounding_box": + continue mob.data[key] = mob.data[key][to_keep] return self @@ -85,7 +94,9 @@ class PMobject(Mobject): lower_index = int(a * pmobject.get_num_points()) upper_index = int(b * pmobject.get_num_points()) for key in self.data: - self.data[key] = pmobject.data[key][lower_index:upper_index] + if key == "bounding_box": + continue + self.data[key] = pmobject.data[key][lower_index:upper_index].copy() return self diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 98f8458d..69bfd0b9 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -127,7 +127,7 @@ class VMobject(Mobject): if isinstance(width, np.ndarray): arr = width.reshape((len(width), 1)) else: - arr = np.array([[w] for w in listify(width)]) + arr = np.array([[w] for w in listify(width)], dtype=float) mob.data['stroke_width'] = arr if background is not None: @@ -149,6 +149,7 @@ class VMobject(Mobject): stroke_opacity=None, stroke_rgba=None, stroke_width=None, + stroke_background=True, gloss=None, shadow=None, recurse=True): @@ -163,13 +164,17 @@ class VMobject(Mobject): if stroke_rgba is not None: self.data['stroke_rgba'] = resize_with_interpolation(stroke_rgba, len(fill_rgba)) - self.set_stroke(width=stroke_width) + self.set_stroke( + width=stroke_width, + background=stroke_background, + ) else: self.set_stroke( color=stroke_color, width=stroke_width, opacity=stroke_opacity, recurse=recurse, + background=stroke_background, ) if gloss is not None: @@ -183,6 +188,7 @@ class VMobject(Mobject): "fill_rgba": self.data['fill_rgba'], "stroke_rgba": self.data['stroke_rgba'], "stroke_width": self.data['stroke_width'], + "stroke_background": self.draw_stroke_behind_fill, "gloss": self.get_gloss(), "shadow": self.get_shadow(), } @@ -423,7 +429,10 @@ class VMobject(Mobject): def set_points_smoothly(self, points, true_smooth=False): self.set_points_as_corners(points) - self.make_smooth() + if true_smooth: + self.make_smooth() + else: + self.make_approximately_smooth() return self def change_anchor_mode(self, mode): diff --git a/manimlib/mobject/value_tracker.py b/manimlib/mobject/value_tracker.py index d7d3421e..2232f6c7 100644 --- a/manimlib/mobject/value_tracker.py +++ b/manimlib/mobject/value_tracker.py @@ -28,7 +28,10 @@ class ValueTracker(Mobject): ) def get_value(self): - return self.data["value"][0, :] + result = self.data["value"][0, :] + if len(result) == 1: + return result[0] + return result def set_value(self, value): self.data["value"][0, :] = value diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index d5f318f2..21f05241 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -45,6 +45,7 @@ class Scene(object): from manimlib.window import Window self.window = Window(scene=self, **self.window_config) self.camera_config["ctx"] = self.window.ctx + self.camera_config["frame_rate"] = 30 # Where's that 30 from? else: self.window = None @@ -105,7 +106,7 @@ and the mouse to interact with the scene. Just press `q` if you want to quit.") self.quit_interaction = False self.lock_static_mobject_data() while not (self.window.is_closing or self.quit_interaction): - self.update_frame() + self.update_frame(1 / self.camera.frame_rate) if self.window.is_closing: self.window.destroy() if self.quit_interaction: @@ -120,6 +121,9 @@ and the mouse to interact with the scene. Just press `q` if you want to quit.") self.linger_after_completion = False self.update_frame() + # Save scene state at the point of embedding + self.save_state() + from IPython.terminal.embed import InteractiveShellEmbed shell = InteractiveShellEmbed() # Have the frame update after each command @@ -251,6 +255,18 @@ the window directly. To do so, you need to type `touch()` or `self.interact()`") def get_mobject_copies(self): return [m.copy() for m in self.mobjects] + def point_to_mobject(self, point, search_set=None, buff=0): + """ + E.g. if clicking on the scene, this returns the top layer mobject + under a given point + """ + if search_set is None: + search_set = self.mobjects + for mobject in reversed(search_set): + if mobject.is_point_touching(point, buff=buff): + return mobject + return None + # Related to skipping def update_skipping_status(self): if self.start_at_animation_number is not None: diff --git a/manimlib/shaders/inserts/add_light.glsl b/manimlib/shaders/inserts/add_light.glsl deleted file mode 100644 index 8ef7cc11..00000000 --- a/manimlib/shaders/inserts/add_light.glsl +++ /dev/null @@ -1,43 +0,0 @@ -///// INSERT COLOR_MAP FUNCTION HERE ///// - -vec4 add_light(vec4 color, - vec3 point, - vec3 unit_normal, - vec3 light_coords, - float gloss, - float shadow){ - ///// INSERT COLOR FUNCTION HERE ///// - // The line above may be replaced by arbitrary code snippets, as per - // the method Mobject.set_color_by_code - if(gloss == 0.0 && shadow == 0.0) return color; - - // TODO, do we actually want this? It effectively treats surfaces as two-sided - if(unit_normal.z < 0){ - unit_normal *= -1; - } - - // TODO, read this in as a uniform? - float camera_distance = 6; - // Assume everything has already been rotated such that camera is in the z-direction - vec3 to_camera = vec3(0, 0, camera_distance) - point; - vec3 to_light = light_coords - point; - vec3 light_reflection = -to_light + 2 * unit_normal * dot(to_light, unit_normal); - float dot_prod = dot(normalize(light_reflection), normalize(to_camera)); - float shine = gloss * exp(-3 * pow(1 - dot_prod, 2)); - float dp2 = dot(normalize(to_light), unit_normal); - float darkening = mix(1, max(dp2, 0), shadow); - return vec4( - darkening * mix(color.rgb, vec3(1.0), shine), - color.a - ); -} - -vec4 finalize_color(vec4 color, - vec3 point, - vec3 unit_normal, - vec3 light_coords, - float gloss, - float shadow){ - // Put insertion here instead - return add_light(color, point, unit_normal, light_coords, gloss, shadow); -} \ No newline at end of file diff --git a/manimlib/shaders/inserts/complex_functions.glsl b/manimlib/shaders/inserts/complex_functions.glsl new file mode 100644 index 00000000..0d9a0ec1 --- /dev/null +++ b/manimlib/shaders/inserts/complex_functions.glsl @@ -0,0 +1,15 @@ +vec2 complex_mult(vec2 z, vec2 w){ + return vec2(z.x * w.x - z.y * w.y, z.x * w.y + z.y * w.x); +} + +vec2 complex_div(vec2 z, vec2 w){ + return complex_mult(z, vec2(w.x, -w.y)) / (w.x * w.x + w.y * w.y); +} + +vec2 complex_pow(vec2 z, int n){ + vec2 result = vec2(1.0, 0.0); + for(int i = 0; i < n; i++){ + result = complex_mult(result, z); + } + return result; +} \ No newline at end of file diff --git a/manimlib/shaders/mandelbrot_fractal/frag.glsl b/manimlib/shaders/mandelbrot_fractal/frag.glsl new file mode 100644 index 00000000..7d7e593f --- /dev/null +++ b/manimlib/shaders/mandelbrot_fractal/frag.glsl @@ -0,0 +1,77 @@ +#version 330 + +uniform vec3 light_source_position; +uniform float gloss; +uniform float shadow; +uniform float focal_distance; + +uniform vec2 parameter; +uniform float opacity; +uniform float n_steps; +uniform float mandelbrot; + +uniform vec3 color0; +uniform vec3 color1; +uniform vec3 color2; +uniform vec3 color3; +uniform vec3 color4; +uniform vec3 color5; +uniform vec3 color6; +uniform vec3 color7; +uniform vec3 color8; + +uniform vec2 frame_shape; + +in vec3 xyz_coords; + +out vec4 frag_color; + +#INSERT finalize_color.glsl +#INSERT complex_functions.glsl + +const int MAX_DEGREE = 5; + +void main() { + vec3 color_map[9] = vec3[9]( + color0, color1, color2, color3, + color4, color5, color6, color7, color8 + ); + vec3 color; + + vec2 z; + vec2 c; + + if(bool(mandelbrot)){ + c = xyz_coords.xy; + z = vec2(0.0, 0.0); + }else{ + c = parameter; + z = xyz_coords.xy; + } + + float outer_bound = 2.0; + bool stable = true; + for(int n = 0; n < int(n_steps); n++){ + z = complex_mult(z, z) + c; + if(length(z) > outer_bound){ + float float_n = float(n); + float_n += log(outer_bound) / log(length(z)); + float_n += 0.5 * length(c); + color = float_to_color(sqrt(float_n), 1.5, 8.0, color_map); + stable = false; + break; + } + } + if(stable){ + color = vec3(0.0, 0.0, 0.0); + } + + frag_color = finalize_color( + vec4(color, opacity), + xyz_coords, + vec3(0.0, 0.0, 1.0), + light_source_position, + gloss, + shadow + ); + } \ No newline at end of file diff --git a/manimlib/shaders/mandelbrot_fractal/vert.glsl b/manimlib/shaders/mandelbrot_fractal/vert.glsl new file mode 100644 index 00000000..dbbb87f2 --- /dev/null +++ b/manimlib/shaders/mandelbrot_fractal/vert.glsl @@ -0,0 +1,17 @@ +#version 330 + +#INSERT camera_uniform_declarations.glsl + +in vec3 point; +out vec3 xyz_coords; + +uniform float scale_factor; +uniform vec3 offset; + +#INSERT position_point_into_frame.glsl +#INSERT get_gl_Position.glsl + +void main(){ + xyz_coords = (point - offset) / scale_factor; + gl_Position = get_gl_Position(position_point_into_frame(point)); +} \ No newline at end of file diff --git a/manimlib/shaders/newton_fractal/frag.glsl b/manimlib/shaders/newton_fractal/frag.glsl new file mode 100644 index 00000000..8315f9ec --- /dev/null +++ b/manimlib/shaders/newton_fractal/frag.glsl @@ -0,0 +1,157 @@ +#version 330 + +uniform vec3 light_source_position; +uniform float gloss; +uniform float shadow; +uniform float focal_distance; + +uniform vec4 color0; +uniform vec4 color1; +uniform vec4 color2; +uniform vec4 color3; +uniform vec4 color4; + +uniform vec2 coef0; +uniform vec2 coef1; +uniform vec2 coef2; +uniform vec2 coef3; +uniform vec2 coef4; +uniform vec2 coef5; + +uniform vec2 root0; +uniform vec2 root1; +uniform vec2 root2; +uniform vec2 root3; +uniform vec2 root4; + +uniform float n_roots; +uniform float n_steps; +uniform float julia_highlight; +uniform float saturation_factor; +uniform float black_for_cycles; +uniform float is_parameter_space; + +uniform vec2 frame_shape; + +in vec3 xyz_coords; + +out vec4 frag_color; + +#INSERT finalize_color.glsl +#INSERT complex_functions.glsl + +const int MAX_DEGREE = 5; +const float CLOSE_ENOUGH = 1e-3; + + +vec2 poly(vec2 z, vec2[MAX_DEGREE + 1] coefs){ + vec2 result = vec2(0.0); + for(int n = 0; n < int(n_roots) + 1; n++){ + result += complex_mult(coefs[n], complex_pow(z, n)); + } + return result; +} + +vec2 dpoly(vec2 z, vec2[MAX_DEGREE + 1] coefs){ + vec2 result = vec2(0.0); + for(int n = 1; n < int(n_roots) + 1; n++){ + result += n * complex_mult(coefs[n], complex_pow(z, n - 1)); + } + return result; +} + +vec2 seek_root(vec2 z, vec2[MAX_DEGREE + 1] coefs, int max_steps, out float n_iters){ + float last_len; + float curr_len; + float threshold = CLOSE_ENOUGH; + + for(int i = 0; i < max_steps; i++){ + last_len = curr_len; + n_iters = float(i); + vec2 step = complex_div(poly(z, coefs), dpoly(z, coefs)); + curr_len = length(step); + if(curr_len < threshold){ + break; + } + z = z - step; + } + n_iters -= clamp((threshold - curr_len) / (last_len - curr_len), 0.0, 1.0); + + return z; +} + + +void main() { + vec2[MAX_DEGREE + 1] coefs = vec2[MAX_DEGREE + 1](coef0, coef1, coef2, coef3, coef4, coef5); + vec2[MAX_DEGREE] roots = vec2[MAX_DEGREE](root0, root1, root2, root3, root4); + vec4[MAX_DEGREE] colors = vec4[MAX_DEGREE](color0, color1, color2, color3, color4); + + vec2 z = xyz_coords.xy; + + if(is_parameter_space > 0){ + // In this case, pixel should correspond to one of the roots + roots[2] = xyz_coords.xy; + vec2 r0 = roots[0]; + vec2 r1 = roots[1]; + vec2 r2 = roots[2]; + + // It is assumed that the polynomial is cubid... + coefs[0] = -complex_mult(complex_mult(r0, r1), r2); + coefs[1] = complex_mult(r0, r1) + complex_mult(r0, r2) + complex_mult(r1, r2); + coefs[2] = -(r0 + r1 + r2); + coefs[3] = vec2(1.0, 0.0); + + // Seed value is always center of the roots + z = -coefs[2] / 3.0; + } + + float n_iters; + vec2 found_root = seek_root(z, coefs, int(n_steps), n_iters); + + vec4 color = vec4(0.0); + float min_dist = 1e10; + float dist; + for(int i = 0; i < int(n_roots); i++){ + dist = distance(roots[i], found_root); + if(dist < min_dist){ + min_dist = dist; + color = colors[i]; + } + } + color *= 1.0 + (0.01 * saturation_factor) * (n_iters - 5 * saturation_factor); + + if(black_for_cycles > 0 && min_dist > CLOSE_ENOUGH){ + color = vec4(0.0, 0.0, 0.0, 1.0); + } + + if(julia_highlight > 0.0){ + float radius = julia_highlight; + vec2[4] samples = vec2[4]( + z + vec2(radius, 0.0), + z + vec2(-radius, 0.0), + z + vec2(0.0, radius), + z + vec2(0.0, -radius) + ); + for(int i = 0; i < 4; i++){ + for(int j = 0; j < n_steps; j++){ + vec2 z = samples[i]; + z = z - complex_div(poly(z, coefs), dpoly(z, coefs)); + samples[i] = z; + } + } + float max_dist = 0.0; + for(int i = 0; i < 4; i++){ + max_dist = max(max_dist, distance(samples[i], samples[(i + 1) % 4])); + } + color *= 1.0 * smoothstep(0, 0.1, max_dist); + } + + frag_color = finalize_color( + color, + xyz_coords, + vec3(0.0, 0.0, 1.0), + light_source_position, + gloss, + shadow + ); + } \ No newline at end of file diff --git a/manimlib/shaders/newton_fractal/vert.glsl b/manimlib/shaders/newton_fractal/vert.glsl new file mode 100644 index 00000000..dbbb87f2 --- /dev/null +++ b/manimlib/shaders/newton_fractal/vert.glsl @@ -0,0 +1,17 @@ +#version 330 + +#INSERT camera_uniform_declarations.glsl + +in vec3 point; +out vec3 xyz_coords; + +uniform float scale_factor; +uniform vec3 offset; + +#INSERT position_point_into_frame.glsl +#INSERT get_gl_Position.glsl + +void main(){ + xyz_coords = (point - offset) / scale_factor; + gl_Position = get_gl_Position(position_point_into_frame(point)); +} \ No newline at end of file diff --git a/manimlib/utils/bezier.py b/manimlib/utils/bezier.py index c0decf59..f5317584 100644 --- a/manimlib/utils/bezier.py +++ b/manimlib/utils/bezier.py @@ -4,6 +4,7 @@ import numpy as np from manimlib.utils.simple_functions import choose from manimlib.utils.space_ops import find_intersection from manimlib.utils.space_ops import cross2d +from manimlib.utils.space_ops import midpoint from manimlib.logger import log CLOSED_THRESHOLD = 0.001 @@ -131,6 +132,8 @@ def get_smooth_quadratic_bezier_handle_points(points): another that would produce a parabola passing through P0, call it smooth_to_left, and use the midpoint between the two. """ + if len(points) == 2: + return midpoint(*points) smooth_to_right, smooth_to_left = [ 0.25 * ps[0:-2] + ps[1:-1] - 0.25 * ps[2:] for ps in (points, points[::-1]) diff --git a/manimlib/utils/space_ops.py b/manimlib/utils/space_ops.py index 74271642..0e8c55e3 100644 --- a/manimlib/utils/space_ops.py +++ b/manimlib/utils/space_ops.py @@ -1,5 +1,4 @@ import numpy as np -import itertools as it import operator as op from functools import reduce import math @@ -14,7 +13,7 @@ from manimlib.utils.iterables import adjacent_pairs def get_norm(vect): - return sum([x**2 for x in vect])**0.5 + return sum((x**2 for x in vect))**0.5 # Quaternions diff --git a/manimlib/window.py b/manimlib/window.py index ed3a6fb5..63d0ffd1 100644 --- a/manimlib/window.py +++ b/manimlib/window.py @@ -1,3 +1,4 @@ +import numpy as np import moderngl_window as mglw from moderngl_window.context.pyglet.window import Window as PygletWindow from moderngl_window.timers.clock import Timer @@ -59,7 +60,17 @@ class Window(PygletWindow): # Delegate event handling to scene def pixel_coords_to_space_coords(self, px, py, relative=False): - return self.scene.camera.pixel_coords_to_space_coords(px, py, relative) + pw, ph = self.size + fw, fh = self.scene.camera.get_frame_shape() + fc = self.scene.camera.get_frame_center() + if relative: + return np.array([px / pw, py / ph, 0]) + else: + return np.array([ + fc[0] + px * fw / pw - fw / 2, + fc[1] + py * fh / ph - fh / 2, + 0 + ]) def on_mouse_motion(self, x, y, dx, dy): super().on_mouse_motion(x, y, dx, dy)