diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 573ef10df0..a98867ca68 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -56,7 +56,7 @@ repos: flake8-docstrings==1.6.0, flake8-rst-docstrings==0.2.3, flake8-pytest-style==1.5.0, flake8-simplify==0.14.1, flake8-comprehensions>=3.6.1] - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v0.950' + rev: 'v0.960' hooks: - id: mypy additional_dependencies: [types-decorator, types-docutils, types-requests, types-setuptools] diff --git a/manim/mobject/opengl/opengl_mobject.py b/manim/mobject/opengl/opengl_mobject.py index e75b363e45..bfc53f5919 100644 --- a/manim/mobject/opengl/opengl_mobject.py +++ b/manim/mobject/opengl/opengl_mobject.py @@ -2,22 +2,23 @@ import copy import itertools as it +import pickle import random import sys from functools import partialmethod, wraps from math import ceil -from typing import Iterable, Sequence +from typing import TYPE_CHECKING import moderngl import numpy as np -from colour import Color -from manim import config +from manim import config, logger from manim.constants import * from manim.utils.bezier import integer_interpolate, interpolate from manim.utils.color import * from manim.utils.color import Colors from manim.utils.config_ops import _Data, _Uniforms +from manim.utils.deprecation import deprecated # from ..utils.iterables import batch_by_property from manim.utils.iterables import ( @@ -38,6 +39,29 @@ rotation_matrix_transpose, ) +if True: + from typing import ( + Any, + Callable, + Iterable, + List, + Optional, + Sequence, + Tuple, + Type, + Union, + ) + + import numpy.typing as npt + from colour import Color + + TimeBasedUpdater = Callable[["OpenGLMobject", float], None] + NonTimeUpdater = Callable[["OpenGLMobject"], None] + Updater = Union[TimeBasedUpdater, NonTimeUpdater] + + ManimColor = Union[str, Color] + Array = npt.NDArray[np.float64] + class OpenGLMobject: """Mathematical Object: base class for objects that can be displayed on screen. @@ -55,8 +79,8 @@ class OpenGLMobject: """ - shader_dtype = [ - ("point", np.float32, (3,)), + shader_dtype: list[tuple[str, npt.DTypeLike, tuple[int]]] = [ + ("point", np.float32, (3,)) ] shader_folder = "" @@ -70,81 +94,114 @@ class OpenGLMobject: fixed_orientation_center = _Uniforms() # for fixed orientation reference gloss = _Uniforms() shadow = _Uniforms() + reflectiveness = _Uniforms() + + def init_constructor(self: OpenGLMobject) -> None: + """Initializes the object.""" + self._submobjects: list[OpenGLMobject] = [] + self.parents: list[OpenGLMobject] = [] + self.parent: None | ( + OpenGLMobject + ) = None # TODO: Does not exist in manimgl anymore + self.family: list[OpenGLMobject] = [self] + self.locked_data_keys: set[str] = set() + self.needs_new_bounding_box = True + self.saved_state = None + self.target = None + + self.init_data() + self.init_updaters() + # self.init_event_listners() + self.init_points() + self.init_colors() + # self.init_shader_data() # Unclear usage of function on line 1748 in manimgl + + if self.depth_test: + self.apply_depth_test() def __init__( - self, - color=WHITE, - opacity=1, - dim=3, # TODO, get rid of this - # Lighting parameters - # Positive gloss up to 1 makes it reflect the light. - gloss=0.0, - # Positive shadow up to 1 makes a side opposite the light darker - shadow=0.0, - # For shaders - render_primitive=moderngl.TRIANGLES, - texture_paths=None, - depth_test=False, - # If true, the mobject will not get rotated according to camera position - is_fixed_in_frame=False, - is_fixed_orientation=False, - # Must match in attributes of vert shader - # Event listener - listen_to_events=False, - model_matrix=None, - should_render=True, - **kwargs, + self: OpenGLMobject, + color: ManimColor | Iterable[ManimColor] = WHITE, + opacity: float | Iterable[float] = 1, + dim: int = 3, # TODO, get rid of this + reflectiveness: float = 0.0, + shadow: float = 0.0, + gloss: float = 0.0, + render_primitive: int = moderngl.TRIANGLES, + texture_paths: list[str] | None = None, + depth_test: bool = False, + is_fixed_in_frame: bool = False, + is_fixed_orientation: bool = False, + listen_to_events: bool = False, + model_matrix: Array | None = None, # TODO: Does not exist in manimgl + should_render: bool = True, + **kwargs: Any, ): + """The base class for everything that can be rendered in manim + + Parameters + ---------- + color + The color of the object. Can be a single color or a list of colors. + opacity + The opacity of the object. Can be a single opacity or a list of opacities. + dim + The dimension of the object. (3 for 3D, 2 for 2D) + reflectiveness + The reflectiveness of the object. 1 is fully reflective, 0 is not reflective. Larger values make the object brighter when facing the light. + shadow + The shadow of the object. 1 is fully shadowed, 0 is not shadowed. Larger values make the object darker when facing opposite the light. + gloss + The gloss of the object. 1 is fully glossed, 0 is not glossed. Larger values make the object reflect the light. + render_primitive + The primitive to render the object with. Can be one of the following: ``moderngl.TRIANGLES``, ``moderngl.LINES``, ``moderngl.POINTS``, ``moderngl.LINE_STRIP``, ``moderngl.LINE_LOOP``, ``moderngl.TRIANGLE_STRIP``, ``moderngl.TRIANGLE_FAN``. and so on + texture_paths + The texture paths of the object. + depth_test + Whether to enable depth testing. + is_fixed_in_frame + Whether the object should be fixed in the frame. + is_fixed_orientation + Whether the object should be fixed in orientation. If true, the object will not be rotated according to camera orientation. + listen_to_events + Whether the object should listen to events. + model_matrix + The model matrix of the object. + should_render + Whether the object should be rendered. + """ + logger.debug("M __init__") # getattr in case data/uniforms are already defined in parent classes. - self.data = getattr(self, "data", {}) - self.uniforms = getattr(self, "uniforms", {}) + self.color = color self.opacity = opacity self.dim = dim # TODO, get rid of this - # Lighting parameters - # Positive gloss up to 1 makes it reflect the light. - self.gloss = gloss - # Positive shadow up to 1 makes a side opposite the light darker - self.shadow = shadow - # For shaders + + # For Shaders self.render_primitive = render_primitive self.texture_paths = texture_paths self.depth_test = depth_test - # If true, the mobject will not get rotated according to camera position - self.is_fixed_in_frame = float(is_fixed_in_frame) - self.is_fixed_orientation = float(is_fixed_orientation) - self.fixed_orientation_center = (0, 0, 0) - # Must match in attributes of vert shader - # Event listener self.listen_to_events = listen_to_events - - self._submobjects = [] - self.parents = [] - self.parent = None - self.family = [self] - self.locked_data_keys = set() - self.needs_new_bounding_box = True + self.shader_indices = None if model_matrix is None: self.model_matrix = np.eye(4) else: self.model_matrix = model_matrix - self.init_data() - self.init_updaters() - # self.init_event_listners() - self.init_points() - self.color = Color(color) if color else None - self.init_colors() - - self.shader_indices = None - - if self.depth_test: - self.apply_depth_test() + # Init Uniforms + self.uniforms = getattr(self, "uniforms", {}) + self.is_fixed_in_frame = float(is_fixed_in_frame) + self.is_fixed_orientation = float(is_fixed_orientation) + self.fixed_orientation_center = np.asarray((0, 0, 0)) + self.gloss = gloss + self.shadow = shadow + self.reflectiveness = reflectiveness + self.init_constructor() self.should_render = should_render @classmethod - def __init_subclass__(cls, **kwargs): + def __init_subclass__(cls: type[OpenGLMobject], **kwargs): super().__init_subclass__(**kwargs) cls._original__init__ = cls.__init__ @@ -160,8 +217,15 @@ def __sub__(self, other): def __isub__(self, other): raise NotImplementedError - def __add__(self, mobject): - raise NotImplementedError + # TODO TEST + def __add__(self, other: OpenGLMobject) -> OpenGLMobject: + assert isinstance(other, OpenGLMobject) + return self.get_group_class()(self, other) + + # TODO TEST + def __mul__(self, other: int) -> OpenGLMobject: + assert isinstance(other, int) + return self.replicate(other) def __iadd__(self, mobject): raise NotImplementedError @@ -206,7 +270,7 @@ def construct(self): self.add(Text("Changing default values is easy!")) # we revert the colour back to the default to prevent a bug in the docs. - Text.set_default(color=WHITE) + Tex -> Nonet.set_default(color=WHITE) """ if kwargs: @@ -214,28 +278,31 @@ def construct(self): else: cls.__init__ = cls._original__init__ - def init_data(self): + def init_data(self) -> None: """Initializes the ``points``, ``bounding_box`` and ``rgbas`` attributes and groups them into self.data. Subclasses can inherit and overwrite this method to extend `self.data`.""" + logger.debug("M init_data") + # In case parent class already has data defined + self.data: dict[str, Array] = getattr(self, "data", {}) self.points = np.zeros((0, 3)) - self.bounding_box = np.zeros((3, 3)) - self.rgbas = np.zeros((1, 4)) - - def init_colors(self): - """Initializes the colors. + self.bounding_box = np.zeros((0, 3)) + self.rgbas = np.zeros((0, 4)) - Gets called upon creation""" + def init_colors(self) -> None: + logger.debug("M init_colors") self.set_color(self.color, self.opacity) - def init_points(self): + def init_points(self) -> None: """Initializes :attr:`points` and therefore the shape. Gets called upon creation. This is an empty method that can be implemented by - subclasses.""" - # Typically implemented in subclass, unless purposefully left blank + subclasses. + + Typically implemented in a subclass, unless purposefully left empty. + """ pass - def set(self, **kwargs) -> OpenGLMobject: + def set(self, **kwargs: dict[str, Any]) -> OpenGLMobject: """Sets attributes. Mainly to be used along with :attr:`animate` to @@ -269,18 +336,20 @@ def set(self, **kwargs) -> OpenGLMobject: return self - def set_data(self, data): + def set_data(self, data: dict[str, Any]) -> OpenGLMobject: for key in data: self.data[key] = data[key].copy() return self - def set_uniforms(self, uniforms): - for key in uniforms: - self.uniforms[key] = uniforms[key] # Copy? + def set_uniforms(self, uniforms: dict[str, Any]) -> OpenGLMobject: + for key, value in uniforms.items(): + if isinstance(value, np.ndarray): + value = value.copy() + self.uniforms[key] = value return self @property - def animate(self): + def animate(self) -> _AnimationBuilder: """Used to animate the application of a method. .. warning:: @@ -368,7 +437,7 @@ def construct(self): return _AnimationBuilder(self) @property - def width(self): + def width(self) -> float: """The width of the mobject. Returns @@ -402,11 +471,11 @@ def construct(self): # Only these methods should directly affect points @width.setter - def width(self, value): + def width(self, value: float) -> None: self.rescale_to_fit(value, 0, stretch=False) @property - def height(self): + def height(self) -> float: """The height of the mobject. Returns @@ -439,11 +508,11 @@ def construct(self): return self.length_over_dim(1) @height.setter - def height(self, value): + def height(self, value: float) -> None: self.rescale_to_fit(value, 1, stretch=False) @property - def depth(self): + def depth(self) -> float: """The depth of the mobject. Returns @@ -460,71 +529,51 @@ def depth(self): return self.length_over_dim(2) @depth.setter - def depth(self, value): + def depth(self, value: float) -> None: self.rescale_to_fit(value, 2, stretch=False) - def resize_points(self, new_length, resize_func=resize_array): + """ + Point functions, only these methods should directly affect the points + """ + + def resize_points( + self, new_length: int, resize_func: Callable[[Array, int], Array] = resize_array + ) -> OpenGLMobject: if new_length != len(self.points): self.points = resize_func(self.points, new_length) self.refresh_bounding_box() return self - def set_points(self, points): + # TODO: Why exactly are we not only using the last case ? + def set_points(self, points: npt.ArrayLike) -> OpenGLMobject: + # TODO: ArrayLike doesn't have __len__ implemented if len(points) == len(self.points): self.points[:] = points elif isinstance(points, np.ndarray): self.points = points.copy() else: - self.points = np.array(points) + self.points = np.asarray(points) self.refresh_bounding_box() return self - def apply_over_attr_arrays(self, func): - for attr in self.get_array_attrs(): - setattr(self, attr, func(getattr(self, attr))) - return self - - def append_points(self, new_points): + def append_points(self, new_points: npt.ArrayLike) -> OpenGLMobject: self.points = np.vstack([self.points, new_points]) self.refresh_bounding_box() return self - def reverse_points(self): + def reverse_points(self) -> OpenGLMobject: for mob in self.get_family(): for key in mob.data: mob.data[key] = mob.data[key][::-1] return self - def get_midpoint(self) -> np.ndarray: - """Get coordinates of the middle of the path that forms the :class:`~.OpenGLMobject`. - - Examples - -------- - - .. manim:: AngleMidPoint - :save_last_frame: - - class AngleMidPoint(Scene): - def construct(self): - line1 = Line(ORIGIN, 2*RIGHT) - line2 = Line(ORIGIN, 2*RIGHT).rotate_about_origin(80*DEGREES) - - a = Angle(line1, line2, radius=1.5, other_angle=False) - d = Dot(a.get_midpoint()).set_color(RED) - - self.add(line1, line2, a, d) - self.wait() - - """ - return self.point_from_proportion(0.5) - def apply_points_function( self, - func, - about_point=None, - about_edge=ORIGIN, - works_on_bounding_box=False, - ): + func: Callable[[Array], Array], + about_point: Array | None = None, + about_edge: Array = ORIGIN, + works_on_bounding_box: bool = False, + ) -> OpenGLMobject: if about_point is None and about_edge is not None: about_point = self.get_bounding_box_point(about_edge) @@ -548,9 +597,11 @@ def apply_points_function( parent.refresh_bounding_box() return self - # Others related to points + """ + Other Functions related to points + """ - def match_points(self, mobject): + def match_points(self, mobject: OpenGLMobject) -> None: """Edit points, positions, and submobjects to be identical to another :class:`~.OpenGLMobject`, while keeping the style unchanged. @@ -569,28 +620,28 @@ def construct(self): """ self.set_points(mobject.points) - def clear_points(self): + def clear_points(self) -> None: self.resize_points(0) - def get_num_points(self): + def get_num_points(self) -> int: return len(self.points) - def get_all_points(self): + def get_all_points(self) -> Array: if self.submobjects: return np.vstack([sm.points for sm in self.get_family()]) else: return self.points - def has_points(self): + def has_points(self) -> bool: return self.get_num_points() > 0 - def get_bounding_box(self): + def get_bounding_box(self) -> Array: if self.needs_new_bounding_box: self.bounding_box = self.compute_bounding_box() self.needs_new_bounding_box = False return self.bounding_box - def compute_bounding_box(self): + def compute_bounding_box(self) -> Array: all_points = np.vstack( [ self.points, @@ -610,7 +661,9 @@ def compute_bounding_box(self): mids = (mins + maxs) / 2 return np.array([mins, mids, maxs]) - def refresh_bounding_box(self, recurse_down=False, recurse_up=True): + def refresh_bounding_box( + self, recurse_down: bool = False, recurse_up: bool = True + ) -> OpenGLMobject: for mob in self.get_family(recurse_down): mob.needs_new_bounding_box = True if recurse_up: @@ -618,13 +671,61 @@ def refresh_bounding_box(self, recurse_down=False, recurse_up=True): parent.refresh_bounding_box() return self - def is_point_touching(self, point, buff=MED_SMALL_BUFF): + def are_points_touching(self, points: Array, buff: float = 0) -> bool: bb = self.get_bounding_box() mins = bb[0] - buff maxs = bb[2] + buff - return (point >= mins).all() and (point <= maxs).all() + return ((points >= mins) * (points <= maxs)).all(1) + + def is_point_touching(self, point: Array, buff: float = MED_SMALL_BUFF): + return self.are_points_touching(np.array(point, ndmin=2), buff) + + def is_touching(self, mobject: OpenGLMobject, buff: float = 1e-2) -> bool: + bb1 = self.get_bounding_box() + bb2 = mobject.get_bounding_box() + return not any( + ( + ( + bb2[2] < bb1[0] - buff + ).any(), # E.g. Right of mobject is left of self's left + ( + bb2[0] > bb1[2] + buff + ).any(), # E.g. Left of mobject is right of self's right + ) + ) + + # TODO: fix error get_array_attrs not found + def apply_over_attr_arrays(self, func): + for attr in self.get_array_attrs(): + setattr(self, attr, func(getattr(self, attr))) + return self + + def get_midpoint(self) -> Array: + """Get coordinates of the middle of the path that forms the :class:`~.OpenGLMobject`. + + Examples + -------- - # Family matters + .. manim:: AngleMidPoint + :save_last_frame: + + class AngleMidPoint(Scene): + def construct(self): + line1 = Line(ORIGIN, 2*RIGHT) + line2 = Line(ORIGIN, 2*RIGHT).rotate_about_origin(80*DEGREES) + + a = Angle(line1, line2, radius=1.5, other_angle=False) + d = Dot(a.get_midpoint()).set_color(RED) + + self.add(line1, line2, a, d) + self.wait() + + """ + return self.point_from_proportion(0.5) + + """ + Function which operate on the family + """ def __getitem__(self, value): if isinstance(value, slice): @@ -641,7 +742,7 @@ def __len__(self): def split(self): return self.submobjects - def assemble_family(self): + def assemble_family(self) -> OpenGLMobject: sub_families = (sm.get_family() for sm in self.submobjects) self.family = [self, *uniq_chain(*sub_families)] self.refresh_has_updater_status() @@ -650,15 +751,37 @@ def assemble_family(self): parent.assemble_family() return self - def get_family(self, recurse=True): + def get_family(self, recurse: bool = True) -> list[OpenGLMobject]: if recurse and hasattr(self, "family"): return self.family else: return [self] - def family_members_with_points(self): + def family_members_with_points(self) -> list[OpenGLMobject]: return [m for m in self.get_family() if m.has_points()] + # TODO update Documentation + def get_ancestors(self, extended: bool = False) -> list[OpenGLMobject]: + """ + Returns parents, grandparents, etc. + Order of result should be from higher members of the hierarchy down. + + If extended is set to true, it includes the ancestors of all family members, + e.g. any other parents of a submobject + """ + ancestors = [] + to_process = list(self.get_family(recurse=extended)) + excluded = set(to_process) + while to_process: + for p in to_process.pop().parents: + if p not in excluded: + ancestors.append(p) + to_process.append(p) + # Ensure mobjects highest in the hierarchy show up first + ancestors.reverse() + # Remove list redundancies while preserving order + return list(dict.fromkeys(ancestors)) + def add( self, *mobjects: OpenGLMobject, update_parent: bool = False ) -> OpenGLMobject: @@ -737,7 +860,10 @@ def add( return self def remove( - self, *mobjects: OpenGLMobject, update_parent: bool = False + self, + *mobjects: OpenGLMobject, + update_parent: bool = False, + reassemble: bool = True, ) -> OpenGLMobject: """Remove :attr:`submobjects`. @@ -760,6 +886,7 @@ def remove( :meth:`add` """ + # TODO: Why exactly is there an update parent? it seems pretty random if update_parent: assert len(mobjects) == 1, "Can't remove multiple parents." mobjects[0].parent = None @@ -820,7 +947,9 @@ def add_to_back(self, *mobjects: OpenGLMobject) -> OpenGLMobject: self.submobjects = list_update(mobjects, self.submobjects) return self - def replace_submobject(self, index, new_submob): + def replace_submobject( + self, index: int, new_submob: OpenGLMobject + ) -> OpenGLMobject: old_submob = self.submobjects[index] if self in old_submob.parents: old_submob.parents.remove(self) @@ -828,36 +957,30 @@ def replace_submobject(self, index, new_submob): self.assemble_family() return self - def invert(self, recursive=False): - """Inverts the list of :attr:`submobjects`. - - Parameters - ---------- - recursive - If ``True``, all submobject lists of this mobject's family are inverted. - - Examples - -------- + def insert_submobject(self, index: int, new_submob: OpenGLMobject) -> OpenGLMobject: + self.submobjects.insert(index, new_submob) + self.assemble_family() + return self - .. manim:: InvertSumobjectsExample + def set_submobjects(self, submobject_list: list[OpenGLMobject]) -> OpenGLMobject: + self.remove(*self.submobjects, reassemble=False) + self.add(*submobject_list) + return self - class InvertSumobjectsExample(Scene): - def construct(self): - s = VGroup(*[Dot().shift(i*0.1*RIGHT) for i in range(-20,20)]) - s2 = s.copy() - s2.invert() - s2.shift(DOWN) - self.play(Write(s), Write(s2)) + def digest_mobject_attrs(self): """ - if recursive: - for submob in self.submobjects: - submob.invert(recursive=True) - list.reverse(self.submobjects) - self.assemble_family() + Ensures all attributes which are mobjects are included + in the submobjects list. + """ + mobject_attrs = [ + x for x in list(self.__dict__.values()) if isinstance(x, OpenGLMobject) + ] + self.set_submobjects(list_update(self.submobjects, mobject_attrs)) + return self # Submobject organization - def arrange(self, direction=RIGHT, center=True, **kwargs): + def arrange(self, direction: Array = RIGHT, center: bool = True, **kwargs): """Sorts :class:`~.OpenGLMobject` next to each other on screen. Examples @@ -881,6 +1004,7 @@ def construct(self): self.center() return self + # TODO: Needs test def arrange_in_grid( self, rows: int | None = None, @@ -983,9 +1107,10 @@ def construct(self): """ + # TODO: Why is there a local import here ? from manim.mobject.geometry.line import Line - mobs = self.submobjects.copy() + mobs = self.submobjects start_pos = self.get_center() # get cols / rows values if given (implicitly) @@ -1136,19 +1261,45 @@ def get_grid(self, n_rows, n_cols, height=None, **kwargs): grid.set_height(height) return grid - def duplicate(self, n: int): - """Returns an :class:`~.OpenGLVGroup` containing ``n`` copies of the mobject.""" - return self.get_group_class()(*[self.copy() for _ in range(n)]) + def _arrange_to_fit_dim(self, length: float, dim: int, about_edge: Array = ORIGIN): + ref_point = self.get_bounding_box_point(about_edge) + n_submobs = len(self.submobjects) + if n_submobs <= 1: + return + total_length = sum(sm.length_over_dim(dim) for sm in self.submobjects) + buff = (length - total_length) / (n_submobs - 1) + vect = np.zeros(self.dim) + vect[dim] = 1 + x = 0 + for submob in self.submobjects: + submob.set_coord(x, dim, -vect) + x += submob.length_over_dim(dim) + buff + self.move_to(ref_point, about_edge) + return self + + def arrange_to_fit_width(self, width: float, about_edge: Array = ORIGIN): + return self._arrange_to_fit_dim(width, 0, about_edge) - def sort(self, point_to_num_func=lambda p: p[0], submob_func=None): + def arrange_to_fit_height(self, height: float, about_edge: Array = ORIGIN): + return self._arrange_to_fit_dim(height, 1, about_edge) + + def arrange_to_fit_depth(self, depth: float, about_edge: Array = ORIGIN): + return self._arrange_to_fit_dim(depth, 2, about_edge) + + def sort( + self, + point_to_num_func: Callable[[Array], float] = lambda p: p[0], + submob_func: Callable[[OpenGLMobject], float] | None = None, + ): """Sorts the list of :attr:`submobjects` by a function defined by ``submob_func``.""" if submob_func is not None: self.submobjects.sort(key=submob_func) else: self.submobjects.sort(key=lambda m: point_to_num_func(m.get_center())) + self.assemble_family() return self - def shuffle(self, recurse=False): + def shuffle(self, recurse: bool = False): """Shuffles the order of :attr:`submobjects` Examples @@ -1171,6 +1322,10 @@ def construct(self): self.assemble_family() return self + def duplicate(self, n: int): + """Returns an :class:`~.OpenGLVGroup` containing ``n`` copies of the mobject.""" + return self.get_group_class()(*[self.copy() for _ in range(n)]) + def invert(self, recursive=False): """Inverts the list of :attr:`submobjects`. @@ -1197,8 +1352,47 @@ def construct(self): submob.invert(recursive=True) list.reverse(self.submobjects) - # Copying + # Copying and serialization + # @staticmethod + def stash_mobject_pointers(func: Callable): + @wraps(func) + def wrapper(self, *args, **kwargs): + uncopied_attrs = ["parents", "target", "saved_state"] + stash = {} + for attr in uncopied_attrs: + if hasattr(self, attr): + value = getattr(self, attr) + stash[attr] = value + null_value = [] if isinstance(value, list) else None + setattr(self, attr, null_value) + result = func(self, *args, **kwargs) + self.__dict__.update(stash) + return result + + return wrapper + + @stash_mobject_pointers + def serialize(self): + return pickle.dumps(self) + def deserialize(self, data: bytes): + self.become(pickle.loads(data)) + return self + + def deepcopy(self): + # parents = self.parents + # self.parents = [] + # result = copy.deepcopy(self) + # self.parents = parents + # return result + + try: + # Often faster than deepcopy + return pickle.loads(pickle.dumps(self)) + except AttributeError: + return copy.deepcopy(self) + + @stash_mobject_pointers def copy(self, shallow: bool = False): """Create and return an identical copy of the :class:`OpenGLMobject` including all :attr:`submobjects`. @@ -1257,13 +1451,6 @@ def copy(self, shallow: bool = False): # setattr(copy_mobject, attr, value.copy()) return copy_mobject - def deepcopy(self): - parents = self.parents - self.parents = [] - result = copy.deepcopy(self) - self.parents = parents - return result - def generate_target(self, use_deepcopy: bool = False): self.target = None # Prevent exponential explosion if use_deepcopy: @@ -1292,11 +1479,11 @@ def restore(self): # Updating - def init_updaters(self): - self.time_based_updaters = [] - self.non_time_updaters = [] - self.has_updaters = False - self.updating_suspended = False + def init_updaters(self) -> None: + self.time_based_updaters: list[TimeBasedUpdater] = [] + self.non_time_updaters: list[NonTimeUpdater] = [] + self.has_updaters: bool = False + self.updating_suspended: bool = False def update(self, dt=0, recurse=True): if not self.has_updaters or self.updating_suspended: @@ -1335,7 +1522,7 @@ def add_updater(self, update_function, index=None, call_updater=False): self.refresh_has_updater_status() if call_updater: - self.update() + self.update(dt=0) return self def remove_updater(self, update_function): @@ -1869,37 +2056,141 @@ def put_start_and_end_on(self, start, end): return self # Color functions + def set_rgba_array( + self, rgba_array: npt.ArrayLike, name: str = "rgbas", recurse: bool = False + ) -> OpenGLMobject: + """Set the rgba array of the :class:`~.OpenGLMobject` to ``rgba_array``. + + Parameters + ---------- + rgba_array + The rgba array to set. Must be of shape ``(n, 4)``. [[r, g, b, a], ...] where ``n`` should be the number of points in the :class:`~.OpenGLMobject`. + + .. warning:: + + This function should only be used internally please use :meth:`~.OpenGLMobject.set_color` instead. + + name + The name of the rgba array. Default is ``rgbas``. This is used to differ between stroke and fill rgbas in the case of a :class:`~.OpenGLVMobject`. + + recurse + Whether to set the rgba array of all submobjects. Default is ``True``. + + Returns + ------- + self + The :class:`~.OpenGLMobject` itself. - def set_rgba_array(self, color=None, opacity=None, name="rgbas", recurse=True): + Examples + -------- + + >>> m = OpenGLVMobject() + >>> m.set_rgba_array([[1, 0, 0, 1], [0, 1, 0, 1]]) + >>> m.rgbas + array([[1, 0, 0, 1], [0, 1, 0, 1]]) + """ + for mob in self.get_family(recurse): + mob.data[name] = np.asarray(rgba_array) + # mob.data.update(name, rgba_array) + return self + + def set_color_by_rgba_func( + self, + func: Callable[[Array[np.floating]], Sequence[float]], + recurse: bool = True, + ): + """Setting the rgba values by a generating function. + + Parameters + ---------- + func + The function which takes in a point and returns a rgba array of the form ``[r, g, b, a]``. + + recurse + Whether to set the rgba array of all submobjects. Default is ``True``. + + Returns + ------- + self + The :class:`~.OpenGLMobject` itself. + """ + for mob in self.get_family(recurse): + rgba_array = [func(point) for point in mob.points] + mob.set_rgba_array(rgba_array) + return self + + def set_color_by_rgb_func( + self, + func: Callable[[Array[np.floating]], Sequence[float]], + opacity: float = 1, + recurse=True, + ): + """Setting the rgba values by a generating function. (RGB ONLY see :meth:`~.set_color_by_rgba_func` for RGBA) + + Parameters + ---------- + func + The function which takes in a point and returns a rgba array of the form ``[r, g, b]``. + opacity + The opacity of the color. Default is ``1``. Becomes the alpha element of the rgba array. + recurse + Whether to set the rgba array of all submobjects. Default is ``True``. + + Returns + ------- + self + The :class:`~.OpenGLMobject` itself. + """ + for mob in self.get_family(recurse): + rgb_array = [[*func(point), opacity] for point in mob.points] + mob.set_rgba_array(rgb_array) + return self + + def set_rgba_array_by_color( + self, + color: ManimColor | Iterable[ManimColor] | None = None, + opacity: float | Iterable[float] | None = None, + name: str = "rgbas", + recurse: bool = True, + ): + """Set the rgba array of the :class:`~.OpenGLMobject` to the rgba values of the color. + + Parameters + ---------- + color + The color to set. Default is ``None``. + opacity + The opacity of the color. Default is ``None``. Becomes the alpha element of the rgba array. + name + The name of the rgba array. Default is ``rgbas``. This is used to differ between stroke and fill rgbas in the case of a :class:`~.OpenGLVMobject`. + recurse + Whether to set the rgba array of all submobjects. Default is ``True``. + + Returns + ------- + self + The :class:`~.OpenGLMobject` itself. + """ + self.color = color + max_len = 0 if color is not None: rgbs = np.array([color_to_rgb(c) for c in listify(color)]) + max_len = len(rgbs) if opacity is not None: - opacities = listify(opacity) - - # Color only - if color is not None and opacity is None: - for mob in self.get_family(recurse): - mob.data[name] = resize_array( - mob.data[name] if name in mob.data else np.empty((1, 3)), len(rgbs) - ) - mob.data[name][:, :3] = rgbs - - # Opacity only - if color is None and opacity is not None: - for mob in self.get_family(recurse): - mob.data[name] = resize_array( - mob.data[name] if name in mob.data else np.empty((1, 3)), - len(opacities), - ) - mob.data[name][:, 3] = opacities + opacities = np.array(listify(opacity)) + max_len = max(max_len, len(opacities)) - # Color and opacity - if color is not None and opacity is not None: - rgbas = np.array([[*rgb, o] for rgb, o in zip(*make_even(rgbs, opacities))]) - for mob in self.get_family(recurse): - mob.data[name] = rgbas.copy() + for mob in self.get_family(recurse): + if max_len > len(mob.data[name]): + mob.data[name] = resize_array(mob.data[name], max_len) + size = len(mob.data[name]) + if color is not None: + mob.data[name][:, :3] = resize_array(rgbs, size) + if opacity is not None: + mob.data[name][:, 3] = resize_array(opacities, size) return self + @deprecated(message="Use set_rgba_array instead") def set_rgba_array_direct(self, rgbas: np.ndarray, name="rgbas", recurse=True): """Directly set rgba data from `rgbas` and optionally do the same recursively with submobjects. This can be used if the `rgbas` have already been generated @@ -1917,21 +2208,24 @@ def set_rgba_array_direct(self, rgbas: np.ndarray, name="rgbas", recurse=True): for mob in self.get_family(recurse): mob.data[name] = rgbas.copy() - def set_color(self, color, opacity=None, recurse=True): - self.set_rgba_array(color, opacity, recurse=False) - # Recurse to submobjects differently from how set_rgba_array + def set_color( + self, + color: ManimColor | Iterable[ManimColor] | None, + opacity: float | Iterable[float] | None = None, + recurse: bool = True, + ): + self.set_rgba_array_by_color(color, opacity, recurse=False) + # Recurse to submobjects differently from how set_rgba_array_by_color # in case they implement set_color differently - if color is not None: - self.color = Color(color) - if opacity is not None: - self.opacity = opacity if recurse: for submob in self.submobjects: submob.set_color(color, recurse=True) return self - def set_opacity(self, opacity, recurse=True): - self.set_rgba_array(color=None, opacity=opacity, recurse=False) + def set_opacity( + self, opacity: float | Iterable[float] | None, recurse: bool = True + ): + self.set_rgba_array_by_color(color=None, opacity=opacity, recurse=False) if recurse: for submob in self.submobjects: submob.set_opacity(opacity, recurse=True) @@ -2146,7 +2440,7 @@ def get_start_and_end(self): """Returns starting and ending point of a stroke as a ``tuple``.""" return self.get_start(), self.get_end() - def point_from_proportion(self, alpha): + def point_from_proportion(self, alpha) -> Array: points = self.points i, subalpha = integer_interpolate(0, len(points) - 1, alpha) return interpolate(points[i], points[i + 1], subalpha) @@ -2318,7 +2612,13 @@ def add_n_more_submobjects(self, n): # Interpolate - def interpolate(self, mobject1, mobject2, alpha, path_func=straight_path()): + def interpolate( + self, + mobject1: OpenGLMobject, + mobject2: OpenGLMobject, + alpha: float, + path_func: Callable[[Array, Array, float], Array] = straight_path, + ): """Turns this :class:`~.OpenGLMobject` into an interpolation between ``mobject1`` and ``mobject2``. @@ -2351,23 +2651,14 @@ def construct(self): func = path_func else: func = interpolate - + # print(key) + # print(func(mobject1.data[key], mobject2.data[key], alpha)) + # print(self.data[key]) self.data[key][:] = func(mobject1.data[key], mobject2.data[key], alpha) for key in self.uniforms: - if key != "fixed_orientation_center": - self.uniforms[key] = interpolate( - mobject1.uniforms[key], - mobject2.uniforms[key], - alpha, - ) - else: - self.uniforms["fixed_orientation_center"] = tuple( - interpolate( - np.array(mobject1.uniforms["fixed_orientation_center"]), - np.array(mobject2.uniforms["fixed_orientation_center"]), - alpha, - ) - ) + self.uniforms[key] = interpolate( + mobject1.uniforms[key], mobject2.uniforms[key], alpha + ) return self def pointwise_become_partial(self, mobject, a, b): @@ -2379,6 +2670,10 @@ def pointwise_become_partial(self, mobject, a, b): """ pass # To implement in subclass + def replicate(self, n: int) -> OpenGLGroup: + group_class = self.get_group_class() + return group_class(*(self.copy() for _ in range(n))) + def become( self, mobject: OpenGLMobject, @@ -2502,7 +2797,7 @@ def fix_in_frame(self): @affects_shader_info_id def fix_orientation(self): self.is_fixed_orientation = 1.0 - self.fixed_orientation_center = tuple(self.get_center()) + self.fixed_orientation_center = np.asarray(self.get_center()) self.depth_test = True return self @@ -2514,7 +2809,7 @@ def unfix_from_frame(self): @affects_shader_info_id def unfix_orientation(self): self.is_fixed_orientation = 0.0 - self.fixed_orientation_center = (0, 0, 0) + self.fixed_orientation_center = np.asarray((0, 0, 0)) self.depth_test = False return self @@ -2576,9 +2871,9 @@ def set_color_by_xyz_func( # For shader data - # def refresh_shader_wrapper_id(self): - # self.shader_wrapper.refresh_id() - # return self + def refresh_shader_wrapper_id(self): + self.shader_wrapper.refresh_id() + return self def get_shader_wrapper(self): from manim.renderer.shader_wrapper import ShaderWrapper @@ -2617,11 +2912,11 @@ def check_data_alignment(self, array, data_key): # or the length of the array d_len = len(self.data[data_key]) if d_len != 1 and d_len != len(array): - self.data[data_key] = resize_with_interpolation( + return resize_with_interpolation( self.data[data_key], len(array), ) - return self + return self.data[data_key] def get_resized_shader_data_array(self, length): # If possible, try to populate an existing array, rather @@ -2633,8 +2928,8 @@ def get_resized_shader_data_array(self, length): def read_data_to_shader(self, shader_data, shader_data_key, data_key): if data_key in self.locked_data_keys: return - self.check_data_alignment(shader_data, data_key) - shader_data[shader_data_key] = self.data[data_key] + data = self.check_data_alignment(shader_data, data_key) + shader_data[shader_data_key] = data def get_shader_data(self): shader_data = self.get_resized_shader_data_array(self.get_num_points()) @@ -2703,8 +2998,9 @@ def set_location(self, new_loc): self.set_points(np.array(new_loc, ndmin=2, dtype=float)) +# TODO ADD TYPES class _AnimationBuilder: - def __init__(self, mobject): + def __init__(self, mobject: OpenGLMobject): self.mobject = mobject self.mobject.generate_target() diff --git a/manim/mobject/opengl/opengl_vectorized_mobject.py b/manim/mobject/opengl/opengl_vectorized_mobject.py index c866eb2bf2..eccc99f21d 100644 --- a/manim/mobject/opengl/opengl_vectorized_mobject.py +++ b/manim/mobject/opengl/opengl_vectorized_mobject.py @@ -3,13 +3,11 @@ import itertools as it import operator as op from functools import reduce, wraps -from typing import Callable, Iterable, Optional, Sequence import moderngl import numpy as np -from colour import Color -from manim import config +from manim import config, logger from manim.constants import * from manim.mobject.opengl.opengl_mobject import OpenGLMobject, OpenGLPoint from manim.renderer.shader_wrapper import ShaderWrapper @@ -41,6 +39,17 @@ "miter": 3, } +from typing import TYPE_CHECKING + +from colour import Color + +if TYPE_CHECKING: + from typing import Callable, Iterable, List, Optional, Sequence, Tuple, Union + + import numpy.typing as npt + + ManimColor = Union[str, Color] + class OpenGLVMobject(OpenGLMobject): """A vectorized mobject.""" @@ -95,10 +104,9 @@ def __init__( triangulation_locked: bool = False, **kwargs, ): + logger.debug("VM __init__") self.data = {} self.fill_opacity = fill_opacity - self.stroke_opacity = stroke_opacity - self.stroke_width = stroke_width self.draw_stroke_behind_fill = draw_stroke_behind_fill # Indicates that it will not be displayed, but # that it should count in parent mobject's path @@ -125,6 +133,8 @@ def __init__( self.needs_new_triangulation = True self.triangulation = np.zeros(0, dtype="i4") self.orientation = 1 + self._stroke_opacity = stroke_opacity + self._stroke_width = stroke_width super().__init__(**kwargs) self.refresh_unit_normal() @@ -143,33 +153,45 @@ def get_group_class(self): return OpenGLVGroup def init_data(self): + logger.debug("VM init_data") super().init_data() self.data.pop("rgbas") self.fill_rgba = np.zeros((1, 4)) self.stroke_rgba = np.zeros((1, 4)) + self.stroke_width = np.zeros((1, 1)) self.unit_normal = np.zeros((1, 3)) # stroke_width belongs to self.data, but is defined through init_colors+set_stroke # Colors def init_colors(self): + logger.debug("VM init_colors") + super().init_colors() self.set_fill( color=self.fill_color or self.color, opacity=self.fill_opacity, ) self.set_stroke( color=self.stroke_color or self.color, - width=self.stroke_width, - opacity=self.stroke_opacity, + width=self._stroke_width, + opacity=self._stroke_opacity, background=self.draw_stroke_behind_fill, ) self.set_gloss(self.gloss) self.set_flat_stroke(self.flat_stroke) return self + def set_rgba_array( + self, rgba_array: npt.ArrayLike, name: str | None = None, recurse: bool = False + ): + names = ["fill_rgba", "stroke_rgba"] if name is None else [name] + for name in names: + super().set_rgba_array(rgba_array, name, recurse) + return self + def set_fill( self, - color: Color | None = None, - opacity: float | None = None, + color: ManimColor | Iterable[ManimColor] | None = None, + opacity: float | Iterable[float] | None = None, recurse: bool = True, ) -> OpenGLVMobject: """Set the fill color and fill opacity of a :class:`OpenGLVMobject`. @@ -207,46 +229,38 @@ def construct(self): -------- :meth:`~.OpenGLVMobject.set_style` """ - if opacity is not None: - self.fill_opacity = opacity - if recurse: - for submobject in self.submobjects: - submobject.set_fill(color, opacity, recurse) - - self.set_rgba_array(color, opacity, "fill_rgba", recurse) + self.set_rgba_array_by_color(color, opacity, "fill_rgba", recurse) return self def set_stroke( self, - color=None, - width=None, - opacity=None, - background=None, - recurse=True, + color: ManimColor | Iterable[ManimColor] | None = None, + width: float | Iterable[float] | None = None, + opacity: float | Iterable[float] | None = None, + background: bool | None = None, + recurse: bool = True, ): - if opacity is not None: - self.stroke_opacity = opacity - if recurse: - for submobject in self.submobjects: - submobject.set_stroke( - color=color, - width=width, - opacity=opacity, - background=background, - recurse=recurse, - ) - - self.set_rgba_array(color, opacity, "stroke_rgba", recurse) + self.set_rgba_array_by_color(color, opacity, "stroke_rgba", recurse) if width is not None: for mob in self.get_family(recurse): - mob.stroke_width = np.array([[width] for width in listify(width)]) + if isinstance(width, np.ndarray): + arr = width.reshape((width.shape[0], 1)) + else: + arr = np.array([[w] for w in listify(width)]) + mob.stroke_width = arr if background is not None: for mob in self.get_family(recurse): mob.draw_stroke_behind_fill = background return self + def align_stroke_width_data_to_points(self, recurse: bool = True): + for mob in self.get_family(recurse): + mob.stroke_width = resize_with_interpolation( + mob.stroke_width, len(mob.points) + ) + def set_style( self, fill_color=None, @@ -308,88 +322,96 @@ def match_style(self, vmobject, recurse=True): sm1.match_style(sm2) return self - def set_color(self, color, opacity=None, recurse=True): - if opacity is not None: - self.opacity = opacity - + def set_color( + self, + color: ManimColor | Iterable[ManimColor] | None, + opacity: float | Iterable[float] | None = None, + recurse: bool = True, + ): self.set_fill(color, opacity=opacity, recurse=recurse) self.set_stroke(color, opacity=opacity, recurse=recurse) return self - def set_opacity(self, opacity, recurse=True): + def set_opacity( + self, opacity: float | Iterable[float] | None, recurse: bool = True + ): self.set_fill(opacity=opacity, recurse=recurse) self.set_stroke(opacity=opacity, recurse=recurse) return self - def fade(self, darkness=0.5, recurse=True): - factor = 1.0 - darkness - self.set_fill( - opacity=factor * self.get_fill_opacity(), - recurse=False, - ) - self.set_stroke( - opacity=factor * self.get_stroke_opacity(), - recurse=False, - ) - super().fade(darkness, recurse) + def fade(self, darkness: float = 0.5, recurse: bool = True): + mobs = self.get_family() if recurse else [self] + for mob in mobs: + factor = 1.0 - darkness + mob.set_fill( + opacity=factor * mob.get_fill_opacity(), + recurse=False, + ) + mob.set_stroke( + opacity=factor * mob.get_stroke_opacity(), + recurse=False, + ) return self - def get_fill_colors(self): - return [Color(rgb_to_hex(rgba[:3])) for rgba in self.fill_rgba] + def get_fill_colors(self) -> list[str]: + return [rgb_to_hex(rgba[:3]) for rgba in self.fill_rgba] - def get_fill_opacities(self): + def get_fill_opacities(self) -> npt.NDArray[np.floating]: return self.fill_rgba[:, 3] - def get_stroke_colors(self): - return [Color(rgb_to_hex(rgba[:3])) for rgba in self.stroke_rgba] + def get_stroke_colors(self) -> list[str]: + return [rgb_to_hex(rgba[:3]) for rgba in self.stroke_rgba] - def get_stroke_opacities(self): + def get_stroke_opacities(self) -> npt.NDArray[np.floating]: return self.stroke_rgba[:, 3] - def get_stroke_widths(self): - return self.stroke_width + def get_stroke_widths(self) -> npt.NDArray[np.floating]: + return self.stroke_width[:, 0] # TODO, it's weird for these to return the first of various lists # rather than the full information - def get_fill_color(self): + def get_fill_color(self) -> str: """ If there are multiple colors (for gradient) this returns the first one """ return self.get_fill_colors()[0] - def get_fill_opacity(self): + def get_fill_opacity(self) -> float: """ If there are multiple opacities, this returns the first """ return self.get_fill_opacities()[0] - def get_stroke_color(self): + def get_stroke_color(self) -> str: return self.get_stroke_colors()[0] - def get_stroke_width(self): + def get_stroke_width(self) -> float | npt.NDArray: return self.get_stroke_widths()[0] - def get_stroke_opacity(self): + def get_stroke_opacity(self) -> float: return self.get_stroke_opacities()[0] - def get_color(self): - if self.has_stroke(): - return self.get_stroke_color() - return self.get_fill_color() + def set_stroke_opacity(self, opacity) -> None: + return self.set_stroke(opacity=opacity) + + def get_color(self) -> str: + if self.has_fill(): + return self.get_fill_color() + return self.get_stroke_color() - def get_colors(self): - if self.has_stroke(): - return self.get_stroke_colors() - return self.get_fill_colors() + def get_colors(self) -> list[str]: + if self.has_fill(): + return self.get_fill_colors() + return self.get_stroke_colors() stroke_color = property(get_stroke_color, set_stroke) - color = property(get_color, set_color) fill_color = property(get_fill_color, set_fill) + stroke_opacity = property(get_stroke_opacity, set_stroke_opacity) def has_stroke(self): - return any(self.get_stroke_widths()) and any(self.get_stroke_opacities()) + return self.get_stroke_widths().any() and self.get_stroke_opacities().any() def has_fill(self): return any(self.get_fill_opacities()) diff --git a/manim/mobject/svg/svg_mobject.py b/manim/mobject/svg/svg_mobject.py index b5ebd856d7..46bd29257b 100644 --- a/manim/mobject/svg/svg_mobject.py +++ b/manim/mobject/svg/svg_mobject.py @@ -609,6 +609,7 @@ def _move_into_position(self, width, height): def init_colors(self, propagate_colors=False): if config.renderer == "opengl": + super().init_colors() self.set_style( fill_color=self.fill_color or self.color, fill_opacity=self.fill_opacity, diff --git a/manim/renderer/shader.py b/manim/renderer/shader.py index ce40a701ff..d5ff4c3350 100644 --- a/manim/renderer/shader.py +++ b/manim/renderer/shader.py @@ -385,7 +385,10 @@ def __init__( def set_uniform(self, name, value): try: - self.shader_program[name] = value + if isinstance(value, np.ndarray): + self.shader_program[name] = tuple(value) + else: + self.shader_program[name] = value except KeyError: pass diff --git a/manim/utils/color.py b/manim/utils/color.py index 6356fb3f04..7e401fe40a 100644 --- a/manim/utils/color.py +++ b/manim/utils/color.py @@ -473,7 +473,7 @@ def rgba_to_color(rgba: Iterable[float]) -> Color: def rgb_to_hex(rgb: Iterable[float]) -> str: - return "#" + "".join("%02x" % round(255 * x) for x in rgb) + return str.upper("#" + "".join("%02x" % round(255 * x) for x in rgb)) def hex_to_rgb(hex_code: str) -> np.ndarray: diff --git a/poetry.lock b/poetry.lock index ea4f7c2e71..0da11b4800 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1425,7 +1425,7 @@ python-versions = "*" [[package]] name = "pillow" -version = "9.1.0" +version = "9.1.1" description = "Python Imaging Library (Fork)" category = "main" optional = false @@ -3389,44 +3389,44 @@ pickleshare = [ {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, ] pillow = [ - {file = "Pillow-9.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea"}, - {file = "Pillow-9.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652"}, - {file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66822d01e82506a19407d1afc104c3fcea3b81d5eb11485e593ad6b8492f995a"}, - {file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5eaf3b42df2bcda61c53a742ee2c6e63f777d0e085bbc6b2ab7ed57deb13db7"}, - {file = "Pillow-9.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01ce45deec9df310cbbee11104bae1a2a43308dd9c317f99235b6d3080ddd66e"}, - {file = "Pillow-9.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:aea7ce61328e15943d7b9eaca87e81f7c62ff90f669116f857262e9da4057ba3"}, - {file = "Pillow-9.1.0-cp310-cp310-win32.whl", hash = "sha256:7a053bd4d65a3294b153bdd7724dce864a1d548416a5ef61f6d03bf149205160"}, - {file = "Pillow-9.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:97bda660702a856c2c9e12ec26fc6d187631ddfd896ff685814ab21ef0597033"}, - {file = "Pillow-9.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:21dee8466b42912335151d24c1665fcf44dc2ee47e021d233a40c3ca5adae59c"}, - {file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b6d4050b208c8ff886fd3db6690bf04f9a48749d78b41b7a5bf24c236ab0165"}, - {file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5cfca31ab4c13552a0f354c87fbd7f162a4fafd25e6b521bba93a57fe6a3700a"}, - {file = "Pillow-9.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed742214068efa95e9844c2d9129e209ed63f61baa4d54dbf4cf8b5e2d30ccf2"}, - {file = "Pillow-9.1.0-cp37-cp37m-win32.whl", hash = "sha256:c9efef876c21788366ea1f50ecb39d5d6f65febe25ad1d4c0b8dff98843ac244"}, - {file = "Pillow-9.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:de344bcf6e2463bb25179d74d6e7989e375f906bcec8cb86edb8b12acbc7dfef"}, - {file = "Pillow-9.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:17869489de2fce6c36690a0c721bd3db176194af5f39249c1ac56d0bb0fcc512"}, - {file = "Pillow-9.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:25023a6209a4d7c42154073144608c9a71d3512b648a2f5d4465182cb93d3477"}, - {file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8782189c796eff29dbb37dd87afa4ad4d40fc90b2742704f94812851b725964b"}, - {file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:463acf531f5d0925ca55904fa668bb3461c3ef6bc779e1d6d8a488092bdee378"}, - {file = "Pillow-9.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f42364485bfdab19c1373b5cd62f7c5ab7cc052e19644862ec8f15bb8af289e"}, - {file = "Pillow-9.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3fddcdb619ba04491e8f771636583a7cc5a5051cd193ff1aa1ee8616d2a692c5"}, - {file = "Pillow-9.1.0-cp38-cp38-win32.whl", hash = "sha256:4fe29a070de394e449fd88ebe1624d1e2d7ddeed4c12e0b31624561b58948d9a"}, - {file = "Pillow-9.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:c24f718f9dd73bb2b31a6201e6db5ea4a61fdd1d1c200f43ee585fc6dcd21b34"}, - {file = "Pillow-9.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717"}, - {file = "Pillow-9.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c870193cce4b76713a2b29be5d8327c8ccbe0d4a49bc22968aa1e680930f5581"}, - {file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69e5ddc609230d4408277af135c5b5c8fe7a54b2bdb8ad7c5100b86b3aab04c6"}, - {file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35be4a9f65441d9982240e6966c1eaa1c654c4e5e931eaf580130409e31804d4"}, - {file = "Pillow-9.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82283af99c1c3a5ba1da44c67296d5aad19f11c535b551a5ae55328a317ce331"}, - {file = "Pillow-9.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a325ac71914c5c043fa50441b36606e64a10cd262de12f7a179620f579752ff8"}, - {file = "Pillow-9.1.0-cp39-cp39-win32.whl", hash = "sha256:a598d8830f6ef5501002ae85c7dbfcd9c27cc4efc02a1989369303ba85573e58"}, - {file = "Pillow-9.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c51cb9edac8a5abd069fd0758ac0a8bfe52c261ee0e330f363548aca6893595"}, - {file = "Pillow-9.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a336a4f74baf67e26f3acc4d61c913e378e931817cd1e2ef4dfb79d3e051b481"}, - {file = "Pillow-9.1.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb1b89b11256b5b6cad5e7593f9061ac4624f7651f7a8eb4dfa37caa1dfaa4d0"}, - {file = "Pillow-9.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:255c9d69754a4c90b0ee484967fc8818c7ff8311c6dddcc43a4340e10cd1636a"}, - {file = "Pillow-9.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5a3ecc026ea0e14d0ad7cd990ea7f48bfcb3eb4271034657dc9d06933c6629a7"}, - {file = "Pillow-9.1.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5b0ff59785d93b3437c3703e3c64c178aabada51dea2a7f2c5eccf1bcf565a3"}, - {file = "Pillow-9.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7110ec1701b0bf8df569a7592a196c9d07c764a0a74f65471ea56816f10e2c8"}, - {file = "Pillow-9.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458"}, - {file = "Pillow-9.1.0.tar.gz", hash = "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97"}, + {file = "Pillow-9.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:42dfefbef90eb67c10c45a73a9bc1599d4dac920f7dfcbf4ec6b80cb620757fe"}, + {file = "Pillow-9.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffde4c6fabb52891d81606411cbfaf77756e3b561b566efd270b3ed3791fde4e"}, + {file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c857532c719fb30fafabd2371ce9b7031812ff3889d75273827633bca0c4602"}, + {file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59789a7d06c742e9d13b883d5e3569188c16acb02eeed2510fd3bfdbc1bd1530"}, + {file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d45dbe4b21a9679c3e8b3f7f4f42a45a7d3ddff8a4a16109dff0e1da30a35b2"}, + {file = "Pillow-9.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e9ed59d1b6ee837f4515b9584f3d26cf0388b742a11ecdae0d9237a94505d03a"}, + {file = "Pillow-9.1.1-cp310-cp310-win32.whl", hash = "sha256:b3fe2ff1e1715d4475d7e2c3e8dabd7c025f4410f79513b4ff2de3d51ce0fa9c"}, + {file = "Pillow-9.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5b650dbbc0969a4e226d98a0b440c2f07a850896aed9266b6fedc0f7e7834108"}, + {file = "Pillow-9.1.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:0b4d5ad2cd3a1f0d1df882d926b37dbb2ab6c823ae21d041b46910c8f8cd844b"}, + {file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9370d6744d379f2de5d7fa95cdbd3a4d92f0b0ef29609b4b1687f16bc197063d"}, + {file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b761727ed7d593e49671d1827044b942dd2f4caae6e51bab144d4accf8244a84"}, + {file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a66fe50386162df2da701b3722781cbe90ce043e7d53c1fd6bd801bca6b48d4"}, + {file = "Pillow-9.1.1-cp37-cp37m-win32.whl", hash = "sha256:2b291cab8a888658d72b575a03e340509b6b050b62db1f5539dd5cd18fd50578"}, + {file = "Pillow-9.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1d4331aeb12f6b3791911a6da82de72257a99ad99726ed6b63f481c0184b6fb9"}, + {file = "Pillow-9.1.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8844217cdf66eabe39567118f229e275f0727e9195635a15e0e4b9227458daaf"}, + {file = "Pillow-9.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b6617221ff08fbd3b7a811950b5c3f9367f6e941b86259843eab77c8e3d2b56b"}, + {file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20d514c989fa28e73a5adbddd7a171afa5824710d0ab06d4e1234195d2a2e546"}, + {file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:088df396b047477dd1bbc7de6e22f58400dae2f21310d9e2ec2933b2ef7dfa4f"}, + {file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53c27bd452e0f1bc4bfed07ceb235663a1df7c74df08e37fd6b03eb89454946a"}, + {file = "Pillow-9.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3f6c1716c473ebd1649663bf3b42702d0d53e27af8b64642be0dd3598c761fb1"}, + {file = "Pillow-9.1.1-cp38-cp38-win32.whl", hash = "sha256:c67db410508b9de9c4694c57ed754b65a460e4812126e87f5052ecf23a011a54"}, + {file = "Pillow-9.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:f054b020c4d7e9786ae0404278ea318768eb123403b18453e28e47cdb7a0a4bf"}, + {file = "Pillow-9.1.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:c17770a62a71718a74b7548098a74cd6880be16bcfff5f937f900ead90ca8e92"}, + {file = "Pillow-9.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3f6a6034140e9e17e9abc175fc7a266a6e63652028e157750bd98e804a8ed9a"}, + {file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f372d0f08eff1475ef426344efe42493f71f377ec52237bf153c5713de987251"}, + {file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09e67ef6e430f90caa093528bd758b0616f8165e57ed8d8ce014ae32df6a831d"}, + {file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66daa16952d5bf0c9d5389c5e9df562922a59bd16d77e2a276e575d32e38afd1"}, + {file = "Pillow-9.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d78ca526a559fb84faaaf84da2dd4addef5edb109db8b81677c0bb1aad342601"}, + {file = "Pillow-9.1.1-cp39-cp39-win32.whl", hash = "sha256:55e74faf8359ddda43fee01bffbc5bd99d96ea508d8a08c527099e84eb708f45"}, + {file = "Pillow-9.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:7c150dbbb4a94ea4825d1e5f2c5501af7141ea95825fadd7829f9b11c97aaf6c"}, + {file = "Pillow-9.1.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:769a7f131a2f43752455cc72f9f7a093c3ff3856bf976c5fb53a59d0ccc704f6"}, + {file = "Pillow-9.1.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:488f3383cf5159907d48d32957ac6f9ea85ccdcc296c14eca1a4e396ecc32098"}, + {file = "Pillow-9.1.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b525a356680022b0af53385944026d3486fc8c013638cf9900eb87c866afb4c"}, + {file = "Pillow-9.1.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6e760cf01259a1c0a50f3c845f9cad1af30577fd8b670339b1659c6d0e7a41dd"}, + {file = "Pillow-9.1.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4165205a13b16a29e1ac57efeee6be2dfd5b5408122d59ef2145bc3239fa340"}, + {file = "Pillow-9.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937a54e5694684f74dcbf6e24cc453bfc5b33940216ddd8f4cd8f0f79167f765"}, + {file = "Pillow-9.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:baf3be0b9446a4083cc0c5bb9f9c964034be5374b5bc09757be89f5d2fa247b8"}, + {file = "Pillow-9.1.1.tar.gz", hash = "sha256:7502539939b53d7565f3d11d87c78e7ec900d3c72945d4ee0e2f250d598309a0"}, ] platformdirs = [ {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, diff --git a/tests/opengl/test_color_opengl.py b/tests/opengl/test_color_opengl.py index e3f6940ac4..5309e8e8ad 100644 --- a/tests/opengl/test_color_opengl.py +++ b/tests/opengl/test_color_opengl.py @@ -16,153 +16,153 @@ def test_import_color(using_opengl_renderer): def test_background_color(using_opengl_renderer): S = Scene() - S.renderer.background_color = "#ff0000" + S.renderer.background_color = "#FF0000" S.renderer.update_frame(S) assert np.all(S.renderer.get_frame()[0, 0] == np.array([255, 0, 0, 255])) - S.renderer.background_color = "#436f80" + S.renderer.background_color = "#436F80" S.renderer.update_frame(S) assert np.all(S.renderer.get_frame()[0, 0] == np.array([67, 111, 128, 255])) - S.renderer.background_color = "#fff" + S.renderer.background_color = "#FFFFFF" S.renderer.update_frame(S) assert np.all(S.renderer.get_frame()[0, 0] == np.array([255, 255, 255, 255])) def test_set_color(using_opengl_renderer): m = OpenGLMobject() - assert m.color.hex == "#fff" + assert m.color == "#FFFFFF" np.alltrue(m.rgbas == np.array((0.0, 0.0, 0.0, 1.0))) m.set_color(BLACK) - assert m.color.hex == "#000" + assert m.color == "#000000" np.alltrue(m.rgbas == np.array((1.0, 1.0, 1.0, 1.0))) m.set_color(PURE_GREEN, opacity=0.5) - assert m.color.hex == "#0f0" + assert m.color == "#00FF00" np.alltrue(m.rgbas == np.array((0.0, 1.0, 0.0, 0.5))) m = OpenGLVMobject() - assert m.color.hex == "#fff" + assert m.color == "#FFFFFF" np.alltrue(m.fill_rgba == np.array((0.0, 0.0, 0.0, 1.0))) np.alltrue(m.stroke_rgba == np.array((0.0, 0.0, 0.0, 1.0))) m.set_color(BLACK) - assert m.color.hex == "#000" + assert m.color == "#000000" np.alltrue(m.fill_rgba == np.array((1.0, 1.0, 1.0, 1.0))) np.alltrue(m.stroke_rgba == np.array((1.0, 1.0, 1.0, 1.0))) m.set_color(PURE_GREEN, opacity=0.5) - assert m.color.hex == "#0f0" + assert m.color == "#00FF00" np.alltrue(m.fill_rgba == np.array((0.0, 1.0, 0.0, 0.5))) np.alltrue(m.stroke_rgba == np.array((0.0, 1.0, 0.0, 0.5))) def test_set_fill_color(using_opengl_renderer): m = OpenGLVMobject() - assert m.fill_color.hex == "#fff" + assert m.fill_color == "#FFFFFF" np.alltrue(m.fill_rgba == np.array((0.0, 1.0, 0.0, 0.5))) m.set_fill(BLACK) - assert m.fill_color.hex == "#000" + assert m.fill_color == "#000000" np.alltrue(m.fill_rgba == np.array((1.0, 1.0, 1.0, 1.0))) m.set_fill(PURE_GREEN, opacity=0.5) - assert m.fill_color.hex == "#0f0" + assert m.fill_color == "#00FF00" np.alltrue(m.fill_rgba == np.array((0.0, 1.0, 0.0, 0.5))) def test_set_stroke_color(using_opengl_renderer): m = OpenGLVMobject() - assert m.stroke_color.hex == "#fff" + assert m.stroke_color == "#FFFFFF" np.alltrue(m.stroke_rgba == np.array((0.0, 1.0, 0.0, 0.5))) m.set_stroke(BLACK) - assert m.stroke_color.hex == "#000" + assert m.stroke_color == "#000000" np.alltrue(m.stroke_rgba == np.array((1.0, 1.0, 1.0, 1.0))) m.set_stroke(PURE_GREEN, opacity=0.5) - assert m.stroke_color.hex == "#0f0" + assert m.stroke_color == "#00FF00" np.alltrue(m.stroke_rgba == np.array((0.0, 1.0, 0.0, 0.5))) def test_set_fill(using_opengl_renderer): m = OpenGLMobject() - assert m.color.hex == "#fff" + assert m.color == "#FFFFFF" m.set_color(BLACK) - assert m.color.hex == "#000" + assert m.color == "#000000" m = OpenGLVMobject() - assert m.color.hex == "#fff" + assert m.color == "#FFFFFF" m.set_color(BLACK) - assert m.color.hex == "#000" + assert m.color == "#000000" def test_set_color_handles_lists_of_strs(using_opengl_renderer): m = OpenGLVMobject() - assert m.color.hex == "#fff" + assert m.color == "#FFFFFF" m.set_color([BLACK, BLUE, GREEN]) - assert m.get_colors()[0] == Color(BLACK) - assert m.get_colors()[1] == Color(BLUE) - assert m.get_colors()[2] == Color(GREEN) + assert m.get_colors()[0] == BLACK + assert m.get_colors()[1] == BLUE + assert m.get_colors()[2] == GREEN - assert m.get_fill_colors()[0] == Color(BLACK) - assert m.get_fill_colors()[1] == Color(BLUE) - assert m.get_fill_colors()[2] == Color(GREEN) + assert m.get_fill_colors()[0] == BLACK + assert m.get_fill_colors()[1] == BLUE + assert m.get_fill_colors()[2] == GREEN - assert m.get_stroke_colors()[0] == Color(BLACK) - assert m.get_stroke_colors()[1] == Color(BLUE) - assert m.get_stroke_colors()[2] == Color(GREEN) + assert m.get_stroke_colors()[0] == BLACK + assert m.get_stroke_colors()[1] == BLUE + assert m.get_stroke_colors()[2] == GREEN def test_set_color_handles_lists_of_color_objects(using_opengl_renderer): m = OpenGLVMobject() - assert m.color.hex == "#fff" + assert m.color == "#FFFFFF" m.set_color([Color(PURE_BLUE), Color(PURE_GREEN), Color(PURE_RED)]) - assert m.get_colors()[0].hex == "#00f" - assert m.get_colors()[1].hex == "#0f0" - assert m.get_colors()[2].hex == "#f00" + assert m.get_colors()[0] == "#0000FF" + assert m.get_colors()[1] == "#00FF00" + assert m.get_colors()[2] == "#FF0000" - assert m.get_fill_colors()[0].hex == "#00f" - assert m.get_fill_colors()[1].hex == "#0f0" - assert m.get_fill_colors()[2].hex == "#f00" + assert m.get_fill_colors()[0] == "#0000FF" + assert m.get_fill_colors()[1] == "#00FF00" + assert m.get_fill_colors()[2] == "#FF0000" - assert m.get_stroke_colors()[0].hex == "#00f" - assert m.get_stroke_colors()[1].hex == "#0f0" - assert m.get_stroke_colors()[2].hex == "#f00" + assert m.get_stroke_colors()[0] == "#0000FF" + assert m.get_stroke_colors()[1] == "#00FF00" + assert m.get_stroke_colors()[2] == "#FF0000" def test_set_fill_handles_lists_of_strs(using_opengl_renderer): m = OpenGLVMobject() - assert m.fill_color.hex == "#fff" + assert m.fill_color == "#FFFFFF" m.set_fill([BLACK, BLUE, GREEN]) - assert m.get_fill_colors()[0] == Color(BLACK) - assert m.get_fill_colors()[1] == Color(BLUE) - assert m.get_fill_colors()[2] == Color(GREEN) + assert m.get_fill_colors()[0] == BLACK + assert m.get_fill_colors()[1] == BLUE + assert m.get_fill_colors()[2] == GREEN def test_set_fill_handles_lists_of_color_objects(using_opengl_renderer): m = OpenGLVMobject() - assert m.fill_color.hex == "#fff" + assert m.fill_color == "#FFFFFF" m.set_fill([Color(PURE_BLUE), Color(PURE_GREEN), Color(PURE_RED)]) - assert m.get_fill_colors()[0].hex == "#00f" - assert m.get_fill_colors()[1].hex == "#0f0" - assert m.get_fill_colors()[2].hex == "#f00" + assert m.get_fill_colors()[0] == "#0000FF" + assert m.get_fill_colors()[1] == "#00FF00" + assert m.get_fill_colors()[2] == "#FF0000" def test_set_stroke_handles_lists_of_strs(using_opengl_renderer): m = OpenGLVMobject() - assert m.stroke_color.hex == "#fff" + assert m.stroke_color == "#FFFFFF" m.set_stroke([BLACK, BLUE, GREEN]) - assert m.get_stroke_colors()[0] == Color(BLACK) - assert m.get_stroke_colors()[1] == Color(BLUE) - assert m.get_stroke_colors()[2] == Color(GREEN) + assert m.get_stroke_colors()[0] == BLACK + assert m.get_stroke_colors()[1] == BLUE + assert m.get_stroke_colors()[2] == GREEN def test_set_stroke_handles_lists_of_color_objects(using_opengl_renderer): m = OpenGLVMobject() - assert m.stroke_color.hex == "#fff" + assert m.stroke_color == "#FFFFFF" m.set_stroke([Color(PURE_BLUE), Color(PURE_GREEN), Color(PURE_RED)]) - assert m.get_stroke_colors()[0].hex == "#00f" - assert m.get_stroke_colors()[1].hex == "#0f0" - assert m.get_stroke_colors()[2].hex == "#f00" + assert m.get_stroke_colors()[0] == "#0000FF" + assert m.get_stroke_colors()[1] == "#00FF00" + assert m.get_stroke_colors()[2] == "#FF0000" diff --git a/tests/opengl/test_opengl_mobject.py b/tests/opengl/test_opengl_mobject.py index f1a58fa6a3..36d2635434 100644 --- a/tests/opengl/test_opengl_mobject.py +++ b/tests/opengl/test_opengl_mobject.py @@ -1,5 +1,7 @@ from __future__ import annotations +import numpy as np +import numpy.testing as nt import pytest from manim.mobject.opengl.opengl_mobject import OpenGLMobject @@ -49,3 +51,57 @@ def test_opengl_mobject_remove(using_opengl_renderer): assert len(obj.submobjects) == 10 assert obj.remove(OpenGLMobject()) is obj + + +def test_opengl_mobject_arrange_in_grid(using_opengl_renderer): + """Test OpenGLMobject.arrange_in_grid().""" + from manim import Rectangle, VGroup + + boxes = VGroup(*[Rectangle(height=0.5, width=0.5) for _ in range(24)]) + + boxes.arrange_in_grid( + buff=(0.25, 0.5), + col_alignments="lccccr", + row_alignments="uccd", + col_widths=[1, *[None] * 4, 1], + row_heights=[1, None, None, 1], + flow_order="dr", + ) + + solution = np.array( + [ + [-2.375, 2.0, 0.0], + [-2.375, 0.5, 0.0], + [-2.375, -0.5, 0.0], + [-2.375, -2.0, 0.0], + [-1.125, 2.0, 0.0], + [-1.125, 0.5, 0.0], + [-1.125, -0.5, 0.0], + [-1.125, -2.0, 0.0], + [-0.375, 2.0, 0.0], + [-0.375, 0.5, 0.0], + [-0.375, -0.5, 0.0], + [-0.375, -2.0, 0.0], + [0.375, 2.0, 0.0], + [0.375, 0.5, 0.0], + [0.375, -0.5, 0.0], + [0.375, -2.0, 0.0], + [1.125, 2.0, 0.0], + [1.125, 0.5, 0.0], + [1.125, -0.5, 0.0], + [1.125, -2.0, 0.0], + [2.375, 2.0, 0.0], + [2.375, 0.5, 0.0], + [2.375, -0.5, 0.0], + [2.375, -2.0, 0.0], + ] + ) + + for expected, rect in zip(solution, boxes): + c = rect.get_center() + nt.assert_array_equal(expected, c) + + +# for rect in boxes: +# c = rect.get_center() +# print(f"{c!s}") diff --git a/tests/opengl/test_stroke_opengl.py b/tests/opengl/test_stroke_opengl.py index 7a18d5de27..9da11633c0 100644 --- a/tests/opengl/test_stroke_opengl.py +++ b/tests/opengl/test_stroke_opengl.py @@ -1,5 +1,6 @@ from __future__ import annotations +import numpy.testing as nt from colour import Color import manim.utils.color as C @@ -8,7 +9,7 @@ def test_stroke_props_in_ctor(using_opengl_renderer): m = OpenGLVMobject(stroke_color=C.ORANGE, stroke_width=10) - assert m.stroke_color == Color(C.ORANGE) + assert m.stroke_color == C.ORANGE assert m.stroke_width == 10 @@ -17,4 +18,15 @@ def test_set_stroke(using_opengl_renderer): m.set_stroke(color=C.ORANGE, width=2, opacity=0.8) assert m.stroke_width == 2 assert m.stroke_opacity == 0.8 - assert m.stroke_color == Color(C.ORANGE) + assert m.stroke_color == C.ORANGE + + +# def test_set_stroke_list(using_opengl_renderer): +# m = OpenGLVMobject() +# m.set_stroke([C.RED, C.ORANGE], [1, 2, 3], [0, 0.5, 1]) +# assert m.stroke_width == 1 +# assert m.stroke_opacity == 0 +# assert m.stroke_color == C.RED +# m.stroke_opacity = [0.1, 0.2, 0.3] +# assert m.stroke_opacity == 0.1 +# nt.assert_array_equal(m.get_stroke_opacities(), [0.1, 0.2, 0.3]) diff --git a/tests/opengl/test_svg_mobject_opengl.py b/tests/opengl/test_svg_mobject_opengl.py index c672055778..d0e6f87a62 100644 --- a/tests/opengl/test_svg_mobject_opengl.py +++ b/tests/opengl/test_svg_mobject_opengl.py @@ -9,21 +9,21 @@ def test_set_fill_color(using_opengl_renderer): expected_color = "#FF862F" svg = SVGMobject(get_svg_resource("heart.svg"), fill_color=expected_color) - assert svg.fill_color == Color(expected_color) + assert svg.fill_color == expected_color def test_set_stroke_color(using_opengl_renderer): expected_color = "#FFFDDD" svg = SVGMobject(get_svg_resource("heart.svg"), stroke_color=expected_color) - assert svg.stroke_color == Color(expected_color) + assert svg.stroke_color == expected_color def test_set_color_sets_fill_and_stroke(using_opengl_renderer): expected_color = "#EEE777" svg = SVGMobject(get_svg_resource("heart.svg"), color=expected_color) - assert svg.color == Color(expected_color) - assert svg.fill_color == Color(expected_color) - assert svg.stroke_color == Color(expected_color) + assert svg.color == expected_color + assert svg.fill_color == expected_color + assert svg.stroke_color == expected_color def test_set_fill_opacity(using_opengl_renderer): @@ -45,7 +45,7 @@ def test_fill_overrides_color(using_opengl_renderer): color="#123123", fill_color=expected_color, ) - assert svg.fill_color == Color(expected_color) + assert svg.fill_color == expected_color def test_stroke_overrides_color(using_opengl_renderer): @@ -55,4 +55,4 @@ def test_stroke_overrides_color(using_opengl_renderer): color="#334433", stroke_color=expected_color, ) - assert svg.stroke_color == Color(expected_color) + assert svg.stroke_color == expected_color