diff --git a/.gitignore b/.gitignore index 6bbcdb2e..71be35c4 100644 --- a/.gitignore +++ b/.gitignore @@ -91,6 +91,7 @@ ipython_config.py # pyenv .python-version +pyrightconfig.json # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. diff --git a/manimlib/animation/animation.py b/manimlib/animation/animation.py index a8ece9ca..115e9839 100644 --- a/manimlib/animation/animation.py +++ b/manimlib/animation/animation.py @@ -74,6 +74,7 @@ class Animation(object): # It is, however, okay and desirable to call # the internal updaters of self.starting_mobject, # or any others among self.get_all_mobjects() + self.mobject_was_updating = not self.mobject.updating_suspended self.mobject.suspend_updating() self.families = list(self.get_all_families_zipped()) self.interpolate(0) @@ -81,7 +82,7 @@ class Animation(object): def finish(self) -> None: self.interpolate(self.final_alpha_value) self.mobject.set_animating_status(False) - if self.suspend_mobject_updating: + if self.suspend_mobject_updating and self.mobject_was_updating: self.mobject.resume_updating() def clean_up_from_scene(self, scene: Scene) -> None: diff --git a/manimlib/animation/composition.py b/manimlib/animation/composition.py index e4ad5656..aadeac65 100644 --- a/manimlib/animation/composition.py +++ b/manimlib/animation/composition.py @@ -167,7 +167,6 @@ class LaggedStartMap(LaggedStart): self, anim_func: Callable[[Mobject], Animation], group: Mobject, - arg_creator: Callable[[Mobject], tuple] | None = None, run_time: float = 2.0, lag_ratio: float = DEFAULT_LAGGED_START_LAG_RATIO, **kwargs diff --git a/manimlib/animation/creation.py b/manimlib/animation/creation.py index ac73891d..d82b8a06 100644 --- a/manimlib/animation/creation.py +++ b/manimlib/animation/creation.py @@ -12,6 +12,7 @@ from manimlib.utils.bezier import integer_interpolate from manimlib.utils.rate_functions import linear from manimlib.utils.rate_functions import double_smooth from manimlib.utils.rate_functions import smooth +from manimlib.utils.simple_functions import clip from typing import TYPE_CHECKING @@ -138,6 +139,8 @@ class DrawBorderThenFill(Animation): submob.pointwise_become_partial(outline, 0, subalpha) else: submob.interpolate(outline, start, subalpha) + submob.note_changed_stroke() + submob.note_changed_fill() class Write(DrawBorderThenFill): @@ -189,6 +192,7 @@ class ShowIncreasingSubsets(Animation): def interpolate_mobject(self, alpha: float) -> None: n_submobs = len(self.all_submobs) + alpha = self.rate_func(alpha) index = int(self.int_func(alpha * n_submobs)) self.update_submobject_list(index) @@ -206,7 +210,7 @@ class ShowSubmobjectsOneByOne(ShowIncreasingSubsets): super().__init__(group, int_func=int_func, **kwargs) def update_submobject_list(self, index: int) -> None: - # N = len(self.all_submobs) + index = int(clip(index, 0, len(self.all_submobs) - 1)) if index == 0: self.mobject.set_submobjects([]) else: diff --git a/manimlib/animation/indication.py b/manimlib/animation/indication.py index cd01c359..6e95bb7c 100644 --- a/manimlib/animation/indication.py +++ b/manimlib/animation/indication.py @@ -258,6 +258,7 @@ class FlashAround(VShowPassingFlash): self, mobject: Mobject, time_width: float = 1.0, + taper_width: float = 0.0, stroke_width: float = 4.0, color: ManimColor = YELLOW, buff: float = SMALL_BUFF, @@ -270,7 +271,7 @@ class FlashAround(VShowPassingFlash): path.insert_n_curves(n_inserted_curves) path.set_points(path.get_points_without_null_curves()) path.set_stroke(color, stroke_width) - super().__init__(path, time_width=time_width, **kwargs) + super().__init__(path, time_width=time_width, taper_width=taper_width, **kwargs) def get_path(self, mobject: Mobject, buff: float) -> SurroundingRectangle: return SurroundingRectangle(mobject, buff=buff) @@ -278,7 +279,7 @@ class FlashAround(VShowPassingFlash): class FlashUnder(FlashAround): def get_path(self, mobject: Mobject, buff: float) -> Underline: - return Underline(mobject, buff=buff) + return Underline(mobject, buff=buff, stretch_factor=1.0) class ShowCreationThenDestruction(ShowPassingFlash): diff --git a/manimlib/animation/movement.py b/manimlib/animation/movement.py index e5671df2..c208aea9 100644 --- a/manimlib/animation/movement.py +++ b/manimlib/animation/movement.py @@ -11,6 +11,7 @@ if TYPE_CHECKING: import numpy as np from manimlib.mobject.mobject import Mobject + from manimlib.mobject.types.vectorized_mobject import VMobject class Homotopy(Animation): @@ -105,7 +106,7 @@ class MoveAlongPath(Animation): def __init__( self, mobject: Mobject, - path: Mobject, + path: VMobject, suspend_mobject_updating: bool = False, **kwargs ): @@ -113,5 +114,5 @@ class MoveAlongPath(Animation): super().__init__(mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs) def interpolate_mobject(self, alpha: float) -> None: - point = self.path.point_from_proportion(alpha) + point = self.path.quick_point_from_proportion(self.rate_func(alpha)) self.mobject.move_to(point) diff --git a/manimlib/mobject/coordinate_systems.py b/manimlib/mobject/coordinate_systems.py index 615eed7d..fcd11612 100644 --- a/manimlib/mobject/coordinate_systems.py +++ b/manimlib/mobject/coordinate_systems.py @@ -22,6 +22,7 @@ from manimlib.mobject.types.dot_cloud import DotCloud from manimlib.mobject.types.surface import ParametricSurface from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VMobject +from manimlib.utils.bezier import inverse_interpolate from manimlib.utils.dict_ops import merge_dicts_recursively from manimlib.utils.simple_functions import binary_search from manimlib.utils.space_ops import angle_of_vector @@ -174,6 +175,7 @@ class CoordinateSystem(ABC): self, function: Callable[[float], float], x_range: Sequence[float] | None = None, + bind: bool = False, **kwargs ) -> ParametricCurve: x_range = x_range or self.x_range @@ -194,6 +196,10 @@ class CoordinateSystem(ABC): ) graph.underlying_function = function graph.x_range = x_range + + if bind: + self.bind_graph_to_func(graph, function) + return graph def get_parametric_curve( @@ -239,7 +245,7 @@ class CoordinateSystem(ABC): def bind_graph_to_func( self, graph: VMobject, - func: Callable[[Vect3], Vect3], + func: Callable[[VectN], VectN], jagged: bool = False, get_discontinuities: Optional[Callable[[], Vect3]] = None ) -> VMobject: @@ -398,9 +404,24 @@ class CoordinateSystem(ABC): rect.set_fill(negative_color) return result - def get_area_under_graph(self, graph, x_range, fill_color=BLUE, fill_opacity=1): - # TODO - pass + def get_area_under_graph(self, graph, x_range, fill_color=BLUE, fill_opacity=0.5): + if not hasattr(graph, "x_range"): + raise Exception("Argument `graph` must have attribute `x_range`") + + alpha_bounds = [ + inverse_interpolate(*graph.x_range, x) + for x in x_range + ] + sub_graph = graph.copy() + sub_graph.pointwise_become_partial(graph, *alpha_bounds) + sub_graph.add_line_to(self.c2p(x_range[1], 0)) + sub_graph.add_line_to(self.c2p(x_range[0], 0)) + sub_graph.add_line_to(sub_graph.get_start()) + + sub_graph.set_stroke(width=0) + sub_graph.set_fill(fill_color, fill_opacity) + + return sub_graph class Axes(VGroup, CoordinateSystem): @@ -421,6 +442,7 @@ class Axes(VGroup, CoordinateSystem): **kwargs ): CoordinateSystem.__init__(self, x_range, y_range, **kwargs) + kwargs.pop("num_sampled_graph_points_per_tick", None) VGroup.__init__(self, **kwargs) axis_config = dict(**axis_config, unit_size=unit_size) @@ -507,7 +529,7 @@ class ThreeDAxes(Axes): z_range: RangeSpecifier = (-4.0, 4.0, 1.0), z_axis_config: dict = dict(), z_normal: Vect3 = DOWN, - depth: float = 6.0, + depth: float | None = None, flat_stroke: bool = False, **kwargs ): @@ -519,7 +541,7 @@ class ThreeDAxes(Axes): axis_config=merge_dicts_recursively( self.default_axis_config, self.default_z_axis_config, - kwargs.get("axes_config", {}), + kwargs.get("axis_config", {}), z_axis_config ), length=depth, @@ -549,20 +571,47 @@ class ThreeDAxes(Axes): axis.add(label) self.axis_labels = labels - def get_graph(self, func, color=BLUE_E, opacity=0.9, **kwargs): + def get_graph( + self, + func, + color=BLUE_E, + opacity=0.9, + u_range=None, + v_range=None, + **kwargs + ) -> ParametricSurface: xu = self.x_axis.get_unit_size() yu = self.y_axis.get_unit_size() zu = self.z_axis.get_unit_size() x0, y0, z0 = self.get_origin() + u_range = u_range or self.x_range[:2] + v_range = v_range or self.y_range[:2] return ParametricSurface( lambda u, v: [xu * u + x0, yu * v + y0, zu * func(u, v) + z0], - u_range=self.x_range[:2], - v_range=self.y_range[:2], + u_range=u_range, + v_range=v_range, color=color, opacity=opacity, **kwargs ) + def get_parametric_surface( + self, + func, + color=BLUE_E, + opacity=0.9, + **kwargs + ) -> ParametricSurface: + surface = ParametricSurface(func, color=color, opacity=opacity, **kwargs) + xu = self.x_axis.get_unit_size() + yu = self.y_axis.get_unit_size() + zu = self.z_axis.get_unit_size() + axes = [self.x_axis, self.y_axis, self.z_axis] + for dim, axis in zip(range(3), axes): + surface.stretch(axis.get_unit_size(), dim, about_point=ORIGIN) + surface.shift(self.get_origin()) + return surface + class NumberPlane(Axes): default_axis_config: dict = dict( diff --git a/manimlib/mobject/geometry.py b/manimlib/mobject/geometry.py index 57c7bec1..2f1a4730 100644 --- a/manimlib/mobject/geometry.py +++ b/manimlib/mobject/geometry.py @@ -7,12 +7,15 @@ import numpy as np from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, OUT, RIGHT, UL, UP, UR from manimlib.constants import GREY_A, RED, WHITE, BLACK -from manimlib.constants import MED_SMALL_BUFF +from manimlib.constants import MED_SMALL_BUFF, SMALL_BUFF from manimlib.constants import DEGREES, PI, TAU from manimlib.mobject.mobject import Mobject from manimlib.mobject.types.vectorized_mobject import DashedVMobject from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VMobject +from manimlib.utils.bezier import bezier +from manimlib.utils.bezier import quadratic_bezier_points_for_arc +from manimlib.utils.bezier import partial_quadratic_bezier_points from manimlib.utils.iterables import adjacent_n_tuples from manimlib.utils.iterables import adjacent_pairs from manimlib.utils.simple_functions import clip @@ -26,6 +29,7 @@ from manimlib.utils.space_ops import get_norm from manimlib.utils.space_ops import normalize from manimlib.utils.space_ops import rotate_vector from manimlib.utils.space_ops import rotation_matrix_transpose +from manimlib.utils.space_ops import rotation_about_z from typing import TYPE_CHECKING @@ -213,28 +217,11 @@ class Arc(TipableVMobject): ): super().__init__(**kwargs) - self.set_points(Arc.create_quadratic_bezier_points( - angle=angle, - start_angle=start_angle, - n_components=n_components - )) + self.set_points(quadratic_bezier_points_for_arc(angle, n_components)) + self.rotate(start_angle, about_point=ORIGIN) self.scale(radius, about_point=ORIGIN) self.shift(arc_center) - @staticmethod - def create_quadratic_bezier_points( - angle: float, - start_angle: float = 0, - n_components: int = 8 - ) -> Vect3Array: - n_points = 2 * n_components + 1 - angles = np.linspace(start_angle, start_angle + angle, n_points) - points = np.array([np.cos(angles), np.sin(angles), np.zeros(n_points)]).T - # Adjust handles - theta = angle / n_components - points[1::2] /= np.cos(theta / 2) - return points - def get_arc_center(self) -> Vect3: """ Looks at the normals to the first two @@ -448,8 +435,8 @@ class Annulus(VMobject): ) self.radius = outer_radius - outer_path = outer_radius * Arc.create_quadratic_bezier_points(TAU, 0) - inner_path = inner_radius * Arc.create_quadratic_bezier_points(-TAU, 0) + outer_path = outer_radius * quadratic_bezier_points_for_arc(TAU) + inner_path = inner_radius * quadratic_bezier_points_for_arc(-TAU) self.add_subpath(outer_path) self.add_subpath(inner_path) self.shift(center) @@ -466,6 +453,7 @@ class Line(TipableVMobject): ): super().__init__(**kwargs) self.path_arc = path_arc + self.buff = buff self.set_start_and_end_attrs(start, end) self.set_points_by_ends(self.start, self.end, buff, path_arc) @@ -476,32 +464,15 @@ class Line(TipableVMobject): buff: float = 0, path_arc: float = 0 ) -> Self: - vect = end - start - dist = get_norm(vect) - if np.isclose(dist, 0): - self.set_points_as_corners([start, end]) - 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)) + self.clear_points() + self.start_new_path(start) + self.add_arc_to(end, path_arc) - 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]) + # Apply buffer + if buff > 0: + length = self.get_arc_length() + alpha = min(buff / length, 0.5) + self.pointwise_become_partial(self, alpha, 1 - alpha) return self def set_path_arc(self, new_value: float) -> Self: @@ -673,15 +644,17 @@ class Arrow(Line): stroke_width: float = 5, buff: float = 0.25, tip_width_ratio: float = 5, - width_to_tip_len: float = 0.0075, + tip_len_to_width: float = 0.0075, max_tip_length_to_length_ratio: float = 0.3, max_width_to_length_ratio: float = 8.0, **kwargs, ): self.tip_width_ratio = tip_width_ratio - self.width_to_tip_len = width_to_tip_len + self.tip_len_to_width = tip_len_to_width self.max_tip_length_to_length_ratio = max_tip_length_to_length_ratio self.max_width_to_length_ratio = max_width_to_length_ratio + self.n_tip_points = 3 + self.original_stroke_width = stroke_width super().__init__( start, end, stroke_color=stroke_color, @@ -705,27 +678,32 @@ class Arrow(Line): def insert_tip_anchor(self) -> 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: + tip_len = self.get_stroke_width() * self.tip_width_ratio * self.tip_len_to_width + if tip_len >= self.max_tip_length_to_length_ratio * arc_len or arc_len == 0: alpha = self.max_tip_length_to_length_ratio else: alpha = tip_len / arc_len - self.pointwise_become_partial(self, 0, 1 - alpha) - # Dumb that this is needed - self.start_new_path(self.point_from_proportion(1 - 1e-5)) + + if self.path_arc > 0 and self.buff > 0: + self.insert_n_curves(10) # Is this needed? + self.pointwise_become_partial(self, 0.0, 1.0 - alpha) + self.add_line_to(self.get_end()) self.add_line_to(prev_end) + self.n_tip_points = 3 return self @Mobject.affects_data def create_tip_with_stroke_width(self) -> Self: if self.get_num_points() < 3: return self - tip_width = self.tip_width_ratio * min( - float(self.get_stroke_width()), + stroke_width = min( + self.original_stroke_width, self.max_width_to_length_ratio * self.get_length(), ) - self.data['stroke_width'][:-3] = self.data['stroke_width'][0] - self.data['stroke_width'][-3:, 0] = tip_width * np.linspace(1, 0, 3) + tip_width = self.tip_width_ratio * stroke_width + ntp = self.n_tip_points + self.data['stroke_width'][:-ntp] = self.data['stroke_width'][0] + self.data['stroke_width'][-ntp:, 0] = tip_width * np.linspace(1, 0, ntp) return self def reset_tip(self) -> Self: @@ -742,6 +720,7 @@ class Arrow(Line): *args, **kwargs ) -> Self: super().set_stroke(color=color, width=width, *args, **kwargs) + self.original_stroke_width = self.get_stroke_width() if self.has_points(): self.reset_tip() return self @@ -817,7 +796,7 @@ class FillArrow(Line): R = (-b + np.sqrt(b**2 - 4 * a * c)) / (2 * a) # Find arc points - points1 = Arc.create_quadratic_bezier_points(path_arc) + points1 = quadratic_bezier_points_for_arc(path_arc) points2 = np.array(points1[::-1]) points1 *= (R + thickness / 2) points2 *= (R - thickness / 2) @@ -1046,6 +1025,12 @@ class Rectangle(Polygon): self.set_width(width, stretch=True) self.set_height(height, stretch=True) + def surround(self, mobject, buff=SMALL_BUFF) -> Self: + target_shape = np.array(mobject.get_shape()) + 2 * buff + self.set_shape(*target_shape) + self.move_to(mobject) + return self + class Square(Rectangle): def __init__(self, side_length: float = 2.0, **kwargs): diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 9f4f2a18..bca93026 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -224,7 +224,7 @@ class Mobject(object): @affects_family_data def reverse_points(self) -> Self: for mob in self.get_family(): - mob.data = mob.data[::-1] + mob.data[:] = mob.data[::-1] return self @affects_family_data @@ -790,13 +790,13 @@ class Mobject(object): def update(self, dt: float = 0, recurse: bool = True) -> Self: if not self.has_updaters or self.updating_suspended: return self + if recurse: + for submob in self.submobjects: + submob.update(dt, recurse) for updater in self.time_based_updaters: updater(self, dt) for updater in self.non_time_updaters: updater(self) - if recurse: - for submob in self.submobjects: - submob.update(dt, recurse) return self def get_time_based_updaters(self) -> list[TimeBasedUpdater]: @@ -1334,7 +1334,7 @@ class Mobject(object): rgbs = resize_with_interpolation(rgbs, len(data)) data[name][:, :3] = rgbs if opacity is not None: - if isinstance(opacity, list): + if not isinstance(opacity, (float, int)): opacity = resize_with_interpolation(np.array(opacity), len(data)) data[name][:, 3] = opacity return self @@ -1540,6 +1540,9 @@ class Mobject(object): def get_depth(self) -> float: return self.length_over_dim(2) + def get_shape(self) -> Tuple[float]: + return tuple(self.length_over_dim(dim) for dim in range(3)) + def get_coord(self, dim: int, direction: Vect3 = ORIGIN) -> float: """ Meant to generalize get_x, get_y, get_z @@ -1598,6 +1601,12 @@ class Mobject(object): def match_color(self, mobject: Mobject) -> Self: return self.set_color(mobject.get_color()) + def match_style(self, mobject: Mobject) -> Self: + self.set_color(mobject.get_color()) + self.set_opacity(mobject.get_opacity()) + self.set_shading(*mobject.get_shading()) + return self + def match_dim_size(self, mobject: Mobject, dim: int, **kwargs) -> Self: return self.rescale_to_fit( mobject.length_over_dim(dim), dim, diff --git a/manimlib/mobject/number_line.py b/manimlib/mobject/number_line.py index 413c280a..64e8525f 100644 --- a/manimlib/mobject/number_line.py +++ b/manimlib/mobject/number_line.py @@ -94,7 +94,8 @@ class NumberLine(Line): x_max = self.x_max else: x_max = self.x_max + self.x_step - return np.arange(self.x_min, x_max, self.x_step) + result = np.arange(self.x_min, x_max, self.x_step) + return result[result <= self.x_max] def add_ticks(self) -> None: ticks = VGroup() diff --git a/manimlib/mobject/numbers.py b/manimlib/mobject/numbers.py index a5a24708..a4722fff 100644 --- a/manimlib/mobject/numbers.py +++ b/manimlib/mobject/numbers.py @@ -98,6 +98,8 @@ class DecimalNumber(VMobject): formatter = self.get_complex_formatter() else: formatter = self.get_formatter() + if self.num_decimal_places == 0 and isinstance(number, float): + number = int(number) num_string = formatter.format(number) rounded_num = np.round(number, self.num_decimal_places) @@ -149,7 +151,7 @@ class DecimalNumber(VMobject): ":", "+" if config["include_sign"] else "", "," if config["group_with_commas"] else "", - f".{ndp}f", + f".{ndp}f" if ndp > 0 else "d", "}", ]) @@ -169,6 +171,7 @@ class DecimalNumber(VMobject): self.set_submobjects_from_number(number) self.move_to(move_to_point, self.edge_to_fix) self.set_style(**style) + self.fix_in_frame(self._is_fixed_in_frame) return self def _handle_scale_side_effects(self, scale_factor: float) -> Self: diff --git a/manimlib/mobject/shape_matchers.py b/manimlib/mobject/shape_matchers.py index b79491d9..69bd4543 100644 --- a/manimlib/mobject/shape_matchers.py +++ b/manimlib/mobject/shape_matchers.py @@ -27,13 +27,20 @@ class SurroundingRectangle(Rectangle): color: ManimColor = YELLOW, **kwargs ): - super().__init__( - width=mobject.get_width() + 2 * buff, - height=mobject.get_height() + 2 * buff, - color=color, - **kwargs - ) - self.move_to(mobject) + super().__init__(color=color, **kwargs) + self.buff = buff + self.surround(mobject) + + def surround(self, mobject, buff=None) -> Self: + self.mobject = mobject + self.buff = buff if buff is not None else self.buff + super().surround(mobject, self.buff) + return self + + def set_buff(self, buff) -> Self: + self.buff = buff + self.surround(self.mobject) + return self class BackgroundRectangle(SurroundingRectangle): @@ -110,6 +117,7 @@ class Underline(Line): buff: float = SMALL_BUFF, stroke_color=WHITE, stroke_width: float | Sequence[float] = [0, 3, 3, 0], + stretch_factor=1.2, **kwargs ): super().__init__( @@ -119,5 +127,6 @@ class Underline(Line): **kwargs ) self.insert_n_curves(30) - self.match_width(mobject) + self.set_stroke(stroke_color, stroke_width) + self.set_width(mobject.get_width() * stretch_factor) self.next_to(mobject, DOWN, buff=buff) diff --git a/manimlib/mobject/svg/drawings.py b/manimlib/mobject/svg/drawings.py index 4b0449ae..f620b47a 100644 --- a/manimlib/mobject/svg/drawings.py +++ b/manimlib/mobject/svg/drawings.py @@ -1,6 +1,7 @@ from __future__ import annotations import numpy as np +import itertools as it from manimlib.animation.composition import AnimationGroup from manimlib.animation.rotation import Rotating @@ -14,6 +15,7 @@ from manimlib.constants import DOWN from manimlib.constants import FRAME_WIDTH from manimlib.constants import GREEN from manimlib.constants import GREEN_SCREEN +from manimlib.constants import GREEN_E from manimlib.constants import GREY from manimlib.constants import GREY_A from manimlib.constants import GREY_B @@ -26,6 +28,7 @@ from manimlib.constants import ORIGIN from manimlib.constants import OUT from manimlib.constants import PI from manimlib.constants import RED +from manimlib.constants import RED_E from manimlib.constants import RIGHT from manimlib.constants import SMALL_BUFF from manimlib.constants import SMALL_BUFF @@ -36,6 +39,7 @@ from manimlib.constants import DL from manimlib.constants import DR from manimlib.constants import WHITE from manimlib.constants import YELLOW +from manimlib.constants import TAU from manimlib.mobject.boolean_ops import Difference from manimlib.mobject.geometry import Arc from manimlib.mobject.geometry import Circle @@ -44,6 +48,7 @@ from manimlib.mobject.geometry import Line from manimlib.mobject.geometry import Polygon from manimlib.mobject.geometry import Rectangle from manimlib.mobject.geometry import Square +from manimlib.mobject.geometry import AnnularSector from manimlib.mobject.mobject import Mobject from manimlib.mobject.numbers import Integer from manimlib.mobject.svg.svg_mobject import SVGMobject @@ -358,7 +363,7 @@ class Bubble(SVGMobject): stroke_width: float = 3.0, **kwargs ): - self.direction = direction + self.direction = LEFT # Possibly updated below by self.flip() self.bubble_center_adjustment_factor = bubble_center_adjustment_factor self.content_scale_factor = content_scale_factor @@ -380,7 +385,7 @@ class Bubble(SVGMobject): if direction[0] > 0: self.flip() - self.content = Mobject() + self.content = VMobject() def get_tip(self): # TODO, find a better way @@ -403,10 +408,10 @@ class Bubble(SVGMobject): self.direction = -np.array(self.direction) return self - def pin_to(self, mobject): + def pin_to(self, mobject, auto_flip=False): mob_center = mobject.get_center() want_to_flip = np.sign(mob_center[0]) != np.sign(self.direction[0]) - if want_to_flip: + if want_to_flip and auto_flip: self.flip() boundary_point = mobject.get_bounding_box_point(UP - self.direction) vector_from_center = 1.0 * (boundary_point - mob_center) @@ -579,7 +584,6 @@ class Piano3D(VGroup): key.set_color(BLACK) - class DieFace(VGroup): def __init__( self, @@ -590,7 +594,7 @@ class DieFace(VGroup): stroke_width: float = 2.0, fill_color: ManimColor = GREY_E, dot_radius: float = 0.08, - dot_color: ManimColor = BLUE_B, + dot_color: ManimColor = WHITE, dot_coalesce_factor: float = 0.5 ): dot = Dot(radius=dot_radius, fill_color=dot_color) @@ -622,5 +626,50 @@ class DieFace(VGroup): arrangement.space_out_submobjects(dot_coalesce_factor) super().__init__(square, arrangement) + self.dots = arrangement self.value = value self.index = value + + +class Dartboard(VGroup): + radius = 3 + n_sectors = 20 + + def __init__(self, **kwargs): + super().__init__(**kwargs) + n_sectors = self.n_sectors + angle = TAU / n_sectors + + segments = VGroup(*[ + VGroup(*[ + AnnularSector( + inner_radius=in_r, + outer_radius=out_r, + start_angle=n * angle, + angle=angle, + fill_color=color, + ) + for n, color in zip( + range(n_sectors), + it.cycle(colors) + ) + ]) + for colors, in_r, out_r in [ + ([GREY_B, GREY_E], 0, 1), + ([GREEN_E, RED_E], 0.5, 0.55), + ([GREEN_E, RED_E], 0.95, 1), + ] + ]) + segments.rotate(-angle / 2) + bullseyes = VGroup(*[ + Circle(radius=r) + for r in [0.07, 0.035] + ]) + bullseyes.set_fill(opacity=1) + bullseyes.set_stroke(width=0) + bullseyes[0].set_color(GREEN_E) + bullseyes[1].set_color(RED_E) + + self.bullseye = bullseyes[1] + self.add(*segments, *bullseyes) + self.scale(self.radius) diff --git a/manimlib/mobject/svg/string_mobject.py b/manimlib/mobject/svg/string_mobject.py index c675254d..a723601d 100644 --- a/manimlib/mobject/svg/string_mobject.py +++ b/manimlib/mobject/svg/string_mobject.py @@ -47,6 +47,7 @@ class StringMobject(SVGMobject, ABC): self, string: str, fill_color: ManimColor = WHITE, + fill_border_width: float = 0.5, stroke_color: ManimColor = WHITE, stroke_width: float = 0, base_color: ManimColor = WHITE, @@ -65,12 +66,10 @@ class StringMobject(SVGMobject, ABC): self.use_labelled_svg = use_labelled_svg self.parse() - super().__init__( - stroke_color=stroke_color, - fill_color=fill_color, - stroke_width=stroke_width, - **kwargs - ) + super().__init__(**kwargs) + self.set_stroke(stroke_color, stroke_width) + self.set_fill(fill_color, border_width=fill_border_width) + self.note_changed_stroke() self.labels = [submob.label for submob in self.submobjects] def get_file_path(self, is_labelled: bool = False) -> str: diff --git a/manimlib/mobject/types/dot_cloud.py b/manimlib/mobject/types/dot_cloud.py index 43dbcd14..f4a8504b 100644 --- a/manimlib/mobject/types/dot_cloud.py +++ b/manimlib/mobject/types/dot_cloud.py @@ -22,6 +22,7 @@ DEFAULT_GLOW_DOT_RADIUS = 0.2 DEFAULT_GRID_HEIGHT = 6 DEFAULT_BUFF_RATIO = 0.5 + class DotCloud(PMobject): shader_folder: str = "true_dot" render_primitive: int = moderngl.POINTS @@ -116,6 +117,10 @@ class DotCloud(PMobject): def get_radius(self) -> float: return self.get_radii().max() + def scale_radii(self, scale_factor: float) -> Self: + self.set_radius(scale_factor * self.get_radii()) + return self + def set_glow_factor(self, glow_factor: float) -> Self: self.uniforms["glow_factor"] = glow_factor return self diff --git a/manimlib/mobject/types/image_mobject.py b/manimlib/mobject/types/image_mobject.py index 80efe5aa..b49872de 100644 --- a/manimlib/mobject/types/image_mobject.py +++ b/manimlib/mobject/types/image_mobject.py @@ -25,7 +25,7 @@ class ImageMobject(Mobject): ('opacity', np.float32, (1,)), ] - def __init__( + def __init__( self, filename: str, height: float = 4.0, diff --git a/manimlib/mobject/types/surface.py b/manimlib/mobject/types/surface.py index aaac1ebe..51ae324e 100644 --- a/manimlib/mobject/types/surface.py +++ b/manimlib/mobject/types/surface.py @@ -130,11 +130,13 @@ class Surface(Mobject): def get_unit_normals(self) -> Vect3Array: nu, nv = self.resolution indices = np.arange(nu * nv) + if len(indices) == 0: + return np.zeros((3, 0)) - left = indices - 1 + left = indices - 1 right = indices + 1 - up = indices - nv - down = indices + nv + up = indices - nv + down = indices + nv left[0] = indices[0] right[-1] = indices[-1] @@ -166,7 +168,7 @@ class Surface(Mobject): nu, nv = smobject.resolution self.data['point'][:] = self.get_partial_points_array( - self.data['point'], a, b, + smobject.data['point'], a, b, (nu, nv, 3), axis=axis ) @@ -183,7 +185,7 @@ class Surface(Mobject): if len(points) == 0: return points nu, nv = resolution[:2] - points = points.reshape(resolution) + points = points.reshape(resolution).copy() max_index = resolution[axis] - 1 lower_index, lower_residue = integer_interpolate(0, max_index, a) upper_index, upper_residue = integer_interpolate(0, max_index, b) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 72b287af..a3bc87b9 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -11,17 +11,20 @@ from manimlib.constants import DEFAULT_STROKE_WIDTH from manimlib.constants import DEGREES from manimlib.constants import JOINT_TYPE_MAP from manimlib.constants import ORIGIN, OUT +from manimlib.constants import TAU from manimlib.mobject.mobject import Mobject from manimlib.mobject.mobject import Point from manimlib.utils.bezier import bezier from manimlib.utils.bezier import get_quadratic_approximation_of_cubic from manimlib.utils.bezier import approx_smooth_quadratic_bezier_handles from manimlib.utils.bezier import smooth_quadratic_path +from manimlib.utils.bezier import interpolate from manimlib.utils.bezier import integer_interpolate from manimlib.utils.bezier import inverse_interpolate from manimlib.utils.bezier import find_intersection -from manimlib.utils.bezier import partial_quadratic_bezier_points from manimlib.utils.bezier import outer_interpolate +from manimlib.utils.bezier import partial_quadratic_bezier_points +from manimlib.utils.bezier import quadratic_bezier_points_for_arc from manimlib.utils.color import color_gradient from manimlib.utils.color import rgb_to_hex from manimlib.utils.iterables import make_even @@ -38,6 +41,8 @@ from manimlib.utils.space_ops import get_unit_normal from manimlib.utils.space_ops import line_intersects_path from manimlib.utils.space_ops import midpoint from manimlib.utils.space_ops import normalize_along_axis +from manimlib.utils.space_ops import rotation_between_vectors +from manimlib.utils.space_ops import poly_line_length from manimlib.utils.space_ops import z_to_vector from manimlib.shader_wrapper import ShaderWrapper from manimlib.shader_wrapper import FillShaderWrapper @@ -234,7 +239,7 @@ class VMobject(Mobject): stroke_opacity: float | Iterable[float] | None = None, stroke_rgba: Vect4 | None = None, stroke_width: float | Iterable[float] | None = None, - stroke_background: bool = True, + stroke_background: bool = False, shading: Tuple[float, float, float] | None = None, recurse: bool = True ) -> Self: @@ -374,7 +379,7 @@ class VMobject(Mobject): data = self.data if self.has_points() else self._data_defaults return rgb_to_hex(data["stroke_rgba"][0, :3]) - def get_stroke_width(self) -> float | np.ndarray: + def get_stroke_width(self) -> float: data = self.data if self.has_points() else self._data_defaults return data["stroke_width"][0, 0] @@ -423,7 +428,7 @@ class VMobject(Mobject): self, anti_alias_width: float = 0, fill_border_width: float = 0, - recurse: bool=True + recurse: bool = True ) -> Self: super().apply_depth_test(recurse) self.set_anti_alias_width(anti_alias_width) @@ -434,9 +439,9 @@ class VMobject(Mobject): self, anti_alias_width: float = 1.0, fill_border_width: float = 0.5, - recurse: bool=True + recurse: bool = True ) -> Self: - super().apply_depth_test(recurse) + super().deactivate_depth_test(recurse) self.set_anti_alias_width(anti_alias_width) self.set_fill(border_width=fill_border_width) return self @@ -455,6 +460,9 @@ class VMobject(Mobject): anchors: Vect3Array, handles: Vect3Array, ) -> Self: + if len(anchors) == 0: + self.clear_points() + return self assert(len(anchors) == len(handles) + 1) points = resize_array(self.get_points(), 2 * len(anchors) - 1) points[0::2] = anchors @@ -543,6 +551,26 @@ class VMobject(Mobject): self.add_cubic_bezier_curve_to(new_handle, handle, point) return self + def add_arc_to(self, point: Vect3, angle: float, n_components: int | None = None, threshold: float = 1e-3) -> Self: + self.throw_error_if_no_points() + if abs(angle) < threshold: + self.add_line_to(point) + return self + + # Assign default value for n_components + if n_components is None: + n_components = int(np.ceil(8 * abs(angle) / TAU)) + + arc_points = quadratic_bezier_points_for_arc(angle, n_components) + target_vect = point - self.get_end() + curr_vect = arc_points[-1] - arc_points[0] + + arc_points = arc_points @ rotation_between_vectors(curr_vect, target_vect).T + arc_points *= get_norm(target_vect) / get_norm(curr_vect) + arc_points += (self.get_end() - arc_points[0]) + self.append_points(arc_points[1:]) + return self + def has_new_path_started(self) -> bool: points = self.get_points() if len(points) == 0: @@ -642,6 +670,8 @@ class VMobject(Mobject): def change_anchor_mode(self, mode: str) -> Self: assert(mode in ("jagged", "approx_smooth", "true_smooth")) + if self.get_num_points() == 0: + return self subpaths = self.get_subpaths() self.clear_points() for subpath in subpaths: @@ -745,8 +775,8 @@ class VMobject(Mobject): return self.get_subpaths_from_points(self.get_points()) def get_nth_curve_points(self, n: int) -> Vect3Array: - assert(n < self.get_num_curves()) - return self.get_points()[2 * n : 2 * n + 3] + assert n < self.get_num_curves() + return self.get_points()[2 * n:2 * n + 3] def get_nth_curve_function(self, n: int) -> Callable[[float], Vect3]: return bezier(self.get_nth_curve_points(n)) @@ -761,12 +791,14 @@ class VMobject(Mobject): curve_func = self.get_nth_curve_function(n) return curve_func(residue) - def point_from_proportion(self, alpha: float) -> Vect3: - if alpha <= 0: - return self.get_start() - elif alpha >= 1: - return self.get_end() - + def curve_and_prop_of_partial_point(self, alpha) -> Tuple[int, float]: + """ + If you want a point a proportion alpha along the curve, this + gives you the index of the appropriate bezier curve, together + with the proportion along that curve you'd need to travel + """ + if alpha == 0: + return (0, 0.0) partials: list[float] = [0] for tup in self.get_bezier_tuples(): if self.consider_points_equal(tup[0], tup[1]): @@ -778,14 +810,24 @@ class VMobject(Mobject): partials.append(partials[-1] + arclen) full = partials[-1] if full == 0: - return self.get_start() + return len(partials), 1.0 # First index where the partial length is more than alpha times the full length - i = next( + index = next( (i for i, x in enumerate(partials) if x >= full * alpha), - len(partials) # Default + len(partials) - 1 # Default ) - residue = float(inverse_interpolate(partials[i - 1] / full, partials[i] / full, alpha)) - return self.get_nth_curve_function(i - 1)(residue) + residue = float(inverse_interpolate( + partials[index - 1] / full, partials[index] / full, alpha + )) + return index - 1, residue + + def point_from_proportion(self, alpha: float) -> Vect3: + if alpha <= 0: + return self.get_start() + elif alpha >= 1: + return self.get_end() + index, residue = self.curve_and_prop_of_partial_point(alpha) + return self.get_nth_curve_function(index)(residue) def get_anchors_and_handles(self) -> list[Vect3]: """ @@ -814,14 +856,16 @@ class VMobject(Mobject): return np.vstack(new_points) def get_arc_length(self, n_sample_points: int | None = None) -> float: - if n_sample_points is None: - n_sample_points = 4 * self.get_num_curves() + 1 - points = np.array([ - self.point_from_proportion(a) - for a in np.linspace(0, 1, n_sample_points) - ]) - diffs = points[1:] - points[:-1] - return sum(map(get_norm, diffs)) + if n_sample_points is not None: + points = np.array([ + self.quick_point_from_proportion(a) + for a in np.linspace(0, 1, n_sample_points) + ]) + return poly_line_length(points) + points = self.get_points() + inner_len = poly_line_length(points[::2]) + outer_len = poly_line_length(points) + return interpolate(inner_len, outer_len, 1 / 3) def get_area_vector(self) -> Vect3: # Returns a vector whose length is the area bound by diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index ed4bd5fc..79745ca6 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -1021,9 +1021,10 @@ class EndScene(Exception): class ThreeDScene(Scene): samples = 4 default_frame_orientation = (-30, 70) + always_depth_test = True def add(self, *mobjects, set_depth_test: bool = True): for mob in mobjects: - if set_depth_test and not mob.is_fixed_in_frame(): + if set_depth_test and not mob.is_fixed_in_frame() and self.always_depth_test: mob.apply_depth_test() super().add(*mobjects) diff --git a/manimlib/scene/scene_file_writer.py b/manimlib/scene/scene_file_writer.py index 47698d0d..eca49b05 100644 --- a/manimlib/scene/scene_file_writer.py +++ b/manimlib/scene/scene_file_writer.py @@ -42,6 +42,7 @@ class SceneFileWriter(object): # Where should this be written output_directory: str | None = None, file_name: str | None = None, + subdirectory_for_videos: bool = False, open_file_upon_completion: bool = False, show_file_location_upon_completion: bool = False, quiet: bool = False, @@ -49,8 +50,8 @@ class SceneFileWriter(object): progress_description_len: int = 40, video_codec: str = "libx264", pixel_format: str = "yuv420p", - saturation: float = 1.7, - gamma: float = 1.2, + saturation: float = 1.0, + gamma: float = 1.0, ): self.scene: Scene = scene self.write_to_movie = write_to_movie @@ -63,6 +64,7 @@ class SceneFileWriter(object): self.output_directory = output_directory self.file_name = file_name self.open_file_upon_completion = open_file_upon_completion + self.subdirectory_for_videos = subdirectory_for_videos self.show_file_location_upon_completion = show_file_location_upon_completion self.quiet = quiet self.total_frames = total_frames @@ -88,7 +90,10 @@ class SceneFileWriter(object): image_file = add_extension_if_not_present(scene_name, ".png") self.image_file_path = os.path.join(image_dir, image_file) if self.write_to_movie: - movie_dir = guarantee_existence(os.path.join(out_dir, "videos")) + if self.subdirectory_for_videos: + movie_dir = guarantee_existence(os.path.join(out_dir, "videos")) + else: + movie_dir = guarantee_existence(out_dir) movie_file = add_extension_if_not_present(scene_name, self.movie_file_extension) self.movie_file_path = os.path.join(movie_dir, movie_file) if self.break_into_partial_movies: diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index 4e811147..8fa3801e 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -56,6 +56,7 @@ class ShaderWrapper(object): self.init_program_code() self.init_program() + self.texture_names_to_ids = dict() if texture_paths is not None: self.init_textures(texture_paths) self.init_vao() @@ -82,11 +83,10 @@ class ShaderWrapper(object): self.vert_format = moderngl.detect_format(self.program, self.vert_attributes) def init_textures(self, texture_paths: dict[str, str]): - names_to_ids = { + self.texture_names_to_ids = { name: get_texture_id(image_path_to_texture(path, self.ctx)) for name, path in texture_paths.items() } - self.update_program_uniforms(names_to_ids) def init_vao(self): self.vbo = None @@ -138,6 +138,7 @@ class ShaderWrapper(object): self.mobject_uniforms, self.depth_test, self.render_primitive, + self.texture_names_to_ids, ])) def refresh_id(self) -> None: @@ -224,8 +225,9 @@ class ShaderWrapper(object): def update_program_uniforms(self, camera_uniforms: UniformDict): if self.program is None: return - for name, value in (*self.mobject_uniforms.items(), *camera_uniforms.items()): - set_program_uniform(self.program, name, value) + for uniforms in [self.mobject_uniforms, camera_uniforms, self.texture_names_to_ids]: + for name, value in uniforms.items(): + set_program_uniform(self.program, name, value) def get_vertex_buffer_object(self, refresh: bool = True): if refresh: diff --git a/manimlib/shaders/true_dot/frag.glsl b/manimlib/shaders/true_dot/frag.glsl index 8f82ee4c..b3d42656 100644 --- a/manimlib/shaders/true_dot/frag.glsl +++ b/manimlib/shaders/true_dot/frag.glsl @@ -5,7 +5,11 @@ uniform mat4 perspective; in vec4 color; in float scaled_aaw; -in vec3 v_point; +in vec3 point; +in vec3 to_cam; +in vec3 center; +in float radius; +in vec2 uv_coords; out vec4 frag_color; @@ -13,9 +17,8 @@ out vec4 frag_color; #INSERT finalize_color.glsl void main() { - vec2 vect = 2.0 * gl_PointCoord.xy - vec2(1.0); - float r = length(vect); - if(r > 1.0 + scaled_aaw) discard; + float r = length(uv_coords.xy); + if(r > 1.0) discard; frag_color = color; @@ -24,9 +27,9 @@ void main() { } if(shading != vec3(0.0)){ - vec3 normal = vec3(vect, sqrt(1 - r * r)); - normal = (perspective * vec4(normal, 0.0)).xyz; - frag_color = finalize_color(frag_color, v_point, normal); + vec3 point_3d = point + radius * sqrt(1 - r * r) * to_cam; + vec3 normal = normalize(point_3d - center); + frag_color = finalize_color(frag_color, point_3d, normal); } frag_color.a *= smoothstep(1.0, 1.0 - scaled_aaw, r); diff --git a/manimlib/shaders/true_dot/geom.glsl b/manimlib/shaders/true_dot/geom.glsl new file mode 100644 index 00000000..af7b36b2 --- /dev/null +++ b/manimlib/shaders/true_dot/geom.glsl @@ -0,0 +1,44 @@ +#version 330 + +layout (points) in; +layout (triangle_strip, max_vertices = 4) out; + +uniform float pixel_size; +uniform float anti_alias_width; +uniform float frame_scale; +uniform vec3 camera_position; + +in vec3 v_point[1]; +in float v_radius[1]; +in vec4 v_rgba[1]; + +out vec4 color; +out float scaled_aaw; +out vec3 point; +out vec3 to_cam; +out vec3 center; +out float radius; +out vec2 uv_coords; + +#INSERT emit_gl_Position.glsl + +void main(){ + color = v_rgba[0]; + radius = v_radius[0]; + center = v_point[0]; + scaled_aaw = (anti_alias_width * pixel_size) / v_radius[0]; + + to_cam = normalize(camera_position - v_point[0]); + vec3 right = v_radius[0] * normalize(cross(vec3(0, 1, 1), to_cam)); + vec3 up = v_radius[0] * normalize(cross(to_cam, right)); + + for(int i = -1; i < 2; i += 2){ + for(int j = -1; j < 2; j += 2){ + point = v_point[0] + i * right + j * up; + uv_coords = vec2(i, j); + emit_gl_Position(point); + EmitVertex(); + } + } + EndPrimitive(); +} \ No newline at end of file diff --git a/manimlib/shaders/true_dot/vert.glsl b/manimlib/shaders/true_dot/vert.glsl index 3dca6f24..55658063 100644 --- a/manimlib/shaders/true_dot/vert.glsl +++ b/manimlib/shaders/true_dot/vert.glsl @@ -1,26 +1,16 @@ #version 330 -uniform float pixel_size; -uniform float anti_alias_width; - in vec3 point; in float radius; in vec4 rgba; -out vec4 color; -out float scaled_aaw; out vec3 v_point; -out vec3 light_pos; +out float v_radius; +out vec4 v_rgba; -#INSERT emit_gl_Position.glsl void main(){ v_point = point; - color = rgba; - scaled_aaw = (anti_alias_width * pixel_size) / radius; - - emit_gl_Position(point); - float z = -10 * gl_Position.z; - float scaled_radius = radius * 1.0 / (1.0 - z); - gl_PointSize = 2 * ((scaled_radius / pixel_size) + anti_alias_width); + v_radius = radius; + v_rgba = rgba; } \ No newline at end of file diff --git a/manimlib/utils/bezier.py b/manimlib/utils/bezier.py index dc314a9f..accf0012 100644 --- a/manimlib/utils/bezier.py +++ b/manimlib/utils/bezier.py @@ -171,6 +171,16 @@ def match_interpolate( ) +def quadratic_bezier_points_for_arc(angle: float, n_components: int = 8): + n_points = 2 * n_components + 1 + angles = np.linspace(0, angle, n_points) + points = np.array([np.cos(angles), np.sin(angles), np.zeros(n_points)]).T + # Adjust handles + theta = angle / n_components + points[1::2] /= np.cos(theta / 2) + return points + + def approx_smooth_quadratic_bezier_handles( points: FloatArray ) -> FloatArray: diff --git a/manimlib/utils/iterables.py b/manimlib/utils/iterables.py index 3b7dbfba..90cd4c4b 100644 --- a/manimlib/utils/iterables.py +++ b/manimlib/utils/iterables.py @@ -136,6 +136,18 @@ def array_is_constant(arr: np.ndarray) -> bool: return len(arr) > 0 and (arr == arr[0]).all() +def cartesian_product(*arrays: np.ndarray): + """ + Copied from https://stackoverflow.com/a/11146645 + """ + la = len(arrays) + dtype = np.result_type(*arrays) + arr = np.empty([len(a) for a in arrays] + [la], dtype=dtype) + for i, a in enumerate(np.ix_(*arrays)): + arr[..., i] = a + return arr.reshape(-1, la) + + def hash_obj(obj: object) -> int: if isinstance(obj, dict): return hash(tuple(sorted([ diff --git a/manimlib/utils/shaders.py b/manimlib/utils/shaders.py index 7cc1ba5c..18b3d24e 100644 --- a/manimlib/utils/shaders.py +++ b/manimlib/utils/shaders.py @@ -21,6 +21,7 @@ if TYPE_CHECKING: from moderngl.framebuffer import Framebuffer +# Global maps updated as textures are allocated ID_TO_TEXTURE: dict[int, moderngl.Texture] = dict() PROGRAM_UNIFORM_MIRRORS: dict[int, dict[str, float | tuple]] = dict() @@ -56,7 +57,7 @@ def get_shader_program( vertex_shader: str, fragment_shader: Optional[str] = None, geometry_shader: Optional[str] = None, - ) -> moderngl.Program: +) -> moderngl.Program: return ctx.program( vertex_shader=vertex_shader, fragment_shader=fragment_shader, @@ -74,7 +75,7 @@ def set_program_uniform( of previously set uniforms for that program so that it doesn't needlessly reset it, requiring an exchange with gpu memory, if it sees the same value again. - + Returns True if changed the program, False if it left it as is. """ @@ -134,7 +135,6 @@ def get_colormap_code(rgb_list: Sequence[float]) -> str: return f"vec3[{len(rgb_list)}]({data})" - @lru_cache() def get_fill_canvas(ctx: moderngl.Context) -> Tuple[Framebuffer, VertexArray]: """ diff --git a/manimlib/utils/space_ops.py b/manimlib/utils/space_ops.py index fdc2f52b..e89c1dfe 100644 --- a/manimlib/utils/space_ops.py +++ b/manimlib/utils/space_ops.py @@ -61,6 +61,13 @@ def normalize( return np.zeros(len(vect)) +def poly_line_length(points): + """ + Return the sum of the lengths between adjacent points + """ + diffs = points[1:] - points[:-1] + return np.sqrt((diffs**2).sum(1)).sum() + # Operations related to rotation @@ -199,7 +206,7 @@ def normalize_along_axis( ) -> np.ndarray: norms = np.sqrt((array * array).sum(axis)) norms[norms == 0] = 1 - return (array.T / norms).T + return array / norms[:, np.newaxis] def get_unit_normal( diff --git a/manimlib/utils/tex.py b/manimlib/utils/tex.py index 791cdc5c..719d228a 100644 --- a/manimlib/utils/tex.py +++ b/manimlib/utils/tex.py @@ -16,7 +16,7 @@ def num_tex_symbols(tex: str) -> int: # \begin{array}{cc}, etc. pattern = "|".join( rf"(\\{s})" + r"(\{\w+\})?(\{\w+\})?(\[\w+\])?" - for s in ["begin", "end", "phantom", "text"] + for s in ["begin", "end", "phantom"] ) tex = re.sub(pattern, "", tex) diff --git a/manimlib/utils/tex_to_symbol_count.py b/manimlib/utils/tex_to_symbol_count.py index c314b5dd..049e9fde 100644 --- a/manimlib/utils/tex_to_symbol_count.py +++ b/manimlib/utils/tex_to_symbol_count.py @@ -49,6 +49,7 @@ TEX_TO_SYMBOL_COUNT = { R"\div": 2, R"\doteq": 2, R"\dotfill": 0, + R"\dots": 3, R"\emph": 0, R"\exp": 3, R"\fbox": 4, @@ -102,6 +103,7 @@ TEX_TO_SYMBOL_COUNT = { R"\makebox": 0, R"\mapsto": 2, R"\markright": 0, + R"\mathds": 0, R"\max": 3, R"\mbox": 0, R"\medskip": 0, @@ -160,6 +162,7 @@ TEX_TO_SYMBOL_COUNT = { R"\sup": 3, R"\tan": 3, R"\tanh": 4, + R"\text": 0, R"\textbf": 0, R"\textfraction": 2, R"\textstyle": 0, diff --git a/manimlib/window.py b/manimlib/window.py index 299a4c8d..152da54d 100644 --- a/manimlib/window.py +++ b/manimlib/window.py @@ -27,7 +27,7 @@ class Window(PygletWindow): self, scene: Scene, size: tuple[int, int] = (1280, 720), - samples = 0 + samples: int = 0 ): scene.window = self super().__init__(size=size, samples=samples) @@ -47,9 +47,12 @@ class Window(PygletWindow): self.to_default_position() def to_default_position(self): - self.size = self.default_size self.position = self.default_position - self.swap_buffers() + # Hack. Sometimes, namely when configured to open in a separate window, + # the window needs to be resized to display correctly. + w, h = self.default_size + self.size = (w - 1, h - 1) + self.size = (w, h) def find_initial_position(self, size: tuple[int, int]) -> tuple[int, int]: custom_position = get_customization()["window_position"]